#!/usr/bin/env python3
"""Summarize provenance for the route-blocking selectionBuffer[0x20] value."""
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"
CURRENT_ROOT = "0x00540714"
SOURCE_SIDE_ROOT = "0x00501808"
PREDECESSOR_ROOT = "0x00478364"


def sort_hex(values: list[str]) -> list[str]:
    return sorted(values, key=lambda value: int(value, 16) if value.startswith("0x") else value)


def unique(values: list[Any]) -> list[Any]:
    seen = set()
    result = []
    for value in values:
        key = json.dumps(value, sort_keys=True, ensure_ascii=False) if isinstance(value, dict) else value
        if key in seen:
            continue
        seen.add(key)
        result.append(value)
    return result


def field_maps(context: dict | None) -> list[str]:
    maps = []
    for record in (context or {}).get("fieldRecords") or []:
        name = record.get("map")
        if name and name not in maps:
            maps.append(name)
    return maps


def labels(context: dict | None) -> list[str]:
    return list((context or {}).get("labels") or [])


def root_role(root_hex: str, maps: list[str]) -> str:
    has_source = SOURCE in maps
    has_target = TARGET in maps
    if root_hex == CURRENT_ROOT:
        return "current-route-root"
    if root_hex == SOURCE_SIDE_ROOT:
        return "source-side-root"
    if root_hex == PREDECESSOR_ROOT:
        return "predecessor-target-side-root"
    if has_source and has_target:
        return "route-pair-root"
    if has_source:
        return "source-side-root"
    if has_target:
        return "target-side-root"
    return "unrelated-root"


def build_root_summaries(selection_writers: dict) -> dict[str, dict]:
    roots: dict[str, dict] = {}
    for row in selection_writers.get("interestingOffsetRows") or []:
        context = row.get("selectorRootContext") or {}
        root_hex = context.get("selectedPointerHex") or "unattributed"
        summary = roots.setdefault(
            root_hex,
            {
                "rootHex": root_hex,
                "labels": [],
                "fieldMaps": [],
                "writerCount": 0,
                "readerCount": 0,
                "sampleRows": [],
            },
        )
        summary["labels"] = unique(summary["labels"] + labels(context))
        summary["fieldMaps"] = unique(summary["fieldMaps"] + field_maps(context))
        if row.get("operation") == "writer":
            summary["writerCount"] += 1
        elif row.get("operation") == "reader":
            summary["readerCount"] += 1
        if len(summary["sampleRows"]) < 8:
            summary["sampleRows"].append({
                "vaHex": row.get("vaHex"),
                "operation": row.get("operation"),
                "opcodeHex": row.get("opcodeHex"),
                "valueHex": row.get("valueHex"),
                "stateTable": row.get("stateTable"),
                "meaning": row.get("meaning"),
            })

    for summary in roots.values():
        maps = summary["fieldMaps"]
        summary["hasSourceMap"] = SOURCE in maps
        summary["hasTargetMap"] = TARGET in maps
        summary["containsRoutePair"] = SOURCE in maps and TARGET in maps
        summary["role"] = root_role(summary["rootHex"], maps)
    return roots


def root_by_label(roots: dict[str, dict], label: str) -> dict | None:
    for summary in roots.values():
        if label in (summary.get("labels") or []):
            return summary
    return None


def root_or_empty(roots: dict[str, dict], root_hex: str, label: str) -> dict:
    return roots.get(root_hex) or root_by_label(roots, label) or {
        "rootHex": root_hex,
        "labels": [label],
        "fieldMaps": [],
        "writerCount": 0,
        "readerCount": 0,
        "hasSourceMap": False,
        "hasTargetMap": False,
        "containsRoutePair": False,
        "role": "missing",
        "sampleRows": [],
    }


def local_writer_summary(selection_writers: dict, current_writer_paths: list[dict]) -> dict:
    writer_rows = selection_writers.get("currentFrontierRootWritersBeforeFirstReader") or []
    by_writer = {row.get("writerVaHex"): row for row in current_writer_paths}
    decorated = []
    for row in writer_rows:
        path = by_writer.get(row.get("vaHex")) or {}
        decorated.append({
            "writerVaHex": row.get("vaHex"),
            "writerValueHex": row.get("valueHex"),
            "opcodeHex": row.get("opcodeHex"),
            "stateTable": row.get("stateTable"),
            "meaning": row.get("meaning"),
            "streamStartHex": path.get("streamStartHex"),
            "classification": path.get("classification"),
            "nextQuestion": path.get("nextQuestion"),
        })
    nearest = max(
        (row for row in decorated if row.get("writerVaHex")),
        key=lambda row: int(row["writerVaHex"], 16),
        default=None,
    )
    return {
        "localWriterCountBeforeFirstReader": len(decorated),
        "localWritersBeforeFirstReader": decorated,
        "nearestCurrentWriter": nearest,
    }


def build_summary(selection_writers: dict, current_writer_paths: list[dict]) -> dict:
    roots = build_root_summaries(selection_writers)
    writer_root_count = sum(1 for row in roots.values() if row.get("writerCount", 0) > 0)
    reader_root_count = sum(1 for row in roots.values() if row.get("readerCount", 0) > 0)
    current_root = root_or_empty(roots, CURRENT_ROOT, "2:0")
    source_side_root = root_or_empty(roots, SOURCE_SIDE_ROOT, "0:0")
    predecessor_root = root_or_empty(roots, PREDECESSOR_ROOT, "1:0")
    route_related_roots = [
        row for row in roots.values()
        if row.get("hasSourceMap") or row.get("hasTargetMap") or row.get("rootHex") in {CURRENT_ROOT, SOURCE_SIDE_ROOT, PREDECESSOR_ROOT}
    ]
    route_related_roots.sort(
        key=lambda row: (
            0 if row.get("rootHex") == CURRENT_ROOT else
            1 if row.get("rootHex") == SOURCE_SIDE_ROOT else
            2 if row.get("rootHex") == PREDECESSOR_ROOT else
            3 if row.get("containsRoutePair") else
            4 if row.get("hasSourceMap") else
            5,
            row.get("rootHex") or "",
        )
    )
    top_writer_roots = sorted(
        roots.values(),
        key=lambda row: (-row.get("writerCount", 0), row.get("rootHex") or ""),
    )[:12]
    top_reader_roots = sorted(
        roots.values(),
        key=lambda row: (-row.get("readerCount", 0), row.get("rootHex") or ""),
    )[:12]
    frontier_rows = selection_writers.get("currentFrontierRows") or []
    frontier_reader = frontier_rows[0] if frontier_rows else None
    local_writers = local_writer_summary(selection_writers, current_writer_paths)
    nearest = local_writers.get("nearestCurrentWriter") or {}
    conclusion = (
        "selectionBuffer[0x20] is a broad VM selection slot. The current 2:0 root contains both source and "
        "target scene records, but source-only 0:0 and predecessor target-only 1:0 roots show that root-level "
        "reader/writer counts are not enough to promote map1_01a -> map2_02d. Runtime branch-state values, "
        "control flow to the 0x00542b0c reader, and a strict source hotspot remain required."
    )
    return {
        "source": SOURCE,
        "target": TARGET,
        "selectionBufferOffsetHex": "0x20",
        "writerCountFor0x20": selection_writers.get("writerCountFor0x20"),
        "readerCountFor0x20": selection_writers.get("readerCountFor0x20"),
        "writerRootCount": writer_root_count,
        "readerRootCount": reader_root_count,
        "currentRoot": current_root,
        "sourceSideRoot": source_side_root,
        "predecessorRoot": predecessor_root,
        "routeRelatedRoots": route_related_roots,
        "topWriterRoots": top_writer_roots,
        "topReaderRoots": top_reader_roots,
        "frontierReader": {
            "readerVaHex": frontier_reader.get("vaHex"),
            "valueHex": frontier_reader.get("valueHex"),
            "stateTable": frontier_reader.get("stateTable"),
            "condition": frontier_reader.get("meaning"),
            "branchTargetHex": frontier_reader.get("branchTargetHex"),
            "branchTargetKind": frontier_reader.get("branchTargetKind"),
            "leafPointerHex": (frontier_reader.get("selectorContext") or {}).get("leafPointerHex"),
            "rootHex": (frontier_reader.get("selectorRootContext") or {}).get("selectedPointerHex"),
        } if frontier_reader else None,
        "currentLocalWriters": local_writers,
        "brief": {
            "currentSelector": ",".join(current_root.get("labels") or []),
            "currentRootHex": current_root.get("rootHex"),
            "currentWriterCount": current_root.get("writerCount"),
            "currentReaderCount": current_root.get("readerCount"),
            "writerRootCount": writer_root_count,
            "readerRootCount": reader_root_count,
            "nearestCurrentWriter": nearest.get("writerVaHex"),
            "frontierReader": frontier_reader.get("vaHex") if frontier_reader else None,
            "sourceSideSelector": ",".join(source_side_root.get("labels") or []),
            "predecessorSelector": ",".join(predecessor_root.get("labels") or []),
            "promotionStatus": "blocked",
        },
        "promotionStatus": "blocked",
        "remainingProofs": [
            "Capture or emulate the runtime branch-state values used by 0x005428bc and 0x00542b0c.",
            "Prove the control path that reaches the 0x00542b0c reader in selector 2:0.",
            "Find a strict map1_01a source coordinate/hotspot for map2_02d.",
        ],
        "conclusion": conclusion,
    }


def root_label(row: dict) -> str:
    labels_text = ",".join(row.get("labels") or []) or "-"
    return f"{labels_text} {row.get('rootHex') or '-'}"


def maps_text(row: dict, limit: int = 8) -> str:
    maps = row.get("fieldMaps") or []
    if not maps:
        return "-"
    text = ", ".join(maps[:limit])
    if len(maps) > limit:
        text += f", +{len(maps) - limit}"
    return text


def markdown(summary: dict) -> str:
    current = summary["currentRoot"]
    source_side = summary["sourceSideRoot"]
    predecessor = summary["predecessorRoot"]
    nearest = (summary.get("currentLocalWriters") or {}).get("nearestCurrentWriter") or {}
    reader = summary.get("frontierReader") or {}
    lines = [
        "# Selection Buffer 0x20 Provenance",
        "",
        f"- source -> target: `{summary['source']} -> {summary['target']}`.",
        f"- writer candidates: {summary.get('writerCountFor0x20')}.",
        f"- reader candidates: {summary.get('readerCountFor0x20')}.",
        f"- writer roots: {summary.get('writerRootCount')}.",
        f"- reader roots: {summary.get('readerRootCount')}.",
        f"- current root 2:0: `{current.get('rootHex')}` writers={current.get('writerCount')} readers={current.get('readerCount')} routePair={current.get('containsRoutePair')}.",
        f"- source-side root 0:0: `{source_side.get('rootHex')}` writers={source_side.get('writerCount')} readers={source_side.get('readerCount')} sourceOnly={source_side.get('hasSourceMap') and not source_side.get('hasTargetMap')}.",
        f"- predecessor root 1:0: `{predecessor.get('rootHex')}` writers={predecessor.get('writerCount')} readers={predecessor.get('readerCount')} targetOnly={predecessor.get('hasTargetMap') and not predecessor.get('hasSourceMap')}.",
        f"- nearest current writer: {nearest.get('writerVaHex') or '-'} `{nearest.get('writerValueHex') or '-'}`.",
        f"- frontier reader: {reader.get('readerVaHex') or '-'} `{reader.get('valueHex') or '-'}`.",
        f"- promotion status: {summary.get('promotionStatus')}.",
        "",
        summary.get("conclusion") or "",
        "",
        "## Route Related Roots",
        "",
        "| role | root | writers | readers | source | target | maps |",
        "| --- | --- | ---: | ---: | --- | --- | --- |",
    ]
    for row in summary.get("routeRelatedRoots") or []:
        lines.append(
            f"| {row.get('role')} | {root_label(row)} | {row.get('writerCount')} | {row.get('readerCount')} | "
            f"{row.get('hasSourceMap')} | {row.get('hasTargetMap')} | {maps_text(row)} |"
        )
    lines.extend([
        "",
        "## Local Writers Before Frontier Reader",
        "",
        "| writer | value | stream | kind | next question |",
        "| --- | --- | --- | --- | --- |",
    ])
    for row in (summary.get("currentLocalWriters") or {}).get("localWritersBeforeFirstReader") or []:
        lines.append(
            f"| {row.get('writerVaHex')} | `{row.get('writerValueHex')}` | {row.get('streamStartHex') or '-'} | "
            f"{row.get('classification') or '-'} | {row.get('nextQuestion') or '-'} |"
        )
    lines.extend([
        "",
        "## Top Writer Roots",
        "",
        "| root | writers | readers | role | maps |",
        "| --- | ---: | ---: | --- | --- |",
    ])
    for row in summary.get("topWriterRoots") or []:
        lines.append(
            f"| {root_label(row)} | {row.get('writerCount')} | {row.get('readerCount')} | {row.get('role')} | {maps_text(row, 5)} |"
        )
    lines.extend([
        "",
        "## Remaining Proofs",
        "",
    ])
    for proof in summary.get("remainingProofs") or []:
        lines.append(f"- {proof}")
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    def esc(value: Any) -> str:
        return html.escape(str(value))

    current = summary["currentRoot"]
    source_side = summary["sourceSideRoot"]
    predecessor = summary["predecessorRoot"]
    nearest = (summary.get("currentLocalWriters") or {}).get("nearestCurrentWriter") or {}
    reader = summary.get("frontierReader") or {}
    route_rows = []
    for row in summary.get("routeRelatedRoots") or []:
        route_rows.append(
            "<tr>"
            f"<td>{esc(row.get('role'))}</td>"
            f"<td><code>{esc(row.get('rootHex'))}</code><br>{esc(','.join(row.get('labels') or []) or '-')}</td>"
            f"<td>{esc(row.get('writerCount'))}</td>"
            f"<td>{esc(row.get('readerCount'))}</td>"
            f"<td>{esc(row.get('hasSourceMap'))}</td>"
            f"<td>{esc(row.get('hasTargetMap'))}</td>"
            f"<td>{esc(maps_text(row))}</td>"
            "</tr>"
        )
    writer_rows = []
    for row in (summary.get("currentLocalWriters") or {}).get("localWritersBeforeFirstReader") or []:
        writer_rows.append(
            "<tr>"
            f"<td><code>{esc(row.get('writerVaHex'))}</code></td>"
            f"<td><code>{esc(row.get('writerValueHex'))}</code></td>"
            f"<td><code>{esc(row.get('streamStartHex') or '-')}</code></td>"
            f"<td>{esc(row.get('classification') or '-')}</td>"
            f"<td>{esc(row.get('nextQuestion') or '-')}</td>"
            "</tr>"
        )
    top_rows = []
    for row in summary.get("topWriterRoots") or []:
        top_rows.append(
            "<tr>"
            f"<td><code>{esc(row.get('rootHex'))}</code><br>{esc(','.join(row.get('labels') or []) or '-')}</td>"
            f"<td>{esc(row.get('writerCount'))}</td>"
            f"<td>{esc(row.get('readerCount'))}</td>"
            f"<td>{esc(row.get('role'))}</td>"
            f"<td>{esc(maps_text(row, 5))}</td>"
            "</tr>"
        )
    proof_items = "".join(f"<li>{esc(proof)}</li>" for proof in summary.get("remainingProofs") or [])
    return "\n".join([
        "<!doctype html>",
        '<html lang="en">',
        "<head>",
        '  <meta charset="utf-8">',
        '  <meta name="viewport" content="width=device-width, initial-scale=1">',
        "  <title>Selection Buffer 0x20 Provenance</title>",
        "  <style>body{margin:24px;background:#101010;color:#eee;font:14px system-ui,sans-serif}table{border-collapse:collapse;width:100%;margin:16px 0 28px}th,td{border:1px solid #333;padding:6px 8px;vertical-align:top}th{background:#1d1d1d;position:sticky;top:0}code{color:#f5d76e}</style>",
        "</head>",
        "<body>",
        "  <h1>Selection Buffer 0x20 Provenance</h1>",
        f"  <p><code>{esc(summary['source'])}</code> -&gt; <code>{esc(summary['target'])}</code>. "
        f"Writers: {esc(summary.get('writerCountFor0x20'))}. Readers: {esc(summary.get('readerCountFor0x20'))}. "
        f"writer roots: {esc(summary.get('writerRootCount'))}. reader roots: {esc(summary.get('readerRootCount'))}.</p>",
        f"  <p>current root 2:0: <code>{esc(current.get('rootHex'))}</code> writers={esc(current.get('writerCount'))} readers={esc(current.get('readerCount'))} routePair={esc(current.get('containsRoutePair'))}.</p>",
        f"  <p>source-side root 0:0: <code>{esc(source_side.get('rootHex'))}</code> writers={esc(source_side.get('writerCount'))} readers={esc(source_side.get('readerCount'))} sourceOnly={esc(source_side.get('hasSourceMap') and not source_side.get('hasTargetMap'))}.</p>",
        f"  <p>predecessor root 1:0: <code>{esc(predecessor.get('rootHex'))}</code> writers={esc(predecessor.get('writerCount'))} readers={esc(predecessor.get('readerCount'))} targetOnly={esc(predecessor.get('hasTargetMap') and not predecessor.get('hasSourceMap'))}.</p>",
        f"  <p>nearest current writer: <code>{esc(nearest.get('writerVaHex') or '-')}</code>. frontier reader: <code>{esc(reader.get('readerVaHex') or '-')}</code>. promotion status: {esc(summary.get('promotionStatus'))}.</p>",
        f"  <p>{esc(summary.get('conclusion') or '')}</p>",
        "  <h2>Route Related Roots</h2>",
        "  <table><thead><tr><th>role</th><th>root</th><th>writers</th><th>readers</th><th>source</th><th>target</th><th>maps</th></tr></thead>",
        f"  <tbody>{''.join(route_rows)}</tbody></table>",
        "  <h2>Local Writers Before Frontier Reader</h2>",
        "  <table><thead><tr><th>writer</th><th>value</th><th>stream</th><th>kind</th><th>next question</th></tr></thead>",
        f"  <tbody>{''.join(writer_rows)}</tbody></table>",
        "  <h2>Top Writer Roots</h2>",
        "  <table><thead><tr><th>root</th><th>writers</th><th>readers</th><th>role</th><th>maps</th></tr></thead>",
        f"  <tbody>{''.join(top_rows)}</tbody></table>",
        "  <h2>Remaining Proofs</h2>",
        f"  <ul>{proof_items}</ul>",
        "</body>",
        "</html>",
        "",
    ])


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


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--selection-writers", type=Path, default=OUT / "save_selector_selection_writers.json")
    parser.add_argument("--current-writer-paths", type=Path, default=OUT / "save_selector_current_writer_paths.json")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    args = parser.parse_args()
    summary = build_summary(
        json.loads(args.selection_writers.read_text(encoding="utf-8")),
        json.loads(args.current_writer_paths.read_text(encoding="utf-8")),
    )
    write_outputs(summary, args.out_dir)
    print(
        "wrote selectionBuffer[0x20] provenance "
        f"({summary['writerRootCount']} writer roots, {summary['readerRootCount']} reader roots) "
        f"-> {args.out_dir / 'save_selector_selection_buffer20_provenance.html'}"
    )


if __name__ == "__main__":
    main()
