from __future__ import annotations

import hashlib
import json
import threading
from collections.abc import Iterator, Mapping
from contextlib import contextmanager
from decimal import Decimal
from typing import Any

from sqlalchemy import text
from sqlalchemy.orm import Session

from app.api.deps import CurrentUserContext


class SavingsConflictError(RuntimeError):
    pass


class SavingsVersionConflictError(SavingsConflictError):
    def __init__(self, current_version: int) -> None:
        self.current_version = current_version
        super().__init__(f"节省事实版本已更新，当前版本为 {current_version}。")


class SavingsIdempotencyConflictError(SavingsConflictError):
    pass


class SavingsRequestProtocol:
    """为账本动作提供 PostgreSQL advisory lock 和测试环境进程锁。"""

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

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

    @contextmanager
    def serialize(
        self,
        *,
        tenant_id: str,
        actor_id: str,
        request_id: str,
    ) -> Iterator[None]:
        name = f"savings:{tenant_id}:{actor_id}:{request_id}"
        bind = self.db.get_bind()
        if bind is not None and bind.dialect.name == "postgresql":
            self.db.execute(
                text("SELECT pg_advisory_xact_lock(:lock_id)"),
                {"lock_id": self._signed_lock_id(name)},
            )
            yield
            return
        with self._fallback_lock(name):
            yield

    @classmethod
    @contextmanager
    def _fallback_lock(cls, name: str) -> Iterator[None]:
        with cls._registry_guard:
            lock, references = cls._locks.get(name, (threading.RLock(), 0))
            cls._locks[name] = (lock, references + 1)
        try:
            with lock:
                yield
        finally:
            with cls._registry_guard:
                current = cls._locks.get(name)
                if current is None or current[0] is not lock:
                    pass
                elif current[1] <= 1:
                    cls._locks.pop(name, None)
                else:
                    cls._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


def savings_payload_fingerprint(
    *,
    action: str,
    aggregate_type: str,
    aggregate_id: str,
    actor_id: str,
    payload: Mapping[str, Any],
) -> str:
    canonical = json.dumps(
        {
            "action": str(action or "").strip(),
            "aggregate_type": str(aggregate_type or "").strip(),
            "aggregate_id": str(aggregate_id or "").strip(),
            "actor_id": str(actor_id or "").strip().casefold(),
            "payload": _json_safe(dict(payload)),
        },
        ensure_ascii=False,
        sort_keys=True,
        separators=(",", ":"),
    )
    return hashlib.sha256(canonical.encode("utf-8")).hexdigest()


def savings_actor(current_user: CurrentUserContext) -> tuple[str, str]:
    actor_id = str(current_user.employee_id or current_user.username or "").strip()
    if not actor_id:
        raise ValueError("当前用户缺少可审计的账号标识。")
    actor_name = str(current_user.name or current_user.username or actor_id).strip()
    return actor_id, actor_name


def _json_safe(value: Any) -> Any:
    if isinstance(value, Decimal):
        return f"{value:f}"
    if hasattr(value, "isoformat"):
        return value.isoformat()
    if isinstance(value, Mapping):
        return {str(key): _json_safe(item) for key, item in value.items()}
    if isinstance(value, (list, tuple, set, frozenset)):
        return [_json_safe(item) for item in value]
    return value
