#!/usr/bin/env python3
"""Summarize the original EXE title/start selected-pointer context."""
from __future__ import annotations

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


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"
CURRENT_ROUTE_SELECTOR = "2:0"
CURRENT_ROUTE_ROOT_HEX = "0x00540714"


def web_url(map_name: str, **params: Any) -> str:
    query = {"map": map_name}
    query.update({key: value for key, value in params.items() if value is not None and value != ""})
    pairs = []
    for key, value in query.items():
        text = str(value).replace(",", "%2C")
        pairs.append(f"{key}={text}")
    return "../web/game.html?" + "&".join(pairs)


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


def selector_row(selectors: list[dict], selector: str | None) -> dict | None:
    if not selector or ":" not in selector:
        return None
    group_text, slot_text = selector.split(":", 1)
    try:
        group = int(group_text)
        slot = int(slot_text)
    except ValueError:
        return None
    for row in selectors:
        if row.get("group") == group and row.get("slot") == slot:
            return row
    return None


def selector_ranges(selectors: list[dict]) -> tuple[list[int], dict[int, dict]]:
    contexts = {}
    for row in selectors:
        root = row.get("selectedPointer")
        if isinstance(root, int):
            contexts[root] = row
    return sorted(contexts), contexts


def selector_context_for_va(va: int | None, roots: list[int], contexts: dict[int, dict]) -> dict | None:
    if va is None or not roots:
        return None
    index = bisect.bisect_right(roots, va) - 1
    if index < 0:
        return None
    root = roots[index]
    end = roots[index + 1] if index + 1 < len(roots) else root + 0x4000
    if va >= end:
        return None
    row = contexts[root]
    return {
        "selector": f"{row.get('group')}:{row.get('slot')}",
        "rootHex": f"0x{root:08x}",
        "rootEndHex": f"0x{end:08x}",
        "relativeOffsetHex": f"+0x{va - root:x}",
    }


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


def plain_cns_names(rows: list[dict]) -> list[str]:
    return [row.get("filename") for row in rows if row.get("filename")]


def walk_objects(value: Any) -> list[dict]:
    found = []
    if isinstance(value, dict):
        found.append(value)
        for child in value.values():
            found.extend(walk_objects(child))
    elif isinstance(value, list):
        for child in value:
            found.extend(walk_objects(child))
    return found


def rows_for_selector(selection_writers: dict, selector: str | None) -> list[dict]:
    if not selector:
        return []
    rows = []
    seen = set()
    for row in walk_objects(selection_writers):
        context = row.get("selectorRootContext") or {}
        if selector not in (context.get("labels") or []):
            continue
        key = row.get("vaHex")
        if key in seen:
            continue
        seen.add(key)
        rows.append({
            "vaHex": row.get("vaHex"),
            "operation": row.get("operation"),
            "opcodeHex": row.get("opcodeHex"),
            "meaning": row.get("meaning"),
            "nextDwordHex": row.get("nextDwordHex"),
            "nextDwordCns": row.get("nextDwordCns"),
        })
    return sorted(rows, key=lambda row: row.get("vaHex") or "")


def selector_summary(selectors: list[dict], selector: str | None) -> dict:
    row = selector_row(selectors, selector)
    if not row:
        return {
            "selector": selector,
            "fieldMaps": [],
            "linkedCns": [],
            "fieldMapCount": 0,
        }
    return {
        "selector": selector,
        "selectedPointerHex": row.get("selectedPointerHex"),
        "fieldMaps": row.get("fieldMaps") or [],
        "fieldMapCount": len(row.get("fieldMaps") or []),
        "linkedCns": row.get("linkedCns") or [],
    }


def payload_lookup(cns_payloads: list[dict]) -> dict[str, dict]:
    return {row.get("name"): row for row in cns_payloads if row.get("name")}


def payload_summary(name: str, payloads: dict[str, dict]) -> dict:
    payload = payloads.get(name) or {}
    return {
        "name": name,
        "kind": payload.get("kind"),
        "width": payload.get("width"),
        "height": payload.get("height"),
        "bpp": payload.get("bpp"),
        "paletteColors": payload.get("paletteColors"),
        "decodedSize": payload.get("decodedSize"),
    }


def payload_summaries(names: list[str], payloads: dict[str, dict]) -> list[dict]:
    seen = set()
    rows = []
    for name in names:
        if not name or name in seen:
            continue
        seen.add(name)
        rows.append(payload_summary(name, payloads))
    return rows


def payload_label(row: dict) -> str:
    size = (
        f"{row.get('width')}x{row.get('height')}"
        if row.get("width") is not None and row.get("height") is not None
        else "-"
    )
    return f"{row.get('name')}:{row.get('kind')}:{size}"


def manifest_records_for(
    manifest: list[dict],
    map_name: str,
    roots: list[int],
    contexts: dict[int, dict],
) -> list[dict]:
    rows = []
    for row in manifest:
        if row.get("map") != map_name:
            continue
        record_va = row.get("recordVa")
        rows.append({
            "recordVaHex": row.get("recordVaHex"),
            "selectorContext": selector_context_for_va(record_va, roots, contexts),
            "sceneIdHex": row.get("sceneIdHex"),
            "tilesets": row.get("tilesets") or [],
            "resourceSource": row.get("resourceSource"),
        })
    return rows


def record_clusters(candidate_rows: list[dict]) -> list[dict]:
    clusters: dict[str, dict] = {}
    for candidate in candidate_rows:
        for record in candidate.get("records") or []:
            context = record.get("selectorContext") or {}
            label = context.get("selector") or "outside-selector-ranges"
            cluster = clusters.setdefault(
                label,
                {
                    "selector": label,
                    "rootHex": context.get("rootHex"),
                    "rootEndHex": context.get("rootEndHex"),
                    "recordCount": 0,
                    "maps": [],
                    "recordVas": [],
                    "sceneIds": [],
                    "tilesetFamilies": [],
                },
            )
            cluster["recordCount"] += 1
            if candidate.get("map") not in cluster["maps"]:
                cluster["maps"].append(candidate.get("map"))
            if record.get("recordVaHex"):
                cluster["recordVas"].append(record.get("recordVaHex"))
            if record.get("sceneIdHex") and record.get("sceneIdHex") not in cluster["sceneIds"]:
                cluster["sceneIds"].append(record.get("sceneIdHex"))
            tilesets = ",".join(record.get("tilesets") or [])
            if tilesets and tilesets not in cluster["tilesetFamilies"]:
                cluster["tilesetFamilies"].append(tilesets)
    for cluster in clusters.values():
        cluster["maps"] = sorted(name for name in cluster["maps"] if name)
        cluster["recordVas"] = sorted(cluster["recordVas"])
        cluster["sceneIds"] = sorted(cluster["sceneIds"])
        cluster["tilesetFamilies"] = sorted(cluster["tilesetFamilies"])
        if cluster["recordVas"]:
            cluster["recordSpanHex"] = f"{cluster['recordVas'][0]}..{cluster['recordVas'][-1]}"
    order = {"8:0": 0, "16:0": 1, "11:0": 2, "outside-selector-ranges": 99}
    return sorted(clusters.values(), key=lambda row: (order.get(row["selector"], 50), row["selector"]))


def candidate_maps(
    field_maps: list[str],
    manifest: list[dict],
    confirmed_reachable: list[str],
    candidate_reachable: list[str],
    roots: list[int],
    contexts: dict[int, dict],
) -> list[dict]:
    confirmed = set(confirmed_reachable)
    candidate = set(candidate_reachable)
    rows = []
    for index, map_name in enumerate(field_maps):
        records = manifest_records_for(manifest, map_name, roots, contexts)
        first_record = records[0] if records else {}
        rows.append({
            "order": index + 1,
            "map": map_name,
            "records": records,
            "recordCount": len(records),
            "sceneIds": sorted({record.get("sceneIdHex") for record in records if record.get("sceneIdHex")}),
            "recordVas": [record.get("recordVaHex") for record in records if record.get("recordVaHex")],
            "tilesets": first_record.get("tilesets") or [],
            "resourceSource": first_record.get("resourceSource"),
            "inWebConfirmedReachable": map_name in confirmed,
            "inSaveSelectorCandidateReachable": map_name in candidate,
            "openUrl": web_url(map_name, saveGroup=8, saveSlot=0, events=1, overview=1),
            "walkUrl": web_url(map_name, saveGroup=8, saveSlot=0),
            "reviewUrl": f"../web/map_review.html?map={map_name}",
        })
    return rows


def build_summary(
    runtime_memory_context: dict,
    runtime_input_probe: dict,
    runtime_key_sequence_probe: dict,
    playable_progress: dict,
    selectors: list[dict],
    scene_manifest: list[dict] | None = None,
    selection_writers: dict | None = None,
    cns_payloads: list[dict] | None = None,
) -> dict:
    selected_selector = runtime_memory_context.get("selectedPointerContextSelector")
    row = selector_row(selectors, selected_selector)
    roots, contexts = selector_ranges(selectors)
    field_maps = (
        runtime_memory_context.get("selectedPointerContextFieldMaps")
        or (row or {}).get("fieldMaps")
        or []
    )
    resources = (
        runtime_memory_context.get("selectedPointerContextResources")
        or (row or {}).get("linkedCns")
        or []
    )
    web_start_map = playable_progress.get("startMap")
    confirmed_reachable = playable_progress.get("reachableFromStart") or []
    candidate_reachable = playable_progress.get("saveSelectorCandidateReachableFromStart") or []
    field_set = set(field_maps)
    confirmed_overlap = sorted(field_set.intersection(confirmed_reachable))
    candidate_overlap = sorted(field_set.intersection(candidate_reachable))
    candidates = candidate_maps(
        field_maps,
        scene_manifest or [],
        confirmed_reachable,
        candidate_reachable,
        roots,
        contexts,
    )
    clusters = record_clusters(candidates)
    baseline_context = runtime_input_probe.get("baselineSelectedPointerContext") or {}
    final_context = runtime_input_probe.get("finalSelectedPointerContext") or {}
    final_selector = final_context.get("selector")
    nearby_cns = plain_cns_names(runtime_memory_context.get("nearbyCnsStrings") or [])
    key_sequence_count = runtime_key_sequence_probe.get("sequenceCount")
    reached_route = runtime_key_sequence_probe.get("anyReachedRouteSelectorContext")
    selected_pointer = runtime_memory_context.get("selectedPointerStaticValueHex")
    selected_root = (row or {}).get("selectedPointerHex") or (baseline_context or {}).get("rootHex")
    live_pointer = int_hex(selected_pointer)
    selector8_cluster = next((cluster for cluster in clusters if cluster.get("selector") == selected_selector), {})
    selector8_record_vas = [int_hex(value) for value in selector8_cluster.get("recordVas") or []]
    selector8_record_vas = [value for value in selector8_record_vas if value is not None]
    field_span = (
        f"0x{min(selector8_record_vas):08x}..0x{max(selector8_record_vas):08x}"
        if selector8_record_vas else None
    )
    nearby_rows = runtime_memory_context.get("nearbyCnsStrings") or []
    nearby_vas = [int_hex(item.get("vaHex")) for item in nearby_rows]
    nearby_vas = [value for value in nearby_vas if value is not None]
    title_tail_span = (
        f"0x{min(nearby_vas):08x}..0x{max(nearby_vas):08x}"
        if nearby_vas else None
    )
    title_resource_writers = [
        writer for writer in runtime_memory_context.get("nearbySelectionWriterRows") or []
        if writer.get("nextDwordCns") in {"compile.cns", "aaa.cns", "title.cns"}
    ]
    final_selector_summary = selector_summary(selectors, final_selector)
    final_selector_rows = rows_for_selector(selection_writers or {}, final_selector)
    payloads = payload_lookup(cns_payloads or [])
    final_selector_cns = sorted({
        name for name in (
            final_selector_summary.get("linkedCns")
            + [row.get("nextDwordCns") for row in final_selector_rows if row.get("nextDwordCns")]
        )
        if name
    })
    input_transition = {
        "fromPointerHex": runtime_input_probe.get("baselineSelectedPointerStaticHex"),
        "fromSelector": baseline_context.get("selector"),
        "toPointerHex": (runtime_input_probe.get("finalSample") or {}).get("selectedPointerStaticHex"),
        "toSelector": final_selector,
        "toSelectedRootHex": final_selector_summary.get("selectedPointerHex"),
        "toFieldMapCount": final_selector_summary.get("fieldMapCount"),
        "toFieldMaps": final_selector_summary.get("fieldMaps"),
        "toLinkedCns": final_selector_summary.get("linkedCns"),
        "toSelectionRows": final_selector_rows,
        "toSelectionReaderCount": sum(1 for row in final_selector_rows if row.get("operation") == "reader"),
        "toSelectionWriterCount": sum(1 for row in final_selector_rows if row.get("operation") == "writer"),
        "toReferencedCns": final_selector_cns,
        "toResourcePayloads": payload_summaries(final_selector_cns, payloads),
        "keyBufferChangedSelectedPointer": runtime_input_probe.get("keyBufferPokeChangedSelectedPointer"),
        "classification": (
            "non-field-map-resource-context"
            if final_selector and final_selector_summary.get("fieldMapCount") == 0
            else "field-map-or-unresolved-context"
        ),
        "promotionStatus": "diagnostic-only",
    }
    live_after_field_records = bool(
        live_pointer is not None
        and selector8_record_vas
        and live_pointer > max(selector8_record_vas)
    )
    live_near_title_tail = bool(
        live_pointer is not None
        and nearby_vas
        and min(nearby_vas) <= live_pointer <= max(nearby_vas) + 0x80
    )
    live_pointer_classification = (
        "title-resource-tail-not-field-record"
        if selected_selector == "8:0" and live_after_field_records and live_near_title_tail
        else "selector-context-unresolved"
    )
    conclusion = (
        "The original EXE runtime title/start sample resolves to selector 8:0, but the live pointer is "
        "past selector 8:0's map5_* field-record cluster and lands near the compile/aaa/title resource "
        "tail. The map5_* field records are duplicated in selector 16:0 and 11:0 too, so they are reused "
        "scene lists rather than a unique new-game start proof. The current web confirmed start remains "
        "map1_02b and confirmed progression reaches only map1_02b -> map1_01a. This separates "
        "title/start diagnostics from the blocked map1_01a -> map2_02d route and does not promote selector 2:0."
    )
    return {
        "objective": "classify the original EXE title/start selected-pointer context against the current web route",
        "selectedPointerStaticHex": selected_pointer,
        "selectedPointerRuntimeHex": runtime_memory_context.get("selectedPointerRuntimeValueHex"),
        "selectedPointerSelector": selected_selector,
        "selectedPointerRootHex": selected_root,
        "selectedPointerRangeHex": runtime_memory_context.get("selectedPointerContextRangeHex"),
        "selectedPointerEqualsCurrentRouteRoot": runtime_memory_context.get("selectedPointerEqualsCurrentRouteRoot"),
        "currentRouteSelector": CURRENT_ROUTE_SELECTOR,
        "currentRouteRootHex": CURRENT_ROUTE_ROOT_HEX,
        "fieldMaps": field_maps,
        "fieldMapCount": len(field_maps),
        "candidateMaps": candidates,
        "recordClusters": clusters,
        "selector8FieldRecordSpanHex": field_span,
        "titleResourceTailSpanHex": title_tail_span,
        "titleResourceWriterRows": title_resource_writers,
        "inputTransitionContext": input_transition,
        "livePointerAfterSelector8FieldRecords": live_after_field_records,
        "livePointerNearTitleResourceTail": live_near_title_tail,
        "livePointerClassification": live_pointer_classification,
        "resources": resources,
        "nearbyCnsStrings": nearby_cns,
        "nearbyCnsPayloads": payload_summaries(nearby_cns, payloads),
        "hasTitleResourceNearby": "title.cns" in nearby_cns,
        "inputProbeBaselinePointerHex": runtime_input_probe.get("baselineSelectedPointerStaticHex"),
        "inputProbeBaselineSelector": baseline_context.get("selector"),
        "inputProbeFinalPointerHex": (runtime_input_probe.get("finalSample") or {}).get("selectedPointerStaticHex"),
        "inputProbeFinalSelector": final_context.get("selector"),
        "inputProbeKeyBufferChangedSelectedPointer": runtime_input_probe.get("keyBufferPokeChangedSelectedPointer"),
        "keySequenceCount": key_sequence_count,
        "keySequenceReachedCurrentRoot": runtime_key_sequence_probe.get("anyReachedCurrentRoot"),
        "keySequenceReachedRouteSelectorContext": reached_route,
        "webConfirmedStartMap": web_start_map,
        "webConfirmedReachable": confirmed_reachable,
        "webConfirmedOverlapWithTitleContext": confirmed_overlap,
        "saveSelectorCandidateOverlapWithTitleContext": candidate_overlap,
        "promotionStatus": "diagnostic-only",
        "routePromotionAllowed": False,
        "conclusion": conclusion,
    }


def markdown(summary: dict) -> str:
    lines = [
        "# Runtime Title Start Context",
        "",
        f"- selected pointer: `{summary.get('selectedPointerStaticHex')}`",
        f"- selected selector: `{summary.get('selectedPointerSelector')}`",
        f"- selected root: `{summary.get('selectedPointerRootHex')}`",
        f"- current route selector: `{summary.get('currentRouteSelector')}`",
        f"- current route root: `{summary.get('currentRouteRootHex')}`",
        f"- equals current route root: {summary.get('selectedPointerEqualsCurrentRouteRoot')}",
        f"- title resource nearby: {summary.get('hasTitleResourceNearby')}",
        f"- web confirmed start: `{summary.get('webConfirmedStartMap')}`",
        f"- web confirmed overlap with title context: {', '.join(summary.get('webConfirmedOverlapWithTitleContext') or []) or 'none'}",
        f"- save-selector candidate overlap: {', '.join(summary.get('saveSelectorCandidateOverlapWithTitleContext') or []) or 'none'}",
        f"- key sequence count: {summary.get('keySequenceCount')}",
        f"- key sequence reached selector 2:0: {summary.get('keySequenceReachedRouteSelectorContext')}",
        f"- input transition: `{(summary.get('inputTransitionContext') or {}).get('fromSelector')} -> {(summary.get('inputTransitionContext') or {}).get('toSelector')}`",
        f"- input transition classification: `{(summary.get('inputTransitionContext') or {}).get('classification')}`",
        f"- live pointer classification: `{summary.get('livePointerClassification')}`",
        f"- selector 8:0 field-record span: `{summary.get('selector8FieldRecordSpanHex')}`",
        f"- title resource tail span: `{summary.get('titleResourceTailSpanHex')}`",
        f"- promotion status: `{summary.get('promotionStatus')}`",
        "",
        summary.get("conclusion") or "",
        "",
        "## Record Clusters",
        "",
        "| selector | records | maps | span | scene ids | tilesets |",
        "| --- | ---: | ---: | --- | --- | --- |",
    ]
    for row in summary.get("recordClusters") or []:
        lines.append(
            f"| `{row.get('selector')}` | {row.get('recordCount')} | "
            f"{len(row.get('maps') or [])} | `{row.get('recordSpanHex') or '-'}` | "
            f"{', '.join(row.get('sceneIds') or []) or '-'} | "
            f"{'; '.join(row.get('tilesetFamilies') or []) or '-'} |"
        )
    lines.extend([
        "",
        "## Candidate Maps",
        "",
        "| order | map | scene ids | records | tilesets | confirmed | candidate | open | walk | review |",
        "| ---: | --- | --- | ---: | --- | --- | --- | --- | --- | --- |",
    ])
    for row in summary.get("candidateMaps") or []:
        lines.append(
            f"| {row.get('order')} | `{row.get('map')}` | "
            f"{', '.join(row.get('sceneIds') or []) or '-'} | "
            f"{row.get('recordCount')} | "
            f"{', '.join(row.get('tilesets') or []) or '-'} | "
            f"{row.get('inWebConfirmedReachable')} | "
            f"{row.get('inSaveSelectorCandidateReachable')} | "
            f"[open]({row.get('openUrl')}) | "
            f"[walk]({row.get('walkUrl')}) | "
            f"[review]({row.get('reviewUrl')}) |"
        )
    lines.extend([
        "",
        "## Input Transition Context",
        "",
    ])
    transition = summary.get("inputTransitionContext") or {}
    lines.extend([
        f"- from: `{transition.get('fromSelector')}` `{transition.get('fromPointerHex')}`",
        f"- to: `{transition.get('toSelector')}` `{transition.get('toPointerHex')}`",
        f"- to selected root: `{transition.get('toSelectedRootHex')}`",
        f"- to field maps: {transition.get('toFieldMapCount')}",
        f"- to linked CNS: {', '.join(transition.get('toLinkedCns') or []) or '-'}",
        f"- to referenced CNS: {', '.join(transition.get('toReferencedCns') or []) or '-'}",
        f"- to resource payloads: {', '.join(payload_label(row) for row in transition.get('toResourcePayloads') or []) or '-'}",
        f"- selection rows: readers `{transition.get('toSelectionReaderCount')}` / writers `{transition.get('toSelectionWriterCount')}`",
        f"- classification: `{transition.get('classification')}`",
        "",
        "| va | op | opcode | next | meaning |",
        "| --- | --- | --- | --- | --- |",
    ])
    for row in transition.get("toSelectionRows") or []:
        lines.append(
            f"| `{row.get('vaHex')}` | {row.get('operation')} | `{row.get('opcodeHex')}` | "
            f"`{row.get('nextDwordCns') or row.get('nextDwordHex') or '-'}` | {row.get('meaning') or '-'} |"
        )
    lines.extend([
        "",
        "## Field Maps",
        "",
    ])
    lines.extend(f"- `{name}`" for name in summary.get("fieldMaps") or [])
    lines.extend(["", "## Nearby CNS Strings", ""])
    lines.extend(f"- `{name}`" for name in summary.get("nearbyCnsStrings") or [])
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    field_items = "".join(
        f"<li><code>{html.escape(str(name))}</code></li>"
        for name in summary.get("fieldMaps") or []
    )
    cns_items = "".join(
        f"<li><code>{html.escape(str(name))}</code></li>"
        for name in summary.get("nearbyCnsStrings") or []
    )
    cluster_rows = "".join(
        "<tr>"
        f"<td><code>{html.escape(str(row.get('selector')))}</code></td>"
        f"<td>{html.escape(str(row.get('recordCount')))}</td>"
        f"<td>{html.escape(str(len(row.get('maps') or [])))}</td>"
        f"<td><code>{html.escape(str(row.get('recordSpanHex') or '-'))}</code></td>"
        f"<td>{html.escape(', '.join(row.get('sceneIds') or []) or '-')}</td>"
        f"<td>{html.escape('; '.join(row.get('tilesetFamilies') or []) or '-')}</td>"
        "</tr>"
        for row in summary.get("recordClusters") or []
    )
    candidate_rows = "".join(
        "<tr>"
        f"<td>{html.escape(str(row.get('order')))}</td>"
        f"<td><code>{html.escape(str(row.get('map')))}</code></td>"
        f"<td>{html.escape(', '.join(row.get('sceneIds') or []) or '-')}</td>"
        f"<td>{html.escape(str(row.get('recordCount')))}</td>"
        f"<td>{html.escape(', '.join(row.get('tilesets') or []) or '-')}</td>"
        f"<td>{html.escape(str(row.get('inWebConfirmedReachable')))}</td>"
        f"<td>{html.escape(str(row.get('inSaveSelectorCandidateReachable')))}</td>"
        f"<td><a href=\"{html.escape(str(row.get('openUrl')))}\">open</a></td>"
        f"<td><a href=\"{html.escape(str(row.get('walkUrl')))}\">walk</a></td>"
        f"<td><a href=\"{html.escape(str(row.get('reviewUrl')))}\">review</a></td>"
        "</tr>"
        for row in summary.get("candidateMaps") or []
    )
    transition = summary.get("inputTransitionContext") or {}
    transition_rows = "".join(
        "<tr>"
        f"<td><code>{html.escape(str(row.get('vaHex')))}</code></td>"
        f"<td>{html.escape(str(row.get('operation')))}</td>"
        f"<td><code>{html.escape(str(row.get('opcodeHex')))}</code></td>"
        f"<td><code>{html.escape(str(row.get('nextDwordCns') or row.get('nextDwordHex') or '-'))}</code></td>"
        f"<td>{html.escape(str(row.get('meaning') or '-'))}</td>"
        "</tr>"
        for row in transition.get("toSelectionRows") or []
    )
    return "\n".join([
        "<!doctype html><meta charset=\"utf-8\"><title>Runtime Title Start 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%}td,th{border:1px solid #444;padding:6px 8px;vertical-align:top}a{color:#9bd4ff}code{color:#9bd4ff}</style>",
        "<h1>Runtime Title Start Context</h1>",
        "<ul>",
        f"<li>selected pointer: <code>{html.escape(str(summary.get('selectedPointerStaticHex')))}</code></li>",
        f"<li>selected selector: <code>{html.escape(str(summary.get('selectedPointerSelector')))}</code></li>",
        f"<li>selected root: <code>{html.escape(str(summary.get('selectedPointerRootHex')))}</code></li>",
        f"<li>current route root: <code>{html.escape(str(summary.get('currentRouteRootHex')))}</code></li>",
        f"<li>equals current route root: {html.escape(str(summary.get('selectedPointerEqualsCurrentRouteRoot')))}</li>",
        f"<li>title resource nearby: {html.escape(str(summary.get('hasTitleResourceNearby')))}</li>",
        f"<li>web confirmed start: <code>{html.escape(str(summary.get('webConfirmedStartMap')))}</code></li>",
        f"<li>web confirmed overlap with title context: {html.escape(', '.join(summary.get('webConfirmedOverlapWithTitleContext') or []) or 'none')}</li>",
        f"<li>save-selector candidate overlap: {html.escape(', '.join(summary.get('saveSelectorCandidateOverlapWithTitleContext') or []) or 'none')}</li>",
        f"<li>key sequence count: {html.escape(str(summary.get('keySequenceCount')))}</li>",
        f"<li>key sequence reached selector 2:0: {html.escape(str(summary.get('keySequenceReachedRouteSelectorContext')))}</li>",
        f"<li>input transition: <code>{html.escape(str(transition.get('fromSelector')))} -&gt; {html.escape(str(transition.get('toSelector')))}</code></li>",
        f"<li>input transition classification: <code>{html.escape(str(transition.get('classification')))}</code></li>",
        f"<li>live pointer classification: <code>{html.escape(str(summary.get('livePointerClassification')))}</code></li>",
        f"<li>selector 8:0 field-record span: <code>{html.escape(str(summary.get('selector8FieldRecordSpanHex')))}</code></li>",
        f"<li>title resource tail span: <code>{html.escape(str(summary.get('titleResourceTailSpanHex')))}</code></li>",
        f"<li>promotion status: <code>{html.escape(str(summary.get('promotionStatus')))}</code></li>",
        "</ul>",
        f"<p>{html.escape(str(summary.get('conclusion') or ''))}</p>",
        "<h2>Record Clusters</h2>",
        "<table><thead><tr><th>selector</th><th>records</th><th>maps</th><th>span</th><th>scene ids</th><th>tilesets</th></tr></thead><tbody>",
        cluster_rows,
        "</tbody></table>",
        "<h2>Candidate Maps</h2>",
        "<table><thead><tr><th>order</th><th>map</th><th>scene ids</th><th>records</th><th>tilesets</th><th>confirmed</th><th>candidate</th><th>open</th><th>walk</th><th>review</th></tr></thead><tbody>",
        candidate_rows,
        "</tbody></table>",
        "<h2>Input Transition Context</h2>",
        f"<p>From <code>{html.escape(str(transition.get('fromSelector')))}</code> <code>{html.escape(str(transition.get('fromPointerHex')))}</code> "
        f"to <code>{html.escape(str(transition.get('toSelector')))}</code> <code>{html.escape(str(transition.get('toPointerHex')))}</code>. "
        f"Final field maps: <code>{html.escape(str(transition.get('toFieldMapCount')))}</code>; "
        f"linked CNS: <code>{html.escape(', '.join(transition.get('toLinkedCns') or []) or '-')}</code>; "
        f"referenced CNS: <code>{html.escape(', '.join(transition.get('toReferencedCns') or []) or '-')}</code>; "
        f"payloads: <code>{html.escape(', '.join(payload_label(row) for row in transition.get('toResourcePayloads') or []) or '-')}</code>; "
        f"classification: <code>{html.escape(str(transition.get('classification')))}</code>.</p>",
        "<table><thead><tr><th>va</th><th>op</th><th>opcode</th><th>next</th><th>meaning</th></tr></thead><tbody>",
        transition_rows,
        "</tbody></table>",
        "<h2>Field Maps</h2>",
        f"<ul>{field_items}</ul>",
        "<h2>Nearby CNS Strings</h2>",
        f"<ul>{cns_items}</ul>",
    ])


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 / "runtime_title_start_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("--out-dir", type=Path, default=OUT)
    parser.add_argument(
        "--html-out",
        type=Path,
        default=None,
        help="Optional legacy HTML output path. JSON is the default retained artifact.",
    )
    args = parser.parse_args()
    summary = build_summary(
        load_json(args.out_dir / "runtime_memory_snapshot_context.json", {}),
        load_json(args.out_dir / "runtime_input_path_probe.json", {}),
        load_json(args.out_dir / "runtime_key_sequence_probe.json", {}),
        load_json(args.out_dir / "playable_progress.json", {}),
        load_json(args.out_dir / "save_scene_selectors.json", []),
        load_json(args.out_dir / "scene_manifest.json", []),
        load_json(args.out_dir / "save_selector_selection_writers.json", {}),
        load_json(args.out_dir / "cns_payloads.json", []),
    )
    json_out = write_outputs(summary, args.out_dir, args.html_out)
    print(f"wrote runtime title start context -> {json_out}")


if __name__ == "__main__":
    main()
