#!/usr/bin/env python3
"""Summarize the opcode 0x24 payload boundary after the current route writer."""
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 find_cns_strings, 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"


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


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 refs_to_value(exe: bytes, sections: list[dict], value: int, limit: int = 16) -> list[dict]:
    needle = struct.pack("<I", value)
    refs = []
    for section in sections:
        start = section["raw"]
        end = start + section["raw_size"]
        data = exe[start:end]
        pos = data.find(needle)
        while pos >= 0:
            refs.append({
                "section": section["name"],
                "vaHex": f"0x{section['va'] + pos:08x}",
            })
            if len(refs) >= limit:
                return refs
            pos = data.find(needle, pos + 1)
    return refs


def row_at(exe: bytes, sections: list[dict], strings: dict[int, str], va: int) -> dict:
    value = dword_at(exe, sections, va)
    row = {
        "vaHex": f"0x{va:08x}",
        "valueHex": f"0x{value:08x}" if value is not None else None,
    }
    if value is None:
        row["kind"] = "unreadable"
        return row
    opcode = value & 0xFF
    handler = handler_for_opcode(exe, sections, opcode)
    row.update({
        "lo16": value & 0xFFFF,
        "hi16": (value >> 16) & 0xFFFF,
        "lowByteHex": f"0x{opcode:02x}",
        "lowByteHandlerVaHex": handler.get("handlerVaHex"),
        "lowByteHandlerSection": handler.get("handlerSection"),
        "lowByteIsCodeHandler": handler.get("isCodeHandler"),
        "refs": refs_to_value(exe, sections, va, 8),
    })
    section = section_name_for_va(sections, value)
    if value in strings:
        row["kind"] = "cns"
        row["target"] = strings[value]
    elif section:
        row["kind"] = "pointer"
        row["target"] = f"{section}+0x{value - next(s['va'] for s in sections if s['name'] == section and s['va'] <= value < s['va'] + s['raw_size']):x}"
        row["targetVaHex"] = f"0x{value:08x}"
    elif (value >> 16) <= 0x200 and (value & 0xFFFF) <= 0x200:
        row["kind"] = "pair"
    else:
        row["kind"] = "scalar"
    return row


def build_rows(exe: bytes, gate_paths: list[dict]) -> list[dict]:
    sections = read_sections(exe)
    strings = find_cns_strings(exe, sections)
    rows = []
    for gate in gate_paths:
        boundary = gate.get("opcode24Boundary") or {}
        opcode_va = parse_hex(boundary.get("opcodeVaHex"))
        stop_va = parse_hex(gate.get("dispatchStopVaHex"))
        if opcode_va is None or stop_va is None:
            continue
        window_start = opcode_va - 0x10
        window_rows = [
            row_at(exe, sections, strings, va)
            for va in range(window_start, stop_va + 0x50, 4)
        ]
        pointer_rows = [
            item for item in window_rows
            if item.get("kind") == "pointer"
        ]
        code_low_bytes_after_boundary = [
            item for item in window_rows
            if parse_hex(item.get("vaHex")) and parse_hex(item.get("vaHex")) > opcode_va
            and item.get("lowByteIsCodeHandler") is True
        ]
        data_low_bytes_after_boundary = [
            item for item in window_rows
            if parse_hex(item.get("vaHex")) and parse_hex(item.get("vaHex")) > opcode_va
            and item.get("lowByteHandlerSection") == ".data"
        ]
        rows.append({
            "source": gate.get("source"),
            "target": gate.get("target"),
            "opcode24VaHex": boundary.get("opcodeVaHex"),
            "opcode24ValueHex": boundary.get("opcodeValueHex"),
            "opcode24HandlerVaHex": boundary.get("handlerVaHex"),
            "dispatchStopVaHex": gate.get("dispatchStopVaHex"),
            "dispatchStopValueHex": next(
                (item.get("valueHex") for item in window_rows if item.get("vaHex") == gate.get("dispatchStopVaHex")),
                None,
            ),
            "dispatchStopHandlerVaHex": gate.get("dispatchStopHandlerVaHex"),
            "dispatchStopHandlerSection": gate.get("dispatchStopHandlerSection"),
            "windowStartHex": f"0x{window_start:08x}",
            "windowEndHex": f"0x{stop_va + 0x4c:08x}",
            "windowRows": window_rows,
            "pointerRowCount": len(pointer_rows),
            "codeLowByteCountAfterBoundary": len(code_low_bytes_after_boundary),
            "dataLowByteCountAfterBoundary": len(data_low_bytes_after_boundary),
            "conclusion": (
                "Opcode 0x24 at 0x005428e4 advances by +4 only in the statically visible handler path, "
                "but the following region contains self/pointer-table values such as 0x005428e8 and 0x0054294c. "
                "A naive low-byte trace through those dwords reaches 0xe8 at 0x005428f4, whose handler-table entry "
                "is .data, not code. Treat the region after 0x005428e4 as unresolved payload/table data until "
                "the opcode 0x24 runtime mode is decoded."
            ),
        })
    return rows


def markdown(rows: list[dict]) -> str:
    lines = [
        "# Save Selector Opcode 0x24 Payload",
        "",
        "Payload/table evidence for the opcode 0x24 boundary after the current route writer.",
        "",
    ]
    for row in rows:
        lines.extend([
            f"## {row.get('source')} -> {row.get('target')}",
            "",
            f"- opcode 0x24: `{row.get('opcode24VaHex')}` value `{row.get('opcode24ValueHex')}` handler `{row.get('opcode24HandlerVaHex')}`",
            f"- naive stop: `{row.get('dispatchStopVaHex')}` value `{row.get('dispatchStopValueHex')}` handler `{row.get('dispatchStopHandlerVaHex')}` in {row.get('dispatchStopHandlerSection')}",
            f"- pointer rows in window: {row.get('pointerRowCount')}",
            f"- code-like low bytes after boundary: {row.get('codeLowByteCountAfterBoundary')}",
            f"- data low-byte entries after boundary: {row.get('dataLowByteCountAfterBoundary')}",
            f"- conclusion: {row.get('conclusion')}",
            "",
            "| va | value | kind | target | low byte | handler | refs to this va |",
            "| --- | --- | --- | --- | --- | --- | --- |",
        ])
        for item in row.get("windowRows") or []:
            refs = ", ".join(f"{ref.get('section')}:{ref.get('vaHex')}" for ref in item.get("refs") or []) or "-"
            target = item.get("targetVaHex") or item.get("target") or "-"
            lines.append(
                f"| `{item.get('vaHex')}` | `{item.get('valueHex')}` | {item.get('kind')} | "
                f"{target} | `{item.get('lowByteHex') or '-'}` | "
                f"`{item.get('lowByteHandlerVaHex') or '-'}` {item.get('lowByteHandlerSection') or '-'} | {refs} |"
            )
        lines.append("")
    return "\n".join(lines)


def html_page(rows: list[dict]) -> str:
    parts = [
        "<!doctype html><meta charset=\"utf-8\"><title>Save Selector Opcode 0x24 Payload</title>",
        "<style>body{font-family:system-ui,sans-serif;background:#111;color:#eee}table{border-collapse:collapse}td,th{border:1px solid #444;padding:6px 8px;vertical-align:top}code{color:#9bd4ff}</style>",
        "<h1>Save Selector Opcode 0x24 Payload</h1>",
        "<p>Payload/table evidence for the opcode 0x24 boundary after the current route writer.</p>",
    ]
    for row in rows:
        parts.extend([
            f"<h2>{html.escape(str(row.get('source')))} -&gt; {html.escape(str(row.get('target')))}</h2>",
            "<ul>",
            f"<li>opcode 0x24: <code>{html.escape(str(row.get('opcode24VaHex')))}</code> value <code>{html.escape(str(row.get('opcode24ValueHex')))}</code> handler <code>{html.escape(str(row.get('opcode24HandlerVaHex')))}</code></li>",
            f"<li>naive stop: <code>{html.escape(str(row.get('dispatchStopVaHex')))}</code> value <code>{html.escape(str(row.get('dispatchStopValueHex')))}</code> handler <code>{html.escape(str(row.get('dispatchStopHandlerVaHex')))}</code> in {html.escape(str(row.get('dispatchStopHandlerSection')))}</li>",
            f"<li>pointer rows in window: {row.get('pointerRowCount')}</li>",
            f"<li>code-like low bytes after boundary: {row.get('codeLowByteCountAfterBoundary')}</li>",
            f"<li>data low-byte entries after boundary: {row.get('dataLowByteCountAfterBoundary')}</li>",
            f"<li>{html.escape(str(row.get('conclusion')))}</li>",
            "</ul>",
            "<table><thead><tr><th>va</th><th>value</th><th>kind</th><th>target</th><th>low byte</th><th>handler</th><th>refs to this va</th></tr></thead><tbody>",
        ])
        for item in row.get("windowRows") or []:
            refs = ", ".join(f"{ref.get('section')}:{ref.get('vaHex')}" for ref in item.get("refs") or []) or "-"
            target = item.get("targetVaHex") or item.get("target") or "-"
            parts.append(
                "<tr>"
                f"<td><code>{html.escape(str(item.get('vaHex')))}</code></td>"
                f"<td><code>{html.escape(str(item.get('valueHex')))}</code></td>"
                f"<td>{html.escape(str(item.get('kind')))}</td>"
                f"<td>{html.escape(str(target))}</td>"
                f"<td><code>{html.escape(str(item.get('lowByteHex') or '-'))}</code></td>"
                f"<td><code>{html.escape(str(item.get('lowByteHandlerVaHex') or '-'))}</code> {html.escape(str(item.get('lowByteHandlerSection') or '-'))}</td>"
                f"<td>{html.escape(refs)}</td>"
                "</tr>"
            )
        parts.append("</tbody></table>")
    return "\n".join(parts)


def write_outputs(rows: list[dict], out_dir: Path) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "save_selector_opcode24_payload.json").write_text(
        json.dumps(rows, ensure_ascii=False, indent=2) + "\n",
        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)
    args = parser.parse_args()
    rows = build_rows(
        args.exe.read_bytes(),
        load_json(args.out_dir / "save_selector_gate_paths.json", []),
    )
    write_outputs(rows, args.out_dir)
    print(f"wrote {len(rows)} opcode 0x24 payload rows -> {args.out_dir / 'save_selector_opcode24_payload.json'}")


if __name__ == "__main__":
    main()
