#!/usr/bin/env python3
"""Summarize reverse-transition candidates for confirmed map entries."""
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"


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


def point_text(point: dict | None) -> str:
    if not point:
        return "-"
    return f"{point.get('x')},{point.get('y')}"


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 hint_for(candidate: dict, target: str) -> dict | None:
    for hint in candidate.get("targetHints") or []:
        if hint.get("target") == target:
            return hint
    return None


def candidate_at(row: dict, side: str | None, x: int | None, y: int | None) -> dict | None:
    if not side or not isinstance(x, int) or not isinstance(y, int):
        return None
    for candidate in row.get("exitCandidates") or []:
        point = sample(candidate)
        if candidate.get("side") == side and point.get("x") == x and point.get("y") == y:
            return candidate
    return None


def selector_records(
    save_selector_scene_links: dict[str, dict],
    source: str,
    target: str,
) -> list[dict]:
    entry = save_selector_scene_links.get(source) or {}
    return [
        record
        for record in entry.get("records") or []
        if target in (record.get("targets") or record.get("fieldMaps") or [])
    ]


def strict_event_records(event_transitions: list[dict], source: str, target: str) -> list[dict]:
    return [
        row
        for row in event_transitions
        if row.get("map") == source and target in (row.get("targets") or [])
    ]


def confirmed_reviews(reviews: dict[str, dict], source: str, target: str) -> list[dict]:
    return [
        review
        for review in reviews.values()
        if review.get("source") == source
        and review.get("target") == target
        and review.get("state") == "confirmed"
    ]


def reverse_exit_candidates(map_exit_candidates: list[dict], source: str, target: str) -> list[dict]:
    source_row = map_exit_row(map_exit_candidates, source)
    target_row = map_exit_row(map_exit_candidates, target)
    rows = []
    for candidate in source_row.get("exitCandidates") or []:
        point = sample(candidate)
        hint = hint_for(candidate, target)
        if not hint:
            continue
        hinted_candidate = candidate_at(target_row, hint.get("side"), hint.get("x"), hint.get("y"))
        reverse_hint = hint_for(hinted_candidate or {}, source) if hinted_candidate else None
        reciprocal = False
        if reverse_hint:
            reciprocal = (
                reverse_hint.get("side") == candidate.get("side")
                and reverse_hint.get("x") == point.get("x")
                and reverse_hint.get("y") == point.get("y")
            )
        rows.append({
            "side": candidate.get("side"),
            "x": point.get("x"),
            "y": point.get("y"),
            "standable": point.get("standable"),
            "edgeDistance": candidate.get("edgeDistance"),
            "autoTrigger": candidate.get("edgeDistance") == 0,
            "targetHint": {
                "side": hint.get("side"),
                "x": hint.get("x"),
                "y": hint.get("y"),
                "standable": hint.get("standable"),
                "edgeDistance": hint.get("edgeDistance"),
                "autoTrigger": hint.get("autoTrigger"),
                "projectionDelta": hint.get("projectionDelta"),
                "sideMatch": hint.get("sideMatch"),
            },
            "reciprocalHint": reciprocal,
            "reviewUrl": candidate.get("reviewUrl"),
            "trialUrl": candidate.get("trialUrl"),
        })
    rows.sort(key=lambda row: (
        not row.get("reciprocalHint"),
        not row.get("autoTrigger"),
        row.get("edgeDistance") if row.get("edgeDistance") is not None else 999,
        row.get("side") or "",
        row.get("y") if row.get("y") is not None else 999,
        row.get("x") if row.get("x") is not None else 999,
    ))
    return rows


def classify(reverse_strict: list[dict], reverse_confirmed: list[dict], selector_rows: list[dict], exits: list[dict]) -> str:
    if reverse_confirmed:
        return "confirmed"
    if reverse_strict:
        return "strict-event-unreviewed"
    if selector_rows and exits:
        return "selector-and-geometry-candidate"
    if selector_rows:
        return "selector-only-candidate"
    if exits:
        return "geometry-only-candidate"
    return "no-reverse-evidence"


def build_summary(
    playable_progress: dict | None = None,
    transition_reviews: dict | None = None,
    event_transitions: list[dict] | None = None,
    save_selector_scene_links: dict[str, dict] | None = None,
    map_exit_candidates: list[dict] | None = None,
) -> dict:
    playable_progress = playable_progress if playable_progress is not None else load_json(
        OUT / "playable_progress.json",
        {},
    )
    transition_reviews = transition_reviews if transition_reviews is not None else load_json(
        ROOT / "data" / "transition_reviews.json",
        {},
    )
    event_transitions = event_transitions if event_transitions is not None else load_json(
        OUT / "event_transitions.json",
        [],
    )
    save_selector_scene_links = save_selector_scene_links if save_selector_scene_links is not None else load_json(
        OUT / "save_selector_scene_links.json",
        {},
    )
    map_exit_candidates = map_exit_candidates if map_exit_candidates is not None else load_json(
        OUT / "map_exit_candidates.json",
        [],
    )
    rows = []
    for edge in playable_progress.get("confirmedEdges") or []:
        source = edge.get("source")
        target = edge.get("target")
        if not isinstance(source, str) or not isinstance(target, str):
            continue
        reverse_source = target
        reverse_target = source
        selector_rows = selector_records(save_selector_scene_links, reverse_source, reverse_target)
        exits = reverse_exit_candidates(map_exit_candidates, reverse_source, reverse_target)
        reverse_strict = strict_event_records(event_transitions or [], reverse_source, reverse_target)
        reverse_confirmed = confirmed_reviews(transition_reviews or {}, reverse_source, reverse_target)
        selectors = sorted({record.get("selector") for record in selector_rows if record.get("selector")})
        leaf_pointers = sorted({record.get("leafPointerHex") for record in selector_rows if record.get("leafPointerHex")})
        entry_points = edge.get("points") or []
        rows.append({
            "entry": {
                "source": source,
                "target": target,
                "points": entry_points,
                "spawn": (
                    {"x": entry_points[0].get("spawnX"), "y": entry_points[0].get("spawnY")}
                    if entry_points
                    else None
                ),
            },
            "reverse": {
                "source": reverse_source,
                "target": reverse_target,
                "classification": classify(reverse_strict, reverse_confirmed, selector_rows, exits),
                "promotionStatus": "confirmed" if reverse_confirmed else "manual-review-only",
                "selectorRecordCount": len(selector_rows),
                "selectors": selectors,
                "leafPointers": leaf_pointers,
                "strictEventRecordCount": len(reverse_strict),
                "confirmedReviewCount": len(reverse_confirmed),
                "exitCandidateCount": len(exits),
                "reciprocalExitCandidateCount": sum(1 for row in exits if row.get("reciprocalHint")),
                "autoExitCandidateCount": sum(1 for row in exits if row.get("autoTrigger")),
                "exitCandidates": exits,
            },
        })
    manual = sum(1 for row in rows if (row.get("reverse") or {}).get("promotionStatus") != "confirmed")
    return {
        "objective": "reverse candidates for already-confirmed map entries",
        "confirmedEdgeCount": len(playable_progress.get("confirmedEdges") or []),
        "candidateCount": len(rows),
        "manualReviewOnlyCount": manual,
        "rows": rows,
        "conclusion": (
            "Reverse candidates are useful for route-assist playtesting, but they are not normal gameplay proof "
            "unless the reverse side has confirmed reviews or a strict source event record. The current start-route "
            "reverse map1_01a -> map1_02b has selector and geometry evidence only."
        ),
    }


def exit_text(row: dict) -> str:
    hint = row.get("targetHint") or {}
    return (
        f"{row.get('side')} {row.get('x')},{row.get('y')} auto={row.get('autoTrigger')} "
        f"hint={hint.get('side')} {hint.get('x')},{hint.get('y')} "
        f"reciprocal={row.get('reciprocalHint')}"
    )


def markdown(summary: dict) -> str:
    lines = [
        "# Reciprocal Transition Candidates",
        "",
        "Generated from confirmed transition reviews, save-selector scene links, and geometry-only map exit candidates.",
        "",
        f"- confirmed edges scanned: {summary['confirmedEdgeCount']}",
        f"- reverse candidates: {summary['candidateCount']}",
        f"- manual-review-only reverse candidates: {summary['manualReviewOnlyCount']}",
        "",
        summary["conclusion"],
        "",
        "| confirmed entry | spawn | reverse | class | selector | strict events | exits | first exit | open |",
        "| --- | --- | --- | --- | --- | ---: | ---: | --- | --- |",
    ]
    for row in summary["rows"]:
        entry = row["entry"]
        reverse = row["reverse"]
        first_exit = next(iter(reverse.get("exitCandidates") or []), {})
        open_href = first_exit.get("trialUrl") or first_exit.get("reviewUrl") or (
            f"../web/game.html?map={reverse['source']}&trialTransitions=saveSelectorLinks"
        )
        selector = ", ".join(reverse.get("selectors") or []) or "-"
        lines.append(
            f"| `{entry['source']}` -> `{entry['target']}` | `{point_text(entry.get('spawn'))}` | "
            f"`{reverse['source']}` -> `{reverse['target']}` | {reverse['classification']} | "
            f"{selector} | {reverse['strictEventRecordCount']} | {reverse['exitCandidateCount']} | "
            f"{exit_text(first_exit) if first_exit else '-'} | [open]({open_href}) |"
        )
    if not summary["rows"]:
        lines.append("| - | - | - | - | - | - | - | - | - |")
    lines.extend(["", "## Details", ""])
    for row in summary["rows"]:
        entry = row["entry"]
        reverse = row["reverse"]
        lines.extend([
            f"### {reverse['source']} -> {reverse['target']}",
            "",
            f"- confirmed entry: `{entry['source']}` -> `{entry['target']}` points "
            + ", ".join(
                f"`{point.get('x')},{point.get('y')} -> {point.get('spawnX')},{point.get('spawnY')}`"
                for point in entry.get("points") or []
            ),
            f"- classification: `{reverse['classification']}`",
            f"- promotion status: `{reverse['promotionStatus']}`",
            f"- selector records: {reverse['selectorRecordCount']} "
            f"({', '.join(reverse.get('selectors') or []) or '-'})",
            f"- strict event records: {reverse['strictEventRecordCount']}",
            f"- confirmed reverse reviews: {reverse['confirmedReviewCount']}",
            "",
        ])
        if reverse.get("exitCandidates"):
            for candidate in reverse["exitCandidates"]:
                lines.append(f"- {exit_text(candidate)}")
        else:
            lines.append("- no geometry exit candidate")
        lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    rows = []
    for row in summary["rows"]:
        entry = row["entry"]
        reverse = row["reverse"]
        first_exit = next(iter(reverse.get("exitCandidates") or []), {})
        open_href = first_exit.get("trialUrl") or first_exit.get("reviewUrl") or (
            f"../web/game.html?map={reverse['source']}&trialTransitions=saveSelectorLinks"
        )
        rows.append(
            "<tr>"
            f"<td><code>{html.escape(entry['source'])}</code> -> <code>{html.escape(entry['target'])}</code></td>"
            f"<td><code>{html.escape(point_text(entry.get('spawn')))}</code></td>"
            f"<td><code>{html.escape(reverse['source'])}</code> -> <code>{html.escape(reverse['target'])}</code></td>"
            f"<td>{html.escape(reverse['classification'])}</td>"
            f"<td>{html.escape(', '.join(reverse.get('selectors') or []) or '-')}</td>"
            f"<td>{reverse['strictEventRecordCount']}</td>"
            f"<td>{reverse['exitCandidateCount']}</td>"
            f"<td>{html.escape(exit_text(first_exit) if first_exit else '-')}</td>"
            f"<td><a href=\"{html.escape(open_href)}\">open</a></td>"
            "</tr>"
        )
    detail_rows = []
    for row in summary["rows"]:
        reverse = row["reverse"]
        exits = "".join(
            f"<li>{html.escape(exit_text(candidate))}</li>"
            for candidate in reverse.get("exitCandidates") or []
        ) or "<li>no geometry exit candidate</li>"
        detail_rows.append(
            f"<h2><code>{html.escape(reverse['source'])}</code> -> <code>{html.escape(reverse['target'])}</code></h2>"
            f"<ul><li>classification <code>{html.escape(reverse['classification'])}</code></li>"
            f"<li>promotion status <code>{html.escape(reverse['promotionStatus'])}</code></li>"
            f"<li>selector records {reverse['selectorRecordCount']}</li>"
            f"<li>strict event records {reverse['strictEventRecordCount']}</li></ul>"
            f"<ul>{exits}</ul>"
        )
    return "\n".join([
        "<!doctype html><meta charset=\"utf-8\"><title>Reciprocal Transition Candidates</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>Reciprocal Transition Candidates</h1>",
        f"<p>Confirmed edges scanned: {summary['confirmedEdgeCount']}; reverse candidates: {summary['candidateCount']}; manual-review-only: {summary['manualReviewOnlyCount']}.</p>",
        f"<p>{html.escape(summary['conclusion'])}</p>",
        "<table><thead><tr><th>confirmed entry</th><th>spawn</th><th>reverse</th><th>class</th><th>selector</th><th>strict events</th><th>exits</th><th>first exit</th><th>open</th></tr></thead><tbody>",
        "\n".join(rows) or "<tr><td colspan=\"9\">none</td></tr>",
        "</tbody></table>",
        "\n".join(detail_rows),
    ])


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


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--playable-progress", type=Path, default=OUT / "playable_progress.json")
    parser.add_argument("--transition-reviews", type=Path, default=ROOT / "data" / "transition_reviews.json")
    parser.add_argument("--event-transitions", type=Path, default=OUT / "event_transitions.json")
    parser.add_argument("--save-selector-scene-links", type=Path, default=OUT / "save_selector_scene_links.json")
    parser.add_argument("--map-exits", type=Path, default=OUT / "map_exit_candidates.json")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    args = parser.parse_args()
    summary = build_summary(
        load_json(args.playable_progress, {}),
        load_json(args.transition_reviews, {}),
        load_json(args.event_transitions, []),
        load_json(args.save_selector_scene_links, {}),
        load_json(args.map_exits, []),
    )
    write_outputs(summary, args.out_dir)
    print(f"wrote reciprocal transition candidates -> {args.out_dir / 'reciprocal_transition_candidates.html'}")


if __name__ == "__main__":
    main()
