#!/usr/bin/env python3
"""Apply public savedat active-order samples to opcode 0x20 descriptor effects."""
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_FRONTIER_GROUP = 2
CURRENT_FRONTIER_SLOT = 0
FAILED_OPCODE20_SAMPLE_ORDER_GATE_IDS = [
    "current-selector-2-0-sample",
    "runtime-active-order",
    "non-pointer-context-a8-effect",
    "route-promotion-proof",
]
OPCODE20_SAMPLE_ORDER_MISSING_EVIDENCE = [
    "public or captured sample covering current selector 2:0 active order",
    "runtime active order/count on the current route path",
    "non-pointer context+0xa8 effect in active descriptors",
    "route-promotion proof linking sample order to map1_01a -> map2_02d",
]
OPCODE20_SAMPLE_ORDER_EVIDENCE_REFS = [
    {
        "path": "data/savedata_sample_party_order.json",
        "description": "public savedat active count/order byte samples",
    },
    {
        "path": "out/save_selector_opcode20_descriptor_scripts.json",
        "description": "descriptor+4 static effects applied to public sample orders",
    },
]


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


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


def descriptor_rows_by_index(descriptor_scripts: dict) -> dict[int, dict]:
    return {
        row["index"]: row
        for row in descriptor_scripts.get("descriptorRows") or []
        if isinstance(row.get("index"), int)
    }


def active_indices(sample: dict) -> list[int]:
    count = sample.get("activeSlotCount")
    order = sample.get("activeOrderBytes") or []
    if not isinstance(count, int):
        return []
    return [value for value in order[:count] if isinstance(value, int)]


def sample_signature(sample: dict) -> str:
    active = ",".join(str(value) for value in active_indices(sample))
    return f"{sample.get('selectorGroup')}:{sample.get('selectorSlot')} count={sample.get('activeSlotCount')} order=[{active}]"


def sample_effect(sample: dict, descriptors: dict[int, dict]) -> dict:
    effects = []
    final_shaped_context = None
    final_non_pointer_context = None
    missing_descriptor_indices = []
    for slot, descriptor_index in enumerate(active_indices(sample)):
        descriptor = descriptors.get(descriptor_index)
        if descriptor is None:
            missing_descriptor_indices.append(descriptor_index)
            effects.append({
                "activeSlot": slot,
                "descriptorIndex": descriptor_index,
                "missingDescriptor": True,
            })
            continue
        last_shaped_context = descriptor.get("script4LastContextA8SetterRow")
        last_non_pointer_context = descriptor.get("script4LastNonPointerContextA8SetterRow")
        if last_shaped_context:
            final_shaped_context = last_shaped_context
        if last_non_pointer_context:
            final_non_pointer_context = last_non_pointer_context
        effects.append({
            "activeSlot": slot,
            "descriptorIndex": descriptor_index,
            "descriptorVaHex": descriptor.get("descriptorVaHex"),
            "script0LinkedCns": descriptor.get("script0LinkedCns") or [],
            "script4VaHex": descriptor.get("script4VaHex"),
            "script4ContextA8SetterRowCount": descriptor.get("script4ContextA8SetterRowCount"),
            "script4ContextA8NonPointerSetterRowCount": descriptor.get("script4ContextA8NonPointerSetterRowCount"),
            "script4LastContextA8BaseExpression": (last_shaped_context or {}).get("baseExpression"),
            "script4LastContextA8OpcodeHex": (last_shaped_context or {}).get("opcodeHex"),
            "script4LastContextA8IsPointerDword": (last_shaped_context or {}).get("isPointerDword"),
            "script4LastNonPointerContextA8BaseExpression": (last_non_pointer_context or {}).get("baseExpression"),
            "script4LastNonPointerContextA8OpcodeHex": (last_non_pointer_context or {}).get("opcodeHex"),
            "missingDescriptor": False,
        })
    return {
        "id": sample.get("id"),
        "path": sample.get("path"),
        "source": sample.get("source"),
        "sourceUrl": sample.get("sourceUrl"),
        "selector": f"{sample.get('selectorGroup')}:{sample.get('selectorSlot')}",
        "selectorGroup": sample.get("selectorGroup"),
        "selectorSlot": sample.get("selectorSlot"),
        "tile": f"{sample.get('tileX')},{sample.get('tileY')}",
        "activeSlotCount": sample.get("activeSlotCount"),
        "activeOrderBytes": sample.get("activeOrderBytes") or [],
        "activeDescriptorIndices": active_indices(sample),
        "activeFlagByte": sample.get("activeFlagByte"),
        "signature": sample_signature(sample),
        "descriptorEffects": effects,
        "missingDescriptorIndices": missing_descriptor_indices,
        "finalShapedContextA8BaseExpression": (final_shaped_context or {}).get("baseExpression"),
        "finalShapedContextA8OpcodeHex": (final_shaped_context or {}).get("opcodeHex"),
        "finalShapedContextA8SetterVaHex": (final_shaped_context or {}).get("vaHex"),
        "finalShapedContextA8IsPointerDword": (final_shaped_context or {}).get("isPointerDword"),
        "finalNonPointerContextA8BaseExpression": (final_non_pointer_context or {}).get("baseExpression"),
        "finalNonPointerContextA8OpcodeHex": (final_non_pointer_context or {}).get("opcodeHex"),
        "finalNonPointerContextA8SetterVaHex": (final_non_pointer_context or {}).get("vaHex"),
        "coversCurrentFrontierSelector": (
            sample.get("selectorGroup") == CURRENT_FRONTIER_GROUP
            and sample.get("selectorSlot") == CURRENT_FRONTIER_SLOT
        ),
    }


def histogram(values: list[str | None]) -> list[dict]:
    counts: dict[str, int] = {}
    for value in values:
        key = value or "none"
        counts[key] = counts.get(key, 0) + 1
    return [
        {"value": value, "count": count}
        for value, count in sorted(counts.items())
    ]


def build_summary(samples: dict | None = None, descriptor_scripts: dict | None = None) -> dict:
    samples = samples if samples is not None else load_json(ROOT / "data" / "savedata_sample_party_order.json", {})
    descriptor_scripts = descriptor_scripts if descriptor_scripts is not None else load_json(
        OUT / "save_selector_opcode20_descriptor_scripts.json",
        {},
    )
    descriptors = descriptor_rows_by_index(descriptor_scripts)
    sample_rows = [
        sample_effect(sample, descriptors)
        for sample in samples.get("samples") or []
    ]
    sample_sources = []
    for sample in samples.get("samples") or []:
        source = sample.get("source") or samples.get("source")
        source_url = sample.get("sourceUrl") or samples.get("sourceUrl")
        item = {"source": source, "sourceUrl": source_url}
        if source and item not in sample_sources:
            sample_sources.append(item)
    unique_signatures = sorted({row["signature"] for row in sample_rows})
    current_frontier_covered = any(row["coversCurrentFrontierSelector"] for row in sample_rows)
    conclusion = (
        "The public sample saves make the opcode 0x20 count/order bytes concrete: "
        "selector 0:0 and 1:0 samples use count 1 with descriptor order [0], and progressed selector 22:0 samples "
        "use count 3 with [0,1,2]. "
        "Applying those orders to descriptor+4 static effects leaves only pointer-dword low-byte collisions in the "
        "last shaped context+0xa8 setter rows; there is no non-pointer context+0xa8 setter in the active descriptors "
        "covered by the public samples. None of the public samples covers the current frontier selector 2:0, so this "
        "remains calibration evidence rather than promotion proof for map1_01a -> map2_02d."
    )
    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,
        "activeSlotCountOffsetHex": ((samples.get("fields") or {}).get("activeSlotCountOffsetHex") or hex16(0x0010)),
        "activeOrderOffsetHex": ((samples.get("fields") or {}).get("activeOrderOffsetHex") or hex16(0x0011)),
        "sampleCount": len(sample_rows),
        "uniqueSampleStateCount": len(unique_signatures),
        "uniqueSampleSignatures": unique_signatures,
        "currentFrontierSelector": f"{CURRENT_FRONTIER_GROUP}:{CURRENT_FRONTIER_SLOT}",
        "currentFrontierSampleCovered": current_frontier_covered,
        "sampleRows": sample_rows,
        "sampleFinalShapedContextA8BaseHistogram": histogram([
            row["finalShapedContextA8BaseExpression"] for row in sample_rows
        ]),
        "sampleFinalNonPointerContextA8BaseHistogram": histogram([
            row["finalNonPointerContextA8BaseExpression"] for row in sample_rows
        ]),
        "allSamplesHaveKnownDescriptorIndices": all(not row["missingDescriptorIndices"] for row in sample_rows),
        "controlPathProofStatus": "blocked",
        "proofFound": False,
        "opcode20SampleOrderProofFound": False,
        "failedOpcode20SampleOrderGateIds": FAILED_OPCODE20_SAMPLE_ORDER_GATE_IDS,
        "missingEvidence": OPCODE20_SAMPLE_ORDER_MISSING_EVIDENCE,
        "evidenceRefs": OPCODE20_SAMPLE_ORDER_EVIDENCE_REFS,
        "evidenceRefCount": len(OPCODE20_SAMPLE_ORDER_EVIDENCE_REFS),
        "promotionStatus": "blocked",
        "conclusion": conclusion,
    }


def markdown(summary: dict) -> str:
    lines = [
        "# Save Selector Opcode 0x20 Sample Order Effects",
        "",
        f"- route: {summary['source']} -> {summary['target']}",
        f"- sample source: {summary['sampleSource']} ({summary['sampleSourceUrl']})",
        f"- active slot count offset: `{summary['activeSlotCountOffsetHex']}`",
        f"- active order offset: `{summary['activeOrderOffsetHex']}`",
        f"- sample count: {summary['sampleCount']}",
        f"- unique sample states: {summary['uniqueSampleStateCount']}",
        f"- current frontier selector: `{summary['currentFrontierSelector']}`",
        f"- current frontier sample covered: {summary['currentFrontierSampleCovered']}",
        f"- all samples have known descriptor indices: {summary['allSamplesHaveKnownDescriptorIndices']}",
        f"- proofFound: `{summary['proofFound']}`",
        f"- opcode20SampleOrderProofFound: `{summary['opcode20SampleOrderProofFound']}`",
        f"- promotion status: `{summary['promotionStatus']}`",
        "",
        summary["conclusion"],
        "",
        "## Failed Gates",
        "",
    ]
    lines.extend(f"- `{item}`" for item in summary["failedOpcode20SampleOrderGateIds"])
    lines.extend([
        "",
        "## Missing Evidence",
        "",
    ])
    lines.extend(f"- {item}" for item in summary["missingEvidence"])
    lines.extend([
        "",
        "## Evidence Refs",
        "",
    ])
    lines.extend(
        f"- `{row['path']}`: {row['description']}"
        for row in summary["evidenceRefs"]
    )
    lines.extend([
        "",
        "## Final Context+0xa8 Base Histogram",
        "",
        "| kind | base expression | sample count |",
        "| --- | --- | ---: |",
    ])
    for row in summary["sampleFinalShapedContextA8BaseHistogram"]:
        lines.append(f"| shaped | `{row['value']}` | {row['count']} |")
    for row in summary["sampleFinalNonPointerContextA8BaseHistogram"]:
        lines.append(f"| non-pointer | `{row['value']}` | {row['count']} |")
    lines.extend([
        "",
        "## Samples",
        "",
        "| sample | selector | tile | count | active descriptors | final shaped context+0xa8 base | final non-pointer context+0xa8 base | covers current selector |",
        "| --- | --- | --- | ---: | --- | --- | --- |",
    ])
    for row in summary["sampleRows"]:
        active = ",".join(str(value) for value in row["activeDescriptorIndices"])
        lines.append(
            f"| {row['id']} | `{row['selector']}` | `{row['tile']}` | {row['activeSlotCount']} | "
            f"`[{active}]` | `{row['finalShapedContextA8BaseExpression'] or '-'}` | "
            f"`{row['finalNonPointerContextA8BaseExpression'] or '-'}` | {row['coversCurrentFrontierSelector']} |"
        )
    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["failedOpcode20SampleOrderGateIds"]
    )
    missing_evidence = "".join(
        f"<li>{html.escape(item)}</li>"
        for item in summary["missingEvidence"]
    )
    evidence_refs = "".join(
        f"<li><code>{html.escape(row['path'])}</code>: {html.escape(row['description'])}</li>"
        for row in summary["evidenceRefs"]
    )
    histogram_rows = [
        f"<tr><td>shaped</td><td><code>{html.escape(row['value'])}</code></td><td>{row['count']}</td></tr>"
        for row in summary["sampleFinalShapedContextA8BaseHistogram"]
    ] + [
        f"<tr><td>non-pointer</td><td><code>{html.escape(row['value'])}</code></td><td>{row['count']}</td></tr>"
        for row in summary["sampleFinalNonPointerContextA8BaseHistogram"]
    ]
    sample_rows = []
    for row in summary["sampleRows"]:
        active = ",".join(str(value) for value in row["activeDescriptorIndices"])
        sample_rows.append(
            "<tr>"
            f"<td>{html.escape(row['id'] or '-')}</td>"
            f"<td><code>{html.escape(row['selector'])}</code></td>"
            f"<td><code>{html.escape(row['tile'])}</code></td>"
            f"<td>{row['activeSlotCount']}</td>"
            f"<td><code>[{html.escape(active)}]</code></td>"
            f"<td><code>{html.escape(row['finalShapedContextA8BaseExpression'] or '-')}</code></td>"
            f"<td><code>{html.escape(row['finalNonPointerContextA8BaseExpression'] or '-')}</code></td>"
            f"<td>{row['coversCurrentFrontierSelector']}</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>Save Selector Opcode 0x20 Sample Order Effects</title>",
        "  <style>",
        "    :root { color-scheme: dark; font-family: system-ui, sans-serif; background: #101010; color: #eee; }",
        "    body { margin: 0; padding: 24px; }",
        "    h1 { margin: 0 0 8px; font-size: 24px; }",
        "    h2 { margin: 24px 0 8px; font-size: 18px; }",
        "    p { max-width: 1120px; color: #bbb; line-height: 1.45; }",
        "    table { width: 100%; border-collapse: collapse; margin: 12px 0 20px; font-size: 13px; }",
        "    th, td { border-bottom: 1px solid #303030; padding: 7px 8px; text-align: left; vertical-align: top; }",
        "    th { position: sticky; top: 0; background: #181818; color: #ddd; }",
        "    code { color: #d7f0ff; }",
        "  </style>",
        "</head>",
        "<body>",
        "  <h1>Save Selector Opcode 0x20 Sample Order Effects</h1>",
        f"  <p>route {html.escape(summary['source'])} -&gt; {html.escape(summary['target'])}; sample source {html.escape(summary['sampleSource'] or '-')}; active slot count offset <code>{html.escape(summary['activeSlotCountOffsetHex'])}</code>; active order offset <code>{html.escape(summary['activeOrderOffsetHex'])}</code>; current frontier selector <code>{html.escape(summary['currentFrontierSelector'])}</code>; current frontier sample covered {summary['currentFrontierSampleCovered']}; proofFound <code>{summary['proofFound']}</code>; promotion status <code>{html.escape(summary['promotionStatus'])}</code>.</p>",
        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"  <ul>{evidence_refs}</ul>",
        "  <h2>Final Context+0xa8 Base Histogram</h2>",
        "  <table><thead><tr><th>kind</th><th>base expression</th><th>sample count</th></tr></thead><tbody>",
        *histogram_rows,
        "  </tbody></table>",
        "  <h2>Samples</h2>",
        "  <table><thead><tr><th>sample</th><th>selector</th><th>tile</th><th>count</th><th>active descriptors</th><th>final shaped context+0xa8 base</th><th>final non-pointer context+0xa8 base</th><th>covers current selector</th></tr></thead><tbody>",
        *sample_rows,
        "  </tbody></table>",
        "</body>",
        "</html>",
        "",
    ])


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


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--samples", type=Path, default=ROOT / "data" / "savedata_sample_party_order.json")
    parser.add_argument("--descriptor-scripts", type=Path, default=OUT / "save_selector_opcode20_descriptor_scripts.json")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    args = parser.parse_args()
    summary = build_summary(
        load_json(args.samples, {}),
        load_json(args.descriptor_scripts, {}),
    )
    write_outputs(summary, args.out_dir)
    print(f"wrote save selector opcode 0x20 sample order effects -> {args.out_dir / 'save_selector_opcode20_sample_order_effects.html'}")


if __name__ == "__main__":
    main()
