from __future__ import annotations

from decimal import Decimal, InvalidOperation
from typing import Any

from app.models.financial_record import ExpenseClaim


class BudgetExpenseControlModel:
    """预算费用管控模型：用预算容量、单据影响、规则动作和信息完整度给出建议。"""

    def assess(self, budget_context: dict[str, Any], claim: ExpenseClaim | None = None) -> dict[str, Any]:
        context = dict(budget_context or {})
        amount = self._money(context.get("claim_amount"))
        if not context.get("budget_applicable", True):
            return self._build_result(
                context=context,
                score=72,
                rating="reference",
                risk_level="low",
                summary="该费用类型暂未纳入预算强管控，本次仅作为申请合理性参考。",
                basis=["当前费用科目未启用预算控制。"],
                suggestions=["预算管理者可结合项目必要性和历史同类费用进行人工判断。"],
            )
        if not context.get("matched"):
            return self._build_result(
                context=context,
                score=38,
                rating="block",
                risk_level="high",
                summary="未匹配到可用预算池，建议先完成预算编制或调整预算维度后再审批。",
                basis=["系统按部门、成本中心、项目和费用科目未找到匹配预算额度。"],
                suggestions=["请预算编制者补充对应预算池，或核对申请部门、项目和费用类型是否填写正确。"],
            )

        total = self._money(context.get("total_amount"))
        reserved = self._money(context.get("reserved_amount"))
        consumed = self._money(context.get("consumed_amount"))
        current_reserved = self._money(context.get("current_reserved_amount"))
        warning_threshold = self._money(context.get("warning_threshold") or "80")
        used_before = max(reserved + consumed - current_reserved, Decimal("0.00"))
        available_before = max(total - used_before, Decimal("0.00"))
        after_used = used_before + amount
        claim_ratio = self._percent(amount, total)
        after_usage_rate = self._percent(after_used, total)
        over_budget_amount = max(amount - available_before, Decimal("0.00"))

        score = 100
        basis = [
            f"预算池 {context.get('budget_no') or '未命名'} 总额度 {self._fmt(total)} 元。",
            f"本次申请金额 {self._fmt(amount)} 元，占预算 {self._fmt(claim_ratio)}%。",
            f"审批后预算使用率预计 {self._fmt(after_usage_rate)}%，预警线 {self._fmt(warning_threshold)}%。",
        ]
        suggestions: list[str] = []

        if over_budget_amount > Decimal("0.00"):
            score -= 55
            basis.append(f"按当前预算余额测算，本次申请将超出预算 {self._fmt(over_budget_amount)} 元。")
            suggestions.append("建议先追加或调剂预算，再允许申请继续流转。")
        elif after_usage_rate >= Decimal("100.00"):
            score -= 38
            basis.append("审批后预算使用率将达到或超过 100%。")
            suggestions.append("建议预算管理者复核剩余额度，并确认是否需要预算调剂。")
        elif after_usage_rate >= warning_threshold:
            score -= 20
            basis.append("审批后预算使用率将触达预算预警线。")
            suggestions.append("建议关注后续同类费用，必要时提前调整预算节奏。")
        elif after_usage_rate >= Decimal("70.00"):
            score -= 8
            basis.append("审批后预算使用率较高，但尚未触达预警线。")

        if claim_ratio >= Decimal("50.00"):
            score -= 20
            basis.append("单笔申请占预算比例超过 50%，对预算池影响较大。")
            suggestions.append("建议补充业务必要性、交付范围和费用拆分依据。")
        elif claim_ratio >= Decimal("30.00"):
            score -= 12
            basis.append("单笔申请占预算比例超过 30%，需要关注预算节奏。")
        elif claim_ratio >= Decimal("15.00"):
            score -= 5
            basis.append("单笔申请占预算比例超过 15%，属于中等预算影响。")

        missing_fields = self._collect_context_gaps(claim)
        if missing_fields:
            score -= min(12, len(missing_fields) * 4)
            basis.append(f"申请信息仍缺少：{'、'.join(missing_fields)}。")
            suggestions.append("建议申请人补齐业务背景，便于预算管理者判断费用必要性。")

        control_action = str(context.get("control_action") or "").strip().lower()
        if control_action == "block" and over_budget_amount > Decimal("0.00"):
            suggestions.append("该预算池为硬控制口径，超预算时不建议直接通过。")

        score = max(0, min(100, int(round(score))))
        rating, risk_level = self._rate(score, over_budget_amount)
        if not suggestions:
            suggestions.append("预算额度与本次费用影响基本匹配，可以结合业务必要性继续审批。")

        return self._build_result(
            context={
                **context,
                "claim_amount_ratio": str(claim_ratio),
                "after_usage_rate": str(after_usage_rate),
                "available_before_amount": str(available_before),
                "over_budget_amount": str(over_budget_amount),
            },
            score=score,
            rating=rating,
            risk_level=risk_level,
            summary=self._summary(rating),
            basis=basis,
            suggestions=suggestions,
        )

    @staticmethod
    def _collect_context_gaps(claim: ExpenseClaim | None) -> list[str]:
        if claim is None:
            return []
        gaps = []
        if not str(claim.reason or "").strip():
            gaps.append("申请事由")
        if not str(claim.location or "").strip():
            gaps.append("地点")
        if not str(claim.project_code or "").strip():
            gaps.append("项目编号")
        return gaps

    @staticmethod
    def _rate(score: int, over_budget_amount: Decimal) -> tuple[str, str]:
        if over_budget_amount > Decimal("0.00") or score < 50:
            return "block", "high"
        if score < 70:
            return "review", "medium"
        if score < 85:
            return "caution", "medium"
        return "recommended", "low"

    @staticmethod
    def _summary(rating: str) -> str:
        summaries = {
            "recommended": "预算容量充足，单据费用与当前预算节奏基本匹配。",
            "caution": "预算整体可承接，但本次费用对预算池已有一定影响。",
            "review": "预算影响偏高，建议预算管理者结合业务必要性复核后再通过。",
            "block": "预算风险较高，不建议在未补充依据或调整预算前直接通过。",
        }
        return summaries.get(rating, "已完成预算费用合理性测算。")

    @staticmethod
    def _build_result(
        *,
        context: dict[str, Any],
        score: int,
        rating: str,
        risk_level: str,
        summary: str,
        basis: list[str],
        suggestions: list[str],
    ) -> dict[str, Any]:
        return {
            "budget_context": context,
            "score": score,
            "rating": rating,
            "risk_level": risk_level,
            "summary": summary,
            "basis": basis,
            "suggestions": suggestions,
            "metrics": {
                "claim_amount": context.get("claim_amount"),
                "total_amount": context.get("total_amount"),
                "claim_amount_ratio": context.get("claim_amount_ratio", "0.00"),
                "usage_rate": context.get("usage_rate", "0.00"),
                "after_usage_rate": context.get("after_usage_rate", context.get("usage_rate", "0.00")),
                "available_amount": context.get("available_amount"),
                "available_before_amount": context.get("available_before_amount"),
                "over_budget_amount": context.get("over_budget_amount", "0.00"),
            },
        }

    @staticmethod
    def _money(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")

    @staticmethod
    def _percent(numerator: Decimal, denominator: Decimal) -> Decimal:
        if denominator <= Decimal("0.00"):
            return Decimal("0.00")
        return ((numerator / denominator) * Decimal("100")).quantize(Decimal("0.01"))

    @staticmethod
    def _fmt(value: Decimal) -> str:
        return f"{value.quantize(Decimal('0.01'))}"

