#!/usr/bin/env python3
"""Build a direct bridge matrix for the 0:0, 1:0, and 2:0 selector roots."""
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 read_sections, va_to_offset
from summarize_save_selector_stream_traces import byte_at, handler_entry


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

SOURCE = "map1_01a"
TARGET = "map2_02d"
SOURCE_SELECTOR = "0:0"
PREDECESSOR_SELECTOR = "1:0"
CURRENT_SELECTOR = "2:0"
FAILED_MERGE_BRIDGE_GATE_IDS = [
    "selector-2:0-runtime-or-savedata",
    "source-current-vm-control-flow",
    "strict-source-hotspot",
]
MERGE_BRIDGE_MISSING_EVIDENCE = [
    "selector 2:0 gameplay savedat or selected-pointer runtime trace",
    "VM control flow from source-side 0:0 into current 2:0",
    "strict map1_01a source coordinate or hotspot",
]
MERGE_BRIDGE_EVIDENCE_REFS = [
    {
        "path": "Hwanse2.exe",
        "fields": [
            "0:0 source selector root range",
            "1:0 predecessor selector root range",
            "2:0 current selector root range",
            "encoded anchor scan windows",
        ],
    },
    {
        "path": "out/save_scene_selectors.json",
        "fields": ["group", "slot", "selectedPointerHex", "fieldMaps"],
    },
]

PREDECESSOR_FILL_SITES = {0x004844D0, 0x004844D8}
FIRST_PREDECESSOR_FILL = min(PREDECESSOR_FILL_SITES)

CONTEXTS = {
    SOURCE_SELECTOR: {
        "role": "source-side confirmed selector",
        "root": 0x00501808,
        "scan_start": 0x00501808,
        "scan_end": 0x00503570,
        "exact_targets": {
            0x00501808: "source root 0:0",
            0x00503234: "source pointer node",
            0x00503244: "source map1_01a leaf",
            0x00503260: "source shared leaf",
            0x0050327C: "source shared leaf",
            0x005032D8: "source entry scene record map1_01a",
            0x005032E4: "source map1_02b leaf",
            0x00503350: "source strict event scene record map1_02b",
            0x0050335C: "source event dispatch neighborhood",
        },
    },
    PREDECESSOR_SELECTOR: {
        "role": "target-side predecessor selector",
        "root": 0x00478364,
        "scan_start": 0x00478364,
        "scan_end": 0x0048458C,
        "exact_targets": {
            0x00478364: "predecessor root 1:0",
            0x004844D0: "predecessor secondaryBranchState fill",
            0x004844D8: "predecessor secondaryBranchState fill",
        },
    },
    CURRENT_SELECTOR: {
        "role": "current merge/frontier selector",
        "root": 0x00540714,
        "scan_start": 0x00540714,
        "scan_end": 0x00543578,
        "exact_targets": {
            0x00540714: "current root 2:0",
            0x005429DC: "current pointer node",
            0x00542A84: "current map1_01a leaf",
            0x00542A98: "current map1_01a leaf",
            0x00542AAC: "current map1_01a leaf",
            0x00542AC0: "current shared leaf",
            0x00542AD4: "current shared leaf",
            0x00542AE8: "current shared leaf",
            0x00542B0C: "current frontier reader/resource gate",
            0x00542B44: "current source scene record map1_01a",
            0x00542BAC: "current target scene record map2_02d",
        },
    },
}


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


def range_hex(start: int, end: int) -> str:
    return f"{hex32(start)}..{hex32(end)}"


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 selector_row(selectors: list[dict], label: str) -> dict:
    group_text, slot_text = label.split(":", 1)
    group = int(group_text)
    slot = int(slot_text)
    return next(
        (
            row for row in selectors
            if row.get("group") == group and row.get("slot") == slot
        ),
        {},
    )


def dword_at_va(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 u16_at_va(exe: bytes, sections: list[dict], va: int) -> int | None:
    offset = va_to_offset(sections, va)
    if offset is None or offset + 2 > len(exe):
        return None
    return struct.unpack_from("<H", exe, offset)[0]


def s16_at_va(exe: bytes, sections: list[dict], va: int) -> int | None:
    offset = va_to_offset(sections, va)
    if offset is None or offset + 2 > len(exe):
        return None
    return struct.unpack_from("<h", exe, offset)[0]


def s32_at_va(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 readable_vas(sections: list[dict], start_va: int, end_va: int) -> list[int]:
    spans: list[tuple[int, int]] = []
    for section in sections:
        section_start = int(section["va"])
        section_end = section_start + int(section["raw_size"])
        start = max(start_va, section_start)
        end = min(end_va, section_end)
        if start < end:
            spans.append((start, end))
    vas: list[int] = []
    for start, end in spans:
        vas.extend(range(start, end))
    return vas


def context_summary(selectors: list[dict], label: str) -> dict:
    context = CONTEXTS[label]
    row = selector_row(selectors, label)
    return {
        "selector": label,
        "role": context["role"],
        "rootVaHex": hex32(context["root"]),
        "scanRangeHex": range_hex(context["scan_start"], context["scan_end"]),
        "fieldMaps": row.get("fieldMaps") or [],
        "linkedCns": row.get("linkedCns") or [],
    }


def exact_target_rows(targets: dict[int, str]) -> list[dict]:
    return [
        {"vaHex": hex32(value), "label": label}
        for value, label in sorted(targets.items())
    ]


def scan_encoded_anchor_bridge(
    exe: bytes,
    sections: list[dict],
    source_label: str,
    target_label: str,
) -> dict:
    source = CONTEXTS[source_label]
    target = CONTEXTS[target_label]
    target_rows = sorted(target["exact_targets"].items())
    low16_targets: dict[int, list[tuple[int, str]]] = {}
    source_rel16_targets: dict[int, tuple[int, str]] = {}
    source_rel32_targets: dict[int, tuple[int, str]] = {}
    exact_targets = {va: label for va, label in target_rows}
    for target_va, label in target_rows:
        low16_targets.setdefault(target_va & 0xFFFF, []).append((target_va, label))
        rel = target_va - source["scan_start"]
        if 0 <= rel <= 0xFFFF:
            source_rel16_targets[rel] = (target_va, label)
        if 0 <= rel <= 0xFFFFFFFF:
            source_rel32_targets[rel] = (target_va, label)

    handler_cache: dict[int, dict] = {}
    raw_rows: list[dict] = []
    branch_attached_rows: list[dict] = []
    modeled_rows: list[dict] = []
    counts = {
        "abs16Low": 0,
        "sourceRelativeU16": 0,
        "sourceRelativeU32": 0,
        "signedRel16SitePlus2": 0,
        "signedRel32SitePlus4": 0,
    }

    def cached_handler(opcode: int | None) -> dict:
        if opcode is None:
            return {}
        if opcode not in handler_cache:
            handler_cache[opcode] = handler_entry(exe, sections, opcode)
        return handler_cache[opcode]

    def add_row(kind: str, site_va: int, value: int, target_va: int, label: str, width: int) -> None:
        row_va = site_va & ~3
        opcode = byte_at(exe, sections, row_va)
        handler = cached_handler(opcode)
        row = {
            "kind": kind,
            "siteVaHex": hex32(site_va),
            "rowVaHex": hex32(row_va),
            "valueHex": f"0x{value & ((1 << (width * 8)) - 1):0{width * 2}x}",
            "targetVaHex": hex32(target_va),
            "targetLabel": label,
            "opcodeHex": f"0x{opcode:02x}" if opcode is not None else None,
            "handlerVaHex": handler.get("handlerVaHex"),
            "handlerSection": handler.get("handlerSection"),
            "handlerCanJumpToDwordAtPlus4": handler.get("canJumpToDwordAtPlus4") is True,
            "handlerFixedAdvances": handler.get("fixedAdvances") or [],
            "startsAtBranchTargetField": (
                handler.get("canJumpToDwordAtPlus4") is True and site_va == row_va + 4
            ),
        }
        if len(raw_rows) < 32:
            raw_rows.append(row)
        if row["startsAtBranchTargetField"] and len(branch_attached_rows) < 32:
            branch_attached_rows.append(row)

    for site_va in readable_vas(sections, source["scan_start"], source["scan_end"]):
        value16 = u16_at_va(exe, sections, site_va)
        if value16 is not None:
            for target_va, label in low16_targets.get(value16, []):
                counts["abs16Low"] += 1
                add_row("abs16-low", site_va, value16, target_va, label, 2)
            rel_target = source_rel16_targets.get(value16)
            if rel_target is not None:
                target_va, label = rel_target
                counts["sourceRelativeU16"] += 1
                add_row("source-relative-u16", site_va, value16, target_va, label, 2)
            rel16 = s16_at_va(exe, sections, site_va)
            if rel16 is not None:
                target_va = site_va + 2 + rel16
                label = exact_targets.get(target_va)
                if label is not None:
                    counts["signedRel16SitePlus2"] += 1
                    add_row("signed-rel16-site-plus2", site_va, rel16, target_va, label, 2)
        value32 = dword_at_va(exe, sections, site_va)
        if value32 is not None:
            rel_target = source_rel32_targets.get(value32)
            if rel_target is not None:
                target_va, label = rel_target
                counts["sourceRelativeU32"] += 1
                add_row("source-relative-u32", site_va, value32, target_va, label, 4)
            rel32 = s32_at_va(exe, sections, site_va)
            if rel32 is not None:
                target_va = site_va + 4 + rel32
                label = exact_targets.get(target_va)
                if label is not None:
                    counts["signedRel32SitePlus4"] += 1
                    add_row("signed-rel32-site-plus4", site_va, rel32, target_va, label, 4)

    for row_va in range(source["scan_start"], source["scan_end"], 4):
        opcode = byte_at(exe, sections, row_va)
        handler = cached_handler(opcode)
        for advance in handler.get("fixedAdvances") or []:
            target_va = row_va + advance
            label = exact_targets.get(target_va)
            if label is None:
                continue
            modeled_rows.append({
                "siteVaHex": hex32(row_va),
                "advance": advance,
                "targetVaHex": hex32(target_va),
                "targetLabel": label,
                "handlerVaHex": handler.get("handlerVaHex"),
                "handlerSection": handler.get("handlerSection"),
            })

    raw_count = sum(counts.values())
    branch_attached_count = len(branch_attached_rows)
    modeled_count = len(modeled_rows)
    promoting_count = modeled_count
    if promoting_count:
        classification = "encoded-anchor-control-flow-candidate"
    elif raw_count:
        classification = "raw-encoded-anchor-scalars-nonpromoting"
    else:
        classification = "no-encoded-anchor-candidates"
    return {
        "sourceSelector": source_label,
        "targetSelector": target_label,
        "sourceRangeHex": range_hex(source["scan_start"], source["scan_end"]),
        "targetExactAnchorCount": len(target_rows),
        "targetExactAnchors": exact_target_rows(target["exact_targets"]),
        "rawScalarCandidateCounts": counts,
        "rawScalarCandidateCount": raw_count,
        "branchAttachedEncodedFieldCount": branch_attached_count,
        "modeledControlFlowCandidateCount": modeled_count,
        "promotingCandidateCount": promoting_count,
        "classification": classification,
        "promotionStatus": "ready-for-review" if promoting_count else "blocked",
        "rawScalarCandidateRows": raw_rows,
        "branchAttachedEncodedRows": branch_attached_rows,
        "modeledControlFlowRows": modeled_rows[:32],
    }


def scan_direct_dword_bridge(
    exe: bytes,
    sections: list[dict],
    source_label: str,
    target_label: str,
) -> dict:
    source = CONTEXTS[source_label]
    target = CONTEXTS[target_label]
    hits = []
    readable = 0
    unreadable = 0
    for va in range(source["scan_start"], source["scan_end"], 4):
        value = dword_at_va(exe, sections, va)
        if value is None:
            unreadable += 1
            continue
        readable += 1
        inside_target_range = target["scan_start"] <= value < target["scan_end"]
        exact_label = target["exact_targets"].get(value)
        if not inside_target_range and exact_label is None:
            continue
        hits.append({
            "sourceVaHex": hex32(va),
            "valueHex": hex32(value),
            "insideTargetRange": inside_target_range,
            "targetExactLabel": exact_label,
            "beforePredecessorFill": target_label == PREDECESSOR_SELECTOR and value < FIRST_PREDECESSOR_FILL,
            "targetsPredecessorFillSite": value in PREDECESSOR_FILL_SITES,
        })
    return {
        "sourceSelector": source_label,
        "targetSelector": target_label,
        "sourceRangeHex": range_hex(source["scan_start"], source["scan_end"]),
        "targetRangeHex": range_hex(target["scan_start"], target["scan_end"]),
        "readableDwordCount": readable,
        "unreadableDwordCount": unreadable,
        "hitCount": len(hits),
        "hitCountBeforePredecessorFill": sum(1 for row in hits if row["beforePredecessorFill"]),
        "hitCountToPredecessorFillSites": sum(1 for row in hits if row["targetsPredecessorFillSite"]),
        "targetExactValues": exact_target_rows(target["exact_targets"]),
        "hits": hits,
    }


def build_summary(exe: bytes, selectors: list[dict] | None = None) -> dict:
    sections = read_sections(exe)
    selectors = selectors if selectors is not None else load_json(OUT / "save_scene_selectors.json", [])
    labels = [SOURCE_SELECTOR, PREDECESSOR_SELECTOR, CURRENT_SELECTOR]
    pair_rows = [
        {
            **scan_direct_dword_bridge(exe, sections, source, target),
            "encodedAnchorScan": scan_encoded_anchor_bridge(exe, sections, source, target),
        }
        for source in labels
        for target in labels
        if source != target
    ]
    by_direction = {
        (row["sourceSelector"], row["targetSelector"]): row
        for row in pair_rows
    }
    source_to_predecessor = by_direction[(SOURCE_SELECTOR, PREDECESSOR_SELECTOR)]
    source_to_current = by_direction[(SOURCE_SELECTOR, CURRENT_SELECTOR)]
    predecessor_to_current = by_direction[(PREDECESSOR_SELECTOR, CURRENT_SELECTOR)]
    current_to_predecessor = by_direction[(CURRENT_SELECTOR, PREDECESSOR_SELECTOR)]
    forward_merge_bridge_count = (
        source_to_predecessor["hitCount"]
        + source_to_current["hitCount"]
        + predecessor_to_current["hitCount"]
    )
    forward_encoded_anchor_raw_count = sum(
        (row.get("encodedAnchorScan") or {}).get("rawScalarCandidateCount", 0)
        for row in (source_to_predecessor, source_to_current, predecessor_to_current)
    )
    forward_encoded_anchor_promoting_count = sum(
        (row.get("encodedAnchorScan") or {}).get("promotingCandidateCount", 0)
        for row in (source_to_predecessor, source_to_current, predecessor_to_current)
    )
    encoded_anchor_promoting_count = sum(
        (row.get("encodedAnchorScan") or {}).get("promotingCandidateCount", 0)
        for row in pair_rows
    )
    current_predecessor_hits_before_fill_only = (
        current_to_predecessor["hitCount"] > 0
        and current_to_predecessor["hitCountBeforePredecessorFill"] == current_to_predecessor["hitCount"]
        and current_to_predecessor["hitCountToPredecessorFillSites"] == 0
    )
    direct_merge_execution_bridge_found = forward_merge_bridge_count > 0
    encoded_merge_execution_bridge_found = forward_encoded_anchor_promoting_count > 0
    conclusion = (
        "The pairwise direct-dword bridge matrix finds no source-side 0:0 -> target-side 1:0 bridge, "
        "no source-side 0:0 -> current 2:0 bridge, and no target-side 1:0 -> current 2:0 bridge. "
        "The only route-relevant nonzero bridge remains the current 2:0 -> predecessor 1:0 reverse reuse, "
        "and every such target lands before the predecessor secondaryBranchState fill sites. The encoded exact-anchor "
        "scan adds no modeled control-flow candidate in the forward merge directions; any encoded anchor matches are "
        "raw scalar collisions unless a runtime VM path proves otherwise. This keeps the selector-merge hypothesis "
        "blocked: static data pointers and encoded scalar anchors do not prove that the confirmed source route executes "
        "through 1:0 and then into 2:0."
    )
    return {
        "source": SOURCE,
        "target": TARGET,
        "sourceSelector": SOURCE_SELECTOR,
        "predecessorSelector": PREDECESSOR_SELECTOR,
        "currentSelector": CURRENT_SELECTOR,
        "contexts": [context_summary(selectors, label) for label in labels],
        "pairs": pair_rows,
        "sourceToPredecessorHitCount": source_to_predecessor["hitCount"],
        "sourceToCurrentHitCount": source_to_current["hitCount"],
        "predecessorToCurrentHitCount": predecessor_to_current["hitCount"],
        "currentToPredecessorHitCount": current_to_predecessor["hitCount"],
        "currentToPredecessorBeforeFillHitCount": current_to_predecessor["hitCountBeforePredecessorFill"],
        "currentToPredecessorFillSiteHitCount": current_to_predecessor["hitCountToPredecessorFillSites"],
        "currentPredecessorHitsBeforeFillOnly": current_predecessor_hits_before_fill_only,
        "forwardMergeBridgeHitCount": forward_merge_bridge_count,
        "directMergeExecutionBridgeFound": direct_merge_execution_bridge_found,
        "forwardEncodedAnchorRawScalarCandidateCount": forward_encoded_anchor_raw_count,
        "forwardEncodedAnchorPromotingCandidateCount": forward_encoded_anchor_promoting_count,
        "encodedAnchorPromotingCandidateCount": encoded_anchor_promoting_count,
        "encodedMergeExecutionBridgeFound": encoded_merge_execution_bridge_found,
        "selectorMergeProofStatus": "blocked",
        "proofFound": False,
        "mergeBridgeMatrixProofFound": False,
        "failedMergeBridgeGateIds": FAILED_MERGE_BRIDGE_GATE_IDS,
        "missingEvidence": MERGE_BRIDGE_MISSING_EVIDENCE,
        "evidenceRefs": MERGE_BRIDGE_EVIDENCE_REFS,
        "evidenceRefCount": len(MERGE_BRIDGE_EVIDENCE_REFS),
        "promotionStatus": "blocked",
        "limitations": [
            "This matrix covers direct dword values and exact-anchor encoded scalar shapes in the known selector/root context windows.",
            "It does not rule out VM-dispatched execution order, save-state progression, or indirect runtime state changes.",
            "It does not replace the strict source hotspot or runtime selected-pointer proof requirement.",
        ],
        "remainingProofs": MERGE_BRIDGE_MISSING_EVIDENCE,
        "conclusion": conclusion,
    }


def pair_brief(row: dict) -> str:
    return (
        f"{row['sourceSelector']}->{row['targetSelector']} hits={row['hitCount']} "
        f"beforeFill={row['hitCountBeforePredecessorFill']} "
        f"fillSites={row['hitCountToPredecessorFillSites']}"
    )


def markdown(summary: dict) -> str:
    lines = [
        "# Save Selector Merge Bridge Matrix",
        "",
        f"- route: {summary['source']} -> {summary['target']}",
        f"- selectors: source `{summary['sourceSelector']}`, predecessor `{summary['predecessorSelector']}`, current `{summary['currentSelector']}`",
        f"- source -> predecessor hits: {summary['sourceToPredecessorHitCount']}",
        f"- source -> current hits: {summary['sourceToCurrentHitCount']}",
        f"- predecessor -> current hits: {summary['predecessorToCurrentHitCount']}",
        f"- current -> predecessor hits: {summary['currentToPredecessorHitCount']} (before fill {summary['currentToPredecessorBeforeFillHitCount']}, fill sites {summary['currentToPredecessorFillSiteHitCount']})",
        f"- predecessor fill sites: `{hex32(0x004844D0)}`, `{hex32(0x004844D8)}`",
        f"- forward merge bridge hits: {summary['forwardMergeBridgeHitCount']}",
        f"- direct merge execution bridge found: {summary['directMergeExecutionBridgeFound']}",
        f"- forward encoded-anchor raw scalars: {summary['forwardEncodedAnchorRawScalarCandidateCount']}",
        f"- forward encoded-anchor promoting candidates: {summary['forwardEncodedAnchorPromotingCandidateCount']}",
        f"- encoded merge execution bridge found: {summary['encodedMergeExecutionBridgeFound']}",
        f"- selector merge proof status: `{summary['selectorMergeProofStatus']}`",
        f"- proof found: {summary['proofFound']}",
        f"- failed merge-bridge gates: {', '.join(summary['failedMergeBridgeGateIds'])}",
        f"- missing evidence count: {len(summary['missingEvidence'])}",
        f"- evidence refs: {summary.get('evidenceRefCount')}",
        f"- promotion status: `{summary['promotionStatus']}`",
        "",
        summary["conclusion"],
        "",
        "## Contexts",
        "",
        "| selector | role | scan range | field maps |",
        "| --- | --- | --- | --- |",
    ]
    for row in summary["contexts"]:
        lines.append(
            f"| `{row['selector']}` | {row['role']} | `{row['scanRangeHex']}` | "
            f"{', '.join(row['fieldMaps']) or '-'} |"
        )
    lines.extend([
        "",
        "## Pair Matrix",
        "",
        "| direction | source range | target range | dword hits | before fill | fill-site hits | encoded raw | encoded promoting |",
        "| --- | --- | --- | ---: | ---: | ---: | ---: | ---: |",
    ])
    for row in summary["pairs"]:
        encoded = row.get("encodedAnchorScan") or {}
        lines.append(
            f"| `{row['sourceSelector']} -> {row['targetSelector']}` | `{row['sourceRangeHex']}` | "
            f"`{row['targetRangeHex']}` | {row['hitCount']} | "
            f"{row['hitCountBeforePredecessorFill']} | {row['hitCountToPredecessorFillSites']} | "
            f"{encoded.get('rawScalarCandidateCount')} | {encoded.get('promotingCandidateCount')} |"
        )
    lines.extend([
        "",
        "## Encoded Anchor Scan",
        "",
        "| direction | classification | raw scalars | branch-attached | modeled control-flow | promoting |",
        "| --- | --- | ---: | ---: | ---: | ---: |",
    ])
    for row in summary["pairs"]:
        encoded = row.get("encodedAnchorScan") or {}
        lines.append(
            f"| `{row['sourceSelector']} -> {row['targetSelector']}` | "
            f"{encoded.get('classification')} | {encoded.get('rawScalarCandidateCount')} | "
            f"{encoded.get('branchAttachedEncodedFieldCount')} | "
            f"{encoded.get('modeledControlFlowCandidateCount')} | "
            f"{encoded.get('promotingCandidateCount')} |"
        )
    lines.extend(["", "## Route-Relevant Hits", ""])
    for row in summary["pairs"]:
        if row["hitCount"] == 0:
            continue
        lines.extend([
            f"### {row['sourceSelector']} -> {row['targetSelector']}",
            "",
            "| source VA | value | exact target | before predecessor fill | fill site |",
            "| --- | --- | --- | --- | --- |",
        ])
        for hit in row["hits"][:80]:
            lines.append(
                f"| `{hit['sourceVaHex']}` | `{hit['valueHex']}` | "
                f"{hit.get('targetExactLabel') or '-'} | {hit['beforePredecessorFill']} | "
                f"{hit['targetsPredecessorFillSite']} |"
            )
        if len(row["hits"]) > 80:
            lines.append(f"| ... | ... | {len(row['hits']) - 80} more hits omitted | ... | ... |")
        lines.append("")
    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:
    context_rows = "\n".join(
        "<tr>"
        f"<td><code>{html.escape(row['selector'])}</code></td>"
        f"<td>{html.escape(row['role'])}</td>"
        f"<td><code>{html.escape(row['scanRangeHex'])}</code></td>"
        f"<td>{html.escape(', '.join(row['fieldMaps']) or '-')}</td>"
        "</tr>"
        for row in summary["contexts"]
    )
    def pair_row(row: dict) -> str:
        encoded = row.get("encodedAnchorScan") or {}
        return (
            "<tr>"
            f"<td><code>{html.escape(row['sourceSelector'])} -&gt; {html.escape(row['targetSelector'])}</code></td>"
            f"<td><code>{html.escape(row['sourceRangeHex'])}</code></td>"
            f"<td><code>{html.escape(row['targetRangeHex'])}</code></td>"
            f"<td>{row['hitCount']}</td>"
            f"<td>{row['hitCountBeforePredecessorFill']}</td>"
            f"<td>{row['hitCountToPredecessorFillSites']}</td>"
            f"<td>{encoded.get('rawScalarCandidateCount')}</td>"
            f"<td>{encoded.get('promotingCandidateCount')}</td>"
            "</tr>"
        )

    pair_rows = "\n".join(
        pair_row(row)
        for row in summary["pairs"]
    )
    encoded_rows = "\n".join(
        "<tr>"
        f"<td><code>{html.escape(row['sourceSelector'])} -&gt; {html.escape(row['targetSelector'])}</code></td>"
        f"<td>{html.escape(str((row.get('encodedAnchorScan') or {}).get('classification')))}</td>"
        f"<td>{(row.get('encodedAnchorScan') or {}).get('rawScalarCandidateCount')}</td>"
        f"<td>{(row.get('encodedAnchorScan') or {}).get('branchAttachedEncodedFieldCount')}</td>"
        f"<td>{(row.get('encodedAnchorScan') or {}).get('modeledControlFlowCandidateCount')}</td>"
        f"<td>{(row.get('encodedAnchorScan') or {}).get('promotingCandidateCount')}</td>"
        "</tr>"
        for row in summary["pairs"]
    )
    hit_sections = []
    for row in summary["pairs"]:
        if row["hitCount"] == 0:
            continue
        hit_rows = "\n".join(
            "<tr>"
            f"<td><code>{html.escape(hit['sourceVaHex'])}</code></td>"
            f"<td><code>{html.escape(hit['valueHex'])}</code></td>"
            f"<td>{html.escape(hit.get('targetExactLabel') or '-')}</td>"
            f"<td>{hit['beforePredecessorFill']}</td>"
            f"<td>{hit['targetsPredecessorFillSite']}</td>"
            "</tr>"
            for hit in row["hits"][:80]
        )
        hit_sections.append(
            f"<h2>{html.escape(row['sourceSelector'])} -&gt; {html.escape(row['targetSelector'])}</h2>"
            "<table><thead><tr><th>source VA</th><th>value</th><th>exact target</th>"
            "<th>before predecessor fill</th><th>fill site</th></tr></thead><tbody>"
            f"{hit_rows}</tbody></table>"
        )
    proofs = "".join(f"<li>{html.escape(item)}</li>" for item in summary["remainingProofs"])
    limitations = "".join(f"<li>{html.escape(item)}</li>" for item in summary["limitations"])
    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 Merge Bridge Matrix</title>",
        "  <style>",
        "    :root { color-scheme: dark; font-family: system-ui, sans-serif; background: #101010; color: #eee; }",
        "    body { margin: 0; padding: 24px; }",
        "    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 { background: #181818; color: #ddd; }",
        "    code { color: #d7f0ff; }",
        "  </style>",
        "</head>",
        "<body>",
        "  <h1>Save Selector Merge Bridge Matrix</h1>",
        f"  <p>route {html.escape(summary['source'])} -&gt; {html.escape(summary['target'])}; forward merge bridge hits {summary['forwardMergeBridgeHitCount']}; direct merge execution bridge found {summary['directMergeExecutionBridgeFound']}; encoded merge execution bridge found {summary['encodedMergeExecutionBridgeFound']}; promotion status <code>{html.escape(summary['promotionStatus'])}</code>.</p>",
        f"  <p>source -> predecessor hits {summary['sourceToPredecessorHitCount']}; source -> current hits {summary['sourceToCurrentHitCount']}; predecessor -> current hits {summary['predecessorToCurrentHitCount']}; current -> predecessor hits {summary['currentToPredecessorHitCount']}.</p>",
        f"  <p>proof found <code>{summary['proofFound']}</code>; failed merge-bridge gates <code>{html.escape(','.join(summary['failedMergeBridgeGateIds']))}</code>; missing evidence <code>{len(summary['missingEvidence'])}</code>; evidence refs <code>{summary.get('evidenceRefCount')}</code>.</p>",
        "  <p>predecessor fill sites <code>0x004844d0</code>, <code>0x004844d8</code>; current reverse fill-site hits "
        f"{summary['currentToPredecessorFillSiteHitCount']}.</p>",
        f"  <p>{html.escape(summary['conclusion'])}</p>",
        "  <h2>Contexts</h2>",
        "  <table><thead><tr><th>selector</th><th>role</th><th>scan range</th><th>field maps</th></tr></thead><tbody>",
        context_rows,
        "  </tbody></table>",
        "  <h2>Pair Matrix</h2>",
        "  <table><thead><tr><th>direction</th><th>source range</th><th>target range</th><th>dword hits</th><th>before fill</th><th>fill-site hits</th><th>encoded raw</th><th>encoded promoting</th></tr></thead><tbody>",
        pair_rows,
        "  </tbody></table>",
        "  <h2>Encoded Anchor Scan</h2>",
        "  <table><thead><tr><th>direction</th><th>classification</th><th>raw scalars</th><th>branch-attached</th><th>modeled control-flow</th><th>promoting</th></tr></thead><tbody>",
        encoded_rows,
        "  </tbody></table>",
        *hit_sections,
        f"  <h2>Remaining Proofs</h2><ul>{proofs}</ul>",
        f"  <h2>Limitations</h2><ul>{limitations}</ul>",
        "</body></html>",
    ])


def write_outputs(summary: dict, out_dir: Path, html_out: Path | None = None) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "save_selector_merge_bridge_matrix.json").write_text(
        json.dumps(summary, ensure_ascii=False, separators=(",", ":")),
        encoding="utf-8",
    )
    if html_out is not None:
        html_out.parent.mkdir(parents=True, exist_ok=True)
        html_out.write_text(html_page(summary), encoding="utf-8")


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--exe", type=Path, default=ROOT / "Hwanse2.exe")
    parser.add_argument("--selectors", type=Path, default=OUT / "save_scene_selectors.json")
    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.selectors, []),
    )
    write_outputs(summary, args.out_dir, args.html_out)
    print(f"wrote save selector merge bridge matrix -> {args.out_dir / 'save_selector_merge_bridge_matrix.json'}")


if __name__ == "__main__":
    main()
