"""Ontology-to-risk-graph mapping utilities."""

from __future__ import annotations

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

from .models import RiskGraphEdge, RiskGraphNode
from .signals import NormalizedRiskSignal, normalize_risk_signals

ONTOLOGY_NODE_TYPE_MAP = {
    "expense_type": "expense_type",
    "document_type": "document",
    "employee": "employee",
    "department": "department",
    "vendor": "vendor",
    "supplier": "vendor",
    "merchant": "vendor",
    "customer": "customer",
    "risk_signal": "risk_signal",
    "invoice": "invoice",
    "claim": "claim",
}

ALLOWED_ONTOLOGY_EDGE_TYPES = {
    "ontology_extracts",
    "ontology_constrains",
    "ontology_signals",
}


@dataclass(slots=True)
class OntologyRiskGraphMapping:
    ontology_parse_id: str
    ontology_version: str
    domain: str
    scenario: str
    intent: str
    confidence: Decimal
    gate: str
    nodes: list[RiskGraphNode] = field(default_factory=list)
    edges: list[RiskGraphEdge] = field(default_factory=list)
    risk_signals: list[NormalizedRiskSignal] = field(default_factory=list)
    canonical_subject_key: str = ""
    raw_payload: dict[str, Any] = field(default_factory=dict)

    def as_dict(self) -> dict[str, Any]:
        return {
            "ontology_parse_id": self.ontology_parse_id,
            "ontology_version": self.ontology_version,
            "domain": self.domain,
            "scenario": self.scenario,
            "intent": self.intent,
            "confidence": str(self.confidence),
            "gate": self.gate,
            "canonical_subject_key": self.canonical_subject_key,
            "risk_signals": [item.as_dict() for item in self.risk_signals],
        }


def map_ontology_to_risk_graph(
    ontology: Any,
    *,
    ontology_parse_id: str = "",
    ontology_version: str = "ontology.v1",
) -> OntologyRiskGraphMapping:
    payload = _model_to_dict(ontology)
    if not payload:
        return OntologyRiskGraphMapping(
            ontology_parse_id=ontology_parse_id,
            ontology_version=ontology_version,
            domain="unknown",
            scenario="unknown",
            intent="query",
            confidence=Decimal("0"),
            gate="candidate_only",
        )

    parse_id = str(
        ontology_parse_id
        or payload.get("ontology_parse_id")
        or payload.get("parse_id")
        or payload.get("run_id")
        or "ontology_parse"
    )
    scenario = str(payload.get("scenario") or "unknown")
    intent = str(payload.get("intent") or "query")
    domain = str(payload.get("domain") or scenario)
    confidence = _to_decimal(payload.get("confidence"))
    gate = _gate_from_confidence(confidence)

    nodes: list[RiskGraphNode] = [
        RiskGraphNode(
            key=f"ontology:{parse_id}",
            node_type="ontology_parse",
            label=parse_id,
            canonical_key=f"ontology:{parse_id}",
            canonical_id=parse_id,
            ontology_type="ontology_parse",
            ontology_parse_id=parse_id,
            ontology_version=ontology_version,
            metadata={
                "scenario": scenario,
                "intent": intent,
                "domain": domain,
                "confidence": str(confidence),
            },
        )
    ]
    edges: list[RiskGraphEdge] = []
    canonical_subject_key = ""

    for entity in list(payload.get("entities") or []):
        entity_payload = _model_to_dict(entity)
        raw_type = str(entity_payload.get("type") or "").strip().lower()
        node_type = ONTOLOGY_NODE_TYPE_MAP.get(raw_type, raw_type or "entity")
        value = str(
            entity_payload.get("normalized_value")
            or entity_payload.get("value")
            or ""
        ).strip()
        if not value:
            continue
        key = f"{node_type}:{_canonical_key(value)}"
        nodes.append(
            RiskGraphNode(
                key=key,
                node_type=node_type,
                label=value,
                canonical_key=key,
                canonical_id=_canonical_key(value),
                ontology_type=raw_type or node_type,
                ontology_parse_id=parse_id,
                ontology_version=ontology_version,
                metadata={
                    "role": entity_payload.get("role") or "target",
                    "confidence": entity_payload.get("confidence") or 0,
                },
            )
        )
        edges.append(
            RiskGraphEdge(
                source_key=f"ontology:{parse_id}",
                target_key=key,
                edge_type="ontology_extracts",
                source="ontology",
                metadata={"raw_type": raw_type},
            )
        )
        if not canonical_subject_key and node_type in {"employee", "claim", "vendor"}:
            canonical_subject_key = key

    for constraint in list(payload.get("constraints") or []):
        constraint_payload = _model_to_dict(constraint)
        field = str(constraint_payload.get("field") or "").strip()
        operator = str(constraint_payload.get("operator") or "").strip()
        value = str(constraint_payload.get("value") or "").strip()
        if not field or not value:
            continue
        key = f"constraint:{_canonical_key(field)}:{_canonical_key(value)}"
        nodes.append(
            RiskGraphNode(
                key=key,
                node_type="constraint",
                label=f"{field} {operator} {value}".strip(),
                canonical_key=key,
                canonical_id=key,
                ontology_type="constraint",
                ontology_parse_id=parse_id,
                ontology_version=ontology_version,
                metadata=constraint_payload,
            )
        )
        edges.append(
            RiskGraphEdge(
                source_key=f"ontology:{parse_id}",
                target_key=key,
                edge_type="ontology_constrains",
                source="ontology",
            )
        )

    risk_signals = normalize_risk_signals(list(payload.get("risk_flags") or []), source="ontology")
    for signal in risk_signals:
        key = f"risk_signal:{signal.code}"
        nodes.append(
            RiskGraphNode(
                key=key,
                node_type="risk_signal",
                label=signal.label,
                canonical_key=key,
                canonical_id=signal.code,
                ontology_type="risk_signal",
                ontology_parse_id=parse_id,
                ontology_version=ontology_version,
                metadata={"severity": signal.severity, "score": signal.score},
            )
        )
        edges.append(
            RiskGraphEdge(
                source_key=f"ontology:{parse_id}",
                target_key=key,
                edge_type="ontology_signals",
                source="ontology",
                metadata={"gate": gate},
            )
        )

    return OntologyRiskGraphMapping(
        ontology_parse_id=parse_id,
        ontology_version=ontology_version,
        domain=domain,
        scenario=scenario,
        intent=intent,
        confidence=confidence,
        gate=gate,
        nodes=_dedupe_nodes(nodes),
        edges=_dedupe_edges(edges),
        risk_signals=risk_signals,
        canonical_subject_key=canonical_subject_key,
        raw_payload=payload,
    )


def _model_to_dict(value: Any) -> dict[str, Any]:
    if value is None:
        return {}
    if isinstance(value, dict):
        return dict(value)
    if hasattr(value, "model_dump"):
        return dict(value.model_dump(mode="json"))
    if hasattr(value, "dict"):
        return dict(value.dict())
    return {}


def _gate_from_confidence(confidence: Decimal) -> str:
    if confidence >= Decimal("0.78"):
        return "automatic"
    if confidence >= Decimal("0.55"):
        return "review"
    return "candidate_only"


def _canonical_key(value: str) -> str:
    return "_".join(str(value or "").strip().lower().split())


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


def _dedupe_nodes(nodes: list[RiskGraphNode]) -> list[RiskGraphNode]:
    by_key: dict[str, RiskGraphNode] = {}
    for node in nodes:
        by_key.setdefault(node.key, node)
    return list(by_key.values())


def _dedupe_edges(edges: list[RiskGraphEdge]) -> list[RiskGraphEdge]:
    by_key: dict[tuple[str, str, str], RiskGraphEdge] = {}
    for edge in edges:
        if edge.edge_type not in ALLOWED_ONTOLOGY_EDGE_TYPES:
            continue
        by_key.setdefault(edge.edge_key(), edge)
    return list(by_key.values())
