#!/usr/bin/env python3
"""Summarize storage/producer context for opcode 0x24 runtime enabled flag."""
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


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

ROUTE_SOURCE = "map1_01a"
ROUTE_TARGET = "map2_02d"
RUNTIME_ENABLED_FLAG = 0x0059E34D
OPCODE24_HANDLER = 0x0040C513
OPCODE24_FLAG_READ = 0x0040C51E
OPCODE24_FLAG_PASS_TARGET = 0x0040C538
OPCODE24_FLAG_FAIL_ADVANCE = 0x0040C52C
DIAGNOSTIC_WATCH_POLL = "runtime_selected_pointer_patched_public_selector_2_0_active_order_poll.json"
DIAGNOSTIC_WATCH_NAMES = [
    "opcode24Mode1Source",
    "opcode24RuntimeFlag",
    "opcode24CurrentObjectIndex",
]
OPCODE24_RUNTIME_ENABLED_MISSING_EVIDENCE_BY_GATE = {
    "runtime-enabled-flag-producer": (
        "runtime producer for 0x0059e34d if opcode 0x24 mode dispatch is needed"
    ),
    "mode1-source-runtime-producer": "runtime producer for mode1 source 0x0059e348",
    "selected-root-or-strict-hotspot-proof": (
        "selector 2:0 execution order or strict map1_01a source hotspot"
    ),
}
OPCODE24_RUNTIME_ENABLED_EVIDENCE_REFS = [
    {
        "path": "Hwanse2.exe",
        "fields": [".text", ".data", "0x0059e34d"],
    },
    {
        "path": "out/save_loader_trace.json",
        "fields": ["saveReadBlocks", "targetVaHex", "sizeHex"],
    },
    {
        "path": "out/save_selector_opcode24_globals.json",
        "fields": ["rows", "globalVaHex", "directTextRefCount", "directWriteCount"],
    },
    {
        "path": f"out/{DIAGNOSTIC_WATCH_POLL}",
        "fields": ["rows", "uniqueWatchValues", "reachedRouteSelectorContext"],
    },
]


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


def parse_hex(value: str | None) -> int:
    return int(value or "0", 16)


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


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


def section_summary(exe: bytes, sections: list[dict]) -> dict:
    section = section_for_va(sections, RUNTIME_ENABLED_FLAG)
    if not section:
        return {
            "sectionName": None,
            "runtimeEnabledFlagInKnownSection": False,
            "runtimeEnabledFlagHasRawByte": False,
            "staticInitialValue": None,
            "staticInitialValueHex": None,
            "staticInitialValueKind": "unknown-section",
        }
    raw_end_va = section["va"] + section["raw_size"]
    virtual_end_va = section["va"] + section["size"]
    raw_offset = va_to_offset(sections, RUNTIME_ENABLED_FLAG)
    has_raw_byte = raw_offset is not None and raw_offset < len(exe)
    static_value = exe[raw_offset] if has_raw_byte else 0
    return {
        "sectionName": section["name"],
        "sectionVaHex": hex32(section["va"]),
        "sectionRawSizeHex": hex32(section["raw_size"]),
        "sectionVirtualSizeHex": hex32(section["size"]),
        "sectionRawEndVaHex": hex32(raw_end_va),
        "sectionVirtualEndVaHex": hex32(virtual_end_va),
        "runtimeEnabledFlagInKnownSection": True,
        "runtimeEnabledFlagRawOffsetHex": hex32(raw_offset) if raw_offset is not None else None,
        "runtimeEnabledFlagHasRawByte": has_raw_byte,
        "runtimeEnabledFlagAfterRawEndBytes": max(0, RUNTIME_ENABLED_FLAG - raw_end_va),
        "runtimeEnabledFlagAfterRawEndHex": hex32(max(0, RUNTIME_ENABLED_FLAG - raw_end_va)),
        "staticInitialValue": static_value,
        "staticInitialValueHex": f"0x{static_value:02x}",
        "staticInitialValueKind": "raw-file-byte" if has_raw_byte else "pe-zero-filled-section-tail",
        "zeroInitializedByPeLoader": not has_raw_byte,
    }


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 classify_direct_ref(data: bytes, pos: int) -> dict:
    candidates = []
    if pos >= 1 and data[pos - 1] in {0xA0, 0xA1, 0xA2, 0xA3}:
        opcode = data[pos - 1]
        if opcode == 0xA0:
            candidates.append(("read", 1, "mov al, ds:[addr]", 1))
        elif opcode == 0xA1:
            candidates.append(("read", 4, "mov eax, ds:[addr]", 1))
        elif opcode == 0xA2:
            candidates.append(("write", 1, "mov ds:[addr], al", 1))
        elif opcode == 0xA3:
            candidates.append(("write", 4, "mov ds:[addr], eax", 1))
    for prefix, access, width, instruction in [
        (b"\x8a\x0d", "read", 1, "mov cl, byte ptr ds:[addr]"),
        (b"\x8b\x0d", "read", 4, "mov ecx, dword ptr ds:[addr]"),
        (b"\x88\x0d", "write", 1, "mov byte ptr ds:[addr], cl"),
        (b"\x89\x0d", "write", 4, "mov dword ptr ds:[addr], ecx"),
        (b"\x8a\x15", "read", 1, "mov dl, byte ptr ds:[addr]"),
        (b"\x8b\x15", "read", 4, "mov edx, dword ptr ds:[addr]"),
        (b"\x88\x15", "write", 1, "mov byte ptr ds:[addr], dl"),
        (b"\x89\x15", "write", 4, "mov dword ptr ds:[addr], edx"),
        (b"\xc6\x05", "write", 1, "mov byte ptr ds:[addr], imm8"),
        (b"\xc7\x05", "write", 4, "mov dword ptr ds:[addr], imm32"),
    ]:
        if pos >= len(prefix) and data[pos - len(prefix):pos] == prefix:
            candidates.append((access, width, instruction, len(prefix)))
    if not candidates:
        return {
            "accessKind": "unknown",
            "width": None,
            "instruction": "unclassified direct address reference",
            "prefixLength": 0,
        }
    access, width, instruction, prefix_length = candidates[0]
    return {
        "accessKind": access,
        "width": width,
        "instruction": instruction,
        "prefixLength": prefix_length,
    }


def classify_context(instruction_va: int) -> str:
    if 0x00406A54 <= instruction_va <= 0x00406D8C:
        return "early object/event handler gate"
    if 0x0040C513 <= instruction_va <= 0x0040C6CB:
        return "opcode 0x24 handler runtime gate"
    if 0x0040FA50 <= instruction_va <= 0x0040FB80:
        return "adjacent object-list/object-copy handler"
    if 0x0041DCCC <= instruction_va <= 0x0041DF2D:
        return "object/stat comparison branch-state handler"
    if 0x004204B6 <= instruction_va <= 0x00420539:
        return "runtime object count/availability helper"
    if 0x004348D0 <= instruction_va <= 0x004353B8:
        return "object stat/attribute helper"
    return "other text routine"


def scan_exact_refs(exe: bytes, sections: list[dict], address: int) -> list[dict]:
    rows = []
    needle = struct.pack("<I", address)
    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"] != ".text":
            continue
        ref_va = offset_to_va(sections, hit)
        if ref_va is None:
            continue
        ref = classify_direct_ref(exe, hit)
        instruction_va = ref_va - (ref.get("prefixLength") or 0)
        rows.append({
            "addressHex": hex32(address),
            "refVaHex": hex32(ref_va),
            "instructionVaHex": hex32(instruction_va),
            "accessKind": ref["accessKind"],
            "width": ref["width"],
            "instruction": ref["instruction"],
            "context": classify_context(instruction_va),
        })
    rows.sort(key=lambda row: row["instructionVaHex"])
    return rows


def save_block_rows(save_loader_trace: dict) -> list[dict]:
    rows = []
    for block in save_loader_trace.get("saveReadBlocks") or []:
        target = parse_hex(block.get("targetVaHex"))
        size = parse_hex(block.get("sizeHex"))
        end = target + size
        contains = target <= RUNTIME_ENABLED_FLAG < end
        rows.append({
            "callVaHex": block.get("callVaHex"),
            "targetVaHex": block.get("targetVaHex"),
            "endVaHex": hex32(end),
            "saveOffsetHex": block.get("saveOffsetHex"),
            "sizeHex": block.get("sizeHex"),
            "description": block.get("description"),
            "containsRuntimeEnabledFlag": contains,
            "distanceAfterEnd": max(0, RUNTIME_ENABLED_FLAG - end),
            "distanceAfterEndHex": hex32(max(0, RUNTIME_ENABLED_FLAG - end)),
        })
    return rows


def aggregate_watch_values(rows: list[dict]) -> dict[str, list[dict[str, Any]]]:
    aggregate: dict[str, dict[str, int]] = {}
    for row in rows:
        for name, values in (row.get("uniqueWatchValues") or {}).items():
            if name not in DIAGNOSTIC_WATCH_NAMES:
                continue
            target = aggregate.setdefault(name, {})
            for value_row in values or []:
                value_hex = value_row.get("valueHex")
                if value_hex is None:
                    continue
                target[value_hex] = target.get(value_hex, 0) + int(value_row.get("count") or 0)
    return {
        name: [
            {"valueHex": value_hex, "count": count}
            for value_hex, count in sorted(value_rows.items(), key=lambda item: (-item[1], item[0]))
        ]
        for name, value_rows in sorted(aggregate.items())
    }


def stable_watch_hex(watch_values: dict[str, list[dict[str, Any]]], sample_count: int, name: str) -> str | None:
    rows = watch_values.get(name) or []
    if len(rows) != 1:
        return None
    if rows[0].get("count") != sample_count:
        return None
    return rows[0].get("valueHex")


def build_diagnostic_runtime_watch(active_order_poll: dict | None) -> dict:
    active_order_poll = active_order_poll or {}
    if not active_order_poll:
        return {
            "available": False,
            "sourcePoll": DIAGNOSTIC_WATCH_POLL,
            "diagnosticOnly": True,
        }
    all_rows = active_order_poll.get("rows") or []
    route_rows = [row for row in all_rows if row.get("reachedRouteSelectorContext")]
    if not route_rows:
        return {
            "available": False,
            "sourcePoll": DIAGNOSTIC_WATCH_POLL,
            "diagnosticOnly": True,
            "totalSampleCount": active_order_poll.get("sampleCount"),
            "totalSequenceCount": active_order_poll.get("sequenceCount"),
            "routeSampleCount": 0,
            "routeSequenceCount": 0,
            "promotionStatus": "missing-route-row",
        }
    route_sample_count = sum(int(row.get("sampleCount") or 0) for row in route_rows)
    watch_values = aggregate_watch_values(route_rows)
    mode1_source = stable_watch_hex(watch_values, route_sample_count, "opcode24Mode1Source")
    runtime_flag = stable_watch_hex(watch_values, route_sample_count, "opcode24RuntimeFlag")
    current_object_index = stable_watch_hex(watch_values, route_sample_count, "opcode24CurrentObjectIndex")
    return {
        "available": True,
        "sourcePoll": DIAGNOSTIC_WATCH_POLL,
        "diagnosticOnly": True,
        "totalSampleCount": active_order_poll.get("sampleCount"),
        "totalSequenceCount": active_order_poll.get("sequenceCount"),
        "routeSampleCount": route_sample_count,
        "routeSequenceCount": len(route_rows),
        "routeSequenceNames": [row.get("name") for row in route_rows if row.get("name")],
        "observedSelectors": active_order_poll.get("observedSelectors") or [],
        "reachedCurrentRoot": active_order_poll.get("anyReachedCurrentRoot"),
        "reachedRouteSelector": active_order_poll.get("anyReachedRouteSelectorContext"),
        "mode1SourceValueHex": mode1_source,
        "runtimeFlagValueHex": runtime_flag,
        "currentObjectIndexValueHex": current_object_index,
        "allWatchedValuesStable": all(
            stable_watch_hex(watch_values, route_sample_count, name) is not None
            for name in DIAGNOSTIC_WATCH_NAMES
        ),
        "runtimeFlagStaysDisabledInDiagnostic": runtime_flag == "0x00",
        "mode1SourceStaysZeroInDiagnostic": mode1_source == "0x00",
        "currentObjectIndexStaysZeroInDiagnostic": current_object_index == "0x00",
        "normalRouteProof": False,
        "notRoutePromotionProof": True,
        "promotionStatus": "diagnostic-only",
    }


def build_summary(
    exe: bytes,
    save_loader_trace: dict,
    opcode24_globals: dict,
    active_order_poll: dict | None = None,
) -> dict:
    sections = read_sections(exe)
    storage = section_summary(exe, sections)
    refs = scan_exact_refs(exe, sections, RUNTIME_ENABLED_FLAG)
    blocks = save_block_rows(save_loader_trace)
    global_row = next(
        (row for row in opcode24_globals.get("rows") or [] if row.get("globalVaHex") == hex32(RUNTIME_ENABLED_FLAG)),
        {},
    )
    direct_read_count = sum(1 for row in refs if row.get("accessKind") == "read")
    direct_write_count = sum(1 for row in refs if row.get("accessKind") == "write")
    containing_blocks = [row for row in blocks if row["containsRuntimeEnabledFlag"]]
    opcode24_gate_ref = next(
        (row for row in refs if row.get("instructionVaHex") == hex32(OPCODE24_FLAG_READ)),
        None,
    )
    diagnostic_watch = build_diagnostic_runtime_watch(active_order_poll)
    conclusion = (
        "Opcode 0x24 first checks runtime flag 0x0059e34d. If it is not 1, the handler advances the stream by "
        "+4 at 0x0040c52c and skips the mode-specific object-state update; if it is 1, it enters the mode dispatch "
        "at 0x0040c538. Static storage evidence puts this flag in the PE zero-filled .data tail with process-start "
        "value 0, outside the known save-loader blocks, and with no direct .text writer. The patched selector 2:0 "
        "diagnostic route row also keeps 0x0059e34d at 0x00 while reaching the constructed selector, so the "
        "diagnostic capture does not prove the mode dispatch runs. Therefore the current opcode 0x24 mode1 path is "
        "runtime-flag dependent: static and diagnostic evidence alone cannot prove that the 0x0059e348 object+0x61 "
        "write runs on the normal route path."
    )
    failed_gate_ids = list(OPCODE24_RUNTIME_ENABLED_MISSING_EVIDENCE_BY_GATE)
    missing_evidence = [
        OPCODE24_RUNTIME_ENABLED_MISSING_EVIDENCE_BY_GATE[gate_id]
        for gate_id in failed_gate_ids
    ]
    return {
        "source": ROUTE_SOURCE,
        "target": ROUTE_TARGET,
        "runtimeEnabledFlagHex": hex32(RUNTIME_ENABLED_FLAG),
        "handlerVaHex": hex32(OPCODE24_HANDLER),
        "flagReadInstructionVaHex": hex32(OPCODE24_FLAG_READ),
        "flagPassTargetVaHex": hex32(OPCODE24_FLAG_PASS_TARGET),
        "flagFailAdvanceVaHex": hex32(OPCODE24_FLAG_FAIL_ADVANCE),
        "storage": storage,
        "saveReadBlocks": blocks,
        "saveReadBlockContainsRuntimeEnabledFlag": bool(containing_blocks),
        "directTextRefCount": len(refs),
        "directReadCount": direct_read_count,
        "directWriteCount": direct_write_count,
        "opcode24GlobalsDirectTextRefCount": global_row.get("directTextRefCount"),
        "opcode24GlobalsDirectReadCount": global_row.get("directReadCount"),
        "opcode24GlobalsDirectWriteCount": global_row.get("directWriteCount"),
        "refs": refs,
        "opcode24GateRef": opcode24_gate_ref,
        "runtimeFlagUnwrittenStaticSource": direct_write_count == 0,
        "runtimeFlagNotSavedataBacked": not containing_blocks,
        "runtimeFlagStaticInitialZero": storage.get("staticInitialValue") == 0,
        "diagnosticRuntimeWatch": diagnostic_watch,
        "modeDispatchRequiresRuntimeFlagOne": True,
        "staticEvidenceProvesModeDispatch": False,
        "proofFound": False,
        "opcode24RuntimeEnabledProofFound": False,
        "opcode24ModeDispatchProofFound": False,
        "failedOpcode24RuntimeEnabledGateIds": failed_gate_ids,
        "missingEvidence": missing_evidence,
        "evidenceRefs": OPCODE24_RUNTIME_ENABLED_EVIDENCE_REFS,
        "evidenceRefCount": len(OPCODE24_RUNTIME_ENABLED_EVIDENCE_REFS),
        "promotionStatus": "blocked",
        "remainingProofs": [
            "capture or infer a runtime producer for 0x0059e34d if opcode 0x24 mode dispatch is needed",
            "capture or infer the runtime producer for mode1 source 0x0059e348",
            "prove selector 2:0 execution order or find a strict map1_01a source hotspot",
        ],
        "conclusion": conclusion,
    }


def markdown(summary: dict) -> str:
    storage = summary["storage"]
    diagnostic = summary.get("diagnosticRuntimeWatch") or {}
    observed_selectors = ", ".join(diagnostic.get("observedSelectors") or []) or "-"
    route_sequences = ", ".join(diagnostic.get("routeSequenceNames") or []) or "-"
    lines = [
        "# Save Selector Opcode 0x24 Runtime Enabled Context",
        "",
        f"- route: {summary['source']} -> {summary['target']}",
        f"- runtime enabled flag: `{summary['runtimeEnabledFlagHex']}`",
        f"- handler/read/pass/fail: `{summary['handlerVaHex']}` / `{summary['flagReadInstructionVaHex']}` / `{summary['flagPassTargetVaHex']}` / `{summary['flagFailAdvanceVaHex']}`",
        f"- section: `{storage.get('sectionName')}` raw end `{storage.get('sectionRawEndVaHex')}` virtual end `{storage.get('sectionVirtualEndVaHex')}`",
        f"- has raw file byte: {storage.get('runtimeEnabledFlagHasRawByte')}",
        f"- static initial value: `{storage.get('staticInitialValueHex')}` ({storage.get('staticInitialValueKind')})",
        f"- save loader block contains runtime flag: {summary['saveReadBlockContainsRuntimeEnabledFlag']}",
        f"- direct text refs/read/write: {summary['directTextRefCount']} / {summary['directReadCount']} / {summary['directWriteCount']}",
        f"- mode dispatch requires runtime flag == 1: {summary['modeDispatchRequiresRuntimeFlagOne']}",
        f"- static evidence proves mode dispatch: {summary['staticEvidenceProvesModeDispatch']}",
        f"- proofFound: {summary.get('proofFound')}",
        f"- opcode24 runtime enabled proof found: {summary.get('opcode24RuntimeEnabledProofFound')}",
        f"- opcode24 mode dispatch proof found: {summary.get('opcode24ModeDispatchProofFound')}",
        f"- failed opcode24 runtime enabled gates: `{','.join(summary.get('failedOpcode24RuntimeEnabledGateIds') or []) or '-'}`",
        f"- missing evidence count: {len(summary.get('missingEvidence') or [])}",
        f"- evidence refs: {summary.get('evidenceRefCount')}",
        f"- promotion status: `{summary['promotionStatus']}`",
        "",
        summary["conclusion"],
        "",
        "## Missing Evidence",
        "",
        *[f"- {item}" for item in summary.get("missingEvidence") or []],
        "",
        "## Diagnostic Runtime Watch",
        "",
        "| field | value |",
        "| --- | --- |",
        f"| source poll | `{diagnostic.get('sourcePoll')}` |",
        f"| available | `{diagnostic.get('available')}` |",
        f"| diagnostic only | `{diagnostic.get('diagnosticOnly')}` |",
        f"| route samples | `{diagnostic.get('routeSampleCount')}` across `{diagnostic.get('routeSequenceCount')}` route-reaching sequence(s) |",
        f"| total samples | `{diagnostic.get('totalSampleCount')}` across `{diagnostic.get('totalSequenceCount')}` sequence(s) |",
        f"| route sequence names | `{route_sequences}` |",
        f"| observed selectors | `{observed_selectors}` |",
        f"| mode1 source | `{diagnostic.get('mode1SourceValueHex')}` |",
        f"| runtime flag | `{diagnostic.get('runtimeFlagValueHex')}` |",
        f"| current object index | `{diagnostic.get('currentObjectIndexValueHex')}` |",
        f"| all watched values stable | `{diagnostic.get('allWatchedValuesStable')}` |",
        f"| normal route proof | `{diagnostic.get('normalRouteProof')}` |",
        f"| promotion status | `{diagnostic.get('promotionStatus')}` |",
        "",
        "## Direct Text References",
        "",
        "| instruction | access | width | context | instruction text |",
        "| --- | --- | ---: | --- | --- |",
    ]
    for row in summary["refs"]:
        lines.append(
            f"| `{row.get('instructionVaHex')}` | {row.get('accessKind')} | {row.get('width')} | "
            f"{row.get('context')} | {row.get('instruction')} |"
        )
    lines.extend([
        "",
        "## Save Read Blocks",
        "",
        "| call | target | end | save offset | size | contains | distance after end | description |",
        "| --- | --- | --- | --- | --- | --- | ---: | --- |",
    ])
    for row in summary["saveReadBlocks"]:
        lines.append(
            f"| `{row.get('callVaHex')}` | `{row.get('targetVaHex')}` | `{row.get('endVaHex')}` | "
            f"`{row.get('saveOffsetHex')}` | `{row.get('sizeHex')}` | {row.get('containsRuntimeEnabledFlag')} | "
            f"`{row.get('distanceAfterEndHex')}` | {row.get('description') or '-'} |"
        )
    lines.extend(["", "## Remaining Proofs", ""])
    lines.extend(f"- {item}" for item in summary["remainingProofs"])
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    storage = summary["storage"]
    diagnostic = summary.get("diagnosticRuntimeWatch") or {}
    observed_selectors = ", ".join(diagnostic.get("observedSelectors") or []) or "-"
    route_sequences = ", ".join(diagnostic.get("routeSequenceNames") or []) or "-"
    ref_rows = "\n".join(
        "<tr>"
        f"<td><code>{html.escape(str(row.get('instructionVaHex')))}</code></td>"
        f"<td>{html.escape(str(row.get('accessKind')))}</td>"
        f"<td>{html.escape(str(row.get('width')))}</td>"
        f"<td>{html.escape(str(row.get('context')))}</td>"
        f"<td>{html.escape(str(row.get('instruction')))}</td>"
        "</tr>"
        for row in summary["refs"]
    )
    block_rows = "\n".join(
        "<tr>"
        f"<td><code>{html.escape(str(row.get('callVaHex')))}</code></td>"
        f"<td><code>{html.escape(str(row.get('targetVaHex')))}</code></td>"
        f"<td><code>{html.escape(str(row.get('endVaHex')))}</code></td>"
        f"<td><code>{html.escape(str(row.get('saveOffsetHex')))}</code></td>"
        f"<td><code>{html.escape(str(row.get('sizeHex')))}</code></td>"
        f"<td>{row.get('containsRuntimeEnabledFlag')}</td>"
        f"<td><code>{html.escape(str(row.get('distanceAfterEndHex')))}</code></td>"
        f"<td>{html.escape(str(row.get('description') or '-'))}</td>"
        "</tr>"
        for row in summary["saveReadBlocks"]
    )
    proofs = "\n".join(f"<li>{html.escape(item)}</li>" for item in summary["remainingProofs"])
    missing_items = "\n".join(
        f"<li>{html.escape(item)}</li>" for item in summary.get("missingEvidence") or []
    ) or "<li>-</li>"
    diagnostic_rows = "\n".join([
        f"<tr><th>source poll</th><td><code>{html.escape(str(diagnostic.get('sourcePoll')))}</code></td></tr>",
        f"<tr><th>available</th><td><code>{html.escape(str(diagnostic.get('available')))}</code></td></tr>",
        f"<tr><th>diagnostic only</th><td><code>{html.escape(str(diagnostic.get('diagnosticOnly')))}</code></td></tr>",
        f"<tr><th>route samples</th><td><code>{html.escape(str(diagnostic.get('routeSampleCount')))}</code> across <code>{html.escape(str(diagnostic.get('routeSequenceCount')))}</code> route-reaching sequence(s)</td></tr>",
        f"<tr><th>total samples</th><td><code>{html.escape(str(diagnostic.get('totalSampleCount')))}</code> across <code>{html.escape(str(diagnostic.get('totalSequenceCount')))}</code> sequence(s)</td></tr>",
        f"<tr><th>route sequence names</th><td><code>{html.escape(route_sequences)}</code></td></tr>",
        f"<tr><th>observed selectors</th><td><code>{html.escape(observed_selectors)}</code></td></tr>",
        f"<tr><th>mode1 source</th><td><code>{html.escape(str(diagnostic.get('mode1SourceValueHex')))}</code></td></tr>",
        f"<tr><th>runtime flag</th><td><code>{html.escape(str(diagnostic.get('runtimeFlagValueHex')))}</code></td></tr>",
        f"<tr><th>current object index</th><td><code>{html.escape(str(diagnostic.get('currentObjectIndexValueHex')))}</code></td></tr>",
        f"<tr><th>all watched values stable</th><td><code>{html.escape(str(diagnostic.get('allWatchedValuesStable')))}</code></td></tr>",
        f"<tr><th>normal route proof</th><td><code>{html.escape(str(diagnostic.get('normalRouteProof')))}</code></td></tr>",
        f"<tr><th>promotion status</th><td><code>{html.escape(str(diagnostic.get('promotionStatus')))}</code></td></tr>",
    ])
    return "\n".join([
        "<!doctype html><meta charset=\"utf-8\"><title>Save Selector Opcode 0x24 Runtime Enabled Context</title>",
        "<style>body{font-family:system-ui,sans-serif;background:#111;color:#eee;max-width:1180px;margin:24px auto;line-height:1.45}table{border-collapse:collapse;width:100%;margin:16px 0 28px}td,th{border:1px solid #444;padding:6px 8px;vertical-align:top}th{background:#202020}code{color:#9bd4ff}</style>",
        "<h1>Save Selector Opcode 0x24 Runtime Enabled Context</h1>",
        "<ul>",
        f"<li>route: {html.escape(summary['source'])} -&gt; {html.escape(summary['target'])}</li>",
        f"<li>runtime enabled flag: <code>{html.escape(summary['runtimeEnabledFlagHex'])}</code></li>",
        f"<li>handler/read/pass/fail: <code>{html.escape(summary['handlerVaHex'])}</code> / <code>{html.escape(summary['flagReadInstructionVaHex'])}</code> / <code>{html.escape(summary['flagPassTargetVaHex'])}</code> / <code>{html.escape(summary['flagFailAdvanceVaHex'])}</code></li>",
        f"<li>section: <code>{html.escape(str(storage.get('sectionName')))}</code> raw end <code>{html.escape(str(storage.get('sectionRawEndVaHex')))}</code> virtual end <code>{html.escape(str(storage.get('sectionVirtualEndVaHex')))}</code></li>",
        f"<li>has raw file byte: {storage.get('runtimeEnabledFlagHasRawByte')}</li>",
        f"<li>static initial value: <code>{html.escape(str(storage.get('staticInitialValueHex')))}</code> ({html.escape(str(storage.get('staticInitialValueKind')))}</li>",
        f"<li>save loader block contains runtime flag: {summary['saveReadBlockContainsRuntimeEnabledFlag']}</li>",
        f"<li>direct text refs/read/write: {summary['directTextRefCount']} / {summary['directReadCount']} / {summary['directWriteCount']}</li>",
        f"<li>mode dispatch requires runtime flag == 1: {summary['modeDispatchRequiresRuntimeFlagOne']}</li>",
        f"<li>static evidence proves mode dispatch: {summary['staticEvidenceProvesModeDispatch']}</li>",
        f"<li>proofFound: {summary.get('proofFound')}</li>",
        f"<li>opcode24 runtime enabled proof found: {summary.get('opcode24RuntimeEnabledProofFound')}</li>",
        f"<li>opcode24 mode dispatch proof found: {summary.get('opcode24ModeDispatchProofFound')}</li>",
        f"<li>failed opcode24 runtime enabled gates: <code>{html.escape(','.join(summary.get('failedOpcode24RuntimeEnabledGateIds') or []) or '-')}</code></li>",
        f"<li>missing evidence count: {len(summary.get('missingEvidence') or [])}</li>",
        f"<li>evidence refs: {summary.get('evidenceRefCount')}</li>",
        f"<li>promotion status: <code>{html.escape(summary['promotionStatus'])}</code></li>",
        "</ul>",
        f"<p>{html.escape(summary['conclusion'])}</p>",
        "<h2>Missing Evidence</h2><ul>",
        missing_items,
        "</ul>",
        "<h2>Diagnostic Runtime Watch</h2>",
        "<table><tbody>",
        diagnostic_rows,
        "</tbody></table>",
        "<h2>Direct Text References</h2>",
        "<table><thead><tr><th>instruction</th><th>access</th><th>width</th><th>context</th><th>instruction text</th></tr></thead><tbody>",
        ref_rows,
        "</tbody></table>",
        "<h2>Save Read Blocks</h2>",
        "<table><thead><tr><th>call</th><th>target</th><th>end</th><th>save offset</th><th>size</th><th>contains</th><th>distance after end</th><th>description</th></tr></thead><tbody>",
        block_rows,
        "</tbody></table>",
        "<h2>Remaining Proofs</h2><ul>",
        proofs,
        "</ul>",
    ])


def write_outputs(summary: dict, out_dir: Path, html_out: Path | None = None) -> Path:
    out_dir.mkdir(parents=True, exist_ok=True)
    json_out = out_dir / "save_selector_opcode24_runtime_enabled_context.json"
    json_out.write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        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")
    return json_out


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("--opcode24-globals", type=Path, default=OUT / "save_selector_opcode24_globals.json")
    parser.add_argument("--diagnostic-poll", type=Path, default=OUT / DIAGNOSTIC_WATCH_POLL)
    parser.add_argument("--out-dir", type=Path, default=OUT)
    parser.add_argument(
        "--html-out",
        type=Path,
        default=None,
        help="Optional legacy HTML output path. JSON is the default retained artifact.",
    )
    args = parser.parse_args()
    summary = build_summary(
        args.exe.read_bytes(),
        load_json(args.save_loader_trace, {}),
        load_json(args.opcode24_globals, {}),
        load_json(args.diagnostic_poll, {}),
    )
    json_out = write_outputs(summary, args.out_dir, args.html_out)
    print(f"wrote opcode24 runtime enabled context -> {json_out}")


if __name__ == "__main__":
    main()
