#!/usr/bin/env python3
"""Compare save-selector scene-record adjacency with strict/confirmed transitions."""
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"
TARGET = "map2_02d"
FAILED_SCENE_ADJACENCY_GATE_IDS = [
    "strict-event-backed-adjacency",
    "confirmed-review-backed-adjacency",
    "strict-hotspot-or-coordinate",
]
SCENE_ADJACENCY_MISSING_EVIDENCE = [
    "strict event-backed map1_01a -> map2_02d adjacency",
    "confirmed transition-review backed map1_01a -> map2_02d adjacency",
    "strict source coordinate or hotspot backing selector adjacency",
]
SCENE_ADJACENCY_REMAINING_PROOFS = [
    "find a strict event transition backing map1_01a -> map2_02d",
    "confirm the selector adjacency through transition review",
    "find a strict source coordinate or hotspot backing selector adjacency",
]
SCENE_ADJACENCY_EVIDENCE_REFS = [
    {
        "path": "out/save_scene_selector_references.json",
        "fields": ["kind", "sceneMatched", "pathHex", "resource"],
    },
    {
        "path": "out/event_transitions.json",
        "fields": ["map", "targets", "events"],
    },
    {
        "path": "out/transition_reviews.json",
        "fields": ["source", "target", "state", "recordVaHex"],
    },
]


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


def unique(values: list[str]) -> list[str]:
    seen = set()
    result = []
    for value in values:
        if value in seen:
            continue
        seen.add(value)
        result.append(value)
    return result


def pair_key(source: str, target: str) -> str:
    return f"{source}->{target}"


def strict_event_edges(event_transitions: list[dict]) -> set[tuple[str, str]]:
    return {
        (row.get("map"), target)
        for row in event_transitions
        for target in row.get("targets") or []
        if row.get("map") and target
    }


def confirmed_review_edges(transition_reviews: dict[str, dict]) -> set[tuple[str, str]]:
    return {
        (row.get("source"), row.get("target"))
        for row in transition_reviews.values()
        if row.get("state") == "confirmed" and row.get("source") and row.get("target")
    }


def field_map_leaf_groups(reference_rows: list[dict]) -> dict[tuple[str, str], list[dict]]:
    leaves: dict[tuple[str, str], list[dict]] = {}
    for row in reference_rows:
        if row.get("kind") != "fieldMap" or row.get("sceneMatched") is not True:
            continue
        path = row.get("pathHex") or []
        if not path:
            continue
        leaves.setdefault((row.get("label") or "-", path[-1]), []).append(row)
    return leaves


def adjacent_occurrences(reference_rows: list[dict]) -> tuple[int, int, list[dict]]:
    leaves = field_map_leaf_groups(reference_rows)
    occurrences = []
    for (selector, leaf), rows in sorted(leaves.items()):
        ordered = sorted(rows, key=lambda row: int(row.get("refVaHex") or "0", 16))
        for source, target in zip(ordered, ordered[1:]):
            source_map = source.get("resource")
            target_map = target.get("resource")
            if not source_map or not target_map or source_map == target_map:
                continue
            occurrences.append({
                "source": source_map,
                "target": target_map,
                "selector": selector,
                "leafPointerHex": leaf,
                "sourceRecordVaHex": source.get("refVaHex"),
                "targetRecordVaHex": target.get("refVaHex"),
                "sourceSceneIdHex": source.get("sceneIdHex"),
                "targetSceneIdHex": target.get("sceneIdHex"),
                "sourceTilesets": source.get("tilesets") or [],
                "targetTilesets": target.get("tilesets") or [],
            })
    return len(leaves), sum(len(rows) for rows in leaves.values()), occurrences


def aggregate_pairs(occurrences: list[dict], strict_edges: set[tuple[str, str]], confirmed_edges: set[tuple[str, str]]) -> list[dict]:
    pairs: dict[tuple[str, str], dict] = {}
    for occurrence in occurrences:
        key = (occurrence["source"], occurrence["target"])
        pair = pairs.setdefault(key, {
            "source": occurrence["source"],
            "target": occurrence["target"],
            "occurrenceCount": 0,
            "selectors": [],
            "leafPointers": [],
            "sourceRecords": [],
            "targetRecords": [],
            "strictEventBacked": key in strict_edges,
            "confirmedReviewBacked": key in confirmed_edges,
        })
        pair["occurrenceCount"] += 1
        pair["selectors"] = unique([*pair["selectors"], occurrence["selector"]])
        pair["leafPointers"] = unique([*pair["leafPointers"], occurrence["leafPointerHex"]])
        pair["sourceRecords"] = unique([*pair["sourceRecords"], occurrence.get("sourceRecordVaHex") or "-"])
        pair["targetRecords"] = unique([*pair["targetRecords"], occurrence.get("targetRecordVaHex") or "-"])
    rows = []
    for pair in pairs.values():
        pair["selectorAdjacencyOnly"] = not pair["strictEventBacked"] and not pair["confirmedReviewBacked"]
        pair["classification"] = (
            "strict-event-backed" if pair["strictEventBacked"] else
            "confirmed-review-backed" if pair["confirmedReviewBacked"] else
            "selector-adjacency-only"
        )
        rows.append(pair)
    return sorted(rows, key=lambda row: (row["source"], row["target"]))


def build_summary(
    reference_rows: list[dict],
    event_transitions: list[dict] | None = None,
    transition_reviews: dict[str, dict] | None = None,
) -> dict:
    event_transitions = event_transitions or load_json(OUT / "event_transitions.json", [])
    transition_reviews = transition_reviews or load_json(OUT / "transition_reviews.json", {})
    strict_edges = strict_event_edges(event_transitions)
    confirmed_edges = confirmed_review_edges(transition_reviews)
    leaf_count, field_ref_count, occurrences = adjacent_occurrences(reference_rows)
    pairs = aggregate_pairs(occurrences, strict_edges, confirmed_edges)
    current_occurrences = [
        row for row in occurrences
        if row.get("source") == SOURCE and row.get("target") == TARGET
    ]
    current_pair = next((row for row in pairs if row["source"] == SOURCE and row["target"] == TARGET), None)
    if current_pair is None:
        current_pair = {
            "source": SOURCE,
            "target": TARGET,
            "occurrenceCount": 0,
            "selectors": [],
            "leafPointers": [],
            "sourceRecords": [],
            "targetRecords": [],
            "strictEventBacked": (SOURCE, TARGET) in strict_edges,
            "confirmedReviewBacked": (SOURCE, TARGET) in confirmed_edges,
            "selectorAdjacencyOnly": False,
            "classification": "not-selector-adjacent",
        }
    current_pair = {**current_pair, "occurrences": current_occurrences}
    strict_adjacent = [row for row in pairs if row["strictEventBacked"]]
    confirmed_adjacent = [row for row in pairs if row["confirmedReviewBacked"]]
    conclusion = (
        "Save-selector field-map adjacency is a broad scene-list signal: none of the directed adjacent pairs "
        "overlap the current strict event-transition edge set or confirmed transition reviews. The current "
        "map1_01a->map2_02d pair appears three times as selector 2:0 scene-record adjacency, but it remains "
        "selector-adjacency-only without strict hotspot/coordinate evidence."
    )
    return {
        "scope": "all save-selector field-map scene-record adjacencies",
        "source": SOURCE,
        "target": TARGET,
        "selectorLeafCount": leaf_count,
        "fieldMapReferenceCount": field_ref_count,
        "adjacentOccurrenceCount": len(occurrences),
        "uniqueDirectedAdjacentPairCount": len(pairs),
        "strictEventEdgeCount": len(strict_edges),
        "confirmedReviewEdgeCount": len(confirmed_edges),
        "adjacentPairsWithStrictEventCount": len(strict_adjacent),
        "adjacentPairsWithConfirmedReviewCount": len(confirmed_adjacent),
        "adjacentPairsWithoutStrictOrConfirmedCount": sum(1 for row in pairs if row["selectorAdjacencyOnly"]),
        "currentPair": current_pair,
        "currentPairOccurrenceCount": current_pair.get("occurrenceCount"),
        "currentPairStrictEventBacked": current_pair.get("strictEventBacked"),
        "currentPairConfirmedReviewBacked": current_pair.get("confirmedReviewBacked"),
        "currentPairSelectorAdjacencyOnly": current_pair.get("selectorAdjacencyOnly"),
        "proofFound": False,
        "sceneAdjacencyStrictProofFound": False,
        "failedSceneAdjacencyGateIds": FAILED_SCENE_ADJACENCY_GATE_IDS,
        "missingEvidence": SCENE_ADJACENCY_MISSING_EVIDENCE,
        "remainingProofs": SCENE_ADJACENCY_REMAINING_PROOFS,
        "evidenceRefs": SCENE_ADJACENCY_EVIDENCE_REFS,
        "evidenceRefCount": len(SCENE_ADJACENCY_EVIDENCE_REFS),
        "promotionStatus": "blocked",
        "pairRows": pairs,
        "conclusion": conclusion,
    }


def pair_brief(row: dict) -> str:
    return (
        f"occ={row.get('occurrenceCount')} "
        f"selectors={','.join(row.get('selectors') or [])} "
        f"strict={row.get('strictEventBacked')} "
        f"confirmed={row.get('confirmedReviewBacked')} "
        f"class={row.get('classification')}"
    )


def markdown(summary: dict) -> str:
    lines = [
        "# Save Selector Scene Adjacency Index",
        "",
        f"- scope: {summary['scope']}",
        f"- current route: `{summary['source']} -> {summary['target']}`",
        f"- selector leaves: {summary['selectorLeafCount']}",
        f"- field-map references: {summary['fieldMapReferenceCount']}",
        f"- adjacent occurrences: {summary['adjacentOccurrenceCount']}",
        f"- unique directed adjacent pairs: {summary['uniqueDirectedAdjacentPairCount']}",
        f"- strict event edges: {summary['strictEventEdgeCount']}",
        f"- confirmed review edges: {summary['confirmedReviewEdgeCount']}",
        f"- adjacent pairs with strict events: {summary['adjacentPairsWithStrictEventCount']}",
        f"- adjacent pairs with confirmed reviews: {summary['adjacentPairsWithConfirmedReviewCount']}",
        f"- selector-adjacency-only pairs: {summary['adjacentPairsWithoutStrictOrConfirmedCount']}",
        f"- current pair occurrence count: {summary['currentPairOccurrenceCount']}",
        f"- current pair strict/confirmed backed: {summary['currentPairStrictEventBacked']} / {summary['currentPairConfirmedReviewBacked']}",
        f"- current pair selector-adjacency-only: {summary['currentPairSelectorAdjacencyOnly']}",
        f"- proof found: {summary['proofFound']}",
        f"- scene adjacency strict proof found: {summary['sceneAdjacencyStrictProofFound']}",
        f"- failed scene adjacency gates: {', '.join(summary.get('failedSceneAdjacencyGateIds') or []) or '-'}",
        f"- missing evidence count: {len(summary.get('missingEvidence') or [])}",
        f"- evidence refs: {summary.get('evidenceRefCount')}",
        f"- promotion status: `{summary['promotionStatus']}`",
        "",
        summary["conclusion"],
        "",
        "## Missing Evidence",
        "",
    ]
    lines.extend(f"- {item}" for item in summary.get("missingEvidence") or [])
    lines.extend([
        "",
        "## Current Pair Occurrences",
        "",
        "| selector | leaf | source record | target record | source scene | target scene |",
        "| --- | --- | --- | --- | --- | --- |",
    ])
    for row in summary["currentPair"].get("occurrences") or []:
        lines.append(
            f"| `{row.get('selector')}` | `{row.get('leafPointerHex')}` | `{row.get('sourceRecordVaHex')}` | "
            f"`{row.get('targetRecordVaHex')}` | `{row.get('sourceSceneIdHex')}` | `{row.get('targetSceneIdHex')}` |"
        )
    if not summary["currentPair"].get("occurrences"):
        lines.append("| - | - | - | - | - | - |")
    lines.extend([
        "",
        "## Directed Adjacent Pairs",
        "",
        "| source | target | counts |",
        "| --- | --- | --- |",
    ])
    current_key = (summary["source"], summary["target"])
    ordered = sorted(
        summary["pairRows"],
        key=lambda row: ((row["source"], row["target"]) != current_key, row["source"], row["target"]),
    )
    for row in ordered:
        lines.append(f"| `{row['source']}` | `{row['target']}` | {pair_brief(row)} |")
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    failed_gates = "".join(
        f"<li>{html.escape(item)}</li>" for item in summary.get("failedSceneAdjacencyGateIds") or []
    )
    missing = "".join(f"<li>{html.escape(item)}</li>" for item in summary.get("missingEvidence") or [])
    current_rows = "\n".join(
        "<tr>"
        f"<td><code>{html.escape(row.get('selector') or '-')}</code></td>"
        f"<td><code>{html.escape(row.get('leafPointerHex') or '-')}</code></td>"
        f"<td><code>{html.escape(row.get('sourceRecordVaHex') or '-')}</code></td>"
        f"<td><code>{html.escape(row.get('targetRecordVaHex') or '-')}</code></td>"
        f"<td><code>{html.escape(row.get('sourceSceneIdHex') or '-')}</code></td>"
        f"<td><code>{html.escape(row.get('targetSceneIdHex') or '-')}</code></td>"
        "</tr>"
        for row in summary["currentPair"].get("occurrences") or []
    ) or "<tr><td colspan=\"6\">none</td></tr>"
    current_key = (summary["source"], summary["target"])
    ordered = sorted(
        summary["pairRows"],
        key=lambda row: ((row["source"], row["target"]) != current_key, row["source"], row["target"]),
    )
    pair_rows = "\n".join(
        "<tr>"
        f"<td><code>{html.escape(row['source'])}</code></td>"
        f"<td><code>{html.escape(row['target'])}</code></td>"
        f"<td>{html.escape(pair_brief(row))}</td>"
        "</tr>"
        for row in ordered
    )
    return "\n".join([
        "<!doctype html>",
        '<html lang="en">',
        "<head>",
        '  <meta charset="utf-8">',
        '  <meta name="viewport" content="width=device-width, initial-scale=1">',
        "  <title>Save Selector Scene Adjacency Index</title>",
        "  <style>",
        "    body { margin: 24px; background: #101010; color: #eee; font: 14px system-ui, sans-serif; }",
        "    table { border-collapse: collapse; width: 100%; margin: 12px 0 20px; }",
        "    th, td { border: 1px solid #333; padding: 6px 8px; vertical-align: top; }",
        "    th { background: #1d1d1d; }",
        "    code { color: #f5d76e; }",
        "  </style>",
        "</head>",
        "<body>",
        "  <h1>Save Selector Scene Adjacency Index</h1>",
        f"  <p>adjacent occurrences <code>{summary['adjacentOccurrenceCount']}</code>; unique directed adjacent pairs <code>{summary['uniqueDirectedAdjacentPairCount']}</code>; adjacent pairs with strict events <code>{summary['adjacentPairsWithStrictEventCount']}</code>; adjacent pairs with confirmed reviews <code>{summary['adjacentPairsWithConfirmedReviewCount']}</code>; selector-adjacency-only pairs <code>{summary['adjacentPairsWithoutStrictOrConfirmedCount']}</code>.</p>",
        f"  <p>current pair occurrence count <code>{summary['currentPairOccurrenceCount']}</code>; strict/confirmed backed <code>{summary['currentPairStrictEventBacked']}</code> / <code>{summary['currentPairConfirmedReviewBacked']}</code>; selector-adjacency-only <code>{summary['currentPairSelectorAdjacencyOnly']}</code>; proofFound <code>{summary['proofFound']}</code>; sceneAdjacencyStrictProofFound <code>{summary['sceneAdjacencyStrictProofFound']}</code>; missingEvidenceCount <code>{len(summary.get('missingEvidence') or [])}</code>; evidence refs <code>{summary.get('evidenceRefCount')}</code>; promotion status <code>{html.escape(summary['promotionStatus'])}</code>.</p>",
        f"  <p>{html.escape(summary['conclusion'])}</p>",
        f"  <h2>Failed Scene Adjacency Gates</h2><ul>{failed_gates}</ul>",
        f"  <h2>Missing Evidence</h2><ul>{missing}</ul>",
        "  <h2>Current Pair Occurrences</h2>",
        "  <table><thead><tr><th>selector</th><th>leaf</th><th>source record</th><th>target record</th><th>source scene</th><th>target scene</th></tr></thead><tbody>",
        current_rows,
        "  </tbody></table>",
        "  <h2>Directed Adjacent Pairs</h2>",
        "  <table><thead><tr><th>source</th><th>target</th><th>counts</th></tr></thead><tbody>",
        pair_rows,
        "  </tbody></table>",
        "</body></html>",
    ])


def write_outputs(summary: dict, out_dir: Path = OUT, html_out: Path | None = None) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "save_selector_scene_adjacency_index.json").write_text(
        json.dumps(summary, ensure_ascii=False, separators=(",", ":")) + "\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")


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--references", type=Path, default=OUT / "save_scene_selector_references.json")
    parser.add_argument("--event-transitions", type=Path, default=OUT / "event_transitions.json")
    parser.add_argument("--transition-reviews", type=Path, default=OUT / "transition_reviews.json")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    parser.add_argument("--html-out", type=Path, default=None, help="Optional HTML report output path.")
    args = parser.parse_args()
    summary = build_summary(
        load_json(args.references, []),
        load_json(args.event_transitions, []),
        load_json(args.transition_reviews, {}),
    )
    write_outputs(summary, args.out_dir, args.html_out)
    print(f"wrote save selector scene adjacency index -> {args.out_dir / 'save_selector_scene_adjacency_index.json'}")


if __name__ == "__main__":
    main()
