from __future__ import annotations

import uuid
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta

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

from app.api.deps import CurrentUserContext
from app.models.approval_action import ApprovalActionLedger
from app.models.approval_task import ApprovalTask
from app.models.employee import Employee
from app.models.expense_case import BusinessEvent, ExpenseCaseLink
from app.models.financial_record import ExpenseClaim
from app.schemas.approval_task import ApprovalTaskMutationRead
from app.services.approval_task_access_policy import ApprovalTaskAccessPolicy
from app.services.approval_task_protocol import ApprovalTaskEventProtocol, task_state
from app.services.expense_claim_tenant_scope import ExpenseClaimTenantScopeMixin
from app.services.expense_claim_workflow_constants import (
    BUDGET_MANAGER_APPROVAL_STAGE,
    DIRECT_MANAGER_APPROVAL_STAGE,
    FINANCE_APPROVAL_STAGE,
)

OPEN_TASK_STATUSES = ("waiting", "pending")


class ApprovalTaskConfigurationError(ValueError):
    pass


@dataclass(frozen=True, slots=True)
class ApprovalNode:
    key: str
    label: str
    sla_hours: int


@dataclass(frozen=True, slots=True)
class ApprovalAssignment:
    kind: str
    key: str
    employee_id: str | None
    name: str


@dataclass(slots=True)
class ApprovalTaskDecisionContext:
    task: ApprovalTask
    expected_task_version: int
    before_json: dict[str, object]


_NODES = {
    DIRECT_MANAGER_APPROVAL_STAGE: ApprovalNode(
        key="direct_manager",
        label=DIRECT_MANAGER_APPROVAL_STAGE,
        sla_hours=24,
    ),
    BUDGET_MANAGER_APPROVAL_STAGE: ApprovalNode(
        key="budget_manager",
        label=BUDGET_MANAGER_APPROVAL_STAGE,
        sla_hours=12,
    ),
    FINANCE_APPROVAL_STAGE: ApprovalNode(
        key="finance",
        label=FINANCE_APPROVAL_STAGE,
        sla_hours=8,
    ),
}


def approval_node_for_stage(stage: object) -> ApprovalNode | None:
    return _NODES.get(str(stage or "").strip())


class ApprovalTaskLifecycleService:
    """把既有费用状态机投影成可分配、可审计的个人审批任务。"""

    def __init__(self, db: Session) -> None:
        self.db = db
        self.access = ApprovalTaskAccessPolicy(db)
        self.events = ApprovalTaskEventProtocol(db)

    def ensure_root_task(
        self,
        claim: ExpenseClaim,
        *,
        tenant_id: str,
        entered_at: datetime,
        entered_at_source: str,
        business_event: BusinessEvent | None = None,
        actor_id: str = "system",
        actor_name: str = "系统",
        request_id: str | None = None,
    ) -> ApprovalTask | None:
        node = approval_node_for_stage(claim.approval_stage)
        if str(claim.status or "").strip().lower() != "submitted" or node is None:
            return None
        tenant = ExpenseClaimTenantScopeMixin.normalize_tenant_id(tenant_id)
        existing = self.db.scalar(
            select(ApprovalTask).where(
                ApprovalTask.tenant_id == tenant,
                ApprovalTask.claim_id == claim.id,
                ApprovalTask.task_kind == "root",
                ApprovalTask.status.in_(OPEN_TASK_STATUSES),
            )
        )
        if existing is not None:
            if (
                existing.node_key == node.key
                and existing.claim_stage_snapshot == str(claim.approval_stage or "").strip()
            ):
                return existing
            self._supersede_stale_root(existing, claim=claim)

        assignment = self.resolve_assignment(claim, node=node)
        sequence = (
            int(
                self.db.scalar(
                    select(func.max(ApprovalTask.node_sequence)).where(
                        ApprovalTask.tenant_id == tenant,
                        ApprovalTask.claim_id == claim.id,
                        ApprovalTask.task_kind == "root",
                    )
                )
                or 0
            )
            + 1
        )
        now = datetime.now(UTC)
        normalized_entered_at = _aware_utc(entered_at)
        task_id = str(uuid.uuid4())
        entry_seed = (
            business_event.id
            if business_event is not None
            else f"{normalized_entered_at.isoformat()}:{uuid.uuid4().hex[:12]}"
        )
        task = ApprovalTask(
            id=task_id,
            tenant_id=tenant,
            claim_id=claim.id,
            expense_case_id=self._expense_case_id(claim, business_event=business_event),
            node_instance_id=f"{claim.id}:{sequence}:{node.key}:{uuid.uuid4().hex[:12]}",
            node_entry_key=f"{claim.id}:{sequence}:{node.key}:{entry_seed}",
            parent_task_id=None,
            task_kind="root",
            node_key=node.key,
            node_label=node.label,
            node_sequence=sequence,
            sequence_order=0,
            coordination_mode="single",
            owner_kind=assignment.kind,
            owner_key=assignment.key,
            owner_employee_id=assignment.employee_id,
            owner_name=assignment.name,
            assignee_kind=assignment.kind,
            assignee_key=assignment.key,
            assignee_employee_id=assignment.employee_id,
            assignee_name=assignment.name,
            delegated_by=None,
            delegation_expires_at=None,
            status="pending",
            decision=None,
            opinion=None,
            version=1,
            claim_status_snapshot=str(claim.status or "").strip().lower(),
            claim_stage_snapshot=str(claim.approval_stage or "").strip(),
            entered_at=normalized_entered_at,
            entered_at_source=entered_at_source,
            activated_at=normalized_entered_at,
            sla_hours_snapshot=node.sla_hours,
            due_at=normalized_entered_at + timedelta(hours=node.sla_hours),
            completed_at=None,
            cancelled_at=None,
            escalation_level=0,
            escalated_at=None,
            next_escalation_at=normalized_entered_at + timedelta(hours=node.sla_hours),
            priority_score=0,
            priority_tier="normal",
            priority_reasons_json=[],
            risk_level="low",
            open_risk_count=0,
            evidence_completeness=0,
            batch_eligible=False,
            batch_block_reasons_json=[],
            projection_updated_at=now,
            created_at=now,
            updated_at=now,
        )
        self._apply_projection(task, claim=claim, now=now)
        self.db.add(task)
        self.db.flush()
        event_request_id = str(request_id or "").strip() or f"node-enter:{task.id}"
        self.events.append_event(
            task=task,
            event_type="node_entered",
            actor_id=actor_id,
            actor_name=actor_name,
            actor_type="system" if actor_id == "system" else "user",
            request_id=event_request_id,
            expected_task_version=0,
            payload={
                "claim_id": claim.id,
                "node_key": node.key,
                "node_label": node.label,
                "entered_at_source": entered_at_source,
            },
            before_json={},
            business_event_id=business_event.id if business_event is not None else None,
            correlation_id=(business_event.correlation_id if business_event is not None else None),
            occurred_at=now,
        )
        return task

    def begin_claim_decision(
        self,
        claim: ExpenseClaim,
        current_user: CurrentUserContext,
        *,
        action: str,
        task_id: str | None,
        expected_task_version: int | None,
    ) -> ApprovalTaskDecisionContext:
        tenant = ExpenseClaimTenantScopeMixin.normalize_tenant_id(current_user.tenant_id)
        statement = select(ApprovalTask).where(
            ApprovalTask.tenant_id == tenant,
            ApprovalTask.claim_id == claim.id,
            ApprovalTask.status.in_(OPEN_TASK_STATUSES),
        )
        if task_id:
            statement = statement.where(ApprovalTask.id == str(task_id).strip())
        else:
            statement = statement.where(ApprovalTask.task_kind == "root")
        bind = self.db.get_bind()
        if bind is not None and bind.dialect.name == "postgresql":
            statement = statement.with_for_update()
        task = self.db.scalar(statement.execution_options(populate_existing=True))
        if task is None and not task_id:
            task = self.ensure_root_task(
                claim,
                tenant_id=tenant,
                entered_at=claim.submitted_at or claim.updated_at or datetime.now(UTC),
                entered_at_source=("submitted_at" if claim.submitted_at else "backfill"),
                actor_id="system",
                actor_name="系统兼容桥",
            )
        if task is None:
            raise ApprovalTaskConflictError("当前单据没有可执行的审批任务。")
        if task.task_kind != "root" and action != "return":
            raise ApprovalTaskConflictError("加签或会签通过不会直接推进单据节点。")
        if task.status != "pending":
            raise ApprovalTaskConflictError("当前审批任务正在等待加签或会签完成。")
        if (
            task.claim_status_snapshot != str(claim.status or "").strip().lower()
            or task.claim_stage_snapshot != str(claim.approval_stage or "").strip()
        ):
            raise ApprovalTaskConflictError("审批任务与单据节点不一致，请刷新任务队列。")
        expected = int(expected_task_version or task.version)
        if task.version != expected:
            from app.services.approval_task_protocol import (
                ApprovalTaskVersionConflictError,
            )

            raise ApprovalTaskVersionConflictError(task.version)
        self.access.require_action(task, current_user, action=action, claim=claim)
        return ApprovalTaskDecisionContext(
            task=task,
            expected_task_version=expected,
            before_json=task_state(task),
        )

    def complete_claim_decision(
        self,
        context: ApprovalTaskDecisionContext,
        claim: ExpenseClaim,
        current_user: CurrentUserContext,
        *,
        action: str,
        opinion: str,
        request_id: str,
        ledger: ApprovalActionLedger,
        business_event: BusinessEvent | None = None,
    ) -> ApprovalTaskMutationRead:
        task = context.task
        now = datetime.now(UTC)
        task.status = "completed" if action == "approve" else "returned"
        task.decision = "approved" if action == "approve" else "returned"
        task.opinion = str(opinion or "").strip() or None
        task.version += 1
        task.claim_status_snapshot = str(claim.status or "").strip().lower()
        task.claim_stage_snapshot = str(claim.approval_stage or "").strip()
        task.completed_at = now
        task.updated_at = now
        related_tasks: list[ApprovalTask] = []
        if action == "return" and task.task_kind != "root":
            related_tasks = self._cancel_node_family(task, claim=claim, now=now)
        # Session 关闭了 autoflush；先持久化当前根任务的终态，后续创建下一
        # 节点时才不会把同一对象误判成仍开放并改写为 superseded。
        self.db.flush()
        next_task = self.ensure_root_task(
            claim,
            tenant_id=task.tenant_id,
            entered_at=(business_event.occurred_at if business_event is not None else now),
            entered_at_source=("workflow_event" if business_event is not None else "backfill"),
            business_event=business_event,
            actor_id="system",
            actor_name="审批任务引擎",
        )
        if next_task is not None:
            related_tasks.append(next_task)
        _, response = self.events.append_event(
            task=task,
            event_type=("task_approved" if action == "approve" else "task_returned"),
            actor_id=self.access.actor_id(current_user),
            actor_name=str(current_user.name or current_user.username),
            actor_type="user",
            request_id=request_id,
            expected_task_version=context.expected_task_version,
            payload={"opinion": str(opinion or "").strip()},
            before_json=context.before_json,
            related_tasks=related_tasks,
            approval_action_ledger_id=ledger.id,
            business_event_id=business_event.id if business_event is not None else None,
            correlation_id=(business_event.correlation_id if business_event is not None else None),
            occurred_at=now,
        )
        return response

    def _cancel_node_family(
        self,
        task: ApprovalTask,
        *,
        claim: ExpenseClaim,
        now: datetime,
    ) -> list[ApprovalTask]:
        statement = (
            select(ApprovalTask)
            .where(
                ApprovalTask.tenant_id == task.tenant_id,
                ApprovalTask.node_instance_id == task.node_instance_id,
                ApprovalTask.id != task.id,
                ApprovalTask.status.in_(OPEN_TASK_STATUSES),
            )
            .order_by(ApprovalTask.sequence_order.asc())
        )
        bind = self.db.get_bind()
        if bind is not None and bind.dialect.name == "postgresql":
            statement = statement.with_for_update()
        related = list(self.db.scalars(statement).all())
        for item in related:
            before = task_state(item)
            expected = item.version
            item.status = "cancelled"
            item.decision = None
            item.completed_at = None
            item.cancelled_at = now
            item.version += 1
            item.claim_status_snapshot = str(claim.status or "").strip().lower()
            item.claim_stage_snapshot = str(claim.approval_stage or "").strip()
            item.updated_at = now
            self.events.append_event(
                task=item,
                event_type="task_cancelled",
                actor_id="system",
                actor_name="审批任务引擎",
                actor_type="system",
                request_id=f"cancel:{item.id}:{item.version}",
                expected_task_version=expected,
                payload={"reason": "participant_returned_claim"},
                before_json=before,
                occurred_at=now,
            )
        return related

    def resolve_assignment(
        self,
        claim: ExpenseClaim,
        *,
        node: ApprovalNode,
    ) -> ApprovalAssignment:
        if node.key == "direct_manager":
            employee = self.access.claim_policy.resolve_claim_employee_for_backfill(claim)
            manager = employee.manager if employee is not None else None
            if manager is None:
                manager = self.access.claim_policy.resolve_employee_by_identity_candidates(
                    [str(claim.manager_name or "").strip()],
                    tenant_id=self.access.claim_policy.resolve_structured_claim_tenant_id(claim),
                )
            if manager is None:
                raise ApprovalTaskConfigurationError(
                    "未配置可唯一识别的直属领导，无法创建审批任务。"
                )
            return self._employee_assignment(manager)
        if node.key == "budget_manager":
            employee = self.access.claim_policy.resolve_department_budget_manager(claim)
            if employee is None:
                raise ApprovalTaskConfigurationError(
                    "未配置同部门 P8 预算管理者，无法创建审批任务。"
                )
            return self._employee_assignment(employee)
        finance = self.access.claim_policy.resolve_finance_approver(claim)
        if finance is not None:
            return self._employee_assignment(finance)
        return ApprovalAssignment(
            kind="role",
            key="finance",
            employee_id=None,
            name="财务审批组",
        )

    def _supersede_stale_root(
        self,
        task: ApprovalTask,
        *,
        claim: ExpenseClaim,
    ) -> None:
        before = task_state(task)
        now = datetime.now(UTC)
        expected = task.version
        task.status = "superseded"
        task.decision = None
        task.completed_at = None
        task.cancelled_at = now
        task.version += 1
        task.claim_status_snapshot = str(claim.status or "").strip().lower()
        task.claim_stage_snapshot = str(claim.approval_stage or "").strip()
        task.updated_at = now
        self.events.append_event(
            task=task,
            event_type="task_superseded",
            actor_id="system",
            actor_name="审批任务引擎",
            actor_type="system",
            request_id=f"supersede:{task.id}:{task.version}",
            expected_task_version=expected,
            payload={"reason": "claim_stage_changed"},
            before_json=before,
            occurred_at=now,
        )

    def _expense_case_id(
        self,
        claim: ExpenseClaim,
        *,
        business_event: BusinessEvent | None,
    ) -> str | None:
        if business_event is not None:
            return business_event.expense_case_id
        return self.db.scalar(
            select(ExpenseCaseLink.expense_case_id).where(
                ExpenseCaseLink.resource_type == "expense_claim",
                ExpenseCaseLink.resource_id == claim.id,
            )
        )

    @staticmethod
    def _employee_assignment(employee: Employee) -> ApprovalAssignment:
        key = str(employee.id or employee.email or employee.employee_no).strip()
        if not key:
            raise ApprovalTaskConfigurationError("审批人缺少稳定身份标识。")
        return ApprovalAssignment(
            kind="employee",
            key=key,
            employee_id=employee.id,
            name=str(employee.name or employee.email or employee.employee_no).strip(),
        )

    @staticmethod
    def _read_without_permission(task: ApprovalTask):
        from app.services.approval_task_protocol import task_read

        return task_read(task)

    @staticmethod
    def _apply_projection(
        task: ApprovalTask,
        *,
        claim: ExpenseClaim,
        now: datetime,
    ) -> None:
        from app.services.approval_task_projection import ApprovalTaskProjectionService

        ApprovalTaskProjectionService.apply(task, claim=claim, now=now)


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


# Local alias keeps callers independent of protocol module layout.
from app.services.approval_task_protocol import ApprovalTaskConflictError  # noqa: E402
