#!/usr/bin/env python3
"""Scan opcode 0x24-looking rows inside the current selector 2:0 root."""
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


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"
CURRENT_SOURCE = "map1_01a"
CURRENT_TARGET = "map2_02d"
MODE1_SOURCE = 0x0059E348
CURRENT_BOUNDARY_VA = 0x005428E4
FRONTIER_VALUES = {
    0x00542AE8: "frontier leaf pointer",
    0x00542B0C: "frontier reader",
    0x0053F46F: "frontier reader false branch target",
}


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 parse_hex(value: str | None) -> int | None:
    if not isinstance(value, str):
        return None
    return int(value, 16)


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 current_root_context(selection_writers: dict) -> dict:
    for row in selection_writers.get("currentFrontierRootRows") or []:
        context = row.get("selectorRootContext") or {}
        if context.get("selectedPointerHex") == "0x00540714":
            return context
    return {}


def classify_value(value: int | None, sections: list[dict], cns_strings: dict[int, str]) -> dict:
    if value is None:
        return {"kind": "missing", "meaning": "-"}
    if value in FRONTIER_VALUES:
        return {"kind": "frontier", "meaning": FRONTIER_VALUES[value]}
    if value in cns_strings:
        return {"kind": "cns", "meaning": cns_strings[value]}
    if va_to_offset(sections, value) is not None:
        return {"kind": "executable-pointer", "meaning": "VA in executable image"}
    if value <= 0xFFFF:
        return {"kind": "small-scalar", "meaning": "small scalar or scene id"}
    return {"kind": "scalar", "meaning": "non-pointer scalar"}


def mode_meaning(mode: int, pointer_collision: bool) -> str:
    if pointer_collision:
        return "low-byte collision inside an executable pointer"
    if mode == 0:
        return "opcode 0x24 mode 0 object-state branch"
    if mode == 1:
        return "opcode 0x24 mode 1 writes byte(0x0059e348)+3 to object+0x61"
    if mode == 2:
        return "opcode 0x24 mode 2 writes byte(0x0059e347) to object+0x61"
    return "non-dispatch mode; treat as opcode-like data until runtime trace proves execution"


def scan_rows(exe: bytes, sections: list[dict], cns_strings: dict[int, str], start: int, end: int) -> list[dict]:
    rows = []
    for va in range(start, end, 4):
        value = dword_at(exe, sections, va)
        if value is None or value & 0xFF != 0x24:
            continue
        mode = (value >> 8) & 0xFF
        byte2 = (value >> 16) & 0xFF
        byte3 = (value >> 24) & 0xFF
        pointer_collision = va_to_offset(sections, value) is not None
        lookahead = []
        for offset in range(4, 20, 4):
            item_value = dword_at(exe, sections, va + offset)
            item_class = classify_value(item_value, sections, cns_strings)
            lookahead.append({
                "offset": offset,
                "vaHex": hex32(va + offset),
                "valueHex": hex32(item_value) if item_value is not None else None,
                "kind": item_class["kind"],
                "meaning": item_class["meaning"],
            })
        rows.append({
            "vaHex": hex32(va),
            "relativeOffsetHex": f"+0x{va - start:x}",
            "valueHex": hex32(value),
            "mode": mode,
            "modeHex": f"0x{mode:02x}",
            "byte2": byte2,
            "byte2Hex": f"0x{byte2:02x}",
            "byte3": byte3,
            "byte3Hex": f"0x{byte3:02x}",
            "pointerCollision": pointer_collision,
            "opcodeCandidate": not pointer_collision,
            "currentBoundary": va == CURRENT_BOUNDARY_VA,
            "meaning": mode_meaning(mode, pointer_collision),
            "lookahead": lookahead,
            "frontierOperand": any(item["kind"] == "frontier" for item in lookahead),
            "routeCnsOperand": any(item["meaning"] in {"map1_01a.cns", "map2_02d.cns"} for item in lookahead),
        })
    return rows


def build_summary(
    exe: bytes,
    selection_writers: dict,
    gate_paths: list[dict],
) -> dict:
    sections = read_sections(exe)
    cns_strings = find_cns_strings(exe, sections)
    context = current_root_context(selection_writers)
    start = parse_hex(context.get("rangeStartHex"))
    end = parse_hex(context.get("rangeEndHex"))
    if start is None or end is None:
        raise ValueError("current selector root range was not found in save_selector_selection_writers.json")
    rows = scan_rows(exe, sections, cns_strings, start, end)
    opcode_rows = [row for row in rows if row["opcodeCandidate"]]
    mode1_rows = [row for row in opcode_rows if row["mode"] == 1]
    pointer_rows = [row for row in rows if row["pointerCollision"]]
    frontier_rows = [row for row in opcode_rows if row["frontierOperand"]]
    route_cns_rows = [row for row in opcode_rows if row["routeCnsOperand"]]
    current_boundary = next((row for row in rows if row["currentBoundary"]), None)
    gate_boundary = next(
        (
            row.get("opcode24Boundary") or {}
            for row in gate_paths
            if row.get("source") == CURRENT_SOURCE and row.get("target") == CURRENT_TARGET
        ),
        {},
    )
    conclusion = (
        "The current selector 2:0 root has multiple opcode-0x24-looking mode 1 rows. "
        "The known gate boundary 0x005428e4 is present, but no opcode candidate in this root carries a direct "
        "frontier leaf, frontier reader, false-branch target, or map1_01a/map2_02d CNS operand in the next four dwords. "
        "This keeps opcode 0x24 mode 1 classified as object-state code that still needs runtime producer/selection proof, "
        "not as a strict map1_01a->map2_02d transition."
    )
    return {
        "scope": "current selector 2:0 root low-byte opcode 0x24 scan",
        "source": CURRENT_SOURCE,
        "target": CURRENT_TARGET,
        "selector": "2:0",
        "selectedPointerHex": context.get("selectedPointerHex"),
        "rootRangeHex": f"{context.get('rangeStartHex')}..{context.get('rangeEndHex')}",
        "rootByteLength": end - start,
        "mode1SourceHex": hex32(MODE1_SOURCE),
        "gateBoundaryVaHex": gate_boundary.get("opcodeVaHex") or hex32(CURRENT_BOUNDARY_VA),
        "gateBoundaryValueHex": gate_boundary.get("opcodeValueHex"),
        "rowCount": len(rows),
        "opcodeCandidateCount": len(opcode_rows),
        "pointerCollisionCount": len(pointer_rows),
        "mode1CandidateCount": len(mode1_rows),
        "frontierOperandCount": len(frontier_rows),
        "routeCnsOperandCount": len(route_cns_rows),
        "currentBoundaryFound": current_boundary is not None,
        "currentBoundary": current_boundary,
        "modeRows": rows,
        "frontierOperandRows": frontier_rows,
        "routeCnsOperandRows": route_cns_rows,
        "promotionStatus": "blocked",
        "conclusion": conclusion,
    }


def markdown(summary: dict) -> str:
    lines = [
        "# Save Selector Opcode 0x24 Current Root Modes",
        "",
        f"Scope: {summary['scope']}.",
        "",
        f"- route: `{summary['source']} -> {summary['target']}`",
        f"- selector: `{summary['selector']}` / selected pointer `{summary['selectedPointerHex']}`",
        f"- root range: `{summary['rootRangeHex']}` ({summary['rootByteLength']} bytes)",
        f"- mode1 source: `{summary['mode1SourceHex']}`",
        f"- gate boundary: `{summary['gateBoundaryVaHex']}` value `{summary.get('gateBoundaryValueHex')}`",
        f"- low-byte 0x24 rows: {summary['rowCount']}",
        f"- opcode candidates: {summary['opcodeCandidateCount']}",
        f"- pointer collisions: {summary['pointerCollisionCount']}",
        f"- mode1 candidates: {summary['mode1CandidateCount']}",
        f"- direct frontier operands: {summary['frontierOperandCount']}",
        f"- route CNS operands: {summary['routeCnsOperandCount']}",
        f"- current boundary found: {summary['currentBoundaryFound']}",
        f"- promotion status: {summary['promotionStatus']}",
        f"- conclusion: {summary['conclusion']}",
        "",
        "## Rows",
        "",
        "| va | value | mode | candidate | current | next dwords | meaning |",
        "| --- | --- | --- | --- | --- | --- | --- |",
    ]
    for row in summary["modeRows"]:
        lookahead = "<br>".join(
            f"`{item['valueHex']}` {item['kind']} {html.escape(str(item['meaning']))}"
            for item in row.get("lookahead") or []
        )
        lines.append(
            f"| `{row['vaHex']}` | `{row['valueHex']}` | `{row['modeHex']}` | "
            f"{row['opcodeCandidate']} | {row['currentBoundary']} | {lookahead} | {row['meaning']} |"
        )
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    rows = "\n".join(
        "<tr>"
        f"<td><code>{html.escape(str(row['vaHex']))}</code></td>"
        f"<td><code>{html.escape(str(row['valueHex']))}</code></td>"
        f"<td><code>{html.escape(str(row['modeHex']))}</code></td>"
        f"<td>{row['opcodeCandidate']}</td>"
        f"<td>{row['currentBoundary']}</td>"
        f"<td>{'<br>'.join('<code>' + html.escape(str(item['valueHex'])) + '</code> ' + html.escape(str(item['kind'])) + ' ' + html.escape(str(item['meaning'])) for item in row.get('lookahead') or [])}</td>"
        f"<td>{html.escape(str(row['meaning']))}</td>"
        "</tr>"
        for row in summary["modeRows"]
    )
    return "\n".join([
        "<!doctype html><meta charset=\"utf-8\"><title>Save Selector Opcode 0x24 Current Root Modes</title>",
        "<style>body{font-family:system-ui,sans-serif;background:#111;color:#eee;max-width:1400px;margin:24px auto}table{border-collapse:collapse;width:100%}td,th{border:1px solid #444;padding:6px 8px;vertical-align:top}code{color:#9bd4ff}</style>",
        "<h1>Save Selector Opcode 0x24 Current Root Modes</h1>",
        f"<p>Route <code>{html.escape(summary['source'])} -&gt; {html.escape(summary['target'])}</code>, selector <code>{html.escape(summary['selector'])}</code>, root <code>{html.escape(summary['rootRangeHex'])}</code>.</p>",
        "<ul>",
        f"<li>low-byte 0x24 rows: {summary['rowCount']}</li>",
        f"<li>opcode candidates: {summary['opcodeCandidateCount']}</li>",
        f"<li>pointer collisions: {summary['pointerCollisionCount']}</li>",
        f"<li>mode1 candidates: {summary['mode1CandidateCount']}</li>",
        f"<li>direct frontier operands: {summary['frontierOperandCount']}</li>",
        f"<li>route CNS operands: {summary['routeCnsOperandCount']}</li>",
        f"<li>current boundary found: {summary['currentBoundaryFound']}</li>",
        f"<li>promotion status: {summary['promotionStatus']}</li>",
        "</ul>",
        f"<p>{html.escape(summary['conclusion'])}</p>",
        "<table><thead><tr><th>va</th><th>value</th><th>mode</th><th>candidate</th><th>current</th><th>next dwords</th><th>meaning</th></tr></thead><tbody>",
        rows,
        "</tbody></table>",
    ])


def write_outputs(summary: dict, out_dir: Path, html_out: Path | None = None) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "save_selector_opcode24_current_root_modes.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("--selection-writers", type=Path, default=OUT / "save_selector_selection_writers.json")
    parser.add_argument("--gate-paths", type=Path, default=OUT / "save_selector_gate_paths.json")
    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.selection_writers, {}),
        load_json(args.gate_paths, []),
    )
    write_outputs(summary, args.out_dir, args.html_out)
    print(f"wrote opcode24 current root modes -> {args.out_dir / 'save_selector_opcode24_current_root_modes.json'}")


if __name__ == "__main__":
    main()
