from typing import NamedTuple, NotRequired, TypedDict


class NodeKey(NamedTuple):
    kind: str
    file: str
    start_line: int


class DefNode(NamedTuple):
    key: NodeKey
    name: str
    end_line: int


class EdgeKey(NamedTuple):
    rel_type: str
    parent: NodeKey
    child: NodeKey


class NameEdge(NamedTuple):
    rel_type: str
    source: NodeKey
    target_name: str


class GraphData(NamedTuple):
    nodes: dict[NodeKey, DefNode]
    edges: set[EdgeKey]
    name_edges: set[NameEdge]


class GraphState(NamedTuple):
    # (H) A flat, comparable snapshot of a whole captured graph: node identities
    # (H) (label, unique-id) and directed edges (from_label, from_id, rel,
    # (H) to_label, to_id). Used to diff an incremental update against a clean
    # (H) re-index, where the clean index is the oracle.
    nodes: frozenset[tuple[str, str]]
    edges: frozenset[tuple[str, str, str, str, str]]


class ScoreRow(TypedDict):
    category: str
    label: str
    tp: int
    fp: int
    fn: int
    precision: float
    recall: float
    f1: float


class LocationStats(NamedTuple):
    matched: int
    end_exact: int
    end_within_one: int
    mean_abs_delta: float
    max_abs_delta: int


class DiffBucket(TypedDict):
    missing: list[str]
    extra: list[str]


class ScoreResult(NamedTuple):
    rows: list[ScoreRow]
    location: LocationStats
    diff: dict[str, DiffBucket]


class OracleRecord(TypedDict):
    kind: str
    file: str
    line: int
    name: str
    # (H) Optional so oracles that have not yet adopted span emission keep working
    # (H) (records_to_nodes falls back to the start line).
    end_line: NotRequired[int]


class OracleNodeRef(TypedDict):
    kind: str
    file: str
    line: int


class OracleEdge(TypedDict):
    rel: str
    parent: OracleNodeRef
    child: OracleNodeRef


class OracleNameEdge(TypedDict):
    rel: str
    source: OracleNodeRef
    target_name: str


class OracleCall(TypedDict):
    file: str
    name: str


class OraclePayload(TypedDict):
    nodes: list[OracleRecord]
    edges: list[OracleEdge]
    name_edges: list[OracleNameEdge]
    # (H) Optional: only the call-aware oracles (Go multi-language retrieval) emit
    # (H) call sites; structure-only oracles omit this key.
    calls: NotRequired[list[OracleCall]]
