from __future__ import annotations

from dataclasses import dataclass
from datetime import UTC, datetime

from sqlalchemy import select
from sqlalchemy.orm import Session

from app.models.financial_record import ExpenseClaim
from app.models.risk_disposition import RiskDisposition
from app.models.risk_observation import RiskObservation
from app.services.document_numbering import is_application_claim_no
from app.services.expense_claim_risk_flags import (
    claim_risk_flag_observation_key,
    claim_risk_flag_severity,
    is_blocking_claim_risk_flag,
)
from app.services.expense_claim_risk_stage import (
    risk_business_stage_for_claim,
    risk_flag_business_stage,
)
from app.services.expense_claim_tenant_scope import ExpenseClaimTenantScopeMixin

_BLOCKING_LEVELS = {"high", "critical", "danger"}
_LEGACY_CLOSED_STATUSES = {"false_positive", "ignored", "resolved"}


@dataclass(frozen=True, slots=True)
class BlockingRiskObservation:
    observation_id: str
    title: str
    risk_level: str
    adjudication: str
    lifecycle_status: str


class ExpenseClaimRiskBlockedError(ValueError):
    def __init__(self, blockers: list[BlockingRiskObservation]) -> None:
        self.blockers = blockers
        titles = "、".join(item.title for item in blockers[:3])
        suffix = f"等 {len(blockers)} 项" if len(blockers) > 3 else ""
        super().__init__(
            "存在尚未完成处置的高风险观察，暂不能审批通过："
            f"{titles}{suffix}。请先在风险证据链中确认并关闭风险，或标记为误报。"
        )


class ExpenseClaimRiskGate:
    """把持久化风险处置状态变成审批前的强制业务门禁。"""

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

    def find_blockers(
        self,
        claim: ExpenseClaim,
        *,
        tenant_id: str | None,
    ) -> list[BlockingRiskObservation]:
        normalized_tenant = ExpenseClaimTenantScopeMixin.normalize_tenant_id(tenant_id)
        claim_business_stage = self._claim_business_stage(claim)
        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 == claim.id,
            )
            .order_by(
                RiskObservation.risk_score.desc(),
                RiskObservation.created_at.desc(),
            )
        ).all()

        blockers: list[BlockingRiskObservation] = []
        materialized_keys: set[str] = set()
        for observation, disposition in rows:
            materialized_keys.add(str(observation.observation_key or "").strip())
            observation_stage = risk_flag_business_stage(
                {"control_stage": observation.control_stage},
                default="",
            )
            if observation_stage and observation_stage != claim_business_stage:
                continue
            level = str(observation.risk_level or "").strip().lower()
            if level not in _BLOCKING_LEVELS:
                continue
            if self._is_closed(observation, disposition):
                continue
            blockers.append(
                BlockingRiskObservation(
                    observation_id=observation.id,
                    title=str(observation.title or observation.risk_signal or "高风险").strip(),
                    risk_level=level,
                    adjudication=(
                        str(disposition.adjudication or "unreviewed").strip().lower()
                        if disposition is not None
                        else str(observation.feedback_status or "unreviewed").strip().lower()
                    ),
                    lifecycle_status=(
                        str(disposition.lifecycle_status or "open").strip().lower()
                        if disposition is not None
                        else "open"
                    ),
                )
            )
        for index, flag in enumerate(list(claim.risk_flags_json or [])):
            if not isinstance(flag, dict) or not is_blocking_claim_risk_flag(flag):
                continue
            flag_stage = risk_flag_business_stage(flag, default="")
            if flag_stage and flag_stage != claim_business_stage:
                continue
            observation_key = claim_risk_flag_observation_key(flag, claim_id=claim.id)
            if observation_key and observation_key in materialized_keys:
                # 完整物化后的处置投影优先；仅对没有 Observation 的高风险兜底。
                continue
            blockers.append(
                BlockingRiskObservation(
                    observation_id=observation_key or f"raw:{claim.id}:{index}",
                    title=str(
                        flag.get("label")
                        or flag.get("title")
                        or flag.get("message")
                        or "未物化高风险"
                    ).strip(),
                    risk_level=claim_risk_flag_severity(flag),
                    adjudication=str(flag.get("adjudication") or "unreviewed").strip().lower(),
                    lifecycle_status=str(
                        flag.get("lifecycle_status") or flag.get("lifecycleStatus") or "open"
                    )
                    .strip()
                    .lower(),
                )
            )
        return blockers

    @staticmethod
    def _claim_business_stage(claim: ExpenseClaim) -> str:
        expense_type = str(claim.expense_type or "").strip().lower()
        is_application = (
            is_application_claim_no(str(claim.claim_no or "").strip())
            or expense_type == "application"
            or expense_type.endswith("_application")
        )
        return risk_business_stage_for_claim(is_application_claim=is_application)

    def ensure_approvable(
        self,
        claim: ExpenseClaim,
        *,
        tenant_id: str | None,
    ) -> None:
        blockers = self.find_blockers(claim, tenant_id=tenant_id)
        if blockers:
            raise ExpenseClaimRiskBlockedError(blockers)

    @staticmethod
    def _is_closed(
        observation: RiskObservation,
        disposition: RiskDisposition | None,
    ) -> bool:
        if disposition is not None:
            adjudication = str(disposition.adjudication or "").strip().lower()
            lifecycle = str(disposition.lifecycle_status or "").strip().lower()
            if lifecycle == "waived":
                expires_at = _as_utc(disposition.waiver_expires_at)
                return expires_at is not None and expires_at > datetime.now(UTC)
            return adjudication == "false_positive" or lifecycle == "resolved"
        status = str(observation.status or "").strip().lower()
        feedback_status = str(observation.feedback_status or "").strip().lower()
        return status in _LEGACY_CLOSED_STATUSES or feedback_status == "false_positive"


def _as_utc(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)
