#!/usr/bin/env python3
"""Summarize the save-selector dispatch stop that resolves into .data."""
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
from summarize_script_handler_table import HANDLER_TABLE_VA, handler_for_opcode, section_name_for_va


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"

ROUTE_SOURCE = "map1_01a"
ROUTE_TARGET = "map2_02d"
DISPATCH_STOP_VA = 0x005428F4
FRONTIER_LEAF = 0x00542AE8
FRONTIER_READER = 0x00542B0C
SCAN_SECTIONS = {".text", ".data", ".rdata"}


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


def hex32(value: int | None) -> str | None:
    return f"0x{value:08x}" if value is not None else None


def parse_hex(value: str | None) -> int | None:
    return int(value, 16) if value else None


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 section_for_offset(sections: list[dict], offset: int) -> dict | None:
    for section in sections:
        start = section["raw"]
        end = start + section["raw_size"]
        if start <= offset < end:
            return section
    return None


def value_refs(exe: bytes, sections: list[dict], value: int) -> list[dict]:
    needle = struct.pack("<I", value)
    refs = []
    search = 0
    while True:
        hit = exe.find(needle, search)
        if hit < 0:
            break
        search = hit + 1
        section = section_for_offset(sections, hit)
        if section is None or section["name"] not in SCAN_SECTIONS:
            continue
        ref_va = offset_to_va(sections, hit)
        if ref_va is None:
            continue
        refs.append({
            "section": section["name"],
            "fileOffsetHex": f"0x{hit:06x}",
            "refVaHex": hex32(ref_va),
            "valueHex": hex32(value),
        })
    return refs


def dword_row(exe: bytes, sections: list[dict], va: int) -> dict:
    value = dword_at(exe, sections, va)
    section = section_name_for_va(sections, value) if value is not None else None
    row = {
        "vaHex": hex32(va),
        "valueHex": hex32(value),
        "valueSection": section,
    }
    if value is None:
        row["kind"] = "unreadable"
    elif section:
        row["kind"] = "pointer"
    else:
        row["kind"] = "scalar"
    return row


def dword_window(exe: bytes, sections: list[dict], start_va: int, count: int) -> list[dict]:
    return [dword_row(exe, sections, start_va + index * 4) for index in range(count)]


def route_gate(gate_paths: list[dict]) -> dict:
    return next(
        (
            row for row in gate_paths
            if row.get("source") == ROUTE_SOURCE and row.get("target") == ROUTE_TARGET
        ),
        {},
    )


def build_summary(exe: bytes, gate_paths: list[dict] | None = None) -> dict:
    sections = read_sections(exe)
    gate = route_gate(gate_paths or [])
    stop_va = parse_hex(gate.get("dispatchStopVaHex")) or DISPATCH_STOP_VA
    stop_value = dword_at(exe, sections, stop_va)
    stop_opcode = stop_value & 0xFF if stop_value is not None else parse_hex(gate.get("dispatchStopOpcodeHex"))
    handler = handler_for_opcode(exe, sections, stop_opcode or 0)
    entry_va = HANDLER_TABLE_VA + (stop_opcode or 0) * 4
    entry_value = dword_at(exe, sections, entry_va)
    entry_section = section_name_for_va(sections, entry_value) if entry_value is not None else None
    target_refs = value_refs(exe, sections, entry_value) if entry_value is not None else []
    target_ref_sections = sorted({ref["section"] for ref in target_refs})
    target_window_start = (entry_value or 0) if entry_value is not None else 0
    table_window_start = entry_va - 0x20
    dispatch_window_start = stop_va - 0x10
    conclusion = (
        "The dispatch stop at 0x005428f4 uses opcode low byte 0xe8, whose handler-table entry "
        "0x00440ac0 resolves to 0x00440c28 in .data. The target has only .data pointer refs in "
        "the scanned executable sections, so this is data descriptor context, not an executable code handler. "
        "It reinforces the stop at 0x005428f4 and does not prove that the 0x005428bc writer path selects "
        "frontier leaf 0x00542ae8 or reaches reader 0x00542b0c."
    )
    return {
        "source": ROUTE_SOURCE,
        "target": ROUTE_TARGET,
        "dispatchStopVaHex": hex32(stop_va),
        "dispatchStopValueHex": hex32(stop_value),
        "dispatchStopOpcodeHex": f"0x{(stop_opcode or 0):02x}",
        "handlerTableVaHex": hex32(HANDLER_TABLE_VA),
        "handlerTableEntryVaHex": hex32(entry_va),
        "handlerTableEntryValueHex": hex32(entry_value),
        "entryValueHex": hex32(entry_value),
        "entryValueSection": entry_section,
        "entryValueIsCodeHandler": entry_section == ".text",
        "handlerForOpcode": handler,
        "gateDispatchStopHandlerVaHex": gate.get("dispatchStopHandlerVaHex"),
        "gateDispatchStopHandlerSection": gate.get("dispatchStopHandlerSection"),
        "gateDispatchStopIsCodeHandler": gate.get("dispatchStopIsCodeHandler"),
        "frontierLeafHex": hex32(FRONTIER_LEAF),
        "frontierReaderHex": hex32(FRONTIER_READER),
        "targetPointerRefCount": len(target_refs),
        "targetPointerRefSections": target_ref_sections,
        "targetPointerRefs": target_refs,
        "handlerTableEntryWindow": dword_window(exe, sections, table_window_start, 32),
        "targetDescriptorWindow": dword_window(exe, sections, target_window_start, 32) if entry_value is not None else [],
        "dispatchStopWindow": dword_window(exe, sections, dispatch_window_start, 12),
        "dispatchStopIsDescriptorContext": entry_section == ".data" and target_ref_sections == [".data"],
        "promotionStatus": "blocked",
        "remainingProofs": [
            "prove which descriptor/table row the 0x24 action-state boundary selects",
            "prove execution reaches frontier leaf 0x00542ae8 before reader 0x00542b0c",
            "find a strict map1_01a source coordinate or hotspot",
        ],
        "conclusion": conclusion,
    }


def markdown(summary: dict) -> str:
    lines = [
        "# Save Selector Dispatch Stop Context",
        "",
        f"- route: {summary['source']} -> {summary['target']}",
        f"- dispatch stop: `{summary['dispatchStopVaHex']}` value `{summary['dispatchStopValueHex']}` opcode `{summary['dispatchStopOpcodeHex']}`",
        f"- handler table: `{summary['handlerTableVaHex']}`",
        f"- handler table entry: `{summary['handlerTableEntryVaHex']}` -> `{summary['entryValueHex']}` in {summary['entryValueSection']}",
        f"- entry value is code handler: {summary['entryValueIsCodeHandler']}",
        f"- target pointer refs: {summary['targetPointerRefCount']} in {', '.join(summary['targetPointerRefSections']) or '-'}",
        f"- descriptor context: {summary['dispatchStopIsDescriptorContext']}",
        f"- frontier leaf: `{summary['frontierLeafHex']}`",
        f"- frontier reader: `{summary['frontierReaderHex']}`",
        f"- promotion status: {summary['promotionStatus']}",
        "",
        summary["conclusion"],
        "",
        "## Target Pointer Refs",
        "",
        "| ref va | section | file offset | value |",
        "| --- | --- | --- | --- |",
    ]
    for ref in summary["targetPointerRefs"]:
        lines.append(
            f"| `{ref['refVaHex']}` | {ref['section']} | `{ref['fileOffsetHex']}` | `{ref['valueHex']}` |"
        )
    lines.extend(["", "## Handler Table Entry Window", "", "| va | value | kind | value section |", "| --- | --- | --- | --- |"])
    for row in summary["handlerTableEntryWindow"]:
        lines.append(
            f"| `{row['vaHex']}` | `{row['valueHex']}` | {row['kind']} | {row.get('valueSection') or '-'} |"
        )
    lines.extend(["", "## Target Descriptor Window", "", "| va | value | kind | value section |", "| --- | --- | --- | --- |"])
    for row in summary["targetDescriptorWindow"]:
        lines.append(
            f"| `{row['vaHex']}` | `{row['valueHex']}` | {row['kind']} | {row.get('valueSection') or '-'} |"
        )
    lines.extend(["", "## Dispatch Stop Window", "", "| va | value | kind | value section |", "| --- | --- | --- | --- |"])
    for row in summary["dispatchStopWindow"]:
        lines.append(
            f"| `{row['vaHex']}` | `{row['valueHex']}` | {row['kind']} | {row.get('valueSection') or '-'} |"
        )
    lines.extend(["", "## Remaining Proofs", ""])
    lines.extend(f"- {item}" for item in summary["remainingProofs"])
    lines.append("")
    return "\n".join(lines)


def html_table(rows: list[dict], columns: list[tuple[str, str]]) -> str:
    header = "".join(f"<th>{html.escape(label)}</th>" for _key, label in columns)
    body = []
    for row in rows:
        body.append(
            "<tr>"
            + "".join(
                f"<td><code>{html.escape(str(row.get(key) or '-'))}</code></td>"
                for key, _label in columns
            )
            + "</tr>"
        )
    return f"<table><thead><tr>{header}</tr></thead><tbody>{''.join(body)}</tbody></table>"


def html_page(summary: dict) -> str:
    ref_rows = [
        {
            "refVaHex": ref["refVaHex"],
            "section": ref["section"],
            "fileOffsetHex": ref["fileOffsetHex"],
            "valueHex": ref["valueHex"],
        }
        for ref in summary["targetPointerRefs"]
    ]
    proof_items = "".join(f"<li>{html.escape(item)}</li>" for item in summary["remainingProofs"])
    dword_columns = [
        ("vaHex", "va"),
        ("valueHex", "value"),
        ("kind", "kind"),
        ("valueSection", "value section"),
    ]
    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 Dispatch Stop Context</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>Save Selector Dispatch Stop Context</h1>",
        f"  <p>route: {html.escape(summary['source'])} -&gt; {html.escape(summary['target'])}; dispatch stop <code>{summary['dispatchStopVaHex']}</code>; promotion status: {html.escape(summary['promotionStatus'])}.</p>",
        f"  <p>handler table <code>{summary['handlerTableVaHex']}</code>; handler table entry <code>{summary['handlerTableEntryVaHex']}</code> resolves to <code>{summary['entryValueHex']}</code> in {html.escape(str(summary['entryValueSection']))}; entry value is code handler: {summary['entryValueIsCodeHandler']}.</p>",
        f"  <p>target pointer refs: {summary['targetPointerRefCount']} in {html.escape(', '.join(summary['targetPointerRefSections']) or '-')}; descriptor context: {summary['dispatchStopIsDescriptorContext']}.</p>",
        f"  <p>{html.escape(summary['conclusion'])}</p>",
        "  <h2>Target Pointer Refs</h2>",
        html_table(ref_rows, [("refVaHex", "ref va"), ("section", "section"), ("fileOffsetHex", "file offset"), ("valueHex", "value")]),
        "  <h2>Handler Table Entry Window</h2>",
        html_table(summary["handlerTableEntryWindow"], dword_columns),
        "  <h2>Target Descriptor Window</h2>",
        html_table(summary["targetDescriptorWindow"], dword_columns),
        "  <h2>Dispatch Stop Window</h2>",
        html_table(summary["dispatchStopWindow"], dword_columns),
        "  <h2>Remaining Proofs</h2>",
        f"  <ul>{proof_items}</ul>",
        "</body>",
        "</html>",
        "",
    ])


def write_outputs(summary: dict, out_dir: Path = OUT, html_out: Path | None = None) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "save_selector_dispatch_stop_context.json").write_text(
        json.dumps(summary, ensure_ascii=False, separators=(",", ":")),
        encoding="utf-8",
    )
    if html_out is not None:
        html_out.parent.mkdir(parents=True, exist_ok=True)
        html_out.write_text(html_page(summary), encoding="utf-8")


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--exe", type=Path, default=ROOT / "Hwanse2.exe")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    parser.add_argument("--html-out", type=Path, default=None, help="Optional HTML report output path.")
    args = parser.parse_args()
    summary = build_summary(
        args.exe.read_bytes(),
        load_json(args.out_dir / "save_selector_gate_paths.json", []),
    )
    write_outputs(summary, args.out_dir, args.html_out)
    print(f"wrote save selector dispatch stop context -> {args.out_dir / 'save_selector_dispatch_stop_context.json'}")


if __name__ == "__main__":
    main()
