#!/usr/bin/env python3
"""Map route-relevant save-selector low bytes that resolve to data descriptors."""
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 offset_to_va, read_sections, va_to_offset
from summarize_script_handler_table import HANDLER_TABLE_VA, handler_for_opcode, section_name_for_va


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"
ROUTE_SOURCE = "map1_01a"
ROUTE_TARGET = "map2_02d"
RELEVANT_SITES = [
    ("route-dispatch-stop", 0x005428F4),
    ("wrapper-child-pointer", 0x00542A08),
    ("direct-leaf-entry-0", 0x005429F4),
    ("direct-leaf-entry-1", 0x005429FC),
    ("predecessor-root-entry-stop", 0x004783E0),
    ("predecessor-fill-fragment-stop", 0x004844DC),
]
SCAN_SECTIONS = {".text", ".data", ".rdata"}
FAILED_DATA_DESCRIPTOR_GATE_IDS = [
    "runtime-leaf-selection",
    "reader-control-path-after-descriptor",
    "predecessor-root-to-fill-control-path",
    "predecessor-fill-descriptor-bridge",
    "strict-source-hotspot",
]
MISSING_EVIDENCE = [
    "runtime selector/index choosing wrapper 0x00542a04 or frontier leaf 0x00542ae8",
    "control path reaching reader 0x00542b0c after the route descriptor boundary",
    "decoded or observed predecessor root path from 0x004783e0 to 0x004844d0/0x004844d8",
    "execution bridge across predecessor fill-fragment descriptor boundary 0x004844dc",
    "strict map1_01a source coordinate or hotspot",
]


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


def hex_opcode(value: int | None) -> str | None:
    if value is None:
        return None
    return f"0x{value:02x}" if value <= 0xFF else f"0x{value:x}"


def dword_at(exe: bytes, sections: list[dict], va: int) -> int | 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 section_for_offset(sections: list[dict], offset: int) -> dict | None:
    for section in sections:
        start = section["raw"]
        end = start + section["raw_size"]
        if start <= offset < end:
            return section
    return None


def value_refs(exe: bytes, sections: list[dict], value: int) -> list[dict]:
    needle = struct.pack("<I", value)
    refs = []
    search = 0
    while True:
        hit = exe.find(needle, search)
        if hit < 0:
            break
        search = hit + 1
        section = section_for_offset(sections, hit)
        if section is None or section["name"] not in SCAN_SECTIONS:
            continue
        ref_va = offset_to_va(sections, hit)
        if ref_va is None:
            continue
        entry_offset = ref_va - HANDLER_TABLE_VA
        handler_opcode = entry_offset // 4 if entry_offset >= 0 and entry_offset % 4 == 0 else None
        refs.append({
            "section": section["name"],
            "fileOffsetHex": f"0x{hit:06x}",
            "refVaHex": hex32(ref_va),
            "valueHex": hex32(value),
            "handlerTableOpcode": handler_opcode,
            "handlerTableOpcodeHex": hex_opcode(handler_opcode),
            "isByteOpcodeHandlerEntry": handler_opcode is not None and 0 <= handler_opcode <= 0xFF,
            "isTableAlignedRef": handler_opcode is not None and 0 <= handler_opcode <= 0x180,
        })
    return refs


def descriptor_window(exe: bytes, sections: list[dict], start_va: int, count: int = 16) -> list[dict]:
    rows = []
    for index in range(count):
        va = start_va + index * 4
        value = dword_at(exe, sections, va)
        section = section_name_for_va(sections, value) if value is not None else None
        rows.append({
            "vaHex": hex32(va),
            "valueHex": hex32(value),
            "valueSection": section,
            "kind": "pointer" if section else "scalar" if value is not None else "unreadable",
        })
    return rows


def site_row(exe: bytes, sections: list[dict], role: str, va: int) -> dict:
    value = dword_at(exe, sections, va)
    opcode = value & 0xFF if value is not None else None
    handler = handler_for_opcode(exe, sections, opcode or 0)
    return {
        "role": role,
        "siteVaHex": hex32(va),
        "valueHex": hex32(value),
        "valueSection": section_name_for_va(sections, value) if value is not None else None,
        "lowByteOpcode": opcode,
        "lowByteOpcodeHex": hex_opcode(opcode),
        "handlerEntryVaHex": handler.get("entryVaHex"),
        "handlerValueHex": handler.get("handlerVaHex"),
        "handlerSection": handler.get("handlerSection"),
        "isCodeHandler": handler.get("isCodeHandler"),
        "isDataDescriptor": handler.get("handlerSection") == ".data",
    }


def data_descriptor_summary(exe: bytes, sections: list[dict], target_hex: str) -> dict:
    target = int(target_hex, 16)
    refs = value_refs(exe, sections, target)
    handler_refs = [ref for ref in refs if ref.get("isByteOpcodeHandlerEntry")]
    table_aligned_refs = [ref for ref in refs if ref.get("isTableAlignedRef")]
    handler_opcode_hexes = [
        ref["handlerTableOpcodeHex"]
        for ref in handler_refs
        if ref.get("handlerTableOpcodeHex")
    ]
    table_aligned_ref_hexes = [
        ref["handlerTableOpcodeHex"]
        for ref in table_aligned_refs
        if ref.get("handlerTableOpcodeHex")
    ]
    return {
        "descriptorVaHex": target_hex,
        "descriptorSection": section_name_for_va(sections, target),
        "pointerRefCount": len(refs),
        "pointerRefSections": sorted({ref["section"] for ref in refs}),
        "byteOpcodeHandlerEntryCount": len(handler_refs),
        "sharedHandlerOpcodes": handler_opcode_hexes,
        "tableAlignedRefCount": len(table_aligned_refs),
        "tableAlignedRefIndexes": table_aligned_ref_hexes,
        "refs": refs,
        "descriptorWindow": descriptor_window(exe, sections, target),
    }


def build_summary(
    exe: bytes,
    dispatch_stop_context: dict | None = None,
    leaf_table_context: dict | None = None,
    opcode24_payload_table: dict | None = None,
) -> dict:
    sections = read_sections(exe)
    sites = [site_row(exe, sections, role, va) for role, va in RELEVANT_SITES]
    data_targets = sorted({
        row["handlerValueHex"]
        for row in sites
        if row.get("handlerSection") == ".data" and row.get("handlerValueHex")
    })
    descriptor_summaries = [data_descriptor_summary(exe, sections, target) for target in data_targets]
    by_role = {row["role"]: row for row in sites}
    route_stop = by_role.get("route-dispatch-stop") or {}
    wrapper_child = by_role.get("wrapper-child-pointer") or {}
    direct_leaf = by_role.get("direct-leaf-entry-0") or {}
    predecessor_root_stop = by_role.get("predecessor-root-entry-stop") or {}
    predecessor_fill_stop = by_role.get("predecessor-fill-fragment-stop") or {}
    route_and_wrapper_share_e8_descriptor = (
        route_stop.get("lowByteOpcodeHex") == "0xe8"
        and wrapper_child.get("lowByteOpcodeHex") == "0xe8"
        and route_stop.get("handlerValueHex") == "0x00440c28"
        and wrapper_child.get("handlerValueHex") == "0x00440c28"
    )
    direct_leaf_uses_distinct_descriptor = (
        direct_leaf.get("handlerValueHex") == "0x00440c5c"
        and direct_leaf.get("handlerValueHex") != route_stop.get("handlerValueHex")
    )
    e8_descriptor = next((row for row in descriptor_summaries if row["descriptorVaHex"] == "0x00440c28"), {})
    d0_descriptor = next((row for row in descriptor_summaries if row["descriptorVaHex"] == "0x00440a9c"), {})
    c0_descriptor = next((row for row in descriptor_summaries if row["descriptorVaHex"] == "0x00440c5c"), {})
    predecessor_root_stop_is_descriptor = (
        predecessor_root_stop.get("lowByteOpcodeHex") == "0xd0"
        and predecessor_root_stop.get("handlerValueHex") == "0x00440a9c"
        and predecessor_root_stop.get("handlerSection") == ".data"
    )
    predecessor_fill_stop_is_descriptor = (
        predecessor_fill_stop.get("lowByteOpcodeHex") == "0xc0"
        and predecessor_fill_stop.get("handlerValueHex") == "0x00440c5c"
        and predecessor_fill_stop.get("handlerSection") == ".data"
    )
    conclusion = (
        "The route dispatch stop at 0x005428f4 and the wrapper child pointer at 0x00542a08 both resolve low byte "
        "0xe8 through the save-selector table to data descriptor 0x00440c28. That descriptor is shared by byte-opcode "
        "entries 0xa7, 0xaf, 0xe0, and 0xe8; its table-aligned refs also include 0x100 and 0x109, and all refs are "
        "data-only. The direct current-root leaf entry 0x005429f4 resolves through a different data descriptor, "
        "0x00440c5c. The predecessor root-entry traversal stop at 0x004783e0 resolves low byte 0xd0 to data "
        "descriptor 0x00440a9c, while the predecessor fill-fragment stop at 0x004844dc resolves low byte 0xc0 "
        "to descriptor 0x00440c5c. Each row is a data-descriptor boundary, not executable control-flow proof "
        "that the route dispatch reaches wrapper 0x00542a04, frontier leaf 0x00542ae8, reader 0x00542b0c, "
        "or the predecessor fill sites from the 1:0 root entry."
    )
    evidence_refs = [
        {
            "path": "Hwanse2.exe",
            "fields": [
                "handler table 0x00440720",
                "route-dispatch-stop 0x005428f4",
                "wrapper-child-pointer 0x00542a08",
                "predecessor-root-entry-stop 0x004783e0",
                "predecessor-fill-fragment-stop 0x004844dc",
            ],
        },
        {
            "path": "out/save_selector_dispatch_stop_context.json",
            "fields": [
                "promotionStatus",
                "routeStopVaHex",
                "wrapperChildPointerVaHex",
                "frontierReaderHex",
            ],
        },
        {
            "path": "out/save_selector_leaf_table_context.json",
            "fields": [
                "runtimeSelectionProven",
                "wrapperLeafHex",
                "frontierLeafHex",
                "frontierReaderHex",
            ],
        },
        {
            "path": "out/save_selector_opcode24_payload_table.json",
            "fields": [
                "payloadDirectlyTargetsLeafTable",
                "payloadGraphEdgeCount",
                "payloadGraphAllEdgesLocal",
                "payloadGraphReachesFrontierTarget",
            ],
        },
    ]
    return {
        "source": ROUTE_SOURCE,
        "target": ROUTE_TARGET,
        "handlerTableVaHex": hex32(HANDLER_TABLE_VA),
        "sites": sites,
        "dataDescriptors": descriptor_summaries,
        "routeAndWrapperShareE8Descriptor": route_and_wrapper_share_e8_descriptor,
        "directLeafUsesDistinctDescriptor": direct_leaf_uses_distinct_descriptor,
        "predecessorRootStopIsDataDescriptor": predecessor_root_stop_is_descriptor,
        "predecessorFillStopIsDataDescriptor": predecessor_fill_stop_is_descriptor,
        "d0DescriptorSharedHandlerOpcodes": d0_descriptor.get("sharedHandlerOpcodes") or [],
        "d0DescriptorTableAlignedRefIndexes": d0_descriptor.get("tableAlignedRefIndexes") or [],
        "d0DescriptorPointerRefSections": d0_descriptor.get("pointerRefSections") or [],
        "c0DescriptorSharedHandlerOpcodes": c0_descriptor.get("sharedHandlerOpcodes") or [],
        "c0DescriptorTableAlignedRefIndexes": c0_descriptor.get("tableAlignedRefIndexes") or [],
        "c0DescriptorPointerRefSections": c0_descriptor.get("pointerRefSections") or [],
        "e8DescriptorSharedHandlerOpcodes": e8_descriptor.get("sharedHandlerOpcodes") or [],
        "e8DescriptorTableAlignedRefIndexes": e8_descriptor.get("tableAlignedRefIndexes") or [],
        "e8DescriptorPointerRefSections": e8_descriptor.get("pointerRefSections") or [],
        "dispatchStopContextStatus": (dispatch_stop_context or {}).get("promotionStatus"),
        "leafTableRuntimeSelectionProven": (leaf_table_context or {}).get("runtimeSelectionProven"),
        "payloadDirectlyTargetsLeafTable": (opcode24_payload_table or {}).get("payloadDirectlyTargetsLeafTable"),
        "payloadGraphEdgeCount": (opcode24_payload_table or {}).get("payloadGraphEdgeCount"),
        "payloadGraphComponentCount": (opcode24_payload_table or {}).get("payloadGraphComponentCount"),
        "payloadGraphExternalEdgeCount": (opcode24_payload_table or {}).get("payloadGraphExternalEdgeCount"),
        "payloadGraphAllEdgesLocal": (opcode24_payload_table or {}).get("payloadGraphAllEdgesLocal"),
        "payloadGraphReachesFrontierTarget": (opcode24_payload_table or {}).get("payloadGraphReachesFrontierTarget"),
        "proofFound": False,
        "dataDescriptorOpcodeProofFound": False,
        "failedDataDescriptorGateIds": FAILED_DATA_DESCRIPTOR_GATE_IDS,
        "missingEvidence": MISSING_EVIDENCE,
        "evidenceRefs": evidence_refs,
        "evidenceRefCount": len(evidence_refs),
        "promotionStatus": "blocked",
        "remainingProofs": [
            "prove runtime selection of wrapper 0x00542a04 or frontier leaf 0x00542ae8",
            "prove the control path reaches reader 0x00542b0c after the descriptor boundary",
            "decode or observe the non-linear predecessor root path from 0x004783e0 to the fill fragment",
            "bridge the predecessor fill-fragment descriptor boundary at 0x004844dc without guessing through data rows",
            "find a strict map1_01a source coordinate or hotspot",
        ],
        "conclusion": conclusion,
    }


def markdown(summary: dict) -> str:
    lines = [
        "# Save Selector Data Descriptor Opcode Map",
        "",
        f"- route: {summary['source']} -> {summary['target']}",
        f"- handler table: `{summary['handlerTableVaHex']}`",
        f"- route and wrapper share 0xe8 descriptor: {summary['routeAndWrapperShareE8Descriptor']}",
        f"- e8 shared handler opcodes: {', '.join(summary['e8DescriptorSharedHandlerOpcodes']) or '-'}",
        f"- e8 table-aligned refs: {', '.join(summary['e8DescriptorTableAlignedRefIndexes']) or '-'}",
        f"- e8 descriptor refs: {', '.join(summary['e8DescriptorPointerRefSections']) or '-'}",
        f"- direct leaf uses distinct descriptor: {summary['directLeafUsesDistinctDescriptor']}",
        f"- predecessor root stop is data descriptor: {summary['predecessorRootStopIsDataDescriptor']}",
        f"- d0 shared handler opcodes: {', '.join(summary['d0DescriptorSharedHandlerOpcodes']) or '-'}",
        f"- predecessor fill stop is data descriptor: {summary['predecessorFillStopIsDataDescriptor']}",
        f"- c0 shared handler opcodes: {', '.join(summary['c0DescriptorSharedHandlerOpcodes']) or '-'}",
        f"- payload directly targets leaf table: {summary['payloadDirectlyTargetsLeafTable']}",
        f"- payload graph components: {summary['payloadGraphComponentCount']}",
        f"- payload graph all edges local: {summary['payloadGraphAllEdgesLocal']}",
        f"- payload graph reaches frontier target: {summary['payloadGraphReachesFrontierTarget']}",
        f"- proof found: {summary['proofFound']}",
        f"- failed data-descriptor gates: {', '.join(summary['failedDataDescriptorGateIds']) or '-'}",
        f"- missing evidence count: {len(summary['missingEvidence'])}",
        f"- evidence refs: {summary['evidenceRefCount']}",
        f"- promotion status: {summary['promotionStatus']}",
        "",
        summary["conclusion"],
        "",
        "## Relevant Sites",
        "",
        "| role | site | value | low byte | handler entry | handler value | section | code? |",
        "| --- | --- | --- | --- | --- | --- | --- | --- |",
    ]
    for row in summary["sites"]:
        lines.append(
            f"| {row['role']} | `{row['siteVaHex']}` | `{row['valueHex']}` | `{row['lowByteOpcodeHex']}` | "
            f"`{row['handlerEntryVaHex']}` | `{row['handlerValueHex']}` | {row.get('handlerSection') or '-'} | {row.get('isCodeHandler')} |"
        )
    lines.extend(["", "## Data Descriptors", ""])
    for descriptor in summary["dataDescriptors"]:
        lines.extend([
            f"### {descriptor['descriptorVaHex']}",
            "",
            f"- refs: {descriptor['pointerRefCount']} in {', '.join(descriptor['pointerRefSections']) or '-'}",
            f"- byte-opcode handler entries: {', '.join(descriptor['sharedHandlerOpcodes']) or '-'}",
            f"- table-aligned refs: {', '.join(descriptor['tableAlignedRefIndexes']) or '-'}",
            "",
            "| va | value | kind | value section |",
            "| --- | --- | --- | --- |",
        ])
        for row in descriptor["descriptorWindow"]:
            lines.append(
                f"| `{row['vaHex']}` | `{row['valueHex']}` | {row['kind']} | {row.get('valueSection') or '-'} |"
            )
        lines.extend(["", "Refs:", "", "| ref | section | byte opcode? | table index |", "| --- | --- | --- | --- |"])
        for ref in descriptor["refs"]:
            lines.append(
                f"| `{ref['refVaHex']}` | {ref['section']} | {ref.get('isByteOpcodeHandlerEntry')} | {ref.get('handlerTableOpcodeHex') or '-'} |"
            )
        lines.append("")
    lines.extend(["## Remaining Proofs", ""])
    for proof in summary["remainingProofs"]:
        lines.append(f"- {proof}")
    lines.extend(["", "## Missing Evidence", ""])
    for item in summary["missingEvidence"]:
        lines.append(f"- {item}")
    lines.extend(["", "## Evidence Refs", ""])
    for ref in summary["evidenceRefs"]:
        lines.append(f"- {ref['path']}: {', '.join(ref.get('fields') or []) or '-'}")
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    def esc(value: Any) -> str:
        return html.escape(str(value))

    site_rows = []
    for row in summary["sites"]:
        site_rows.append(
            "<tr>"
            f"<td>{esc(row['role'])}</td>"
            f"<td><code>{esc(row['siteVaHex'])}</code></td>"
            f"<td><code>{esc(row['valueHex'])}</code></td>"
            f"<td><code>{esc(row['lowByteOpcodeHex'])}</code></td>"
            f"<td><code>{esc(row['handlerEntryVaHex'])}</code></td>"
            f"<td><code>{esc(row['handlerValueHex'])}</code></td>"
            f"<td>{esc(row.get('handlerSection') or '-')}</td>"
            f"<td>{esc(row.get('isCodeHandler'))}</td>"
            "</tr>"
        )
    descriptor_sections = []
    for descriptor in summary["dataDescriptors"]:
        window_rows = []
        for row in descriptor["descriptorWindow"]:
            window_rows.append(
                "<tr>"
                f"<td><code>{esc(row['vaHex'])}</code></td>"
                f"<td><code>{esc(row['valueHex'])}</code></td>"
                f"<td>{esc(row['kind'])}</td>"
                f"<td>{esc(row.get('valueSection') or '-')}</td>"
                "</tr>"
            )
        ref_rows = []
        for ref in descriptor["refs"]:
            ref_rows.append(
                "<tr>"
                f"<td><code>{esc(ref['refVaHex'])}</code></td>"
                f"<td>{esc(ref['section'])}</td>"
                f"<td>{esc(ref.get('isByteOpcodeHandlerEntry'))}</td>"
                f"<td>{esc(ref.get('handlerTableOpcodeHex') or '-')}</td>"
                "</tr>"
            )
        descriptor_sections.append(
            f"<h2>{esc(descriptor['descriptorVaHex'])}</h2>"
            f"<p>refs: {esc(descriptor['pointerRefCount'])} in {esc(', '.join(descriptor['pointerRefSections']) or '-')}; "
            f"byte-opcode handler entries: {esc(', '.join(descriptor['sharedHandlerOpcodes']) or '-')}; "
            f"table-aligned refs: {esc(', '.join(descriptor['tableAlignedRefIndexes']) or '-')}</p>"
            "<table><thead><tr><th>va</th><th>value</th><th>kind</th><th>value section</th></tr></thead>"
            f"<tbody>{''.join(window_rows)}</tbody></table>"
            "<table><thead><tr><th>ref</th><th>section</th><th>byte opcode?</th><th>table index</th></tr></thead>"
            f"<tbody>{''.join(ref_rows)}</tbody></table>"
        )
    proof_items = "".join(f"<li>{esc(proof)}</li>" for proof in summary["remainingProofs"])
    missing_items = "".join(f"<li>{esc(item)}</li>" for item in summary["missingEvidence"])
    evidence_ref_items = "".join(
        f"<li><code>{esc(ref['path'])}</code>: {esc(', '.join(ref.get('fields') or []) or '-')}</li>"
        for ref in summary["evidenceRefs"]
    )
    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 Data Descriptor Opcode Map</title>",
        "  <style>body{margin:24px;background:#101010;color:#eee;font:14px system-ui,sans-serif}table{border-collapse:collapse;width:100%;margin:16px 0 28px}th,td{border:1px solid #333;padding:6px 8px;vertical-align:top}th{background:#1d1d1d}code{color:#f5d76e}</style>",
        "</head>",
        "<body>",
        "  <h1>Save Selector Data Descriptor Opcode Map</h1>",
        f"  <p>route <code>{esc(summary['source'])}</code> -&gt; <code>{esc(summary['target'])}</code>; "
        f"handler table <code>{esc(summary['handlerTableVaHex'])}</code>; promotion status: {esc(summary['promotionStatus'])}.</p>",
        f"  <p>route and wrapper share 0xe8 descriptor: {esc(summary['routeAndWrapperShareE8Descriptor'])}; "
        f"e8 shared handler opcodes: {esc(', '.join(summary['e8DescriptorSharedHandlerOpcodes']) or '-')}; "
        f"e8 table-aligned refs: {esc(', '.join(summary['e8DescriptorTableAlignedRefIndexes']) or '-')}; "
        f"direct leaf uses distinct descriptor: {esc(summary['directLeafUsesDistinctDescriptor'])}.</p>",
        f"  <p>predecessor root stop is data descriptor: {esc(summary['predecessorRootStopIsDataDescriptor'])}; "
        f"d0 shared handler opcodes: {esc(', '.join(summary['d0DescriptorSharedHandlerOpcodes']) or '-')}; "
        f"predecessor fill stop is data descriptor: {esc(summary['predecessorFillStopIsDataDescriptor'])}; "
        f"c0 shared handler opcodes: {esc(', '.join(summary['c0DescriptorSharedHandlerOpcodes']) or '-')}.</p>",
        f"  <p>payload graph components: {esc(summary['payloadGraphComponentCount'])}; "
        f"payload graph all edges local: {esc(summary['payloadGraphAllEdgesLocal'])}; "
        f"payload graph reaches frontier target: {esc(summary['payloadGraphReachesFrontierTarget'])}; "
        f"payload directly targets leaf table: {esc(summary['payloadDirectlyTargetsLeafTable'])}.</p>",
        f"  <p>proofFound: {esc(summary['proofFound'])}; "
        f"failedDataDescriptorGates: {esc(', '.join(summary['failedDataDescriptorGateIds']) or '-')}; "
        f"missingEvidenceCount: {esc(len(summary['missingEvidence']))}; "
        f"evidenceRefs: {esc(summary['evidenceRefCount'])}.</p>",
        f"  <p>{esc(summary['conclusion'])}</p>",
        "  <h2>Relevant Sites</h2>",
        "  <table><thead><tr><th>role</th><th>site</th><th>value</th><th>low byte</th><th>handler entry</th><th>handler value</th><th>section</th><th>code?</th></tr></thead>",
        f"  <tbody>{''.join(site_rows)}</tbody></table>",
        "\n".join(descriptor_sections),
        "  <h2>Remaining Proofs</h2>",
        f"  <ul>{proof_items}</ul>",
        "  <h2>Missing Evidence</h2>",
        f"  <ul>{missing_items}</ul>",
        "  <h2>Evidence Refs</h2>",
        f"  <ul>{evidence_ref_items}</ul>",
        "</body>",
        "</html>",
        "",
    ])


def write_outputs(summary: dict, out_dir: Path = OUT, html_out: Path | None = None) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "save_selector_data_descriptor_opcode_map.json").write_text(
        json.dumps(summary, ensure_ascii=False, separators=(",", ":")),
        encoding="utf-8",
    )
    if html_out is not None:
        html_out.parent.mkdir(parents=True, exist_ok=True)
        html_out.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("--dispatch-stop-context", type=Path, default=OUT / "save_selector_dispatch_stop_context.json")
    parser.add_argument("--leaf-table-context", type=Path, default=OUT / "save_selector_leaf_table_context.json")
    parser.add_argument("--opcode24-payload-table", type=Path, default=OUT / "save_selector_opcode24_payload_table.json")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    parser.add_argument("--html-out", type=Path, default=None, help="Optional HTML report output path.")
    args = parser.parse_args()
    summary = build_summary(
        args.exe.read_bytes(),
        json.loads(args.dispatch_stop_context.read_text(encoding="utf-8")) if args.dispatch_stop_context.exists() else {},
        json.loads(args.leaf_table_context.read_text(encoding="utf-8")) if args.leaf_table_context.exists() else {},
        json.loads(args.opcode24_payload_table.read_text(encoding="utf-8")) if args.opcode24_payload_table.exists() else {},
    )
    write_outputs(summary, args.out_dir, args.html_out)
    print(
        "wrote save-selector data descriptor opcode map "
        f"({len(summary['dataDescriptors'])} descriptors) -> {args.out_dir / 'save_selector_data_descriptor_opcode_map.json'}"
    )


if __name__ == "__main__":
    main()
