"""Graph construction helpers for expense risk analysis."""

from __future__ import annotations

from decimal import Decimal

from .models import RiskGraphClaimSnapshot, RiskGraphEdge, RiskGraphNode

ALLOWED_EDGE_TYPES = {
    "department_has_employee",
    "employee_submits_claim",
    "claim_has_item",
    "claim_expense_type",
    "claim_location",
    "claim_invoice",
    "claim_has_risk_signal",
    "claim_similar_to",
    "claim_duplicate_invoice",
    "ontology_extracts",
    "ontology_constrains",
    "ontology_signals",
}


def build_claim_graph(
    claims: list[RiskGraphClaimSnapshot],
) -> tuple[list[RiskGraphNode], list[RiskGraphEdge]]:
    nodes: dict[str, RiskGraphNode] = {}
    edges: dict[tuple[str, str, str], RiskGraphEdge] = {}

    for claim in claims:
        claim_key = claim_node_key(claim)
        _add_node(
            nodes,
            RiskGraphNode(
                key=claim_key,
                node_type="claim",
                label=claim.claim_no or claim.claim_id,
                canonical_key=claim_key,
                canonical_id=claim.claim_id or claim.claim_no,
                metadata={
                    "claim_id": claim.claim_id,
                    "amount": str(_to_decimal(claim.amount)),
                    "expense_type": claim.expense_type,
                    "status": claim.status,
                },
            ),
        )

        employee_key = employee_node_key(claim)
        if employee_key:
            _add_node(
                nodes,
                RiskGraphNode(
                    key=employee_key,
                    node_type="employee",
                    label=claim.employee_name or claim.employee_id or "unknown",
                    canonical_key=employee_key,
                    canonical_id=claim.employee_id or claim.employee_name,
                    metadata={"employee_id": claim.employee_id, "grade": claim.employee_grade},
                ),
            )
            _add_edge(
                edges,
                RiskGraphEdge(
                    source_key=employee_key,
                    target_key=claim_key,
                    edge_type="employee_submits_claim",
                    metadata={"amount": str(_to_decimal(claim.amount))},
                ),
            )

        department_key = department_node_key(claim)
        if department_key:
            _add_node(
                nodes,
                RiskGraphNode(
                    key=department_key,
                    node_type="department",
                    label=claim.department_name or claim.department_id or "unknown",
                    canonical_key=department_key,
                    canonical_id=claim.department_id or claim.department_name,
                    metadata={"department_id": claim.department_id},
                ),
            )
            if employee_key:
                _add_edge(
                    edges,
                    RiskGraphEdge(
                        source_key=department_key,
                        target_key=employee_key,
                        edge_type="department_has_employee",
                    ),
                )

        expense_key = expense_type_node_key(claim.expense_type)
        if expense_key:
            _add_node(
                nodes,
                RiskGraphNode(
                    key=expense_key,
                    node_type="expense_type",
                    label=claim.expense_type,
                    canonical_key=expense_key,
                    canonical_id=claim.expense_type,
                ),
            )
            _add_edge(
                edges,
                RiskGraphEdge(
                    source_key=claim_key,
                    target_key=expense_key,
                    edge_type="claim_expense_type",
                ),
            )

        location_key = location_node_key(claim.location)
        if location_key:
            _add_node(
                nodes,
                RiskGraphNode(
                    key=location_key,
                    node_type="location",
                    label=claim.location,
                    canonical_key=location_key,
                    canonical_id=claim.location,
                ),
            )
            _add_edge(
                edges,
                RiskGraphEdge(
                    source_key=claim_key,
                    target_key=location_key,
                    edge_type="claim_location",
                ),
            )

        for item in claim.items:
            item_key = f"claim_item:{item.item_id}" if item.item_id else ""
            if item_key:
                _add_node(
                    nodes,
                    RiskGraphNode(
                        key=item_key,
                        node_type="claim_item",
                        label=item.item_type or item.item_id,
                        canonical_key=item_key,
                        canonical_id=item.item_id,
                        metadata={
                            "amount": str(_to_decimal(item.item_amount)),
                            "location": item.item_location,
                            "invoice_id": item.invoice_id,
                        },
                    ),
                )
                _add_edge(
                    edges,
                    RiskGraphEdge(
                        source_key=claim_key,
                        target_key=item_key,
                        edge_type="claim_has_item",
                    ),
                )
            if item.invoice_id:
                invoice_key = invoice_node_key(item.invoice_id)
                _add_node(
                    nodes,
                    RiskGraphNode(
                        key=invoice_key,
                        node_type="invoice",
                        label=item.invoice_id,
                        canonical_key=invoice_key,
                        canonical_id=item.invoice_id,
                    ),
                )
                _add_edge(
                    edges,
                    RiskGraphEdge(
                        source_key=claim_key,
                        target_key=invoice_key,
                        edge_type="claim_invoice",
                    ),
                )

    _link_duplicate_invoices(claims, edges)
    _link_similar_claims(claims, edges)
    return list(nodes.values()), list(edges.values())


def claim_node_key(claim: RiskGraphClaimSnapshot) -> str:
    return f"claim:{claim.claim_id or claim.claim_no}"


def employee_node_key(claim: RiskGraphClaimSnapshot) -> str:
    identifier = claim.employee_id or claim.employee_name
    return f"employee:{_canonical_key(identifier)}" if identifier else ""


def department_node_key(claim: RiskGraphClaimSnapshot) -> str:
    identifier = claim.department_id or claim.department_name
    return f"department:{_canonical_key(identifier)}" if identifier else ""


def expense_type_node_key(expense_type: str) -> str:
    return f"expense_type:{_canonical_key(expense_type)}" if str(expense_type or "").strip() else ""


def location_node_key(location: str) -> str:
    return f"location:{_canonical_key(location)}" if str(location or "").strip() else ""


def invoice_node_key(invoice_id: str) -> str:
    return f"invoice:{_canonical_key(invoice_id)}"


def _link_duplicate_invoices(
    claims: list[RiskGraphClaimSnapshot],
    edges: dict[tuple[str, str, str], RiskGraphEdge],
) -> None:
    by_invoice: dict[str, list[RiskGraphClaimSnapshot]] = {}
    for claim in claims:
        for item in claim.items:
            if item.invoice_id:
                by_invoice.setdefault(item.invoice_id, []).append(claim)

    for invoice_id, invoice_claims in by_invoice.items():
        unique_claims = {claim.claim_id: claim for claim in invoice_claims}
        if len(unique_claims) < 2:
            continue
        claim_list = list(unique_claims.values())
        for source in claim_list:
            for target in claim_list:
                if source.claim_id == target.claim_id:
                    continue
                _add_edge(
                    edges,
                    RiskGraphEdge(
                        source_key=claim_node_key(source),
                        target_key=claim_node_key(target),
                        edge_type="claim_duplicate_invoice",
                        weight=Decimal("2"),
                        evidence=f"invoice:{invoice_id}",
                    ),
                )


def _link_similar_claims(
    claims: list[RiskGraphClaimSnapshot],
    edges: dict[tuple[str, str, str], RiskGraphEdge],
) -> None:
    for index, source in enumerate(claims):
        for target in claims[index + 1 :]:
            if not _is_similar_claim(source, target):
                continue
            _add_edge(
                edges,
                RiskGraphEdge(
                    source_key=claim_node_key(source),
                    target_key=claim_node_key(target),
                    edge_type="claim_similar_to",
                    weight=Decimal("0.7"),
                    metadata={"reason": "same employee and expense type"},
                ),
            )
            _add_edge(
                edges,
                RiskGraphEdge(
                    source_key=claim_node_key(target),
                    target_key=claim_node_key(source),
                    edge_type="claim_similar_to",
                    weight=Decimal("0.7"),
                    metadata={"reason": "same employee and expense type"},
                ),
            )


def _is_similar_claim(source: RiskGraphClaimSnapshot, target: RiskGraphClaimSnapshot) -> bool:
    source_employee = source.employee_id or source.employee_name
    target_employee = target.employee_id or target.employee_name
    if not source_employee or _canonical_key(source_employee) != _canonical_key(target_employee):
        return False
    if _canonical_key(source.expense_type) != _canonical_key(target.expense_type):
        return False
    if source.occurred_at is None or target.occurred_at is None:
        return True
    return abs((source.occurred_at.date() - target.occurred_at.date()).days) <= 30


def _add_node(nodes: dict[str, RiskGraphNode], node: RiskGraphNode) -> None:
    nodes.setdefault(node.key, node)


def _add_edge(edges: dict[tuple[str, str, str], RiskGraphEdge], edge: RiskGraphEdge) -> None:
    if edge.edge_type not in ALLOWED_EDGE_TYPES:
        return
    edges.setdefault(edge.edge_key(), edge)


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


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