from __future__ import annotations

from sqlalchemy import func, or_, select
from sqlalchemy.orm import selectinload

from app.api.deps import CurrentUserContext
from app.models.employee import Employee
from app.models.financial_record import ExpenseClaim


class ExpenseClaimEmployeeResolverMixin:
    """集中处理报销单与员工身份的租户内解析和回填。"""

    def resolve_structured_claim_tenant_id(self, claim: ExpenseClaim) -> str:
        tenant_id = str(claim.tenant_id or "").strip()
        if tenant_id:
            return self.normalize_tenant_id(tenant_id)
        return self.resolve_claim_tenant_id(self.db, claim.id)

    def resolve_current_employee(self, current_user: CurrentUserContext) -> Employee | None:
        return self.resolve_employee_by_identity_candidates(
            [
                str(current_user.employee_id or "").strip(),
                str(current_user.username or "").strip(),
                str(current_user.name or "").strip(),
                str(current_user.employee_no or "").strip(),
            ],
            tenant_id=current_user.tenant_id,
        )

    def resolve_current_user_display_name(self, current_user: CurrentUserContext) -> str:
        current_employee = self.resolve_current_employee(current_user)
        if current_employee is not None and str(current_employee.name or "").strip():
            return str(current_employee.name).strip()

        for candidate in (current_user.name, current_user.username):
            normalized = str(candidate or "").strip()
            if normalized and not self.is_email_like(normalized):
                return normalized

        return str(current_user.username or current_user.name or "anonymous").strip() or "anonymous"

    def resolve_claim_employee_for_backfill(self, claim: ExpenseClaim) -> Employee | None:
        tenant_id = self.resolve_structured_claim_tenant_id(claim)
        if claim.employee is not None:
            employee = self.db.scalar(
                select(Employee)
                .options(
                    selectinload(Employee.organization_unit),
                    selectinload(Employee.manager),
                    selectinload(Employee.roles),
                )
                .where(
                    Employee.tenant_id == tenant_id,
                    Employee.id == claim.employee.id,
                )
                .limit(1)
            )
            if employee is not None:
                return employee
            if self.normalize_tenant_id(claim.employee.tenant_id) == tenant_id:
                return claim.employee
            return None

        employee_id = str(claim.employee_id or "").strip()
        if employee_id:
            employee = self.db.scalar(
                select(Employee)
                .options(
                    selectinload(Employee.organization_unit),
                    selectinload(Employee.manager),
                    selectinload(Employee.roles),
                )
                .where(
                    Employee.tenant_id == tenant_id,
                    Employee.id == employee_id,
                )
                .limit(1)
            )
            if employee is not None:
                return employee

        return self.resolve_employee_by_identity_candidates(
            [str(claim.employee_name or "").strip()],
            tenant_id=tenant_id,
        )

    def resolve_employee_by_identity_candidates(
        self,
        candidates: list[str],
        *,
        tenant_id: str,
    ) -> Employee | None:
        trusted_tenant_id = self.normalize_tenant_id(tenant_id)
        normalized_candidates = [
            item
            for item in dict.fromkeys(str(candidate or "").strip() for candidate in candidates)
            if item
        ]
        if not normalized_candidates:
            return None

        load_options = (
            selectinload(Employee.organization_unit),
            selectinload(Employee.manager),
            selectinload(Employee.roles),
        )

        for candidate in normalized_candidates:
            employee = self.db.scalar(
                select(Employee)
                .options(*load_options)
                .where(
                    Employee.tenant_id == trusted_tenant_id,
                    or_(
                        Employee.id == candidate,
                        func.lower(Employee.email) == candidate.lower(),
                        func.lower(Employee.employee_no) == candidate.lower(),
                    ),
                )
                .limit(1)
            )
            if employee is not None:
                return employee

        for candidate in normalized_candidates:
            if self.is_email_like(candidate):
                continue
            matches = list(
                self.db.scalars(
                    select(Employee)
                    .options(*load_options)
                    .where(
                        Employee.tenant_id == trusted_tenant_id,
                        func.lower(Employee.email).like(f"{candidate.lower()}@%"),
                    )
                    .limit(2)
                ).all()
            )
            if len(matches) == 1:
                return matches[0]

        for candidate in normalized_candidates:
            matches = list(
                self.db.scalars(
                    select(Employee)
                    .options(*load_options)
                    .where(
                        Employee.tenant_id == trusted_tenant_id,
                        Employee.name == candidate,
                    )
                    .limit(2)
                ).all()
            )
            if len(matches) == 1:
                return matches[0]

        return None

    def backfill_claim_identity_from_current_user(
        self,
        claim: ExpenseClaim,
        current_user: CurrentUserContext,
    ) -> None:
        employee = self.resolve_claim_employee_for_backfill(claim) or self.resolve_current_employee(
            current_user
        )

        if employee is not None:
            claim_employee_id = str(claim.employee_id or "").strip()
            claim_employee_name = str(claim.employee_name or "").strip()
            employee_names = {
                str(employee.name or "").strip(),
                str(employee.email or "").strip(),
                str(employee.employee_no or "").strip(),
            }
            employee_names.discard("")

            can_apply_employee = (
                not claim_employee_id
                or claim_employee_id == employee.id
                or self.is_missing_value(claim_employee_name)
                or claim_employee_name in employee_names
            )

            if can_apply_employee:
                claim.employee = employee
                claim.employee_id = employee.id
                if employee.name:
                    claim.employee_name = employee.name
                if employee.organization_unit is not None:
                    claim.department_id = employee.organization_unit_id
                    claim.department_name = employee.organization_unit.name
                return

        context_department = str(
            getattr(current_user, "department_name", "")
            or getattr(current_user, "department", "")
            or getattr(current_user, "departmentName", "")
            or ""
        ).strip()
        if context_department and self.is_missing_value(claim.department_name):
            claim.department_name = context_department

        context_name = str(current_user.name or current_user.username or "").strip()
        if context_name and self.is_missing_value(claim.employee_name):
            claim.employee_name = context_name

    def employee_name_is_unique(self, employee: Employee) -> bool:
        normalized_name = str(employee.name or "").strip()
        if not normalized_name:
            return False

        same_name_count = int(
            self.db.scalar(
                select(func.count())
                .select_from(Employee)
                .where(Employee.name == normalized_name)
                .where(Employee.tenant_id == self.normalize_tenant_id(employee.tenant_id))
            )
            or 0
        )
        return same_name_count == 1
