#!/usr/bin/env python3
"""Summarize confirmed-route blockers and the evidence needed to unblock them."""
from __future__ import annotations

import argparse
import html
import json
from pathlib import Path
from urllib.parse import urlencode


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


def web_href(params: dict[str, str]) -> str:
    return f"../web/game.html?{urlencode(params)}"


def first_match(rows: list[dict], **fields) -> dict:
    for row in rows:
        if all(row.get(key) == value for key, value in fields.items()):
            return row
    return {}


def leaf_rows_for(leaf_rows: list[dict], leaf_pointers: list[str]) -> list[dict]:
    wanted = set(leaf_pointers)
    return [row for row in leaf_rows if row.get("leafPointerHex") in wanted]


def trace_rows_for(trace_rows: list[dict], leaf_pointers: list[str]) -> list[dict]:
    wanted = set(leaf_pointers)
    return [
        row
        for row in trace_rows
        if row.get("leafPointerHex") in wanted or row.get("streamVaHex") in wanted
    ]


def selector_progress_hint(scene_selector_progress: dict, source: str, target: str) -> dict:
    for row in scene_selector_progress.get("progressRows") or []:
        for previous in row.get("previousCandidates") or []:
            if source in (previous.get("sharedMaps") or []) and target in (previous.get("introducedMaps") or []):
                return {
                    "fromLabel": previous.get("fromLabel"),
                    "label": row.get("label"),
                    "group": row.get("group"),
                    "slot": row.get("slot"),
                    "sharedMaps": previous.get("sharedMaps") or [],
                    "introducedMaps": previous.get("introducedMaps") or [],
                }
    return {}


def build_rows(
    playable_progress: dict,
    save_selector_frontier: list[dict],
    save_selector_xrefs: list[dict],
    save_selector_leaf_streams: list[dict],
    save_selector_stream_traces: list[dict],
    save_selector_frontier_branches: list[dict],
    save_loader_trace: dict,
    scene_selector_progress: dict,
) -> list[dict]:
    rows = []
    for blocker in playable_progress.get("confirmedRouteBlockers") or []:
        source = blocker.get("map")
        for target in blocker.get("saveSelectorFrontierTargets") or [None]:
            frontier = first_match(save_selector_frontier, source=source, target=target) if target else {}
            xref = first_match(save_selector_xrefs, source=source, target=target) if target else {}
            branch = first_match(save_selector_frontier_branches, source=source, target=target) if target else {}
            leaf_pointers = frontier.get("leafPointers") or []
            leaves = leaf_rows_for(save_selector_leaf_streams, leaf_pointers)
            traces = trace_rows_for(save_selector_stream_traces, leaf_pointers)
            branch_steps = branch.get("branchSteps") or []
            row = {
                "source": source,
                "target": target,
                "eventTransitionRecords": blocker.get("eventTransitionRecords", 0),
                "reason": blocker.get("reason"),
                "status": "blocked",
                "strictEventCoordinate": bool(blocker.get("eventTransitionRecords")),
                "selectorOnly": bool(frontier and not frontier.get("directEventTransition")),
                "promotionRisk": branch.get("promotionRisk") or "unknown",
                "missingEvidence": [],
                "nonPromotingEvidence": [],
                "selectors": frontier.get("selectors") or [],
                "leafPointers": leaf_pointers,
                "scenePairs": frontier.get("scenePairs") or [],
                "branchConditions": [
                    step.get("condition")
                    for step in branch_steps
                    if step.get("condition")
                ],
                "branchTargets": [
                    {
                        "leafPointerHex": step.get("leafPointerHex"),
                        "fallthroughVaHex": step.get("fallthroughVaHex"),
                        "branchTargetHex": step.get("branchTargetHex"),
                        "branchTargetKind": step.get("branchTargetKind"),
                    }
                    for step in branch_steps
                ],
                "sourceRecordTilesets": branch.get("sourceRecordTilesets") or [],
                "targetRecordTilesets": branch.get("targetRecordTilesets") or [],
                "sourceAcceptedRender": branch.get("sourceAcceptedRender") or {},
                "targetAcceptedRender": branch.get("targetAcceptedRender") or {},
                "sourceTilesetMatch": branch.get("sourceTilesetMatch"),
                "targetTilesetMatch": branch.get("targetTilesetMatch"),
                "xrefScenePairs": xref.get("scenePairs") or [],
                "xrefCurrentRootRefIsSelectorRowEntry": xref.get("currentRootRefIsSelectorRowEntry"),
                "xrefCurrentRootNonMetadataRefCount": xref.get("currentRootNonMetadataRefCount"),
                "xrefRoutePromotionStatus": xref.get("routePromotionStatus"),
                "leafSummaries": [
                    {
                        "leafPointerHex": leaf.get("leafPointerHex"),
                        "nestedPointerHex": leaf.get("nestedPointerHex"),
                        "linkedCns": leaf.get("linkedCns") or [],
                        "fieldRecords": leaf.get("fieldRecords") or [],
                        "nestedFieldRecords": leaf.get("nestedFieldRecords") or [],
                    }
                    for leaf in leaves
                ],
                "traceStops": [
                    {
                        "streamVaHex": trace.get("streamVaHex"),
                        "leafPointerHex": trace.get("leafPointerHex"),
                        "stopReasons": sorted({
                            step.get("stopReason")
                            for step in trace.get("trace") or []
                            if step.get("stopReason")
                        }),
                    }
                    for trace in traces
                ],
                "saveLoaderFrontierExample": save_loader_trace.get("frontierExample") or {},
                "selectorProgressHint": selector_progress_hint(scene_selector_progress, source, target) if target else {},
                "sourceOpen": web_href({"map": source, "events": "1", "overview": "1"}) if source else "",
                "targetOpen": web_href({"map": target, "events": "1", "overview": "1"}) if target else "",
                "trialOpen": web_href({"map": source, "trialTransitions": "routeAssist"}) if source and target else "",
                "selectorTrialOpen": web_href({"map": source, "trialTransitions": "saveSelectorLinks"}) if source and target else "",
                "selectorOpen": "",
            }
            if row["selectors"]:
                group_slot = row["selectors"][0].split(":", 1)
                if len(group_slot) == 2 and all(part.isdigit() for part in group_slot):
                    row["selectorOpen"] = web_href({
                        "saveGroup": group_slot[0],
                        "saveSlot": group_slot[1],
                        "events": "1",
                        "overview": "1",
                    })
            if row["eventTransitionRecords"] == 0:
                row["missingEvidence"].append("strict source coordinate")
            if row["selectorOnly"]:
                row["missingEvidence"].append("tile hotspot confirmation")
            if row.get("xrefCurrentRootRefIsSelectorRowEntry") is True and row.get("xrefCurrentRootNonMetadataRefCount") == 0:
                row["missingEvidence"].append("selected-root execution ref")
            if any(
                str(step.get("branchTargetKind") or "").startswith("cns:")
                and not str(step.get("branchTargetKind") or "").startswith("cns:map")
                for step in branch_steps
            ):
                row["nonPromotingEvidence"].append("scene-list/resource gate only")
            if row["promotionRisk"] == "high":
                row["missingEvidence"].append("branch-state outcome")
            if row["sourceTilesetMatch"] is False:
                row["missingEvidence"].append("source scene/render mismatch")
            if not row["target"]:
                row["missingEvidence"].append("candidate target")
            rows.append(row)
    return rows


def markdown(rows: list[dict]) -> str:
    lines = [
        "# Confirmed Route Blockers",
        "",
        "Blockers on the normal confirmed route. These rows explain why a candidate is not promoted to normal gameplay.",
        "",
        "| source | candidate target | status | missing evidence | non-promoting evidence | selectors | branch gates | render mismatch | open |",
        "| --- | --- | --- | --- | --- | --- | --- | --- | --- |",
    ]
    for row in rows:
        gates = ", ".join(row.get("branchConditions") or []) or "-"
        mismatch = (
            f"source {row.get('sourceTilesetMatch')}, target {row.get('targetTilesetMatch')}; "
            f"record {','.join(row.get('sourceRecordTilesets') or []) or '-'} -> "
            f"accepted {','.join((row.get('sourceAcceptedRender') or {}).get('tilesets') or []) or '-'}"
        )
        open_links = [f"[source]({row['sourceOpen']})"]
        if row.get("targetOpen"):
            open_links.append(f"[target]({row['targetOpen']})")
        if row.get("trialOpen"):
            open_links.append(f"[assist]({row['trialOpen']})")
        if row.get("selectorTrialOpen"):
            open_links.append(f"[selector trial]({row['selectorTrialOpen']})")
        if row.get("selectorOpen"):
            open_links.append(f"[selector]({row['selectorOpen']})")
        lines.append(
            f"| {row.get('source') or '-'} | {row.get('target') or '-'} | {row.get('status')} / {row.get('promotionRisk')} | "
            f"{', '.join(row.get('missingEvidence') or []) or '-'} | "
            f"{', '.join(row.get('nonPromotingEvidence') or []) or '-'} | "
            f"{', '.join(row.get('selectors') or []) or '-'} {', '.join(row.get('leafPointers') or []) or ''} | "
            f"{gates} | {mismatch} | {' / '.join(open_links)} |"
        )
    if not rows:
        lines.append("| - | - | - | - | - | - | - |")
    lines.extend([
        "",
        "## Evidence Notes",
        "",
    ])
    for row in rows:
        lines.extend([
            f"### {row.get('source')} -> {row.get('target') or '-'}",
            "",
            f"- event records: {row.get('eventTransitionRecords', 0)}",
            f"- selector only: {row.get('selectorOnly')}",
            f"- non-promoting evidence: {', '.join(row.get('nonPromotingEvidence') or []) or '-'}",
            f"- branch targets: {json.dumps(row.get('branchTargets') or [], ensure_ascii=False)}",
            f"- selector progress hint: {json.dumps(row.get('selectorProgressHint') or {}, ensure_ascii=False)}",
            f"- xref current root: rowEntry={row.get('xrefCurrentRootRefIsSelectorRowEntry')}, "
            f"nonMetadataRefs={row.get('xrefCurrentRootNonMetadataRefCount')}, "
            f"status={row.get('xrefRoutePromotionStatus')}",
            f"- trace stops: {json.dumps(row.get('traceStops') or [], ensure_ascii=False)}",
            "",
        ])
    return "\n".join(lines)


def html_page(rows: list[dict]) -> str:
    body = []
    for row in rows:
        links = [f'<a href="{html.escape(row["sourceOpen"])}">source</a>']
        if row.get("targetOpen"):
            links.append(f'<a href="{html.escape(row["targetOpen"])}">target</a>')
        if row.get("trialOpen"):
            links.append(f'<a href="{html.escape(row["trialOpen"])}">assist</a>')
        if row.get("selectorTrialOpen"):
            links.append(f'<a href="{html.escape(row["selectorTrialOpen"])}">selector trial</a>')
        if row.get("selectorOpen"):
            links.append(f'<a href="{html.escape(row["selectorOpen"])}">selector</a>')
        mismatch = (
            f"source {row.get('sourceTilesetMatch')}, target {row.get('targetTilesetMatch')}<br>"
            f"record <code>{html.escape(','.join(row.get('sourceRecordTilesets') or []) or '-')}</code><br>"
            f"accepted <code>{html.escape(','.join((row.get('sourceAcceptedRender') or {}).get('tilesets') or []) or '-')}</code>"
        )
        details = html.escape(json.dumps({
            "branchTargets": row.get("branchTargets") or [],
            "selectorProgressHint": row.get("selectorProgressHint") or {},
            "xrefCurrentRoot": {
                "rowEntry": row.get("xrefCurrentRootRefIsSelectorRowEntry"),
                "nonMetadataRefs": row.get("xrefCurrentRootNonMetadataRefCount"),
                "status": row.get("xrefRoutePromotionStatus"),
            },
            "traceStops": row.get("traceStops") or [],
        }, ensure_ascii=False, indent=2))
        body.append(
            "<tr>"
            f"<td>{html.escape(row.get('source') or '-')}</td>"
            f"<td>{html.escape(row.get('target') or '-')}</td>"
            f"<td>{html.escape(row.get('status') or '-')} / {html.escape(row.get('promotionRisk') or '-')}</td>"
            f"<td>{html.escape(', '.join(row.get('missingEvidence') or []) or '-')}</td>"
            f"<td>{html.escape(', '.join(row.get('nonPromotingEvidence') or []) or '-')}</td>"
            f"<td>{html.escape(', '.join(row.get('selectors') or []) or '-')}<br><code>{html.escape(', '.join(row.get('leafPointers') or []) or '-')}</code></td>"
            f"<td>{html.escape(', '.join(row.get('branchConditions') or []) or '-')}</td>"
            f"<td>{mismatch}</td>"
            f"<td>{' '.join(links)}</td>"
            f"<td><pre>{details}</pre></td>"
            "</tr>"
        )
    return "\n".join([
        "<!doctype html>",
        '<html lang="en">',
        "<head>",
        '  <meta charset="utf-8">',
        '  <meta name="viewport" content="width=device-width, initial-scale=1">',
        "  <title>Confirmed Route Blockers</title>",
        "  <style>",
        "    body { margin: 24px; background: #101010; color: #eee; font: 14px system-ui, sans-serif; }",
        "    table { border-collapse: collapse; width: 100%; }",
        "    th, td { border: 1px solid #333; padding: 6px 8px; vertical-align: top; }",
        "    th { background: #1d1d1d; position: sticky; top: 0; }",
        "    a { color: #9bd4ff; } code { color: #f5d76e; }",
        "    pre { margin: 0; max-width: 420px; white-space: pre-wrap; font-size: 12px; color: #bbb; }",
        "  </style>",
        "</head>",
        "<body>",
        "  <h1>Confirmed Route Blockers</h1>",
        "  <p>Blockers on the normal confirmed route. These rows explain why a candidate is not promoted to normal gameplay.</p>",
        "  <table><thead><tr><th>source</th><th>candidate target</th><th>status</th><th>missing evidence</th><th>non-promoting evidence</th><th>selectors</th><th>branch gates</th><th>render mismatch</th><th>open</th><th>details</th></tr></thead>",
        f"  <tbody>{''.join(body) or '<tr><td colspan=\"10\">No blockers.</td></tr>'}</tbody></table>",
        "</body>",
        "</html>",
        "",
    ])


def write_outputs(rows: list[dict], out_dir: Path = OUT) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "confirmed_route_blockers.json").write_text(
        json.dumps(rows, ensure_ascii=False, indent=2) + "\n",
        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("--frontier", type=Path, default=OUT / "save_selector_frontier.json")
    parser.add_argument("--xrefs", type=Path, default=OUT / "save_selector_xrefs.json")
    parser.add_argument("--leaf-streams", type=Path, default=OUT / "save_selector_leaf_streams.json")
    parser.add_argument("--stream-traces", type=Path, default=OUT / "save_selector_stream_traces.json")
    parser.add_argument("--frontier-branches", type=Path, default=OUT / "save_selector_frontier_branches.json")
    parser.add_argument("--save-loader-trace", type=Path, default=OUT / "save_loader_trace.json")
    parser.add_argument("--scene-selector-progress", type=Path, default=OUT / "scene_selector_progress.json")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    args = parser.parse_args()
    rows = build_rows(
        json.loads(args.playable_progress.read_text(encoding="utf-8")),
        json.loads(args.frontier.read_text(encoding="utf-8")),
        json.loads(args.xrefs.read_text(encoding="utf-8")),
        json.loads(args.leaf_streams.read_text(encoding="utf-8")),
        json.loads(args.stream_traces.read_text(encoding="utf-8")),
        json.loads(args.frontier_branches.read_text(encoding="utf-8")),
        json.loads(args.save_loader_trace.read_text(encoding="utf-8")),
        json.loads(args.scene_selector_progress.read_text(encoding="utf-8")),
    )
    write_outputs(rows, args.out_dir)
    print(f"wrote {len(rows)} route blocker rows -> {args.out_dir / 'confirmed_route_blockers.json'}")


if __name__ == "__main__":
    main()
