from __future__ import annotations

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

from sqlalchemy import select
from sqlalchemy.orm import Session

from app.api.deps import CurrentUserContext
from app.models.financial_record import ExpenseClaim
from app.models.risk_disposition import RiskDisposition
from app.models.risk_observation import RiskObservation
from app.schemas.approval_workbench import (
    ApprovalWorkbenchEvidenceRead,
    ApprovalWorkbenchItemRead,
    ApprovalWorkbenchListRead,
    ApprovalWorkbenchPriorityReasonRead,
    ApprovalWorkbenchSuggestionRead,
)
from app.schemas.reimbursement import ExpenseClaimRead
from app.services.expense_claim_risk_flags import (
    claim_risk_flag_observation_key,
    claim_risk_flag_severity,
    is_open_claim_risk_flag,
)
from app.services.expense_claim_tenant_scope import ExpenseClaimTenantScopeMixin

_SLA_HOURS = 24
_RISK_WEIGHTS = {"critical": 38, "high": 30, "medium": 14, "low": 0}
_RISK_ORDER = {"low": 0, "medium": 1, "high": 2, "critical": 3}
_RESOLVED_STATUSES = {"resolved", "accepted", "waived", "false_positive"}


class ApprovalWorkbenchService:
    """构建只读、可解释且不扩大审批权限的例外审批队列。"""

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

    def list_items(
        self,
        current_user: CurrentUserContext,
        *,
        limit: int = 100,
        now: datetime | None = None,
    ) -> ApprovalWorkbenchListRead:
        # 延迟导入，避免 ExpenseClaimService 的 mixin 聚合产生循环依赖。
        from app.services.expense_claims import ExpenseClaimService

        generated_at = now or datetime.now(UTC)
        claims = ExpenseClaimService(self.db).list_approval_claims(current_user)
        observations_by_claim = self._risk_observations_by_claim(
            claims,
            tenant_id=current_user.tenant_id,
        )
        items = [
            self.build_item(
                claim,
                now=generated_at,
                observation_rows=observations_by_claim.get(claim.id),
            )
            for claim in claims
        ]
        items.sort(
            key=lambda item: (
                -item.priority_score,
                -item.waiting_hours,
                item.claim.claim_no,
            )
        )
        return ApprovalWorkbenchListRead(
            items=items[: max(1, min(int(limit), 200))],
            total=len(items),
            generated_at=generated_at,
        )

    @classmethod
    def build_item(
        cls,
        claim: ExpenseClaim,
        *,
        now: datetime | None = None,
        observation_rows: list[tuple[RiskObservation, RiskDisposition | None]] | None = None,
    ) -> ApprovalWorkbenchItemRead:
        generated_at = now or datetime.now(UTC)
        submitted_at = cls._aware(claim.submitted_at or claim.created_at)
        waiting_hours = max(
            0.0,
            (generated_at - submitted_at).total_seconds() / 3600,
        )
        sla_due_at = submitted_at + timedelta(hours=_SLA_HOURS)
        risk_level, open_risk_count = cls._combined_risk_summary(
            claim,
            observation_rows or [],
        )
        budget_usage_rate = cls._budget_usage_rate(claim.risk_flags_json)
        evidence = cls._evidence_summary(claim)
        reasons = cls._priority_reasons(
            claim=claim,
            risk_level=risk_level,
            open_risk_count=open_risk_count,
            budget_usage_rate=budget_usage_rate,
            waiting_hours=waiting_hours,
            evidence=evidence,
        )
        score = min(100, sum(item.weight for item in reasons))
        tier = "urgent" if score >= 65 else "high" if score >= 40 else "normal"
        return ApprovalWorkbenchItemRead(
            task_key=f"{claim.id}:{claim.approval_stage or ''}:{submitted_at.isoformat()}",
            claim=cls._safe_claim_read(claim),
            priority_score=score,
            priority_tier=tier,
            priority_reasons=reasons,
            risk_level=risk_level,
            open_risk_count=open_risk_count,
            budget_usage_rate=budget_usage_rate,
            waiting_hours=round(waiting_hours, 2),
            sla_due_at=sla_due_at,
            sla_overdue=generated_at >= sla_due_at,
            evidence=evidence,
            suggestion=cls._suggestion(
                risk_level=risk_level,
                open_risk_count=open_risk_count,
                budget_usage_rate=budget_usage_rate,
                evidence=evidence,
            ),
        )

    def _risk_observations_by_claim(
        self,
        claims: list[ExpenseClaim],
        *,
        tenant_id: str | None,
    ) -> dict[str, list[tuple[RiskObservation, RiskDisposition | None]]]:
        claim_ids = [str(claim.id) for claim in claims if str(claim.id or "").strip()]
        if not claim_ids:
            return {}
        normalized_tenant = ExpenseClaimTenantScopeMixin.normalize_tenant_id(tenant_id)
        rows = self.db.execute(
            select(RiskObservation, RiskDisposition)
            .outerjoin(
                RiskDisposition,
                (
                    (RiskDisposition.tenant_id == RiskObservation.tenant_id)
                    & (RiskDisposition.observation_id == RiskObservation.id)
                ),
            )
            .where(
                RiskObservation.tenant_id == normalized_tenant,
                RiskObservation.claim_id.in_(claim_ids),
            )
        ).all()
        grouped: dict[str, list[tuple[RiskObservation, RiskDisposition | None]]] = {}
        for observation, disposition in rows:
            grouped.setdefault(str(observation.claim_id), []).append((observation, disposition))
        return grouped

    @classmethod
    def _safe_claim_read(cls, claim: ExpenseClaim) -> ExpenseClaimRead:
        claim_read = ExpenseClaimRead.model_validate(claim)
        claim_read.risk_flags_json = [
            cls._safe_risk_flag(flag)
            for flag in list(claim_read.risk_flags_json or [])
            if isinstance(flag, dict)
        ]
        return claim_read

    @staticmethod
    def _safe_risk_flag(flag: dict[str, Any]) -> dict[str, Any]:
        safe_flag = dict(flag)
        if "historical_case_evidence" not in safe_flag:
            return safe_flag
        evidence: list[dict[str, Any]] = []
        for item in list(safe_flag.get("historical_case_evidence") or []):
            if not isinstance(item, dict):
                continue
            label = str(item.get("label") or "").strip().lower()
            if label not in {"confirmed", "false_positive"}:
                continue
            evidence.append(
                {
                    "label": label,
                    "label_text": (
                        "历史已确认，仅供复核" if label == "confirmed" else "历史误报，仅供复核"
                    ),
                    "advisory_only": True,
                    "stale": bool(item.get("stale")),
                    "summary": (
                        "历史相似案例经人工复核确认风险成立。"
                        if label == "confirmed"
                        else "历史相似案例经人工复核判定为误报。"
                    ),
                }
            )
        safe_flag["historical_case_evidence"] = evidence
        return safe_flag

    @classmethod
    def _priority_reasons(
        cls,
        *,
        claim: ExpenseClaim,
        risk_level: str,
        open_risk_count: int,
        budget_usage_rate: float | None,
        waiting_hours: float,
        evidence: ApprovalWorkbenchEvidenceRead,
    ) -> list[ApprovalWorkbenchPriorityReasonRead]:
        reasons: list[ApprovalWorkbenchPriorityReasonRead] = []
        risk_weight = _RISK_WEIGHTS[risk_level]
        if risk_weight:
            reasons.append(
                ApprovalWorkbenchPriorityReasonRead(
                    code="open_risk",
                    label=f"{risk_level.upper()} 风险 {open_risk_count} 项待复核",
                    weight=risk_weight,
                    tone="danger" if risk_level in {"high", "critical"} else "warning",
                )
            )
        if waiting_hours >= _SLA_HOURS:
            reasons.append(
                ApprovalWorkbenchPriorityReasonRead(
                    code="sla_overdue",
                    label="已超过 24 小时审批 SLA",
                    weight=26,
                    tone="danger",
                )
            )
        elif waiting_hours >= 16:
            reasons.append(
                ApprovalWorkbenchPriorityReasonRead(
                    code="sla_near_due",
                    label="审批 SLA 即将到期",
                    weight=16,
                    tone="warning",
                )
            )
        elif waiting_hours >= 8:
            reasons.append(
                ApprovalWorkbenchPriorityReasonRead(
                    code="waiting",
                    label="等待时间已超过 8 小时",
                    weight=8,
                    tone="warning",
                )
            )
        if budget_usage_rate is not None and budget_usage_rate >= 90:
            reasons.append(
                ApprovalWorkbenchPriorityReasonRead(
                    code="budget_pressure",
                    label=f"审批后预算占用约 {budget_usage_rate:.0f}%",
                    weight=18,
                    tone="danger" if budget_usage_rate >= 100 else "warning",
                )
            )
        amount = cls._decimal(claim.amount)
        if amount >= Decimal("50000"):
            reasons.append(
                ApprovalWorkbenchPriorityReasonRead(
                    code="large_amount",
                    label="大额费用需重点核对",
                    weight=15,
                    tone="warning",
                )
            )
        elif amount >= Decimal("10000"):
            reasons.append(
                ApprovalWorkbenchPriorityReasonRead(
                    code="amount_attention",
                    label="金额超过 1 万元",
                    weight=8,
                    tone="warning",
                )
            )
        if evidence.completeness < 1:
            reasons.append(
                ApprovalWorkbenchPriorityReasonRead(
                    code="evidence_gap",
                    label="材料仍有缺口：" + "、".join(evidence.missing_labels),
                    weight=12,
                    tone="warning",
                )
            )
        if not reasons:
            reasons.append(
                ApprovalWorkbenchPriorityReasonRead(
                    code="routine",
                    label="常规低风险待办",
                )
            )
        return reasons

    @classmethod
    def _risk_summary(
        cls,
        raw_flags: Any,
        *,
        claim_id: str = "",
        materialized_keys: set[str] | None = None,
    ) -> tuple[str, int]:
        flags = raw_flags if isinstance(raw_flags, list) else [raw_flags]
        persisted_keys = materialized_keys or set()
        level = "low"
        count = 0
        for flag in flags:
            if not isinstance(flag, dict) or not is_open_claim_risk_flag(flag):
                continue
            observation_key = claim_risk_flag_observation_key(flag, claim_id=claim_id)
            if observation_key and observation_key in persisted_keys:
                continue
            candidate = claim_risk_flag_severity(flag) or "medium"
            if candidate not in _RISK_ORDER:
                candidate = "medium"
            count += 1
            if _RISK_ORDER[candidate] > _RISK_ORDER[level]:
                level = candidate
        return level, count

    @classmethod
    def _combined_risk_summary(
        cls,
        claim: ExpenseClaim,
        rows: list[tuple[RiskObservation, RiskDisposition | None]],
    ) -> tuple[str, int]:
        persisted_level, persisted_count = cls._persisted_risk_summary(rows)
        materialized_keys = {
            str(observation.observation_key or "").strip()
            for observation, _disposition in rows
            if str(observation.observation_key or "").strip()
        }
        raw_level, raw_count = cls._risk_summary(
            claim.risk_flags_json,
            claim_id=str(claim.id or ""),
            materialized_keys=materialized_keys,
        )
        return (
            max((persisted_level, raw_level), key=lambda item: _RISK_ORDER[item]),
            persisted_count + raw_count,
        )

    @staticmethod
    def _persisted_risk_summary(
        rows: list[tuple[RiskObservation, RiskDisposition | None]],
    ) -> tuple[str, int]:
        level = "low"
        count = 0
        for observation, disposition in rows:
            adjudication = (
                str(disposition.adjudication or "").strip().lower()
                if disposition is not None
                else str(observation.feedback_status or "").strip().lower()
            )
            lifecycle = (
                str(disposition.lifecycle_status or "").strip().lower()
                if disposition is not None
                else "open"
            )
            status = str(observation.status or "").strip().lower()
            if (
                adjudication == "false_positive"
                or lifecycle == "resolved"
                or status in _RESOLVED_STATUSES
            ):
                continue
            candidate = str(observation.risk_level or "medium").strip().lower()
            if candidate == "danger":
                candidate = "high"
            if candidate not in _RISK_ORDER:
                candidate = "medium"
            count += 1
            if _RISK_ORDER[candidate] > _RISK_ORDER[level]:
                level = candidate
        return level, count

    @classmethod
    def _budget_usage_rate(cls, raw_flags: Any) -> float | None:
        values: list[float] = []
        flags = raw_flags if isinstance(raw_flags, list) else [raw_flags]
        for flag in flags:
            if not isinstance(flag, dict):
                continue
            root_containers = [
                flag,
                flag.get("metrics"),
                flag.get("budget_result"),
                (flag.get("route_decision") or {}).get("budget_result")
                if isinstance(flag.get("route_decision"), dict)
                else None,
            ]
            containers = [item for item in root_containers if isinstance(item, dict)]
            containers.extend(
                item["metrics"]
                for item in list(containers)
                if isinstance(item.get("metrics"), dict)
            )
            for container in containers:
                for key in (
                    "after_usage_rate",
                    "budget_usage_rate",
                    "usage_rate",
                    "utilization_rate",
                ):
                    value = cls._float(container.get(key))
                    if value is None:
                        continue
                    values.append(value * 100 if 0 < value <= 1 else value)
        return round(max(values), 2) if values else None

    @classmethod
    def _evidence_summary(cls, claim: ExpenseClaim) -> ApprovalWorkbenchEvidenceRead:
        is_application = cls._is_application(claim)
        checks = [
            ("事由", bool(str(claim.reason or "").strip())),
            ("地点", bool(str(claim.location or "").strip())),
            ("费用明细", bool(list(claim.items or []))),
        ]
        if not is_application:
            checks.append(
                (
                    "票据",
                    int(claim.invoice_count or 0) > 0
                    or any(str(item.invoice_id or "").strip() for item in claim.items or []),
                )
            )
        missing = [label for label, present in checks if not present]
        historical_labels = cls._historical_labels(claim.risk_flags_json)
        present_count = len(checks) - len(missing)
        return ApprovalWorkbenchEvidenceRead(
            completeness=round(present_count / len(checks), 4) if checks else 1,
            present_count=present_count,
            required_count=len(checks),
            missing_labels=missing,
            historical_labels=historical_labels,
        )

    @staticmethod
    def _historical_labels(raw_flags: Any) -> list[str]:
        result: list[str] = []
        flags = raw_flags if isinstance(raw_flags, list) else [raw_flags]
        for flag in flags:
            if not isinstance(flag, dict):
                continue
            for item in list(flag.get("historical_case_evidence") or []):
                if not isinstance(item, dict):
                    continue
                label = str(item.get("label") or "").strip().lower()
                text = (
                    "历史已确认，仅供复核"
                    if label == "confirmed"
                    else "历史误报，仅供复核"
                    if label == "false_positive"
                    else ""
                )
                if text and text not in result:
                    result.append(text)
        return result

    @staticmethod
    def _suggestion(
        *,
        risk_level: str,
        open_risk_count: int,
        budget_usage_rate: float | None,
        evidence: ApprovalWorkbenchEvidenceRead,
    ) -> ApprovalWorkbenchSuggestionRead:
        if risk_level in {"high", "critical"} and open_risk_count:
            return ApprovalWorkbenchSuggestionRead(
                action="manual_review",
                label="先核对风险再决策",
                reason="存在高风险关注项，AI 不建议直接通过。",
            )
        if evidence.missing_labels:
            return ApprovalWorkbenchSuggestionRead(
                action="request_supplement",
                label="建议退回补充材料",
                reason="缺少" + "、".join(evidence.missing_labels) + "。",
            )
        if budget_usage_rate is not None and budget_usage_rate >= 90:
            return ApprovalWorkbenchSuggestionRead(
                action="budget_review",
                label="重点复核预算影响",
                reason="审批后预算占用已达到复核线。",
            )
        return ApprovalWorkbenchSuggestionRead(
            action="approve_candidate",
            label="可人工确认后通过",
            reason="材料相对完整且未发现高风险阻断项。",
        )

    @staticmethod
    def _is_application(claim: ExpenseClaim) -> bool:
        claim_no = str(claim.claim_no or "").strip().upper()
        expense_type = str(claim.expense_type or "").strip().lower()
        return (
            claim_no.startswith(("AP-", "APP-"))
            or expense_type == "application"
            or expense_type.endswith("_application")
        )

    @staticmethod
    def _aware(value: datetime) -> datetime:
        return value if value.tzinfo is not None else value.replace(tzinfo=UTC)

    @staticmethod
    def _decimal(value: Any) -> Decimal:
        try:
            return Decimal(str(value or "0"))
        except (InvalidOperation, ValueError):
            return Decimal("0")

    @staticmethod
    def _float(value: Any) -> float | None:
        try:
            number = float(value)
        except (TypeError, ValueError):
            return None
        return number if number >= 0 else None
