#!/usr/bin/env python3
"""Summarize runtime writers that materialize opcode 0x20 slot descriptors."""
from __future__ import annotations

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

sys.path.insert(0, str(Path(__file__).resolve().parent))

from probe_exe_scene_tables import offset_to_va, read_sections, va_to_offset


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"
SOURCE = "map1_01a"
TARGET = "map2_02d"
FAILED_OPCODE20_DESCRIPTOR_WRITER_GATE_IDS = [
    "runtime-active-order",
    "descriptor-materializer-execution",
    "descriptor-plus4-selection",
    "selected-root-execution-or-trace",
]
OPCODE20_DESCRIPTOR_WRITER_MISSING_EVIDENCE = [
    "runtime active order/count selecting descriptor table entries",
    "descriptor materializer routine execution on the current route path",
    "descriptor+4 script selected by opcode 0x20 mode 0 for selector 2:0",
    "selected-root execution or equivalent runtime trace reaching descriptor materialization",
]
OPCODE20_DESCRIPTOR_WRITER_EVIDENCE_REFS = [
    {
        "path": "Hwanse2.exe",
        "description": "descriptor table references and materializer routine writes",
    },
    {
        "path": "out/save_selector_opcode20_slot_sources.json",
        "description": "slot base/count source and runtime descriptor pointer requirement",
    },
]

DESCRIPTOR_TABLE_VA = 0x00442D95
COUNT_RUNTIME_VA = 0x004576E8
ORDER_BYTES_VA = 0x004576E9
SLOT_BASE_VA = 0x00457750
SLOT_STRIDE = 0x00D8
RUNTIME_SLOT_BASE_TABLE = 0x0059DB30
RUNTIME_OBJECT_TABLE = 0x0059DD70

ROUTINES = [
    {
        "routineVaHex": "0x0043215c",
        "name": "add active save/object slot",
        "role": "adds an id to 0x004576e9[count], stores slot base in 0x0059db30[count], increments count, then materializes descriptors",
        "orderWriteVaHex": "0x00432167",
        "slotBaseTableWriteVaHex": "0x00432188",
        "countWriteVaHex": "0x0043218f",
        "descriptorWriteVaHex": "0x004321cd",
        "descriptorScriptOffset": "0x00",
        "nestedRunVaHex": "0x004321fe",
        "objectStoreVaHex": "0x004322bd",
    },
    {
        "routineVaHex": "0x00432323",
        "name": "rebuild active slot descriptors",
        "role": "rebuilds active descriptors from 0x004576e9 order bytes and stores created runtime objects in 0x0059dd70",
        "orderWriteVaHex": "-",
        "slotBaseTableWriteVaHex": "0x004323ba",
        "countWriteVaHex": "-",
        "descriptorWriteVaHex": "0x004323dd",
        "descriptorScriptOffset": "0x00",
        "nestedRunVaHex": "0x0043240e",
        "objectStoreVaHex": "0x004324cd",
    },
    {
        "routineVaHex": "0x00432541",
        "name": "remove/rebuild active slot descriptors",
        "role": "runs descriptor+4 scripts for active objects, removes a matching id, compacts order/slot tables, then rematerializes descriptors",
        "orderWriteVaHex": "0x004326a3",
        "slotBaseTableWriteVaHex": "0x004326b6",
        "countWriteVaHex": "0x004326c2",
        "descriptorWriteVaHex": "0x0043271f",
        "descriptorScriptOffset": "0x00 after removal; 0x04 before removal",
        "nestedRunVaHex": "0x004325a7, 0x00432750",
        "objectStoreVaHex": "0x0043280f",
    },
]


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


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


def dword_at(exe: bytes, sections: list[dict], va: int) -> int | None:
    offset = va_to_offset(sections, va)
    if offset is None or offset + 4 > len(exe):
        return None
    return struct.unpack_from("<I", exe, offset)[0]


def pattern_hits(exe: bytes, sections: list[dict], pattern: bytes) -> list[int]:
    rows = []
    offset = 0
    while True:
        hit = exe.find(pattern, offset)
        if hit < 0:
            return rows
        va = offset_to_va(sections, hit)
        if va is not None:
            rows.append(va)
        offset = hit + 1


def dword_ref_summary(exe: bytes, sections: list[dict], value: int) -> dict:
    refs = pattern_hits(exe, sections, struct.pack("<I", value))
    return {
        "targetVaHex": hex32(value),
        "count": len(refs),
        "sampleRefs": [hex32(ref) for ref in refs[:24]],
    }


def descriptor_rows(exe: bytes, sections: list[dict], limit: int = 12) -> list[dict]:
    rows = []
    for index in range(limit):
        entry_va = DESCRIPTOR_TABLE_VA + index * 4
        descriptor = dword_at(exe, sections, entry_va)
        first = dword_at(exe, sections, descriptor) if descriptor else None
        second = dword_at(exe, sections, descriptor + 4) if descriptor else None
        third = dword_at(exe, sections, descriptor + 8) if descriptor else None
        rows.append({
            "index": index,
            "entryVaHex": hex32(entry_va),
            "descriptorVaHex": hex32(descriptor) if descriptor is not None else None,
            "script0VaHex": hex32(first) if first is not None else None,
            "script4VaHex": hex32(second) if second is not None else None,
            "script8VaHex": hex32(third) if third is not None else None,
        })
    return rows


def build_summary(
    exe: bytes,
    opcode20_slot_sources: dict | None = None,
) -> dict:
    sections = read_sections(exe)
    opcode20_slot_sources = (
        opcode20_slot_sources
        if opcode20_slot_sources is not None
        else load_json(OUT / "save_selector_opcode20_slot_sources.json", {})
    )
    descriptor_refs = dword_ref_summary(exe, sections, DESCRIPTOR_TABLE_VA)
    conclusion = (
        "The opcode 0x20 slot first dword is a runtime descriptor pointer, not a static object pointer. "
        "The materializer routines write descriptors from table 0x00442d95 into 0x00457750 + slot*0xd8, "
        "using active order bytes at 0x004576e9 and the runtime count at 0x004576e8. Opcode 0x20 mode 0 "
        "therefore follows descriptor+4 scripts only after these routines have run. The next proof target is "
        "the active order/count at the current frontier or the descriptor+4 script chosen for that order; route "
        "promotion remains blocked."
    )
    return {
        "source": SOURCE,
        "target": TARGET,
        "slotBaseHex": opcode20_slot_sources.get("slotBaseHex") or hex32(SLOT_BASE_VA),
        "slotStrideHex": opcode20_slot_sources.get("slotStrideHex") or "0x00d8",
        "countRuntimeVaHex": hex32(COUNT_RUNTIME_VA),
        "orderBytesVaHex": hex32(ORDER_BYTES_VA),
        "runtimeSlotBaseTableHex": hex32(RUNTIME_SLOT_BASE_TABLE),
        "runtimeObjectTableHex": hex32(RUNTIME_OBJECT_TABLE),
        "descriptorTableVaHex": hex32(DESCRIPTOR_TABLE_VA),
        "descriptorTableRefSummary": descriptor_refs,
        "descriptorRows": descriptor_rows(exe, sections),
        "routines": ROUTINES,
        "descriptorWriteCount": len(ROUTINES),
        "slotFirstDwordSource": "runtime descriptor table 0x00442d95[index]",
        "opcode20Mode0ScriptSource": "descriptor+4",
        "runtimeActiveOrderRequired": True,
        "controlPathProofStatus": "blocked",
        "proofFound": False,
        "opcode20DescriptorWriterProofFound": False,
        "failedOpcode20DescriptorWriterGateIds": FAILED_OPCODE20_DESCRIPTOR_WRITER_GATE_IDS,
        "missingEvidence": OPCODE20_DESCRIPTOR_WRITER_MISSING_EVIDENCE,
        "evidenceRefs": OPCODE20_DESCRIPTOR_WRITER_EVIDENCE_REFS,
        "evidenceRefCount": len(OPCODE20_DESCRIPTOR_WRITER_EVIDENCE_REFS),
        "promotionStatus": "blocked",
        "conclusion": conclusion,
    }


def markdown(summary: dict) -> str:
    lines = [
        "# Save Selector Opcode 0x20 Slot Descriptor Writers",
        "",
        f"- route: {summary['source']} -> {summary['target']}",
        f"- slot base: `{summary['slotBaseHex']}` stride `{summary['slotStrideHex']}`",
        f"- count byte: `{summary['countRuntimeVaHex']}`",
        f"- order bytes: `{summary['orderBytesVaHex']}`",
        f"- descriptor table: `{summary['descriptorTableVaHex']}`",
        f"- descriptor table refs: {summary['descriptorTableRefSummary']['count']}",
        f"- slot first dword source: {summary['slotFirstDwordSource']}",
        f"- opcode 0x20 mode 0 script source: `{summary['opcode20Mode0ScriptSource']}`",
        f"- runtime active order required: {summary['runtimeActiveOrderRequired']}",
        f"- proofFound: `{summary['proofFound']}`",
        f"- opcode20DescriptorWriterProofFound: `{summary['opcode20DescriptorWriterProofFound']}`",
        f"- promotion status: `{summary['promotionStatus']}`",
        "",
        summary["conclusion"],
        "",
        "## Failed Gates",
        "",
    ]
    lines.extend(f"- `{item}`" for item in summary["failedOpcode20DescriptorWriterGateIds"])
    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([
        "",
        "## Materializer Routines",
        "",
        "| routine | name | descriptor write | nested run | object store | role |",
        "| --- | --- | --- | --- | --- | --- |",
    ])
    for row in summary["routines"]:
        lines.append(
            f"| `{row['routineVaHex']}` | {row['name']} | `{row['descriptorWriteVaHex']}` | "
            f"`{row['nestedRunVaHex']}` | `{row['objectStoreVaHex']}` | {row['role']} |"
        )
    lines.extend([
        "",
        "## Descriptor Table Samples",
        "",
        "| index | entry | descriptor | script+0 | script+4 | script+8 |",
        "| ---: | --- | --- | --- | --- | --- |",
    ])
    for row in summary["descriptorRows"]:
        lines.append(
            f"| {row['index']} | `{row['entryVaHex']}` | `{row['descriptorVaHex']}` | "
            f"`{row['script0VaHex']}` | `{row['script4VaHex']}` | `{row['script8VaHex']}` |"
        )
    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["failedOpcode20DescriptorWriterGateIds"]
    )
    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"]
    )
    routine_rows = []
    for row in summary["routines"]:
        routine_rows.append(
            "<tr>"
            f"<td><code>{html.escape(row['routineVaHex'])}</code></td>"
            f"<td>{html.escape(row['name'])}</td>"
            f"<td><code>{html.escape(row['descriptorWriteVaHex'])}</code></td>"
            f"<td><code>{html.escape(row['nestedRunVaHex'])}</code></td>"
            f"<td><code>{html.escape(row['objectStoreVaHex'])}</code></td>"
            f"<td>{html.escape(row['role'])}</td>"
            "</tr>"
        )
    descriptor_rows = []
    for row in summary["descriptorRows"]:
        descriptor_rows.append(
            "<tr>"
            f"<td>{row['index']}</td>"
            f"<td><code>{html.escape(row['entryVaHex'])}</code></td>"
            f"<td><code>{html.escape(row['descriptorVaHex'] or '-')}</code></td>"
            f"<td><code>{html.escape(row['script0VaHex'] or '-')}</code></td>"
            f"<td><code>{html.escape(row['script4VaHex'] or '-')}</code></td>"
            f"<td><code>{html.escape(row['script8VaHex'] or '-')}</code></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 Slot Descriptor Writers</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 Slot Descriptor Writers</h1>",
        f"  <p>route {html.escape(summary['source'])} -&gt; {html.escape(summary['target'])}; descriptor table <code>{html.escape(summary['descriptorTableVaHex'])}</code>; slot base <code>{html.escape(summary['slotBaseHex'])}</code>; count byte <code>{html.escape(summary['countRuntimeVaHex'])}</code>; order bytes <code>{html.escape(summary['orderBytesVaHex'])}</code>; opcode 0x20 mode 0 script source <code>{html.escape(summary['opcode20Mode0ScriptSource'])}</code>; runtime active order required: {summary['runtimeActiveOrderRequired']}; 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>Materializer Routines</h2>",
        "  <table><thead><tr><th>routine</th><th>name</th><th>descriptor write</th><th>nested run</th><th>object store</th><th>role</th></tr></thead><tbody>",
        *routine_rows,
        "  </tbody></table>",
        "  <h2>Descriptor Table Samples</h2>",
        "  <table><thead><tr><th>index</th><th>entry</th><th>descriptor</th><th>script+0</th><th>script+4</th><th>script+8</th></tr></thead><tbody>",
        *descriptor_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_slot_descriptor_writers.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    (out_dir / "save_selector_opcode20_slot_descriptor_writers.html").write_text(html_page(summary), encoding="utf-8")


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--exe", type=Path, default=ROOT / "Hwanse2.exe")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    args = parser.parse_args()
    summary = build_summary(args.exe.read_bytes())
    write_outputs(summary, args.out_dir)
    print(
        "wrote save selector opcode 0x20 slot descriptor writers -> "
        f"{args.out_dir / 'save_selector_opcode20_slot_descriptor_writers.html'}"
    )


if __name__ == "__main__":
    main()
