#!/usr/bin/env python3
"""Tie predecessor fill-site rows to opcode 0x10 handler evidence."""
from __future__ import annotations

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

from probe_exe_scene_tables import offset_to_va, read_sections, va_to_offset
from summarize_script_handler_table import handler_bytes, handler_for_opcode, stream_effect_text


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

OPCODE10 = 0x10
OPCODE10_HANDLER = 0x0040B49E
HELPER_VA = 0x00410C90
CURRENT_READER = 0x00542B0C
CURRENT_ROOT = 0x00540714
PREDECESSOR_ROOT = 0x00478364
FILL_SITES = [0x004844D0, 0x004844D8]
FILL_FRAGMENT_STOP = 0x004844DC
OPCODE10_CONTEXT_MISSING_EVIDENCE_BY_GATE = {
    "root-entry-to-fill": (
        "root-entry execution path from 0x00478364 to 0x004844d0/0x004844d8"
    ),
    "descriptor-boundary-bridge": (
        "descriptor-boundary bridge from predecessor root stop through fill to current reader"
    ),
    "dispatch-slice-runtime-proof": (
        "runtime proof that predecessor fill stream executes through the save-selector dispatch slice"
    ),
    "runtime-fill-observed": (
        "runtime branch-state observation matching the predecessor fill hypothesis"
    ),
    "route-order": "predecessor-to-current route order proof before the 0x00542b0c reader",
    "selector-merge-closed": (
        "closed selector-merge proof connecting predecessor 1:0 and source 0:0 into current 2:0"
    ),
}
OPCODE10_CONTEXT_EVIDENCE_REFS = [
    {
        "path": "Hwanse2.exe",
        "fields": [
            "0x0040b49e opcode 0x10 handler",
            "0x00410c90 helper call",
            "0x004844d0/0x004844d8 fill rows",
            "0x00542b0c current reader",
        ],
    },
    {
        "path": "out/script_handler_table.json",
        "fields": ["0x10", "handlerVaHex", "streamEffect", "referenceCount"],
    },
    {
        "path": "out/save_selector_predecessor_fill_execution_order_gap.json",
        "fields": [
            "fillSites",
            "rootTailIsolationScan",
            "directFillSiteRefCounts",
            "proofFound",
        ],
    },
    {
        "path": "out/save_selector_predecessor_branch_state_execution_gap.json",
        "fields": [
            "runtimeBranchStateSplit",
            "runtimePredecessorFillObserved",
            "branchStateExecutionProofFound",
        ],
    },
    {
        "path": "out/save_selector_predecessor_fill_site_execution_context.json",
        "fields": [
            "requiredProofGates",
            "requiredProofGateFailIds",
            "branchStatePollFillMatchCount",
            "fillSiteExecutionContextProven",
        ],
    },
    {
        "path": "out/save_selector_secondary_reset_scope.json",
        "fields": ["helper", "closedStaticResetScope", "promotionStatus"],
    },
]


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


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


def hex_to_int(value: str | None) -> int | None:
    if not value:
        return None
    try:
        return int(value, 16)
    except ValueError:
        return None


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


def direct_dword_refs(exe: bytes, sections: list[dict], target: int) -> list[dict]:
    refs = []
    needle = struct.pack("<I", target)
    offset = 0
    while True:
        hit = exe.find(needle, offset)
        if hit < 0:
            break
        va = offset_to_va(sections, hit)
        if va is not None:
            refs.append({
                "siteVaHex": hex32(va),
                "section": section_name_for_va(sections, va),
                "targetVaHex": hex32(target),
            })
        offset = hit + 1
    return refs


def refs_by_section(refs: list[dict]) -> dict[str, int]:
    counts: dict[str, int] = {}
    for ref in refs:
        section = ref.get("section") or "unknown"
        counts[section] = counts.get(section, 0) + 1
    return dict(sorted(counts.items()))


def call_refs(exe: bytes, sections: list[dict], target: int) -> list[dict]:
    rows = []
    for section in sections:
        if section.get("name") != ".text":
            continue
        start = section["raw"]
        end = start + section["raw_size"]
        data = exe[start:end]
        for index in range(0, max(0, len(data) - 4)):
            if data[index] != 0xE8:
                continue
            va = offset_to_va(sections, start + index)
            if va is None:
                continue
            rel = struct.unpack_from("<i", data, index + 1)[0]
            dest = va + 5 + rel
            if dest == target:
                rows.append({
                    "callVaHex": hex32(va),
                    "targetVaHex": hex32(dest),
                })
    return rows


def dword_hits_in_bytes(base_va: int, data: bytes, targets: dict[str, int]) -> dict[str, list[str]]:
    hits: dict[str, list[str]] = {name: [] for name in targets}
    needles = {name: struct.pack("<I", value) for name, value in targets.items()}
    for name, needle in needles.items():
        offset = 0
        while True:
            hit = data.find(needle, offset)
            if hit < 0:
                break
            hits[name].append(hex32(base_va + hit))
            offset = hit + 1
    return hits


def fill_fragment_rows(root_tail: dict) -> list[dict]:
    rows = []
    fill_start = min(FILL_SITES)
    fill_end = FILL_FRAGMENT_STOP + 4
    for row in root_tail.get("nearbyRows") or []:
        va = hex_to_int(row.get("vaHex"))
        if va is not None and fill_start <= va < fill_end:
            rows.append(row)
    return rows


def row_count(rows: list[dict], *, opcode: str | None = None, handler: str | None = None) -> int:
    count = 0
    for row in rows:
        if opcode is not None and row.get("opcodeHex") != opcode:
            continue
        if handler is not None and row.get("handlerVaHex") != handler:
            continue
        count += 1
    return count


def decode_opcode10_row(row: dict) -> dict:
    value = hex_to_int(row.get("valueHex"))
    if value is None:
        return {}
    opcode = value & 0xFF
    table_selector = (value >> 8) & 0xFF
    helper_case = (value >> 16) & 0xFF
    spare = (value >> 24) & 0xFF
    target_table = "secondaryBranchState" if table_selector else "primaryBranchState"
    target_base = 0x0059E360 if table_selector else 0x0059E370
    helper_kind = "unknown-helper-case"
    helper_fill_count = None
    expected_prefix: list[int] = []
    if helper_case in {0, 1, 2, 3}:
        helper_kind = "constant-prefix-fill"
        helper_fill_count = {0: 2, 1: 4, 2: 5, 3: 6}[helper_case]
        expected_prefix = [1 if index < helper_fill_count else 0 for index in range(12)]
    return {
        "vaHex": row.get("vaHex"),
        "valueHex": row.get("valueHex"),
        "opcodeByteHex": f"0x{opcode:02x}",
        "tableSelectorByteHex": f"0x{table_selector:02x}",
        "helperCaseByteHex": f"0x{helper_case:02x}",
        "spareByteHex": f"0x{spare:02x}",
        "targetTable": target_table,
        "targetBaseHex": hex32(target_base),
        "helperKind": helper_kind,
        "helperFillCount": helper_fill_count,
        "expectedStatePrefix": expected_prefix,
        "fillsSecondaryBranchState": target_table == "secondaryBranchState",
    }


def bool_status(value: bool, true_status: str, false_status: str) -> str:
    return true_status if value else false_status


def build_summary(
    exe: bytes,
    fill_order_gap: dict,
    branch_state_gap: dict,
    fill_site_context: dict,
    secondary_reset_scope: dict,
    script_handler_table: dict | None = None,
) -> dict:
    sections = read_sections(exe)
    handler = handler_for_opcode(exe, sections, OPCODE10)
    handler_code = handler_bytes(exe, sections, OPCODE10_HANDLER)
    handler_effect = handler.get("streamEffect") or {}
    helper_calls = call_refs(exe, sections, HELPER_VA)
    handler_start = OPCODE10_HANDLER
    handler_end = OPCODE10_HANDLER + len(handler_code)
    for row in helper_calls:
        call_va = hex_to_int(row.get("callVaHex"))
        row["insideOpcode10Handler"] = (
            call_va is not None and handler_start <= call_va < handler_end
        )

    direct_targets = {
        "opcode10Handler": OPCODE10_HANDLER,
        "helper": HELPER_VA,
        "predecessorFillSite0": FILL_SITES[0],
        "predecessorFillSite1": FILL_SITES[1],
        "predecessorFillStop": FILL_FRAGMENT_STOP,
        "predecessorRoot": PREDECESSOR_ROOT,
        "currentRoot": CURRENT_ROOT,
        "currentReader": CURRENT_READER,
    }
    direct_ref_rows = {
        name: direct_dword_refs(exe, sections, value)
        for name, value in direct_targets.items()
    }
    direct_refs = {
        name: {
            "targetVaHex": hex32(direct_targets[name]),
            "count": len(rows),
            "textCount": sum(1 for row in rows if row.get("section") == ".text"),
            "sections": refs_by_section(rows),
            "refs": rows[:20],
        }
        for name, rows in direct_ref_rows.items()
    }

    handler_immediates = dword_hits_in_bytes(
        OPCODE10_HANDLER,
        handler_code,
        {
            "predecessorFillSite0": FILL_SITES[0],
            "predecessorFillSite1": FILL_SITES[1],
            "predecessorFillStop": FILL_FRAGMENT_STOP,
            "currentRoot": CURRENT_ROOT,
            "currentReader": CURRENT_READER,
        },
    )
    handler_route_immediate_count = sum(len(values) for values in handler_immediates.values())
    root_tail = fill_order_gap.get("rootTailIsolationScan") or {}
    fragment_rows = fill_fragment_rows(root_tail)
    fragment_opcodes = [row.get("opcodeHex") for row in fragment_rows]
    expected_fragment_opcodes = ["0x10", "0x00", "0x10", "0xc0"]
    decoded_opcode10_rows = [
        decoded
        for row in fragment_rows
        if row.get("opcodeHex") == "0x10"
        for decoded in [decode_opcode10_row(row)]
        if decoded
    ]
    decoded_fill_targets_secondary = (
        bool(decoded_opcode10_rows)
        and all(row.get("fillsSecondaryBranchState") for row in decoded_opcode10_rows)
    )
    decoded_fill_counts = [
        row.get("helperFillCount")
        for row in decoded_opcode10_rows
        if row.get("helperFillCount") is not None
    ]
    decoded_expected_prefix = decoded_opcode10_rows[0].get("expectedStatePrefix") if decoded_opcode10_rows else []
    decoded_expected_fill_hexes = [
        f"0x{value:02x}" for value in decoded_expected_prefix
    ]
    helper_scope = secondary_reset_scope.get("helper") or {}
    runtime_split = branch_state_gap.get("runtimeBranchStateSplit") or {}

    fill_site_text_ref_count = (
        direct_refs["predecessorFillSite0"]["textCount"]
        + direct_refs["predecessorFillSite1"]["textCount"]
    )
    root_tail_branch_to_fill = root_tail.get("branchToFillFragmentCount")
    root_tail_branch_to_reader = root_tail.get("branchToCurrentReaderCount")
    root_tail_fixed_to_fill = root_tail.get("fixedFallthroughToFillCount")
    no_fill_entry_proof = (
        fill_order_gap.get("localFillTraceReachesCurrentReader") is False
        and fill_order_gap.get("rootEntryFixedTraversalFillSitesReachable") is False
        and fill_site_text_ref_count == 0
        and root_tail_branch_to_fill == 0
        and root_tail_branch_to_reader == 0
        and root_tail_fixed_to_fill == 0
    )
    runtime_still_unproven = (
        runtime_split.get("publicPredecessorReached") is True
        and runtime_split.get("observedAllZero") is True
        and runtime_split.get("observedMatchesFill") is False
        and fill_site_context.get("branchStatePollFillMatchCount") == 0
    )
    proof_found = (
        fill_order_gap.get("proofFound") is True
        or fill_site_context.get("fillSiteExecutionContextProven") is True
        or runtime_split.get("fillExecutionOrderProofFound") is True
    )
    required_proof_gate_fail_ids = fill_site_context.get("requiredProofGateFailIds") or []
    missing_evidence = [
        OPCODE10_CONTEXT_MISSING_EVIDENCE_BY_GATE.get(gate_id, gate_id)
        for gate_id in required_proof_gate_fail_ids
    ]
    evidence = [
        {
            "kind": "opcode10-handler",
            "status": bool_status(
                handler.get("handlerVa") == OPCODE10_HANDLER and len(handler_code) > 0,
                "handler-resolved",
                "handler-unresolved",
            ),
            "detail": (
                f"opcode=0x10 handler={handler.get('handlerVaHex')} "
                f"effect={stream_effect_text(handler_effect)} bytes={len(handler_code)}"
            ),
        },
        {
            "kind": "helper-call-scope",
            "status": bool_status(
                len(helper_calls) == 1 and all(row.get("insideOpcode10Handler") for row in helper_calls),
                "helper-only-called-from-opcode10",
                "helper-scope-open",
            ),
            "detail": (
                f"helper={hex32(HELPER_VA)} calls={len(helper_calls)} "
                f"secondaryResetScopeOnly={helper_scope.get('onlyDirectCallInsideOpcode10Handler')}"
            ),
        },
        {
            "kind": "fill-fragment-shape",
            "status": bool_status(
                fragment_opcodes == expected_fragment_opcodes,
                "opcode10-fill-then-descriptor-boundary",
                "unexpected-fragment-shape",
            ),
            "detail": (
                f"rows={','.join(row.get('vaHex') or '-' for row in fragment_rows)} "
                f"opcodes={','.join(fragment_opcodes)}"
            ),
        },
        {
            "kind": "opcode10-decoded-fill",
            "status": bool_status(
                decoded_fill_targets_secondary and decoded_fill_counts == [2, 2],
                "secondary-branch-state-prefix-fill",
                "decoded-fill-open",
            ),
            "detail": (
                "rows="
                f"{';'.join(f'{row.get('vaHex')}:{row.get('targetTable')}:case={row.get('helperCaseByteHex')}:count={row.get('helperFillCount')}' for row in decoded_opcode10_rows) or '-'} "
                f"expectedPrefix={','.join(decoded_expected_fill_hexes) or '-'}"
            ),
        },
        {
            "kind": "direct-fill-entry-refs",
            "status": bool_status(fill_site_text_ref_count == 0, "no-fill-site-text-refs", "has-fill-site-text-refs"),
            "detail": (
                f"fillTextRefs={fill_site_text_ref_count} "
                f"directFillCounts={fill_order_gap.get('directFillSiteRefCounts')}"
            ),
        },
        {
            "kind": "root-tail-control",
            "status": bool_status(
                no_fill_entry_proof,
                "no-branch-or-fallthrough-to-fill",
                "possible-fill-entry-proof",
            ),
            "detail": (
                f"branchToFill={root_tail_branch_to_fill} "
                f"branchToReader={root_tail_branch_to_reader} "
                f"fixedToFill={root_tail_fixed_to_fill}"
            ),
        },
        {
            "kind": "runtime-branch-state",
            "status": bool_status(
                runtime_still_unproven,
                "public-predecessor-all-zero-fill-not-observed",
                "runtime-fill-observed-or-open",
            ),
            "detail": (
                f"publicPredecessor={runtime_split.get('publicPredecessorReached')} "
                f"matchesFill={runtime_split.get('observedMatchesFill')} "
                f"allZero={runtime_split.get('observedAllZero')} "
                f"fillMatches={fill_site_context.get('branchStatePollFillMatchCount')}"
            ),
        },
        {
            "kind": "required-proof-gates",
            "status": bool_status(
                fill_site_context.get("requiredProofGatePassCount") == 0,
                "no-proof-gates-passed",
                "some-proof-gate-passed",
            ),
            "detail": (
                f"pass={fill_site_context.get('requiredProofGatePassCount')} "
                f"fail={fill_site_context.get('requiredProofGateFailCount')} "
                f"allBlocked={fill_site_context.get('requiredProofGateAllBlocked')} "
                "failedIds="
                f"{','.join(fill_site_context.get('requiredProofGateFailIds') or []) or '-'}"
            ),
        },
    ]
    promotion_status = "blocked" if not proof_found else "ready-for-review"
    conclusion = (
        "Opcode 0x10 and helper 0x00410c90 explain the two predecessor fill rows, but this does not prove "
        "the predecessor fill fragment executes before reader 0x00542b0c. There are no direct .text refs to "
        "the fill sites, the predecessor root tail has no branch/fallthrough to the fragment, and bounded "
        "runtime polls reach public predecessor selector 1:0 with secondaryBranchState still all zero."
        if promotion_status == "blocked"
        else "The predecessor fill opcode context has enough evidence for review."
    )
    return {
        "source": "map1_01a",
        "target": "map2_02d",
        "predecessorSelector": fill_order_gap.get("predecessorSelector") or "1:0",
        "predecessorRootHex": fill_order_gap.get("predecessorRootHex") or hex32(PREDECESSOR_ROOT),
        "currentSelector": fill_order_gap.get("currentSelector") or "2:0",
        "currentRootHex": fill_order_gap.get("currentRootHex") or hex32(CURRENT_ROOT),
        "currentReaderHex": fill_order_gap.get("currentReaderHex") or hex32(CURRENT_READER),
        "opcodeHex": "0x10",
        "opcodeHandlerHex": handler.get("handlerVaHex"),
        "opcodeHandlerExpectedHex": hex32(OPCODE10_HANDLER),
        "opcodeHandlerMatchesExpected": handler.get("handlerVa") == OPCODE10_HANDLER,
        "opcodeHandlerCodeSize": len(handler_code),
        "opcodeHandlerStreamEffect": handler_effect,
        "opcodeHandlerStreamEffectText": stream_effect_text(handler_effect),
        "scriptHandlerTableReferenceCount": next(
            (
                row.get("referenceCount")
                for row in (script_handler_table or {}).get("entries") or []
                if row.get("opcodeHex") == "0x10"
            ),
            None,
        ),
        "helperVaHex": hex32(HELPER_VA),
        "helperDirectCallCount": len(helper_calls),
        "helperDirectCalls": helper_calls,
        "helperOnlyDirectCallInsideOpcode10Handler": (
            len(helper_calls) == 1 and all(row.get("insideOpcode10Handler") for row in helper_calls)
        ),
        "secondaryResetScopeHelperOnlyDirectCallInsideOpcode10Handler": helper_scope.get(
            "onlyDirectCallInsideOpcode10Handler"
        ),
        "fillSites": [hex32(value) for value in FILL_SITES],
        "fillFragmentRangeHex": f"{hex32(min(FILL_SITES))}..{hex32(FILL_FRAGMENT_STOP + 4)}",
        "fillFragmentRows": fragment_rows,
        "fillFragmentOpcodes": fragment_opcodes,
        "fillFragmentExpectedOpcodes": expected_fragment_opcodes,
        "fillFragmentHasOnlyExpectedRows": fragment_opcodes == expected_fragment_opcodes,
        "decodedOpcode10Rows": decoded_opcode10_rows,
        "decodedOpcode10FillTargetTables": sorted({row.get("targetTable") for row in decoded_opcode10_rows}),
        "decodedOpcode10HelperFillCounts": decoded_fill_counts,
        "decodedOpcode10ExpectedStatePrefix": decoded_expected_prefix,
        "decodedOpcode10ExpectedFillHexes": decoded_expected_fill_hexes,
        "decodedOpcode10AllSecondaryBranchState": decoded_fill_targets_secondary,
        "fillOpcode10RowCount": row_count(fragment_rows, opcode="0x10", handler=hex32(OPCODE10_HANDLER)),
        "fillDefaultAdvanceRowCount": row_count(fragment_rows, handler="0x0040239f"),
        "fillDescriptorStopRowCount": row_count(fragment_rows, opcode="0xc0", handler="0x00440c5c"),
        "localFillTraceReachesCurrentReader": fill_order_gap.get("localFillTraceReachesCurrentReader"),
        "rootEntryFixedTraversalFillSitesReachable": fill_order_gap.get(
            "rootEntryFixedTraversalFillSitesReachable"
        ),
        "rootTailDescriptorIsolated": fill_order_gap.get("rootTailDescriptorIsolated"),
        "rootTailBranchToFillFragmentCount": root_tail_branch_to_fill,
        "rootTailBranchToCurrentReaderCount": root_tail_branch_to_reader,
        "rootTailFixedFallthroughToFillCount": root_tail_fixed_to_fill,
        "directRefs": direct_refs,
        "directFillSiteTextRefCount": fill_site_text_ref_count,
        "directCurrentReaderTextRefCount": direct_refs["currentReader"]["textCount"],
        "opcode10HandlerRouteImmediateCount": handler_route_immediate_count,
        "opcode10HandlerImmediateHits": handler_immediates,
        "publicPredecessorReached": runtime_split.get("publicPredecessorReached"),
        "runtimeObservedAllZero": runtime_split.get("observedAllZero"),
        "runtimeMatchesExpectedFill": runtime_split.get("observedMatchesFill"),
        "runtimeExpectedFillHexes": runtime_split.get("expectedFillHexes") or [],
        "branchStatePollSampleCount": fill_site_context.get("branchStatePollSampleCount"),
        "branchStatePollPublicPredecessorHitCount": fill_site_context.get(
            "branchStatePollPublicPredecessorHitCount"
        ),
        "branchStatePollCurrentRootHitCount": fill_site_context.get(
            "branchStatePollCurrentRootHitCount"
        ),
        "branchStatePollRouteSelectorHitCount": fill_site_context.get(
            "branchStatePollRouteSelectorHitCount"
        ),
        "branchStatePollAllZeroCount": fill_site_context.get("branchStatePollAllZeroCount"),
        "branchStatePollFillMatchCount": fill_site_context.get("branchStatePollFillMatchCount"),
        "requiredProofGates": fill_site_context.get("requiredProofGates") or [],
        "requiredProofGateCount": fill_site_context.get("requiredProofGateCount"),
        "requiredProofGatePassCount": fill_site_context.get("requiredProofGatePassCount"),
        "requiredProofGateFailCount": fill_site_context.get("requiredProofGateFailCount"),
        "requiredProofGateFailIds": required_proof_gate_fail_ids,
        "requiredProofGateAllBlocked": fill_site_context.get("requiredProofGateAllBlocked"),
        "fillExecutionOrderProofFound": fill_order_gap.get("proofFound"),
        "fillSiteExecutionContextProven": fill_site_context.get("fillSiteExecutionContextProven"),
        "proofFound": proof_found,
        "failedPredecessorFillOpcode10GateIds": required_proof_gate_fail_ids,
        "missingEvidence": missing_evidence,
        "evidenceRefs": OPCODE10_CONTEXT_EVIDENCE_REFS,
        "evidenceRefCount": len(OPCODE10_CONTEXT_EVIDENCE_REFS),
        "evidence": evidence,
        "promotionStatus": promotion_status,
        "conclusion": conclusion,
    }


def markdown(summary: dict) -> str:
    lines = [
        "# Save Selector Predecessor Fill Opcode10 Context",
        "",
        summary["conclusion"],
        "",
        f"- route: `{summary['source']} -> {summary['target']}`",
        f"- predecessor/current: `{summary['predecessorSelector']}` `{summary['predecessorRootHex']}` -> `{summary['currentSelector']}` `{summary['currentRootHex']}`",
        f"- current reader: `{summary['currentReaderHex']}`",
        f"- opcode handler: `{summary['opcodeHex']}` -> `{summary['opcodeHandlerHex']}` ({summary['opcodeHandlerStreamEffectText']})",
        f"- helper: `{summary['helperVaHex']}` calls={summary['helperDirectCallCount']} onlyOpcode10={summary['helperOnlyDirectCallInsideOpcode10Handler']}",
        f"- fill fragment: `{summary['fillFragmentRangeHex']}` opcodes={','.join(summary['fillFragmentOpcodes'])}",
        f"- decoded opcode10 fill: tables={','.join(summary['decodedOpcode10FillTargetTables']) or '-'} counts={','.join(str(value) for value in summary['decodedOpcode10HelperFillCounts']) or '-'} expected={','.join(summary['decodedOpcode10ExpectedFillHexes']) or '-'}",
        f"- fill rows: opcode10={summary['fillOpcode10RowCount']} default={summary['fillDefaultAdvanceRowCount']} descriptorStop={summary['fillDescriptorStopRowCount']}",
        f"- direct refs: fillText={summary['directFillSiteTextRefCount']} currentReaderText={summary['directCurrentReaderTextRefCount']} handlerRouteImmediate={summary['opcode10HandlerRouteImmediateCount']}",
        f"- root tail: isolated={summary['rootTailDescriptorIsolated']} branchToFill={summary['rootTailBranchToFillFragmentCount']} branchToReader={summary['rootTailBranchToCurrentReaderCount']} fixedToFill={summary['rootTailFixedFallthroughToFillCount']}",
        f"- runtime: publicPred={summary['publicPredecessorReached']} allZero={summary['runtimeObservedAllZero']} matchesFill={summary['runtimeMatchesExpectedFill']} polls={summary['branchStatePollSampleCount']} fillMatches={summary['branchStatePollFillMatchCount']}",
        f"- proof gates: gates={summary['requiredProofGatePassCount']}/{summary['requiredProofGateFailCount']} allBlocked={summary['requiredProofGateAllBlocked']} failed={','.join(summary['requiredProofGateFailIds']) or '-'}",
        f"- proof: order={summary['fillExecutionOrderProofFound']} context={summary['fillSiteExecutionContextProven']} proofFound={summary['proofFound']}",
        f"- failed opcode10 context gates: `{','.join(summary.get('failedPredecessorFillOpcode10GateIds') or []) or '-'}`",
        f"- missing evidence count: {len(summary.get('missingEvidence') or [])}",
        f"- evidence refs: {summary.get('evidenceRefCount')}",
        f"- promotion status: {summary['promotionStatus']}",
        "",
        "## Evidence",
        "",
        "| kind | status | detail |",
        "| --- | --- | --- |",
    ]
    for row in summary["evidence"]:
        lines.append(f"| {row['kind']} | {row['status']} | {row['detail']} |")
    lines.extend(["", "## Fill Fragment", "", "| va | value | opcode | handler | fixed advances |", "| --- | --- | --- | --- | --- |"])
    for row in summary["fillFragmentRows"]:
        lines.append(
            f"| `{row.get('vaHex')}` | `{row.get('valueHex')}` | `{row.get('opcodeHex')}` | "
            f"`{row.get('handlerVaHex')}` | {','.join(str(value) for value in row.get('fixedAdvances') or []) or '-'} |"
        )
    lines.extend([
        "",
        "## Decoded Opcode10 Fill Rows",
        "",
        "| va | table byte | helper case | target table | fill count | expected prefix |",
        "| --- | --- | --- | --- | ---: | --- |",
    ])
    for row in summary["decodedOpcode10Rows"]:
        lines.append(
            f"| `{row.get('vaHex')}` | `{row.get('tableSelectorByteHex')}` | "
            f"`{row.get('helperCaseByteHex')}` | {row.get('targetTable')} | "
            f"{row.get('helperFillCount')} | {','.join(f'0x{value:02x}' for value in row.get('expectedStatePrefix') or []) or '-'} |"
        )
    lines.extend(["", "## Direct Refs", "", "| target | refs | text refs | sections |", "| --- | ---: | ---: | --- |"])
    for name, row in summary["directRefs"].items():
        sections = ",".join(f"{key}:{value}" for key, value in (row.get("sections") or {}).items()) or "-"
        lines.append(
            f"| {name} `{row.get('targetVaHex')}` | {row.get('count')} | {row.get('textCount')} | {sections} |"
        )
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    evidence_rows = "\n".join(
        "<tr>"
        f"<td>{html.escape(row['kind'])}</td>"
        f"<td>{html.escape(row['status'])}</td>"
        f"<td>{html.escape(row['detail'])}</td>"
        "</tr>"
        for row in summary["evidence"]
    )
    fragment_rows = "\n".join(
        "<tr>"
        f"<td><code>{html.escape(row.get('vaHex') or '-')}</code></td>"
        f"<td><code>{html.escape(row.get('valueHex') or '-')}</code></td>"
        f"<td><code>{html.escape(row.get('opcodeHex') or '-')}</code></td>"
        f"<td><code>{html.escape(row.get('handlerVaHex') or '-')}</code></td>"
        f"<td>{html.escape(','.join(str(value) for value in row.get('fixedAdvances') or []) or '-')}</td>"
        "</tr>"
        for row in summary["fillFragmentRows"]
    )
    decoded_rows = "\n".join(
        "<tr>"
        f"<td><code>{html.escape(row.get('vaHex') or '-')}</code></td>"
        f"<td><code>{html.escape(row.get('tableSelectorByteHex') or '-')}</code></td>"
        f"<td><code>{html.escape(row.get('helperCaseByteHex') or '-')}</code></td>"
        f"<td>{html.escape(row.get('targetTable') or '-')}</td>"
        f"<td>{html.escape(str(row.get('helperFillCount')))}</td>"
        f"<td>{html.escape(','.join(f'0x{value:02x}' for value in row.get('expectedStatePrefix') or []) or '-')}</td>"
        "</tr>"
        for row in summary["decodedOpcode10Rows"]
    )
    ref_rows = "\n".join(
        "<tr>"
        f"<td>{html.escape(name)} <code>{html.escape(row.get('targetVaHex') or '-')}</code></td>"
        f"<td>{row.get('count')}</td>"
        f"<td>{row.get('textCount')}</td>"
        f"<td>{html.escape(','.join(f'{key}:{value}' for key, value in (row.get('sections') or {}).items()) or '-')}</td>"
        "</tr>"
        for name, row in summary["directRefs"].items()
    )
    return "\n".join([
        "<!doctype html><meta charset=\"utf-8\"><title>Save Selector Predecessor Fill Opcode10 Context</title>",
        "<style>body{font-family:system-ui,sans-serif;background:#111;color:#eee;max-width:1100px;margin:24px auto}table{border-collapse:collapse;width:100%;margin:18px 0}td,th{border:1px solid #444;padding:6px 8px;vertical-align:top}code{color:#9bd4ff}</style>",
        "<h1>Save Selector Predecessor Fill Opcode10 Context</h1>",
        f"<p>{html.escape(summary['conclusion'])}</p>",
        f"<p>route <code>{html.escape(summary['source'])} -&gt; {html.escape(summary['target'])}</code>; predecessor <code>{html.escape(summary['predecessorSelector'])}</code> <code>{html.escape(summary['predecessorRootHex'])}</code>; current <code>{html.escape(summary['currentSelector'])}</code> <code>{html.escape(summary['currentRootHex'])}</code>; reader <code>{html.escape(summary['currentReaderHex'])}</code>.</p>",
        f"<p>opcode <code>{html.escape(summary['opcodeHex'])}</code> handler <code>{html.escape(summary['opcodeHandlerHex'] or '-')}</code> ({html.escape(summary['opcodeHandlerStreamEffectText'])}); helper <code>{html.escape(summary['helperVaHex'])}</code> calls {summary['helperDirectCallCount']} onlyOpcode10={summary['helperOnlyDirectCallInsideOpcode10Handler']}.</p>",
        f"<p>fill fragment <code>{html.escape(summary['fillFragmentRangeHex'])}</code>; opcodes {html.escape(','.join(summary['fillFragmentOpcodes']))}; opcode10={summary['fillOpcode10RowCount']}; default={summary['fillDefaultAdvanceRowCount']}; descriptorStop={summary['fillDescriptorStopRowCount']}; fillText={summary['directFillSiteTextRefCount']}; currentReaderText={summary['directCurrentReaderTextRefCount']}; handlerRouteImmediate={summary['opcode10HandlerRouteImmediateCount']}.</p>",
        f"<p>decoded opcode10 fill tables={html.escape(','.join(summary['decodedOpcode10FillTargetTables']) or '-')}; counts={html.escape(','.join(str(value) for value in summary['decodedOpcode10HelperFillCounts']) or '-')}; expected={html.escape(','.join(summary['decodedOpcode10ExpectedFillHexes']) or '-')}.</p>",
        f"<p>runtime publicPred={summary['publicPredecessorReached']}; allZero={summary['runtimeObservedAllZero']}; matchesFill={summary['runtimeMatchesExpectedFill']}; polls={summary['branchStatePollSampleCount']}; fillMatches={summary['branchStatePollFillMatchCount']}; proof gates {summary['requiredProofGatePassCount']}/{summary['requiredProofGateFailCount']}; allBlocked={summary['requiredProofGateAllBlocked']}; failed={html.escape(','.join(summary['requiredProofGateFailIds']) or '-')}; proofFound={summary['proofFound']}; failed opcode10 context gates={html.escape(','.join(summary.get('failedPredecessorFillOpcode10GateIds') or []) or '-')}; missing evidence count={len(summary.get('missingEvidence') or [])}; evidence refs={summary.get('evidenceRefCount')}; promotion status {html.escape(summary['promotionStatus'])}.</p>",
        "<h2>Evidence</h2><table><thead><tr><th>kind</th><th>status</th><th>detail</th></tr></thead><tbody>",
        evidence_rows,
        "</tbody></table>",
        "<h2>Fill Fragment</h2><table><thead><tr><th>va</th><th>value</th><th>opcode</th><th>handler</th><th>fixed advances</th></tr></thead><tbody>",
        fragment_rows,
        "</tbody></table>",
        "<h2>Decoded Opcode10 Fill Rows</h2><table><thead><tr><th>va</th><th>table byte</th><th>helper case</th><th>target table</th><th>fill count</th><th>expected prefix</th></tr></thead><tbody>",
        decoded_rows,
        "</tbody></table>",
        "<h2>Direct Refs</h2><table><thead><tr><th>target</th><th>refs</th><th>text refs</th><th>sections</th></tr></thead><tbody>",
        ref_rows,
        "</tbody></table>",
    ])


def write_outputs(summary: dict, out_dir: Path = OUT, html_out: Path | None = None) -> Path:
    out_dir.mkdir(parents=True, exist_ok=True)
    json_out = out_dir / "save_selector_predecessor_fill_opcode10_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("--out-dir", type=Path, default=OUT)
    parser.add_argument("--html-out", type=Path)
    args = parser.parse_args()
    summary = build_summary(
        args.exe.read_bytes(),
        load_json(args.out_dir / "save_selector_predecessor_fill_execution_order_gap.json", {}),
        load_json(args.out_dir / "save_selector_predecessor_branch_state_execution_gap.json", {}),
        load_json(args.out_dir / "save_selector_predecessor_fill_site_execution_context.json", {}),
        load_json(args.out_dir / "save_selector_secondary_reset_scope.json", {}),
        load_json(args.out_dir / "script_handler_table.json", {}),
    )
    json_out = write_outputs(summary, args.out_dir, args.html_out)
    print(
        "wrote predecessor fill opcode10 context -> "
        f"{json_out}"
    )


if __name__ == "__main__":
    main()
