#!/usr/bin/env python3
"""Scan runtime coordinate sources after loading the public predecessor save."""
from __future__ import annotations

import json
import subprocess
import time
from argparse import Namespace
from pathlib import Path
from typing import Any

from probe_runtime_input_path import (
    CURRENT_ROOT,
    DEFAULT_PREFIX,
    IMAGE_BASE,
    IMAGE_SIZE,
    OUT,
    ROOT,
    SELECTED_POINTER_GLOBAL,
    env_for,
    find_free_display,
    find_hwanse_pid,
    hex32,
    load_selector_contexts,
    loaded_base,
    sample_process,
    selected_pointer_context,
    truncate,
    write_key_buffer,
)
from probe_runtime_key_sequences import KEY_OFFSETS, run_input_path_prelude
from probe_runtime_selected_pointer_poll import focus_windows
from probe_runtime_selected_pointer_multislot_savedata_poll import (
    cleanup_temporary_saves,
    prepare_temporary_saves,
)
from runtime_case_aliases import cleanup_case_aliases, prepare_case_aliases


OUTPUT_PREFIX = "runtime_predecessor_coordinate_source_scan"
SOURCE_SAVE = ROOT / "data" / "public_savedata" / "flack3r" / "savedat2.dat"
SCAN_SEQUENCE = ["Down", "Return"]

CAMERA_TILE_X = 0x004576DC
CAMERA_TILE_Y = 0x004576DE
SAVE_BLOCK = 0x004576D8
ACTIVE_ORDER_COUNT = 0x004576E8
ACTIVE_ORDER = 0x004576E9
ACTIVE_SLOT_BASE = 0x00457750
ACTIVE_SLOT_STRIDE = 0xD8
SELECTION_BUFFER = 0x0059E310
RUNTIME_SLOT_BASE_TABLE = 0x0059DB30
RUNTIME_OBJECT_TABLE_ALT = 0x0059DB3C
RUNTIME_OBJECT_TABLE = 0x0059DD70
ACTOR_HISTORY_INDEX_TABLE = 0x00574540
ACTOR_SEED_X_TABLE = 0x00574550
ACTOR_SEED_Y_TABLE = 0x00574552
ACTOR_HISTORY_DIRECTION_TABLE = 0x00574554
ACTOR_HISTORY_STRIDE_BYTES = 6
ACTOR_HISTORY_SLOT_COUNT = 7

PAIR_SCAN_TARGETS = [
    {"name": "public-save-start", "x": 5, "y": 15},
    {"name": "observed-trail", "x": 15, "y": 26},
    {"name": "reciprocal-left", "x": 1, "y": 14},
    {"name": "reciprocal-top", "x": 46, "y": 0},
    {"name": "reciprocal-bottom", "x": 47, "y": 47},
    {"name": "reciprocal-right", "x": 95, "y": 16},
]

FAILED_PREDECESSOR_COORDINATE_SOURCE_GATE_IDS = [
    "public-predecessor-gameplay-object-source",
    "reciprocal-target-actor-or-trail-source",
    "selector-2-0-current-root-execution",
]
PREDECESSOR_COORDINATE_SOURCE_MISSING_EVIDENCE = [
    "runtime object or actor tile source for the public predecessor start coordinate",
    "reciprocal target coordinate observed in actor/trail source instead of camera or image memory only",
    "selector 2:0/current-root execution connected to the coordinate source before promotion",
]
PREDECESSOR_COORDINATE_SOURCE_EVIDENCE_REFS = [
    {
        "path": "data/public_savedata/flack3r/savedat2.dat",
        "fields": ["sourceSave", "sequence", "snapshots"],
    },
    {
        "path": "out/runtime_predecessor_coordinate_source_scan.json",
        "fields": [
            "pairHitSummaryRows",
            "pointerTablePairHits",
            "staticBasePairHits",
            "trailRingPairHits",
            "imagePairHits",
        ],
    },
    {
        "path": "out/runtime_selected_pointer_predecessor_coordinate_branch_state_poll.json",
        "fields": [
            "coordinateRuntimeBranchStateSplit",
            "secondaryBranchStateAllZero",
            "anyReachedCurrentRoot",
            "anyReachedRouteSelectorContext",
        ],
    },
    {
        "path": "out/runtime_predecessor_field_entry_sequence_scan.json",
        "fields": [
            "fieldEntryCandidateCount",
            "snapshotRouteCandidateCount",
            "finalSelectorCounts",
            "snapshotSelectorCounts",
        ],
    },
]

KNOWN_POINTER_TABLES = [
    {"name": "runtimeSlotBaseTable", "staticVa": RUNTIME_SLOT_BASE_TABLE, "slots": 12},
    {"name": "runtimeObjectTableAlt", "staticVa": RUNTIME_OBJECT_TABLE_ALT, "slots": 12},
    {"name": "runtimeObjectTable", "staticVa": RUNTIME_OBJECT_TABLE, "slots": 12},
]

PAIR_FIELD_OFFSETS = [
    {"name": "tile_e8_ea", "x": 0xE8, "y": 0xEA},
    {"name": "draw_1c_20_words", "x": 0x1C, "y": 0x20},
    {"name": "save_04_06", "x": 0x04, "y": 0x06},
    {"name": "selection_20_22", "x": 0x20, "y": 0x22},
]

STATIC_BASES = [
    {"name": "saveRuntimeBlock", "staticVa": SAVE_BLOCK},
    {"name": "selectionBuffer", "staticVa": SELECTION_BUFFER},
    *[
        {"name": f"activeSlot{index}", "staticVa": ACTIVE_SLOT_BASE + index * ACTIVE_SLOT_STRIDE}
        for index in range(3)
    ],
]


def runtime_address(base: int, static_va: int) -> int:
    return base + (static_va - IMAGE_BASE)


def static_from_runtime(base: int, address: int | None) -> int | None:
    if address is None:
        return None
    if base <= address < base + IMAGE_SIZE:
        return IMAGE_BASE + (address - base)
    return None


def read_u(mem: Any, address: int, size: int) -> int | None:
    try:
        mem.seek(address)
        raw = mem.read(size)
    except OSError:
        return None
    if len(raw) != size:
        return None
    return int.from_bytes(raw, "little")


def read_pair(mem: Any, address: int, x_offset: int, y_offset: int) -> dict[str, Any]:
    x = read_u(mem, address + x_offset, 2)
    y = read_u(mem, address + y_offset, 2)
    return {
        "x": x,
        "y": y,
        "pairHex": pair_hex(x, y) if x is not None and y is not None else None,
        "plausibleTile": x is not None and y is not None and 0 <= x <= 200 and 0 <= y <= 200,
    }


def pair_bytes(x: int, y: int) -> bytes:
    return int(x).to_bytes(2, "little") + int(y).to_bytes(2, "little")


def pair_hex(x: int | None, y: int | None) -> str | None:
    if x is None or y is None:
        return None
    return f"0x{((y & 0xFFFF) << 16) | (x & 0xFFFF):08x}"


def snapshot_basic(pid: int, base: int) -> dict[str, Any]:
    roots, contexts = load_selector_contexts()
    sample = sample_process(pid, base)
    context = selected_pointer_context(sample.get("selectedPointerStaticHex"), roots, contexts)
    try:
        with (Path("/proc") / str(pid) / "mem").open("r+b", buffering=0) as mem:
            camera_x = read_u(mem, runtime_address(base, CAMERA_TILE_X), 2)
            camera_y = read_u(mem, runtime_address(base, CAMERA_TILE_Y), 2)
            active_count = read_u(mem, runtime_address(base, ACTIVE_ORDER_COUNT), 1)
            active_order = [
                read_u(mem, runtime_address(base, ACTIVE_ORDER + index), 1)
                for index in range(6)
            ]
            selected_pointer = read_u(mem, runtime_address(base, SELECTED_POINTER_GLOBAL), 4)
    except OSError as exc:
        return {"readOk": False, "error": str(exc), "selectedPointerContext": context}
    return {
        "readOk": True,
        "selectedPointerRuntimeHex": hex32(selected_pointer),
        "selectedPointerStaticHex": sample.get("selectedPointerStaticHex"),
        "selectedPointerContext": context,
        "selectedPointerEqualsCurrentRoot": sample.get("selectedPointerEqualsCurrentRoot"),
        "cameraTile": {"x": camera_x, "y": camera_y, "pairHex": pair_hex(camera_x, camera_y)},
        "activeOrderCount": active_count,
        "activeOrderBytes": active_order,
        "pressedKeyOffsets": sample.get("pressedKeyOffsets") or [],
    }


def scan_pointer_tables(mem: Any, base: int) -> list[dict[str, Any]]:
    rows = []
    for table in KNOWN_POINTER_TABLES:
        table_runtime = runtime_address(base, table["staticVa"])
        entries = []
        for slot in range(int(table["slots"])):
            pointer_address = table_runtime + slot * 4
            pointer = read_u(mem, pointer_address, 4)
            pointer_static = static_from_runtime(base, pointer)
            field_rows = []
            if pointer:
                for field in PAIR_FIELD_OFFSETS:
                    pair = read_pair(mem, pointer, int(field["x"]), int(field["y"]))
                    field_rows.append({
                        "name": field["name"],
                        "xOffsetHex": hex32(field["x"]),
                        "yOffsetHex": hex32(field["y"]),
                        **pair,
                    })
            entries.append({
                "slot": slot,
                "pointerRuntimeHex": hex32(pointer),
                "pointerStaticHex": hex32(pointer_static),
                "pointerAddressStaticHex": hex32(table["staticVa"] + slot * 4),
                "nonZero": bool(pointer),
                "fieldPairs": field_rows,
            })
        rows.append({
            "name": table["name"],
            "staticVaHex": hex32(table["staticVa"]),
            "entries": entries,
        })
    return rows


def scan_static_bases(mem: Any, base: int) -> list[dict[str, Any]]:
    rows = []
    for base_row in STATIC_BASES:
        address = runtime_address(base, int(base_row["staticVa"]))
        field_rows = []
        for field in PAIR_FIELD_OFFSETS:
            pair = read_pair(mem, address, int(field["x"]), int(field["y"]))
            field_rows.append({
                "name": field["name"],
                "xOffsetHex": hex32(field["x"]),
                "yOffsetHex": hex32(field["y"]),
                **pair,
            })
        rows.append({
            "name": base_row["name"],
            "staticVaHex": hex32(base_row["staticVa"]),
            "fieldPairs": field_rows,
        })
    return rows


def scan_trail_ring(mem: Any, base: int) -> list[dict[str, Any]]:
    rows = []
    indices = [
        read_u(mem, runtime_address(base, ACTOR_HISTORY_INDEX_TABLE + index), 1)
        for index in range(3)
    ]
    for index in range(ACTOR_HISTORY_SLOT_COUNT):
        x_static = ACTOR_SEED_X_TABLE + index * ACTOR_HISTORY_STRIDE_BYTES
        y_static = ACTOR_SEED_Y_TABLE + index * ACTOR_HISTORY_STRIDE_BYTES
        direction_static = ACTOR_HISTORY_DIRECTION_TABLE + index * ACTOR_HISTORY_STRIDE_BYTES
        x = read_u(mem, runtime_address(base, x_static), 2)
        y = read_u(mem, runtime_address(base, y_static), 2)
        direction = read_u(mem, runtime_address(base, direction_static), 2)
        rows.append({
            "slot": index,
            "tile": {"x": x, "y": y, "pairHex": pair_hex(x, y)},
            "direction": direction,
        })
    return [{"historyIndices": indices, "rows": rows}]


def scan_image_pairs(mem: Any, base: int) -> list[dict[str, Any]]:
    try:
        mem.seek(base)
        image = mem.read(IMAGE_SIZE)
    except OSError:
        return []
    rows = []
    for target in PAIR_SCAN_TARGETS:
        needle = pair_bytes(int(target["x"]), int(target["y"]))
        hits = []
        start = 0
        while True:
            index = image.find(needle, start)
            if index < 0:
                break
            static_va = IMAGE_BASE + index
            hits.append({
                "staticVaHex": hex32(static_va),
                "runtimeAddressHex": hex32(base + index),
                "nearKnownGlobal": known_global_label(static_va),
            })
            start = index + 1
            if len(hits) >= 64:
                break
        rows.append({
            "name": target["name"],
            "x": target["x"],
            "y": target["y"],
            "pairHex": pair_hex(target["x"], target["y"]),
            "hitCountCapped": len(hits),
            "hits": hits,
        })
    return rows


def pair_target_by_hex() -> dict[str, dict[str, Any]]:
    return {
        str(pair_hex(int(target["x"]), int(target["y"]))): target
        for target in PAIR_SCAN_TARGETS
    }


def scan_pointer_pair_hits(pointer_tables: list[dict[str, Any]]) -> list[dict[str, Any]]:
    targets = pair_target_by_hex()
    hits = []
    for table in pointer_tables:
        for entry in table.get("entries") or []:
            for field in entry.get("fieldPairs") or []:
                target = targets.get(field.get("pairHex"))
                if not target:
                    continue
                hits.append({
                    "target": target.get("name"),
                    "pairHex": field.get("pairHex"),
                    "table": table.get("name"),
                    "slot": entry.get("slot"),
                    "pointerStaticHex": entry.get("pointerStaticHex"),
                    "field": field.get("name"),
                    "x": field.get("x"),
                    "y": field.get("y"),
                })
    return hits


def scan_static_base_pair_hits(static_bases: list[dict[str, Any]]) -> list[dict[str, Any]]:
    targets = pair_target_by_hex()
    hits = []
    for base_row in static_bases:
        for field in base_row.get("fieldPairs") or []:
            target = targets.get(field.get("pairHex"))
            if not target:
                continue
            hits.append({
                "target": target.get("name"),
                "pairHex": field.get("pairHex"),
                "base": base_row.get("name"),
                "baseStaticVaHex": base_row.get("staticVaHex"),
                "field": field.get("name"),
                "x": field.get("x"),
                "y": field.get("y"),
            })
    return hits


def scan_trail_pair_hits(trail_ring: list[dict[str, Any]]) -> list[dict[str, Any]]:
    targets = pair_target_by_hex()
    hits = []
    for ring in trail_ring:
        for row in ring.get("rows") or []:
            tile = row.get("tile") or {}
            target = targets.get(tile.get("pairHex"))
            if not target:
                continue
            hits.append({
                "target": target.get("name"),
                "pairHex": tile.get("pairHex"),
                "slot": row.get("slot"),
                "x": tile.get("x"),
                "y": tile.get("y"),
                "direction": row.get("direction"),
            })
    return hits


def image_pair_known_global_hits(image_pair_hits: list[dict[str, Any]], target_name: str) -> list[dict[str, Any]]:
    for row in image_pair_hits:
        if row.get("name") == target_name:
            return [
                hit
                for hit in row.get("hits") or []
                if hit.get("nearKnownGlobal")
            ]
    return []


def pair_hit_summary_rows(summary: dict[str, Any]) -> list[dict[str, Any]]:
    pointer_hits = summary.get("pointerTablePairHits") or []
    static_hits = summary.get("staticBasePairHits") or []
    trail_hits = summary.get("trailRingPairHits") or []
    image_hits = summary.get("imagePairHits") or []
    rows = []
    for target in PAIR_SCAN_TARGETS:
        name = str(target["name"])
        image_row = next((row for row in image_hits if row.get("name") == name), {})
        target_pointer_hits = [row for row in pointer_hits if row.get("target") == name]
        target_static_hits = [row for row in static_hits if row.get("target") == name]
        target_trail_hits = [row for row in trail_hits if row.get("target") == name]
        rows.append({
            "target": name,
            "pairHex": pair_hex(int(target["x"]), int(target["y"])),
            "pointerTableHitCount": len(target_pointer_hits),
            "pointerTableTileHitCount": sum(
                1 for row in target_pointer_hits if row.get("field") == "tile_e8_ea"
            ),
            "staticBaseHitCount": len(target_static_hits),
            "trailRingHitCount": len(target_trail_hits),
            "imageHitCount": image_row.get("hitCountCapped", 0),
            "knownGlobalImageHitCount": len(image_pair_known_global_hits(image_hits, name)),
        })
    return rows


def coordinate_source_rejection(summary: dict[str, Any]) -> str:
    rows = summary.get("pairHitSummaryRows") or []
    start = next((row for row in rows if row.get("target") == "public-save-start"), {})
    reciprocal_rows = [row for row in rows if str(row.get("target", "")).startswith("reciprocal-")]
    reciprocal_object_or_trail_hits = sum(
        int(row.get("pointerTableTileHitCount") or 0) + int(row.get("trailRingHitCount") or 0)
        for row in reciprocal_rows
    )
    if (
        int(start.get("pointerTableTileHitCount") or 0) == 0
        and reciprocal_object_or_trail_hits == 0
        and (
            int(start.get("staticBaseHitCount") or 0) > 0
            or int(start.get("knownGlobalImageHitCount") or 0) > 0
        )
    ):
        return "save-camera-memory-only-no-route-object-coordinate-source"
    return "coordinate-source-review-required"


def known_global_label(static_va: int) -> str | None:
    ranges = [
        ("cameraTile", CAMERA_TILE_X, CAMERA_TILE_Y + 2),
        ("saveRuntimeBlock", SAVE_BLOCK, SAVE_BLOCK + 0x120),
        ("activeSlots", ACTIVE_SLOT_BASE, ACTIVE_SLOT_BASE + ACTIVE_SLOT_STRIDE * 3),
        ("partyTrailRing", ACTOR_HISTORY_INDEX_TABLE, ACTOR_HISTORY_DIRECTION_TABLE + ACTOR_HISTORY_SLOT_COUNT * ACTOR_HISTORY_STRIDE_BYTES),
        ("runtimeSlotBaseTable", RUNTIME_SLOT_BASE_TABLE, RUNTIME_SLOT_BASE_TABLE + 0x40),
        ("runtimeObjectTable", RUNTIME_OBJECT_TABLE, RUNTIME_OBJECT_TABLE + 0x40),
        ("selectionBuffer", SELECTION_BUFFER, SELECTION_BUFFER + 0x120),
    ]
    for name, start, end in ranges:
        if start <= static_va < end:
            return name
    return None


def run_scan(args: Namespace) -> dict[str, Any]:
    display = find_free_display()
    env = env_for(args.prefix, display)
    args.prefix.mkdir(parents=True, exist_ok=True)
    xvfb = subprocess.Popen(
        ["Xvfb", display, "-screen", "0", "1280x1024x24"],
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        text=True,
        errors="replace",
    )
    wine = None
    try:
        time.sleep(1)
        wine = subprocess.Popen(
            ["wine", "explorer", "/desktop=hwanse,640x480", "Hwanse2.exe"],
            cwd=ROOT,
            env=env,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            text=True,
            errors="replace",
        )
        time.sleep(args.startup_wait)
        pid = find_hwanse_pid()
        base = loaded_base(pid) if pid else None
        focus = focus_windows(env)
        prelude_rows = run_input_path_prelude(focus.get("focusedWindows") or [], env, pid, base) if pid and base else []
        snapshots = []
        if pid and base:
            snapshots.append({"phase": "beforeSequence", **snapshot_basic(pid, base)})
            for key in args.sequence:
                offset = KEY_OFFSETS[key]
                write = write_key_buffer(pid, base, offset, duration=args.hold)
                time.sleep(args.gap)
                snapshots.append({"phase": f"after:{key}", "write": write, **snapshot_basic(pid, base)})
            time.sleep(args.final_wait)
            snapshots.append({"phase": "final", **snapshot_basic(pid, base)})
            with (Path("/proc") / str(pid) / "mem").open("r+b", buffering=0) as mem:
                pointer_tables = scan_pointer_tables(mem, base)
                static_bases = scan_static_bases(mem, base)
                trail_ring = scan_trail_ring(mem, base)
                image_pair_hits = scan_image_pairs(mem, base)
        else:
            pointer_tables = []
            static_bases = []
            trail_ring = []
            image_pair_hits = []
    finally:
        subprocess.run(["wineserver", "-k"], cwd=ROOT, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=5)
        wine_output = ""
        if wine:
            try:
                wine_output, _ = wine.communicate(timeout=5)
            except subprocess.TimeoutExpired:
                wine.kill()
                wine_output, _ = wine.communicate(timeout=5)
        xvfb.terminate()
        try:
            xvfb_output, _ = xvfb.communicate(timeout=5)
        except subprocess.TimeoutExpired:
            xvfb.kill()
            xvfb_output, _ = xvfb.communicate(timeout=5)
    return {
        "display": display,
        "linuxPid": pid,
        "loadedBaseHex": hex32(base),
        **focus,
        "preludeRowCount": len(prelude_rows),
        "sequence": args.sequence,
        "snapshots": snapshots,
        "pointerTables": pointer_tables,
        "staticBases": static_bases,
        "trailRing": trail_ring,
        "imagePairHits": image_pair_hits,
        "startupOutput": truncate(wine_output or ""),
        "xvfbOutput": truncate(xvfb_output or ""),
    }


def classify(summary: dict[str, Any]) -> str:
    start_pair = pair_hex(5, 15)
    object_hits = []
    for table in summary.get("pointerTables") or []:
        for entry in table.get("entries") or []:
            for field in entry.get("fieldPairs") or []:
                if field.get("pairHex") == start_pair:
                    object_hits.append((table.get("name"), entry.get("slot"), field.get("name")))
    if object_hits:
        return "start-coordinate-found-in-pointer-table"
    image_hits = [
        hit
        for row in summary.get("imagePairHits") or []
        if row.get("pairHex") == start_pair
        for hit in row.get("hits") or []
    ]
    if image_hits:
        return "start-coordinate-found-in-image-memory"
    return "start-coordinate-not-found-outside-camera-snapshot"


def markdown(summary: dict[str, Any]) -> str:
    lines = [
        "# Runtime Predecessor Coordinate Source Scan",
        "",
        f"- sequence: `{','.join(summary.get('sequence') or [])}`",
        f"- loaded base: `{summary.get('loadedBaseHex')}`",
        f"- classification: `{summary.get('classification')}`",
        f"- coordinate-source rejection: `{summary.get('coordinateSourceRejectionClassification')}`",
        f"- promotion status: `{summary.get('promotionStatus')}`",
        f"- proof found: {summary.get('proofFound')}",
        f"- predecessor coordinate source proof found: {summary.get('predecessorCoordinateSourceProofFound')}",
        f"- failed predecessor coordinate source gates: `{','.join(summary.get('failedPredecessorCoordinateSourceGateIds') or [])}`",
        f"- missing evidence count: {len(summary.get('missingEvidence') or [])}",
        f"- evidence refs: {summary.get('evidenceRefCount')}",
        "",
        "## Missing Evidence",
        "",
        *[f"- {item}" for item in summary.get("missingEvidence") or []],
        "",
        "## Evidence Refs",
        "",
        "| path | fields |",
        "| --- | --- |",
        *[
            f"| `{row.get('path')}` | {', '.join(row.get('fields') or []) or '-'} |"
            for row in summary.get("evidenceRefs") or []
        ],
        "",
        "## Snapshots",
        "",
        "| phase | selected | selector | camera | active count | active order |",
        "| --- | --- | --- | --- | ---: | --- |",
    ]
    for row in summary.get("snapshots") or []:
        context = row.get("selectedPointerContext") or {}
        camera = row.get("cameraTile") or {}
        lines.append(
            f"| `{row.get('phase')}` | `{row.get('selectedPointerStaticHex')}` | "
            f"`{context.get('selector') or '-'}` | `{camera.get('x')},{camera.get('y')}` | "
            f"{row.get('activeOrderCount')} | `{','.join(str(value) for value in row.get('activeOrderBytes') or [])}` |"
        )
    lines.extend(["", "## Pointer Tables", ""])
    for table in summary.get("pointerTables") or []:
        lines.append(f"### {table.get('name')} `{table.get('staticVaHex')}`")
        lines.append("| slot | pointer static | field | x,y | plausible |")
        lines.append("| ---: | --- | --- | --- | --- |")
        for entry in table.get("entries") or []:
            for field in entry.get("fieldPairs") or []:
                if entry.get("nonZero") and (field.get("plausibleTile") or field.get("name") == "tile_e8_ea"):
                    lines.append(
                        f"| {entry.get('slot')} | `{entry.get('pointerStaticHex')}` | `{field.get('name')}` | "
                        f"`{field.get('x')},{field.get('y')}` | {field.get('plausibleTile')} |"
                    )
        lines.append("")
    lines.extend([
        "## Pair Hit Summary",
        "",
        "| target | pair | pointer tile hits | static hits | trail hits | image hits | known-global image hits |",
        "| --- | --- | ---: | ---: | ---: | ---: | ---: |",
    ])
    for row in summary.get("pairHitSummaryRows") or []:
        lines.append(
            f"| `{row.get('target')}` | `{row.get('pairHex')}` | "
            f"{row.get('pointerTableTileHitCount')} | {row.get('staticBaseHitCount')} | "
            f"{row.get('trailRingHitCount')} | {row.get('imageHitCount')} | "
            f"{row.get('knownGlobalImageHitCount')} |"
        )
    lines.append("")
    lines.extend(["## Image Pair Hits", "", "| target | pair | hits | first labels |", "| --- | --- | ---: | --- |"])
    for row in summary.get("imagePairHits") or []:
        labels = ",".join(str(hit.get("nearKnownGlobal") or hit.get("staticVaHex")) for hit in (row.get("hits") or [])[:8])
        lines.append(f"| `{row.get('name')}` | `{row.get('pairHex')}` | {row.get('hitCountCapped')} | `{labels or '-'}` |")
    lines.extend(["", "## Remaining Proofs", ""])
    lines.extend(f"- {item}" for item in summary.get("remainingProofs") or [])
    return "\n".join(lines) + "\n"


def write_outputs(summary: dict[str, Any], out_dir: Path = OUT) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / f"{OUTPUT_PREFIX}.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )


def main() -> None:
    args = Namespace(
        startup_wait=18.0,
        hold=0.7,
        gap=0.25,
        final_wait=1.0,
        sequence=SCAN_SEQUENCE,
        prefix=DEFAULT_PREFIX,
    )
    case_aliases = prepare_case_aliases(ROOT, True)
    temporary_saves = []
    directory_info = {"createdDirectory": False}
    try:
        temporary_saves, directory_info = prepare_temporary_saves([(1, SOURCE_SAVE)])
        summary = run_scan(args)
    finally:
        cleanup_temporary_saves(temporary_saves, directory_info["createdDirectory"])
        cleanup_case_aliases(ROOT, case_aliases)
    summary["objective"] = "short runtime coordinate-source scan after loading public predecessor save"
    summary["sourceSave"] = str(SOURCE_SAVE.relative_to(ROOT))
    summary["scanTargets"] = PAIR_SCAN_TARGETS
    summary["classification"] = classify(summary)
    summary["pointerTablePairHits"] = scan_pointer_pair_hits(summary.get("pointerTables") or [])
    summary["staticBasePairHits"] = scan_static_base_pair_hits(summary.get("staticBases") or [])
    summary["trailRingPairHits"] = scan_trail_pair_hits(summary.get("trailRing") or [])
    summary["pairHitSummaryRows"] = pair_hit_summary_rows(summary)
    start_row = next(
        (row for row in summary["pairHitSummaryRows"] if row.get("target") == "public-save-start"),
        {},
    )
    observed_trail_row = next(
        (row for row in summary["pairHitSummaryRows"] if row.get("target") == "observed-trail"),
        {},
    )
    reciprocal_rows = [
        row
        for row in summary["pairHitSummaryRows"]
        if str(row.get("target", "")).startswith("reciprocal-")
    ]
    summary["publicSaveStartPointerTableTileHitCount"] = start_row.get("pointerTableTileHitCount")
    summary["publicSaveStartStaticBaseHitCount"] = start_row.get("staticBaseHitCount")
    summary["publicSaveStartTrailRingHitCount"] = start_row.get("trailRingHitCount")
    summary["publicSaveStartImageHitCount"] = start_row.get("imageHitCount")
    summary["publicSaveStartKnownGlobalImageHitCount"] = start_row.get("knownGlobalImageHitCount")
    summary["observedTrailPointerTableTileHitCount"] = observed_trail_row.get("pointerTableTileHitCount")
    summary["observedTrailStaticBaseHitCount"] = observed_trail_row.get("staticBaseHitCount")
    summary["observedTrailTrailRingHitCount"] = observed_trail_row.get("trailRingHitCount")
    summary["observedTrailImageHitCount"] = observed_trail_row.get("imageHitCount")
    summary["observedTrailKnownGlobalImageHitCount"] = observed_trail_row.get("knownGlobalImageHitCount")
    summary["reciprocalPointerTableTileHitCount"] = sum(
        int(row.get("pointerTableTileHitCount") or 0) for row in reciprocal_rows
    )
    summary["reciprocalStaticBaseHitCount"] = sum(
        int(row.get("staticBaseHitCount") or 0) for row in reciprocal_rows
    )
    summary["reciprocalTrailRingHitCount"] = sum(
        int(row.get("trailRingHitCount") or 0) for row in reciprocal_rows
    )
    summary["reciprocalImageHitCount"] = sum(
        int(row.get("imageHitCount") or 0) for row in reciprocal_rows
    )
    summary["coordinateSourceRejectionClassification"] = coordinate_source_rejection(summary)
    summary["remainingProofs"] = [
        "find a runtime object or actor tile source for the public predecessor start coordinate",
        "observe a reciprocal target coordinate in an actor/trail source, not only camera or image memory",
        "connect the coordinate source to selector 2:0/current root execution before promotion",
    ]
    summary["promotionStatus"] = "diagnostic-only"
    summary["proofFound"] = False
    summary["predecessorCoordinateSourceProofFound"] = False
    summary["failedPredecessorCoordinateSourceGateIds"] = (
        FAILED_PREDECESSOR_COORDINATE_SOURCE_GATE_IDS
    )
    summary["missingEvidence"] = PREDECESSOR_COORDINATE_SOURCE_MISSING_EVIDENCE
    summary["evidenceRefs"] = PREDECESSOR_COORDINATE_SOURCE_EVIDENCE_REFS
    summary["evidenceRefCount"] = len(PREDECESSOR_COORDINATE_SOURCE_EVIDENCE_REFS)
    summary["conclusion"] = (
        "This scan locates where public predecessor coordinate pairs live after the original load-menu path. "
        "It is diagnostic source-finding for coordinate watches, not normal route promotion."
    )
    write_outputs(summary, OUT)
    print(f"wrote predecessor coordinate source scan -> {OUT / (OUTPUT_PREFIX + '.json')}")


if __name__ == "__main__":
    main()
