"""Risk signal normalization shared by rules, ontology, and graph scoring."""

from __future__ import annotations

from dataclasses import dataclass
from decimal import Decimal
from typing import Any

SEVERITY_SCORE = {
    "info": 12,
    "low": 32,
    "medium": 58,
    "high": 82,
    "critical": 100,
}

SIGNAL_ALIASES: dict[str, str] = {
    "amount_over_limit": "amount_limit_exceeded",
    "over_budget": "budget_overrun",
    "budget_exceeded": "budget_overrun",
    "duplicate_expense": "duplicate_invoice",
    "duplicate_ticket": "duplicate_invoice",
    "risk.invoice.duplicate_invoice": "duplicate_invoice",
    "location_mismatch": "location_mismatch",
    "city_mismatch": "location_mismatch",
    "hotel_itinerary_mismatch": "hotel_itinerary_mismatch",
    "date_outside_trip": "date_outside_trip",
    "preapproval_absent": "preapproval_absent",
    "application_fields_missing": "application_fields_missing",
    "attachment_ocr_missing": "attachment_missing",
    "missing_attachment": "attachment_missing",
    "reason_too_brief": "reason_too_brief",
    "vague_ticket_content": "vague_goods_description",
    "personal_purpose": "personal_purpose",
    "split_billing": "split_billing",
    "frequency_anomaly": "frequency_anomaly",
    "collusion": "cross_department_cluster",
    "cross_department_cluster": "cross_department_cluster",
    "buyer_name_mismatch": "buyer_name_mismatch",
    "document_expense_mismatch": "document_expense_mismatch",
    "void_or_red_invoice": "void_or_red_invoice",
    "cross_year_invoice": "cross_year_invoice",
    "entertainment_missing_detail": "entertainment_missing_detail",
}

SIGNAL_LABELS: dict[str, str] = {
    "amount_limit_exceeded": "Amount limit exceeded",
    "budget_overrun": "Budget overrun",
    "duplicate_invoice": "Duplicate invoice",
    "location_mismatch": "Location mismatch",
    "hotel_itinerary_mismatch": "Hotel and itinerary mismatch",
    "date_outside_trip": "Date outside approved trip",
    "preapproval_absent": "Pre-approval missing",
    "application_fields_missing": "Application fields missing",
    "attachment_missing": "Attachment missing",
    "reason_too_brief": "Reason too brief",
    "vague_goods_description": "Vague goods description",
    "personal_purpose": "Possible personal purpose",
    "split_billing": "Split billing pattern",
    "frequency_anomaly": "Frequency anomaly",
    "cross_department_cluster": "Cross-department spending cluster",
    "buyer_name_mismatch": "Buyer name mismatch",
    "document_expense_mismatch": "Document and expense mismatch",
    "void_or_red_invoice": "Void or red invoice",
    "cross_year_invoice": "Cross-year invoice",
    "entertainment_missing_detail": "Entertainment detail missing",
}

SIGNAL_DEFAULT_SEVERITY: dict[str, str] = {
    "duplicate_invoice": "critical",
    "personal_purpose": "high",
    "preapproval_absent": "high",
    "date_outside_trip": "high",
    "amount_limit_exceeded": "high",
    "budget_overrun": "high",
    "split_billing": "high",
    "cross_department_cluster": "high",
    "location_mismatch": "medium",
    "hotel_itinerary_mismatch": "medium",
    "frequency_anomaly": "medium",
    "buyer_name_mismatch": "medium",
    "document_expense_mismatch": "medium",
    "void_or_red_invoice": "high",
    "cross_year_invoice": "medium",
    "entertainment_missing_detail": "medium",
    "application_fields_missing": "low",
    "attachment_missing": "low",
    "reason_too_brief": "low",
    "vague_goods_description": "low",
}

POLICY_BOUND_SIGNALS = {
    "amount_limit_exceeded",
    "budget_overrun",
    "preapproval_absent",
    "date_outside_trip",
    "hotel_itinerary_mismatch",
    "location_mismatch",
    "document_expense_mismatch",
    "buyer_name_mismatch",
    "entertainment_missing_detail",
    "application_fields_missing",
    "attachment_missing",
}


@dataclass(slots=True)
class NormalizedRiskSignal:
    code: str
    raw_code: str
    label: str
    severity: str
    score: int
    confidence: Decimal = Decimal("1")
    source: str = "rule"
    metadata: dict[str, Any] | None = None

    def as_dict(self) -> dict[str, Any]:
        return {
            "code": self.code,
            "raw_code": self.raw_code,
            "label": self.label,
            "severity": self.severity,
            "score": self.score,
            "confidence": str(self.confidence),
            "source": self.source,
            "metadata": self.metadata or {},
        }


def normalize_risk_signal(value: Any, *, source: str = "rule") -> NormalizedRiskSignal | None:
    if isinstance(value, dict):
        raw_code = _first_present(
            value,
            "risk_signal",
            "signal",
            "code",
            "risk_type",
            "rule_code",
            "type",
        )
        severity = str(value.get("severity") or value.get("risk_level") or "").strip().lower()
        confidence = _to_decimal(value.get("confidence") or value.get("score_confidence") or 1)
        explicit_score = value.get("risk_score") or value.get("score")
        metadata = dict(value)
    else:
        raw_code = str(value or "").strip()
        severity = ""
        confidence = Decimal("1")
        explicit_score = None
        metadata = {}

    if not raw_code:
        return None

    canonical = SIGNAL_ALIASES.get(raw_code.strip().lower(), raw_code.strip().lower())
    canonical = canonical.replace(" ", "_")
    severity = severity or SIGNAL_DEFAULT_SEVERITY.get(canonical, "medium")
    score = _score_from_value(explicit_score, severity=severity)
    return NormalizedRiskSignal(
        code=canonical,
        raw_code=raw_code,
        label=SIGNAL_LABELS.get(canonical, canonical.replace("_", " ").title()),
        severity=severity,
        score=score,
        confidence=max(Decimal("0"), min(Decimal("1"), confidence)),
        source=source,
        metadata=metadata,
    )


def normalize_risk_signals(
    values: list[Any],
    *,
    source: str = "rule",
) -> list[NormalizedRiskSignal]:
    by_code: dict[str, NormalizedRiskSignal] = {}
    for value in values:
        signal = normalize_risk_signal(value, source=source)
        if signal is None:
            continue
        current = by_code.get(signal.code)
        if current is None or signal.score > current.score:
            by_code[signal.code] = signal
    return sorted(by_code.values(), key=lambda item: (item.score, item.code), reverse=True)


def policy_refs_for_signal(signal_code: str) -> list[str]:
    signal_code = SIGNAL_ALIASES.get(str(signal_code or "").strip().lower(), signal_code)
    if signal_code not in POLICY_BOUND_SIGNALS:
        return []
    return [f"policy.{signal_code}"]


def severity_from_score(score: int) -> str:
    normalized = max(0, min(100, int(score or 0)))
    if normalized >= 90:
        return "critical"
    if normalized >= 70:
        return "high"
    if normalized >= 45:
        return "medium"
    return "low"


def _first_present(value: dict[str, Any], *keys: str) -> str:
    for key in keys:
        candidate = str(value.get(key) or "").strip()
        if candidate:
            return candidate
    return ""


def _score_from_value(value: Any, *, severity: str) -> int:
    if value is None or value == "":
        return SEVERITY_SCORE.get(severity, SEVERITY_SCORE["medium"])
    try:
        numeric = Decimal(str(value))
    except Exception:
        return SEVERITY_SCORE.get(severity, SEVERITY_SCORE["medium"])
    if numeric <= Decimal("1"):
        numeric *= Decimal("100")
    return max(0, min(100, int(numeric.to_integral_value())))


def _to_decimal(value: Any) -> Decimal:
    try:
        return Decimal(str(value))
    except Exception:
        return Decimal("0")
