from __future__ import annotations

import hashlib
import json
import threading
import uuid
from collections.abc import Callable, Iterator, Mapping
from contextlib import contextmanager
from dataclasses import dataclass
from datetime import UTC, datetime
from typing import Any

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

from app.api.deps import CurrentUserContext
from app.models.approval_action import ApprovalActionLedger
from app.models.financial_record import ExpenseClaim
from app.services.expense_claim_tenant_scope import ExpenseClaimTenantScopeMixin


class ApprovalActionConflictError(ValueError):
    """请求已被占用，或动作前置状态已经过期。"""


@dataclass(frozen=True, slots=True)
class ApprovalActionStart:
    claim: ExpenseClaim | None
    ledger: ApprovalActionLedger | None
    request_id: str
    replayed: bool


class ApprovalActionProtocol:
    """为 approve/return/pay 提供并发安全和事务内幂等语义。"""

    _fallback_registry_guard = threading.Lock()
    _fallback_locks: dict[str, tuple[threading.RLock, int]] = {}

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

    @contextmanager
    def serialize_request(
        self,
        *,
        tenant_id: str,
        actor_id: str,
        request_id: str | None,
    ) -> Iterator[str]:
        normalized_request_id = self._normalize_request_id(request_id)
        lock_name = f"approval-action:{tenant_id}:{actor_id}:{normalized_request_id}"
        if self._dialect_name() == "postgresql":
            self.db.execute(
                text("SELECT pg_advisory_xact_lock(:lock_id)"),
                {"lock_id": self._signed_lock_id(lock_name)},
            )
            yield normalized_request_id
            return

        with self._serialize_fallback(lock_name):
            yield normalized_request_id

    def begin(
        self,
        *,
        action: str,
        claim_id: str,
        current_user: CurrentUserContext,
        request_id: str,
        expected_status: str | None,
        expected_approval_stage: str | None,
        payload: Mapping[str, Any],
        claim_loader: Callable[[], ExpenseClaim | None],
    ) -> ApprovalActionStart:
        tenant_id = ExpenseClaimTenantScopeMixin.normalize_tenant_id(current_user.tenant_id)
        actor_id = self._normalize_actor_id(current_user.username)
        normalized_action = self._normalize_action(action)
        existing = self.db.scalar(
            select(ApprovalActionLedger).where(
                ApprovalActionLedger.tenant_id == tenant_id,
                ApprovalActionLedger.actor_id == actor_id,
                ApprovalActionLedger.request_id == request_id,
            )
        )
        if existing is not None:
            supplied_fingerprint = self._fingerprint(
                action=normalized_action,
                claim_id=claim_id,
                expected_status=expected_status or existing.expected_status,
                expected_approval_stage=(
                    expected_approval_stage or existing.expected_approval_stage
                ),
                payload=payload,
            )
            if supplied_fingerprint != existing.payload_fingerprint:
                raise ApprovalActionConflictError(
                    "该 request_id 已用于另一项审批动作，请生成新的 request_id 后重试。"
                )
            if existing.completed_at is None:
                raise ApprovalActionConflictError("该审批动作仍在处理中，请稍后重试。")
            return ApprovalActionStart(
                claim=None,
                ledger=existing,
                request_id=request_id,
                replayed=True,
            )

        claim = claim_loader()
        if claim is None:
            return ApprovalActionStart(
                claim=None,
                ledger=None,
                request_id=request_id,
                replayed=False,
            )

        actual_tenant_id = ExpenseClaimTenantScopeMixin.resolve_claim_tenant_id(
            self.db,
            claim.id,
        )
        if actual_tenant_id != tenant_id:
            # 正常情况下租户查询范围会先拦截；这里是写入账本前的纵深校验。
            raise ApprovalActionConflictError("单据租户上下文已变化，请刷新后重试。")

        current_status = self._normalize_status(claim.status)
        current_stage = self._normalize_stage(claim.approval_stage)
        normalized_expected_status = self._normalize_status(
            expected_status if expected_status is not None else current_status
        )
        normalized_expected_stage = self._normalize_stage(
            expected_approval_stage if expected_approval_stage is not None else current_stage
        )
        if normalized_expected_status != current_status:
            raise ApprovalActionConflictError(
                f"单据状态已从 {normalized_expected_status} 变为 {current_status}，请刷新后重试。"
            )
        if normalized_expected_stage != current_stage:
            raise ApprovalActionConflictError(
                f"审批节点已从 {normalized_expected_stage} 变为 {current_stage}，请刷新后重试。"
            )

        ledger = ApprovalActionLedger(
            tenant_id=tenant_id,
            actor_id=actor_id,
            request_id=request_id,
            claim_id=str(claim.id),
            action=normalized_action,
            payload_fingerprint=self._fingerprint(
                action=normalized_action,
                claim_id=claim.id,
                expected_status=normalized_expected_status,
                expected_approval_stage=normalized_expected_stage,
                payload=payload,
            ),
            expected_status=normalized_expected_status,
            expected_approval_stage=normalized_expected_stage,
        )
        self.db.add(ledger)
        self.db.flush()
        return ApprovalActionStart(
            claim=claim,
            ledger=ledger,
            request_id=request_id,
            replayed=False,
        )

    def complete(
        self,
        ledger: ApprovalActionLedger,
        claim: ExpenseClaim,
        *,
        response_json: Mapping[str, Any] | None = None,
    ) -> None:
        ledger.result_status = self._normalize_status(claim.status)
        ledger.result_approval_stage = self._normalize_stage(claim.approval_stage)
        ledger.response_json = dict(response_json or {})
        ledger.completed_at = datetime.now(UTC)
        self.db.flush()

    def _dialect_name(self) -> str:
        bind = self.db.get_bind()
        return str(bind.dialect.name if bind is not None else "")

    @classmethod
    @contextmanager
    def _serialize_fallback(cls, name: str) -> Iterator[None]:
        with cls._fallback_registry_guard:
            lock, references = cls._fallback_locks.get(
                name,
                (threading.RLock(), 0),
            )
            cls._fallback_locks[name] = (lock, references + 1)
        try:
            with lock:
                yield
        finally:
            with cls._fallback_registry_guard:
                current = cls._fallback_locks.get(name)
                if current is None or current[0] is not lock:
                    return
                if current[1] <= 1:
                    cls._fallback_locks.pop(name, None)
                else:
                    cls._fallback_locks[name] = (lock, current[1] - 1)

    @staticmethod
    def _signed_lock_id(value: str) -> int:
        unsigned = int.from_bytes(
            hashlib.sha256(value.encode("utf-8")).digest()[:8],
            byteorder="big",
            signed=False,
        )
        return unsigned - (1 << 64) if unsigned >= (1 << 63) else unsigned

    @staticmethod
    def _normalize_request_id(value: str | None) -> str:
        normalized = str(value or "").strip() or f"internal:{uuid.uuid4()}"
        if len(normalized) > 120:
            raise ValueError("request_id 最长为 120 个字符。")
        return normalized

    @staticmethod
    def _normalize_actor_id(value: str | None) -> str:
        normalized = str(value or "").strip().casefold()
        if not normalized:
            raise ValueError("当前用户缺少可审计的账号标识。")
        return normalized

    @staticmethod
    def _normalize_action(value: str) -> str:
        normalized = str(value or "").strip().lower()
        if normalized not in {"approve", "return", "pay"}:
            raise ValueError("不支持的审批动作。")
        return normalized

    @staticmethod
    def _normalize_status(value: str | None) -> str:
        return str(value or "").strip().lower()

    @staticmethod
    def _normalize_stage(value: str | None) -> str:
        return str(value or "").strip()

    @classmethod
    def _fingerprint(
        cls,
        *,
        action: str,
        claim_id: str,
        expected_status: str,
        expected_approval_stage: str,
        payload: Mapping[str, Any],
    ) -> str:
        canonical = {
            "action": cls._normalize_action(action),
            "claim_id": str(claim_id or "").strip(),
            "expected_status": cls._normalize_status(expected_status),
            "expected_approval_stage": cls._normalize_stage(expected_approval_stage),
            "payload": dict(payload),
        }
        encoded = json.dumps(
            canonical,
            ensure_ascii=False,
            sort_keys=True,
            separators=(",", ":"),
            default=str,
        ).encode("utf-8")
        return f"sha256:{hashlib.sha256(encoded).hexdigest()}"
