from __future__ import annotations

from datetime import UTC, datetime, timedelta

from sqlalchemy import and_, func, or_, 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.schemas.approval_task import (
    ApprovalTaskListRead,
    ApprovalTaskQueueItemRead,
)
from app.schemas.reimbursement import ExpenseClaimRead
from app.services.approval_task_access_policy import ApprovalTaskAccessPolicy
from app.services.approval_task_protocol import task_read
from app.services.expense_claim_access_policy import ExpenseClaimAccessPolicy
from app.services.expense_claim_tenant_scope import ExpenseClaimTenantScopeMixin


class ApprovalTaskQueryService:
    """服务端分页的个人审批队列；GET 路径严格只读。"""

    def __init__(self, db: Session) -> None:
        self.db = db
        self.task_access = ApprovalTaskAccessPolicy(db)
        self.claim_access = ExpenseClaimAccessPolicy(db)

    def list_tasks(
        self,
        current_user: CurrentUserContext,
        *,
        page: int = 1,
        page_size: int = 20,
        status: str = "pending",
        risk_level: str | None = None,
        sla_state: str | None = None,
        node_key: str | None = None,
        task_kind: str | None = None,
        batch_eligible: bool | None = None,
        assignee: str | None = None,
        sort: str = "priority_desc",
        keyword: str | None = None,
    ) -> ApprovalTaskListRead:
        page = max(int(page), 1)
        page_size = min(max(int(page_size), 1), 200)
        tenant = ExpenseClaimTenantScopeMixin.normalize_tenant_id(current_user.tenant_id)
        now = datetime.now(UTC)
        conditions = [
            ApprovalTask.tenant_id == tenant,
            ExpenseClaimTenantScopeMixin.build_claim_tenant_condition(tenant),
            self._status_condition(status),
        ]
        visibility = self._visibility_condition(current_user)
        if visibility is not None:
            conditions.append(visibility)
        if risk_level:
            conditions.append(ApprovalTask.risk_level == str(risk_level).strip().lower())
        if node_key:
            conditions.append(ApprovalTask.node_key == str(node_key).strip().lower())
        if task_kind:
            normalized_kind = str(task_kind).strip().lower()
            if normalized_kind not in {"root", "add_sign", "countersign"}:
                raise ValueError("不支持的审批任务类型筛选。")
            conditions.append(ApprovalTask.task_kind == normalized_kind)
        if batch_eligible is not None:
            conditions.append(ApprovalTask.batch_eligible.is_(bool(batch_eligible)))
        normalized_assignee = str(assignee or "").strip()
        if normalized_assignee:
            assignee_pattern = f"%{normalized_assignee}%"
            conditions.append(
                or_(
                    ApprovalTask.assignee_name.ilike(assignee_pattern),
                    ApprovalTask.assignee_key.ilike(assignee_pattern),
                )
            )
        normalized_sla_state = str(sla_state or "").strip().lower()
        if normalized_sla_state == "overdue":
            conditions.append(ApprovalTask.due_at < now)
        elif normalized_sla_state == "due_soon":
            conditions.extend(
                (
                    ApprovalTask.due_at >= now,
                    ApprovalTask.due_at <= now + timedelta(hours=4),
                )
            )
        elif normalized_sla_state == "on_track":
            conditions.append(ApprovalTask.due_at > now + timedelta(hours=4))
        elif normalized_sla_state == "escalated":
            conditions.append(ApprovalTask.escalation_level > 0)
        elif normalized_sla_state:
            raise ValueError("不支持的审批任务 SLA 状态筛选。")
        normalized_keyword = str(keyword or "").strip()
        if normalized_keyword:
            pattern = f"%{normalized_keyword}%"
            conditions.append(
                or_(
                    ExpenseClaim.claim_no.ilike(pattern),
                    ExpenseClaim.employee_name.ilike(pattern),
                    ExpenseClaim.reason.ilike(pattern),
                    ApprovalTask.assignee_name.ilike(pattern),
                    ApprovalTask.owner_name.ilike(pattern),
                )
            )

        total = int(
            self.db.scalar(
                select(func.count())
                .select_from(ApprovalTask)
                .join(ExpenseClaim, ExpenseClaim.id == ApprovalTask.claim_id)
                .where(*conditions)
            )
            or 0
        )
        statement = (
            select(ApprovalTask, ExpenseClaim)
            .join(ExpenseClaim, ExpenseClaim.id == ApprovalTask.claim_id)
            .options(
                selectinload(ExpenseClaim.items),
                selectinload(ExpenseClaim.employee).selectinload(Employee.manager),
                selectinload(ExpenseClaim.employee).selectinload(Employee.roles),
                selectinload(ExpenseClaim.employee).selectinload(Employee.organization_unit),
            )
            .where(*conditions)
            .order_by(*self._ordering(sort))
            .offset((page - 1) * page_size)
            .limit(page_size)
        )
        items: list[ApprovalTaskQueueItemRead] = []
        for task, claim in self.db.execute(statement).unique().all():
            permission = self.task_access.evaluate(task, current_user, claim=claim)
            if not permission.can_read:  # SQL 与领域策略双重校验。
                continue
            self.claim_access.attach_approval_snapshot(claim)
            items.append(
                ApprovalTaskQueueItemRead(
                    task=task_read(task, permission=permission),
                    claim=ExpenseClaimRead.model_validate(claim),
                )
            )
        return ApprovalTaskListRead(
            items=items,
            total=total,
            page=page,
            page_size=page_size,
            total_pages=(total + page_size - 1) // page_size if total else 0,
            generated_at=now,
        )

    def get_task(
        self,
        task_id: str,
        current_user: CurrentUserContext,
    ) -> ApprovalTaskQueueItemRead | None:
        tenant = ExpenseClaimTenantScopeMixin.normalize_tenant_id(current_user.tenant_id)
        row = self.db.execute(
            select(ApprovalTask, ExpenseClaim)
            .join(ExpenseClaim, ExpenseClaim.id == ApprovalTask.claim_id)
            .options(
                selectinload(ExpenseClaim.items),
                selectinload(ExpenseClaim.employee).selectinload(Employee.manager),
                selectinload(ExpenseClaim.employee).selectinload(Employee.roles),
                selectinload(ExpenseClaim.employee).selectinload(Employee.organization_unit),
            )
            .where(
                ApprovalTask.tenant_id == tenant,
                ApprovalTask.id == str(task_id).strip(),
                ExpenseClaimTenantScopeMixin.build_claim_tenant_condition(tenant),
            )
        ).unique().one_or_none()
        if row is None:
            return None
        task, claim = row
        permission = self.task_access.evaluate(task, current_user, claim=claim)
        if not permission.can_read:
            return None
        self.claim_access.attach_approval_snapshot(claim)
        return ApprovalTaskQueueItemRead(
            task=task_read(task, permission=permission),
            claim=ExpenseClaimRead.model_validate(claim),
        )

    def _visibility_condition(self, current_user: CurrentUserContext):
        if current_user.is_admin:
            return None
        employee = self.claim_access.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(),
            str(current_user.name or "").strip().casefold(),
        }
        employee_ids: set[str] = set()
        if employee is not None:
            identities.update(
                {
                    str(employee.id or "").strip().casefold(),
                    str(employee.employee_no or "").strip().casefold(),
                    str(employee.email or "").strip().casefold(),
                    str(employee.name or "").strip().casefold(),
                }
            )
            employee_ids.add(employee.id)
        if current_user.employee_id:
            employee_ids.add(str(current_user.employee_id).strip())
        identities.discard("")
        role_codes = ExpenseClaimAccessPolicy.normalize_role_codes(current_user)
        employee_match = or_(
            and_(
                ApprovalTask.assignee_kind == "employee",
                or_(
                    func.lower(ApprovalTask.assignee_key).in_(identities),
                    ApprovalTask.assignee_employee_id.in_(employee_ids),
                ),
            ),
            and_(
                ApprovalTask.owner_kind == "employee",
                or_(
                    func.lower(ApprovalTask.owner_key).in_(identities),
                    ApprovalTask.owner_employee_id.in_(employee_ids),
                ),
            ),
        )
        role_match = or_(
            and_(
                ApprovalTask.assignee_kind == "role",
                func.lower(ApprovalTask.assignee_key).in_(role_codes),
            ),
            and_(
                ApprovalTask.owner_kind == "role",
                func.lower(ApprovalTask.owner_key).in_(role_codes),
            ),
        )
        return or_(employee_match, role_match)

    @staticmethod
    def _status_condition(value: str):
        normalized = str(value or "pending").strip().lower()
        if normalized in {"open", "pending"}:
            return ApprovalTask.status.in_(("waiting", "pending"))
        if normalized == "processed":
            return ApprovalTask.status.in_(("completed", "returned", "cancelled", "superseded"))
        if normalized in {
            "waiting",
            "completed",
            "returned",
            "cancelled",
            "superseded",
        }:
            return ApprovalTask.status == normalized
        raise ValueError("不支持的审批任务状态筛选。")

    @staticmethod
    def _ordering(value: str):
        normalized = str(value or "priority_desc").strip().lower()
        if normalized == "priority_desc":
            return (
                ApprovalTask.priority_score.desc(),
                ApprovalTask.due_at.asc(),
                ApprovalTask.entered_at.asc(),
                ApprovalTask.id.asc(),
            )
        if normalized == "due_asc":
            return (
                ApprovalTask.due_at.asc(),
                ApprovalTask.priority_score.desc(),
                ApprovalTask.entered_at.asc(),
                ApprovalTask.id.asc(),
            )
        if normalized == "entered_desc":
            return (
                ApprovalTask.entered_at.desc(),
                ApprovalTask.priority_score.desc(),
                ApprovalTask.id.asc(),
            )
        raise ValueError("不支持的审批任务排序方式。")
