from __future__ import annotations

from dataclasses import dataclass
from datetime import UTC, datetime

from sqlalchemy import select
from sqlalchemy.orm import Session, selectinload

from app.api.deps import CurrentUserContext
from app.models.approval_task import ApprovalTask
from app.models.employee import Employee
from app.models.financial_record import ExpenseClaim
from app.services.expense_claim_access_policy import ExpenseClaimAccessPolicy
from app.services.expense_claim_tenant_scope import ExpenseClaimTenantScopeMixin

_OPEN_STATUSES = {"pending", "waiting"}
_ACTIVE_EMPLOYMENT_STATUSES = {"", "active", "employed", "在职", "正常"}


class ApprovalTaskPermissionError(ValueError):
    pass


@dataclass(frozen=True, slots=True)
class ApprovalTaskPermission:
    can_read: bool
    can_act: bool
    available_actions: tuple[str, ...]
    read_only_reason: str = ""


class ApprovalTaskAccessPolicy:
    """让列表可见性、任务管理和最终动作共用同一授权判断。"""

    def __init__(self, db: Session) -> None:
        self.db = db
        self.claim_policy = ExpenseClaimAccessPolicy(db)

    def evaluate(
        self,
        task: ApprovalTask,
        current_user: CurrentUserContext,
        *,
        claim: ExpenseClaim | None = None,
    ) -> ApprovalTaskPermission:
        if task.tenant_id != ExpenseClaimTenantScopeMixin.normalize_tenant_id(
            current_user.tenant_id
        ):
            return ApprovalTaskPermission(False, False, (), "任务不在当前租户范围内。")

        assigned = self._matches_assignment(task, current_user, owner=False)
        owner = self._matches_assignment(task, current_user, owner=True)
        admin = bool(current_user.is_admin)
        can_read = admin or assigned or owner
        if not can_read:
            return ApprovalTaskPermission(False, False, (), "当前用户不在该任务参与范围内。")

        available: list[str] = []
        read_only_reason = ""
        can_act = False
        if task.status == "waiting":
            read_only_reason = "当前任务正在等待加签或会签完成。"
        elif task.status != "pending":
            read_only_reason = "该任务已经处理，当前仅可查看审计记录。"
        elif assigned:
            if claim is not None and self.claim_policy.is_claim_owned_by_current_user(
                claim,
                current_user,
            ):
                read_only_reason = "申请人不能审批自己的费用单。"
            else:
                can_act = True
                available.append("approve")
                available.append("return")
                if task.task_kind == "root":
                    available.extend(("delegate", "transfer", "add_sign", "countersign"))
        elif owner and task.delegated_by:
            available.append("delegation_revoke")
            read_only_reason = "任务已委托，原责任人只能撤销委托。"
        elif admin:
            available.extend(("transfer", "sla_escalate"))
            read_only_reason = "管理员可转交或升级任务，但不能凭查看权限直接审批。"
        else:
            read_only_reason = "当前用户不是任务的有效处理人。"

        if admin and "sla_escalate" not in available and task.status in _OPEN_STATUSES:
            available.append("sla_escalate")
        return ApprovalTaskPermission(
            can_read=True,
            can_act=can_act,
            available_actions=tuple(dict.fromkeys(available)),
            read_only_reason=read_only_reason,
        )

    def require_read(
        self,
        task: ApprovalTask,
        current_user: CurrentUserContext,
        *,
        claim: ExpenseClaim | None = None,
    ) -> ApprovalTaskPermission:
        permission = self.evaluate(task, current_user, claim=claim)
        if not permission.can_read:
            raise LookupError("Approval task not found.")
        return permission

    def require_action(
        self,
        task: ApprovalTask,
        current_user: CurrentUserContext,
        *,
        action: str,
        claim: ExpenseClaim,
    ) -> ApprovalTaskPermission:
        permission = self.require_read(task, current_user, claim=claim)
        if action not in permission.available_actions:
            raise ApprovalTaskPermissionError(
                permission.read_only_reason or "当前用户无权执行该审批任务动作。"
            )
        if action in {"approve", "return"} and not permission.can_act:
            raise ApprovalTaskPermissionError(
                permission.read_only_reason or "当前用户不是任务处理人。"
            )
        return permission

    def resolve_and_validate_target(
        self,
        *,
        task: ApprovalTask,
        claim: ExpenseClaim,
        target_employee_id: str,
    ) -> Employee:
        target = self.db.scalar(
            select(Employee)
            .options(
                selectinload(Employee.roles),
                selectinload(Employee.organization_unit),
            )
            .where(Employee.id == str(target_employee_id or "").strip())
        )
        if target is None:
            raise ValueError("目标审批人不存在。")
        employment_status = str(target.employment_status or "").strip().lower()
        if employment_status not in _ACTIVE_EMPLOYMENT_STATUSES:
            raise ValueError("目标审批人不是在职员工。")
        if str(claim.employee_id or "").strip() == target.id:
            raise ValueError("申请人不能成为自己的审批人。")
        if not self._target_qualified_for_node(target, task=task, claim=claim):
            raise ValueError("目标员工不具备当前审批节点的处理资格。")
        return target

    def candidate_employees(
        self,
        *,
        task: ApprovalTask,
        claim: ExpenseClaim,
    ) -> list[tuple[Employee, bool, str]]:
        employees = list(
            self.db.scalars(
                select(Employee)
                .options(
                    selectinload(Employee.roles),
                    selectinload(Employee.organization_unit),
                )
                .order_by(Employee.name.asc(), Employee.employee_no.asc())
            ).all()
        )
        rows: list[tuple[Employee, bool, str]] = []
        for employee in employees:
            if str(claim.employee_id or "").strip() == employee.id:
                rows.append((employee, False, "申请人不能审批自己的费用单。"))
                continue
            status = str(employee.employment_status or "").strip().lower()
            if status not in _ACTIVE_EMPLOYMENT_STATUSES:
                rows.append((employee, False, "员工当前不在职。"))
                continue
            qualified = self._target_qualified_for_node(employee, task=task, claim=claim)
            rows.append((employee, qualified, "" if qualified else "不具备当前节点资格。"))
        return rows

    def actor_id(self, current_user: CurrentUserContext) -> str:
        employee = self.claim_policy.resolve_current_employee(current_user)
        return str(
            (employee.id if employee is not None else "")
            or current_user.employee_id
            or current_user.username
        ).strip()

    def _matches_assignment(
        self,
        task: ApprovalTask,
        current_user: CurrentUserContext,
        *,
        owner: bool,
    ) -> bool:
        if (
            not owner
            and task.delegated_by
            and task.delegation_expires_at is not None
            and _as_utc(task.delegation_expires_at) <= datetime.now(UTC)
        ):
            return False
        kind = str(task.owner_kind if owner else task.assignee_kind).strip().lower()
        key = str(task.owner_key if owner else task.assignee_key).strip().casefold()
        employee_id = str(
            (task.owner_employee_id if owner else task.assignee_employee_id) or ""
        ).strip()
        if kind == "role":
            return key in self.claim_policy.normalize_role_codes(current_user)

        current_employee = self.claim_policy.resolve_current_employee(current_user)
        identities = {
            str(current_user.username or "").strip().casefold(),
            str(current_user.employee_id or "").strip().casefold(),
            str(current_user.employee_no or "").strip().casefold(),
        }
        if current_employee is not None:
            identities.update(
                {
                    str(current_employee.id or "").strip().casefold(),
                    str(current_employee.name or "").strip().casefold(),
                    str(current_employee.employee_no or "").strip().casefold(),
                    str(current_employee.email or "").strip().casefold(),
                }
            )
        identities.discard("")
        return key in identities or bool(employee_id and employee_id.casefold() in identities)

    def _target_qualified_for_node(
        self,
        target: Employee,
        *,
        task: ApprovalTask,
        claim: ExpenseClaim,
    ) -> bool:
        role_codes = {
            str(role.role_code or "").strip().lower()
            for role in list(target.roles or [])
            if str(role.role_code or "").strip()
        }
        if task.node_key == "finance":
            return "finance" in role_codes
        if task.node_key == "budget_manager":
            same_department = bool(
                claim.department_id
                and target.organization_unit_id
                and str(claim.department_id) == str(target.organization_unit_id)
            ) or (
                bool(claim.department_name and target.organization_unit)
                and str(claim.department_name).strip().casefold()
                == str(target.organization_unit.name or "").strip().casefold()
            )
            grade = str(target.grade or "").strip().upper()
            grade_level = int(grade[1:]) if grade.startswith("P") and grade[1:].isdigit() else 0
            return same_department and grade_level >= 8 and bool(
                role_codes & {"budget_monitor", "executive"}
            )
        if claim.employee is not None and claim.employee.manager_id == target.id:
            return True
        return bool(role_codes & {"manager", "approver", "executive"})


def _as_utc(value: datetime) -> datetime:
    if value.tzinfo is None:
        return value.replace(tzinfo=UTC)
    return value.astimezone(UTC)
