from __future__ import annotations

import hashlib
import uuid
from datetime import UTC, datetime, timedelta

from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session

from app.api.deps import CurrentUserContext
from app.models.approval_task import ApprovalTask
from app.models.financial_record import ExpenseClaim
from app.schemas.approval_task import (
    ApprovalTaskActionBase,
    ApprovalTaskAssignmentAction,
    ApprovalTaskCandidateRead,
    ApprovalTaskDecisionAction,
    ApprovalTaskMutationRead,
    ApprovalTaskParticipantsAction,
)
from app.services.approval_task_access_policy import (
    ApprovalTaskAccessPolicy,
    ApprovalTaskPermissionError,
)
from app.services.approval_task_protocol import (
    ApprovalTaskConflictError,
    ApprovalTaskEventProtocol,
    ApprovalTaskVersionConflictError,
    payload_fingerprint,
    task_state,
)
from app.services.expense_claim_tenant_scope import ExpenseClaimTenantScopeMixin


class ApprovalTaskActionService:
    """审批任务辅助动作；不改变费用单状态机。"""

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

    def candidates(
        self,
        task_id: str,
        current_user: CurrentUserContext,
        *,
        action: str | None = None,
        keyword: str | None = None,
        limit: int = 50,
    ) -> list[ApprovalTaskCandidateRead]:
        task, claim = self._read_context(task_id, current_user)
        permission = self.access.require_read(task, current_user, claim=claim)
        normalized_action = str(action or "").strip()
        if normalized_action and normalized_action not in permission.available_actions:
            raise ApprovalTaskPermissionError(
                permission.read_only_reason or "当前用户无权为该任务选择处理人。"
            )
        normalized_keyword = str(keyword or "").strip().casefold()
        rows = [
            ApprovalTaskCandidateRead(
                employee_id=employee.id,
                employee_no=str(employee.employee_no or ""),
                name=str(employee.name or ""),
                email=str(employee.email or ""),
                qualified=qualified,
                reason=reason,
            )
            for employee, qualified, reason in self.access.candidate_employees(
                task=task,
                claim=claim,
            )
        ]
        if normalized_keyword:
            rows = [
                row
                for row in rows
                if normalized_keyword
                in " ".join((row.name, row.employee_no, row.email)).casefold()
            ]
        return rows[: min(max(int(limit), 1), 100)]

    def assign(
        self,
        task_id: str,
        current_user: CurrentUserContext,
        *,
        action: str,
        payload: ApprovalTaskAssignmentAction,
    ) -> ApprovalTaskMutationRead:
        if action not in {"delegate", "transfer"}:
            raise ValueError("不支持的任务分配动作。")
        event_type = "task_delegated" if action == "delegate" else "task_transferred"
        actor_id = self.access.actor_id(current_user)
        event_payload = payload.model_dump(mode="json", exclude={"request_id"})
        fingerprint = payload_fingerprint(
            task_id=task_id,
            actor_id=actor_id,
            event_type=event_type,
            payload=event_payload,
        )
        replay = self.events.find_replay(
            tenant_id=self._tenant(current_user),
            actor_id=actor_id,
            request_id=payload.request_id,
            fingerprint=fingerprint,
        )
        if replay is not None:
            return replay
        try:
            task, claim = self._lock_context(task_id, current_user)
            replay = self.events.find_replay(
                tenant_id=self._tenant(current_user),
                actor_id=actor_id,
                request_id=payload.request_id,
                fingerprint=fingerprint,
            )
            if replay is not None:
                self.db.commit()
                return replay
            self._ensure_expected_version(task, payload.expected_task_version)
            permission = self.access.require_action(
                task,
                current_user,
                action=action,
                claim=claim,
            )
            target = self.access.resolve_and_validate_target(
                task=task,
                claim=claim,
                target_employee_id=payload.target_employee_id,
            )
            if target.id == task.assignee_employee_id:
                raise ApprovalTaskConflictError("目标员工已经是当前任务处理人。")
            if action == "delegate" and task.delegated_by:
                raise ApprovalTaskConflictError("当前任务已有委托，请先撤销委托或执行转交。")
            if action == "delegate" and payload.expires_at is not None:
                if _as_utc(payload.expires_at) <= datetime.now(UTC):
                    raise ValueError("委托失效时间必须晚于当前时间。")
            if action == "transfer" and payload.expires_at is not None:
                raise ValueError("永久转交不能设置委托失效时间。")

            before = task_state(task)
            expected = task.version
            task.assignee_kind = "employee"
            task.assignee_key = target.id
            task.assignee_employee_id = target.id
            task.assignee_name = str(target.name or target.employee_no)
            if action == "delegate":
                task.delegated_by = actor_id
                task.delegation_expires_at = payload.expires_at
            else:
                task.owner_kind = "employee"
                task.owner_key = target.id
                task.owner_employee_id = target.id
                task.owner_name = str(target.name or target.employee_no)
                task.delegated_by = None
                task.delegation_expires_at = None
            task.version += 1
            task.updated_at = datetime.now(UTC)
            permission = self.access.evaluate(task, current_user, claim=claim)
            _, response = self.events.append_event(
                task=task,
                event_type=event_type,
                actor_id=actor_id,
                actor_name=str(current_user.name or current_user.username),
                actor_type="user",
                request_id=payload.request_id,
                expected_task_version=expected,
                payload=event_payload,
                before_json=before,
                permission=permission,
            )
            self.db.commit()
            return response
        except IntegrityError as error:
            self.db.rollback()
            replay = self.events.find_replay(
                tenant_id=self._tenant(current_user),
                actor_id=actor_id,
                request_id=payload.request_id,
                fingerprint=fingerprint,
            )
            if replay is not None:
                return replay
            raise ApprovalTaskConflictError("审批任务已被并发修改，请刷新后重试。") from error
        except Exception:
            self.db.rollback()
            raise

    def revoke_delegation(
        self,
        task_id: str,
        current_user: CurrentUserContext,
        payload: ApprovalTaskActionBase,
    ) -> ApprovalTaskMutationRead:
        return self._simple_mutation(
            task_id,
            current_user,
            action="delegation_revoke",
            payload=payload,
            mutate=self._apply_delegation_revoke,
        )

    def escalate(
        self,
        task_id: str,
        current_user: CurrentUserContext,
        payload: ApprovalTaskActionBase,
    ) -> ApprovalTaskMutationRead:
        return self._simple_mutation(
            task_id,
            current_user,
            action="sla_escalate",
            payload=payload,
            mutate=self._apply_escalation,
        )

    def add_participants(
        self,
        task_id: str,
        current_user: CurrentUserContext,
        *,
        action: str,
        payload: ApprovalTaskParticipantsAction,
    ) -> ApprovalTaskMutationRead:
        if action not in {"add_sign", "countersign"}:
            raise ValueError("不支持的参与人动作。")
        event_type = "add_sign_started" if action == "add_sign" else "countersign_started"
        actor_id = self.access.actor_id(current_user)
        event_payload = payload.model_dump(mode="json", exclude={"request_id"})
        fingerprint = payload_fingerprint(
            task_id=task_id,
            actor_id=actor_id,
            event_type=event_type,
            payload=event_payload,
        )
        replay = self.events.find_replay(
            tenant_id=self._tenant(current_user),
            actor_id=actor_id,
            request_id=payload.request_id,
            fingerprint=fingerprint,
        )
        if replay is not None:
            return replay
        try:
            root, claim = self._lock_context(task_id, current_user)
            replay = self.events.find_replay(
                tenant_id=self._tenant(current_user),
                actor_id=actor_id,
                request_id=payload.request_id,
                fingerprint=fingerprint,
            )
            if replay is not None:
                self.db.commit()
                return replay
            self._ensure_expected_version(root, payload.expected_task_version)
            self.access.require_action(root, current_user, action=action, claim=claim)
            if root.task_kind != "root" or root.status != "pending":
                raise ApprovalTaskConflictError("只有待处理的根任务可以发起加签或会签。")
            existing_children = self.db.scalar(
                select(ApprovalTask.id).where(
                    ApprovalTask.tenant_id == root.tenant_id,
                    ApprovalTask.parent_task_id == root.id,
                    ApprovalTask.status.in_(("waiting", "pending")),
                )
            )
            if existing_children is not None:
                raise ApprovalTaskConflictError("当前节点已有未完成的加签或会签任务。")
            participants = [
                self.access.resolve_and_validate_target(
                    task=root,
                    claim=claim,
                    target_employee_id=employee_id,
                )
                for employee_id in payload.participant_employee_ids
            ]
            occupied = {
                str(root.owner_employee_id or ""),
                str(root.assignee_employee_id or ""),
            }
            if any(employee.id in occupied for employee in participants):
                raise ValueError("加签或会签参与人不能与当前责任人重复。")

            now = datetime.now(UTC)
            before = task_state(root)
            expected = root.version
            root.status = "waiting"
            root.coordination_mode = (
                "sequential_before_owner" if action == "add_sign" else "parallel_all"
            )
            root.version += 1
            root.updated_at = now
            children = [
                self._new_child(
                    root,
                    claim=claim,
                    employee=employee,
                    task_kind=action,
                    sequence_order=index,
                    active=(action == "countersign" or index == 1),
                    now=now,
                )
                for index, employee in enumerate(participants, start=1)
            ]
            self.db.add_all(children)
            self.db.flush()
            for index, child in enumerate(children, start=1):
                self.events.append_event(
                    task=child,
                    event_type="node_entered",
                    actor_id="system",
                    actor_name="审批任务引擎",
                    actor_type="system",
                    request_id=_derived_request(payload.request_id, f"child:{index}"),
                    expected_task_version=0,
                    payload={
                        "parent_task_id": root.id,
                        "task_kind": action,
                        "sequence_order": index,
                    },
                    before_json={},
                    occurred_at=now,
                )
            event, response = self.events.append_event(
                task=root,
                event_type=event_type,
                actor_id=actor_id,
                actor_name=str(current_user.name or current_user.username),
                actor_type="user",
                request_id=payload.request_id,
                expected_task_version=expected,
                payload=event_payload,
                before_json=before,
                related_tasks=children,
            )
            event.response_json = response.model_dump(mode="json")
            self.db.commit()
            return response
        except IntegrityError as error:
            self.db.rollback()
            replay = self.events.find_replay(
                tenant_id=self._tenant(current_user),
                actor_id=actor_id,
                request_id=payload.request_id,
                fingerprint=fingerprint,
            )
            if replay is not None:
                return replay
            raise ApprovalTaskConflictError("审批任务已被并发修改，请刷新后重试。") from error
        except Exception:
            self.db.rollback()
            raise

    def approve_child(
        self,
        task_id: str,
        current_user: CurrentUserContext,
        payload: ApprovalTaskDecisionAction,
    ) -> ApprovalTaskMutationRead:
        actor_id = self.access.actor_id(current_user)
        event_payload = payload.model_dump(mode="json", exclude={"request_id"})
        fingerprint = payload_fingerprint(
            task_id=task_id,
            actor_id=actor_id,
            event_type="task_approved",
            payload=event_payload,
        )
        replay = self.events.find_replay(
            tenant_id=self._tenant(current_user),
            actor_id=actor_id,
            request_id=payload.request_id,
            fingerprint=fingerprint,
        )
        if replay is not None:
            return replay
        try:
            task, claim = self._lock_context(task_id, current_user)
            replay = self.events.find_replay(
                tenant_id=self._tenant(current_user),
                actor_id=actor_id,
                request_id=payload.request_id,
                fingerprint=fingerprint,
            )
            if replay is not None:
                self.db.commit()
                return replay
            if task.task_kind == "root":
                raise ApprovalTaskConflictError("根任务请使用单据审批动作。")
            self._ensure_expected_version(task, payload.expected_task_version)
            self.access.require_action(task, current_user, action="approve", claim=claim)
            before = task_state(task)
            expected = task.version
            now = datetime.now(UTC)
            task.status = "completed"
            task.decision = "approved"
            task.opinion = payload.opinion or payload.reason
            task.completed_at = now
            task.version += 1
            task.updated_at = now
            related = self._advance_node_family(task, claim=claim, now=now)
            event, response = self.events.append_event(
                task=task,
                event_type="task_approved",
                actor_id=actor_id,
                actor_name=str(current_user.name or current_user.username),
                actor_type="user",
                request_id=payload.request_id,
                expected_task_version=expected,
                payload=event_payload,
                before_json=before,
                related_tasks=related,
                occurred_at=now,
            )
            event.response_json = response.model_dump(mode="json")
            self.db.commit()
            return response
        except IntegrityError as error:
            self.db.rollback()
            replay = self.events.find_replay(
                tenant_id=self._tenant(current_user),
                actor_id=actor_id,
                request_id=payload.request_id,
                fingerprint=fingerprint,
            )
            if replay is not None:
                return replay
            raise ApprovalTaskConflictError("审批任务已被并发修改，请刷新后重试。") from error
        except Exception:
            self.db.rollback()
            raise

    def _simple_mutation(
        self,
        task_id: str,
        current_user: CurrentUserContext,
        *,
        action: str,
        payload: ApprovalTaskActionBase,
        mutate,
    ) -> ApprovalTaskMutationRead:
        actor_id = self.access.actor_id(current_user)
        event_payload = payload.model_dump(mode="json", exclude={"request_id"})
        fingerprint = payload_fingerprint(
            task_id=task_id,
            actor_id=actor_id,
            event_type=action,
            payload=event_payload,
        )
        replay = self.events.find_replay(
            tenant_id=self._tenant(current_user),
            actor_id=actor_id,
            request_id=payload.request_id,
            fingerprint=fingerprint,
        )
        if replay is not None:
            return replay
        try:
            task, claim = self._lock_context(task_id, current_user)
            replay = self.events.find_replay(
                tenant_id=self._tenant(current_user),
                actor_id=actor_id,
                request_id=payload.request_id,
                fingerprint=fingerprint,
            )
            if replay is not None:
                self.db.commit()
                return replay
            self._ensure_expected_version(task, payload.expected_task_version)
            self.access.require_action(task, current_user, action=action, claim=claim)
            before = task_state(task)
            expected = task.version
            mutate(task, actor_id=actor_id)
            task.version += 1
            task.updated_at = datetime.now(UTC)
            _, response = self.events.append_event(
                task=task,
                event_type=action,
                actor_id=actor_id,
                actor_name=str(current_user.name or current_user.username),
                actor_type="user",
                request_id=payload.request_id,
                expected_task_version=expected,
                payload=event_payload,
                before_json=before,
            )
            self.db.commit()
            return response
        except IntegrityError as error:
            self.db.rollback()
            replay = self.events.find_replay(
                tenant_id=self._tenant(current_user),
                actor_id=actor_id,
                request_id=payload.request_id,
                fingerprint=fingerprint,
            )
            if replay is not None:
                return replay
            raise ApprovalTaskConflictError("审批任务已被并发修改，请刷新后重试。") from error
        except Exception:
            self.db.rollback()
            raise

    def _read_context(
        self,
        task_id: str,
        current_user: CurrentUserContext,
    ) -> tuple[ApprovalTask, ExpenseClaim]:
        tenant = self._tenant(current_user)
        task = self.db.scalar(
            select(ApprovalTask).where(
                ApprovalTask.tenant_id == tenant,
                ApprovalTask.id == str(task_id).strip(),
            )
        )
        if task is None:
            raise LookupError("Approval task not found.")
        claim = self.db.scalar(
            select(ExpenseClaim).where(
                ExpenseClaim.id == task.claim_id,
                ExpenseClaimTenantScopeMixin.build_claim_tenant_condition(tenant),
            )
        )
        if claim is None:
            raise LookupError("Approval task not found.")
        return task, claim

    def _lock_context(
        self,
        task_id: str,
        current_user: CurrentUserContext,
    ) -> tuple[ApprovalTask, ExpenseClaim]:
        tenant = self._tenant(current_user)
        locator = self.db.execute(
            select(ApprovalTask.claim_id).where(
                ApprovalTask.tenant_id == tenant,
                ApprovalTask.id == str(task_id).strip(),
            )
        ).one_or_none()
        if locator is None:
            raise LookupError("Approval task not found.")
        claim_statement = select(ExpenseClaim).where(
            ExpenseClaim.id == locator[0],
            ExpenseClaimTenantScopeMixin.build_claim_tenant_condition(tenant),
        )
        task_statement = select(ApprovalTask).where(
            ApprovalTask.tenant_id == tenant,
            ApprovalTask.id == str(task_id).strip(),
        )
        bind = self.db.get_bind()
        if bind is not None and bind.dialect.name == "postgresql":
            claim_statement = claim_statement.with_for_update()
            task_statement = task_statement.with_for_update()
        claim = self.db.scalar(claim_statement.execution_options(populate_existing=True))
        task = self.db.scalar(task_statement.execution_options(populate_existing=True))
        if claim is None or task is None or task.claim_id != claim.id:
            raise LookupError("Approval task not found.")
        return task, claim

    def _new_child(
        self,
        root: ApprovalTask,
        *,
        claim: ExpenseClaim,
        employee,
        task_kind: str,
        sequence_order: int,
        active: bool,
        now: datetime,
    ) -> ApprovalTask:
        return ApprovalTask(
            id=str(uuid.uuid4()),
            tenant_id=root.tenant_id,
            claim_id=root.claim_id,
            expense_case_id=root.expense_case_id,
            node_instance_id=root.node_instance_id,
            node_entry_key=f"{root.id}:{task_kind}:{sequence_order}:{employee.id}",
            parent_task_id=root.id,
            task_kind=task_kind,
            node_key=root.node_key,
            node_label=root.node_label,
            node_sequence=root.node_sequence,
            sequence_order=sequence_order,
            coordination_mode=root.coordination_mode,
            owner_kind="employee",
            owner_key=employee.id,
            owner_employee_id=employee.id,
            owner_name=str(employee.name or employee.employee_no),
            assignee_kind="employee",
            assignee_key=employee.id,
            assignee_employee_id=employee.id,
            assignee_name=str(employee.name or employee.employee_no),
            status="pending" if active else "waiting",
            decision=None,
            version=1,
            claim_status_snapshot=str(claim.status or "").strip().lower(),
            claim_stage_snapshot=str(claim.approval_stage or "").strip(),
            entered_at=now,
            entered_at_source="workflow_event",
            activated_at=now if active else None,
            sla_hours_snapshot=root.sla_hours_snapshot,
            due_at=now + timedelta(hours=root.sla_hours_snapshot),
            escalation_level=0,
            next_escalation_at=root.next_escalation_at,
            priority_score=root.priority_score,
            priority_tier=root.priority_tier,
            priority_reasons_json=list(root.priority_reasons_json or []),
            risk_level=root.risk_level,
            open_risk_count=root.open_risk_count,
            evidence_completeness=root.evidence_completeness,
            batch_eligible=False,
            batch_block_reasons_json=["加签或会签任务不能批量审批。"],
            projection_updated_at=root.projection_updated_at,
            created_at=now,
            updated_at=now,
        )

    def _advance_node_family(
        self,
        task: ApprovalTask,
        *,
        claim: ExpenseClaim,
        now: datetime,
    ) -> list[ApprovalTask]:
        root_statement = select(ApprovalTask).where(
            ApprovalTask.tenant_id == task.tenant_id,
            ApprovalTask.id == task.parent_task_id,
        )
        bind = self.db.get_bind()
        if bind is not None and bind.dialect.name == "postgresql":
            root_statement = root_statement.with_for_update()
        root = self.db.scalar(root_statement)
        if root is None or root.status != "waiting":
            raise ApprovalTaskConflictError("加签或会签根任务状态异常。")
        related: list[ApprovalTask] = []
        if task.task_kind == "add_sign":
            next_child = self.db.scalar(
                select(ApprovalTask)
                .where(
                    ApprovalTask.tenant_id == task.tenant_id,
                    ApprovalTask.parent_task_id == root.id,
                    ApprovalTask.status == "waiting",
                )
                .order_by(ApprovalTask.sequence_order.asc())
                .limit(1)
            )
            if next_child is not None:
                self._activate_related(
                    next_child,
                    claim=claim,
                    now=now,
                    event_type="task_activated",
                )
                related.append(next_child)
                return related
        elif self.db.scalar(
            select(ApprovalTask.id).where(
                ApprovalTask.tenant_id == task.tenant_id,
                ApprovalTask.parent_task_id == root.id,
                ApprovalTask.status.in_(("waiting", "pending")),
                ApprovalTask.id != task.id,
            )
        ) is not None:
            return related
        self._activate_related(
            root,
            claim=claim,
            now=now,
            event_type="root_reactivated",
            coordination_mode="single",
        )
        related.append(root)
        return related

    def _activate_related(
        self,
        task: ApprovalTask,
        *,
        claim: ExpenseClaim,
        now: datetime,
        event_type: str,
        coordination_mode: str | None = None,
    ) -> None:
        before = task_state(task)
        expected = task.version
        task.status = "pending"
        if coordination_mode is not None:
            task.coordination_mode = coordination_mode
        task.activated_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=event_type,
            actor_id="system",
            actor_name="审批任务引擎",
            actor_type="system",
            request_id=f"activate:{task.id}:{task.version}",
            expected_task_version=expected,
            payload={"reason": "participant_tasks_completed"},
            before_json=before,
            occurred_at=now,
        )

    @staticmethod
    def _apply_delegation_revoke(task: ApprovalTask, *, actor_id: str) -> None:
        if not task.delegated_by:
            raise ApprovalTaskConflictError("当前任务没有可撤销的委托。")
        task.assignee_kind = task.owner_kind
        task.assignee_key = task.owner_key
        task.assignee_employee_id = task.owner_employee_id
        task.assignee_name = task.owner_name
        task.delegated_by = None
        task.delegation_expires_at = None

    @staticmethod
    def _apply_escalation(task: ApprovalTask, *, actor_id: str) -> None:
        now = datetime.now(UTC)
        if task.escalation_level >= 3:
            raise ApprovalTaskConflictError("该任务已经达到最高升级等级。")
        task.escalation_level += 1
        task.escalated_at = now
        task.next_escalation_at = (
            None if task.escalation_level >= 3 else now + timedelta(hours=4)
        )
        task.priority_score = min(100, int(task.priority_score) + 10)
        task.priority_tier = "urgent" if task.priority_score >= 65 else "high"
        task.priority_reasons_json = [
            *list(task.priority_reasons_json or []),
            {
                "code": f"sla_escalated_l{task.escalation_level}",
                "label": f"审批超时已升级至 L{task.escalation_level}",
                "weight": 10,
                "tone": "danger",
            },
        ]

    @staticmethod
    def _ensure_expected_version(task: ApprovalTask, expected: int) -> None:
        if task.version != expected:
            raise ApprovalTaskVersionConflictError(task.version)

    @staticmethod
    def _tenant(current_user: CurrentUserContext) -> str:
        return ExpenseClaimTenantScopeMixin.normalize_tenant_id(current_user.tenant_id)


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


def _derived_request(request_id: str, suffix: str) -> str:
    value = f"{request_id}:{suffix}"
    if len(value) <= 120:
        return value
    digest = hashlib.sha256(value.encode("utf-8")).hexdigest()
    return f"derived:{digest}"
