from __future__ import annotations

from collections.abc import Callable, Mapping
from typing import Any

from pydantic import ValidationError
from sqlalchemy import select

from app.api.deps import CurrentUserContext
from app.models.approval_action import ApprovalActionLedger
from app.models.approval_task import ApprovalTask
from app.models.financial_record import ExpenseClaim
from app.schemas.reimbursement import ExpenseClaimRead
from app.services.approval_action_protocol import (
    ApprovalActionConflictError,
    ApprovalActionProtocol,
)
from app.services.expense_claim_tenant_scope import ExpenseClaimTenantScopeMixin

ClaimActionExecutor = Callable[
    [ExpenseClaim, ApprovalActionLedger, str],
    ExpenseClaim,
]


class ExpenseClaimActionProtocolMixin:
    def _execute_claim_action(
        self,
        *,
        action: str,
        claim_id: str,
        current_user: CurrentUserContext,
        request_id: str | None,
        expected_status: str | None,
        expected_approval_stage: str | None,
        payload: Mapping[str, Any],
        executor: ClaimActionExecutor,
        task_id: str | None = None,
        expected_task_version: int | None = None,
    ) -> ExpenseClaim | ExpenseClaimRead | None:
        protocol = ApprovalActionProtocol(self.db)
        tenant_id = ExpenseClaimTenantScopeMixin.normalize_tenant_id(current_user.tenant_id)
        actor_id = str(current_user.username or "").strip().casefold()
        with protocol.serialize_request(
            tenant_id=tenant_id,
            actor_id=actor_id,
            request_id=request_id,
        ) as normalized_request_id:
            try:
                started = protocol.begin(
                    action=action,
                    claim_id=claim_id,
                    current_user=current_user,
                    request_id=normalized_request_id,
                    expected_status=expected_status,
                    expected_approval_stage=expected_approval_stage,
                    payload=payload,
                    claim_loader=lambda: self._load_claim_for_action(
                        claim_id,
                        current_user,
                        task_id=task_id,
                    ),
                )
                if started.replayed:
                    if started.ledger is None:  # pragma: no cover - defensive invariant
                        raise RuntimeError("审批动作重放账本缺失。")
                    response = self._response_from_ledger(started.ledger)
                    self.db.commit()
                    return response
                if started.claim is None:
                    self.db.rollback()
                    return None
                if started.ledger is None:  # pragma: no cover - defensive invariant
                    raise RuntimeError("审批动作账本初始化失败。")

                task_lifecycle = None
                task_context = None
                if action in {"approve", "return"}:
                    from app.services.approval_task_lifecycle import (
                        ApprovalTaskLifecycleService,
                    )

                    task_lifecycle = ApprovalTaskLifecycleService(self.db)
                    task_context = task_lifecycle.begin_claim_decision(
                        started.claim,
                        current_user,
                        action=action,
                        task_id=task_id,
                        expected_task_version=expected_task_version,
                    )
                    started.claim._approval_task_authorized = True

                claim = executor(
                    started.claim,
                    started.ledger,
                    normalized_request_id,
                )
                if task_lifecycle is not None and task_context is not None:
                    opinion = str(
                        payload.get("opinion")
                        or payload.get("reason")
                        or ""
                    ).strip()
                    task_lifecycle.complete_claim_decision(
                        task_context,
                        claim,
                        current_user,
                        action=action,
                        opinion=opinion,
                        request_id=normalized_request_id,
                        ledger=started.ledger,
                        business_event=getattr(
                            claim,
                            "_approval_task_business_event",
                            None,
                        ),
                    )
                self.db.flush()
                self.db.refresh(claim)
                response = ExpenseClaimRead.model_validate(
                    self._access_policy.attach_approval_snapshot(claim)
                )
                response_json = response.model_dump(mode="json")
                protocol.complete(
                    started.ledger,
                    claim,
                    response_json=response_json,
                )
                self.db.commit()
                self.db.refresh(claim)
                return self._access_policy.attach_approval_snapshot(claim)
            except Exception:
                self.db.rollback()
                raise

    def _load_claim_for_action(
        self,
        claim_id: str,
        current_user: CurrentUserContext,
        *,
        task_id: str | None = None,
    ) -> ExpenseClaim | None:
        stmt = select(ExpenseClaim).where(ExpenseClaim.id == claim_id)
        if task_id:
            tenant_id = ExpenseClaimTenantScopeMixin.normalize_tenant_id(
                current_user.tenant_id
            )
            task_claim_id = self.db.scalar(
                select(ApprovalTask.claim_id).where(
                    ApprovalTask.tenant_id == tenant_id,
                    ApprovalTask.id == str(task_id).strip(),
                )
            )
            if str(task_claim_id or "") != str(claim_id):
                return None
            stmt = stmt.where(
                ExpenseClaimTenantScopeMixin.build_claim_tenant_condition(tenant_id)
            )
        else:
            stmt = self._access_policy.apply_claim_scope(
                stmt,
                current_user,
                include_approval_scope=True,
            )
        bind = self.db.get_bind()
        if bind is not None and bind.dialect.name == "postgresql":
            stmt = stmt.with_for_update()
        claim = self.db.scalar(stmt)
        if claim is not None:
            # 动作协议已经持有事务级请求锁和 Claim 行锁；这里只允许在同一
            # 事务内修正对象，不能调用会自行 commit 的读取兼容修复入口。
            self._repair_duplicate_budget_approval_stage(claim)
        return claim

    @staticmethod
    def _response_from_ledger(ledger: ApprovalActionLedger) -> ExpenseClaimRead:
        try:
            response = ExpenseClaimRead.model_validate(ledger.response_json)
        except (TypeError, ValueError, ValidationError) as error:
            raise ApprovalActionConflictError(
                "该审批动作缺少可验证的原始响应快照，请刷新单据后使用新的 request_id。"
            ) from error
        if str(response.id) != str(ledger.claim_id):
            raise ApprovalActionConflictError("审批动作原始响应快照与单据不一致。")
        if str(response.status or "").strip().lower() != str(
            ledger.result_status or ""
        ).strip().lower():
            raise ApprovalActionConflictError("审批动作原始响应快照状态校验失败。")
        if str(response.approval_stage or "").strip() != str(
            ledger.result_approval_stage or ""
        ).strip():
            raise ApprovalActionConflictError("审批动作原始响应快照节点校验失败。")
        return response
