from __future__ import annotations

from datetime import UTC, datetime, timedelta
from decimal import Decimal, InvalidOperation
from typing import Any

from sqlalchemy import or_, select

from app.models.financial_record import ExpenseClaim
from app.services.budget import BudgetService
from app.services.expense_claim_constants import AI_REVIEW_LOOKBACK_DAYS
from app.services.expense_claim_risk_stage import (
    risk_business_stage_for_claim,
    risk_flag_business_stage,
    with_risk_business_stage,
)
from app.services.expense_claim_tenant_scope import ExpenseClaimTenantScopeMixin


class ExpenseClaimApprovalRoutingMixin:
    _APPLICATION_BUDGET_REVIEW_USAGE_THRESHOLD = Decimal("90.00")
    _BUDGET_REVIEW_RATINGS = {"block"}
    _BUDGET_REVIEW_RISK_LEVELS = {"high", "critical"}
    _ROUTE_RISK_SEVERITIES = {"medium", "high", "critical", "danger"}
    _ROUTE_RISK_SOURCES = {
        "attachment_analysis",
        "budget",
        "budget_control",
        "manual_return",
        "platform_risk",
        "platform_risk_rule",
        "policy_review",
        "risk_rule",
        "scene_policy",
        "submission_review",
        "travel_policy",
    }
    _ROUTE_IGNORED_SOURCES = {
        "application_detail",
        "application_handoff",
        "approval_routing",
        "budget_approval",
        "finance_approval",
        "manual_approval",
        "payment",
    }
    _ROUTE_RISK_EVENT_TYPES = {
        "budget_frozen",
        "budget_insufficient",
        "budget_missing",
        "platform_risk_rule_hit",
        "risk_rule_hit",
    }
    _BUDGET_ROUTE_EVENT_TYPES = {
        "budget_frozen",
        "budget_insufficient",
        "budget_missing",
    }

    def _build_approval_route_decision(
        self,
        claim: ExpenseClaim,
        *,
        is_application_claim: bool,
    ) -> dict[str, Any]:
        business_stage = risk_business_stage_for_claim(is_application_claim=is_application_claim)
        budget_result = BudgetService(self.db).analyze_claim_budget(claim)
        budget_reasons = (
            self._collect_application_budget_route_reasons(budget_result)
            if is_application_claim
            else self._collect_budget_route_reasons(budget_result)
        )
        current_risk_reasons = self._collect_current_route_risk_reasons(
            claim.risk_flags_json,
            business_stage=business_stage,
        )
        application_risk_reasons = (
            self._collect_application_route_risk_reasons(
                claim.risk_flags_json,
                business_stage=business_stage,
            )
            if is_application_claim
            else []
        )
        historical_risk_count = self._count_recent_substantive_risky_claims(claim)
        historical_risk_reasons = (
            [f"申请人近 {AI_REVIEW_LOOKBACK_DAYS} 天存在 {historical_risk_count} 笔实质风险记录"]
            if historical_risk_count > 0
            else []
        )
        reasons = self._dedupe_reasons(
            [*budget_reasons, *application_risk_reasons]
            if is_application_claim
            else [*budget_reasons, *current_risk_reasons, *historical_risk_reasons]
        )
        requires_budget_review = bool(reasons)
        route = (
            "budget_manager"
            if requires_budget_review
            else "approval_done"
            if is_application_claim
            else "finance"
        )
        label = "需要预算管理者复核" if requires_budget_review else "跳过预算管理者复核"
        if is_application_claim:
            message = (
                "系统根据预算占用与高风险复核结果判断，"
                "该申请单需要预算管理者二次确认。"
                if requires_budget_review
                else "系统根据预算占用与风险复核结果判断，"
                "该申请单可跳过预算管理者复核。"
            )
        else:
            message = (
                "系统根据预算、当前风险和历史风险判断，该单据需要预算管理者二次确认。"
                if requires_budget_review
                else "系统根据预算、当前风险和历史风险判断，该单据可跳过预算管理者复核。"
            )

        return with_risk_business_stage(
            {
                "source": "approval_routing",
                "event_type": (
                    "expense_application_route_decision"
                    if is_application_claim
                    else "expense_claim_route_decision"
                ),
                "severity": "medium" if requires_budget_review else "info",
                "label": label,
                "message": message,
                "requires_budget_review": requires_budget_review,
                "route": route,
                "reasons": reasons,
                "budget_result": self._compact_budget_result(budget_result),
                "current_risk_count": len(current_risk_reasons),
                "historical_risk_count": historical_risk_count,
                "created_at": datetime.now(UTC).isoformat(),
            },
            business_stage,
        )

    def _collect_budget_route_reasons(self, budget_result: dict[str, Any]) -> list[str]:
        rating = str(budget_result.get("rating") or "").strip().lower()
        risk_level = str(budget_result.get("risk_level") or "").strip().lower()
        metrics = budget_result.get("metrics") if isinstance(budget_result.get("metrics"), dict) else {}
        context = (
            budget_result.get("budget_context")
            if isinstance(budget_result.get("budget_context"), dict)
            else {}
        )
        reasons: list[str] = []
        if context.get("budget_applicable") is True and context.get("matched") is False:
            reasons.append("未匹配到可用预算池")
        if rating in self._BUDGET_REVIEW_RATINGS:
            summary = str(budget_result.get("summary") or "").strip()
            reasons.append(summary or f"预算测算评级为 {rating}")
        if risk_level in self._BUDGET_REVIEW_RISK_LEVELS:
            reasons.append(f"预算风险等级为 {risk_level}")
        over_budget_amount = self._decimal(metrics.get("over_budget_amount"))
        if over_budget_amount > Decimal("0.00"):
            reasons.append(f"预计超预算 {over_budget_amount} 元")
        return self._dedupe_reasons(reasons)

    def _collect_application_budget_route_reasons(self, budget_result: dict[str, Any]) -> list[str]:
        metrics = budget_result.get("metrics") if isinstance(budget_result.get("metrics"), dict) else {}
        over_budget_amount = self._decimal(metrics.get("over_budget_amount"))
        if over_budget_amount > Decimal("0.00"):
            return [f"预计超预算 {over_budget_amount} 元"]

        after_usage_rate = self._decimal(metrics.get("after_usage_rate"))
        claim_amount_ratio = self._decimal(metrics.get("claim_amount_ratio"))
        budget_usage_rate = max(after_usage_rate, claim_amount_ratio)
        if budget_usage_rate >= self._APPLICATION_BUDGET_REVIEW_USAGE_THRESHOLD:
            return [f"审批后预算占用达到 {budget_usage_rate}%，触发 90% 预算复核线"]

        return []

    def _collect_current_route_risk_reasons(
        self,
        risk_flags: list[Any] | None,
        *,
        business_stage: str,
    ) -> list[str]:
        reasons: list[str] = []
        for flag in list(risk_flags or []):
            if not isinstance(flag, dict):
                continue
            flag_stage = risk_flag_business_stage(flag)
            if flag_stage and flag_stage != business_stage:
                continue
            if not self._is_substantive_route_risk_flag(flag):
                continue
            label = str(flag.get("label") or flag.get("event_type") or "风险标记").strip()
            message = str(flag.get("message") or "").strip()
            reasons.append(f"{label}：{message}" if message else label)
        return self._dedupe_reasons(reasons)

    def _collect_application_route_risk_reasons(
        self,
        risk_flags: list[Any] | None,
        *,
        business_stage: str,
    ) -> list[str]:
        """申请单仅将高危关注项升级给预算管理者。

        普通预算预警由 90% 占用线决定是否升级，避免中低风险导致
        所有申请都增加一道审批。
        """

        reasons: list[str] = []
        for flag in list(risk_flags or []):
            if not isinstance(flag, dict):
                continue
            flag_stage = risk_flag_business_stage(flag)
            if flag_stage and flag_stage != business_stage:
                continue
            severity = str(flag.get("severity") or "").strip().lower()
            event_type = str(flag.get("event_type") or "").strip().lower()
            is_high_risk = severity in {"high", "critical", "danger"}
            if not is_high_risk and event_type not in self._ROUTE_RISK_EVENT_TYPES:
                continue
            if not self._is_substantive_route_risk_flag(flag):
                continue
            label = str(flag.get("label") or event_type or "风险标记").strip()
            message = str(flag.get("message") or "").strip()
            reasons.append(f"{label}：{message}" if message else label)
        return self._dedupe_reasons(reasons)

    def _count_recent_substantive_risky_claims(self, claim: ExpenseClaim) -> int:
        filters = []
        if claim.employee_id:
            filters.append(ExpenseClaim.employee_id == claim.employee_id)
        elif claim.employee_name:
            filters.append(ExpenseClaim.employee_name == claim.employee_name)
        if not filters:
            return 0

        since = datetime.now(UTC) - timedelta(days=AI_REVIEW_LOOKBACK_DAYS)
        stmt = (
            select(ExpenseClaim)
            .where(or_(*filters))
            .where(ExpenseClaim.id != claim.id)
            .where(ExpenseClaim.occurred_at >= since)
            .where(
                ExpenseClaimTenantScopeMixin.build_claim_tenant_condition(
                    ExpenseClaimTenantScopeMixin.resolve_claim_tenant_id(
                        self.db,
                        claim.id,
                    )
                )
            )
        )
        return sum(
            1
            for item in self.db.scalars(stmt).all()
            if any(
                self._is_substantive_route_risk_flag(flag)
                for flag in list(item.risk_flags_json or [])
                if isinstance(flag, dict)
            )
        )

    def _is_substantive_route_risk_flag(self, flag: dict[str, Any]) -> bool:
        resolution_status = str(
            flag.get("resolution_status") or flag.get("resolutionStatus") or ""
        ).strip().lower()
        if resolution_status in {"resolved", "accepted", "waived"} or bool(
            flag.get("resolved")
        ):
            return False
        source = str(flag.get("source") or "").strip().lower()
        if source in self._ROUTE_IGNORED_SOURCES:
            return False

        event_type = str(flag.get("event_type") or "").strip().lower()
        severity = str(flag.get("severity") or "").strip().lower()
        if source in {"budget", "budget_control"}:
            return event_type in self._BUDGET_ROUTE_EVENT_TYPES or severity in {"high", "critical", "danger"}
        if event_type in self._ROUTE_RISK_EVENT_TYPES:
            return True
        if severity in self._ROUTE_RISK_SEVERITIES:
            return source in self._ROUTE_RISK_SOURCES or bool(source)
        return source in self._ROUTE_RISK_SOURCES and bool(flag.get("triggered"))

    @staticmethod
    def _compact_budget_result(budget_result: dict[str, Any]) -> dict[str, Any]:
        return {
            "score": budget_result.get("score"),
            "rating": budget_result.get("rating"),
            "risk_level": budget_result.get("risk_level"),
            "summary": budget_result.get("summary"),
            "metrics": budget_result.get("metrics") if isinstance(budget_result.get("metrics"), dict) else {},
        }

    @staticmethod
    def _dedupe_reasons(reasons: list[str]) -> list[str]:
        deduped: list[str] = []
        seen: set[str] = set()
        for reason in reasons:
            text = str(reason or "").strip()
            if not text or text in seen:
                continue
            seen.add(text)
            deduped.append(text)
        return deduped

    @staticmethod
    def _decimal(value: Any) -> Decimal:
        try:
            return Decimal(str(value if value is not None else "0")).quantize(Decimal("0.01"))
        except (InvalidOperation, ValueError):
            return Decimal("0.00")
