#!/usr/bin/env python3
"""Group nearby field-map scene records and event records into route-root candidates."""
from __future__ import annotations

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

from map_render_reviews import accepted_by_map


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"
CLUSTER_GAP = 0x180
SOURCE = "map1_01a"
TARGET = "map2_02d"
FAILED_FIELD_MAP_RECORD_ROOT_GATE_IDS = [
    "direct-strict-event-transition",
    "source-outgoing-strict-cluster",
    "target-strict-cluster",
    "selector-only-frontier-not-event-linked",
]
FIELD_MAP_RECORD_ROOT_MISSING_EVIDENCE = [
    "direct strict event transition map1_01a -> map2_02d",
    "strict event cluster sourced by map1_01a and linking to map2_02d",
    "strict target-linked cluster for map2_02d",
    "non-selector-only current frontier event link or equivalent trigger proof",
]
FIELD_MAP_RECORD_ROOT_EVIDENCE_REFS = [
    {
        "path": "out/scene_manifest.json",
        "fields": ["recordVa", "map", "sceneIdHex", "tilesets"],
    },
    {
        "path": "out/scene_events.json",
        "fields": ["recordVa", "map", "eventKind", "points", "eventDispatchRefs"],
    },
    {
        "path": "out/save_scene_selector_references.json",
        "fields": ["label", "pathHex", "refVaHex", "resource", "sceneMatched"],
    },
    {
        "path": "out/save_selector_frontier.json",
        "fields": ["source", "target", "directEventTransition", "scenePairs"],
    },
    {
        "path": "out/map_render_reviews.json",
        "fields": ["acceptedTilesets", "matchesAccepted"],
    },
]


def hex32(value: int) -> str:
    return f"0x{value:08x}"


def parse_hex(value: str | None) -> int | None:
    return int(value, 16) if isinstance(value, str) else None


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 web_href(params: dict[str, str]) -> str:
    return f"../web/game.html?{urlencode(params)}"


def event_field_links(event: dict) -> list[str]:
    links = []
    for ref in event.get("eventDispatchRefs") or []:
        for linked in ref.get("conditionLinkedStrings") or []:
            if linked.startswith("map") and linked.endswith(".cns") and "_" in linked and linked[3:4].isdigit():
                links.append(linked[:-4])
    return unique(links)


def save_selector_refs_by_va(rows: list[dict]) -> dict[str, list[dict]]:
    refs: dict[str, list[dict]] = {}
    for row in rows:
        refs.setdefault(row.get("refVaHex"), []).append(row)
    return refs


def frontier_pairs(frontier_rows: list[dict]) -> set[tuple[str, str]]:
    return {
        (row.get("source"), row.get("target"))
        for row in frontier_rows
        if row.get("source") and row.get("target")
    }


def node_rows(manifest: list[dict], events: list[dict]) -> list[dict]:
    rows = []
    for record in manifest:
        rows.append({
            "kind": "manifest",
            "va": record["recordVa"],
            "map": record["map"],
            "sceneIdHex": record.get("sceneIdHex"),
            "tilesets": record.get("tilesets") or [],
            "sprites": record.get("sprites") or [],
        })
    for event in events:
        rows.append({
            "kind": "event",
            "va": event["recordVa"],
            "map": event["map"],
            "sceneIdHex": event.get("sceneIdHex"),
            "eventKind": event.get("eventKind"),
            "pointCount": len(event.get("points") or []),
            "fieldLinks": event_field_links(event),
        })
    return sorted(rows, key=lambda row: row["va"])


def cluster_nodes(nodes: list[dict], gap: int = CLUSTER_GAP) -> list[list[dict]]:
    clusters: list[list[dict]] = []
    for node in nodes:
        if not clusters or node["va"] - clusters[-1][-1]["va"] > gap:
            clusters.append([node])
        else:
            clusters[-1].append(node)
    return clusters


def render_match(record: dict, accepted: dict[str, dict]) -> dict:
    accepted_record = accepted.get(record["map"]) or {}
    accepted_tilesets = accepted_record.get("tilesets") or []
    record_tilesets = record.get("tilesets") or []
    return {
        "acceptedTilesets": accepted_tilesets,
        "recordTilesets": record_tilesets,
        "matchesAccepted": bool(accepted_tilesets) and record_tilesets[: len(accepted_tilesets)] == accepted_tilesets,
    }


def summarize_cluster(
    cluster: list[dict],
    save_refs_by_va: dict[str, list[dict]],
    accepted: dict[str, dict],
    frontier: set[tuple[str, str]],
) -> dict:
    manifest_records = [node for node in cluster if node["kind"] == "manifest"]
    event_records = [node for node in cluster if node["kind"] == "event"]
    manifest_maps = unique([node["map"] for node in manifest_records])
    event_sources = unique([node["map"] for node in event_records])
    event_links = unique([link for node in event_records for link in node.get("fieldLinks") or []])
    selector_refs = []
    for record in manifest_records:
        for ref in save_refs_by_va.get(hex32(record["va"]), []):
            selector_refs.append({
                "selector": ref.get("label"),
                "pathHex": ref.get("pathHex") or [],
                "resource": ref.get("resource"),
                "refVaHex": ref.get("refVaHex"),
            })
    route_pairs = []
    for source in unique([*manifest_maps, *event_sources]):
        for target in unique([*manifest_maps, *event_links]):
            if source != target:
                route_pairs.append({"source": source, "target": target, "isCurrentFrontier": (source, target) in frontier})
    route_pairs = [dict(item) for item in {tuple(sorted(row.items())): row for row in route_pairs}.values()]
    records = []
    for record in manifest_records:
        records.append({
            "recordVaHex": hex32(record["va"]),
            "map": record["map"],
            "sceneIdHex": record.get("sceneIdHex"),
            "tilesets": record.get("tilesets") or [],
            "render": render_match(record, accepted),
            "saveSelectorRefs": save_refs_by_va.get(hex32(record["va"]), []),
        })
    classification = "unlinked manifest cluster"
    if event_records:
        classification = "strict event-linked cluster"
    if selector_refs and not event_records:
        classification = "save-selector-only cluster"
    if any(row["isCurrentFrontier"] for row in route_pairs) and selector_refs and not event_records:
        classification = "current frontier selector-only cluster"
    return {
        "clusterStartHex": hex32(cluster[0]["va"]),
        "clusterEndHex": hex32(cluster[-1]["va"]),
        "classification": classification,
        "manifestMaps": manifest_maps,
        "eventSources": event_sources,
        "eventFieldLinks": event_links,
        "eventRecordCount": len(event_records),
        "saveSelectorRefCount": len(selector_refs),
        "currentFrontierPairCount": sum(1 for row in route_pairs if row["isCurrentFrontier"]),
        "routePairs": sorted(route_pairs, key=lambda row: (not row["isCurrentFrontier"], row["source"], row["target"])),
        "manifestRecords": records,
        "eventRecords": [
            {
                "recordVaHex": hex32(event["va"]),
                "map": event["map"],
                "sceneIdHex": event.get("sceneIdHex"),
                "eventKind": event.get("eventKind"),
                "pointCount": event.get("pointCount", 0),
                "fieldLinks": event.get("fieldLinks") or [],
            }
            for event in event_records
        ],
    }


def build_summary(
    manifest: list[dict],
    events: list[dict],
    save_selector_refs: list[dict],
    frontier_rows: list[dict],
    render_reviews: dict[str, dict],
) -> dict:
    accepted = accepted_by_map(render_reviews)
    clusters = [
        summarize_cluster(cluster, save_selector_refs_by_va(save_selector_refs), accepted, frontier_pairs(frontier_rows))
        for cluster in cluster_nodes(node_rows(manifest, events))
    ]
    interesting = [
        cluster for cluster in clusters
        if cluster["eventRecordCount"] or cluster["saveSelectorRefCount"] or cluster["currentFrontierPairCount"]
    ]
    current_frontier = [cluster for cluster in interesting if cluster["currentFrontierPairCount"]]
    strict_event = [cluster for cluster in interesting if cluster["eventRecordCount"]]
    selector_only = [cluster for cluster in interesting if cluster["saveSelectorRefCount"] and not cluster["eventRecordCount"]]
    return {
        "source": SOURCE,
        "target": TARGET,
        "scope": "nearby field-map scene records and strict event records grouped into route-root candidates",
        "clusterGapHex": hex32(CLUSTER_GAP),
        "clusterCount": len(clusters),
        "interestingClusterCount": len(interesting),
        "strictEventLinkedClusterCount": len(strict_event),
        "selectorOnlyClusterCount": len(selector_only),
        "currentFrontierClusterCount": len(current_frontier),
        "proofFound": False,
        "fieldMapRecordRootsProofFound": False,
        "failedFieldMapRecordRootGateIds": FAILED_FIELD_MAP_RECORD_ROOT_GATE_IDS,
        "missingEvidence": FIELD_MAP_RECORD_ROOT_MISSING_EVIDENCE,
        "evidenceRefs": FIELD_MAP_RECORD_ROOT_EVIDENCE_REFS,
        "evidenceRefCount": len(FIELD_MAP_RECORD_ROOT_EVIDENCE_REFS),
        "promotionStatus": "blocked",
        "conclusion": (
            "The current map1_01a -> map2_02d frontier belongs to a save-selector-only record cluster. "
            "That cluster has no strict event record; its source and target scene tilesets now match the accepted renders "
            "after using preceding scene resources. Strict event-linked clusters should remain the safer source for "
            "promotable tile transitions until a source coordinate or equivalent hotspot is found."
        ),
        "clusters": interesting,
    }


def markdown(summary: dict) -> str:
    lines = [
        "# Field Map Record Roots",
        "",
        "Nearby manifest scene records and strict event records grouped by executable address.",
        "",
        f"- interesting clusters: {summary['interestingClusterCount']} / {summary['clusterCount']}",
        f"- strict event-linked clusters: {summary['strictEventLinkedClusterCount']}",
        f"- selector-only clusters: {summary['selectorOnlyClusterCount']}",
        f"- current frontier clusters: {summary['currentFrontierClusterCount']}",
        f"- proofFound: `{summary['proofFound']}`",
        f"- field-map record roots proof found: `{summary['fieldMapRecordRootsProofFound']}`",
        f"- failed field-map record root gates: {', '.join(summary['failedFieldMapRecordRootGateIds'])}",
        f"- missing evidence count: {len(summary['missingEvidence'])}",
        f"- evidence refs: {summary['evidenceRefCount']}",
        f"- promotion status: `{summary['promotionStatus']}`",
        "",
        summary["conclusion"],
        "",
        "## Failed Gates",
        "",
    ]
    lines.extend(f"- `{item}`" for item in summary["failedFieldMapRecordRootGateIds"])
    lines.extend([
        "",
        "## Missing Evidence",
        "",
    ])
    lines.extend(f"- {item}" for item in summary["missingEvidence"])
    lines.extend([
        "",
        "## Evidence Refs",
        "",
        "| path | fields |",
        "| --- | --- |",
    ])
    for ref in summary["evidenceRefs"]:
        lines.append(f"| `{ref.get('path')}` | {', '.join(ref.get('fields') or [])} |")
    lines.extend([
        "",
        "## Clusters",
        "",
        "| cluster | class | manifest maps | events | selector refs | current frontier | render mismatches | open |",
        "| --- | --- | --- | ---: | ---: | ---: | --- | --- |",
    ])
    for cluster in summary["clusters"]:
        mismatches = []
        for record in cluster["manifestRecords"]:
            render = record["render"]
            if render["acceptedTilesets"] and not render["matchesAccepted"]:
                mismatches.append(
                    f"{record['map']} `{','.join(render['recordTilesets'])}` -> `{','.join(render['acceptedTilesets'])}`"
                )
        first_map = (cluster["manifestMaps"] or cluster["eventSources"] or [""])[0]
        open_link = f"[open]({web_href({'map': first_map, 'events': '1', 'overview': '1'})})" if first_map else "-"
        lines.append(
            f"| `{cluster['clusterStartHex']}`..`{cluster['clusterEndHex']}` | {cluster['classification']} | "
            f"{', '.join(cluster['manifestMaps']) or '-'} | {cluster['eventRecordCount']} | "
            f"{cluster['saveSelectorRefCount']} | {cluster['currentFrontierPairCount']} | "
            f"{'; '.join(mismatches) or '-'} | {open_link} |"
        )
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    failed_gates = "".join(
        f"<li><code>{html.escape(item)}</code></li>"
        for item in summary.get("failedFieldMapRecordRootGateIds") or []
    )
    missing_evidence = "".join(
        f"<li>{html.escape(item)}</li>" for item in summary.get("missingEvidence") or []
    )
    evidence_refs = "".join(
        "<tr>"
        f"<td><code>{html.escape(ref.get('path') or '')}</code></td>"
        f"<td>{html.escape(', '.join(ref.get('fields') or []))}</td>"
        "</tr>"
        for ref in summary.get("evidenceRefs") or []
    )
    rows = []
    for cluster in summary["clusters"]:
        pairs = "<br>".join(
            f"{html.escape(row['source'])} -&gt; {html.escape(row['target'])}"
            + (" <strong>current</strong>" if row["isCurrentFrontier"] else "")
            for row in cluster["routePairs"][:10]
        ) or "-"
        records = "<br>".join(
            f"<code>{html.escape(record['recordVaHex'])}</code> {html.escape(record['map'])} "
            f"<code>{html.escape(','.join(record['tilesets']))}</code>"
            for record in cluster["manifestRecords"]
        ) or "-"
        events = "<br>".join(
            f"<code>{html.escape(event['recordVaHex'])}</code> {html.escape(event['map'])} "
            f"points {event['pointCount']} links {html.escape(','.join(event['fieldLinks']))}"
            for event in cluster["eventRecords"]
        ) or "-"
        mismatches = "<br>".join(
            f"{html.escape(record['map'])}: <code>{html.escape(','.join(record['render']['recordTilesets']))}</code> -> "
            f"<code>{html.escape(','.join(record['render']['acceptedTilesets']))}</code>"
            for record in cluster["manifestRecords"]
            if record["render"]["acceptedTilesets"] and not record["render"]["matchesAccepted"]
        ) or "-"
        first_map = (cluster["manifestMaps"] or cluster["eventSources"] or [""])[0]
        open_link = f'<a href="{html.escape(web_href({"map": first_map, "events": "1", "overview": "1"}))}">open</a>' if first_map else "-"
        rows.append(
            "<tr>"
            f"<td><code>{html.escape(cluster['clusterStartHex'])}</code><br><code>{html.escape(cluster['clusterEndHex'])}</code></td>"
            f"<td>{html.escape(cluster['classification'])}</td>"
            f"<td>{records}</td>"
            f"<td>{events}</td>"
            f"<td>{pairs}</td>"
            f"<td>{mismatches}</td>"
            f"<td>{open_link}</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>Field Map Record Roots</title>",
        "  <style>",
        "    body { margin: 24px; background: #101010; color: #eee; font: 14px system-ui, sans-serif; }",
        "    table { border-collapse: collapse; width: 100%; margin-bottom: 24px; }",
        "    th, td { border: 1px solid #333; padding: 6px 8px; vertical-align: top; }",
        "    th { background: #1d1d1d; }",
        "    code { color: #f5d76e; }",
        "    a { color: #9bd4ff; }",
        "  </style>",
        "</head>",
        "<body>",
        "  <h1>Field Map Record Roots</h1>",
        f"  <p>Interesting clusters: {summary['interestingClusterCount']} / {summary['clusterCount']}; "
        f"strict event-linked: {summary['strictEventLinkedClusterCount']}; selector-only: {summary['selectorOnlyClusterCount']}.</p>",
        "  <ul>",
        f"    <li>proofFound: <code>{html.escape(str(summary['proofFound']))}</code></li>",
        f"    <li>field-map record roots proof found: <code>{html.escape(str(summary['fieldMapRecordRootsProofFound']))}</code></li>",
        f"    <li>evidence refs: {summary['evidenceRefCount']}</li>",
        f"    <li>promotion status: <code>{html.escape(summary['promotionStatus'])}</code></li>",
        "  </ul>",
        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"  <table><thead><tr><th>path</th><th>fields</th></tr></thead><tbody>{evidence_refs}</tbody></table>",
        "  <h2>Clusters</h2>",
        "  <table><thead><tr><th>cluster</th><th>class</th><th>manifest records</th><th>events</th><th>route pairs</th><th>render mismatches</th><th>open</th></tr></thead>",
        f"  <tbody>{''.join(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 / "field_map_record_roots.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 load_json(path: Path, default):
    if not path.exists():
        return default
    return json.loads(path.read_text(encoding="utf-8"))


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    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.out_dir / "scene_manifest.json", []),
        load_json(args.out_dir / "scene_events.json", []),
        load_json(args.out_dir / "save_scene_selector_references.json", []),
        load_json(args.out_dir / "save_selector_frontier.json", []),
        load_json(args.out_dir / "map_render_reviews.json", {}),
    )
    write_outputs(summary, args.out_dir, args.html_out)
    print(f"wrote field map record roots -> {args.out_dir / 'field_map_record_roots.json'}")


if __name__ == "__main__":
    main()
