#!/usr/bin/env python3
"""Summarize direct entry-pointer source refs for current route-pair leaf entries."""
from __future__ import annotations

import argparse
import html
import json
import struct
import sys
from pathlib import Path
from typing import Any

sys.path.insert(0, str(Path(__file__).resolve().parent))

from probe_exe_scene_tables import read_sections, va_to_offset
from summarize_script_handler_table import handler_for_opcode


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"

SOURCE = "map1_01a"
TARGET = "map2_02d"
SELECTOR = "2:0"
CURRENT_ROOT = 0x00540714
ROOT_TABLE_POINTER = 0x005429DC
TABLE_WINDOW_START = 0x005429A8
ROOT_TABLE_END_EXCLUSIVE = 0x00542A04

EVIDENCE_REFS = [
    {
        "path": "out/save_selector_leaf_table_global_context.json",
        "fields": [
            "currentSelectorRouteRows",
            "currentSelectorRoutePairIndices",
            "currentSelectorNegativeRoutePairRowCount",
            "currentSelectorNonNegativeRoutePairRowCount",
            "currentFrontierLeafOnlyNegative",
        ],
    },
    {
        "path": "out/script_handler_table.json",
        "fields": [
            "handlerTableVaHex",
            "opcodeCount",
            "codeHandlerCount",
            "defaultHandlerCount",
            "entries",
        ],
    },
    {
        "path": "out/save_selector_opcode07_indexed_pointers.json",
        "fields": [
            "opcode07IndexMode",
            "rowCount",
            "validTableRowCount",
            "selectedLeafTableWindowSlotCount",
            "selectedNegativeRootEntrySlotCount",
            "selectedCurrentRootEntrySlotCount",
            "selectedWrapperEntrySlotCount",
            "directFrontierTargetCount",
            "promotionStatus",
        ],
    },
]

ROUTE_PAIR_INDEX_SOURCE_MISSING_EVIDENCE_BY_GATE = {
    "direct-entry-pointer-source": (
        "code-backed direct entry-pointer source for route-pair entries 6/8 or wrapper entry -12"
    ),
    "encoded-entry-anchor-control-flow": (
        "modeled encoded entry-anchor control-flow candidate for route-pair entries 6/8"
    ),
    "nonnegative-entry-index-source": (
        "non-direct higher-level selector/table index source selecting entries 6 or 8"
    ),
    "negative-wrapper-index-source": (
        "normal runtime selection of negative -12 wrapper entry"
    ),
}


def load_json(path: Path, fallback: Any) -> Any:
    if not path.exists():
        return fallback
    return json.loads(path.read_text(encoding="utf-8"))


def hex32(value: int | None) -> str | None:
    return f"0x{value:08x}" if isinstance(value, int) else None


def parse_hex(value: str | None) -> int | None:
    return int(value, 16) if value else None


def dword_at(exe: bytes, sections: list[dict], va: int | None) -> int | None:
    if not isinstance(va, int):
        return None
    offset = va_to_offset(sections, va)
    if offset is None or offset + 4 > len(exe):
        return None
    return struct.unpack_from("<I", exe, offset)[0]


def u16_at(exe: bytes, sections: list[dict], va: int | None) -> int | None:
    if not isinstance(va, int):
        return None
    offset = va_to_offset(sections, va)
    if offset is None or offset + 2 > len(exe):
        return None
    return struct.unpack_from("<H", exe, offset)[0]


def s16_at(exe: bytes, sections: list[dict], va: int | None) -> int | None:
    if not isinstance(va, int):
        return None
    offset = va_to_offset(sections, va)
    if offset is None or offset + 2 > len(exe):
        return None
    return struct.unpack_from("<h", exe, offset)[0]


def s32_at(exe: bytes, sections: list[dict], va: int | None) -> int | None:
    if not isinstance(va, int):
        return None
    offset = va_to_offset(sections, va)
    if offset is None or offset + 4 > len(exe):
        return None
    return struct.unpack_from("<i", exe, offset)[0]


def byte_at(exe: bytes, sections: list[dict], va: int | None) -> int | None:
    if not isinstance(va, int):
        return None
    offset = va_to_offset(sections, va)
    if offset is None or offset >= len(exe):
        return None
    return exe[offset]


def readable_vas(sections: list[dict], start_va: int, end_va: int) -> list[int]:
    spans: list[tuple[int, int]] = []
    for section in sections:
        section_start = int(section["va"])
        section_end = section_start + int(section["raw_size"])
        start = max(start_va, section_start)
        end = min(end_va, section_end)
        if start < end:
            spans.append((start, end))
    values: list[int] = []
    for start, end in spans:
        values.extend(range(start, end))
    return values


def section_for_va(sections: list[dict], va: int) -> str | None:
    for section in sections:
        if section["va"] <= va < section["va"] + section["size"]:
            return section["name"]
    return None


def refs_to_value(exe: bytes, sections: list[dict], value: int | None) -> list[dict]:
    if not isinstance(value, int):
        return []
    needle = struct.pack("<I", value)
    refs: list[dict] = []
    for section in sections:
        start = section["raw"]
        end = start + section["raw_size"]
        data = exe[start:end]
        pos = data.find(needle)
        while pos >= 0:
            ref_va = section["va"] + pos
            refs.append({
                "section": section["name"],
                "refVa": ref_va,
                "refVaHex": hex32(ref_va),
            })
            pos = data.find(needle, pos + 1)
    return refs


def fixed_advance_bytes(handler: dict) -> list[int]:
    return sorted({
        item.get("bytes")
        for item in (handler.get("streamEffect") or {}).get("fixedAdvances") or []
        if item.get("bytes")
    })


def can_jump_to_plus4(handler: dict) -> bool:
    return (handler.get("streamEffect") or {}).get("canJumpToDwordAtPlus4") is True


def classify_ref(
    exe: bytes,
    sections: list[dict],
    ref: dict,
    *,
    target_kind: str,
    value: int,
    entry_va: int | None,
    descriptor_va: int | None,
) -> dict:
    ref_va = ref["refVa"]
    previous_va = ref_va - 4
    previous_value = dword_at(exe, sections, previous_va)
    previous_opcode = previous_value & 0xFF if previous_value is not None else None
    previous_mode = (previous_value >> 8) & 0xFF if previous_value is not None else None
    previous_handler = handler_for_opcode(exe, sections, previous_opcode) if previous_opcode is not None else {}
    fallthrough_opcode = value & 0xFF
    fallthrough_handler = handler_for_opcode(exe, sections, fallthrough_opcode)
    previous_fixed = fixed_advance_bytes(previous_handler)
    previous_can_jump = can_jump_to_plus4(previous_handler)
    fallthrough_can_jump = can_jump_to_plus4(fallthrough_handler)
    inside_current_root_range = CURRENT_ROOT <= ref_va < ROOT_TABLE_END_EXCLUSIVE
    inside_current_root_entry_run = ROOT_TABLE_POINTER <= ref_va < ROOT_TABLE_END_EXCLUSIVE
    inside_leaf_table_window = TABLE_WINDOW_START <= ref_va < ROOT_TABLE_END_EXCLUSIVE
    is_table_cell = target_kind == "descriptor-pointer" and ref_va == entry_va
    is_descriptor_child = target_kind == "child-pointer" and ref_va == (descriptor_va or 0) + 4

    role = "data-word"
    promotes = False
    if previous_opcode == 0x5A and previous_mode == 0 and previous_fixed == [4] and not previous_can_jump:
        role = "opcode-0x5a-mode0-fallthrough-word"
    elif is_table_cell:
        role = "leaf-table-entry-cell"
    elif is_descriptor_child:
        role = "descriptor-child-word"
    elif ref.get("section") == ".text":
        role = "text-immediate-reference"
        promotes = True
    elif previous_can_jump:
        role = "possible-stream-plus4-branch-target"
        promotes = True

    fallthrough_is_code = fallthrough_handler.get("isCodeHandler") is True
    if role == "opcode-0x5a-mode0-fallthrough-word" and fallthrough_is_code:
        promotes = True

    return {
        **ref,
        "targetKind": target_kind,
        "valueHex": hex32(value),
        "previousVaHex": hex32(previous_va),
        "previousValueHex": hex32(previous_value),
        "previousOpcodeHex": f"0x{previous_opcode:02x}" if previous_opcode is not None else None,
        "previousOpcodeMode": previous_mode,
        "previousHandlerHex": previous_handler.get("handlerVaHex"),
        "previousHandlerSection": previous_handler.get("handlerSection"),
        "previousHandlerFixedAdvances": previous_fixed,
        "previousHandlerCanJumpToDwordAtPlus4": previous_can_jump,
        "fallthroughWordOpcodeHex": f"0x{fallthrough_opcode:02x}",
        "fallthroughWordHandlerEntryHex": fallthrough_handler.get("entryVaHex"),
        "fallthroughWordHandlerHex": fallthrough_handler.get("handlerVaHex"),
        "fallthroughWordHandlerSection": fallthrough_handler.get("handlerSection"),
        "fallthroughWordHandlerIsCode": fallthrough_is_code,
        "fallthroughWordCanJumpToDwordAtPlus4": fallthrough_can_jump,
        "insideCurrentRootRange": inside_current_root_range,
        "insideCurrentRootEntryRun": inside_current_root_entry_run,
        "insideLeafTableWindow": inside_leaf_table_window,
        "referenceRole": role,
        "promotesEntrySelection": promotes,
    }


def compact_handler(row: dict) -> str:
    return (
        f"{row.get('valueHex')}/{row.get('fallthroughWordOpcodeHex')}"
        f"->{row.get('fallthroughWordHandlerHex') or '-'}"
        f"/{row.get('fallthroughWordHandlerSection') or '-'}"
    )


def route_entry_rows(leaf_table_global_context: dict) -> list[dict]:
    rows = [
        row for row in leaf_table_global_context.get("currentSelectorRouteRows") or []
        if row.get("selector") == SELECTOR
    ]
    return sorted(rows, key=lambda row: row.get("entryIndex", 0))


def encoded_entry_anchor_scan(exe: bytes, sections: list[dict], entries: list[dict]) -> dict:
    target_rows = [
        {
            "entryIndex": row.get("entryIndex"),
            "entryVa": parse_hex(row.get("entryVaHex")),
            "entryVaHex": row.get("entryVaHex"),
        }
        for row in entries
        if row.get("entryVaHex")
    ]
    low16_targets: dict[int, list[dict]] = {}
    root_rel16_targets: dict[int, dict] = {}
    root_rel32_targets: dict[int, dict] = {}
    exact_targets: dict[int, dict] = {}
    for row in target_rows:
        target = row.get("entryVa")
        if not isinstance(target, int):
            continue
        low16_targets.setdefault(target & 0xFFFF, []).append(row)
        rel = target - CURRENT_ROOT
        if 0 <= rel <= 0xFFFF:
            root_rel16_targets[rel] = row
        if 0 <= rel <= 0xFFFFFFFF:
            root_rel32_targets[rel] = row
        exact_targets[target] = row

    raw_rows: list[dict] = []
    branch_attached_rows: list[dict] = []
    modeled_rows: list[dict] = []
    counts = {
        "abs16Low": 0,
        "currentRootRelativeU16": 0,
        "currentRootRelativeU32": 0,
        "signedRel16SitePlus2": 0,
        "signedRel32SitePlus4": 0,
    }

    def handler_at_row(row_va: int) -> dict:
        opcode = byte_at(exe, sections, row_va)
        return handler_for_opcode(exe, sections, opcode) if opcode is not None else {}

    def record(kind: str, site_va: int, value: int, target_row: dict, width: int) -> None:
        row_va = site_va & ~3
        handler = handler_at_row(row_va)
        stream_effect = handler.get("streamEffect") or {}
        row = {
            "kind": kind,
            "siteVaHex": hex32(site_va),
            "rowVaHex": hex32(row_va),
            "valueHex": f"0x{value & ((1 << (width * 8)) - 1):0{width * 2}x}",
            "targetEntryIndex": target_row.get("entryIndex"),
            "targetEntryVaHex": target_row.get("entryVaHex"),
            "rowOpcodeHex": f"0x{byte_at(exe, sections, row_va):02x}"
            if byte_at(exe, sections, row_va) is not None
            else None,
            "handlerVaHex": handler.get("handlerVaHex"),
            "handlerSection": handler.get("handlerSection"),
            "handlerCanJumpToDwordAtPlus4": stream_effect.get("canJumpToDwordAtPlus4") is True,
            "handlerFixedAdvances": fixed_advance_bytes(handler),
            "startsAtBranchTargetField": (
                stream_effect.get("canJumpToDwordAtPlus4") is True and site_va == row_va + 4
            ),
        }
        if len(raw_rows) < 32:
            raw_rows.append(row)
        if row["startsAtBranchTargetField"] and len(branch_attached_rows) < 32:
            branch_attached_rows.append(row)

    for site_va in readable_vas(sections, CURRENT_ROOT, ROOT_TABLE_END_EXCLUSIVE):
        value16 = u16_at(exe, sections, site_va)
        if value16 is not None:
            for target_row in low16_targets.get(value16, []):
                counts["abs16Low"] += 1
                record("abs16-low", site_va, value16, target_row, 2)
            target_row = root_rel16_targets.get(value16)
            if target_row is not None:
                counts["currentRootRelativeU16"] += 1
                record("current-root-relative-u16", site_va, value16, target_row, 2)
            rel16 = s16_at(exe, sections, site_va)
            if rel16 is not None:
                target = site_va + 2 + rel16
                target_row = exact_targets.get(target)
                if target_row is not None:
                    counts["signedRel16SitePlus2"] += 1
                    record("signed-rel16-site-plus2", site_va, rel16, target_row, 2)
        value32 = dword_at(exe, sections, site_va)
        if value32 is not None:
            target_row = root_rel32_targets.get(value32)
            if target_row is not None:
                counts["currentRootRelativeU32"] += 1
                record("current-root-relative-u32", site_va, value32, target_row, 4)
            rel32 = s32_at(exe, sections, site_va)
            if rel32 is not None:
                target = site_va + 4 + rel32
                target_row = exact_targets.get(target)
                if target_row is not None:
                    counts["signedRel32SitePlus4"] += 1
                    record("signed-rel32-site-plus4", site_va, rel32, target_row, 4)

    for row_va in range(CURRENT_ROOT, ROOT_TABLE_END_EXCLUSIVE, 4):
        handler = handler_at_row(row_va)
        for advance in fixed_advance_bytes(handler):
            target = row_va + advance
            target_row = exact_targets.get(target)
            if target_row is None:
                continue
            modeled_rows.append({
                "siteVaHex": hex32(row_va),
                "advance": advance,
                "targetEntryIndex": target_row.get("entryIndex"),
                "targetEntryVaHex": target_row.get("entryVaHex"),
                "handlerVaHex": handler.get("handlerVaHex"),
                "handlerSection": handler.get("handlerSection"),
            })

    raw_count = sum(counts.values())
    branch_attached_count = len(branch_attached_rows)
    modeled_count = len(modeled_rows)
    promoting_count = modeled_count
    if promoting_count:
        classification = "encoded-entry-anchor-control-flow-candidate"
    elif raw_count:
        classification = "raw-encoded-entry-anchor-scalars-nonpromoting"
    else:
        classification = "no-encoded-entry-anchor-candidates"
    return {
        "scanRangeHex": f"{hex32(CURRENT_ROOT)}..{hex32(ROOT_TABLE_END_EXCLUSIVE)}",
        "targetEntryCount": len(target_rows),
        "targetEntries": [
            {"entryIndex": row.get("entryIndex"), "entryVaHex": row.get("entryVaHex")}
            for row in target_rows
        ],
        "rawScalarCandidateCounts": counts,
        "rawScalarCandidateCount": raw_count,
        "branchAttachedEncodedFieldCount": branch_attached_count,
        "modeledControlFlowCandidateCount": modeled_count,
        "promotingCandidateCount": promoting_count,
        "classification": classification,
        "promotionStatus": "ready-for-review" if promoting_count else "blocked",
        "rawScalarCandidateRows": raw_rows,
        "branchAttachedEncodedRows": branch_attached_rows,
        "modeledControlFlowRows": modeled_rows[:32],
    }


def build_entry_summary(exe: bytes, sections: list[dict], row: dict) -> dict:
    entry_va = parse_hex(row.get("entryVaHex"))
    descriptor_va = parse_hex(row.get("descriptorHex"))
    child_va = parse_hex(row.get("childPointerHex"))
    entry_refs = [
        classify_ref(
            exe,
            sections,
            ref,
            target_kind="entry-pointer",
            value=entry_va,
            entry_va=entry_va,
            descriptor_va=descriptor_va,
        )
        for ref in refs_to_value(exe, sections, entry_va)
    ]
    descriptor_refs = [
        classify_ref(
            exe,
            sections,
            ref,
            target_kind="descriptor-pointer",
            value=descriptor_va,
            entry_va=entry_va,
            descriptor_va=descriptor_va,
        )
        for ref in refs_to_value(exe, sections, descriptor_va)
    ]
    child_refs = [
        classify_ref(
            exe,
            sections,
            ref,
            target_kind="child-pointer",
            value=child_va,
            entry_va=entry_va,
            descriptor_va=descriptor_va,
        )
        for ref in refs_to_value(exe, sections, child_va)
    ]
    return {
        "entryIndex": row.get("entryIndex"),
        "entryIsNonNegative": row.get("entryIsNonNegative"),
        "entryVaHex": row.get("entryVaHex"),
        "descriptorHex": row.get("descriptorHex"),
        "descriptorMarkerHex": row.get("descriptorMarkerHex"),
        "descriptorIsMarkerShape": row.get("descriptorIsMarkerShape"),
        "childPointerHex": row.get("childPointerHex"),
        "descriptorHasRoutePair": row.get("descriptorHasRoutePair"),
        "childHasRoutePair": row.get("childHasRoutePair"),
        "isFrontierLeafChild": row.get("isFrontierLeafChild"),
        "entryPointerRefs": entry_refs,
        "descriptorPointerRefs": descriptor_refs,
        "childPointerRefs": child_refs,
        "entryPointerRefCount": len(entry_refs),
        "entryPointerTextRefCount": sum(1 for ref in entry_refs if ref.get("section") == ".text"),
        "entryPointerPromotingRefCount": sum(1 for ref in entry_refs if ref.get("promotesEntrySelection")),
        "entryPointerOpcode5aFallthroughRefCount": sum(
            1 for ref in entry_refs if ref.get("referenceRole") == "opcode-0x5a-mode0-fallthrough-word"
        ),
        "entryPointerFallthroughNonCodeRefCount": sum(
            1
            for ref in entry_refs
            if ref.get("referenceRole") == "opcode-0x5a-mode0-fallthrough-word"
            and ref.get("fallthroughWordHandlerIsCode") is False
            and ref.get("fallthroughWordCanJumpToDwordAtPlus4") is False
        ),
        "entryPointerCurrentRootRangeRefCount": sum(
            1 for ref in entry_refs if ref.get("insideCurrentRootRange")
        ),
        "entryPointerCurrentRootEntryRunRefCount": sum(
            1 for ref in entry_refs if ref.get("insideCurrentRootEntryRun")
        ),
        "entryPointerFallthroughHandlerSummaries": [
            compact_handler(ref)
            for ref in entry_refs
            if ref.get("referenceRole") == "opcode-0x5a-mode0-fallthrough-word"
        ],
        "descriptorPointerRefCount": len(descriptor_refs),
        "descriptorPointerTextRefCount": sum(1 for ref in descriptor_refs if ref.get("section") == ".text"),
        "descriptorPointerPromotingRefCount": sum(
            1 for ref in descriptor_refs if ref.get("promotesEntrySelection")
        ),
        "descriptorPointerOnlyTableCell": (
            bool(descriptor_refs)
            and all(ref.get("referenceRole") == "leaf-table-entry-cell" for ref in descriptor_refs)
        ),
        "childPointerRefCount": len(child_refs),
        "childPointerTextRefCount": sum(1 for ref in child_refs if ref.get("section") == ".text"),
        "childPointerPromotingRefCount": sum(1 for ref in child_refs if ref.get("promotesEntrySelection")),
        "childPointerOnlyDescriptorChildWord": (
            bool(child_refs)
            and all(ref.get("referenceRole") == "descriptor-child-word" for ref in child_refs)
        )
        if child_va is not None
        else None,
    }


def build_summary(exe: bytes, leaf_table_global_context: dict, opcode07_indexed: dict | None = None) -> dict:
    sections = read_sections(exe)
    opcode07_indexed = opcode07_indexed or {}
    entries = [build_entry_summary(exe, sections, row) for row in route_entry_rows(leaf_table_global_context)]
    route_pair_entries = [row for row in entries if row.get("entryIndex") in {6, 8}]
    negative_reader_entries = [row for row in entries if row.get("isFrontierLeafChild")]
    all_entry_refs = [ref for row in entries for ref in row.get("entryPointerRefs") or []]
    all_descriptor_refs = [ref for row in entries for ref in row.get("descriptorPointerRefs") or []]
    all_child_refs = [ref for row in entries for ref in row.get("childPointerRefs") or []]
    entry_pointer_promoting_ref_count = sum(1 for ref in all_entry_refs if ref.get("promotesEntrySelection"))
    descriptor_pointer_promoting_ref_count = sum(
        1 for ref in all_descriptor_refs if ref.get("promotesEntrySelection")
    )
    child_pointer_promoting_ref_count = sum(1 for ref in all_child_refs if ref.get("promotesEntrySelection"))
    nonnegative_entry_pointer_promoting_ref_count = sum(
        row.get("entryPointerPromotingRefCount") or 0 for row in route_pair_entries
    )
    negative_reader_entry_pointer_promoting_ref_count = sum(
        row.get("entryPointerPromotingRefCount") or 0 for row in negative_reader_entries
    )
    entry_pointer_opcode5a_fallthrough_ref_count = sum(
        1 for ref in all_entry_refs if ref.get("referenceRole") == "opcode-0x5a-mode0-fallthrough-word"
    )
    entry_pointer_fallthrough_non_code_ref_count = sum(
        1
        for ref in all_entry_refs
        if ref.get("referenceRole") == "opcode-0x5a-mode0-fallthrough-word"
        and ref.get("fallthroughWordHandlerIsCode") is False
        and ref.get("fallthroughWordCanJumpToDwordAtPlus4") is False
    )
    entry_pointer_text_ref_count = sum(1 for ref in all_entry_refs if ref.get("section") == ".text")
    encoded_anchor_scan = encoded_entry_anchor_scan(exe, sections, entries)
    opcode07_selected_leaf_table_window_slot_count = (
        opcode07_indexed.get("selectedLeafTableWindowSlotCount") or 0
    )
    opcode07_selected_negative_root_entry_slot_count = (
        opcode07_indexed.get("selectedNegativeRootEntrySlotCount") or 0
    )
    opcode07_selected_current_root_entry_slot_count = (
        opcode07_indexed.get("selectedCurrentRootEntrySlotCount") or 0
    )
    opcode07_selected_wrapper_entry_slot_count = (
        opcode07_indexed.get("selectedWrapperEntrySlotCount") or 0
    )
    opcode07_direct_frontier_target_count = opcode07_indexed.get("directFrontierTargetCount") or 0
    opcode07_direct_entry_selection_absent = (
        opcode07_selected_leaf_table_window_slot_count == 0
        and opcode07_selected_negative_root_entry_slot_count == 0
        and opcode07_selected_current_root_entry_slot_count == 0
        and opcode07_selected_wrapper_entry_slot_count == 0
        and opcode07_direct_frontier_target_count == 0
    )
    descriptor_refs_only_table_cells = bool(all_descriptor_refs) and all(
        ref.get("referenceRole") == "leaf-table-entry-cell" for ref in all_descriptor_refs
    )
    child_refs_only_descriptor_words = bool(all_child_refs) and all(
        ref.get("referenceRole") == "descriptor-child-word" for ref in all_child_refs
    )
    higher_level_index_source_proven = (
        entry_pointer_promoting_ref_count > 0
        or nonnegative_entry_pointer_promoting_ref_count > 0
        or negative_reader_entry_pointer_promoting_ref_count > 0
        or encoded_anchor_scan.get("promotingCandidateCount", 0) > 0
        or not opcode07_direct_entry_selection_absent
    )
    failed_index_source_gate_ids = []
    if entry_pointer_promoting_ref_count == 0:
        failed_index_source_gate_ids.append("direct-entry-pointer-source")
    if encoded_anchor_scan.get("promotingCandidateCount", 0) == 0:
        failed_index_source_gate_ids.append("encoded-entry-anchor-control-flow")
    if nonnegative_entry_pointer_promoting_ref_count == 0:
        failed_index_source_gate_ids.append("nonnegative-entry-index-source")
    if negative_reader_entry_pointer_promoting_ref_count == 0:
        failed_index_source_gate_ids.append("negative-wrapper-index-source")
    missing_evidence = [
        ROUTE_PAIR_INDEX_SOURCE_MISSING_EVIDENCE_BY_GATE.get(gate_id, gate_id)
        for gate_id in failed_index_source_gate_ids
    ]
    conclusion = (
        "The current selector's route-pair entries have no code-backed direct source refs. Entry -13 has no "
        "direct entry-pointer ref at all; entries -12, 6, and 8 each have one direct entry-pointer ref, but all "
        "three are opcode 0x5a mode 0 fallthrough words whose low-byte handlers are not executable code. The "
        "descriptor refs are only the leaf-table cells and child refs are only descriptor child words. The "
        "encoded entry-anchor scan adds no modeled control-flow candidate; raw scalar matches stay non-promoting. "
        "The opcode 0x07 zero-extended indexer also selects no leaf-table window, negative entry, current-root "
        "entry, wrapper entry, or direct frontier target. This removes the direct/encoded/opcode07 path as proof "
        "that a higher-level index selects entries 6/8 or the "
        "negative -12 wrapper; normal-route promotion still needs runtime selected-root execution, a decoded "
        "index source, or a strict source hotspot."
    )
    return {
        "source": SOURCE,
        "target": TARGET,
        "selector": SELECTOR,
        "currentRootHex": hex32(CURRENT_ROOT),
        "rootTablePointerHex": hex32(ROOT_TABLE_POINTER),
        "tableWindowHex": f"{hex32(TABLE_WINDOW_START)}..{hex32(ROOT_TABLE_END_EXCLUSIVE - 4)}",
        "routeEntryIndices": [row.get("entryIndex") for row in entries],
        "routePairEntryIndices": [row.get("entryIndex") for row in route_pair_entries],
        "negativeReaderEntryIndices": [row.get("entryIndex") for row in negative_reader_entries],
        "entryPointerRefCount": len(all_entry_refs),
        "entryPointerTextRefCount": entry_pointer_text_ref_count,
        "entryPointerPromotingRefCount": entry_pointer_promoting_ref_count,
        "encodedEntryAnchorScan": encoded_anchor_scan,
        "encodedEntryAnchorRawScalarCandidateCount": encoded_anchor_scan.get("rawScalarCandidateCount"),
        "encodedEntryAnchorBranchAttachedEncodedFieldCount": encoded_anchor_scan.get(
            "branchAttachedEncodedFieldCount"
        ),
        "encodedEntryAnchorModeledControlFlowCandidateCount": encoded_anchor_scan.get(
            "modeledControlFlowCandidateCount"
        ),
        "encodedEntryAnchorPromotingCandidateCount": encoded_anchor_scan.get("promotingCandidateCount"),
        "encodedEntryAnchorClassification": encoded_anchor_scan.get("classification"),
        "entryPointerOpcode5aFallthroughRefCount": entry_pointer_opcode5a_fallthrough_ref_count,
        "entryPointerFallthroughNonCodeRefCount": entry_pointer_fallthrough_non_code_ref_count,
        "nonNegativeEntryPointerPromotingRefCount": nonnegative_entry_pointer_promoting_ref_count,
        "negativeReaderEntryPointerPromotingRefCount": negative_reader_entry_pointer_promoting_ref_count,
        "opcode07IndexMode": opcode07_indexed.get("opcode07IndexMode"),
        "opcode07RowCount": opcode07_indexed.get("rowCount"),
        "opcode07ValidTableRowCount": opcode07_indexed.get("validTableRowCount"),
        "opcode07SelectedLeafTableWindowSlotCount": opcode07_selected_leaf_table_window_slot_count,
        "opcode07SelectedNegativeRootEntrySlotCount": opcode07_selected_negative_root_entry_slot_count,
        "opcode07SelectedCurrentRootEntrySlotCount": opcode07_selected_current_root_entry_slot_count,
        "opcode07SelectedWrapperEntrySlotCount": opcode07_selected_wrapper_entry_slot_count,
        "opcode07DirectFrontierTargetCount": opcode07_direct_frontier_target_count,
        "opcode07DirectEntrySelectionAbsent": opcode07_direct_entry_selection_absent,
        "entryPointerFallthroughHandlerSummaries": [
            compact_handler(ref)
            for ref in all_entry_refs
            if ref.get("referenceRole") == "opcode-0x5a-mode0-fallthrough-word"
        ],
        "descriptorPointerRefCount": len(all_descriptor_refs),
        "descriptorPointerTextRefCount": sum(1 for ref in all_descriptor_refs if ref.get("section") == ".text"),
        "descriptorPointerPromotingRefCount": descriptor_pointer_promoting_ref_count,
        "descriptorRefsOnlyTableCells": descriptor_refs_only_table_cells,
        "childPointerRefCount": len(all_child_refs),
        "childPointerTextRefCount": sum(1 for ref in all_child_refs if ref.get("section") == ".text"),
        "childPointerPromotingRefCount": child_pointer_promoting_ref_count,
        "childRefsOnlyDescriptorChildWords": child_refs_only_descriptor_words,
        "higherLevelIndexSourceProven": higher_level_index_source_proven,
        "directPointerRefPromotesRoute": higher_level_index_source_proven,
        "proofFound": higher_level_index_source_proven,
        "failedRoutePairIndexSourceGateIds": failed_index_source_gate_ids,
        "missingEvidence": missing_evidence,
        "promotionStatus": "blocked",
        "entries": entries,
        "evidenceRefs": EVIDENCE_REFS,
        "evidenceRefCount": len(EVIDENCE_REFS),
        "remainingProofs": [
            "decode a non-direct higher-level selector/table index source for entries 6 or 8",
            "prove normal runtime selection of the negative -12 wrapper entry",
            "prove selected-root execution reaches selector 2:0 on the normal route path",
            "find a strict map1_01a source coordinate or hotspot linked to map2_02d",
        ],
        "conclusion": conclusion,
    }


def csv(values: list[Any] | None) -> str:
    return ",".join(str(value) for value in values or []) or "-"


def markdown(summary: dict) -> str:
    lines = [
        "# Save Selector Route-Pair Index Source Gap",
        "",
        summary["conclusion"],
        "",
        f"- route: `{summary['source']} -> {summary['target']}`",
        f"- selector/root: `{summary['selector']}` / `{summary['currentRootHex']}`",
        f"- root table pointer: `{summary['rootTablePointerHex']}`",
        f"- route entry indices: `{csv(summary['routeEntryIndices'])}`",
        f"- route-pair entry indices: `{csv(summary['routePairEntryIndices'])}`",
        f"- negative reader entry indices: `{csv(summary['negativeReaderEntryIndices'])}`",
        f"- entry pointer refs/text/promoting: `{summary['entryPointerRefCount']}` / `{summary['entryPointerTextRefCount']}` / `{summary['entryPointerPromotingRefCount']}`",
        f"- encoded entry-anchor raw/branch/modeled/promoting: `{summary['encodedEntryAnchorRawScalarCandidateCount']}` / `{summary['encodedEntryAnchorBranchAttachedEncodedFieldCount']}` / `{summary['encodedEntryAnchorModeledControlFlowCandidateCount']}` / `{summary['encodedEntryAnchorPromotingCandidateCount']}`",
        f"- encoded entry-anchor classification: `{summary['encodedEntryAnchorClassification']}`",
        f"- entry pointer opcode5a fallthrough refs: `{summary['entryPointerOpcode5aFallthroughRefCount']}`",
        f"- entry pointer fallthrough non-code refs: `{summary['entryPointerFallthroughNonCodeRefCount']}`",
        f"- non-negative entry pointer promoting refs: `{summary['nonNegativeEntryPointerPromotingRefCount']}`",
        f"- negative reader entry pointer promoting refs: `{summary['negativeReaderEntryPointerPromotingRefCount']}`",
        f"- opcode07 index mode: `{summary['opcode07IndexMode']}`",
        f"- opcode07 direct entry selection absent: `{summary['opcode07DirectEntrySelectionAbsent']}`",
        f"- opcode07 rows/valid/direct frontier: `{summary['opcode07RowCount']}` / `{summary['opcode07ValidTableRowCount']}` / `{summary['opcode07DirectFrontierTargetCount']}`",
        f"- opcode07 selected leaf/negative/current/wrapper slots: `{summary['opcode07SelectedLeafTableWindowSlotCount']}` / `{summary['opcode07SelectedNegativeRootEntrySlotCount']}` / `{summary['opcode07SelectedCurrentRootEntrySlotCount']}` / `{summary['opcode07SelectedWrapperEntrySlotCount']}`",
        f"- entry pointer fallthrough handlers: `{csv(summary['entryPointerFallthroughHandlerSummaries'])}`",
        f"- descriptor refs only table cells: `{summary['descriptorRefsOnlyTableCells']}`",
        f"- child refs only descriptor child words: `{summary['childRefsOnlyDescriptorChildWords']}`",
        f"- higher-level index source proven: `{summary['higherLevelIndexSourceProven']}`",
        f"- proof found: `{summary['proofFound']}`",
        f"- failed route-pair index-source gates: `{csv(summary.get('failedRoutePairIndexSourceGateIds'))}`",
        f"- missing evidence count: `{len(summary.get('missingEvidence') or [])}`",
        f"- evidence refs: `{summary['evidenceRefCount']}`",
        f"- promotion status: `{summary['promotionStatus']}`",
        "",
        "## Missing Evidence",
        "",
        *[f"- {item}" for item in summary.get("missingEvidence") or []],
        "",
        "## Entries",
        "",
        "| index | entry | descriptor | child | entry refs | text refs | promoting refs | opcode5a refs | non-code refs | handlers | desc table-only | child word-only |",
        "| ---: | --- | --- | --- | ---: | ---: | ---: | ---: | ---: | --- | --- | --- |",
    ]
    for row in summary["entries"]:
        lines.append(
            f"| {row.get('entryIndex')} | `{row.get('entryVaHex')}` | `{row.get('descriptorHex')}` | "
            f"`{row.get('childPointerHex') or '-'}` | {row.get('entryPointerRefCount')} | "
            f"{row.get('entryPointerTextRefCount')} | {row.get('entryPointerPromotingRefCount')} | "
            f"{row.get('entryPointerOpcode5aFallthroughRefCount')} | "
            f"{row.get('entryPointerFallthroughNonCodeRefCount')} | "
            f"{csv(row.get('entryPointerFallthroughHandlerSummaries'))} | "
            f"{row.get('descriptorPointerOnlyTableCell')} | {row.get('childPointerOnlyDescriptorChildWord')} |"
        )
    lines.extend([
        "",
        "## Encoded Entry Anchors",
        "",
        "| classification | raw scalars | branch-attached | modeled control-flow | promoting |",
        "| --- | ---: | ---: | ---: | ---: |",
        (
            f"| {summary['encodedEntryAnchorClassification']} | "
            f"{summary['encodedEntryAnchorRawScalarCandidateCount']} | "
            f"{summary['encodedEntryAnchorBranchAttachedEncodedFieldCount']} | "
            f"{summary['encodedEntryAnchorModeledControlFlowCandidateCount']} | "
            f"{summary['encodedEntryAnchorPromotingCandidateCount']} |"
        ),
        "",
        "## Entry Pointer Refs",
        "",
        "| index | ref | section | role | previous | fallthrough handler | current root range | entry run | promotes |",
        "| ---: | --- | --- | --- | --- | --- | --- | --- | --- |",
    ])
    for row in summary["entries"]:
        for ref in row.get("entryPointerRefs") or []:
            lines.append(
                f"| {row.get('entryIndex')} | `{ref.get('refVaHex')}` | {ref.get('section')} | "
                f"{ref.get('referenceRole')} | `{ref.get('previousValueHex')}` / `{ref.get('previousOpcodeHex')}` | "
                f"`{ref.get('fallthroughWordHandlerHex')}` / {ref.get('fallthroughWordHandlerSection') or '-'} | "
                f"{ref.get('insideCurrentRootRange')} | {ref.get('insideCurrentRootEntryRun')} | "
                f"{ref.get('promotesEntrySelection')} |"
            )
    lines.extend(["", "## Evidence Refs", ""])
    for ref in summary.get("evidenceRefs") or []:
        fields = ", ".join(ref.get("fields") or [])
        lines.append(f"- `{ref['path']}`: {fields}")
    lines.extend(["", "## Remaining Proofs", ""])
    lines.extend(f"- {item}" for item in summary["remainingProofs"])
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    entry_rows = []
    for row in summary["entries"]:
        entry_rows.append(
            "<tr>"
            f"<td>{row.get('entryIndex')}</td>"
            f"<td><code>{html.escape(row.get('entryVaHex') or '-')}</code></td>"
            f"<td><code>{html.escape(row.get('descriptorHex') or '-')}</code></td>"
            f"<td><code>{html.escape(row.get('childPointerHex') or '-')}</code></td>"
            f"<td>{row.get('entryPointerRefCount')}</td>"
            f"<td>{row.get('entryPointerTextRefCount')}</td>"
            f"<td>{row.get('entryPointerPromotingRefCount')}</td>"
            f"<td>{row.get('entryPointerOpcode5aFallthroughRefCount')}</td>"
            f"<td>{row.get('entryPointerFallthroughNonCodeRefCount')}</td>"
            f"<td>{html.escape(csv(row.get('entryPointerFallthroughHandlerSummaries')))}</td>"
            f"<td>{row.get('descriptorPointerOnlyTableCell')}</td>"
            f"<td>{row.get('childPointerOnlyDescriptorChildWord')}</td>"
            "</tr>"
        )
    ref_rows = []
    for row in summary["entries"]:
        for ref in row.get("entryPointerRefs") or []:
            ref_rows.append(
                "<tr>"
                f"<td>{row.get('entryIndex')}</td>"
                f"<td><code>{html.escape(ref.get('refVaHex') or '-')}</code></td>"
                f"<td>{html.escape(ref.get('section') or '-')}</td>"
                f"<td>{html.escape(ref.get('referenceRole') or '-')}</td>"
                f"<td><code>{html.escape(ref.get('previousValueHex') or '-')}</code> / "
                f"<code>{html.escape(ref.get('previousOpcodeHex') or '-')}</code></td>"
                f"<td><code>{html.escape(ref.get('fallthroughWordHandlerHex') or '-')}</code> / "
                f"{html.escape(ref.get('fallthroughWordHandlerSection') or '-')}</td>"
                f"<td>{ref.get('insideCurrentRootRange')}</td>"
                f"<td>{ref.get('insideCurrentRootEntryRun')}</td>"
                f"<td>{ref.get('promotesEntrySelection')}</td>"
                "</tr>"
            )
    proof_items = "".join(f"<li>{html.escape(item)}</li>" for item in summary["remainingProofs"])
    missing_items = "".join(
        f"<li>{html.escape(item)}</li>" for item in summary.get("missingEvidence") or []
    )
    evidence_ref_items = "".join(
        "<li>"
        f"<code>{html.escape(ref.get('path') or '')}</code>: "
        f"{html.escape(', '.join(ref.get('fields') or []))}"
        "</li>"
        for ref in summary.get("evidenceRefs") or []
    )
    return "\n".join([
        "<!doctype html>",
        '<html lang="en">',
        "<head>",
        '  <meta charset="utf-8">',
        '  <meta name="viewport" content="width=device-width, initial-scale=1">',
        "  <title>Save Selector Route-Pair Index Source Gap</title>",
        "  <style>body{margin:24px;background:#111;color:#eee;font:14px system-ui,sans-serif;line-height:1.45}table{border-collapse:collapse;width:100%;margin:18px 0}td,th{border:1px solid #333;padding:6px 8px;text-align:left;vertical-align:top}th{background:#202020}code{color:#9bd4ff}</style>",
        "</head>",
        "<body>",
        "  <h1>Save Selector Route-Pair Index Source Gap</h1>",
        f"  <p>{html.escape(summary['conclusion'])}</p>",
        (
            "  <p><b>Route:</b> "
            f"<code>{html.escape(summary['source'])} -&gt; {html.escape(summary['target'])}</code>; "
            f"selector <code>{html.escape(summary['selector'])}</code>; "
            f"root <code>{html.escape(summary['currentRootHex'])}</code>; "
            f"promotion status <code>{html.escape(summary['promotionStatus'])}</code>.</p>"
        ),
        (
            "  <p>entry pointer refs/text/promoting: "
            f"{summary['entryPointerRefCount']}/"
            f"{summary['entryPointerTextRefCount']}/"
            f"{summary['entryPointerPromotingRefCount']}; "
            "encoded raw/branch/modeled/promoting: "
            f"{summary['encodedEntryAnchorRawScalarCandidateCount']}/"
            f"{summary['encodedEntryAnchorBranchAttachedEncodedFieldCount']}/"
            f"{summary['encodedEntryAnchorModeledControlFlowCandidateCount']}/"
            f"{summary['encodedEntryAnchorPromotingCandidateCount']}; "
            "opcode07 rows/valid/leaf/negative/current/wrapper/direct: "
            f"{summary['opcode07RowCount']}/"
            f"{summary['opcode07ValidTableRowCount']}/"
            f"{summary['opcode07SelectedLeafTableWindowSlotCount']}/"
            f"{summary['opcode07SelectedNegativeRootEntrySlotCount']}/"
            f"{summary['opcode07SelectedCurrentRootEntrySlotCount']}/"
            f"{summary['opcode07SelectedWrapperEntrySlotCount']}/"
            f"{summary['opcode07DirectFrontierTargetCount']}; "
            "opcode07 direct entry selection absent: "
            f"{summary['opcode07DirectEntrySelectionAbsent']}; "
            "opcode5a fallthrough/non-code: "
            f"{summary['entryPointerOpcode5aFallthroughRefCount']}/"
            f"{summary['entryPointerFallthroughNonCodeRefCount']}; "
            "higher-level index source proven: "
            f"{summary['higherLevelIndexSourceProven']}; "
            f"proofFound={summary['proofFound']}; "
            "failedRoutePairIndexSourceGates="
            f"<code>{html.escape(csv(summary.get('failedRoutePairIndexSourceGateIds')))}</code>; "
            f"missingEvidenceCount={len(summary.get('missingEvidence') or [])}.</p>"
        ),
        f"  <p><b>Evidence refs:</b> {summary['evidenceRefCount']}.</p>",
        "  <h2>Missing Evidence</h2>",
        f"  <ul>{missing_items}</ul>",
        "  <h2>Entries</h2>",
        "  <table><thead><tr><th>index</th><th>entry</th><th>descriptor</th><th>child</th><th>entry refs</th><th>text refs</th><th>promoting refs</th><th>opcode5a refs</th><th>non-code refs</th><th>handlers</th><th>desc table-only</th><th>child word-only</th></tr></thead>",
        f"  <tbody>{''.join(entry_rows)}</tbody></table>",
        "  <h2>Encoded Entry Anchors</h2>",
        "  <table><thead><tr><th>classification</th><th>raw scalars</th><th>branch-attached</th><th>modeled control-flow</th><th>promoting</th></tr></thead><tbody>",
        "  <tr>"
        f"<td>{html.escape(str(summary['encodedEntryAnchorClassification']))}</td>"
        f"<td>{summary['encodedEntryAnchorRawScalarCandidateCount']}</td>"
        f"<td>{summary['encodedEntryAnchorBranchAttachedEncodedFieldCount']}</td>"
        f"<td>{summary['encodedEntryAnchorModeledControlFlowCandidateCount']}</td>"
        f"<td>{summary['encodedEntryAnchorPromotingCandidateCount']}</td>"
        "</tr></tbody></table>",
        "  <h2>Entry Pointer Refs</h2>",
        "  <table><thead><tr><th>index</th><th>ref</th><th>section</th><th>role</th><th>previous</th><th>fallthrough handler</th><th>current root range</th><th>entry run</th><th>promotes</th></tr></thead>",
        f"  <tbody>{''.join(ref_rows)}</tbody></table>",
        "  <h2>Evidence Refs</h2>",
        f"  <ul>{evidence_ref_items}</ul>",
        "  <h2>Remaining Proofs</h2>",
        f"  <ul>{proof_items}</ul>",
        "</body>",
        "</html>",
        "",
    ])


def write_outputs(summary: dict, out_dir: Path = OUT) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "save_selector_route_pair_index_source_gap.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    (out_dir / "save_selector_route_pair_index_source_gap.html").write_text(
        html_page(summary),
        encoding="utf-8",
    )


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--exe", type=Path, default=ROOT / "Hwanse2.exe")
    parser.add_argument("--leaf-table-global", type=Path, default=OUT / "save_selector_leaf_table_global_context.json")
    parser.add_argument("--opcode07-indexed", type=Path, default=OUT / "save_selector_opcode07_indexed_pointers.json")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    args = parser.parse_args()
    summary = build_summary(
        args.exe.read_bytes(),
        load_json(args.leaf_table_global, {}),
        load_json(args.opcode07_indexed, {}),
    )
    write_outputs(summary, args.out_dir)
    print(
        "wrote route-pair index source gap -> "
        f"{args.out_dir / 'save_selector_route_pair_index_source_gap.html'}"
    )


if __name__ == "__main__":
    main()
