#!/usr/bin/env python3
"""Rank map1_01a geometry exit candidates for the current route blocker."""
from __future__ import annotations

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


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"
SOURCE = "map1_01a"
BLOCKED_TARGET = "map2_02d"
RETURN_TARGET = "map1_02b"
FAILED_EXIT_TARGET_RANKING_GATE_IDS = [
    "strict-source-hotspot",
    "target-map-control-flow",
    "strict-or-confirmed-outgoing-transition",
    "selected-root-or-real-save-proof",
]
EXIT_TARGET_RANKING_MISSING_EVIDENCE = [
    "strict map1_01a source coordinate or hotspot",
    "runtime/control-flow proof that the selected exit targets map2_02d",
    "strict or confirmed outgoing transition for map1_01a -> map2_02d rather than selector-adjacency-only",
    "real selector 2:0 savedata or selected-root execution trace",
]
EXIT_TARGET_RANKING_EVIDENCE_REFS = [
    {
        "path": "out/map_exit_candidates.json",
        "description": "geometry exit candidates, target hints, and reciprocal exit projections",
    },
    {
        "path": "out/map_exit_coordinate_refs.json",
        "description": "packed-coordinate diagnostics for map1_01a -> map2_02d candidates",
    },
    {
        "path": "out/route_assist_frontier.json",
        "description": "routeAssist trial URLs and source/target spawn probes for candidate exits",
    },
    {
        "path": "data/transition_reviews.json",
        "description": "confirmed incoming reviews separated from outgoing proof requirements",
    },
    {
        "path": "out/save_selector_scene_adjacency_index.json",
        "description": "selector adjacency rows and strict/confirmed outgoing backing flags",
    },
]


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


def map_exit_row(rows: list[dict], name: str) -> dict:
    return next((row for row in rows if row.get("map") == name), {})


def sample(candidate: dict) -> dict:
    return candidate.get("sample") or {}


def candidate_key(candidate: dict) -> tuple[str | None, int | None, int | None]:
    point = sample(candidate)
    return candidate.get("side"), point.get("x"), point.get("y")


def find_hint(candidate: dict, target: str) -> dict | None:
    for hint in candidate.get("targetHints") or []:
        if hint.get("target") == target:
            return hint
    return None


def coordinate_status(rows: list[dict], side: str, x: int, y: int, target: str) -> dict:
    for row in rows:
        if (
            row.get("source") == SOURCE
            and row.get("target") == target
            and row.get("side") == side
            and row.get("x") == x
            and row.get("y") == y
        ):
            scans = row.get("packedScans") or {}
            return {
                "status": row.get("status"),
                "promotable": row.get("promotable"),
                "xyTotal": (scans.get("xy") or {}).get("total"),
                "yxTotal": (scans.get("yx") or {}).get("total"),
            }
    return {
        "status": "not-scanned",
        "promotable": False,
        "xyTotal": None,
        "yxTotal": None,
    }


def reciprocal_exit(target_map: dict, source_candidate: dict, target_hint: dict | None) -> dict | None:
    if not target_hint:
        return None
    source_point = sample(source_candidate)
    for candidate in target_map.get("exitCandidates") or []:
        target_point = sample(candidate)
        if (
            candidate.get("side") == target_hint.get("side")
            and target_point.get("x") == target_hint.get("x")
            and target_point.get("y") == target_hint.get("y")
        ):
            reverse_hint = find_hint(candidate, SOURCE)
            if (
                reverse_hint
                and reverse_hint.get("side") == source_candidate.get("side")
                and reverse_hint.get("x") == source_point.get("x")
                and reverse_hint.get("y") == source_point.get("y")
            ):
                return {
                    "side": candidate.get("side"),
                    "x": target_point.get("x"),
                    "y": target_point.get("y"),
                    "edgeDistance": candidate.get("edgeDistance"),
                    "autoTrigger": candidate.get("edgeDistance") == 0,
                    "reverseProjectionDelta": reverse_hint.get("projectionDelta"),
                }
    return None


def route_assist_candidate(route_assist_rows: list[dict], side: str, x: int, y: int) -> dict | None:
    row = next(
        (
            item for item in route_assist_rows
            if item.get("source") == SOURCE and item.get("target") == BLOCKED_TARGET
        ),
        {},
    )
    for index, candidate in enumerate(row.get("candidateExits") or []):
        point = candidate.get("sample") or {}
        if candidate.get("side") == side and point.get("x") == x and point.get("y") == y:
            return {
                "order": index + 1,
                "autoTrigger": candidate.get("autoTrigger"),
                "sourceStandable": candidate.get("sourceStandable"),
                "routeAssistUrl": candidate.get("routeAssistUrl"),
                "targetSpawnUrl": candidate.get("targetSpawnUrl"),
            }
    return None


def confirmed_incoming_reviews(reviews: dict) -> list[dict]:
    rows = []
    for review in reviews.values():
        if review.get("target") != SOURCE or review.get("state") != "confirmed":
            continue
        rows.append({
            "source": review.get("source"),
            "target": review.get("target"),
            "x": review.get("x"),
            "y": review.get("y"),
            "spawnX": review.get("spawnX"),
            "spawnY": review.get("spawnY"),
            "recordVaHex": review.get("recordVaHex"),
        })
    return sorted(rows, key=lambda row: (row.get("source") or "", row.get("x") or -1, row.get("y") or -1))


def selector_outgoing_rows(scene_adjacency_index: dict) -> list[dict]:
    rows = []
    for row in scene_adjacency_index.get("pairRows") or []:
        if row.get("source") != SOURCE:
            continue
        rows.append({
            "target": row.get("target"),
            "occurrenceCount": row.get("occurrenceCount"),
            "selectors": row.get("selectors") or [],
            "leafPointers": row.get("leafPointers") or [],
            "sourceRecords": row.get("sourceRecords") or [],
            "targetRecords": row.get("targetRecords") or [],
            "strictEventBacked": row.get("strictEventBacked") is True,
            "confirmedReviewBacked": row.get("confirmedReviewBacked") is True,
            "selectorAdjacencyOnly": row.get("selectorAdjacencyOnly") is True,
            "classification": row.get("classification"),
        })
    return sorted(rows, key=lambda row: (row.get("target") or "", row.get("occurrenceCount") or 0))


def build_summary(
    map_exit_candidates: list[dict] | None = None,
    map_exit_coordinate_refs: dict | None = None,
    route_assist_frontier: list[dict] | None = None,
    transition_reviews: dict | None = None,
    scene_adjacency_index: dict | None = None,
) -> dict:
    map_exit_candidates = map_exit_candidates if map_exit_candidates is not None else load_json(
        OUT / "map_exit_candidates.json",
        [],
    )
    map_exit_coordinate_refs = map_exit_coordinate_refs if map_exit_coordinate_refs is not None else load_json(
        OUT / "map_exit_coordinate_refs.json",
        {},
    )
    route_assist_frontier = route_assist_frontier if route_assist_frontier is not None else load_json(
        OUT / "route_assist_frontier.json",
        [],
    )
    transition_reviews = transition_reviews if transition_reviews is not None else load_json(
        ROOT / "data" / "transition_reviews.json",
        {},
    )
    scene_adjacency_index = scene_adjacency_index if scene_adjacency_index is not None else load_json(
        OUT / "save_selector_scene_adjacency_index.json",
        {},
    )
    source_map = map_exit_row(map_exit_candidates, SOURCE)
    target_map = map_exit_row(map_exit_candidates, BLOCKED_TARGET)
    coord_rows = map_exit_coordinate_refs.get("rows") or []
    exits = []
    for candidate in source_map.get("exitCandidates") or []:
        side, x, y = candidate_key(candidate)
        if not isinstance(x, int) or not isinstance(y, int):
            continue
        targets = []
        for target in sorted(candidate.get("selectorTargetCandidates") or []):
            hint = find_hint(candidate, target)
            target_row = map_exit_row(map_exit_candidates, target)
            targets.append({
                "target": target,
                "blockedRouteTarget": target == BLOCKED_TARGET,
                "targetHint": {
                    "side": hint.get("side"),
                    "x": hint.get("x"),
                    "y": hint.get("y"),
                    "autoTrigger": hint.get("autoTrigger"),
                    "edgeDistance": hint.get("edgeDistance"),
                    "projectionDelta": hint.get("projectionDelta"),
                    "rank": hint.get("rank"),
                } if hint else None,
                "reciprocalExit": reciprocal_exit(target_row, candidate, hint),
                "coordinateEvidence": coordinate_status(coord_rows, side, x, y, target)
                if target == BLOCKED_TARGET
                else None,
            })
        assist = route_assist_candidate(route_assist_frontier, side, x, y)
        exits.append({
            "side": side,
            "x": x,
            "y": y,
            "edgeDistance": candidate.get("edgeDistance"),
            "autoTrigger": candidate.get("edgeDistance") == 0,
            "standable": sample(candidate).get("standable"),
            "layer0": sample(candidate).get("layer0"),
            "layer1": sample(candidate).get("layer1"),
            "targetCount": len(targets),
            "targets": targets,
            "routeAssist": assist,
            "reviewUrl": candidate.get("reviewUrl"),
            "trialUrl": candidate.get("trialUrl"),
        })
    auto_blocked = [
        row for row in exits
        if row.get("autoTrigger")
        and any(target.get("target") == BLOCKED_TARGET for target in row.get("targets") or [])
    ]
    blocked_target_exits = [
        row for row in exits
        if any(target.get("target") == BLOCKED_TARGET for target in row.get("targets") or [])
    ]
    return_overlap_exits = [
        row for row in blocked_target_exits
        if any(target.get("target") == RETURN_TARGET for target in row.get("targets") or [])
    ]
    blocked_target_rows = [
        target
        for row in blocked_target_exits
        for target in row.get("targets") or []
        if target.get("target") == BLOCKED_TARGET
    ]
    blocked_target_reciprocal_count = sum(
        1 for target in blocked_target_rows if target.get("reciprocalExit")
    )
    blocked_target_coordinate_like_count = sum(
        1
        for target in blocked_target_rows
        if (target.get("coordinateEvidence") or {}).get("status")
        == "coordinate-like-hit-without-table-text-ref"
    )
    coordinate_promotable_count = sum(
        1 for row in exits
        for target in row.get("targets") or []
        if (target.get("coordinateEvidence") or {}).get("promotable") is True
    )
    incoming = confirmed_incoming_reviews(transition_reviews)
    selector_outgoing = selector_outgoing_rows(scene_adjacency_index)
    selector_only_outgoing = [row for row in selector_outgoing if row.get("selectorAdjacencyOnly")]
    blocked_selector = next((row for row in selector_outgoing if row.get("target") == BLOCKED_TARGET), {})
    return_selector = next((row for row in selector_outgoing if row.get("target") == RETURN_TARGET), {})
    conclusion = (
        f"{SOURCE} has {len(exits)} geometry exits and {len(auto_blocked)} auto edge candidates for "
        f"{BLOCKED_TARGET}. Reciprocal target hints make the top and bottom exits useful routeAssist tests, "
        "but every candidate remains geometry-only: coordinate scans do not provide a promotable strict source "
        "hotspot, and the confirmed incoming map1_02b event only proves entry into map1_01a, not an outgoing "
        "map1_01a -> map2_02d transition. The scene-list outgoing candidates from map1_01a are also "
        "selector-adjacency-only, so there is no alternate strict or confirmed outgoing transition to promote."
    )
    return {
        "source": SOURCE,
        "blockedTarget": BLOCKED_TARGET,
        "returnTarget": RETURN_TARGET,
        "sourceMapSize": {
            "width": source_map.get("width"),
            "height": source_map.get("height"),
        },
        "selectorTargetCandidates": source_map.get("selectorTargetCandidates") or [],
        "blockedTargetCandidates": source_map.get("blockedTargetCandidates") or [],
        "exitCount": len(exits),
        "blockedTargetExitCount": len(blocked_target_exits),
        "autoBlockedTargetExitCount": len(auto_blocked),
        "blockedTargetReturnOverlapExitCount": len(return_overlap_exits),
        "blockedTargetReciprocalExitCount": blocked_target_reciprocal_count,
        "blockedTargetCoordinateLikeHitCount": blocked_target_coordinate_like_count,
        "coordinatePromotableCount": coordinate_promotable_count,
        "selectorOutgoingCandidateCount": len(selector_outgoing),
        "selectorOutgoingTargets": [row.get("target") for row in selector_outgoing],
        "selectorOutgoingStrictBackedCount": sum(1 for row in selector_outgoing if row.get("strictEventBacked")),
        "selectorOutgoingConfirmedBackedCount": sum(
            1 for row in selector_outgoing if row.get("confirmedReviewBacked")
        ),
        "selectorOutgoingOnlyCount": len(selector_only_outgoing),
        "selectorOutgoingCandidates": selector_outgoing,
        "blockedTargetSelectorOccurrenceCount": blocked_selector.get("occurrenceCount", 0),
        "returnTargetSelectorOccurrenceCount": return_selector.get("occurrenceCount", 0),
        "confirmedIncomingReviews": incoming,
        "confirmedIncomingCount": len(incoming),
        "exits": exits,
        "proofFound": False,
        "exitTargetRankingProofFound": False,
        "failedExitTargetRankingGateIds": FAILED_EXIT_TARGET_RANKING_GATE_IDS,
        "missingEvidence": EXIT_TARGET_RANKING_MISSING_EVIDENCE,
        "evidenceRefs": EXIT_TARGET_RANKING_EVIDENCE_REFS,
        "evidenceRefCount": len(EXIT_TARGET_RANKING_EVIDENCE_REFS),
        "promotionStatus": "blocked",
        "remainingProofs": [
            "strict map1_01a source coordinate or hotspot",
            "runtime/control-flow proof that the selected exit targets map2_02d",
            "real selector 2:0 savedata or equivalent runtime trace",
        ],
        "conclusion": conclusion,
    }


def target_text(target: dict) -> str:
    hint = target.get("targetHint") or {}
    reciprocal = target.get("reciprocalExit") or {}
    coord = target.get("coordinateEvidence") or {}
    hint_text = (
        f"{hint.get('side')} {hint.get('x')},{hint.get('y')} "
        f"auto={hint.get('autoTrigger')} proj={hint.get('projectionDelta')}"
        if hint
        else "-"
    )
    reciprocal_text = (
        f"{reciprocal.get('side')} {reciprocal.get('x')},{reciprocal.get('y')} "
        f"reverseProj={reciprocal.get('reverseProjectionDelta')}"
        if reciprocal
        else "-"
    )
    coord_text = (
        f"{coord.get('status')} xy={coord.get('xyTotal')} yx={coord.get('yxTotal')}"
        if coord
        else "-"
    )
    return f"{target.get('target')} hint {hint_text}; reciprocal {reciprocal_text}; coord {coord_text}"


def markdown(summary: dict) -> str:
    lines = [
        "# map1_01a Exit Target Ranking",
        "",
        f"- source: `{summary['source']}` size {summary['sourceMapSize'].get('width')}x{summary['sourceMapSize'].get('height')}",
        f"- blocked target: `{summary['blockedTarget']}`",
        f"- selector targets: {', '.join(f'`{target}`' for target in summary['selectorTargetCandidates'])}",
        f"- selector outgoing candidates: {summary['selectorOutgoingCandidateCount']} "
        f"(selector-only {summary['selectorOutgoingOnlyCount']}, strict-backed {summary['selectorOutgoingStrictBackedCount']}, "
        f"confirmed-backed {summary['selectorOutgoingConfirmedBackedCount']})",
        f"- exit candidates: {summary['exitCount']} ({summary['blockedTargetExitCount']} mention blocked target, "
        f"{summary['autoBlockedTargetExitCount']} auto for blocked target, "
        f"{summary['blockedTargetReturnOverlapExitCount']} also mention return target)",
        f"- blocked-target reciprocal/coordinate-like hits: {summary['blockedTargetReciprocalExitCount']} / "
        f"{summary['blockedTargetCoordinateLikeHitCount']}",
        f"- coordinate-promotable exits: {summary['coordinatePromotableCount']}",
        f"- confirmed incoming reviews: {summary['confirmedIncomingCount']}",
        f"- proof found: `{summary['proofFound']}`",
        f"- exit target ranking proof found: `{summary['exitTargetRankingProofFound']}`",
        f"- promotion status: {summary['promotionStatus']}",
        "",
        summary["conclusion"],
        "",
        "## Failed Gates",
        "",
    ]
    lines.extend(f"- `{item}`" for item in summary["failedExitTargetRankingGateIds"])
    lines.extend([
        "",
        "## Missing Evidence",
        "",
    ])
    lines.extend(f"- {item}" for item in summary["missingEvidence"])
    lines.extend([
        "",
        "## Evidence Refs",
        "",
    ])
    lines.extend(
        f"- `{row['path']}`: {row['description']}"
        for row in summary["evidenceRefs"]
    )
    lines.extend([
        "",
        "## Confirmed Incoming Context",
        "",
    ])
    for row in summary["confirmedIncomingReviews"]:
        lines.append(
            f"- `{row['source']}` `{row['x']},{row['y']}` -> `{row['target']}` "
            f"spawn `{row.get('spawnX')},{row.get('spawnY')}` record `{row.get('recordVaHex')}`"
        )
    if not summary["confirmedIncomingReviews"]:
        lines.append("- none")
    lines.extend([
        "",
        "## Selector Outgoing Candidates",
        "",
        "| target | occurrences | selectors | records | strict | confirmed | class |",
        "| --- | ---: | --- | --- | --- | --- | --- |",
    ])
    for row in summary["selectorOutgoingCandidates"]:
        records = ",".join(row.get("sourceRecords") or []) + " -> " + ",".join(row.get("targetRecords") or [])
        lines.append(
            f"| `{row.get('target')}` | {row.get('occurrenceCount')} | "
            f"{', '.join(f'`{value}`' for value in row.get('selectors') or []) or '-'} | "
            f"{records} | {row.get('strictEventBacked')} | {row.get('confirmedReviewBacked')} | "
            f"{row.get('classification')} |"
        )
    if not summary["selectorOutgoingCandidates"]:
        lines.append("| - | 0 | - | - | - | - | - |")
    lines.extend([
        "",
        "## Exit Candidates",
        "",
        "| side | tile | auto | assist | targets | review |",
        "| --- | --- | --- | --- | --- | --- |",
    ])
    for row in summary["exits"]:
        assist = row.get("routeAssist") or {}
        assist_text = (
            f"order {assist.get('order')} auto={assist.get('autoTrigger')} standable={assist.get('sourceStandable')}"
            if assist
            else "-"
        )
        targets = "<br>".join(target_text(target) for target in row.get("targets") or [])
        lines.append(
            f"| {row['side']} | `{row['x']},{row['y']}` l0={row.get('layer0')} l1={row.get('layer1')} | "
            f"{row['autoTrigger']} | {assist_text} | {targets} | [open]({row.get('reviewUrl')}) |"
        )
    lines.extend(["", "## Remaining Proofs", ""])
    lines.extend(f"- {item}" for item in summary["remainingProofs"])
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    rows = []
    for row in summary["exits"]:
        assist = row.get("routeAssist") or {}
        assist_text = (
            f"order {assist.get('order')} auto={assist.get('autoTrigger')} standable={assist.get('sourceStandable')}"
            if assist
            else "-"
        )
        targets = "<br>".join(html.escape(target_text(target)) for target in row.get("targets") or [])
        rows.append(
            "<tr>"
            f"<td>{html.escape(str(row['side']))}</td>"
            f"<td><code>{row['x']},{row['y']}</code><br>l0={row.get('layer0')} l1={row.get('layer1')}</td>"
            f"<td>{row['autoTrigger']}</td>"
            f"<td>{html.escape(assist_text)}</td>"
            f"<td>{targets}</td>"
            f"<td><a href=\"{html.escape(row.get('reviewUrl') or '#')}\">open</a></td>"
            "</tr>"
        )
    incoming = "".join(
        f"<li><code>{html.escape(str(row['source']))}</code> "
        f"<code>{row['x']},{row['y']}</code> -> <code>{html.escape(str(row['target']))}</code> "
        f"spawn <code>{row.get('spawnX')},{row.get('spawnY')}</code> record <code>{html.escape(str(row.get('recordVaHex')))}</code></li>"
        for row in summary["confirmedIncomingReviews"]
    ) or "<li>none</li>"
    selector_rows = []
    for row in summary["selectorOutgoingCandidates"]:
        records = ",".join(row.get("sourceRecords") or []) + " -> " + ",".join(row.get("targetRecords") or [])
        selectors = ", ".join(row.get("selectors") or []) or "-"
        selector_rows.append(
            "<tr>"
            f"<td><code>{html.escape(str(row.get('target')))}</code></td>"
            f"<td>{row.get('occurrenceCount')}</td>"
            f"<td><code>{html.escape(selectors)}</code></td>"
            f"<td><code>{html.escape(records)}</code></td>"
            f"<td>{row.get('strictEventBacked')}</td>"
            f"<td>{row.get('confirmedReviewBacked')}</td>"
            f"<td>{html.escape(str(row.get('classification')))}</td>"
            "</tr>"
        )
    proofs = "".join(f"<li>{html.escape(item)}</li>" for item in summary["remainingProofs"])
    failed_gates = "".join(
        f"<li><code>{html.escape(item)}</code></li>"
        for item in summary["failedExitTargetRankingGateIds"]
    )
    missing_evidence = "".join(
        f"<li>{html.escape(item)}</li>"
        for item in summary["missingEvidence"]
    )
    evidence_refs = "".join(
        f"<li><code>{html.escape(row['path'])}</code>: {html.escape(row['description'])}</li>"
        for row in summary["evidenceRefs"]
    )
    return "\n".join([
        "<!doctype html><meta charset=\"utf-8\"><title>map1_01a Exit Target Ranking</title>",
        "<style>body{font-family:system-ui,sans-serif;background:#111;color:#eee;max-width:1200px;margin:24px auto}table{border-collapse:collapse;width:100%}td,th{border:1px solid #444;padding:6px 8px;vertical-align:top}code{color:#9bd4ff}a{color:#9bd4ff}</style>",
        "<h1>map1_01a Exit Target Ranking</h1>",
        f"<p>blocked target <code>{summary['blockedTarget']}</code>; exits {summary['exitCount']}; blocked-target exits {summary['blockedTargetExitCount']}; auto exits for blocked target {summary['autoBlockedTargetExitCount']}; blocked-target exits also naming return target {summary['blockedTargetReturnOverlapExitCount']}; reciprocal blocked-target hints {summary['blockedTargetReciprocalExitCount']}; coordinate-like blocked-target hits {summary['blockedTargetCoordinateLikeHitCount']}; coordinate-promotable exits {summary['coordinatePromotableCount']}; selector outgoing candidates {summary['selectorOutgoingCandidateCount']} / selector-only {summary['selectorOutgoingOnlyCount']}; proof found <code>{summary['proofFound']}</code>; promotion status <code>{summary['promotionStatus']}</code>.</p>",
        f"<p>{html.escape(summary['conclusion'])}</p>",
        "<h2>Failed Gates</h2>",
        f"<ul>{failed_gates}</ul>",
        "<h2>Missing Evidence</h2>",
        f"<ul>{missing_evidence}</ul>",
        "<h2>Evidence Refs</h2>",
        f"<ul>{evidence_refs}</ul>",
        "<h2>Confirmed Incoming Context</h2>",
        f"<ul>{incoming}</ul>",
        "<h2>Selector Outgoing Candidates</h2>",
        "<table><thead><tr><th>target</th><th>occurrences</th><th>selectors</th><th>records</th><th>strict</th><th>confirmed</th><th>class</th></tr></thead><tbody>",
        "\n".join(selector_rows) or '<tr><td colspan="7">No selector outgoing candidates.</td></tr>',
        "</tbody></table>",
        "<h2>Exit Candidates</h2>",
        "<table><thead><tr><th>side</th><th>tile</th><th>auto</th><th>assist</th><th>targets</th><th>review</th></tr></thead><tbody>",
        "\n".join(rows),
        "</tbody></table>",
        "<h2>Remaining Proofs</h2>",
        f"<ul>{proofs}</ul>",
    ])


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


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--map-exits", type=Path, default=OUT / "map_exit_candidates.json")
    parser.add_argument("--coordinate-refs", type=Path, default=OUT / "map_exit_coordinate_refs.json")
    parser.add_argument("--route-assist", type=Path, default=OUT / "route_assist_frontier.json")
    parser.add_argument("--transition-reviews", type=Path, default=ROOT / "data" / "transition_reviews.json")
    parser.add_argument("--scene-adjacency", type=Path, default=OUT / "save_selector_scene_adjacency_index.json")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    args = parser.parse_args()
    summary = build_summary(
        load_json(args.map_exits, []),
        load_json(args.coordinate_refs, {}),
        load_json(args.route_assist, []),
        load_json(args.transition_reviews, {}),
        load_json(args.scene_adjacency, {}),
    )
    write_outputs(summary, args.out_dir)
    print(f"wrote map1_01a exit target ranking -> {args.out_dir / 'map1_01a_exit_target_ranking.json'}")


if __name__ == "__main__":
    main()
