from __future__ import annotations

import hashlib
import json
import re
import threading
from collections.abc import Iterator
from contextlib import contextmanager
from datetime import UTC, datetime
from decimal import Decimal, InvalidOperation
from typing import Any

from sqlalchemy import select, text

from app.api.deps import CurrentUserContext
from app.models.financial_record import ExpenseClaim, ExpenseClaimItem
from app.schemas.reimbursement import (
    ExpenseClaimStandardAdjustmentPayload,
    TravelReimbursementCalculatorRequest,
)
from app.services.expense_claim_constants import STANDARD_ADJUSTMENT_RISK_SOURCE
from app.services.expense_claim_risk_flags import dedupe_claim_risk_flags
from app.services.expense_claim_risk_stage import with_risk_business_stage
from app.services.savings_discovery import SavingsDiscoveryService


class ExpenseClaimStandardAdjustmentMixin:
    _standard_adjustment_lock_registry_guard = threading.Lock()
    _standard_adjustment_locks: dict[str, tuple[threading.RLock, int]] = {}

    @staticmethod
    def _signed_standard_adjustment_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

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

    @contextmanager
    def _serialize_standard_adjustment(
        self,
        *,
        claim_id: str,
        current_user: CurrentUserContext,
    ) -> Iterator[None]:
        tenant_id = str(current_user.tenant_id or "default").strip() or "default"
        lock_name = f"expense-standard-adjustment:{tenant_id}:{claim_id}"
        bind = self.db.get_bind()
        dialect_name = str(bind.dialect.name if bind is not None else "").lower()
        if dialect_name == "postgresql":
            self.db.execute(
                text("SELECT pg_advisory_xact_lock(:lock_id)"),
                {"lock_id": self._signed_standard_adjustment_lock_id(lock_name)},
            )
            yield
            return

        with self._serialize_standard_adjustment_fallback(lock_name):
            yield

    def _supports_standard_adjustment_row_lock(self) -> bool:
        """SQLite 不支持行锁；生产数据库则锁住单据及明细后再读取金额事实。"""

        bind = self.db.get_bind()
        return str(bind.dialect.name or "").strip().lower() != "sqlite"

    def _get_locked_standard_adjustment_claim(
        self,
        *,
        claim_id: str,
        current_user: CurrentUserContext,
    ) -> tuple[ExpenseClaim, dict[str, ExpenseClaimItem]] | None:
        claim_stmt = select(ExpenseClaim).where(ExpenseClaim.id == claim_id)
        claim_stmt = self._access_policy.apply_claim_scope(
            claim_stmt,
            current_user,
            include_approval_scope=True,
        )
        supports_row_lock = self._supports_standard_adjustment_row_lock()
        if supports_row_lock:
            claim_stmt = claim_stmt.with_for_update()

        claim = self.db.scalar(claim_stmt.execution_options(populate_existing=True))
        if claim is None:
            return None

        item_stmt = (
            select(ExpenseClaimItem)
            .where(ExpenseClaimItem.claim_id == claim.id)
            .order_by(ExpenseClaimItem.item_date.asc(), ExpenseClaimItem.id.asc())
        )
        if supports_row_lock:
            item_stmt = item_stmt.with_for_update()
        locked_items = list(
            self.db.scalars(item_stmt.execution_options(populate_existing=True)).all()
        )
        item_map = {
            str(item.id or "").strip(): item for item in locked_items if str(item.id or "").strip()
        }
        return self._access_policy.attach_approval_snapshot(claim), item_map

    @staticmethod
    def _normalize_standard_adjustment_amount(value: Any) -> Decimal | None:
        try:
            raw_value = "" if value is None else value
            amount = Decimal(str(raw_value)).quantize(Decimal("0.01"))
        except (InvalidOperation, ValueError):
            return None
        return amount if amount >= Decimal("0.00") else None

    @staticmethod
    def _format_adjustment_money(value: Decimal) -> str:
        normalized = Decimal(value or Decimal("0.00")).quantize(Decimal("0.01"))
        return f"{normalized:.2f}"

    @staticmethod
    def _normalize_standard_adjustment_days(value: Any) -> int | None:
        if value is None:
            return None
        if isinstance(value, int):
            return value if 1 <= value <= 365 else None
        text = str(value or "").strip()
        if not text:
            return None
        match = re.search(r"\d{1,3}", text)
        if not match:
            return None
        days = int(match.group(0))
        return days if 1 <= days <= 365 else None

    @staticmethod
    def _normalize_standard_adjustment_text(value: Any) -> str:
        text = str(value or "").strip()
        if not text or text in {"-", "N/A", "n/a"}:
            return ""
        if text in {"待补充", "未知", "暂无", "非必填"}:
            return ""
        return text

    def _iter_standard_adjustment_application_details(
        self,
        claim: ExpenseClaim,
    ) -> list[dict[str, Any]]:
        details: list[dict[str, Any]] = []
        for flag in list(claim.risk_flags_json or []):
            if not isinstance(flag, dict):
                continue
            if str(flag.get("source") or "").strip() not in {
                "application_link",
                "application_handoff",
            }:
                continue
            detail = flag.get("application_detail") or flag.get("applicationDetail")
            if isinstance(detail, dict):
                details.append(detail)
            related = flag.get("related_application") or flag.get("relatedApplication")
            if isinstance(related, dict):
                details.append(related)
        return details

    def _resolve_standard_adjustment_days(
        self,
        claim: ExpenseClaim,
        item: ExpenseClaimItem,
    ) -> int:
        # 关联申请由服务端持久化，优先于本次请求里可被篡改的 application_days。
        for detail in self._iter_standard_adjustment_application_details(claim):
            for key in ("application_days", "applicationDays", "days"):
                detail_days = self._normalize_standard_adjustment_days(detail.get(key))
                if detail_days is not None:
                    return detail_days

        candidates = [
            item.item_reason,
            item.item_note,
            claim.reason,
        ]
        for candidate_text in candidates:
            match = re.search(r"(\d{1,3})\s*(?:天|晚|夜)", str(candidate_text or ""))
            if match:
                days = self._normalize_standard_adjustment_days(match.group(1))
                if days is not None:
                    return days
        return 1

    def _resolve_standard_adjustment_location(
        self,
        claim: ExpenseClaim,
        item: ExpenseClaimItem,
    ) -> str:
        for value in (item.item_location, claim.location):
            text = self._normalize_standard_adjustment_text(value)
            if text:
                return text

        for detail in self._iter_standard_adjustment_application_details(claim):
            for key in ("application_location", "applicationLocation", "location", "city"):
                text = self._normalize_standard_adjustment_text(detail.get(key))
                if text:
                    return text
        return ""

    def _resolve_policy_standard_reimbursable_amount(
        self,
        *,
        claim: ExpenseClaim,
        item: ExpenseClaimItem,
        current_user: CurrentUserContext,
    ) -> dict[str, Any] | None:
        item_type = str(item.item_type or "").strip().lower()
        if item_type not in {"hotel", "hotel_ticket"}:
            return None

        location = self._resolve_standard_adjustment_location(claim, item)
        grade = str(claim.employee_grade or current_user.grade or "").strip()
        if not location or not grade:
            return None

        try:
            from app.services.travel_reimbursement_calculator import (
                TravelReimbursementCalculatorService,
            )

            result = TravelReimbursementCalculatorService(self.db).calculate(
                TravelReimbursementCalculatorRequest(
                    days=self._resolve_standard_adjustment_days(claim, item),
                    location=location,
                    grade=grade,
                ),
                current_user,
            )
        except ValueError:
            return None

        policy_amount = self._normalize_standard_adjustment_amount(result.hotel_amount)
        if policy_amount is None or policy_amount <= Decimal("0.00"):
            return None
        rule_name = str(result.rule_name or "公司差旅费报销规则").strip()
        published_rule_version = str(result.rule_version or "").strip()
        version_material = {
            "rule_name": rule_name,
            "days": int(result.days),
            "location": str(result.location or location).strip(),
            "matched_city": str(result.matched_city or "").strip(),
            "grade": str(result.grade or grade).strip(),
            "grade_band": str(result.grade_band or "").strip(),
            "hotel_rate": self._format_adjustment_money(Decimal(result.hotel_rate)),
            "hotel_amount": self._format_adjustment_money(policy_amount),
        }
        calculated_version = (
            "content-sha256:"
            + hashlib.sha256(
                json.dumps(
                    version_material,
                    ensure_ascii=False,
                    sort_keys=True,
                    separators=(",", ":"),
                ).encode("utf-8")
            ).hexdigest()[:16]
        )
        return {
            "amount": policy_amount,
            "days": int(result.days),
            "location": str(result.location or location).strip(),
            "matched_city": str(result.matched_city or "").strip(),
            "grade": str(result.grade or grade).strip(),
            "grade_band": str(result.grade_band or "").strip(),
            "grade_band_label": str(result.grade_band_label or "").strip(),
            "hotel_rate": self._normalize_standard_adjustment_amount(result.hotel_rate),
            "hotel_amount": policy_amount,
            "rule_name": rule_name,
            "rule_version": published_rule_version or calculated_version,
            "rule_version_source": "published" if published_rule_version else "content_fingerprint",
        }

    @staticmethod
    def _standard_adjustment_timestamp(value: datetime | None) -> datetime | None:
        if value is None:
            return None
        if value.tzinfo is None:
            return value.replace(tzinfo=UTC)
        return value.astimezone(UTC)

    def _ensure_standard_adjustment_version(
        self,
        claim: ExpenseClaim,
        expected_updated_at: datetime | None,
    ) -> None:
        if expected_updated_at is None:
            return
        expected = self._standard_adjustment_timestamp(expected_updated_at)
        actual = self._standard_adjustment_timestamp(claim.updated_at)
        if actual is None or expected != actual:
            raise ValueError("报销单内容已被其他操作更新，请刷新页面后重新确认报销标准。")

    @staticmethod
    def _standard_adjustment_request_fingerprint(
        *,
        claim_id: str,
        item_ids: list[str],
    ) -> str:
        material = {
            "action": "standard_adjustment_accept",
            "claim_id": str(claim_id),
            "item_ids": sorted(item_ids),
        }
        return (
            "sha256:"
            + hashlib.sha256(
                json.dumps(material, sort_keys=True, separators=(",", ":")).encode("utf-8")
            ).hexdigest()
        )

    @staticmethod
    def _standard_adjustment_calculation_fingerprint(
        *,
        item_id: str,
        original_amount: Decimal,
        reimbursable_amount: Decimal,
        policy_result: dict[str, Any],
    ) -> str:
        material = {
            "item_id": item_id,
            "original_amount": f"{original_amount:.2f}",
            "reimbursable_amount": f"{reimbursable_amount:.2f}",
            "policy_days": int(policy_result["days"]),
            "policy_location": str(policy_result["location"]),
            "policy_matched_city": str(policy_result["matched_city"]),
            "policy_grade": str(policy_result["grade"]),
            "policy_grade_band": str(policy_result["grade_band"]),
            "policy_hotel_rate": f"{Decimal(policy_result['hotel_rate']):.2f}",
            "policy_hotel_amount": f"{Decimal(policy_result['hotel_amount']):.2f}",
            "policy_rule_name": str(policy_result["rule_name"]),
            "policy_rule_version": str(policy_result["rule_version"]),
        }
        return (
            "sha256:"
            + hashlib.sha256(
                json.dumps(
                    material,
                    ensure_ascii=False,
                    sort_keys=True,
                    separators=(",", ":"),
                ).encode("utf-8")
            ).hexdigest()
        )

    @staticmethod
    def _standard_adjustment_flags_by_item(claim: ExpenseClaim) -> dict[str, dict[str, Any]]:
        flags: dict[str, dict[str, Any]] = {}
        for flag in list(claim.risk_flags_json or []):
            if not isinstance(flag, dict):
                continue
            if str(flag.get("source") or "").strip() != STANDARD_ADJUSTMENT_RISK_SOURCE:
                continue
            item_id = str(flag.get("item_id") or "").strip()
            if item_id:
                flags[item_id] = flag
        return flags

    def _is_standard_adjustment_request_replay(
        self,
        *,
        claim: ExpenseClaim,
        request_id: str | None,
        request_fingerprint: str,
        item_ids: set[str],
    ) -> bool:
        if not request_id:
            return False
        matched_flags = [
            flag
            for flag in self._standard_adjustment_flags_by_item(claim).values()
            if str(flag.get("request_id") or "").strip() == request_id
        ]
        if not matched_flags:
            return False
        fingerprints = {
            str(flag.get("request_fingerprint") or "").strip() for flag in matched_flags
        }
        matched_item_ids = {
            str(flag.get("item_id") or "").strip()
            for flag in matched_flags
            if str(flag.get("item_id") or "").strip()
        }
        if fingerprints != {request_fingerprint} or matched_item_ids != item_ids:
            raise ValueError("该 request_id 已用于另一组报销标准调整，请生成新的 request_id。")
        return True

    def _resolve_standard_adjustment_reimbursable_amount(
        self,
        *,
        claim: ExpenseClaim,
        item: ExpenseClaimItem,
        original_amount: Decimal,
        current_user: CurrentUserContext,
    ) -> tuple[Decimal, dict[str, Any]] | None:
        policy_result = self._resolve_policy_standard_reimbursable_amount(
            claim=claim,
            item=item,
            current_user=current_user,
        )
        if policy_result is None:
            return None
        policy_amount = Decimal(policy_result["amount"]).quantize(Decimal("0.01"))
        return (
            min(max(policy_amount, Decimal("0.00")), original_amount),
            policy_result,
        )

    def accept_standard_adjustment(
        self,
        *,
        claim_id: str,
        payload: ExpenseClaimStandardAdjustmentPayload,
        current_user: CurrentUserContext,
    ) -> ExpenseClaim | None:
        try:
            with self._serialize_standard_adjustment(
                claim_id=claim_id,
                current_user=current_user,
            ):
                claim = self._accept_standard_adjustment_once(
                    claim_id=claim_id,
                    payload=payload,
                    current_user=current_user,
                )
                if claim is None:
                    # 只读未命中也要结束事务，避免 PostgreSQL advisory/row lock
                    # 留到 Session 关闭时才释放。
                    self.db.rollback()
                return claim
        except Exception:
            self.db.rollback()
            raise

    def _complete_standard_adjustment_replay(self, claim: ExpenseClaim) -> ExpenseClaim:
        """结束只读幂等重放事务，确保数据库锁立即释放。"""

        self.db.commit()
        self.db.refresh(claim)
        return self._access_policy.attach_approval_snapshot(claim)

    def _accept_standard_adjustment_once(
        self,
        *,
        claim_id: str,
        payload: ExpenseClaimStandardAdjustmentPayload,
        current_user: CurrentUserContext,
    ) -> ExpenseClaim | None:
        locked_claim = self._get_locked_standard_adjustment_claim(
            claim_id=claim_id,
            current_user=current_user,
        )
        if locked_claim is None:
            return None
        claim, item_map = locked_claim

        self._ensure_draft_claim(claim)
        if self._is_expense_application_claim(claim):
            raise ValueError("费用申请单不支持按报销标准重算。")

        risk_entries = list(payload.risks or [])
        if not risk_entries:
            raise ValueError("请至少选择一条需要按职级标准重算的风险。")

        entries_by_item: dict[str, Any] = {}
        for entry in risk_entries:
            item_id = str(entry.item_id or "").strip()
            if not item_id:
                raise ValueError("需要按报销标准重算的风险缺少费用明细标识。")
            entries_by_item.setdefault(item_id, entry)
        selected_item_ids = set(entries_by_item)
        missing_item_ids = sorted(selected_item_ids - set(item_map))
        if missing_item_ids:
            raise ValueError("费用明细已变化，请刷新页面后重新选择需要调整的风险。")

        request_fingerprint = self._standard_adjustment_request_fingerprint(
            claim_id=claim.id,
            item_ids=list(selected_item_ids),
        )
        request_id = str(payload.request_id or "").strip() or None
        if self._is_standard_adjustment_request_replay(
            claim=claim,
            request_id=request_id,
            request_fingerprint=request_fingerprint,
            item_ids=selected_item_ids,
        ):
            return self._complete_standard_adjustment_replay(claim)
        self._ensure_standard_adjustment_version(claim, payload.expected_updated_at)

        before_json = self._serialize_claim(claim)
        now_text = datetime.now(UTC).isoformat()
        adjustment_flags: list[dict[str, Any]] = []
        unavailable_item_labels: list[str] = []

        for index, (item_id, entry) in enumerate(entries_by_item.items(), start=1):
            item = item_map.get(item_id)
            if item is None:  # pragma: no cover - item map was validated above
                raise ValueError("费用明细已变化，请刷新页面后重试。")

            original_amount = self._normalize_standard_adjustment_amount(item.item_amount)
            if original_amount is None:
                raise ValueError("费用明细金额异常，无法按服务端报销标准重算。")
            item_label = (
                str(item.item_reason or "").strip()
                or str(entry.title or "").strip()
                or f"费用明细第 {index} 条"
            )
            adjustment_result = self._resolve_standard_adjustment_reimbursable_amount(
                claim=claim,
                item=item,
                original_amount=original_amount,
                current_user=current_user,
            )
            if adjustment_result is None:
                unavailable_item_labels.append(item_label)
                continue
            reimbursable_amount, policy_result = adjustment_result
            employee_absorbed_amount = (original_amount - reimbursable_amount).quantize(
                Decimal("0.01")
            )
            source_risk = str(entry.risk or entry.title or "原风险未补充异常说明").strip()
            message = (
                f"提交人已选择按职级最高报销标准审核：{item_label} 原票据金额 "
                f"{self._format_adjustment_money(original_amount)} 元，实际报销金额 "
                f"{self._format_adjustment_money(reimbursable_amount)} 元，超出 "
                f"{self._format_adjustment_money(employee_absorbed_amount)} 元由员工自行承担。"
            )
            adjustment_flags.append(
                with_risk_business_stage(
                    {
                        "source": STANDARD_ADJUSTMENT_RISK_SOURCE,
                        "event_type": "standard_adjustment_accepted",
                        "severity": "medium",
                        "label": "接受职级标准审核",
                        "title": "提交人接受职级最高报销标准",
                        "message": message,
                        "summary": (
                            "提交人未补充异常说明，已选择按职级最高报销标准重算实际报销金额。"
                        ),
                        "suggestion": (
                            "领导和财务审批时请确认该差额由员工自行承担，并按实际报销金额入账。"
                        ),
                        "risk_id": str(entry.risk_id or "").strip(),
                        "source_risk": source_risk,
                        "item_id": item_id,
                        "original_amount": self._format_adjustment_money(original_amount),
                        "reimbursable_amount": self._format_adjustment_money(reimbursable_amount),
                        "employee_absorbed_amount": self._format_adjustment_money(
                            employee_absorbed_amount
                        ),
                        "calculation_source": "server_policy",
                        "policy_days": int(policy_result["days"]),
                        "policy_location": str(policy_result["location"]),
                        "policy_matched_city": str(policy_result["matched_city"]),
                        "policy_grade": str(policy_result["grade"]),
                        "policy_grade_band": str(policy_result["grade_band"]),
                        "policy_grade_band_label": str(policy_result["grade_band_label"]),
                        "policy_hotel_rate": self._format_adjustment_money(
                            Decimal(policy_result["hotel_rate"] or Decimal("0.00"))
                        ),
                        "policy_hotel_amount": self._format_adjustment_money(
                            Decimal(policy_result["hotel_amount"] or Decimal("0.00"))
                        ),
                        "policy_rule_name": str(policy_result["rule_name"]),
                        "policy_rule_version": str(policy_result["rule_version"]),
                        "policy_rule_version_source": str(policy_result["rule_version_source"]),
                        "calculation_fingerprint": (
                            self._standard_adjustment_calculation_fingerprint(
                                item_id=item_id,
                                original_amount=original_amount,
                                reimbursable_amount=reimbursable_amount,
                                policy_result=policy_result,
                            )
                        ),
                        "request_id": request_id,
                        "request_fingerprint": request_fingerprint,
                        "risk_domain": "amount",
                        "actionability": "review_decision",
                        "visibility_scope": "leader",
                        "created_at": now_text,
                    },
                    "reimbursement",
                )
            )

        if unavailable_item_labels:
            labels = "、".join(dict.fromkeys(unavailable_item_labels))
            raise ValueError(
                f"{labels} 暂未取得服务端可验证的报销标准，本次未执行金额调整；"
                "请完善职级、地点或关联申请后重试，也可以补充异常说明。"
            )

        if not adjustment_flags:
            raise ValueError("未找到可按职级标准重算的费用明细。")

        existing_by_item = self._standard_adjustment_flags_by_item(claim)
        if all(
            str(existing_by_item.get(item_id, {}).get("calculation_fingerprint") or "")
            == str(flag.get("calculation_fingerprint") or "")
            for item_id, flag in (
                (str(flag.get("item_id") or ""), flag) for flag in adjustment_flags
            )
        ):
            return self._complete_standard_adjustment_replay(claim)

        preserved_flags = [
            flag
            for flag in list(claim.risk_flags_json or [])
            if not (
                isinstance(flag, dict)
                and str(flag.get("source") or "").strip() == STANDARD_ADJUSTMENT_RISK_SOURCE
                and str(flag.get("item_id") or "").strip() in selected_item_ids
            )
        ]
        claim.risk_flags_json = dedupe_claim_risk_flags([*preserved_flags, *adjustment_flags])
        self._sync_claim_from_items(claim)
        self.refresh_claim_pre_review_state(claim, is_application_claim=False)

        SavingsDiscoveryService(self.db).discover_standard_adjustments(
            claim=claim,
            items_by_id=item_map,
            adjustment_flags=adjustment_flags,
            current_user=current_user,
            request_id=request_id,
        )

        self.audit_service.log_action(
            actor=current_user.name or current_user.username,
            action="expense_claim.standard_adjustment_accept",
            resource_type="expense_claim",
            resource_id=claim.id,
            before_json=before_json,
            after_json=self._serialize_claim(claim),
            request_id=request_id,
            commit=False,
        )

        self.db.commit()
        self.db.refresh(claim)

        return claim
