#!/usr/bin/env python3
"""Summarize whether the opcode 0x24 payload table reaches the frontier leaf table."""
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 read_sections, va_to_offset
from summarize_script_handler_table import handler_for_opcode, section_name_for_va


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

ROUTE_SOURCE = "map1_01a"
ROUTE_TARGET = "map2_02d"
OPCODE24_VA = 0x005428E4
PAYLOAD_START = 0x005428E8
PAYLOAD_END = 0x005429A4
DISPATCH_STOP_REF = 0x005428F4
LEAF_TABLE_START = 0x005429A8
LEAF_TABLE_ROOT = 0x005429DC
WRAPPER_LEAF = 0x00542A04
FRONTIER_LEAF = 0x00542AE8
FRONTIER_READER = 0x00542B0C

TARGET_VALUES = {
    LEAF_TABLE_START: "leaf-table-window-start",
    LEAF_TABLE_ROOT: "root-table-pointer",
    WRAPPER_LEAF: "wrapper-leaf",
    FRONTIER_LEAF: "frontier-leaf",
    FRONTIER_READER: "frontier-reader",
}


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) -> str:
    return f"0x{value:08x}"


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 row_at(exe: bytes, sections: list[dict], va: int) -> dict:
    value = dword_at(exe, sections, va)
    row = {
        "va": va,
        "vaHex": hex32(va),
        "value": value,
        "valueHex": hex32(value) if value is not None else None,
        "insidePayloadWindow": PAYLOAD_START <= va <= PAYLOAD_END,
    }
    if value is None:
        row["kind"] = "unreadable"
        return row
    opcode = value & 0xFF
    handler = handler_for_opcode(exe, sections, opcode)
    row.update({
        "lowByteHex": f"0x{opcode:02x}",
        "lowByteHandlerVaHex": handler.get("handlerVaHex"),
        "lowByteHandlerSection": handler.get("handlerSection"),
        "lowByteIsCodeHandler": handler.get("isCodeHandler"),
    })
    section = section_name_for_va(sections, value)
    if PAYLOAD_START <= value <= PAYLOAD_END:
        row["kind"] = "payload-pointer"
        row["targetVaHex"] = hex32(value)
    elif LEAF_TABLE_START <= value <= FRONTIER_READER:
        row["kind"] = "leaf-table-pointer"
        row["targetVaHex"] = hex32(value)
        row["targetMeaning"] = TARGET_VALUES.get(value, "leaf-table-window-value")
    elif value in TARGET_VALUES:
        row["kind"] = "frontier-target"
        row["targetVaHex"] = hex32(value)
        row["targetMeaning"] = TARGET_VALUES[value]
    elif section:
        row["kind"] = "pointer"
        row["targetVaHex"] = hex32(value)
        row["targetSection"] = section
    elif (value >> 16) <= 0x400 and (value & 0xFFFF) <= 0x10000:
        row["kind"] = "pair"
    else:
        row["kind"] = "scalar"
    return row


def pointer_closure(rows_by_va: dict[int, dict], start_va: int, max_steps: int = 12) -> list[dict]:
    rows = []
    seen: set[int] = set()
    va = start_va
    for step in range(max_steps):
        if va in seen:
            rows.append({"step": step, "vaHex": hex32(va), "stopReason": "loop"})
            break
        seen.add(va)
        source = rows_by_va.get(va)
        if not source:
            rows.append({"step": step, "vaHex": hex32(va), "stopReason": "not-in-payload-window"})
            break
        target = source.get("value")
        item = {
            "step": step,
            "vaHex": source["vaHex"],
            "valueHex": source.get("valueHex"),
            "kind": source.get("kind"),
            "targetVaHex": source.get("targetVaHex"),
        }
        if source.get("kind") in {"payload-pointer", "leaf-table-pointer", "frontier-target"} and isinstance(target, int):
            rows.append(item)
            if target in TARGET_VALUES or target >= LEAF_TABLE_START:
                rows.append({
                    "step": step + 1,
                    "vaHex": hex32(target),
                    "stopReason": "reached-frontier-target",
                })
                break
            va = target
            continue
        item["stopReason"] = "non-pointer-value"
        rows.append(item)
        break
    return rows


def payload_pointer_edges(table_rows: list[dict]) -> list[dict]:
    edges = []
    for row in table_rows:
        target = parse_hex(row.get("targetVaHex"))
        if target is None:
            continue
        target_kind = "payload-window" if PAYLOAD_START <= target <= PAYLOAD_END else "external"
        if target in TARGET_VALUES:
            target_kind = TARGET_VALUES[target]
        elif LEAF_TABLE_START <= target <= FRONTIER_READER:
            target_kind = "leaf-table-window"
        edges.append({
            "sourceVaHex": row["vaHex"],
            "targetVaHex": row["targetVaHex"],
            "targetKind": target_kind,
            "localPayloadEdge": target_kind == "payload-window",
            "frontierTargetEdge": target_kind != "payload-window",
        })
    return edges


def payload_pointer_components(edges: list[dict]) -> list[dict]:
    adjacency: dict[str, set[str]] = {}
    edge_by_source: dict[str, list[dict]] = {}
    for edge in edges:
        source = edge["sourceVaHex"]
        target = edge["targetVaHex"]
        adjacency.setdefault(source, set()).add(target)
        adjacency.setdefault(target, set()).add(source)
        edge_by_source.setdefault(source, []).append(edge)

    components = []
    seen: set[str] = set()
    for node in sorted(adjacency):
        if node in seen:
            continue
        stack = [node]
        nodes: set[str] = set()
        while stack:
            current = stack.pop()
            if current in nodes:
                continue
            nodes.add(current)
            stack.extend(sorted(adjacency.get(current, set()) - nodes))
        seen.update(nodes)
        component_edges = [
            edge
            for source in sorted(nodes)
            for edge in edge_by_source.get(source, [])
        ]
        external_targets = sorted({
            edge["targetVaHex"]
            for edge in component_edges
            if not edge.get("localPayloadEdge")
        })
        reaches_frontier = any(edge.get("frontierTargetEdge") for edge in component_edges)
        components.append({
            "component": len(components),
            "nodes": sorted(nodes),
            "edges": component_edges,
            "externalTargets": external_targets,
            "closedInsidePayload": not external_targets,
            "reachesFrontierTarget": reaches_frontier,
        })
    return components


def build_summary(exe: bytes, gate_paths: list[dict] | None = None) -> dict:
    sections = read_sections(exe)
    table_rows = [row_at(exe, sections, va) for va in range(PAYLOAD_START, PAYLOAD_END + 1, 4)]
    rows_by_va = {row["va"]: row for row in table_rows}
    payload_pointers = [row for row in table_rows if row.get("kind") == "payload-pointer"]
    leaf_table_pointers = [row for row in table_rows if row.get("kind") in {"leaf-table-pointer", "frontier-target"}]
    data_low_bytes = [row for row in table_rows if row.get("lowByteHandlerSection") == ".data"]
    graph_edges = payload_pointer_edges(table_rows)
    graph_components = payload_pointer_components(graph_edges)
    graph_external_edges = [edge for edge in graph_edges if not edge.get("localPayloadEdge")]
    closure = pointer_closure(rows_by_va, DISPATCH_STOP_REF)
    reached_targets = [
        item for item in closure
        if item.get("stopReason") == "reached-frontier-target"
        or parse_hex(item.get("targetVaHex")) in TARGET_VALUES
    ]
    route_gate = next(
        (
            row for row in (gate_paths or [])
            if row.get("source") == ROUTE_SOURCE and row.get("target") == ROUTE_TARGET
        ),
        {},
    )
    conclusion = (
        "The opcode 0x24 payload/action table immediately before the leaf table is pointer-local: "
        "its dword pointers stay inside 0x005428e8..0x005429a4, and none point at the leaf table start, "
        "the 2:0 root table pointer, wrapper leaf 0x00542a04, frontier leaf 0x00542ae8, or reader 0x00542b0c. "
        "The closure from the naive dispatch stop 0x005428f4 stops at 0x005428e8 on a non-pointer value. "
        "This rules out using the opcode 0x24 payload table as proof that the 0x005428bc writer path selects "
        "the frontier leaf; promotion remains blocked."
    )
    return {
        "source": ROUTE_SOURCE,
        "target": ROUTE_TARGET,
        "opcode24VaHex": hex32(OPCODE24_VA),
        "dispatchStopVaHex": hex32(DISPATCH_STOP_REF),
        "payloadWindowHex": f"{hex32(PAYLOAD_START)}..{hex32(PAYLOAD_END)}",
        "leafTableWindowStartHex": hex32(LEAF_TABLE_START),
        "rootTablePointerHex": hex32(LEAF_TABLE_ROOT),
        "wrapperLeafHex": hex32(WRAPPER_LEAF),
        "frontierLeafHex": hex32(FRONTIER_LEAF),
        "frontierReaderHex": hex32(FRONTIER_READER),
        "gateDispatchStopHex": route_gate.get("dispatchStopVaHex"),
        "gateFrontierLeafHex": route_gate.get("frontierLeafPointerHex"),
        "tableRows": table_rows,
        "payloadPointerCount": len(payload_pointers),
        "payloadPointers": payload_pointers,
        "leafTablePointerCount": len(leaf_table_pointers),
        "leafTablePointers": leaf_table_pointers,
        "payloadGraphEdgeCount": len(graph_edges),
        "payloadGraphComponentCount": len(graph_components),
        "payloadGraphEdges": graph_edges,
        "payloadGraphComponents": graph_components,
        "payloadGraphExternalEdgeCount": len(graph_external_edges),
        "payloadGraphAllEdgesLocal": len(graph_external_edges) == 0,
        "payloadGraphReachesFrontierTarget": any(component.get("reachesFrontierTarget") for component in graph_components),
        "dataLowByteCount": len(data_low_bytes),
        "dispatchStopClosure": closure,
        "closureReachesFrontierTarget": bool(reached_targets),
        "payloadDirectlyTargetsLeafTable": bool(leaf_table_pointers),
        "promotionStatus": "blocked",
        "remainingProofs": [
            "decode the selector/table index that chooses current 2:0 leaf table entries",
            "prove wrapper 0x00542a04 executes into 0x00542ae8 during the normal route",
            "find a strict map1_01a source coordinate or hotspot",
        ],
        "conclusion": conclusion,
    }


def markdown(summary: dict) -> str:
    lines = [
        "# Save Selector Opcode 0x24 Payload Table",
        "",
        f"- route: {summary['source']} -> {summary['target']}",
        f"- opcode 0x24: `{summary['opcode24VaHex']}`",
        f"- dispatch stop ref: `{summary['dispatchStopVaHex']}`",
        f"- payload window: `{summary['payloadWindowHex']}`",
        f"- leaf table window start: `{summary['leafTableWindowStartHex']}`",
        f"- root table pointer: `{summary['rootTablePointerHex']}`",
        f"- wrapper leaf: `{summary['wrapperLeafHex']}`",
        f"- frontier leaf: `{summary['frontierLeafHex']}`",
        f"- frontier reader: `{summary['frontierReaderHex']}`",
        f"- payload pointer count: {summary['payloadPointerCount']}",
        f"- payload graph components: {summary['payloadGraphComponentCount']}",
        f"- payload graph all edges local: {summary['payloadGraphAllEdgesLocal']}",
        f"- payload graph reaches frontier target: {summary['payloadGraphReachesFrontierTarget']}",
        f"- leaf table pointer count: {summary['leafTablePointerCount']}",
        f"- closure reaches frontier target: {summary['closureReachesFrontierTarget']}",
        f"- payload directly targets leaf table: {summary['payloadDirectlyTargetsLeafTable']}",
        f"- promotion status: {summary['promotionStatus']}",
        "",
        summary["conclusion"],
        "",
        "## Dispatch Stop Closure",
        "",
        "| step | va | value | kind | target | stop |",
        "| ---: | --- | --- | --- | --- | --- |",
    ]
    for item in summary["dispatchStopClosure"]:
        lines.append(
            f"| {item.get('step')} | `{item.get('vaHex')}` | `{item.get('valueHex') or '-'}` | "
            f"{item.get('kind') or '-'} | `{item.get('targetVaHex') or '-'}` | {item.get('stopReason') or '-'} |"
        )
    lines.extend([
        "",
        "## Payload Pointer Graph",
        "",
        "| component | closed inside payload | reaches frontier | nodes | edges |",
        "| ---: | --- | --- | --- | --- |",
    ])
    for component in summary["payloadGraphComponents"]:
        edge_text = ", ".join(
            f"{edge.get('sourceVaHex')}->{edge.get('targetVaHex')}"
            for edge in component.get("edges") or []
        )
        lines.append(
            f"| {component.get('component')} | {component.get('closedInsidePayload')} | "
            f"{component.get('reachesFrontierTarget')} | "
            f"{', '.join(component.get('nodes') or [])} | {edge_text or '-'} |"
        )
    lines.extend([
        "",
        "## Payload Window",
        "",
        "| va | value | kind | target | low byte | handler |",
        "| --- | --- | --- | --- | --- | --- |",
    ])
    for row in summary["tableRows"]:
        lines.append(
            f"| `{row['vaHex']}` | `{row.get('valueHex')}` | {row.get('kind')} | "
            f"`{row.get('targetVaHex') or '-'}` {row.get('targetMeaning') or row.get('targetSection') or ''} | "
            f"`{row.get('lowByteHex') or '-'}` | `{row.get('lowByteHandlerVaHex') or '-'}` {row.get('lowByteHandlerSection') or '-'} |"
        )
    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:
    closure_rows = []
    for item in summary["dispatchStopClosure"]:
        closure_rows.append(
            "<tr>"
            f"<td>{item.get('step')}</td>"
            f"<td><code>{html.escape(str(item.get('vaHex')))}</code></td>"
            f"<td><code>{html.escape(str(item.get('valueHex') or '-'))}</code></td>"
            f"<td>{html.escape(str(item.get('kind') or '-'))}</td>"
            f"<td><code>{html.escape(str(item.get('targetVaHex') or '-'))}</code></td>"
            f"<td>{html.escape(str(item.get('stopReason') or '-'))}</td>"
            "</tr>"
        )
    table_rows = []
    for row in summary["tableRows"]:
        target = f"{row.get('targetVaHex') or '-'} {row.get('targetMeaning') or row.get('targetSection') or ''}"
        table_rows.append(
            "<tr>"
            f"<td><code>{html.escape(row['vaHex'])}</code></td>"
            f"<td><code>{html.escape(str(row.get('valueHex')))}</code></td>"
            f"<td>{html.escape(str(row.get('kind')))}</td>"
            f"<td><code>{html.escape(target)}</code></td>"
            f"<td><code>{html.escape(str(row.get('lowByteHex') or '-'))}</code></td>"
            f"<td><code>{html.escape(str(row.get('lowByteHandlerVaHex') or '-'))}</code> {html.escape(str(row.get('lowByteHandlerSection') or '-'))}</td>"
            "</tr>"
        )
    graph_rows = []
    for component in summary["payloadGraphComponents"]:
        edge_text = ", ".join(
            f"{edge.get('sourceVaHex')}->{edge.get('targetVaHex')}"
            for edge in component.get("edges") or []
        )
        graph_rows.append(
            "<tr>"
            f"<td>{component.get('component')}</td>"
            f"<td>{html.escape(str(component.get('closedInsidePayload')))}</td>"
            f"<td>{html.escape(str(component.get('reachesFrontierTarget')))}</td>"
            f"<td><code>{html.escape(', '.join(component.get('nodes') or []))}</code></td>"
            f"<td><code>{html.escape(edge_text or '-')}</code></td>"
            "</tr>"
        )
    proof_items = "".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>Save Selector Opcode 0x24 Payload Table</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 Opcode 0x24 Payload Table</h1>",
        f"  <p>route: {html.escape(summary['source'])} -&gt; {html.escape(summary['target'])}; opcode 0x24 <code>{summary['opcode24VaHex']}</code>; payload window <code>{summary['payloadWindowHex']}</code>.</p>",
        f"  <p>leaf table window start: <code>{summary['leafTableWindowStartHex']}</code>; wrapper leaf <code>{summary['wrapperLeafHex']}</code>; frontier leaf <code>{summary['frontierLeafHex']}</code>; frontier reader <code>{summary['frontierReaderHex']}</code>.</p>",
        f"  <p>payload pointer count: {summary['payloadPointerCount']}; graph components: {summary['payloadGraphComponentCount']}; graph all edges local: {summary['payloadGraphAllEdgesLocal']}; graph reaches frontier target: {summary['payloadGraphReachesFrontierTarget']}; leaf table pointer count: {summary['leafTablePointerCount']}; closure reaches frontier target: {summary['closureReachesFrontierTarget']}; payload directly targets leaf table: {summary['payloadDirectlyTargetsLeafTable']}; promotion status: {html.escape(summary['promotionStatus'])}</p>",
        f"  <p>{html.escape(summary['conclusion'])}</p>",
        "  <h2>Dispatch Stop Closure</h2>",
        "  <table><thead><tr><th>step</th><th>va</th><th>value</th><th>kind</th><th>target</th><th>stop</th></tr></thead>",
        f"  <tbody>{''.join(closure_rows)}</tbody></table>",
        "  <h2>Payload Pointer Graph</h2>",
        "  <table><thead><tr><th>component</th><th>closed inside payload</th><th>reaches frontier</th><th>nodes</th><th>edges</th></tr></thead>",
        f"  <tbody>{''.join(graph_rows)}</tbody></table>",
        "  <h2>Payload Window</h2>",
        "  <table><thead><tr><th>va</th><th>value</th><th>kind</th><th>target</th><th>low byte</th><th>handler</th></tr></thead>",
        f"  <tbody>{''.join(table_rows)}</tbody></table>",
        "  <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_opcode24_payload_table.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 opcode 0x24 payload table -> {args.out_dir / 'save_selector_opcode24_payload_table.json'}")


if __name__ == "__main__":
    main()
