#!/usr/bin/env python3
"""Check whether predecessor descriptor boundaries bridge to fill/current route code."""
from __future__ import annotations

import argparse
import html
import json
import struct
import sys
from collections import deque
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
from summarize_script_handler_table import section_name_for_va


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

ROOT_STOP_SITE = 0x004783E0
ROOT_STOP_DESCRIPTOR = 0x00440A9C
FILL_STOP_SITE = 0x004844DC
FILL_STOP_DESCRIPTOR = 0x00440C5C
E8_DESCRIPTOR = 0x00440C28
CURRENT_ROOT = 0x00540714
WRAPPER_LEAF = 0x00542A04
FRONTIER_LEAF = 0x00542AE8
CURRENT_READER = 0x00542B0C

DEFAULT_FILL_SITES = [0x004844D0, 0x004844D8]
SCAN_SECTIONS = {".text", ".data", ".rdata"}
DESCRIPTOR_BRIDGE_MISSING_EVIDENCE_BY_GATE = {
    "root-stop-to-fill-bridge": (
        "root-stop descriptor edge or node reaching the predecessor fill fragment"
    ),
    "fill-stop-to-current-bridge": (
        "fill-stop descriptor edge or node reaching the current root, wrapper, frontier, or reader"
    ),
    "route-execution-target-edge": (
        "descriptor closure target edge classified as route execution rather than descriptor-only data"
    ),
    "descriptor-encoded-route-target-control-flow": (
        "encoded descriptor scalar attached to modeled route-execution control flow"
    ),
    "runtime-or-decoded-nonlinear-vm-path": (
        "runtime trace or decoded non-linear VM path crossing the descriptor boundary"
    ),
}
DESCRIPTOR_BRIDGE_EVIDENCE_REFS = [
    {
        "path": "Hwanse2.exe",
        "fields": [
            "0x00440a9c root-stop descriptor closure",
            "0x00440c5c fill-stop descriptor closure",
            "0x004844d0/0x004844d8 predecessor fill sites",
            "0x00542b0c current reader",
        ],
    },
    {
        "path": "out/save_selector_data_descriptor_opcode_map.json",
        "fields": [
            "predecessorRootStopIsDataDescriptor",
            "predecessorFillStopIsDataDescriptor",
            "d0DescriptorSharedHandlerOpcodes",
            "c0DescriptorSharedHandlerOpcodes",
        ],
    },
    {
        "path": "out/save_selector_predecessor_fill_execution_order_gap.json",
        "fields": [
            "fillSites",
            "localFillTraceStopHex",
            "currentRootHex",
            "currentReaderHex",
            "rootEntryFixedTraversalFillSitesReachable",
        ],
    },
]


def hex32(value: int | None) -> str | None:
    return f"0x{value:08x}" if value is not None else None


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 parse_hex(value: str | None) -> int | None:
    if not value:
        return None
    try:
        return int(value, 16)
    except ValueError:
        return None


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


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


def section_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 value_refs(exe: bytes, sections: list[dict], value: int) -> list[dict]:
    needle = struct.pack("<I", value)
    refs = []
    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"] not in SCAN_SECTIONS:
            continue
        ref_va = offset_to_va(sections, hit)
        if ref_va is None:
            continue
        refs.append({
            "refVaHex": hex32(ref_va),
            "section": section["name"],
            "fileOffsetHex": f"0x{hit:06x}",
        })
    return refs


def site_from_descriptor_map(role: str, descriptor_map: dict) -> dict:
    for row in descriptor_map.get("sites") or []:
        if row.get("role") == role:
            return row
    return {}


def descriptor_closure(
    exe: bytes,
    sections: list[dict],
    start_va: int,
    targets: dict[int, str],
    *,
    window_dwords: int = 16,
    max_nodes: int = 1200,
) -> dict:
    queue = deque([start_va])
    seen: list[int] = []
    seen_set: set[int] = set()
    edges = []
    node_rows = []
    target_node_hits: dict[str, int] = {label: 0 for label in targets.values()}
    target_edge_hits: dict[str, int] = {label: 0 for label in targets.values()}

    while queue and len(seen) < max_nodes:
        va = queue.popleft()
        if va in seen_set:
            continue
        seen_set.add(va)
        seen.append(va)
        if va in targets:
            target_node_hits[targets[va]] += 1

        pointer_values = []
        for index in range(window_dwords):
            row_va = va + index * 4
            value = dword_at(exe, sections, row_va)
            section = section_name_for_va(sections, value) if value is not None else None
            if section == ".data":
                edge = {
                    "sourceVaHex": hex32(row_va),
                    "sourceNodeHex": hex32(va),
                    "valueHex": hex32(value),
                    "targetLabel": targets.get(value),
                    "targetSection": section,
                }
                edges.append(edge)
                pointer_values.append(edge)
                if value in targets:
                    target_edge_hits[targets[value]] += 1
                if value not in seen_set:
                    queue.append(value)
        node_rows.append({
            "nodeVaHex": hex32(va),
            "isTargetNode": va in targets,
            "targetLabel": targets.get(va),
            "pointerCountInWindow": len(pointer_values),
            "pointerValues": pointer_values[:12],
        })

    return {
        "startHex": hex32(start_va),
        "windowDwords": window_dwords,
        "maxNodes": max_nodes,
        "visitedNodeCount": len(seen),
        "truncated": bool(queue),
        "edgeCount": len(edges),
        "targetNodeHits": target_node_hits,
        "targetEdgeHits": target_edge_hits,
        "targetEdgeRows": [edge for edge in edges if edge.get("targetLabel")],
        "firstNodes": [hex32(value) for value in seen[:32]],
        "visitedNodesHex": [hex32(value) for value in seen],
        "nodeRows": node_rows[:32],
    }


def target_ref_rows(exe: bytes, sections: list[dict], targets: dict[int, str]) -> list[dict]:
    rows = []
    for value, label in targets.items():
        refs = value_refs(exe, sections, value)
        rows.append({
            "label": label,
            "targetHex": hex32(value),
            "refCount": len(refs),
            "refSections": sorted({ref["section"] for ref in refs}),
            "refs": refs[:24],
        })
    return rows


def nonzero_hits(hits: dict[str, int], labels: list[str]) -> int:
    return sum(hits.get(label, 0) for label in labels)


def compact_target_edge_rows(closure_name: str, closure: dict) -> list[dict]:
    rows = []
    for row in closure.get("targetEdgeRows") or []:
        source_va = parse_hex(row.get("sourceVaHex"))
        source_node = parse_hex(row.get("sourceNodeHex"))
        rows.append({
            "closure": closure_name,
            "sourceNodeHex": row.get("sourceNodeHex"),
            "sourceVaHex": row.get("sourceVaHex"),
            "sourceOffsetFromNodeHex": hex32(source_va - source_node)
            if source_va is not None and source_node is not None
            else None,
            "targetHex": row.get("valueHex"),
            "targetLabel": row.get("targetLabel"),
            "targetSection": row.get("targetSection"),
        })
    return rows


def descriptor_target_edge_summary(
    root_closure: dict,
    fill_closure: dict,
    route_execution_labels: list[str],
) -> dict:
    root_rows = compact_target_edge_rows("root-stop-descriptor", root_closure)
    fill_rows = compact_target_edge_rows("fill-stop-descriptor", fill_closure)
    descriptor_labels = {"d0-root-stop-descriptor", "c0-fill-stop-descriptor", "e8-route-descriptor"}
    root_descriptor_rows = [row for row in root_rows if row.get("targetLabel") in descriptor_labels]
    fill_descriptor_rows = [row for row in fill_rows if row.get("targetLabel") in descriptor_labels]
    root_route_rows = [row for row in root_rows if row.get("targetLabel") in route_execution_labels]
    fill_route_rows = [row for row in fill_rows if row.get("targetLabel") in route_execution_labels]
    return {
        "rootDescriptorTargetEdgeCount": len(root_descriptor_rows),
        "fillDescriptorTargetEdgeCount": len(fill_descriptor_rows),
        "rootRouteExecutionTargetEdgeCount": len(root_route_rows),
        "fillRouteExecutionTargetEdgeCount": len(fill_route_rows),
        "rootDescriptorOnlyTargetLabels": sorted({row.get("targetLabel") for row in root_descriptor_rows}),
        "fillDescriptorOnlyTargetLabels": sorted({row.get("targetLabel") for row in fill_descriptor_rows}),
        "sharedDescriptorOnly": bool(root_descriptor_rows or fill_descriptor_rows)
        and not root_route_rows
        and not fill_route_rows,
        "rootTargetEdgeRows": root_rows,
        "fillTargetEdgeRows": fill_rows,
    }


def descriptor_edge_rejection_summary(edge_summary: dict) -> dict:
    root_rows = edge_summary.get("rootTargetEdgeRows") or []
    fill_rows = edge_summary.get("fillTargetEdgeRows") or []
    all_rows = root_rows + fill_rows
    root_descriptor_count = edge_summary.get("rootDescriptorTargetEdgeCount") or 0
    fill_descriptor_count = edge_summary.get("fillDescriptorTargetEdgeCount") or 0
    root_route_count = edge_summary.get("rootRouteExecutionTargetEdgeCount") or 0
    fill_route_count = edge_summary.get("fillRouteExecutionTargetEdgeCount") or 0
    descriptor_edge_count = root_descriptor_count + fill_descriptor_count
    route_execution_edge_count = root_route_count + fill_route_count
    all_target_sections_data = bool(all_rows) and all(
        row.get("targetSection") == ".data" for row in all_rows
    )
    if (
        descriptor_edge_count
        and route_execution_edge_count == 0
        and edge_summary.get("sharedDescriptorOnly") is True
        and all_target_sections_data
    ):
        classification = "descriptor-only-no-route-execution-edge"
    elif route_execution_edge_count:
        classification = "route-execution-edge-candidate"
    elif descriptor_edge_count == 0:
        classification = "no-descriptor-target-edge"
    else:
        classification = "review-required"
    return {
        "classification": classification,
        "descriptorTargetEdgeCount": descriptor_edge_count,
        "routeExecutionTargetEdgeCount": route_execution_edge_count,
        "rootDescriptorTargetEdgeCount": root_descriptor_count,
        "fillDescriptorTargetEdgeCount": fill_descriptor_count,
        "rootRouteExecutionTargetEdgeCount": root_route_count,
        "fillRouteExecutionTargetEdgeCount": fill_route_count,
        "sharedDescriptorOnly": edge_summary.get("sharedDescriptorOnly") is True,
        "allTargetSectionsData": all_target_sections_data,
        "rootDescriptorOnlyTargetLabels": edge_summary.get("rootDescriptorOnlyTargetLabels") or [],
        "fillDescriptorOnlyTargetLabels": edge_summary.get("fillDescriptorOnlyTargetLabels") or [],
        "promotionStatus": "blocked"
        if classification == "descriptor-only-no-route-execution-edge"
        else "review-required",
    }


def descriptor_encoded_target_scan(
    exe: bytes,
    sections: list[dict],
    root_closure: dict,
    fill_closure: dict,
    targets: dict[int, str],
    route_execution_labels: list[str],
) -> dict:
    route_label_set = set(route_execution_labels)
    target_items = [
        (target, label)
        for target, label in targets.items()
        if label in route_label_set
    ]
    rows = []
    seen: set[tuple[str, str, int, int]] = set()

    for closure_name, closure in [
        ("root-stop-descriptor", root_closure),
        ("fill-stop-descriptor", fill_closure),
    ]:
        window_dwords = closure.get("windowDwords") or 0
        for node_hex in closure.get("visitedNodesHex") or []:
            node_va = parse_hex(node_hex)
            if node_va is None:
                continue
            for index in range(window_dwords):
                slot_va = node_va + index * 4
                source_section = section_name_for_va(sections, slot_va)
                if source_section != ".data":
                    continue
                values = [
                    ("abs16-low", u16_at(exe, sections, slot_va), None),
                    ("signed-rel16-site-plus2", s16_at(exe, sections, slot_va), slot_va + 2),
                    ("signed-rel32-site-plus4", s32_at(exe, sections, slot_va), slot_va + 4),
                ]
                for kind, value, rel_base in values:
                    if value is None:
                        continue
                    for target_va, target_label in target_items:
                        if kind == "abs16-low":
                            matches = value == (target_va & 0xFFFF)
                        else:
                            matches = rel_base is not None and rel_base + value == target_va
                        if not matches:
                            continue
                        key = (closure_name, kind, slot_va, target_va)
                        if key in seen:
                            continue
                        seen.add(key)
                        rows.append({
                            "closure": closure_name,
                            "kind": kind,
                            "sourceNodeHex": node_hex,
                            "slotVaHex": hex32(slot_va),
                            "slotOffsetFromNodeHex": hex32(slot_va - node_va),
                            "valueHex": hex32(value & 0xFFFFFFFF),
                            "targetHex": hex32(target_va),
                            "targetLabel": target_label,
                            "sourceSection": source_section,
                            "promotionStatus": "blocked",
                            "promotesRouteExecution": False,
                        })

    root_rows = [row for row in rows if row["closure"] == "root-stop-descriptor"]
    fill_rows = [row for row in rows if row["closure"] == "fill-stop-descriptor"]
    target_counts: dict[str, int] = {}
    kind_counts: dict[str, int] = {}
    for row in rows:
        target_counts[row["targetLabel"]] = target_counts.get(row["targetLabel"], 0) + 1
        kind_counts[row["kind"]] = kind_counts.get(row["kind"], 0) + 1
    classification = (
        "descriptor-encoded-route-target-scalars-nonpromoting"
        if rows
        else "no-encoded-route-target-in-descriptor-closure"
    )
    return {
        "classification": classification,
        "rawScalarCandidateCount": len(rows),
        "rootRawScalarCandidateCount": len(root_rows),
        "fillRawScalarCandidateCount": len(fill_rows),
        "promotingCandidateCount": 0,
        "targetLabelCounts": dict(sorted(target_counts.items())),
        "kindCounts": dict(sorted(kind_counts.items())),
        "sampleRows": rows[:24],
        "promotionStatus": "blocked",
    }


def build_summary(
    exe: bytes,
    descriptor_map: dict | None = None,
    predecessor_fill_execution_order_gap: dict | None = None,
) -> dict:
    descriptor_map = descriptor_map or {}
    predecessor_fill_execution_order_gap = predecessor_fill_execution_order_gap or {}
    sections = read_sections(exe)
    fill_sites = [
        parse_hex(value)
        for value in predecessor_fill_execution_order_gap.get("fillSites", [])
    ] or DEFAULT_FILL_SITES
    fill_sites = [value for value in fill_sites if value is not None]
    targets = {
        ROOT_STOP_DESCRIPTOR: "d0-root-stop-descriptor",
        FILL_STOP_DESCRIPTOR: "c0-fill-stop-descriptor",
        E8_DESCRIPTOR: "e8-route-descriptor",
        ROOT_STOP_SITE: "predecessor-root-stop-site",
        FILL_STOP_SITE: "predecessor-fill-stop-site",
        CURRENT_ROOT: "current-root",
        WRAPPER_LEAF: "wrapper-leaf",
        FRONTIER_LEAF: "frontier-leaf",
        CURRENT_READER: "current-reader",
    }
    for index, value in enumerate(fill_sites):
        targets[value] = f"predecessor-fill-site-{index}"

    root_stop_site = site_from_descriptor_map("predecessor-root-entry-stop", descriptor_map)
    fill_stop_site = site_from_descriptor_map("predecessor-fill-fragment-stop", descriptor_map)
    root_closure = descriptor_closure(exe, sections, ROOT_STOP_DESCRIPTOR, targets)
    fill_closure = descriptor_closure(exe, sections, FILL_STOP_DESCRIPTOR, targets)
    target_refs = target_ref_rows(exe, sections, targets)
    ref_counts = {row["label"]: row["refCount"] for row in target_refs}

    root_to_fill_labels = [label for label in targets.values() if label.startswith("predecessor-fill-site")]
    root_to_fill_labels.append("predecessor-fill-stop-site")
    fill_to_current_labels = ["current-root", "wrapper-leaf", "frontier-leaf", "current-reader"]
    route_execution_labels = sorted(set(root_to_fill_labels + fill_to_current_labels))
    edge_summary = descriptor_target_edge_summary(root_closure, fill_closure, route_execution_labels)
    edge_rejection = descriptor_edge_rejection_summary(edge_summary)
    encoded_target_scan = descriptor_encoded_target_scan(
        exe,
        sections,
        root_closure,
        fill_closure,
        targets,
        route_execution_labels,
    )

    root_stop_to_fill_bridge_found = (
        nonzero_hits(root_closure["targetNodeHits"], root_to_fill_labels)
        + nonzero_hits(root_closure["targetEdgeHits"], root_to_fill_labels)
    ) > 0
    fill_stop_to_current_bridge_found = (
        nonzero_hits(fill_closure["targetNodeHits"], fill_to_current_labels)
        + nonzero_hits(fill_closure["targetEdgeHits"], fill_to_current_labels)
    ) > 0
    c0_self_loop_found = fill_closure["targetEdgeHits"].get("c0-fill-stop-descriptor", 0) > 0
    descriptor_bridge_proof_found = root_stop_to_fill_bridge_found and fill_stop_to_current_bridge_found
    failed_descriptor_bridge_gate_ids = []
    if not root_stop_to_fill_bridge_found:
        failed_descriptor_bridge_gate_ids.append("root-stop-to-fill-bridge")
    if not fill_stop_to_current_bridge_found:
        failed_descriptor_bridge_gate_ids.append("fill-stop-to-current-bridge")
    if edge_rejection["routeExecutionTargetEdgeCount"] == 0:
        failed_descriptor_bridge_gate_ids.append("route-execution-target-edge")
    if encoded_target_scan["promotingCandidateCount"] == 0:
        failed_descriptor_bridge_gate_ids.append("descriptor-encoded-route-target-control-flow")
    if not descriptor_bridge_proof_found:
        failed_descriptor_bridge_gate_ids.append("runtime-or-decoded-nonlinear-vm-path")
    missing_evidence = [
        DESCRIPTOR_BRIDGE_MISSING_EVIDENCE_BY_GATE[gate_id]
        for gate_id in failed_descriptor_bridge_gate_ids
    ]
    evidence = [
        {
            "kind": "root-stop-descriptor-closure",
            "status": "no-fill-site-bridge" if not root_stop_to_fill_bridge_found else "bridge-candidate",
            "detail": (
                f"d0 closure nodes={root_closure['visitedNodeCount']} edges={root_closure['edgeCount']} "
                f"targetEdges={root_closure['targetEdgeHits']}"
            ),
        },
        {
            "kind": "fill-stop-descriptor-closure",
            "status": "self-loop-no-current-bridge"
            if c0_self_loop_found and not fill_stop_to_current_bridge_found
            else "bridge-candidate",
            "detail": (
                f"c0 closure nodes={fill_closure['visitedNodeCount']} edges={fill_closure['edgeCount']} "
                f"targetEdges={fill_closure['targetEdgeHits']}"
            ),
        },
        {
            "kind": "direct-target-ref-scan",
            "status": "route-target-refs-absent-or-table-only",
            "detail": f"refCounts={ref_counts}",
        },
        {
            "kind": "descriptor-target-edge-classification",
            "status": "shared-descriptor-only" if edge_summary["sharedDescriptorOnly"] else "route-edge-candidate",
            "detail": (
                f"rootDescriptorEdges={edge_summary['rootDescriptorTargetEdgeCount']}; "
                f"fillDescriptorEdges={edge_summary['fillDescriptorTargetEdgeCount']}; "
                f"rootRouteEdges={edge_summary['rootRouteExecutionTargetEdgeCount']}; "
                f"fillRouteEdges={edge_summary['fillRouteExecutionTargetEdgeCount']}; "
                f"rootLabels={edge_summary['rootDescriptorOnlyTargetLabels']}; "
                f"fillLabels={edge_summary['fillDescriptorOnlyTargetLabels']}"
            ),
        },
        {
            "kind": "descriptor-edge-rejection",
            "status": edge_rejection["classification"],
            "detail": (
                "descriptorEdges="
                f"{edge_rejection['rootDescriptorTargetEdgeCount']}/"
                f"{edge_rejection['fillDescriptorTargetEdgeCount']}; "
                "routeEdges="
                f"{edge_rejection['rootRouteExecutionTargetEdgeCount']}/"
                f"{edge_rejection['fillRouteExecutionTargetEdgeCount']}; "
                f"allTargetSectionsData={edge_rejection['allTargetSectionsData']}; "
                f"rootLabels={edge_rejection['rootDescriptorOnlyTargetLabels']}; "
                f"fillLabels={edge_rejection['fillDescriptorOnlyTargetLabels']}; "
                f"status={edge_rejection['promotionStatus']}"
            ),
        },
        {
            "kind": "descriptor-encoded-target-scan",
            "status": encoded_target_scan["classification"],
            "detail": (
                f"raw={encoded_target_scan['rawScalarCandidateCount']}; "
                f"root/fill={encoded_target_scan['rootRawScalarCandidateCount']}/"
                f"{encoded_target_scan['fillRawScalarCandidateCount']}; "
                f"promoting={encoded_target_scan['promotingCandidateCount']}; "
                f"targets={encoded_target_scan['targetLabelCounts']}; "
                f"kinds={encoded_target_scan['kindCounts']}"
            ),
        },
    ]
    conclusion = (
        "The predecessor root stop descriptor 0x00440a9c has a complete bounded .data pointer closure that reaches shared "
        "descriptor data, including the c0 fill-stop descriptor, but it does not contain an edge or node for the "
        "predecessor fill sites or the 0x004844dc fill-fragment stop. The fill-stop descriptor 0x00440c5c forms a "
        "small self-referential descriptor closure and has no edge to the current root, wrapper leaf, frontier leaf, "
        "or reader. The encoded target scan over descriptor closure slots adds no modeled route-execution edge; any "
        "encoded scalar matches remain descriptor data, not a control-flow bridge. This closes the obvious descriptor "
        "pointer/scalar bridge and leaves the predecessor fill/current-reader order blocked behind a real execution "
        "trace or decoded non-linear VM path. The target-edge rows are classified as descriptor-only data edges, not "
        "route-execution edges."
    )
    return {
        "source": ROUTE_SOURCE,
        "target": ROUTE_TARGET,
        "predecessorSelector": predecessor_fill_execution_order_gap.get("predecessorSelector") or "1:0",
        "currentSelector": predecessor_fill_execution_order_gap.get("currentSelector") or "2:0",
        "predecessorRootStopSiteHex": hex32(ROOT_STOP_SITE),
        "predecessorRootStopDescriptorHex": hex32(ROOT_STOP_DESCRIPTOR),
        "predecessorRootStopSite": root_stop_site,
        "predecessorFillStopSiteHex": hex32(FILL_STOP_SITE),
        "predecessorFillStopDescriptorHex": hex32(FILL_STOP_DESCRIPTOR),
        "predecessorFillStopSite": fill_stop_site,
        "fillSites": [hex32(value) for value in fill_sites],
        "currentRootHex": hex32(CURRENT_ROOT),
        "wrapperLeafHex": hex32(WRAPPER_LEAF),
        "frontierLeafHex": hex32(FRONTIER_LEAF),
        "currentReaderHex": hex32(CURRENT_READER),
        "targetRefs": target_refs,
        "targetRefCounts": ref_counts,
        "rootStopDescriptorClosure": root_closure,
        "fillStopDescriptorClosure": fill_closure,
        "descriptorTargetEdgeSummary": edge_summary,
        "descriptorEdgeRejection": edge_rejection,
        "descriptorEdgeRejectionClassification": edge_rejection["classification"],
        "descriptorEdgeAllTargetSectionsData": edge_rejection["allTargetSectionsData"],
        "descriptorEdgeDescriptorTargetEdgeCount": edge_rejection["descriptorTargetEdgeCount"],
        "descriptorEdgeRouteExecutionTargetEdgeCount": edge_rejection[
            "routeExecutionTargetEdgeCount"
        ],
        "descriptorEdgeRootDescriptorTargetEdgeCount": edge_rejection[
            "rootDescriptorTargetEdgeCount"
        ],
        "descriptorEdgeFillDescriptorTargetEdgeCount": edge_rejection[
            "fillDescriptorTargetEdgeCount"
        ],
        "descriptorEdgeRootRouteExecutionTargetEdgeCount": edge_rejection[
            "rootRouteExecutionTargetEdgeCount"
        ],
        "descriptorEdgeFillRouteExecutionTargetEdgeCount": edge_rejection[
            "fillRouteExecutionTargetEdgeCount"
        ],
        "descriptorEdgeRootDescriptorOnlyTargetLabels": edge_rejection[
            "rootDescriptorOnlyTargetLabels"
        ],
        "descriptorEdgeFillDescriptorOnlyTargetLabels": edge_rejection[
            "fillDescriptorOnlyTargetLabels"
        ],
        "descriptorEncodedTargetScan": encoded_target_scan,
        "descriptorEncodedTargetClassification": encoded_target_scan["classification"],
        "descriptorEncodedTargetRawScalarCandidateCount": encoded_target_scan[
            "rawScalarCandidateCount"
        ],
        "descriptorEncodedTargetRootRawScalarCandidateCount": encoded_target_scan[
            "rootRawScalarCandidateCount"
        ],
        "descriptorEncodedTargetFillRawScalarCandidateCount": encoded_target_scan[
            "fillRawScalarCandidateCount"
        ],
        "descriptorEncodedTargetPromotingCandidateCount": encoded_target_scan[
            "promotingCandidateCount"
        ],
        "descriptorEncodedTargetLabelCounts": encoded_target_scan["targetLabelCounts"],
        "descriptorEncodedTargetKindCounts": encoded_target_scan["kindCounts"],
        "rootStopClosureReachesFillStopDescriptor": (
            root_closure["targetNodeHits"].get("c0-fill-stop-descriptor", 0) > 0
            or root_closure["targetEdgeHits"].get("c0-fill-stop-descriptor", 0) > 0
        ),
        "rootStopToFillBridgeFound": root_stop_to_fill_bridge_found,
        "fillStopClosureSelfLoopFound": c0_self_loop_found,
        "fillStopToCurrentBridgeFound": fill_stop_to_current_bridge_found,
        "descriptorBridgeProofFound": descriptor_bridge_proof_found,
        "proofFound": descriptor_bridge_proof_found,
        "failedDescriptorBridgeGateIds": failed_descriptor_bridge_gate_ids,
        "missingEvidence": missing_evidence,
        "evidenceRefs": DESCRIPTOR_BRIDGE_EVIDENCE_REFS,
        "evidenceRefCount": len(DESCRIPTOR_BRIDGE_EVIDENCE_REFS),
        "promotionStatus": "blocked",
        "evidence": evidence,
        "remainingProofs": [
            "observe execution from 0x004783e0 into the 0x004844d0 fill fragment",
            "decode a non-linear VM path that legally crosses the 0x00440a9c/0x00440c5c descriptor data",
            "observe or decode execution from the fill fragment toward current reader 0x00542b0c",
        ],
        "conclusion": conclusion,
    }


def markdown(summary: dict) -> str:
    lines = [
        "# Save Selector Predecessor Descriptor Bridge Gap",
        "",
        f"- route: {summary['source']} -> {summary['target']}",
        f"- predecessor/current selectors: {summary['predecessorSelector']} -> {summary['currentSelector']}",
        f"- root stop: `{summary['predecessorRootStopSiteHex']}` -> `{summary['predecessorRootStopDescriptorHex']}`",
        f"- fill stop: `{summary['predecessorFillStopSiteHex']}` -> `{summary['predecessorFillStopDescriptorHex']}`",
        f"- fill sites: {', '.join(summary['fillSites'])}",
        f"- current reader: `{summary['currentReaderHex']}`",
        f"- root closure reaches c0 descriptor: {summary['rootStopClosureReachesFillStopDescriptor']}",
        f"- root stop to fill bridge found: {summary['rootStopToFillBridgeFound']}",
        f"- fill stop closure self loop found: {summary['fillStopClosureSelfLoopFound']}",
        f"- fill stop to current bridge found: {summary['fillStopToCurrentBridgeFound']}",
        (
            "- descriptor encoded target raw/root/fill/promoting: "
            f"{summary['descriptorEncodedTargetRawScalarCandidateCount']} / "
            f"{summary['descriptorEncodedTargetRootRawScalarCandidateCount']} / "
            f"{summary['descriptorEncodedTargetFillRawScalarCandidateCount']} / "
            f"{summary['descriptorEncodedTargetPromotingCandidateCount']}"
        ),
        f"- descriptor encoded target classification: `{summary['descriptorEncodedTargetClassification']}`",
        f"- descriptor bridge proof found: {summary['descriptorBridgeProofFound']}",
        f"- proof found: {summary.get('proofFound')}",
        f"- failed descriptor bridge gates: `{', '.join(summary.get('failedDescriptorBridgeGateIds') or []) or '-'}`",
        f"- missing evidence count: {len(summary.get('missingEvidence') or [])}",
        f"- evidence refs: {summary.get('evidenceRefCount')}",
        f"- promotion status: {summary['promotionStatus']}",
        "",
        summary["conclusion"],
        "",
        "## Evidence",
        "",
        "| kind | status | detail |",
        "| --- | --- | --- |",
    ]
    for row in summary["evidence"]:
        lines.append(f"| {row['kind']} | {row['status']} | {row['detail']} |")
    lines.extend(["", "## Target Refs", "", "| label | target | refs | sections |", "| --- | --- | --- | --- |"])
    for row in summary["targetRefs"]:
        lines.append(
            f"| {row['label']} | `{row['targetHex']}` | {row['refCount']} | {', '.join(row['refSections']) or '-'} |"
        )
    edge_summary = summary["descriptorTargetEdgeSummary"]
    edge_rejection = summary["descriptorEdgeRejection"]
    lines.extend([
        "",
        "## Target Edge Classification",
        "",
        f"- shared descriptor only: {edge_summary['sharedDescriptorOnly']}",
        f"- root descriptor target edges: {edge_summary['rootDescriptorTargetEdgeCount']}",
        f"- fill descriptor target edges: {edge_summary['fillDescriptorTargetEdgeCount']}",
        f"- root route execution target edges: {edge_summary['rootRouteExecutionTargetEdgeCount']}",
        f"- fill route execution target edges: {edge_summary['fillRouteExecutionTargetEdgeCount']}",
        f"- descriptor edge rejection: `{edge_rejection['classification']}`",
        f"- descriptor / route execution edge counts: {edge_rejection['descriptorTargetEdgeCount']} / {edge_rejection['routeExecutionTargetEdgeCount']}",
        f"- all target sections data: {edge_rejection['allTargetSectionsData']}",
        "",
        "| closure | source node | source | offset | target | label |",
        "| --- | --- | --- | --- | --- | --- |",
    ])
    for row in edge_summary["rootTargetEdgeRows"] + edge_summary["fillTargetEdgeRows"]:
        lines.append(
            f"| {row['closure']} | `{row['sourceNodeHex']}` | `{row['sourceVaHex']}` | "
            f"`{row['sourceOffsetFromNodeHex']}` | `{row['targetHex']}` | {row['targetLabel']} |"
        )
    lines.extend(["", "## Descriptor Closures", ""])
    for title, closure_key in [
        ("Root Stop Descriptor Closure", "rootStopDescriptorClosure"),
        ("Fill Stop Descriptor Closure", "fillStopDescriptorClosure"),
    ]:
        closure = summary[closure_key]
        lines.extend([
            f"### {title}",
            "",
            f"- start: `{closure['startHex']}`",
            f"- visited nodes: {closure['visitedNodeCount']}",
            f"- edges: {closure['edgeCount']}",
            f"- truncated: {closure['truncated']}",
            f"- target node hits: {closure['targetNodeHits']}",
            f"- target edge hits: {closure['targetEdgeHits']}",
            "",
            "| node | pointer count | target |",
            "| --- | --- | --- |",
        ])
        for row in closure["nodeRows"]:
            lines.append(
                f"| `{row['nodeVaHex']}` | {row['pointerCountInWindow']} | {row.get('targetLabel') or '-'} |"
            )
        lines.append("")
    lines.extend(["## Remaining Proofs", ""])
    for proof in summary["remainingProofs"]:
        lines.append(f"- {proof}")
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    def esc(value: Any) -> str:
        return html.escape(str(value))

    evidence_rows = "".join(
        "<tr>"
        f"<td>{esc(row['kind'])}</td>"
        f"<td>{esc(row['status'])}</td>"
        f"<td>{esc(row['detail'])}</td>"
        "</tr>"
        for row in summary["evidence"]
    )
    target_rows = "".join(
        "<tr>"
        f"<td>{esc(row['label'])}</td>"
        f"<td><code>{esc(row['targetHex'])}</code></td>"
        f"<td>{esc(row['refCount'])}</td>"
        f"<td>{esc(', '.join(row['refSections']) or '-')}</td>"
        "</tr>"
        for row in summary["targetRefs"]
    )
    edge_summary = summary["descriptorTargetEdgeSummary"]
    edge_rejection = summary["descriptorEdgeRejection"]
    edge_rows = "".join(
        "<tr>"
        f"<td>{esc(row['closure'])}</td>"
        f"<td><code>{esc(row['sourceNodeHex'])}</code></td>"
        f"<td><code>{esc(row['sourceVaHex'])}</code></td>"
        f"<td><code>{esc(row['sourceOffsetFromNodeHex'])}</code></td>"
        f"<td><code>{esc(row['targetHex'])}</code></td>"
        f"<td>{esc(row['targetLabel'])}</td>"
        "</tr>"
        for row in edge_summary["rootTargetEdgeRows"] + edge_summary["fillTargetEdgeRows"]
    )
    closure_sections = []
    for title, closure_key in [
        ("Root Stop Descriptor Closure", "rootStopDescriptorClosure"),
        ("Fill Stop Descriptor Closure", "fillStopDescriptorClosure"),
    ]:
        closure = summary[closure_key]
        rows = "".join(
            "<tr>"
            f"<td><code>{esc(row['nodeVaHex'])}</code></td>"
            f"<td>{esc(row['pointerCountInWindow'])}</td>"
            f"<td>{esc(row.get('targetLabel') or '-')}</td>"
            "</tr>"
            for row in closure["nodeRows"]
        )
        closure_sections.append(
            f"<h2>{esc(title)}</h2>"
            f"<p>start <code>{esc(closure['startHex'])}</code>; visited nodes: {esc(closure['visitedNodeCount'])}; "
            f"edges: {esc(closure['edgeCount'])}; truncated: {esc(closure['truncated'])}; "
            f"target node hits: {esc(closure['targetNodeHits'])}; target edge hits: {esc(closure['targetEdgeHits'])}.</p>"
            "<table><thead><tr><th>node</th><th>pointer count</th><th>target</th></tr></thead>"
            f"<tbody>{rows}</tbody></table>"
        )
    proof_items = "".join(f"<li>{esc(proof)}</li>" for proof in summary["remainingProofs"])
    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 Predecessor Descriptor Bridge Gap</title>",
        "  <style>body{margin:24px;background:#101010;color:#eee;font:14px system-ui,sans-serif}table{border-collapse:collapse;width:100%;margin:16px 0 28px}th,td{border:1px solid #333;padding:6px 8px;vertical-align:top}th{background:#1d1d1d}code{color:#f5d76e}</style>",
        "</head>",
        "<body>",
        "  <h1>Save Selector Predecessor Descriptor Bridge Gap</h1>",
        f"  <p>route <code>{esc(summary['source'])}</code> -&gt; <code>{esc(summary['target'])}</code>; "
        f"selectors {esc(summary['predecessorSelector'])} -&gt; {esc(summary['currentSelector'])}; "
        f"promotion status: {esc(summary['promotionStatus'])}.</p>",
        f"  <p>root stop <code>{esc(summary['predecessorRootStopSiteHex'])}</code> -&gt; "
        f"<code>{esc(summary['predecessorRootStopDescriptorHex'])}</code>; fill stop "
        f"<code>{esc(summary['predecessorFillStopSiteHex'])}</code> -&gt; "
        f"<code>{esc(summary['predecessorFillStopDescriptorHex'])}</code>; fill sites "
        f"{esc(', '.join(summary['fillSites']))}; current reader <code>{esc(summary['currentReaderHex'])}</code>.</p>",
        f"  <p>root closure reaches c0 descriptor: {esc(summary['rootStopClosureReachesFillStopDescriptor'])}; "
        f"root stop to fill bridge found: {esc(summary['rootStopToFillBridgeFound'])}; "
        f"fill stop closure self loop found: {esc(summary['fillStopClosureSelfLoopFound'])}; "
        f"fill stop to current bridge found: {esc(summary['fillStopToCurrentBridgeFound'])}; "
        "descriptor encoded target raw/root/fill/promoting: "
        f"{esc(summary['descriptorEncodedTargetRawScalarCandidateCount'])}/"
        f"{esc(summary['descriptorEncodedTargetRootRawScalarCandidateCount'])}/"
        f"{esc(summary['descriptorEncodedTargetFillRawScalarCandidateCount'])}/"
        f"{esc(summary['descriptorEncodedTargetPromotingCandidateCount'])}; "
        f"descriptor encoded target classification: {esc(summary['descriptorEncodedTargetClassification'])}; "
        f"descriptor bridge proof found: {esc(summary['descriptorBridgeProofFound'])}; "
        f"proof found: {esc(summary.get('proofFound'))}; "
        f"failed descriptor bridge gates: {esc(', '.join(summary.get('failedDescriptorBridgeGateIds') or []) or '-')}; "
        f"missing evidence count: {esc(len(summary.get('missingEvidence') or []))}; "
        f"evidence refs: {esc(summary.get('evidenceRefCount'))}.</p>",
        f"  <p>{esc(summary['conclusion'])}</p>",
        "  <h2>Evidence</h2>",
        "  <table><thead><tr><th>kind</th><th>status</th><th>detail</th></tr></thead>",
        f"  <tbody>{evidence_rows}</tbody></table>",
        "  <h2>Target Refs</h2>",
        "  <table><thead><tr><th>label</th><th>target</th><th>refs</th><th>sections</th></tr></thead>",
        f"  <tbody>{target_rows}</tbody></table>",
        "  <h2>Target Edge Classification</h2>",
        f"  <p>shared descriptor only: {esc(edge_summary['sharedDescriptorOnly'])}; "
        f"root descriptor edges: {esc(edge_summary['rootDescriptorTargetEdgeCount'])}; "
        f"fill descriptor edges: {esc(edge_summary['fillDescriptorTargetEdgeCount'])}; "
        f"root route execution edges: {esc(edge_summary['rootRouteExecutionTargetEdgeCount'])}; "
        f"fill route execution edges: {esc(edge_summary['fillRouteExecutionTargetEdgeCount'])}; "
        f"descriptor edge rejection: {esc(edge_rejection['classification'])}; "
        f"descriptor / route execution edge counts: {esc(edge_rejection['descriptorTargetEdgeCount'])} / "
        f"{esc(edge_rejection['routeExecutionTargetEdgeCount'])}; "
        f"all target sections data: {esc(edge_rejection['allTargetSectionsData'])}.</p>",
        "  <table><thead><tr><th>closure</th><th>source node</th><th>source</th><th>offset</th><th>target</th><th>label</th></tr></thead>",
        f"  <tbody>{edge_rows}</tbody></table>",
        "\n".join(closure_sections),
        "  <h2>Remaining Proofs</h2>",
        f"  <ul>{proof_items}</ul>",
        "</body>",
        "</html>",
        "",
    ])


def write_outputs(summary: dict, out_dir: Path = OUT) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "save_selector_predecessor_descriptor_bridge_gap.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    (out_dir / "save_selector_predecessor_descriptor_bridge_gap.html").write_text(
        html_page(summary),
        encoding="utf-8",
    )


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--exe", type=Path, default=ROOT / "Hwanse2.exe")
    parser.add_argument("--descriptor-map", type=Path, default=OUT / "save_selector_data_descriptor_opcode_map.json")
    parser.add_argument(
        "--predecessor-fill-order",
        type=Path,
        default=OUT / "save_selector_predecessor_fill_execution_order_gap.json",
    )
    parser.add_argument("--out-dir", type=Path, default=OUT)
    args = parser.parse_args()
    summary = build_summary(
        args.exe.read_bytes(),
        load_json(args.descriptor_map),
        load_json(args.predecessor_fill_order),
    )
    write_outputs(summary, args.out_dir)
    print(
        "wrote save-selector predecessor descriptor bridge gap "
        f"({summary['rootStopDescriptorClosure']['visitedNodeCount']} root nodes) -> "
        f"{args.out_dir / 'save_selector_predecessor_descriptor_bridge_gap.html'}"
    )


if __name__ == "__main__":
    main()
