#!/usr/bin/env python3
"""Summarize descriptor scripts reached by save-selector opcode 0x20 mode 0."""
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, read_sections, va_to_offset
from summarize_save_selector_opcode20_nested_base_modes import GENERAL_HANDLER_NOTES


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

DESCRIPTOR_TABLE_VA = 0x00442D95
DESCRIPTOR_LIMIT = 12
SCRIPT_SCAN_DWORDS = 96
GATE_OFFSETS = {0xE8, 0xEA}
SELECTION_OPCODES = {0x10, 0x11, 0x12, 0x13}
BASE_SETTER_OPCODES = {0x28, 0x40, 0x41, 0x42, 0x43, 0x44}

FAILED_OPCODE20_DESCRIPTOR_SCRIPT_GATE_IDS = [
    "runtime-active-order",
    "descriptor-script-field-map-or-frontier-proof",
    "descriptor-script-gate-base-proof",
    "strict-source-hotspot",
]
OPCODE20_DESCRIPTOR_SCRIPT_MISSING_EVIDENCE = [
    "runtime active order/count selecting the current descriptor+4 script",
    "field-map/frontier/encoded route-target evidence in descriptor scripts",
    "descriptor script reader/writer hitting gate offsets 0xe8/0xea with a specific base",
    "strict map1_01a source hotspot or equivalent route trigger",
]
OPCODE20_DESCRIPTOR_SCRIPT_EVIDENCE_REFS = [
    {"path": "Hwanse2.exe", "description": "descriptor table, script dword, CNS, and gate-offset scans"},
    {"path": "out/save_selector_opcode20_slot_descriptor_writers.json", "description": "descriptor table and runtime active-order writer path"},
    {"path": "out/save_selector_current_root_frontier_paths.json", "description": "current frontier/root/source/target reference addresses for encoded-target scans"},
]

DEFAULT_FRONTIER_REFS = {
    "currentRoot": 0x00540714,
    "frontierLeaf": 0x00542AE8,
    "frontierReader": 0x00542B0C,
    "sourceRecord": 0x00542B44,
    "targetRecord": 0x00542BAC,
}


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


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


def sign16(value: int) -> int:
    return value - 0x10000 if value & 0x8000 else value


def sign32(value: int) -> int:
    return value - 0x100000000 if value & 0x80000000 else value


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 is_pointer_dword(sections: list[dict], value: int) -> bool:
    return va_to_offset(sections, value) is not None


def field_record_for_cns(exe: bytes, sections: list[dict], row_va: int, name: str) -> dict | None:
    if not name.startswith("map") or name.startswith("map_"):
        return None
    scene_id = dword_at(exe, sections, row_va + 4)
    return {
        "recordVaHex": hex32(row_va),
        "filename": name,
        "map": name[:-4],
        "sceneIdHex": f"0x{scene_id:04x}" if scene_id is not None else None,
    }


def selection_operation(opcode: int) -> str:
    return {
        0x10: "fillBranchStateTable",
        0x11: "readSelectedStateAndBranch",
        0x12: "selectActiveStateSlot",
        0x13: "selectMatchingRuntimeSlot",
    }.get(opcode, "other")


def state_table(stream_plus_1: int) -> str:
    return "primaryBranchState" if stream_plus_1 == 0 else "secondaryBranchState"


def histogram_rows(counts: dict[int, int]) -> list[dict]:
    return [
        {
            "value": value,
            "valueHex": hex8(value),
            "count": count,
        }
        for value, count in sorted(counts.items())
    ]


def string_histogram_rows(counts: dict[str, int]) -> list[dict]:
    return [
        {
            "value": value,
            "count": count,
        }
        for value, count in sorted(counts.items())
    ]


def merge_string_histograms(histograms: list[list[dict]]) -> list[dict]:
    counts: dict[str, int] = {}
    for histogram in histograms:
        for row in histogram:
            value = row.get("value")
            if value is None:
                continue
            counts[value] = counts.get(value, 0) + (row.get("count") or 0)
    return string_histogram_rows(counts)


def context_a8_operand_source(opcode: int, stream_plus_1: int, stream_plus_2: int) -> str:
    if opcode == 0x42:
        if stream_plus_1 == 0x00:
            return "context-f2-runtime-object-pointer"
        if stream_plus_1 == 0x01:
            return f"stream2-object-pointer-{hex8(stream_plus_2)}"
        return f"unsupported-object-pointer-mode-{hex8(stream_plus_1)}"
    if opcode == 0x40:
        return "global-selection-buffer"
    if opcode == 0x41:
        return f"slot-base-{hex8(stream_plus_1)}"
    if opcode == 0x43:
        return "save-runtime-block-base"
    if opcode == 0x28:
        return f"saved-pointer-table-{hex8(stream_plus_2)}"
    return "unknown-context-a8-source"


def frontier_ref_targets(frontier_refs: dict[str, int]) -> list[dict]:
    current_labels = {"currentRoot", "frontierLeaf", "frontierReader"}
    route_record_labels = {"sourceRecord", "targetRecord"}
    rows = []
    for label, va in sorted(frontier_refs.items()):
        if label in current_labels:
            group = "current"
        elif label in route_record_labels:
            group = "route-record"
        else:
            group = "route-context"
        rows.append({
            "label": label,
            "va": va,
            "vaHex": hex32(va),
            "group": group,
        })
    return rows


def encoded_target_rows_for_dword(
    row_va: int,
    value: int,
    targets: list[dict],
    index: int,
    slot_name: str,
) -> list[dict]:
    values = [
        ("abs16-low", value & 0xFFFF, None),
        ("signed-rel16-site-plus2", sign16(value & 0xFFFF), row_va + 2),
        ("signed-rel32-site-plus4", sign32(value), row_va + 4),
    ]
    rows = []
    for kind, scalar, rel_base in values:
        for target in targets:
            target_va = target["va"]
            if kind == "abs16-low":
                matches = scalar == (target_va & 0xFFFF)
            else:
                matches = rel_base is not None and rel_base + scalar == target_va
            if not matches:
                continue
            rows.append({
                "slot": slot_name,
                "index": index,
                "siteVaHex": hex32(row_va),
                "valueHex": hex32(value),
                "kind": kind,
                "scalarHex": hex32(scalar & 0xFFFFFFFF),
                "targetHex": hex32(target_va),
                "targetLabel": target["label"],
                "targetGroup": target["group"],
                "promotionStatus": "blocked",
                "promotesRouteExecution": False,
            })
    return rows


def encoded_target_summary(rows: list[dict]) -> dict:
    group_counts: dict[str, int] = {}
    label_counts: dict[str, int] = {}
    kind_counts: dict[str, int] = {}
    for row in rows:
        group = row.get("targetGroup")
        label = row.get("targetLabel")
        kind = row.get("kind")
        if group:
            group_counts[group] = group_counts.get(group, 0) + 1
        if label:
            label_counts[label] = label_counts.get(label, 0) + 1
        if kind:
            kind_counts[kind] = kind_counts.get(kind, 0) + 1
    route_proof_rows = [
        row for row in rows
        if row.get("targetGroup") in {"current", "route-record"}
    ]
    route_context_rows = [
        row for row in rows
        if row.get("targetGroup") == "route-context"
    ]
    return {
        "classification": (
            "descriptor-script-encoded-route-target-scalars-nonpromoting"
            if rows
            else "no-encoded-route-target-scalars-in-descriptor-scripts"
        ),
        "rawScalarCandidateCount": len(rows),
        "routeProofRawScalarCandidateCount": len(route_proof_rows),
        "routeContextRawScalarCandidateCount": len(route_context_rows),
        "promotingCandidateCount": 0,
        "targetGroupCounts": dict(sorted(group_counts.items())),
        "targetLabelCounts": dict(sorted(label_counts.items())),
        "kindCounts": dict(sorted(kind_counts.items())),
        "sampleRows": rows[:24],
        "promotionStatus": "blocked",
    }


def merge_encoded_target_scans(scans: list[dict]) -> dict:
    raw_count = sum(scan.get("rawScalarCandidateCount") or 0 for scan in scans)
    route_proof_count = sum(scan.get("routeProofRawScalarCandidateCount") or 0 for scan in scans)
    route_context_count = sum(scan.get("routeContextRawScalarCandidateCount") or 0 for scan in scans)
    promoting_count = sum(scan.get("promotingCandidateCount") or 0 for scan in scans)
    group_counts: dict[str, int] = {}
    label_counts: dict[str, int] = {}
    kind_counts: dict[str, int] = {}
    sample_rows = []
    for scan in scans:
        for key, count in (scan.get("targetGroupCounts") or {}).items():
            group_counts[key] = group_counts.get(key, 0) + count
        for key, count in (scan.get("targetLabelCounts") or {}).items():
            label_counts[key] = label_counts.get(key, 0) + count
        for key, count in (scan.get("kindCounts") or {}).items():
            kind_counts[key] = kind_counts.get(key, 0) + count
        if len(sample_rows) < 24:
            sample_rows.extend((scan.get("sampleRows") or [])[:24 - len(sample_rows)])
    return {
        "classification": (
            "descriptor-script-encoded-route-target-scalars-nonpromoting"
            if raw_count
            else "no-encoded-route-target-scalars-in-descriptor-scripts"
        ),
        "rawScalarCandidateCount": raw_count,
        "routeProofRawScalarCandidateCount": route_proof_count,
        "routeContextRawScalarCandidateCount": route_context_count,
        "promotingCandidateCount": promoting_count,
        "targetGroupCounts": dict(sorted(group_counts.items())),
        "targetLabelCounts": dict(sorted(label_counts.items())),
        "kindCounts": dict(sorted(kind_counts.items())),
        "sampleRows": sample_rows,
        "promotionStatus": "blocked",
    }


def script_summary(
    exe: bytes,
    sections: list[dict],
    strings: dict[int, str],
    script_va: int | None,
    slot_name: str,
    frontier_refs: dict[str, int],
) -> dict:
    if script_va is None:
        return {
            "slot": slot_name,
            "scriptVaHex": None,
            "scannedDwordCount": 0,
            "linkedCns": [],
            "fieldRecords": [],
            "fieldRecordCount": 0,
            "pointerDwordCount": 0,
            "currentFrontierDirectRefs": [],
            "currentFrontierDirectRefCount": 0,
            "selectionOpcodeCount": 0,
            "selectionOffsetHistogram": [],
            "gateSelectionRows": [],
            "gateWriterCount": 0,
            "gateReaderCount": 0,
            "baseSetterOpcodeCount": 0,
            "baseSetterOpcodeHistogram": [],
            "baseSetterSampleRows": [],
            "baseSetterRows": [],
            "baseSetterPointerLikeCount": 0,
            "baseSetterNonPointerCount": 0,
            "contextA8SetterRowCount": 0,
            "contextA8PointerLikeSetterRowCount": 0,
            "contextA8NonPointerSetterRowCount": 0,
            "contextA8BaseExpressionHistogram": [],
            "contextA8NonPointerBaseExpressionHistogram": [],
            "contextA8NonPointerOperandSourceHistogram": [],
            "lastBaseSetterRow": None,
            "lastContextA8SetterRow": None,
            "lastNonPointerBaseSetterRow": None,
            "lastNonPointerContextA8SetterRow": None,
            "encodedTargetScan": encoded_target_summary([]),
            "encodedTargetClassification": "no-encoded-route-target-scalars-in-descriptor-scripts",
            "encodedTargetRawScalarCandidateCount": 0,
            "encodedTargetRouteProofRawScalarCandidateCount": 0,
            "encodedTargetRouteContextRawScalarCandidateCount": 0,
            "encodedTargetPromotingCandidateCount": 0,
            "encodedTargetGroupCounts": {},
            "encodedTargetLabelCounts": {},
            "encodedTargetKindCounts": {},
            "interestingRows": [],
        }

    linked_cns = []
    field_records = []
    frontier_hits = []
    encoded_rows = []
    interesting_rows = []
    pointer_count = 0
    selection_offset_counts: dict[int, int] = {}
    selection_opcode_count = 0
    gate_selection_rows = []
    gate_writer_count = 0
    gate_reader_count = 0
    base_setter_counts: dict[int, int] = {}
    context_a8_base_counts: dict[str, int] = {}
    context_a8_non_pointer_base_counts: dict[str, int] = {}
    context_a8_non_pointer_operand_counts: dict[str, int] = {}
    base_setter_sample_rows = []
    base_setter_rows = []

    frontier_by_va = {value: label for label, value in frontier_refs.items()}
    encoded_targets = frontier_ref_targets(frontier_refs)
    for index in range(SCRIPT_SCAN_DWORDS):
        row_va = script_va + index * 4
        value = dword_at(exe, sections, row_va)
        if value is None:
            break

        opcode = value & 0xFF
        stream_plus_1 = (value >> 8) & 0xFF
        stream_plus_2 = (value >> 16) & 0xFF
        row: dict[str, Any] | None = None
        if opcode in SELECTION_OPCODES:
            selection_opcode_count += 1
            selection_offset_counts[stream_plus_2] = selection_offset_counts.get(stream_plus_2, 0) + 1
            is_gate_offset = stream_plus_2 in GATE_OFFSETS
            selection_row = {
                "index": index,
                "vaHex": hex32(row_va),
                "valueHex": hex32(value),
                "opcodeHex": hex8(opcode),
                "operation": selection_operation(opcode),
                "stateTable": state_table(stream_plus_1),
                "selectionBufferOffsetHex": hex8(stream_plus_2),
                "writesSelectionBuffer": opcode in {0x12, 0x13},
                "readsSelectionBuffer": opcode == 0x11,
                "isGateOffset": is_gate_offset,
            }
            if is_gate_offset:
                gate_selection_rows.append(selection_row)
                if selection_row["writesSelectionBuffer"]:
                    gate_writer_count += 1
                if selection_row["readsSelectionBuffer"]:
                    gate_reader_count += 1
        if opcode in BASE_SETTER_OPCODES:
            base_setter_counts[opcode] = base_setter_counts.get(opcode, 0) + 1
            note = GENERAL_HANDLER_NOTES.get(opcode, {})
            base_expression = note.get("baseExpression") or "unknown"
            pointer_like = is_pointer_dword(sections, value)
            writes_context_a8 = bool(note.get("writesContextA8"))
            base_setter_row = {
                "index": index,
                "vaHex": hex32(row_va),
                "valueHex": hex32(value),
                "opcodeHex": hex8(opcode),
                "streamPlus1Hex": hex8(stream_plus_1),
                "streamPlus2Hex": hex8(stream_plus_2),
                "streamPlus3Hex": hex8((value >> 24) & 0xFF),
                "handlerName": note.get("name") or "unknown",
                "writesContextA8": writes_context_a8,
                "baseExpression": base_expression,
                "contextA8OperandSource": (
                    context_a8_operand_source(opcode, stream_plus_1, stream_plus_2)
                    if writes_context_a8 else None
                ),
                "handlerEvidenceVaHex": note.get("evidenceVaHex") or "-",
                "isPointerDword": pointer_like,
                "shapeClassification": "pointer-dword-low-byte-collision" if pointer_like else "non-pointer-opcode-shaped-row",
            }
            base_setter_rows.append(base_setter_row)
            if base_setter_row["writesContextA8"]:
                context_a8_base_counts[base_expression] = context_a8_base_counts.get(base_expression, 0) + 1
                if not pointer_like:
                    context_a8_non_pointer_base_counts[base_expression] = (
                        context_a8_non_pointer_base_counts.get(base_expression, 0) + 1
                    )
                    operand_source = base_setter_row["contextA8OperandSource"] or "unknown-context-a8-source"
                    context_a8_non_pointer_operand_counts[operand_source] = (
                        context_a8_non_pointer_operand_counts.get(operand_source, 0) + 1
                    )
            if len(base_setter_sample_rows) < 12:
                base_setter_sample_rows.append(dict(base_setter_row))
        if value in strings:
            name = strings[value]
            if name not in linked_cns:
                linked_cns.append(name)
            row = {
                "index": index,
                "vaHex": hex32(row_va),
                "valueHex": hex32(value),
                "cns": name,
            }
            field_record = field_record_for_cns(exe, sections, row_va, name)
            if field_record:
                field_records.append(field_record)
                row["fieldMapRecordStart"] = True
                row["sceneIdHex"] = field_record["sceneIdHex"]
        elif va_to_offset(sections, value) is not None:
            pointer_count += 1

        if value in frontier_by_va:
            hit = {
                "index": index,
                "vaHex": hex32(row_va),
                "valueHex": hex32(value),
                "target": frontier_by_va[value],
            }
            frontier_hits.append(hit)
            if row is None:
                row = dict(hit)
            else:
                row["currentFrontierTarget"] = frontier_by_va[value]

        encoded_hits = encoded_target_rows_for_dword(row_va, value, encoded_targets, index, slot_name)
        encoded_rows.extend(encoded_hits)
        if encoded_hits and row is None:
            row = {
                "index": index,
                "vaHex": hex32(row_va),
                "valueHex": hex32(value),
                "encodedTargetHitCount": len(encoded_hits),
                "encodedTargetLabels": sorted({hit["targetLabel"] for hit in encoded_hits}),
            }
        elif encoded_hits and row is not None:
            row["encodedTargetHitCount"] = len(encoded_hits)
            row["encodedTargetLabels"] = sorted({hit["targetLabel"] for hit in encoded_hits})

        if row is not None:
            interesting_rows.append(row)

    encoded_scan = encoded_target_summary(encoded_rows)
    return {
        "slot": slot_name,
        "scriptVaHex": hex32(script_va),
        "scannedDwordCount": SCRIPT_SCAN_DWORDS,
        "linkedCns": linked_cns,
        "fieldRecords": field_records,
        "fieldRecordCount": len(field_records),
        "pointerDwordCount": pointer_count,
        "currentFrontierDirectRefs": frontier_hits,
        "currentFrontierDirectRefCount": len(frontier_hits),
        "selectionOpcodeCount": selection_opcode_count,
        "selectionOffsetHistogram": histogram_rows(selection_offset_counts),
        "gateSelectionRows": gate_selection_rows,
        "gateWriterCount": gate_writer_count,
        "gateReaderCount": gate_reader_count,
        "baseSetterOpcodeCount": sum(base_setter_counts.values()),
        "baseSetterOpcodeHistogram": histogram_rows(base_setter_counts),
        "baseSetterSampleRows": base_setter_sample_rows,
        "baseSetterRows": base_setter_rows,
        "baseSetterPointerLikeCount": sum(1 for row in base_setter_rows if row["isPointerDword"]),
        "baseSetterNonPointerCount": sum(1 for row in base_setter_rows if not row["isPointerDword"]),
        "contextA8SetterRowCount": sum(context_a8_base_counts.values()),
        "contextA8PointerLikeSetterRowCount": sum(
            1 for row in base_setter_rows if row["writesContextA8"] and row["isPointerDword"]
        ),
        "contextA8NonPointerSetterRowCount": sum(context_a8_non_pointer_base_counts.values()),
        "contextA8BaseExpressionHistogram": string_histogram_rows(context_a8_base_counts),
        "contextA8NonPointerBaseExpressionHistogram": string_histogram_rows(context_a8_non_pointer_base_counts),
        "contextA8NonPointerOperandSourceHistogram": string_histogram_rows(context_a8_non_pointer_operand_counts),
        "lastBaseSetterRow": base_setter_rows[-1] if base_setter_rows else None,
        "lastContextA8SetterRow": next(
            (row for row in reversed(base_setter_rows) if row["writesContextA8"]),
            None,
        ),
        "lastNonPointerBaseSetterRow": next(
            (row for row in reversed(base_setter_rows) if not row["isPointerDword"]),
            None,
        ),
        "lastNonPointerContextA8SetterRow": next(
            (row for row in reversed(base_setter_rows) if row["writesContextA8"] and not row["isPointerDword"]),
            None,
        ),
        "encodedTargetScan": encoded_scan,
        "encodedTargetClassification": encoded_scan["classification"],
        "encodedTargetRawScalarCandidateCount": encoded_scan["rawScalarCandidateCount"],
        "encodedTargetRouteProofRawScalarCandidateCount": encoded_scan["routeProofRawScalarCandidateCount"],
        "encodedTargetRouteContextRawScalarCandidateCount": encoded_scan["routeContextRawScalarCandidateCount"],
        "encodedTargetPromotingCandidateCount": encoded_scan["promotingCandidateCount"],
        "encodedTargetGroupCounts": encoded_scan["targetGroupCounts"],
        "encodedTargetLabelCounts": encoded_scan["targetLabelCounts"],
        "encodedTargetKindCounts": encoded_scan["kindCounts"],
        "interestingRows": interesting_rows[:48],
    }


def descriptor_rows(
    exe: bytes,
    sections: list[dict],
    strings: dict[int, str],
    frontier_refs: dict[str, int],
    descriptor_writers: dict,
) -> list[dict]:
    writer_rows = descriptor_writers.get("descriptorRows") or []
    rows = []
    for index in range(DESCRIPTOR_LIMIT):
        writer_row = next((row for row in writer_rows if row.get("index") == index), {})
        entry_va = parse_hex(writer_row.get("entryVaHex")) or DESCRIPTOR_TABLE_VA + index * 4
        descriptor = parse_hex(writer_row.get("descriptorVaHex")) or dword_at(exe, sections, entry_va)
        script0 = parse_hex(writer_row.get("script0VaHex")) or (dword_at(exe, sections, descriptor) if descriptor else None)
        script4 = parse_hex(writer_row.get("script4VaHex")) or (dword_at(exe, sections, descriptor + 4) if descriptor else None)
        script8 = parse_hex(writer_row.get("script8VaHex")) or (dword_at(exe, sections, descriptor + 8) if descriptor else None)
        scripts = [
            script_summary(exe, sections, strings, script0, "script+0", frontier_refs),
            script_summary(exe, sections, strings, script4, "script+4", frontier_refs),
            script_summary(exe, sections, strings, script8, "script+8", frontier_refs),
        ]
        rows.append({
            "index": index,
            "entryVaHex": hex32(entry_va),
            "descriptorVaHex": hex32(descriptor) if descriptor is not None else None,
            "script0VaHex": hex32(script0) if script0 is not None else None,
            "script4VaHex": hex32(script4) if script4 is not None else None,
            "script8VaHex": hex32(script8) if script8 is not None else None,
            "scripts": scripts,
            "script0LinkedCns": scripts[0]["linkedCns"],
            "script4LinkedCns": scripts[1]["linkedCns"],
            "script4FieldRecordCount": scripts[1]["fieldRecordCount"],
            "script4CurrentFrontierDirectRefCount": scripts[1]["currentFrontierDirectRefCount"],
            "script4SelectionOpcodeCount": scripts[1]["selectionOpcodeCount"],
            "script4BaseSetterOpcodeCount": scripts[1]["baseSetterOpcodeCount"],
            "script4BaseSetterPointerLikeCount": scripts[1]["baseSetterPointerLikeCount"],
            "script4BaseSetterNonPointerCount": scripts[1]["baseSetterNonPointerCount"],
            "script4ContextA8SetterRowCount": scripts[1]["contextA8SetterRowCount"],
            "script4ContextA8PointerLikeSetterRowCount": scripts[1]["contextA8PointerLikeSetterRowCount"],
            "script4ContextA8NonPointerSetterRowCount": scripts[1]["contextA8NonPointerSetterRowCount"],
            "script4ContextA8NonPointerOperandSourceHistogram": scripts[1][
                "contextA8NonPointerOperandSourceHistogram"
            ],
            "script4LastBaseSetterRow": scripts[1]["lastBaseSetterRow"],
            "script4LastContextA8SetterRow": scripts[1]["lastContextA8SetterRow"],
            "script4LastNonPointerBaseSetterRow": scripts[1]["lastNonPointerBaseSetterRow"],
            "script4LastNonPointerContextA8SetterRow": scripts[1]["lastNonPointerContextA8SetterRow"],
            "script4GateWriterCount": scripts[1]["gateWriterCount"],
            "script4GateReaderCount": scripts[1]["gateReaderCount"],
            "script4EncodedTargetRawScalarCandidateCount": scripts[1]["encodedTargetRawScalarCandidateCount"],
            "script4EncodedTargetRouteProofRawScalarCandidateCount": scripts[1][
                "encodedTargetRouteProofRawScalarCandidateCount"
            ],
            "script4EncodedTargetRouteContextRawScalarCandidateCount": scripts[1][
                "encodedTargetRouteContextRawScalarCandidateCount"
            ],
            "script4EncodedTargetPromotingCandidateCount": scripts[1]["encodedTargetPromotingCandidateCount"],
            "script4EncodedTargetClassification": scripts[1]["encodedTargetClassification"],
        })
    return rows


def frontier_refs_from_summary(current_root_frontier_paths: dict) -> dict[str, int]:
    refs = dict(DEFAULT_FRONTIER_REFS)
    root = parse_hex(current_root_frontier_paths.get("rootHex"))
    if root is not None:
        refs["currentRoot"] = root
    for leaf in current_root_frontier_paths.get("leafRows") or []:
        if leaf.get("traceContainsFrontierReader"):
            leaf_va = parse_hex(leaf.get("leafPointerHex"))
            if leaf_va is not None:
                refs["frontierLeaf"] = leaf_va
            source_refs = leaf.get("sourceRecordRefs") or []
            target_refs = leaf.get("targetRecordRefs") or []
            if source_refs:
                source_va = parse_hex(source_refs[0])
                if source_va is not None:
                    refs["sourceRecord"] = source_va
            if target_refs:
                target_va = parse_hex(target_refs[0])
                if target_va is not None:
                    refs["targetRecord"] = target_va
            break
    return refs


def build_summary(
    exe: bytes,
    descriptor_writers: dict | None = None,
    current_root_frontier_paths: dict | None = None,
) -> dict:
    sections = read_sections(exe)
    strings = find_cns_strings(exe, sections)
    descriptor_writers = (
        descriptor_writers
        if descriptor_writers is not None
        else load_json(OUT / "save_selector_opcode20_slot_descriptor_writers.json", {})
    )
    current_root_frontier_paths = (
        current_root_frontier_paths
        if current_root_frontier_paths is not None
        else load_json(OUT / "save_selector_current_root_frontier_paths.json", {})
    )
    frontier_refs = frontier_refs_from_summary(current_root_frontier_paths)
    rows = descriptor_rows(exe, sections, strings, frontier_refs, descriptor_writers)
    script4_field_count = sum(row["script4FieldRecordCount"] for row in rows)
    script4_frontier_ref_count = sum(row["script4CurrentFrontierDirectRefCount"] for row in rows)
    all_field_count = sum(script["fieldRecordCount"] for row in rows for script in row["scripts"])
    all_frontier_ref_count = sum(script["currentFrontierDirectRefCount"] for row in rows for script in row["scripts"])
    script4_encoded_scan = merge_encoded_target_scans([
        script["encodedTargetScan"]
        for row in rows
        for script in row["scripts"]
        if script["slot"] == "script+4"
    ])
    all_encoded_scan = merge_encoded_target_scans([
        script["encodedTargetScan"]
        for row in rows
        for script in row["scripts"]
    ])
    script4_selection_opcode_count = sum(row["script4SelectionOpcodeCount"] for row in rows)
    all_selection_opcode_count = sum(script["selectionOpcodeCount"] for row in rows for script in row["scripts"])
    script4_base_setter_count = sum(row["script4BaseSetterOpcodeCount"] for row in rows)
    script4_base_setter_pointer_like_count = sum(row["script4BaseSetterPointerLikeCount"] for row in rows)
    script4_base_setter_non_pointer_count = sum(row["script4BaseSetterNonPointerCount"] for row in rows)
    script4_context_a8_setter_count = sum(row["script4ContextA8SetterRowCount"] for row in rows)
    script4_context_a8_pointer_like_setter_count = sum(row["script4ContextA8PointerLikeSetterRowCount"] for row in rows)
    script4_context_a8_non_pointer_setter_count = sum(row["script4ContextA8NonPointerSetterRowCount"] for row in rows)
    script4_context_a8_base_counts: dict[str, int] = {}
    script4_context_a8_non_pointer_base_counts: dict[str, int] = {}
    script4_last_context_a8_base_counts: dict[str, int] = {}
    script4_last_non_pointer_context_a8_base_counts: dict[str, int] = {}
    script4_context_a8_non_pointer_operand_histogram = merge_string_histograms([
        script["contextA8NonPointerOperandSourceHistogram"]
        for row in rows
        for script in row["scripts"]
        if script["slot"] == "script+4"
    ])
    all_script_context_a8_non_pointer_operand_histogram = merge_string_histograms([
        script["contextA8NonPointerOperandSourceHistogram"]
        for row in rows
        for script in row["scripts"]
    ])
    for row in rows:
        script4 = next(script for script in row["scripts"] if script["slot"] == "script+4")
        for histogram_row in script4["contextA8BaseExpressionHistogram"]:
            value = histogram_row["value"]
            script4_context_a8_base_counts[value] = script4_context_a8_base_counts.get(value, 0) + histogram_row["count"]
        for histogram_row in script4["contextA8NonPointerBaseExpressionHistogram"]:
            value = histogram_row["value"]
            script4_context_a8_non_pointer_base_counts[value] = (
                script4_context_a8_non_pointer_base_counts.get(value, 0) + histogram_row["count"]
            )
        last_context = row["script4LastContextA8SetterRow"]
        if last_context:
            value = last_context["baseExpression"]
            script4_last_context_a8_base_counts[value] = script4_last_context_a8_base_counts.get(value, 0) + 1
        last_non_pointer_context = row["script4LastNonPointerContextA8SetterRow"]
        if last_non_pointer_context:
            value = last_non_pointer_context["baseExpression"]
            script4_last_non_pointer_context_a8_base_counts[value] = (
                script4_last_non_pointer_context_a8_base_counts.get(value, 0) + 1
            )
    script4_gate_writer_count = sum(row["script4GateWriterCount"] for row in rows)
    script4_gate_reader_count = sum(row["script4GateReaderCount"] for row in rows)
    all_gate_writer_count = sum(script["gateWriterCount"] for row in rows for script in row["scripts"])
    all_gate_reader_count = sum(script["gateReaderCount"] for row in rows for script in row["scripts"])
    script_slot_aggregate_rows = []
    for slot in ["script+0", "script+4", "script+8"]:
        slot_scripts = [script for row in rows for script in row["scripts"] if script["slot"] == slot]
        script_slot_aggregate_rows.append({
            "slot": slot,
            "scriptCount": len(slot_scripts),
            "fieldRecordCount": sum(script["fieldRecordCount"] for script in slot_scripts),
            "currentFrontierDirectRefCount": sum(script["currentFrontierDirectRefCount"] for script in slot_scripts),
            "encodedTargetRawScalarCandidateCount": sum(
                script["encodedTargetRawScalarCandidateCount"] for script in slot_scripts
            ),
            "encodedTargetRouteProofRawScalarCandidateCount": sum(
                script["encodedTargetRouteProofRawScalarCandidateCount"] for script in slot_scripts
            ),
            "encodedTargetPromotingCandidateCount": sum(
                script["encodedTargetPromotingCandidateCount"] for script in slot_scripts
            ),
            "selectionOpcodeCount": sum(script["selectionOpcodeCount"] for script in slot_scripts),
            "gateWriterCount": sum(script["gateWriterCount"] for script in slot_scripts),
            "gateReaderCount": sum(script["gateReaderCount"] for script in slot_scripts),
            "contextA8SetterRowCount": sum(script["contextA8SetterRowCount"] for script in slot_scripts),
            "contextA8NonPointerSetterRowCount": sum(
                script["contextA8NonPointerSetterRowCount"] for script in slot_scripts
            ),
        })
    conclusion = (
        "The descriptor table reached by opcode 0x20 mode 0 resolves to character, battle, or object scripts, "
        "not field-map transition records. In the scanned descriptor+4 scripts there are zero field-map CNS records "
        "and zero direct or encoded-scalar references to the current map1_01a frontier "
        "leaf/reader/source/target addresses. "
        "They do contain base-selector opcode-shaped rows, but most 0x28/0x40/0x44 rows are pointer dwords whose "
        "low byte merely collides with a handler opcode. The non-pointer context+0xa8 setter-shaped rows are only "
        "0x42 mode-0 context+0xf2 runtime object selection, not a fixed route/gate base or gate-specific setup. "
        "None of the descriptor+4 selection-buffer reader/writer rows targets gate offsets 0xe8 or 0xea, and the "
        "same gate-offset count is zero across descriptor+0, descriptor+4, and descriptor+8 scripts. "
        "So a specific gate base is not proven. "
        "This descriptor path therefore does not promote map1_01a -> map2_02d; runtime active order/count and "
        "strict hotspot evidence are still required."
    )
    return {
        "source": SOURCE,
        "target": TARGET,
        "descriptorTableVaHex": descriptor_writers.get("descriptorTableVaHex") or hex32(DESCRIPTOR_TABLE_VA),
        "descriptorRowCount": len(rows),
        "scriptScanDwordCount": SCRIPT_SCAN_DWORDS,
        "frontierRefs": {label: hex32(value) for label, value in frontier_refs.items()},
        "descriptorRows": rows,
        "script4FieldRecordCount": script4_field_count,
        "script4CurrentFrontierDirectRefCount": script4_frontier_ref_count,
        "script4EncodedTargetScan": script4_encoded_scan,
        "script4EncodedTargetClassification": script4_encoded_scan["classification"],
        "script4EncodedTargetRawScalarCandidateCount": script4_encoded_scan["rawScalarCandidateCount"],
        "script4EncodedTargetRouteProofRawScalarCandidateCount": script4_encoded_scan[
            "routeProofRawScalarCandidateCount"
        ],
        "script4EncodedTargetRouteContextRawScalarCandidateCount": script4_encoded_scan[
            "routeContextRawScalarCandidateCount"
        ],
        "script4EncodedTargetPromotingCandidateCount": script4_encoded_scan["promotingCandidateCount"],
        "script4EncodedTargetGroupCounts": script4_encoded_scan["targetGroupCounts"],
        "script4EncodedTargetLabelCounts": script4_encoded_scan["targetLabelCounts"],
        "script4EncodedTargetKindCounts": script4_encoded_scan["kindCounts"],
        "allScriptFieldRecordCount": all_field_count,
        "allScriptCurrentFrontierDirectRefCount": all_frontier_ref_count,
        "allScriptEncodedTargetScan": all_encoded_scan,
        "allScriptEncodedTargetClassification": all_encoded_scan["classification"],
        "allScriptEncodedTargetRawScalarCandidateCount": all_encoded_scan["rawScalarCandidateCount"],
        "allScriptEncodedTargetRouteProofRawScalarCandidateCount": all_encoded_scan[
            "routeProofRawScalarCandidateCount"
        ],
        "allScriptEncodedTargetRouteContextRawScalarCandidateCount": all_encoded_scan[
            "routeContextRawScalarCandidateCount"
        ],
        "allScriptEncodedTargetPromotingCandidateCount": all_encoded_scan["promotingCandidateCount"],
        "allScriptEncodedTargetGroupCounts": all_encoded_scan["targetGroupCounts"],
        "allScriptEncodedTargetLabelCounts": all_encoded_scan["targetLabelCounts"],
        "allScriptEncodedTargetKindCounts": all_encoded_scan["kindCounts"],
        "script4SelectionOpcodeCount": script4_selection_opcode_count,
        "allScriptSelectionOpcodeCount": all_selection_opcode_count,
        "script4BaseSetterOpcodeCount": script4_base_setter_count,
        "script4BaseSetterPointerLikeCount": script4_base_setter_pointer_like_count,
        "script4BaseSetterNonPointerCount": script4_base_setter_non_pointer_count,
        "script4ContextA8SetterRowCount": script4_context_a8_setter_count,
        "script4ContextA8PointerLikeSetterRowCount": script4_context_a8_pointer_like_setter_count,
        "script4ContextA8NonPointerSetterRowCount": script4_context_a8_non_pointer_setter_count,
        "script4ContextA8BaseExpressionHistogram": string_histogram_rows(script4_context_a8_base_counts),
        "script4ContextA8NonPointerBaseExpressionHistogram": string_histogram_rows(script4_context_a8_non_pointer_base_counts),
        "script4ContextA8NonPointerOperandSourceHistogram": script4_context_a8_non_pointer_operand_histogram,
        "script4LastContextA8BaseExpressionHistogram": string_histogram_rows(script4_last_context_a8_base_counts),
        "script4LastNonPointerContextA8BaseExpressionHistogram": string_histogram_rows(
            script4_last_non_pointer_context_a8_base_counts
        ),
        "allScriptContextA8NonPointerOperandSourceHistogram": all_script_context_a8_non_pointer_operand_histogram,
        "gateOffsetsHex": [hex8(value) for value in sorted(GATE_OFFSETS)],
        "script4GateWriterCount": script4_gate_writer_count,
        "script4GateReaderCount": script4_gate_reader_count,
        "allScriptGateWriterCount": all_gate_writer_count,
        "allScriptGateReaderCount": all_gate_reader_count,
        "scriptSlotAggregateRows": script_slot_aggregate_rows,
        "script4SpecificGateBaseProven": False,
        "allScriptsSpecificGateBaseProven": False,
        "descriptorScriptClass": "character/battle/object-resource",
        "runtimeActiveOrderRequired": True,
        "controlPathProofStatus": "blocked",
        "proofFound": False,
        "opcode20DescriptorScriptProofFound": False,
        "failedOpcode20DescriptorScriptGateIds": FAILED_OPCODE20_DESCRIPTOR_SCRIPT_GATE_IDS,
        "missingEvidence": OPCODE20_DESCRIPTOR_SCRIPT_MISSING_EVIDENCE,
        "evidenceRefs": OPCODE20_DESCRIPTOR_SCRIPT_EVIDENCE_REFS,
        "evidenceRefCount": len(OPCODE20_DESCRIPTOR_SCRIPT_EVIDENCE_REFS),
        "promotionStatus": "blocked",
        "conclusion": conclusion,
    }


def markdown(summary: dict) -> str:
    non_pointer_operand_sources = ", ".join(
        f"{row['value']}:{row['count']}"
        for row in summary["script4ContextA8NonPointerOperandSourceHistogram"]
    ) or "-"
    lines = [
        "# Save Selector Opcode 0x20 Descriptor Scripts",
        "",
        f"- route: {summary['source']} -> {summary['target']}",
        f"- descriptor table: `{summary['descriptorTableVaHex']}`",
        f"- descriptor rows: {summary['descriptorRowCount']}",
        f"- scan window per script: {summary['scriptScanDwordCount']} dwords",
        f"- descriptor+4 field-map CNS records: {summary['script4FieldRecordCount']}",
        f"- descriptor+4 current frontier direct refs: {summary['script4CurrentFrontierDirectRefCount']}",
        f"- descriptor+4 encoded route-target scalars: "
        f"{summary['script4EncodedTargetRawScalarCandidateCount']} / "
        f"{summary['script4EncodedTargetRouteProofRawScalarCandidateCount']} / "
        f"{summary['script4EncodedTargetPromotingCandidateCount']} "
        f"({summary['script4EncodedTargetClassification']})",
        f"- descriptor+4 selection-buffer opcode-shaped rows: {summary['script4SelectionOpcodeCount']}",
        f"- all descriptor-script selection-buffer opcode-shaped rows: {summary['allScriptSelectionOpcodeCount']}",
        f"- descriptor+4 base-setter opcode-shaped rows: {summary['script4BaseSetterOpcodeCount']}",
        f"- descriptor+4 base-setter pointer-like rows: {summary['script4BaseSetterPointerLikeCount']}",
        f"- descriptor+4 base-setter non-pointer rows: {summary['script4BaseSetterNonPointerCount']}",
        f"- descriptor+4 context+0xa8 setter-shaped rows: {summary['script4ContextA8SetterRowCount']}",
        f"- descriptor+4 context+0xa8 non-pointer setter-shaped rows: {summary['script4ContextA8NonPointerSetterRowCount']}",
        f"- descriptor+4 non-pointer context+0xa8 operand sources: {non_pointer_operand_sources}",
        f"- descriptor+4 specific gate base proven: {summary['script4SpecificGateBaseProven']}",
        f"- descriptor+4 gate `{', '.join(summary['gateOffsetsHex'])}` writers: {summary['script4GateWriterCount']}",
        f"- descriptor+4 gate `{', '.join(summary['gateOffsetsHex'])}` readers: {summary['script4GateReaderCount']}",
        f"- all descriptor-script gate `{', '.join(summary['gateOffsetsHex'])}` writers: {summary['allScriptGateWriterCount']}",
        f"- all descriptor-script gate `{', '.join(summary['gateOffsetsHex'])}` readers: {summary['allScriptGateReaderCount']}",
        f"- all descriptor-script specific gate base proven: {summary['allScriptsSpecificGateBaseProven']}",
        f"- all descriptor-script field-map CNS records: {summary['allScriptFieldRecordCount']}",
        f"- all descriptor-script current frontier direct refs: {summary['allScriptCurrentFrontierDirectRefCount']}",
        f"- all descriptor-script encoded route-target scalars: "
        f"{summary['allScriptEncodedTargetRawScalarCandidateCount']} / "
        f"{summary['allScriptEncodedTargetRouteProofRawScalarCandidateCount']} / "
        f"{summary['allScriptEncodedTargetPromotingCandidateCount']} "
        f"({summary['allScriptEncodedTargetClassification']})",
        f"- script class: {summary['descriptorScriptClass']}",
        f"- runtime active order required: {summary['runtimeActiveOrderRequired']}",
        f"- proofFound: `{summary['proofFound']}`",
        f"- opcode20DescriptorScriptProofFound: `{summary['opcode20DescriptorScriptProofFound']}`",
        f"- promotion status: `{summary['promotionStatus']}`",
        "",
        summary["conclusion"],
        "",
        "## Failed Gates",
        "",
    ]
    lines.extend(f"- `{item}`" for item in summary["failedOpcode20DescriptorScriptGateIds"])
    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([
        "",
        "## Current Frontier Refs",
        "",
        "| label | VA |",
        "| --- | --- |",
    ])
    for label, value in summary["frontierRefs"].items():
        lines.append(f"| {label} | `{value}` |")
    lines.extend([
        "",
        "## Script Slot Aggregate",
        "",
        "| slot | scripts | field maps | frontier refs | encoded raw/proof/promoting | selection opcodes | gate writers | gate readers | context+0xa8 setters | non-pointer context+0xa8 setters |",
        "| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |",
    ])
    for row in summary["scriptSlotAggregateRows"]:
        lines.append(
            f"| {row['slot']} | {row['scriptCount']} | {row['fieldRecordCount']} | "
            f"{row['currentFrontierDirectRefCount']} | "
            f"{row['encodedTargetRawScalarCandidateCount']}/"
            f"{row['encodedTargetRouteProofRawScalarCandidateCount']}/"
            f"{row['encodedTargetPromotingCandidateCount']} | "
            f"{row['selectionOpcodeCount']} | "
            f"{row['gateWriterCount']} | {row['gateReaderCount']} | "
            f"{row['contextA8SetterRowCount']} | {row['contextA8NonPointerSetterRowCount']} |"
        )
    lines.extend([
        "",
        "## Descriptor+4 Context Base Histogram",
        "",
        "| base expression | shaped row count | non-pointer row count | last-shaped descriptor count | last-non-pointer descriptor count |",
        "| --- | ---: | ---: | ---: | ---: |",
    ])
    last_counts = {
        row["value"]: row["count"]
        for row in summary["script4LastContextA8BaseExpressionHistogram"]
    }
    non_pointer_counts = {
        row["value"]: row["count"]
        for row in summary["script4ContextA8NonPointerBaseExpressionHistogram"]
    }
    last_non_pointer_counts = {
        row["value"]: row["count"]
        for row in summary["script4LastNonPointerContextA8BaseExpressionHistogram"]
    }
    for row in summary["script4ContextA8BaseExpressionHistogram"]:
        lines.append(
            f"| `{row['value']}` | {row['count']} | {non_pointer_counts.get(row['value'], 0)} | "
            f"{last_counts.get(row['value'], 0)} | {last_non_pointer_counts.get(row['value'], 0)} |"
        )
    lines.extend([
        "",
        "## Descriptor+4 Non-Pointer Context+0xa8 Operand Sources",
        "",
        "| operand source | row count |",
        "| --- | ---: |",
    ])
    for row in summary["script4ContextA8NonPointerOperandSourceHistogram"]:
        lines.append(f"| `{row['value']}` | {row['count']} |")
    lines.extend([
        "",
        "## Descriptor Rows",
        "",
        "| index | descriptor | script+0 CNS | script+4 CNS | script+4 field maps | script+4 frontier refs | script+4 encoded raw/proof/promoting | script+4 context+0xa8 setters | script+4 non-pointer context+0xa8 setters | script+4 last non-pointer context+0xa8 base | script+4 gate writers | script+4 gate readers |",
        "| ---: | --- | --- | --- | ---: | ---: | ---: | ---: | ---: | --- | ---: | ---: |",
    ])
    for row in summary["descriptorRows"]:
        last_context = row["script4LastNonPointerContextA8SetterRow"]
        last_context_text = last_context["baseExpression"] if last_context else "-"
        lines.append(
            f"| {row['index']} | `{row['descriptorVaHex']}` | "
            f"{', '.join(row['script0LinkedCns']) or '-'} | "
            f"{', '.join(row['script4LinkedCns']) or '-'} | "
            f"{row['script4FieldRecordCount']} | {row['script4CurrentFrontierDirectRefCount']} | "
            f"{row['script4EncodedTargetRawScalarCandidateCount']}/"
            f"{row['script4EncodedTargetRouteProofRawScalarCandidateCount']}/"
            f"{row['script4EncodedTargetPromotingCandidateCount']} | "
            f"{row['script4ContextA8SetterRowCount']} | {row['script4ContextA8NonPointerSetterRowCount']} | "
            f"`{last_context_text}` | "
            f"{row['script4GateWriterCount']} | {row['script4GateReaderCount']} |"
        )
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    non_pointer_operand_sources = ", ".join(
        f"{row['value']}:{row['count']}"
        for row in summary["script4ContextA8NonPointerOperandSourceHistogram"]
    ) or "-"
    frontier_rows = [
        f"<tr><td>{html.escape(label)}</td><td><code>{html.escape(value)}</code></td></tr>"
        for label, value in summary["frontierRefs"].items()
    ]
    descriptor_rows_html = []
    for row in summary["descriptorRows"]:
        last_context = row["script4LastNonPointerContextA8SetterRow"]
        last_context_text = last_context["baseExpression"] if last_context else "-"
        descriptor_rows_html.append(
            "<tr>"
            f"<td>{row['index']}</td>"
            f"<td><code>{html.escape(row['descriptorVaHex'] or '-')}</code></td>"
            f"<td><code>{html.escape(row['script0VaHex'] or '-')}</code></td>"
            f"<td>{html.escape(', '.join(row['script0LinkedCns']) or '-')}</td>"
            f"<td><code>{html.escape(row['script4VaHex'] or '-')}</code></td>"
            f"<td>{html.escape(', '.join(row['script4LinkedCns']) or '-')}</td>"
            f"<td>{row['script4FieldRecordCount']}</td>"
            f"<td>{row['script4CurrentFrontierDirectRefCount']}</td>"
            f"<td>{row['script4EncodedTargetRawScalarCandidateCount']}/"
            f"{row['script4EncodedTargetRouteProofRawScalarCandidateCount']}/"
            f"{row['script4EncodedTargetPromotingCandidateCount']}</td>"
            f"<td>{row['script4ContextA8SetterRowCount']}</td>"
            f"<td>{row['script4ContextA8NonPointerSetterRowCount']}</td>"
            f"<td><code>{html.escape(last_context_text)}</code></td>"
            f"<td>{row['script4GateWriterCount']}</td>"
            f"<td>{row['script4GateReaderCount']}</td>"
            "</tr>"
        )
    base_histogram_rows = [
        (
            f"<tr><td><code>{html.escape(row['value'])}</code></td>"
            f"<td>{row['count']}</td>"
            f"<td>{next((np['count'] for np in summary['script4ContextA8NonPointerBaseExpressionHistogram'] if np['value'] == row['value']), 0)}</td>"
            f"<td>{next((last['count'] for last in summary['script4LastContextA8BaseExpressionHistogram'] if last['value'] == row['value']), 0)}</td>"
            f"<td>{next((last['count'] for last in summary['script4LastNonPointerContextA8BaseExpressionHistogram'] if last['value'] == row['value']), 0)}</td></tr>"
        )
        for row in summary["script4ContextA8BaseExpressionHistogram"]
    ]
    non_pointer_operand_source_rows = [
        f"<tr><td><code>{html.escape(row['value'])}</code></td><td>{row['count']}</td></tr>"
        for row in summary["script4ContextA8NonPointerOperandSourceHistogram"]
    ]
    slot_aggregate_rows = [
        "<tr>"
        f"<td>{html.escape(row['slot'])}</td>"
        f"<td>{row['scriptCount']}</td>"
        f"<td>{row['fieldRecordCount']}</td>"
        f"<td>{row['currentFrontierDirectRefCount']}</td>"
        f"<td>{row['encodedTargetRawScalarCandidateCount']}/"
        f"{row['encodedTargetRouteProofRawScalarCandidateCount']}/"
        f"{row['encodedTargetPromotingCandidateCount']}</td>"
        f"<td>{row['selectionOpcodeCount']}</td>"
        f"<td>{row['gateWriterCount']}</td>"
        f"<td>{row['gateReaderCount']}</td>"
        f"<td>{row['contextA8SetterRowCount']}</td>"
        f"<td>{row['contextA8NonPointerSetterRowCount']}</td>"
        "</tr>"
        for row in summary["scriptSlotAggregateRows"]
    ]
    failed_gates = "".join(
        f"<li><code>{html.escape(item)}</code></li>"
        for item in summary["failedOpcode20DescriptorScriptGateIds"]
    )
    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 Descriptor Scripts</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 Descriptor Scripts</h1>",
        f"  <p>route {html.escape(summary['source'])} -&gt; {html.escape(summary['target'])}; descriptor table <code>{html.escape(summary['descriptorTableVaHex'])}</code>; descriptor rows {summary['descriptorRowCount']}; descriptor+4 field-map CNS records {summary['script4FieldRecordCount']}; descriptor+4 current frontier direct refs {summary['script4CurrentFrontierDirectRefCount']}; descriptor+4 selection-buffer opcode-shaped rows {summary['script4SelectionOpcodeCount']}; all descriptor-script selection-buffer opcode-shaped rows {summary['allScriptSelectionOpcodeCount']}; descriptor+4 context+0xa8 setter-shaped rows {summary['script4ContextA8SetterRowCount']}; descriptor+4 context+0xa8 non-pointer setter-shaped rows {summary['script4ContextA8NonPointerSetterRowCount']}; descriptor+4 non-pointer context+0xa8 operand sources {html.escape(non_pointer_operand_sources)}; descriptor+4 specific gate base proven {summary['script4SpecificGateBaseProven']}; descriptor+4 gate writers {summary['script4GateWriterCount']}; descriptor+4 gate readers {summary['script4GateReaderCount']}; all descriptor-script gate writers {summary['allScriptGateWriterCount']}; all descriptor-script gate readers {summary['allScriptGateReaderCount']}; all descriptor-script specific gate base proven {summary['allScriptsSpecificGateBaseProven']}; proofFound <code>{html.escape(str(summary['proofFound']))}</code>; promotion status <code>{html.escape(summary['promotionStatus'])}</code>.</p>",
        f"  <p>descriptor+4 encoded route-target scalars {summary['script4EncodedTargetRawScalarCandidateCount']}/"
        f"{summary['script4EncodedTargetRouteProofRawScalarCandidateCount']}/"
        f"{summary['script4EncodedTargetPromotingCandidateCount']} "
        f"(<code>{html.escape(summary['script4EncodedTargetClassification'])}</code>); "
        f"all descriptor-script encoded route-target scalars "
        f"{summary['allScriptEncodedTargetRawScalarCandidateCount']}/"
        f"{summary['allScriptEncodedTargetRouteProofRawScalarCandidateCount']}/"
        f"{summary['allScriptEncodedTargetPromotingCandidateCount']} "
        f"(<code>{html.escape(summary['allScriptEncodedTargetClassification'])}</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>Current Frontier Refs</h2>",
        "  <table><thead><tr><th>label</th><th>VA</th></tr></thead><tbody>",
        *frontier_rows,
        "  </tbody></table>",
        "  <h2>Script Slot Aggregate</h2>",
        "  <table><thead><tr><th>slot</th><th>scripts</th><th>field maps</th><th>frontier refs</th><th>encoded raw/proof/promoting</th><th>selection opcodes</th><th>gate writers</th><th>gate readers</th><th>context+0xa8 setters</th><th>non-pointer context+0xa8 setters</th></tr></thead><tbody>",
        *slot_aggregate_rows,
        "  </tbody></table>",
        "  <h2>Descriptor+4 Context Base Histogram</h2>",
        "  <table><thead><tr><th>base expression</th><th>shaped row count</th><th>non-pointer row count</th><th>last-shaped descriptor count</th><th>last-non-pointer descriptor count</th></tr></thead><tbody>",
        *base_histogram_rows,
        "  </tbody></table>",
        "  <h2>Descriptor+4 Non-Pointer Context+0xa8 Operand Sources</h2>",
        "  <table><thead><tr><th>operand source</th><th>row count</th></tr></thead><tbody>",
        *non_pointer_operand_source_rows,
        "  </tbody></table>",
        "  <h2>Descriptor Rows</h2>",
        "  <table><thead><tr><th>index</th><th>descriptor</th><th>script+0</th><th>script+0 CNS</th><th>script+4</th><th>script+4 CNS</th><th>script+4 field maps</th><th>script+4 frontier refs</th><th>script+4 encoded raw/proof/promoting</th><th>script+4 context+0xa8 setters</th><th>script+4 non-pointer context+0xa8 setters</th><th>script+4 last non-pointer context+0xa8 base</th><th>script+4 gate writers</th><th>script+4 gate readers</th></tr></thead><tbody>",
        *descriptor_rows_html,
        "  </tbody></table>",
        "</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_descriptor_scripts.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    (out_dir / "save_selector_opcode20_descriptor_scripts.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("--descriptor-writers", type=Path, default=OUT / "save_selector_opcode20_slot_descriptor_writers.json")
    parser.add_argument("--current-root-frontier", type=Path, default=OUT / "save_selector_current_root_frontier_paths.json")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    args = parser.parse_args()
    summary = build_summary(
        args.exe.read_bytes(),
        load_json(args.descriptor_writers, {}),
        load_json(args.current_root_frontier, {}),
    )
    write_outputs(summary, args.out_dir)
    print(f"wrote save selector opcode 0x20 descriptor scripts -> {args.out_dir / 'save_selector_opcode20_descriptor_scripts.html'}")


if __name__ == "__main__":
    main()
