#!/usr/bin/env python3
"""Summarize which save selectors are covered by public savedat samples."""
from __future__ import annotations

import argparse
import html
import json
from pathlib import Path
from typing import Any

from summarize_savedata_sample_deltas import (
    EXPECTED_SAVE_SIZE,
    sample_rows as savedata_delta_sample_rows,
    semantic_coverage as build_semantic_coverage,
)


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"
CURRENT_SELECTOR = "2:0"
SOURCE = "map1_01a"
TARGET = "map2_02d"


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


def selector_key(group: int | None, slot: int | None) -> str:
    return f"{group}:{slot}"


def selector_map(selectors: list[dict]) -> dict[str, dict]:
    return {
        selector_key(row.get("group"), row.get("slot")): row
        for row in selectors
    }


def combine_samples(gate_samples: dict, order_samples: dict, selectors: list[dict]) -> list[dict]:
    by_id: dict[str, dict] = {}
    selector_rows = selector_map(selectors)
    for sample in gate_samples.get("samples") or []:
        key = sample.get("id")
        if not key:
            continue
        by_id.setdefault(key, {}).update({
            "id": key,
            "path": sample.get("path"),
            "source": sample.get("source") or gate_samples.get("source"),
            "sourceUrl": sample.get("sourceUrl") or gate_samples.get("sourceUrl"),
            "size": sample.get("size"),
            "selectorGroup": sample.get("selectorGroup"),
            "selectorSlot": sample.get("selectorSlot"),
            "tileX": sample.get("tileX"),
            "tileY": sample.get("tileY"),
            "gateValues": sample.get("values") or {},
        })
    for sample in order_samples.get("samples") or []:
        key = sample.get("id")
        if not key:
            continue
        by_id.setdefault(key, {}).update({
            "id": key,
            "path": sample.get("path"),
            "source": sample.get("source") or order_samples.get("source"),
            "sourceUrl": sample.get("sourceUrl") or order_samples.get("sourceUrl"),
            "size": sample.get("size"),
            "selectorGroup": sample.get("selectorGroup"),
            "selectorSlot": sample.get("selectorSlot"),
            "tileX": sample.get("tileX"),
            "tileY": sample.get("tileY"),
            "activeSlotCount": sample.get("activeSlotCount"),
            "activeOrderBytes": sample.get("activeOrderBytes") or [],
            "activeFlagByte": sample.get("activeFlagByte"),
        })
    rows = []
    for sample in sorted(by_id.values(), key=lambda item: item["id"]):
        key = selector_key(sample.get("selectorGroup"), sample.get("selectorSlot"))
        selector = selector_rows.get(key) or {}
        field_maps = selector.get("fieldMaps") or []
        active_count = sample.get("activeSlotCount") or 0
        active_order = (sample.get("activeOrderBytes") or [])[:active_count]
        gate_values = sample.get("gateValues") or {}
        rows.append({
            **sample,
            "selector": key,
            "fieldMaps": field_maps,
            "coversCurrentSelector": key == CURRENT_SELECTOR,
            "coversSourceMap": SOURCE in field_maps,
            "coversTargetMap": TARGET in field_maps,
            "coversRoutePair": SOURCE in field_maps and TARGET in field_maps,
            "usableForRoutePromotion": key == CURRENT_SELECTOR and SOURCE in field_maps and TARGET in field_maps,
            "activeOrder": active_order,
            "saveRuntimeGateValuesHex": {
                offset: f"0x{value:02x}"
                for offset, value in gate_values.items()
                if offset in {"0x00e2", "0x00e4"}
            },
            "linearTrapGateValuesHex": {
                offset: f"0x{value:02x}"
                for offset, value in gate_values.items()
                if offset in {"0x00e8", "0x00ea"}
            },
        })
    return rows


def build_summary(
    gate_samples: dict | None = None,
    order_samples: dict | None = None,
    selectors: list[dict] | None = None,
) -> dict:
    gate_samples = gate_samples if gate_samples is not None else load_json(
        ROOT / "data" / "savedata_sample_gate_values.json",
        {},
    )
    order_samples = order_samples if order_samples is not None else load_json(
        ROOT / "data" / "savedata_sample_party_order.json",
        {},
    )
    selectors = selectors if selectors is not None else load_json(OUT / "save_scene_selectors.json", [])
    rows = combine_samples(gate_samples, order_samples, selectors)
    selectors_seen = sorted({row["selector"] for row in rows})
    sample_sources = []
    for row in rows:
        source = row.get("source")
        source_url = row.get("sourceUrl")
        item = {"source": source, "sourceUrl": source_url}
        if source and item not in sample_sources:
            sample_sources.append(item)
    selector_rows = selector_map(selectors)
    current = selector_rows.get(CURRENT_SELECTOR) or {}
    semantic = build_semantic_coverage(savedata_delta_sample_rows(selectors))
    conclusion = (
        "The public savedat corpus is useful calibration, but it does not cover the current frontier selector 2:0. "
        "The samples cover selector 0:0, which includes map1_01a/map1_02b, selector 1:0, which is target-side for "
        "map2_02d and related maps, and selector 22:0, which covers the map7_09k..map7_12k group. No sample covers "
        "both map1_01a and map2_02d in selector 2:0, so these bytes cannot promote map1_01a -> map2_02d. A real "
        "selector 2:0 save or equivalent runtime trace is still required."
    )
    return {
        "source": SOURCE,
        "target": TARGET,
        "sampleSource": ", ".join(row["source"] for row in sample_sources),
        "sampleSourceUrl": ", ".join(row["sourceUrl"] for row in sample_sources if row.get("sourceUrl")),
        "sampleSources": sample_sources,
        "sampleCount": len(rows),
        "sampleSelectors": selectors_seen,
        "uniqueSelectorCount": len(selectors_seen),
        "currentFrontierSelector": CURRENT_SELECTOR,
        "currentFrontierFieldMaps": current.get("fieldMaps") or [],
        "currentFrontierSampleCovered": any(row["coversCurrentSelector"] for row in rows),
        "sourceMapSampleCovered": any(row["coversSourceMap"] for row in rows),
        "targetMapSampleCovered": any(row["coversTargetMap"] for row in rows),
        "routePairSampleCovered": any(row["coversRoutePair"] for row in rows),
        "routePromotionSampleCount": sum(1 for row in rows if row["usableForRoutePromotion"]),
        "gateOffsetsHex": ["0x00e2", "0x00e4"],
        "linearTrapOffsetsHex": ["0x00e8", "0x00ea"],
        "activeSlotCountOffsetHex": "0x0010",
        "activeOrderOffsetHex": "0x0011",
        "activeFlagOffsetHex": "0x006c",
        "semanticCoverage": semantic,
        "samples": rows,
        "promotionStatus": "blocked",
        "remainingProofs": [
            "capture or construct a savedat sample whose scene selector is 2:0",
            "prove gate-time context+0xa8 base for selector 2:0",
            "find strict map1_01a source coordinate or equivalent runtime trigger",
        ],
        "conclusion": conclusion,
    }


def markdown(summary: dict) -> str:
    coverage = summary.get("semanticCoverage") or {}
    equipment = coverage.get("equipmentEvidence") or {}
    lines = [
        "# Savedata Sample Coverage",
        "",
        f"- route: {summary['source']} -> {summary['target']}",
        f"- sample source: {summary['sampleSource']} ({summary['sampleSourceUrl']})",
        f"- samples: {summary['sampleCount']}",
        f"- sample selectors: {', '.join(summary['sampleSelectors'])}",
        f"- current frontier selector: `{summary['currentFrontierSelector']}`",
        f"- current frontier sample covered: {summary['currentFrontierSampleCovered']}",
        f"- source map sample covered: {summary['sourceMapSampleCovered']}",
        f"- target map sample covered: {summary['targetMapSampleCovered']}",
        f"- route pair sample covered: {summary['routePairSampleCovered']}",
        f"- known semantic bytes: {coverage.get('knownSemanticByteCount', 0)}/{coverage.get('expectedSaveSize', EXPECTED_SAVE_SIZE)}",
        f"- unknown save bytes: {coverage.get('unknownByteCount', 0)}",
        f"- equipment/status/story offset status: `{coverage.get('equipmentStatusStoryOffsetStatus', '-')}`",
        f"- promotion status: `{summary['promotionStatus']}`",
        "",
        summary["conclusion"],
        "",
        "## Semantic Save Coverage",
        "",
        coverage.get("conclusion", ""),
        "",
        "| category | known bytes |",
        "| --- | ---: |",
    ]
    for category, count in (coverage.get("fieldCategoryByteCounts") or {}).items():
        lines.append(f"| {category} | {count} |")
    lines.extend([
        "",
        f"- known semantic fields: {coverage.get('knownFieldCount', 0)}",
        f"- known semantic ranges: {len(coverage.get('knownMergedRanges') or [])}",
        f"- unknown ranges: {coverage.get('unknownRangeCount', 0)}",
        f"- varying bytes: {coverage.get('varyingByteCount', 0)} "
        f"(known {coverage.get('knownVaryingByteCount', 0)}, unknown {coverage.get('unknownVaryingByteCount', 0)})",
        f"- equipment evidence status: `{equipment.get('status', '-')}`",
        f"- equipment evidence source: {equipment.get('source', '-')}",
        "",
        "Unsupported original systems:",
    ])
    lines.extend(f"- {item}" for item in coverage.get("unsupportedOriginalSystems") or [])
    lines.extend([
        "",
        "## Samples",
        "",
        "| sample | source | selector | tile | maps | active count/order | flag | gate e2/e4 | linear e8/ea | route proof |",
        "| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |",
    ])
    for row in summary["samples"]:
        maps = ", ".join(row["fieldMaps"]) or "-"
        active = f"{row.get('activeSlotCount')} / {row.get('activeOrder')}"
        save_gate = ", ".join(f"{key}={value}" for key, value in row["saveRuntimeGateValuesHex"].items()) or "-"
        linear_gate = ", ".join(f"{key}={value}" for key, value in row["linearTrapGateValuesHex"].items()) or "-"
        route_proof = "yes" if row["usableForRoutePromotion"] else "no"
        lines.append(
            f"| `{row['id']}` | {row.get('source') or '-'} | `{row['selector']}` | {row.get('tileX')},{row.get('tileY')} | "
            f"{maps} | `{active}` | `{row.get('activeFlagByte')}` | `{save_gate}` | `{linear_gate}` | {route_proof} |"
        )
    lines.extend(["", "## Remaining Proofs", ""])
    lines.extend(f"- {item}" for item in summary["remainingProofs"])
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    coverage = summary.get("semanticCoverage") or {}
    equipment = coverage.get("equipmentEvidence") or {}
    rows = []
    for row in summary["samples"]:
        maps = ", ".join(row["fieldMaps"]) or "-"
        active = f"{row.get('activeSlotCount')} / {row.get('activeOrder')}"
        save_gate = ", ".join(f"{key}={value}" for key, value in row["saveRuntimeGateValuesHex"].items()) or "-"
        linear_gate = ", ".join(f"{key}={value}" for key, value in row["linearTrapGateValuesHex"].items()) or "-"
        rows.append(
            "<tr>"
            f"<td><code>{html.escape(row['id'])}</code></td>"
            f"<td>{html.escape(row.get('source') or '-')}</td>"
            f"<td><code>{html.escape(row['selector'])}</code></td>"
            f"<td>{row.get('tileX')},{row.get('tileY')}</td>"
            f"<td>{html.escape(maps)}</td>"
            f"<td><code>{html.escape(active)}</code></td>"
            f"<td><code>{html.escape(str(row.get('activeFlagByte')))}</code></td>"
            f"<td><code>{html.escape(save_gate)}</code></td>"
            f"<td><code>{html.escape(linear_gate)}</code></td>"
            f"<td>{'yes' if row['usableForRoutePromotion'] else 'no'}</td>"
            "</tr>"
        )
    category_rows = []
    for category, count in (coverage.get("fieldCategoryByteCounts") or {}).items():
        category_rows.append(
            "<tr>"
            f"<td>{html.escape(category)}</td>"
            f"<td>{count}</td>"
            "</tr>"
        )
    unsupported = "".join(
        f"<li>{html.escape(item)}</li>"
        for item in coverage.get("unsupportedOriginalSystems") or []
    )
    proofs = "".join(f"<li>{html.escape(item)}</li>" for item in summary["remainingProofs"])
    return "\n".join([
        "<!doctype html>",
        '<html lang="en">',
        "<head>",
        '  <meta charset="utf-8">',
        '  <meta name="viewport" content="width=device-width, initial-scale=1">',
        "  <title>Savedata Sample Coverage</title>",
        "  <style>",
        "    body { margin: 24px; background: #111; color: #eee; font: 14px system-ui, sans-serif; }",
        "    table { border-collapse: collapse; width: 100%; margin: 18px 0 28px; }",
        "    th, td { border: 1px solid #3a3a3a; padding: 6px 8px; vertical-align: top; }",
        "    th { background: #202020; position: sticky; top: 0; }",
        "    code { color: #9bd4ff; }",
        "  </style>",
        "</head>",
        "<body>",
        "  <h1>Savedata Sample Coverage</h1>",
        f"  <p>route: {html.escape(summary['source'])} -&gt; {html.escape(summary['target'])}; samples {summary['sampleCount']}; current frontier selector <code>{summary['currentFrontierSelector']}</code>; current frontier sample covered: {summary['currentFrontierSampleCovered']}; promotion status <code>{summary['promotionStatus']}</code>.</p>",
        f"  <p>sample source: {html.escape(summary.get('sampleSource') or '')} ({html.escape(summary.get('sampleSourceUrl') or '')})</p>",
        f"  <p>{html.escape(summary['conclusion'])}</p>",
        "  <h2>Semantic Save Coverage</h2>",
        f"  <p>{html.escape(coverage.get('conclusion', ''))}</p>",
        (
            "  <p>"
            f"known semantic bytes {coverage.get('knownSemanticByteCount', 0)}/"
            f"{coverage.get('expectedSaveSize', EXPECTED_SAVE_SIZE)}; "
            f"unknown save bytes {coverage.get('unknownByteCount', 0)}; "
            f"unknown varying bytes {coverage.get('unknownVaryingByteCount', 0)}; "
            f"equipment/status/story offset status <code>{html.escape(coverage.get('equipmentStatusStoryOffsetStatus', '-'))}</code>; "
            f"equipment evidence <code>{html.escape(equipment.get('status', '-'))}</code>."
            "</p>"
        ),
        "  <table><thead><tr><th>category</th><th>known bytes</th></tr></thead>",
        f"  <tbody>{''.join(category_rows)}</tbody></table>",
        "  <h3>Unsupported Original Systems</h3>",
        f"  <ul>{unsupported}</ul>",
        "  <table><thead><tr><th>sample</th><th>source</th><th>selector</th><th>tile</th><th>maps</th><th>active count/order</th><th>flag</th><th>gate e2/e4</th><th>linear e8/ea</th><th>route proof</th></tr></thead>",
        f"  <tbody>{''.join(rows)}</tbody></table>",
        "  <h2>Remaining Proofs</h2>",
        f"  <ul>{proofs}</ul>",
        "</body>",
        "</html>",
        "",
    ])


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


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--out-dir", type=Path, default=OUT)
    args = parser.parse_args()
    summary = build_summary(
        load_json(ROOT / "data" / "savedata_sample_gate_values.json", {}),
        load_json(ROOT / "data" / "savedata_sample_party_order.json", {}),
        load_json(args.out_dir / "save_scene_selectors.json", []),
    )
    write_outputs(summary, args.out_dir)
    print(f"wrote savedata sample coverage -> {args.out_dir / 'savedata_sample_coverage.md'}")


if __name__ == "__main__":
    main()
