#!/usr/bin/env python3
"""Summarize runtime sources for opcode 0x20 slot count and slot objects."""
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_SLOT_SOURCE_GATE_IDS = [
    "runtime-slot-count",
    "runtime-slot-descriptor-pointers",
    "active-order-runtime-state",
    "selected-root-execution-or-trace",
]
OPCODE20_SLOT_SOURCE_MISSING_EVIDENCE = [
    "runtime slot count byte at 0x004576e8 on the current route path",
    "runtime descriptor pointers materialized at 0x00457750 + slot*0xd8",
    "active order/count values for current selector 2:0",
    "selected-root execution or equivalent runtime trace reaching opcode 0x20",
]
OPCODE20_SLOT_SOURCE_EVIDENCE_REFS = [
    {
        "path": "Hwanse2.exe",
        "description": "static slot/count byte locations and direct-reference scans",
    },
    {
        "path": "out/save_loader_trace.json",
        "description": "save block mapping for count byte and slot base runtime addresses",
    },
    {
        "path": "out/save_selector_opcode20_nested_base_modes.json",
        "description": "current opcode 0x20 mode and nested script path classification",
    },
]

CURRENT_OPCODE20_VA = 0x005428A8
COUNT_RUNTIME_VA = 0x004576E8
FIRST_SAVE_BLOCK_BASE = 0x004576D8
SECOND_SAVE_BLOCK_BASE = 0x00457750
SECOND_SAVE_BLOCK_SIZE = 0x0288
SLOT_STRIDE = 0x00D8
SLOT_SCAN_COUNT = 6


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


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


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


def byte_at(exe: bytes, sections: list[dict], va: int) -> int | None:
    offset = va_to_offset(sections, va)
    if offset is None or offset >= len(exe):
        return None
    return exe[offset]


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_refs(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 save_block_for_runtime_va(save_loader_trace: dict, runtime_va: int) -> dict | None:
    for row in save_loader_trace.get("saveReadBlocks") or []:
        target = int(row["targetVaHex"], 16)
        size = int(row["sizeHex"], 16)
        if target <= runtime_va < target + size:
            save_offset = int(row["saveOffsetHex"], 16) + runtime_va - target
            return {
                "callVaHex": row["callVaHex"],
                "targetVaHex": row["targetVaHex"],
                "sizeHex": row["sizeHex"],
                "description": row["description"],
                "saveOffsetHex": hex16(save_offset),
                "offsetWithinBlockHex": hex16(runtime_va - target),
            }
    return None


def slot_rows(exe: bytes, sections: list[dict], save_loader_trace: dict) -> list[dict]:
    rows = []
    for slot in range(SLOT_SCAN_COUNT):
        base = SECOND_SAVE_BLOCK_BASE + slot * SLOT_STRIDE
        first_dword = dword_at(exe, sections, base)
        plus4_dword = dword_at(exe, sections, base + 4)
        save_block = save_block_for_runtime_va(save_loader_trace, base)
        rows.append({
            "slot": slot,
            "slotBaseHex": hex32(base),
            "saveOffsetHex": save_block["saveOffsetHex"] if save_block else None,
            "insideLoadedSecondBlock": save_block is not None,
            "staticFirstDwordHex": hex32(first_dword) if first_dword is not None else None,
            "staticPlus4DwordHex": hex32(plus4_dword) if plus4_dword is not None else None,
            "staticFirstDwordLooksPointer": bool(first_dword and 0x00400000 <= first_dword <= 0x00600000),
            "runtimeSlotDescriptorPointerRequired": True,
        })
    return rows


def build_summary(
    exe: bytes,
    save_loader_trace: dict | None = None,
    opcode20_nested_base_modes: dict | None = None,
) -> dict:
    sections = read_sections(exe)
    save_loader_trace = save_loader_trace if save_loader_trace is not None else load_json(
        OUT / "save_loader_trace.json",
        {},
    )
    opcode20_nested_base_modes = (
        opcode20_nested_base_modes
        if opcode20_nested_base_modes is not None
        else load_json(OUT / "save_selector_opcode20_nested_base_modes.json", {})
    )

    count_block = save_block_for_runtime_va(save_loader_trace, COUNT_RUNTIME_VA)
    second_block = save_block_for_runtime_va(save_loader_trace, SECOND_SAVE_BLOCK_BASE)
    slots = slot_rows(exe, sections, save_loader_trace)
    loaded_slot_capacity = SECOND_SAVE_BLOCK_SIZE // SLOT_STRIDE
    loaded_slot_remainder = SECOND_SAVE_BLOCK_SIZE % SLOT_STRIDE
    static_first_dword_nonzero = sum(1 for row in slots if row["staticFirstDwordHex"] != "0x00000000")
    conclusion = (
        "Opcode 0x20 mode 0 is controlled by runtime state, not by a static leaf table. "
        "The loop count byte 0x004576e8 sits inside the first save-read block at save offset 0x0010, "
        "while the 0x00457750 slot area is the second save-read block and contains only three complete "
        "0xd8-byte slots. The static first dwords of the scanned slots are zero, so slot descriptor pointers must be "
        "materialized at runtime before descriptor+4 nested scripts can run. This keeps the current "
        "map1_01a -> map2_02d gate proof blocked until runtime slot count and descriptor pointer values are proven."
    )
    return {
        "source": SOURCE,
        "target": TARGET,
        "currentOpcode20VaHex": hex32(CURRENT_OPCODE20_VA),
        "currentOpcode20ValueHex": opcode20_nested_base_modes.get("currentOpcode20ValueHex"),
        "currentMode": opcode20_nested_base_modes.get("currentMode"),
        "currentModeHex": opcode20_nested_base_modes.get("currentModeHex"),
        "countRuntimeVaHex": hex32(COUNT_RUNTIME_VA),
        "countSaveSource": count_block,
        "countStaticInitialByte": byte_at(exe, sections, COUNT_RUNTIME_VA),
        "countStaticInitialByteHex": f"0x{byte_at(exe, sections, COUNT_RUNTIME_VA):02x}",
        "countDirectRefSummary": dword_refs(exe, sections, COUNT_RUNTIME_VA),
        "slotBaseHex": hex32(SECOND_SAVE_BLOCK_BASE),
        "slotStrideHex": hex16(SLOT_STRIDE),
        "slotSaveSource": second_block,
        "loadedSecondBlockSizeHex": hex16(SECOND_SAVE_BLOCK_SIZE),
        "loadedCompleteSlotCapacity": loaded_slot_capacity,
        "loadedSlotRemainderHex": hex16(loaded_slot_remainder),
        "slotBaseDirectRefSummary": dword_refs(exe, sections, SECOND_SAVE_BLOCK_BASE),
        "slotRows": slots,
        "scannedSlotCount": len(slots),
        "staticFirstDwordNonzeroCount": static_first_dword_nonzero,
        "runtimeSlotCountRequired": True,
        "runtimeSlotDescriptorPointersRequired": True,
        "controlPathProofStatus": "blocked",
        "proofFound": False,
        "opcode20SlotSourceProofFound": False,
        "failedOpcode20SlotSourceGateIds": FAILED_OPCODE20_SLOT_SOURCE_GATE_IDS,
        "missingEvidence": OPCODE20_SLOT_SOURCE_MISSING_EVIDENCE,
        "evidenceRefs": OPCODE20_SLOT_SOURCE_EVIDENCE_REFS,
        "evidenceRefCount": len(OPCODE20_SLOT_SOURCE_EVIDENCE_REFS),
        "promotionStatus": "blocked",
        "conclusion": conclusion,
    }


def markdown(summary: dict) -> str:
    count_source = summary["countSaveSource"] or {}
    slot_source = summary["slotSaveSource"] or {}
    lines = [
        "# Save Selector Opcode 0x20 Slot Sources",
        "",
        f"- route: {summary['source']} -> {summary['target']}",
        f"- opcode 0x20 row: `{summary['currentOpcode20VaHex']}` value `{summary['currentOpcode20ValueHex']}` ({summary['currentMode']})",
        f"- count byte: `{summary['countRuntimeVaHex']}` save offset `{count_source.get('saveOffsetHex', '-')}`",
        f"- count static initial byte: `{summary['countStaticInitialByteHex']}`",
        f"- slot base: `{summary['slotBaseHex']}` save offset `{slot_source.get('saveOffsetHex', '-')}`",
        f"- slot stride: `{summary['slotStrideHex']}`",
        f"- loaded complete slot capacity: {summary['loadedCompleteSlotCapacity']}",
        f"- scanned slots with nonzero static first dword: {summary['staticFirstDwordNonzeroCount']}",
        f"- runtime slot count required: {summary['runtimeSlotCountRequired']}",
        f"- runtime slot descriptor pointers required: {summary['runtimeSlotDescriptorPointersRequired']}",
        f"- control path proof status: `{summary['controlPathProofStatus']}`",
        f"- proofFound: `{summary['proofFound']}`",
        f"- opcode20SlotSourceProofFound: `{summary['opcode20SlotSourceProofFound']}`",
        f"- promotion status: `{summary['promotionStatus']}`",
        "",
        summary["conclusion"],
        "",
        "## Failed Gates",
        "",
    ]
    lines.extend(f"- `{item}`" for item in summary["failedOpcode20SlotSourceGateIds"])
    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([
        "",
        "## Save Sources",
        "",
        "| field | runtime VA | save offset | read call | block size | description | direct refs |",
        "| --- | --- | --- | --- | --- | --- | ---: |",
        (
            f"| count byte | `{summary['countRuntimeVaHex']}` | `{count_source.get('saveOffsetHex', '-')}` | "
            f"`{count_source.get('callVaHex', '-')}` | `{count_source.get('sizeHex', '-')}` | "
            f"{count_source.get('description', '-')} | {summary['countDirectRefSummary']['count']} |"
        ),
        (
            f"| slot base | `{summary['slotBaseHex']}` | `{slot_source.get('saveOffsetHex', '-')}` | "
            f"`{slot_source.get('callVaHex', '-')}` | `{slot_source.get('sizeHex', '-')}` | "
            f"{slot_source.get('description', '-')} | {summary['slotBaseDirectRefSummary']['count']} |"
        ),
        "",
        "## Scanned Slots",
        "",
        "| slot | base | save offset | inside loaded block | static first dword | static +4 dword | pointer-like |",
        "| ---: | --- | --- | --- | --- | --- | --- |",
    ])
    for row in summary["slotRows"]:
        lines.append(
            f"| {row['slot']} | `{row['slotBaseHex']}` | `{row['saveOffsetHex'] or '-'}` | "
            f"{row['insideLoadedSecondBlock']} | `{row['staticFirstDwordHex'] or '-'}` | "
            f"`{row['staticPlus4DwordHex'] or '-'}` | {row['staticFirstDwordLooksPointer']} |"
        )
    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["failedOpcode20SlotSourceGateIds"]
    )
    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"]
    )
    count_source = summary["countSaveSource"] or {}
    slot_source = summary["slotSaveSource"] or {}
    save_rows = [
        [
            "count byte",
            summary["countRuntimeVaHex"],
            count_source.get("saveOffsetHex", "-"),
            count_source.get("callVaHex", "-"),
            count_source.get("sizeHex", "-"),
            count_source.get("description", "-"),
            str(summary["countDirectRefSummary"]["count"]),
        ],
        [
            "slot base",
            summary["slotBaseHex"],
            slot_source.get("saveOffsetHex", "-"),
            slot_source.get("callVaHex", "-"),
            slot_source.get("sizeHex", "-"),
            slot_source.get("description", "-"),
            str(summary["slotBaseDirectRefSummary"]["count"]),
        ],
    ]
    save_rows_html = [
        "<tr>" + "".join(f"<td>{html.escape(value)}</td>" for value in row) + "</tr>"
        for row in save_rows
    ]
    slot_rows_html = []
    for row in summary["slotRows"]:
        slot_rows_html.append(
            "<tr>"
            f"<td>{row['slot']}</td>"
            f"<td><code>{html.escape(row['slotBaseHex'])}</code></td>"
            f"<td><code>{html.escape(row['saveOffsetHex'] or '-')}</code></td>"
            f"<td>{row['insideLoadedSecondBlock']}</td>"
            f"<td><code>{html.escape(row['staticFirstDwordHex'] or '-')}</code></td>"
            f"<td><code>{html.escape(row['staticPlus4DwordHex'] or '-')}</code></td>"
            f"<td>{row['staticFirstDwordLooksPointer']}</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 Sources</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 Sources</h1>",
        f"  <p>route {html.escape(summary['source'])} -&gt; {html.escape(summary['target'])}; opcode row <code>{html.escape(summary['currentOpcode20VaHex'])}</code>; count byte <code>{html.escape(summary['countRuntimeVaHex'])}</code> save offset <code>{html.escape(count_source.get('saveOffsetHex', '-'))}</code>; slot base <code>{html.escape(summary['slotBaseHex'])}</code>; slot stride <code>{html.escape(summary['slotStrideHex'])}</code>; loaded complete slot capacity: {summary['loadedCompleteSlotCapacity']}; runtime slot count required: {summary['runtimeSlotCountRequired']}; runtime slot descriptor pointers required: {summary['runtimeSlotDescriptorPointersRequired']}; 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>Save Sources</h2>",
        "  <table><thead><tr><th>field</th><th>runtime VA</th><th>save offset</th><th>read call</th><th>block size</th><th>description</th><th>direct refs</th></tr></thead><tbody>",
        *save_rows_html,
        "  </tbody></table>",
        "  <h2>Scanned Slots</h2>",
        "  <table><thead><tr><th>slot</th><th>base</th><th>save offset</th><th>inside loaded block</th><th>static first dword</th><th>static +4 dword</th><th>pointer-like</th></tr></thead><tbody>",
        *slot_rows_html,
        "  </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_sources.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    (out_dir / "save_selector_opcode20_slot_sources.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(f"wrote save selector opcode 0x20 slot sources -> {args.out_dir / 'save_selector_opcode20_slot_sources.html'}")


if __name__ == "__main__":
    main()
