#!/usr/bin/env python3
"""Summarize runtime materializers for opcode 0x20 active descriptor order."""
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 find_cns_strings, offset_to_va, read_sections, va_to_offset


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

SAVE_SELECTOR_HANDLER_TABLE = 0x00440720
GENERAL_HANDLER_TABLE = 0x00440538
ADD_ACTIVE_SLOT_FUNCTION = 0x00431FE8
ADD_ACTIVE_SLOT_CORE_LABEL = 0x0043215C
REBUILD_ACTIVE_SLOT_DESCRIPTORS = 0x00432323
REMOVE_ACTIVE_SLOT_FUNCTION = 0x00432541
LOAD_REBUILD_CALL = 0x0042349C
SELECTOR_GROUP_LOAD = 0x004234A3
SELECTED_POINTER_STORE = 0x004234BA
ACTIVE_MUTATION_OPCODES = (0x62, 0x63)
DESCRIPTOR_SCRIPT_SCAN_DWORDS = 96

FAILED_OPCODE20_RUNTIME_MATERIALIZER_GATE_IDS = [
    "current-selector-2-0-active-order",
    "selected-root-runtime-materializer-execution",
    "normal-route-descriptor-mutation-observation",
    "route-promotion-proof",
]
OPCODE20_RUNTIME_MATERIALIZER_MISSING_EVIDENCE = [
    "current selector 2:0 active order/count from public or captured save data",
    "selected-root runtime execution through save-load rebuild or materializer path",
    "normal-route observation tying materialized active descriptor order to opcode 0x20 descriptor scripts",
    "route-promotion proof linking active order/materializers to map1_01a -> map2_02d",
]
OPCODE20_RUNTIME_MATERIALIZER_EVIDENCE_REFS = [
    {"path": "Hwanse2.exe", "description": "active slot materializer routines and handler table references"},
    {"path": "out/save_loader_trace.json", "description": "save-load block reads and rebuild-call ordering"},
    {"path": "out/save_selector_opcode20_descriptor_scripts.json", "description": "descriptor script mutation scan and opcode 0x20 self-mutation elimination"},
    {"path": "out/save_selector_opcode20_sample_order_effects.json", "description": "public sample active-order coverage gap"},
    {"path": "out/save_selector_leaf_streams.json", "description": "current selector leaf low-byte rows decoded through the save-selector table"},
]


def hex32(value: int) -> str:
    return f"0x{value:08x}"


def hex8(value: int) -> str:
    return f"0x{value:02x}"


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


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) -> 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_name_for_va(sections: list[dict], va: int | None) -> str | None:
    if va is None:
        return None
    for section in sections:
        start = section["va"]
        end = start + section["raw_size"]
        if start <= va < end:
            return section["name"]
    return None


def text_bytes(exe: bytes, sections: list[dict]) -> tuple[int, bytes]:
    text = next(section for section in sections if section["name"] == ".text")
    return text["va"], exe[text["raw"] : text["raw"] + text["raw_size"]]


def relative_call_refs(exe: bytes, sections: list[dict], target: int) -> list[dict]:
    text_va, raw = text_bytes(exe, sections)
    rows = []
    for index in range(0, len(raw) - 4):
        if raw[index] != 0xE8:
            continue
        rel = struct.unpack_from("<i", raw, index + 1)[0]
        call_va = text_va + index
        resolved = call_va + 5 + rel
        if resolved == target:
            rows.append({
                "callVaHex": hex32(call_va),
                "targetVaHex": hex32(target),
            })
    return rows


def handler_table_row(exe: bytes, sections: list[dict], table_va: int, opcode: int) -> dict:
    entry_va = table_va + opcode * 4
    handler_va = dword_at(exe, sections, entry_va)
    return {
        "opcode": opcode,
        "opcodeHex": hex8(opcode),
        "entryVaHex": hex32(entry_va),
        "handlerVaHex": hex32(handler_va) if handler_va is not None else None,
        "handlerSection": section_name_for_va(sections, handler_va),
    }


def materializer_rows(exe: bytes, sections: list[dict]) -> list[dict]:
    rows = [
        {
            "name": "add active save/object slot",
            "functionStartVaHex": hex32(ADD_ACTIVE_SLOT_FUNCTION),
            "coreLabelVaHex": hex32(ADD_ACTIVE_SLOT_CORE_LABEL),
            "generalOpcodeHex": "0x62",
            "generalHandlerVaHex": "0x00407cc1",
            "directCallTargetVaHex": hex32(ADD_ACTIVE_SLOT_FUNCTION),
            "coreWrites": [
                "0x00432167 writes 0x004576e9[count]",
                "0x00432188 writes 0x0059db30[count]",
                "0x0043218f increments 0x004576e8",
                "0x004321cd writes slot descriptor pointer",
            ],
            "meaning": "Called by the general-table opcode 0x62 handler with stream+1 as the descriptor id.",
        },
        {
            "name": "rebuild active slot descriptors",
            "functionStartVaHex": hex32(REBUILD_ACTIVE_SLOT_DESCRIPTORS),
            "coreLabelVaHex": hex32(REBUILD_ACTIVE_SLOT_DESCRIPTORS),
            "generalOpcodeHex": None,
            "generalHandlerVaHex": None,
            "directCallTargetVaHex": hex32(REBUILD_ACTIVE_SLOT_DESCRIPTORS),
            "coreWrites": [
                "0x004323ba writes 0x0059db30[index]",
                "0x004323dd writes slot descriptor pointer",
            ],
            "meaning": "Called after save blocks are loaded to materialize descriptor pointers from saved count/order bytes.",
        },
        {
            "name": "remove/rebuild active slot descriptors",
            "functionStartVaHex": hex32(REMOVE_ACTIVE_SLOT_FUNCTION),
            "coreLabelVaHex": hex32(REMOVE_ACTIVE_SLOT_FUNCTION),
            "generalOpcodeHex": "0x63",
            "generalHandlerVaHex": "0x00407ce5",
            "directCallTargetVaHex": hex32(REMOVE_ACTIVE_SLOT_FUNCTION),
            "coreWrites": [
                "0x004326a3 compacts 0x004576e9 order bytes",
                "0x004326b6 compacts 0x0059db30 slot bases",
                "0x004326c2 decrements 0x004576e8",
                "0x0043271f writes slot descriptor pointer",
            ],
            "meaning": "Called by the general-table opcode 0x63 handler with stream+1 as the descriptor id to remove.",
        },
    ]
    for row in rows:
        target = parse_hex(row.get("directCallTargetVaHex"))
        row["relativeCallRefs"] = relative_call_refs(exe, sections, target) if target is not None else []
        row["relativeCallRefCount"] = len(row["relativeCallRefs"])
    return rows


def load_rebuild_evidence(save_loader_trace: dict, materializers: list[dict]) -> dict:
    read_calls = [
        parse_hex(row.get("callVaHex"))
        for row in save_loader_trace.get("saveReadBlocks") or []
    ]
    read_calls = [value for value in read_calls if value is not None]
    rebuild_calls = next(
        (row.get("relativeCallRefs") or [] for row in materializers if row["functionStartVaHex"] == hex32(REBUILD_ACTIVE_SLOT_DESCRIPTORS)),
        [],
    )
    rebuild_call_values = [parse_hex(row.get("callVaHex")) for row in rebuild_calls]
    rebuild_call_values = [value for value in rebuild_call_values if value is not None]
    last_save_read_call = max(read_calls) if read_calls else None
    rebuild_call = rebuild_call_values[0] if rebuild_call_values else None
    return {
        "saveReadCallVaHexes": [hex32(value) for value in read_calls],
        "lastSaveReadCallVaHex": hex32(last_save_read_call) if last_save_read_call is not None else None,
        "rebuildCallVaHex": hex32(rebuild_call) if rebuild_call is not None else None,
        "rebuildFunctionVaHex": hex32(REBUILD_ACTIVE_SLOT_DESCRIPTORS),
        "selectorGroupLoadVaHex": hex32(SELECTOR_GROUP_LOAD),
        "selectedPointerStoreVaHex": hex32(SELECTED_POINTER_STORE),
        "rebuildAfterSaveReadBlocks": (
            rebuild_call is not None
            and last_save_read_call is not None
            and last_save_read_call < rebuild_call
        ),
        "rebuildBeforeSelectorPointerSelection": (
            rebuild_call is not None
            and rebuild_call < SELECTOR_GROUP_LOAD < SELECTED_POINTER_STORE
        ),
        "meaning": (
            "The save-load path reads the count/order and slot blocks, calls 0x00432323, then selects "
            "the save selector pointer from 0x004576da/0x004576db."
        ),
    }


def handler_rows(exe: bytes, sections: list[dict]) -> dict:
    return {
        "generalHandlerTableVaHex": hex32(GENERAL_HANDLER_TABLE),
        "saveSelectorHandlerTableVaHex": hex32(SAVE_SELECTOR_HANDLER_TABLE),
        "generalMutationHandlers": [
            {
                **handler_table_row(exe, sections, GENERAL_HANDLER_TABLE, opcode),
                "mutationRole": "add active slot" if opcode == 0x62 else "remove active slot",
            }
            for opcode in ACTIVE_MUTATION_OPCODES
        ],
        "saveSelectorSameLowByteHandlers": [
            {
                **handler_table_row(exe, sections, SAVE_SELECTOR_HANDLER_TABLE, opcode),
                "mutationRole": "not an active-order materializer in the save-selector table",
            }
            for opcode in ACTIVE_MUTATION_OPCODES
        ],
    }


def value_kind(word: dict, strings: dict[int, str], sections: list[dict], value: int) -> str:
    if word.get("cns") or value in strings:
        return "cns"
    if word.get("pointer") is True or va_to_offset(sections, value) is not None:
        return "pointer"
    return "scalar"


def current_route_low_byte_rows(
    exe: bytes,
    sections: list[dict],
    strings: dict[int, str],
    leaf_streams: list[dict],
) -> list[dict]:
    rows = []
    for stream in leaf_streams:
        for stream_kind, words_key, stream_va_key in (
            ("leaf", "words", "leafPointerHex"),
            ("nested", "nestedWords", "nestedPointerHex"),
        ):
            stream_va = stream.get(stream_va_key)
            if not stream_va:
                continue
            for word in stream.get(words_key) or []:
                value = parse_hex(word.get("valueHex"))
                word_va = word.get("vaHex")
                if value is None or word_va is None:
                    continue
                opcode = value & 0xFF
                if opcode not in ACTIVE_MUTATION_OPCODES:
                    continue
                save_handler = handler_table_row(exe, sections, SAVE_SELECTOR_HANDLER_TABLE, opcode)
                general_handler = handler_table_row(exe, sections, GENERAL_HANDLER_TABLE, opcode)
                rows.append({
                    "streamKind": stream_kind,
                    "streamVaHex": stream_va,
                    "wordIndex": word.get("index"),
                    "wordVaHex": word_va,
                    "valueHex": word.get("valueHex"),
                    "lowByteHex": hex8(opcode),
                    "valueKind": value_kind(word, strings, sections, value),
                    "cns": word.get("cns") or strings.get(value),
                    "saveSelectorHandlerVaHex": save_handler.get("handlerVaHex"),
                    "generalHandlerVaHex": general_handler.get("handlerVaHex"),
                    "isGeneralMutationInThisStream": False,
                    "reason": "current route leaf/nested rows are decoded through the save-selector table, not the general nested-object table",
                })
    return rows


def descriptor_script_mutation_rows(
    exe: bytes,
    sections: list[dict],
    strings: dict[int, str],
    descriptor_scripts: dict,
) -> list[dict]:
    rows = []
    for descriptor in descriptor_scripts.get("descriptorRows") or []:
        for field, slot_name in (
            ("script0VaHex", "script+0"),
            ("script4VaHex", "script+4"),
            ("script8VaHex", "script+8"),
        ):
            script_va = parse_hex(descriptor.get(field))
            if script_va is None:
                continue
            for index in range(DESCRIPTOR_SCRIPT_SCAN_DWORDS):
                word_va = script_va + index * 4
                value = dword_at(exe, sections, word_va)
                if value is None:
                    break
                opcode = value & 0xFF
                if opcode not in ACTIVE_MUTATION_OPCODES:
                    continue
                rows.append({
                    "descriptorIndex": descriptor.get("index"),
                    "scriptSlot": slot_name,
                    "scriptVaHex": hex32(script_va),
                    "wordIndex": index,
                    "wordVaHex": hex32(word_va),
                    "valueHex": hex32(value),
                    "lowByteHex": hex8(opcode),
                    "valueKind": "cns" if value in strings else "pointer" if va_to_offset(sections, value) is not None else "scalar",
                    "cns": strings.get(value),
                })
    return rows


def sample_order_summary(sample_order_effects: dict) -> dict:
    sample_rows = sample_order_effects.get("sampleRows") or []
    descriptor_sets: list[list[int]] = []
    for row in sample_rows:
        active = row.get("activeDescriptorIndices") or []
        if active not in descriptor_sets:
            descriptor_sets.append(active)
    return {
        "sampleCount": sample_order_effects.get("sampleCount"),
        "uniqueSampleSignatures": sample_order_effects.get("uniqueSampleSignatures") or [],
        "sampleActiveDescriptorSets": descriptor_sets,
        "currentFrontierSelector": sample_order_effects.get("currentFrontierSelector"),
        "currentFrontierSampleCovered": sample_order_effects.get("currentFrontierSampleCovered"),
    }


def build_summary(
    exe: bytes,
    save_loader_trace: dict | None = None,
    descriptor_scripts: dict | None = None,
    sample_order_effects: dict | None = None,
    leaf_streams: list[dict] | None = None,
) -> dict:
    sections = read_sections(exe)
    strings = find_cns_strings(exe, sections)
    save_loader_trace = save_loader_trace if save_loader_trace is not None else load_json(OUT / "save_loader_trace.json", {})
    descriptor_scripts = (
        descriptor_scripts
        if descriptor_scripts is not None
        else load_json(OUT / "save_selector_opcode20_descriptor_scripts.json", {})
    )
    sample_order_effects = (
        sample_order_effects
        if sample_order_effects is not None
        else load_json(OUT / "save_selector_opcode20_sample_order_effects.json", {})
    )
    leaf_streams = leaf_streams if leaf_streams is not None else load_json(OUT / "save_selector_leaf_streams.json", [])

    materializers = materializer_rows(exe, sections)
    handler_table = handler_rows(exe, sections)
    route_rows = current_route_low_byte_rows(exe, sections, strings, leaf_streams)
    descriptor_mutations = descriptor_script_mutation_rows(exe, sections, strings, descriptor_scripts)
    sample_summary = sample_order_summary(sample_order_effects)
    conclusion = (
        "The active order/count path is now separated from the save-selector stream. Save load materializes "
        "descriptor pointers by calling 0x00432323 after the save blocks are read and before the selector pointer is "
        "chosen. Later add/remove mutations are general nested-script opcodes 0x62/0x63 through table 0x00440538. "
        "The current selector 2:0 leaf rows with low byte 0x63 are CNS/resource words decoded through the "
        "save-selector table, not the general remove handler, and the scanned opcode 0x20 descriptor scripts contain "
        "no general 0x62/0x63 rows. This removes an opcode 0x20 self-mutation path, but it still does not prove the "
        "current selector 2:0 active order because public samples cover selectors 0:0, 1:0, and 22:0 only."
    )
    return {
        "source": SOURCE,
        "target": TARGET,
        "currentFrontierSelector": "2:0",
        "countRuntimeVaHex": "0x004576e8",
        "orderBytesVaHex": "0x004576e9",
        "slotBaseHex": "0x00457750",
        "materializers": materializers,
        "loadRebuildEvidence": load_rebuild_evidence(save_loader_trace, materializers),
        "handlerTables": handler_table,
        "currentRouteSameLowByteRows": route_rows,
        "currentRouteSameLowByteRowCount": len(route_rows),
        "currentRouteGeneralMutationEvidenceCount": sum(1 for row in route_rows if row["isGeneralMutationInThisStream"]),
        "descriptorScriptMutationRows": descriptor_mutations,
        "descriptorScriptMutationRowCount": len(descriptor_mutations),
        "descriptorScriptScanDwordCount": DESCRIPTOR_SCRIPT_SCAN_DWORDS,
        "sampleOrderSummary": sample_summary,
        "opcode20SelfMutationPathEliminated": len(descriptor_mutations) == 0,
        "currentFrontierActiveOrderProven": False,
        "controlPathProofStatus": "blocked",
        "proofFound": False,
        "opcode20RuntimeMaterializerProofFound": False,
        "failedOpcode20RuntimeMaterializerGateIds": FAILED_OPCODE20_RUNTIME_MATERIALIZER_GATE_IDS,
        "missingEvidence": OPCODE20_RUNTIME_MATERIALIZER_MISSING_EVIDENCE,
        "evidenceRefs": OPCODE20_RUNTIME_MATERIALIZER_EVIDENCE_REFS,
        "evidenceRefCount": len(OPCODE20_RUNTIME_MATERIALIZER_EVIDENCE_REFS),
        "promotionStatus": "blocked",
        "conclusion": conclusion,
    }


def markdown(summary: dict) -> str:
    load = summary["loadRebuildEvidence"]
    sample = summary["sampleOrderSummary"]
    lines = [
        "# Save Selector Opcode 0x20 Runtime Materializers",
        "",
        f"- route: {summary['source']} -> {summary['target']}",
        f"- current frontier selector: `{summary['currentFrontierSelector']}`",
        f"- count byte: `{summary['countRuntimeVaHex']}`",
        f"- order bytes: `{summary['orderBytesVaHex']}`",
        f"- slot base: `{summary['slotBaseHex']}`",
        f"- load rebuild call: `{load['rebuildCallVaHex']}` -> `{load['rebuildFunctionVaHex']}`",
        f"- rebuild after save read blocks: {load['rebuildAfterSaveReadBlocks']}",
        f"- rebuild before selector pointer selection: {load['rebuildBeforeSelectorPointerSelection']}",
        f"- current route same-low-byte rows: {summary['currentRouteSameLowByteRowCount']}",
        f"- current route general mutation evidence rows: {summary['currentRouteGeneralMutationEvidenceCount']}",
        f"- descriptor script general 0x62/0x63 mutation rows: {summary['descriptorScriptMutationRowCount']}",
        f"- opcode 0x20 self-mutation path eliminated: {summary['opcode20SelfMutationPathEliminated']}",
        f"- current frontier active order proven: {summary['currentFrontierActiveOrderProven']}",
        f"- proofFound: `{summary['proofFound']}`",
        f"- opcode20RuntimeMaterializerProofFound: `{summary['opcode20RuntimeMaterializerProofFound']}`",
        f"- promotion status: `{summary['promotionStatus']}`",
        "",
        summary["conclusion"],
        "",
        "## Failed Gates",
        "",
    ]
    lines.extend(f"- `{item}`" for item in summary["failedOpcode20RuntimeMaterializerGateIds"])
    lines.extend([
        "",
        "## Missing Evidence",
        "",
    ])
    lines.extend(f"- {item}" for item in summary["missingEvidence"])
    lines.extend([
        "",
        "## Evidence Refs",
        "",
    ])
    lines.extend(
        f"- `{row['path']}`: {row['description']}"
        for row in summary["evidenceRefs"]
    )
    lines.extend([
        "",
        "## Handler Tables",
        "",
        "| table | opcode | handler | role |",
        "| --- | --- | --- | --- |",
    ])
    tables = summary["handlerTables"]
    for row in tables["generalMutationHandlers"]:
        lines.append(
            f"| general `{tables['generalHandlerTableVaHex']}` | `{row['opcodeHex']}` | `{row['handlerVaHex']}` | {row['mutationRole']} |"
        )
    for row in tables["saveSelectorSameLowByteHandlers"]:
        lines.append(
            f"| save-selector `{tables['saveSelectorHandlerTableVaHex']}` | `{row['opcodeHex']}` | `{row['handlerVaHex']}` | {row['mutationRole']} |"
        )
    lines.extend([
        "",
        "## Materializers",
        "",
        "| name | function | core label | direct calls | meaning |",
        "| --- | --- | --- | --- | --- |",
    ])
    for row in summary["materializers"]:
        calls = ", ".join(call["callVaHex"] for call in row["relativeCallRefs"]) or "-"
        lines.append(
            f"| {row['name']} | `{row['functionStartVaHex']}` | `{row['coreLabelVaHex']}` | {calls} | {row['meaning']} |"
        )
    lines.extend([
        "",
        "## Current Route Same-Low-Byte Rows",
        "",
        "| stream | word | value | kind | save-selector handler | general handler | reason |",
        "| --- | --- | --- | --- | --- | --- | --- |",
    ])
    for row in summary["currentRouteSameLowByteRows"][:24]:
        lines.append(
            f"| `{row['streamVaHex']}` {row['streamKind']} | `{row['wordVaHex']}` | `{row['valueHex']}` | "
            f"{row['valueKind']} {row.get('cns') or ''} | `{row['saveSelectorHandlerVaHex']}` | "
            f"`{row['generalHandlerVaHex']}` | {row['reason']} |"
        )
    if not summary["currentRouteSameLowByteRows"]:
        lines.append("| - | - | - | - | - | - | - |")
    lines.extend([
        "",
        "## Public Sample Order",
        "",
        f"- sample count: {sample.get('sampleCount')}",
        f"- unique signatures: {', '.join(sample.get('uniqueSampleSignatures') or [])}",
        f"- active descriptor sets: {sample.get('sampleActiveDescriptorSets')}",
        f"- current frontier sample covered: {sample.get('currentFrontierSampleCovered')}",
        "",
    ])
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    tables = summary["handlerTables"]
    handler_rows = []
    for row in tables["generalMutationHandlers"]:
        handler_rows.append(
            f"<tr><td>general <code>{html.escape(tables['generalHandlerTableVaHex'])}</code></td>"
            f"<td><code>{html.escape(row['opcodeHex'])}</code></td>"
            f"<td><code>{html.escape(row['handlerVaHex'] or '-')}</code></td>"
            f"<td>{html.escape(row['mutationRole'])}</td></tr>"
        )
    for row in tables["saveSelectorSameLowByteHandlers"]:
        handler_rows.append(
            f"<tr><td>save-selector <code>{html.escape(tables['saveSelectorHandlerTableVaHex'])}</code></td>"
            f"<td><code>{html.escape(row['opcodeHex'])}</code></td>"
            f"<td><code>{html.escape(row['handlerVaHex'] or '-')}</code></td>"
            f"<td>{html.escape(row['mutationRole'])}</td></tr>"
        )
    materializer_rows_html = []
    for row in summary["materializers"]:
        calls = ", ".join(call["callVaHex"] for call in row["relativeCallRefs"]) or "-"
        materializer_rows_html.append(
            "<tr>"
            f"<td>{html.escape(row['name'])}</td>"
            f"<td><code>{html.escape(row['functionStartVaHex'])}</code></td>"
            f"<td><code>{html.escape(row['coreLabelVaHex'])}</code></td>"
            f"<td>{html.escape(calls)}</td>"
            f"<td>{html.escape(row['meaning'])}</td>"
            "</tr>"
        )
    route_rows_html = []
    for row in summary["currentRouteSameLowByteRows"][:48]:
        route_rows_html.append(
            "<tr>"
            f"<td><code>{html.escape(row['streamVaHex'])}</code> {html.escape(row['streamKind'])}</td>"
            f"<td><code>{html.escape(row['wordVaHex'])}</code></td>"
            f"<td><code>{html.escape(row['valueHex'])}</code></td>"
            f"<td>{html.escape(row['valueKind'])} {html.escape(row.get('cns') or '')}</td>"
            f"<td><code>{html.escape(row['saveSelectorHandlerVaHex'] or '-')}</code></td>"
            f"<td><code>{html.escape(row['generalHandlerVaHex'] or '-')}</code></td>"
            f"<td>{html.escape(row['reason'])}</td>"
            "</tr>"
        )
    sample = summary["sampleOrderSummary"]
    load = summary["loadRebuildEvidence"]
    failed_gates = "".join(
        f"<li><code>{html.escape(item)}</code></li>"
        for item in summary["failedOpcode20RuntimeMaterializerGateIds"]
    )
    missing_evidence = "".join(
        f"<li>{html.escape(item)}</li>"
        for item in summary["missingEvidence"]
    )
    evidence_refs = "".join(
        f"<li><code>{html.escape(row['path'])}</code>: {html.escape(row['description'])}</li>"
        for row 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 Opcode 0x20 Runtime Materializers</title>",
        "  <style>",
        "    :root { color-scheme: dark; font-family: system-ui, sans-serif; background: #101010; color: #eee; }",
        "    body { margin: 0; padding: 24px; }",
        "    h1 { margin: 0 0 8px; font-size: 24px; }",
        "    h2 { margin: 24px 0 8px; font-size: 18px; }",
        "    p { max-width: 1120px; color: #bbb; line-height: 1.45; }",
        "    table { width: 100%; border-collapse: collapse; margin: 12px 0 20px; font-size: 13px; }",
        "    th, td { border-bottom: 1px solid #303030; padding: 7px 8px; text-align: left; vertical-align: top; }",
        "    th { position: sticky; top: 0; background: #181818; color: #ddd; }",
        "    code { color: #d7f0ff; }",
        "  </style>",
        "</head>",
        "<body>",
        "  <h1>Save Selector Opcode 0x20 Runtime Materializers</h1>",
        f"  <p>route {html.escape(summary['source'])} -&gt; {html.escape(summary['target'])}; current selector <code>{html.escape(summary['currentFrontierSelector'])}</code>; load rebuild call <code>{html.escape(load['rebuildCallVaHex'] or '-')}</code>; descriptor mutation rows {summary['descriptorScriptMutationRowCount']}; opcode 0x20 self-mutation path eliminated: {summary['opcode20SelfMutationPathEliminated']}; current frontier active order proven: {summary['currentFrontierActiveOrderProven']}; proofFound <code>{html.escape(str(summary['proofFound']))}</code>; promotion status <code>{html.escape(summary['promotionStatus'])}</code>.</p>",
        f"  <p>{html.escape(summary['conclusion'])}</p>",
        "  <h2>Failed Gates</h2>",
        f"  <ul>{failed_gates}</ul>",
        "  <h2>Missing Evidence</h2>",
        f"  <ul>{missing_evidence}</ul>",
        "  <h2>Evidence Refs</h2>",
        f"  <ul>{evidence_refs}</ul>",
        "  <h2>Handler Tables</h2>",
        "  <table><thead><tr><th>table</th><th>opcode</th><th>handler</th><th>role</th></tr></thead><tbody>",
        "\n".join(handler_rows),
        "  </tbody></table>",
        "  <h2>Materializers</h2>",
        "  <table><thead><tr><th>name</th><th>function</th><th>core label</th><th>direct calls</th><th>meaning</th></tr></thead><tbody>",
        "\n".join(materializer_rows_html),
        "  </tbody></table>",
        "  <h2>Current Route Same-Low-Byte Rows</h2>",
        "  <table><thead><tr><th>stream</th><th>word</th><th>value</th><th>kind</th><th>save-selector handler</th><th>general handler</th><th>reason</th></tr></thead><tbody>",
        "\n".join(route_rows_html) or '<tr><td colspan="7">-</td></tr>',
        "  </tbody></table>",
        "  <h2>Public Sample Order</h2>",
        f"  <p>sample count {sample.get('sampleCount')}; unique signatures {html.escape(', '.join(sample.get('uniqueSampleSignatures') or []))}; active descriptor sets {html.escape(str(sample.get('sampleActiveDescriptorSets')))}; current frontier sample covered {sample.get('currentFrontierSampleCovered')}.</p>",
        "</body>",
        "</html>",
        "",
    ])


def write_outputs(summary: dict, out_dir: Path = OUT) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "save_selector_opcode20_runtime_materializers.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    (out_dir / "save_selector_opcode20_runtime_materializers.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("--save-loader-trace", type=Path, default=OUT / "save_loader_trace.json")
    parser.add_argument("--descriptor-scripts", type=Path, default=OUT / "save_selector_opcode20_descriptor_scripts.json")
    parser.add_argument("--sample-order-effects", type=Path, default=OUT / "save_selector_opcode20_sample_order_effects.json")
    parser.add_argument("--leaf-streams", type=Path, default=OUT / "save_selector_leaf_streams.json")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    args = parser.parse_args()
    summary = build_summary(
        args.exe.read_bytes(),
        load_json(args.save_loader_trace, {}),
        load_json(args.descriptor_scripts, {}),
        load_json(args.sample_order_effects, {}),
        load_json(args.leaf_streams, []),
    )
    write_outputs(summary, args.out_dir)
    print(f"wrote opcode 0x20 runtime materializers -> {args.out_dir / 'save_selector_opcode20_runtime_materializers.html'}")


if __name__ == "__main__":
    main()
