#!/usr/bin/env python3
"""Classify direct selector/selected-root writers around scene route execution.

The global mode/state frontier narrowed the next proof target to live writers of
0x004576da / 0x004576db / 0x0059de30.  This pass is intentionally small and
strict: it enumerates every direct EXE reference to those globals, separates
save/load restore from VM command handlers, and records what is grounded versus
what still does not promote to a live scene/event route producer.
"""
from __future__ import annotations

import html
import json
import struct
from pathlib import Path
from typing import Any

from build_hud_menu_opener_frontier_review import (
    find_function,
    function_ranges,
    hx,
    text_refs,
)
from probe_exe_scene_tables import offset_to_va, read_sections, va_to_offset


ROOT = Path(__file__).resolve().parents[1]
EXE = ROOT / "Hwanse2.exe"
OUT = ROOT / "out"
WEB = ROOT / "web"

GENERAL_DISPATCH_TABLE_VA = 0x00440538
SAVE_SELECTOR_DISPATCH_SLICE_VA = 0x00440720

SELECTOR_A_VA = 0x004576DA
SELECTOR_B_VA = 0x004576DB
SELECTED_ROOT_VA = 0x0059DE30
SELECTOR_ROOT_TABLE_VA = 0x00442D35

SELECTOR_BYTE_HANDLER_VA = 0x00406DBB
SAVE_LOAD_RESTORE_VA = 0x00423319
SELECTED_ROOT_INDEXED_WRITER_VA = 0x0040AD9B
SELECTED_ROOT_EXECUTOR_VA = 0x0040ADC9
SELECTED_ROOT_DIRECT_WRITER_VA = 0x0040AE0E


def h(value: Any) -> str:
    return html.escape("" if value is None else str(value), quote=True)


def u32(exe: bytes, sections: list[dict[str, Any]], va: int) -> int | None:
    off = va_to_offset(sections, va)
    if off is None or off + 4 > len(exe):
        return None
    return struct.unpack_from("<I", exe, off)[0]


def dword_refs(exe: bytes, sections: list[dict[str, Any]], value: int) -> list[int]:
    needle = struct.pack("<I", value)
    refs: list[int] = []
    pos = exe.find(needle)
    while pos != -1:
        va = offset_to_va(sections, pos)
        if va is not None:
            refs.append(va)
        pos = exe.find(needle, pos + 1)
    return refs


def dispatch_entry_for(exe: bytes, sections: list[dict[str, Any]], handler_va: int) -> dict[str, Any] | None:
    refs = dword_refs(exe, sections, handler_va)
    if not refs:
        return None
    entry_va = refs[0]
    absolute_index = (entry_va - GENERAL_DISPATCH_TABLE_VA) // 4
    save_slice_index = (entry_va - SAVE_SELECTOR_DISPATCH_SLICE_VA) // 4
    return {
        "entryVaHex": hx(entry_va),
        "handlerVaHex": hx(handler_va),
        "generalDispatchIndex": absolute_index if entry_va >= GENERAL_DISPATCH_TABLE_VA else None,
        "generalDispatchIndexHex": hx(absolute_index, 2) if entry_va >= GENERAL_DISPATCH_TABLE_VA else None,
        "saveSelectorSliceIndex": save_slice_index if entry_va >= SAVE_SELECTOR_DISPATCH_SLICE_VA else None,
        "saveSelectorSliceIndexHex": hx(save_slice_index, 2) if entry_va >= SAVE_SELECTOR_DISPATCH_SLICE_VA else None,
    }


def classify_ref(function_va: int, ref_va: int, name: str) -> dict[str, Any]:
    if function_va == SELECTOR_BYTE_HANDLER_VA:
        if name in {"selectorA", "selectorB"}:
            return {
                "role": "selector-byte-writer",
                "promotion": "grounded-vm-writer-primitive",
                "evidence": "opcode handler copies stream operands +2/+3 into selector bytes, then switches to a fixed root pointer.",
            }
    if function_va == SAVE_LOAD_RESTORE_VA:
        return {
            "role": "save-load-restore",
            "promotion": "excluded-save-load",
            "evidence": "restore path reads selector bytes and writes selectedRoot after loading persistent state blocks.",
        }
    if function_va == SELECTED_ROOT_INDEXED_WRITER_VA:
        return {
            "role": "selected-root-indexed-writer",
            "promotion": "grounded-vm-writer-primitive",
            "evidence": "selectedRoot = dword table[index] where table comes from stream +4 and index from stream +1.",
        }
    if function_va == SELECTED_ROOT_DIRECT_WRITER_VA:
        return {
            "role": "selected-root-direct-or-inline-writer",
            "promotion": "grounded-vm-writer-primitive",
            "evidence": "mode 0 sets selectedRoot to inline stream+4; mode 1 sets selectedRoot to stream dword at +4.",
        }
    if function_va == SELECTED_ROOT_EXECUTOR_VA:
        return {
            "role": "selected-root-executor",
            "promotion": "grounded-consumer-not-producer",
            "evidence": "if selectedRoot is nonzero, push continuation and set current stream to selectedRoot.",
        }
    return {
        "role": "unclassified-ref",
        "promotion": "review-required",
        "evidence": f"direct {name} reference outside known rows.",
    }


def build_report() -> dict[str, Any]:
    exe = EXE.read_bytes()
    sections = read_sections(exe)
    functions = function_ranges(exe, sections)
    refs_by_name = {
        "selectorA": text_refs(exe, sections, SELECTOR_A_VA),
        "selectorB": text_refs(exe, sections, SELECTOR_B_VA),
        "selectedRoot": text_refs(exe, sections, SELECTED_ROOT_VA),
    }

    rows: list[dict[str, Any]] = []
    for name, refs in refs_by_name.items():
        for ref_va in refs:
            function = find_function(functions, ref_va)
            function_va = int(function["startVa"]) if function else ref_va
            row = {
                "name": name,
                "refVaHex": hx(ref_va),
                "functionVaHex": hx(function_va),
                "functionEndVaHex": hx(int(function["endVa"])) if function else None,
            }
            row.update(classify_ref(function_va, ref_va, name))
            rows.append(row)

    handler_rows = [
        {
            "label": "selector byte setter",
            "handlerVaHex": hx(SELECTOR_BYTE_HANDLER_VA),
            "dispatch": dispatch_entry_for(exe, sections, SELECTOR_BYTE_HANDLER_VA),
            "semantics": [
                "case 0: push continuation stream+4; jump to dword[0x004dc518]",
                "case 1: selectorA=stream[+2], selectorB=stream[+3]; push continuation stream+4; jump to dword[0x004dc514]",
                "case 2/3: call save/load restore 0x00423319 with mode 0/1",
            ],
        },
        {
            "label": "selectedRoot indexed writer",
            "handlerVaHex": hx(SELECTED_ROOT_INDEXED_WRITER_VA),
            "dispatch": dispatch_entry_for(exe, sections, SELECTED_ROOT_INDEXED_WRITER_VA),
            "semantics": ["selectedRoot = streamTable[index]; advance +8"],
        },
        {
            "label": "selectedRoot executor",
            "handlerVaHex": hx(SELECTED_ROOT_EXECUTOR_VA),
            "dispatch": dispatch_entry_for(exe, sections, SELECTED_ROOT_EXECUTOR_VA),
            "semantics": ["if selectedRoot != 0, push continuation and set current stream to selectedRoot"],
        },
        {
            "label": "selectedRoot direct writer",
            "handlerVaHex": hx(SELECTED_ROOT_DIRECT_WRITER_VA),
            "dispatch": dispatch_entry_for(exe, sections, SELECTED_ROOT_DIRECT_WRITER_VA),
            "semantics": ["mode 0: selectedRoot = inline stream+4", "mode 1: selectedRoot = stream dword at +4"],
        },
    ]

    selector_roots = []
    root_table_off = va_to_offset(sections, SELECTOR_ROOT_TABLE_VA)
    if root_table_off is not None:
        for index in range(32):
            root = struct.unpack_from("<I", exe, root_table_off + index * 4)[0]
            selector_roots.append({"index": index, "rootVaHex": hx(root)})

    selector_byte_writer_refs = [row for row in rows if row["role"] == "selector-byte-writer"]
    selected_root_writers = [
        row for row in rows
        if row["role"] in {"selected-root-indexed-writer", "selected-root-direct-or-inline-writer"}
    ]
    save_load_rows = [row for row in rows if row["role"] == "save-load-restore"]
    review_required = [row for row in rows if row["promotion"] == "review-required"]
    summary = {
        "directRefCount": sum(len(refs) for refs in refs_by_name.values()),
        "selectorByteWriterRefCount": len(selector_byte_writer_refs),
        "selectedRootWriterRefCount": len(selected_root_writers),
        "selectedRootExecutorFound": any(row["role"] == "selected-root-executor" for row in rows),
        "saveLoadRestoreRefCount": len(save_load_rows),
        "reviewRequiredCount": len(review_required),
        "selectorByteWriterGrounded": len(selector_byte_writer_refs) == 2,
        "selectedRootWriterGrounded": len(selected_root_writers) == 3,
        "routeProducerPromoted": False,
        "decision": (
            "selector bytes and selectedRoot writer/executor VM primitives are grounded. "
            "This proves how a decoded stream can choose and execute a selected root, but it still does not prove "
            "which live field/scene route produces that stream in normal gameplay."
        ),
    }

    return {
        "kind": "hwanse-selected-root-live-writer-frontier-review",
        "source": "tools/build_selected_root_live_writer_frontier_review.py",
        "status": "selected-root-writer-primitives-grounded-route-producer-pending",
        "summary": summary,
        "watchedGlobals": [
            {"name": "selectorA", "vaHex": hx(SELECTOR_A_VA)},
            {"name": "selectorB", "vaHex": hx(SELECTOR_B_VA)},
            {"name": "selectedRoot", "vaHex": hx(SELECTED_ROOT_VA)},
        ],
        "handlerRows": handler_rows,
        "directReferenceRows": rows,
        "selectorRootTable": {
            "tableVaHex": hx(SELECTOR_ROOT_TABLE_VA),
            "first32": selector_roots,
        },
        "newEvidence": [
            "0x00406dbb is a real VM handler table entry and writes selectorA/B from stream operands in subcase 1.",
            "0x0040ad9b and 0x0040ae0e are selectedRoot writer primitives.",
            "0x0040adc9 is the selectedRoot executor/consumer.",
            "0x00423319 remains a save/load restore bridge and is not a live route producer.",
        ],
        "remainingProofs": [
            "Find the live scene/field root that reaches the selector byte setter with a normal gameplay stream.",
            "Prove a current stream/root in actual play is dispatched through the selectedRoot writer/executor path.",
            "Connect this selected-root layer upward to map transition, dialogue branch, or encounter setup producer evidence.",
        ],
    }


def render_html(report: dict[str, Any]) -> str:
    summary = report["summary"]
    cards = [
        ("status", report["status"]),
        ("direct refs", summary["directRefCount"]),
        ("selector writer refs", summary["selectorByteWriterRefCount"]),
        ("selectedRoot writers", summary["selectedRootWriterRefCount"]),
        ("review required", summary["reviewRequiredCount"]),
    ]
    card_html = "".join(f"<div class='card'><b>{h(k)}</b><span>{h(v)}</span></div>" for k, v in cards)
    handler_rows = "".join(
        "<tr>"
        f"<td>{h(row['label'])}<br><code>{h(row['handlerVaHex'])}</code></td>"
        f"<td><code>{h((row.get('dispatch') or {}).get('entryVaHex'))}</code><br>{h(json.dumps(row.get('dispatch'), ensure_ascii=False))}</td>"
        f"<td>{''.join(f'<div>{h(item)}</div>' for item in row['semantics'])}</td>"
        "</tr>"
        for row in report["handlerRows"]
    )
    ref_rows = "".join(
        "<tr>"
        f"<td>{h(row['name'])}<br><code>{h(row['refVaHex'])}</code></td>"
        f"<td><code>{h(row['functionVaHex'])}</code><br>{h(row['role'])}</td>"
        f"<td>{h(row['promotion'])}<br><span class='note'>{h(row['evidence'])}</span></td>"
        "</tr>"
        for row in report["directReferenceRows"]
    )
    remaining = "".join(f"<li>{h(item)}</li>" for item in report["remainingProofs"])
    evidence = "".join(f"<li>{h(item)}</li>" for item in report["newEvidence"])
    payload = json.dumps(report, ensure_ascii=False)
    return f"""<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Selected Root Live Writer Frontier</title>
  <style>
    body {{ margin:0; background:#101318; color:#edf1f7; font-family:system-ui,sans-serif; }}
    main {{ max-width:1280px; margin:0 auto; padding:24px; }}
    a {{ color:#8ecbff; }}
    .nav {{ display:flex; flex-wrap:wrap; gap:8px; margin-bottom:16px; }}
    .chip {{ border:1px solid #334155; border-radius:999px; padding:6px 10px; text-decoration:none; background:#161b22; }}
    .summary {{ display:grid; grid-template-columns:repeat(auto-fit,minmax(180px,1fr)); gap:12px; margin:16px 0; }}
    .card {{ border:1px solid #2b3544; border-radius:8px; padding:12px; background:#161b22; }}
    .card b {{ display:block; color:#9fb1c9; font-size:12px; text-transform:uppercase; }}
    .card span {{ display:block; margin-top:8px; font-size:18px; overflow-wrap:anywhere; }}
    section {{ border:1px solid #273244; border-radius:10px; padding:16px; margin:16px 0; background:#141922; overflow:auto; }}
    table {{ border-collapse:collapse; width:100%; min-width:980px; font-size:13px; }}
    th,td {{ border-bottom:1px solid #283342; padding:8px; text-align:left; vertical-align:top; }}
    th {{ color:#b9c7dc; background:#111722; }}
    code {{ color:#dbeafe; }}
    .note {{ display:block; margin-top:6px; color:#9fb1c9; line-height:1.35; }}
    pre {{ white-space:pre-wrap; background:#0b0f14; border:1px solid #253044; border-radius:8px; padding:12px; max-height:420px; overflow:auto; }}
  </style>
</head>
<body>
<main>
  <div class="nav">
    <a class="chip" href="index.html">index</a>
    <a class="chip" href="static_analysis_remaining_work.html">remaining work</a>
    <a class="chip" href="global_mode_state_frontier_review.html">mode/state frontier</a>
    <a class="chip" href="scene_event_text_consumer_trace_review.html">text consumer</a>
    <a class="chip" href="selected_scene_text_root_consumer_review.html">selected root consumer</a>
    <a class="chip" href="../out/selected_root_live_writer_frontier_review.json">JSON</a>
  </div>
  <h1>Selected Root Live Writer Frontier</h1>
  <p>{h(summary["decision"])}</p>
  <div class="summary">{card_html}</div>
  <section>
    <h2>New Evidence</h2>
    <ul>{evidence}</ul>
  </section>
  <section>
    <h2>Handler Rows</h2>
    <table><thead><tr><th>handler</th><th>dispatch entry</th><th>semantics</th></tr></thead><tbody>{handler_rows}</tbody></table>
  </section>
  <section>
    <h2>Direct References</h2>
    <table><thead><tr><th>global/ref</th><th>function</th><th>classification</th></tr></thead><tbody>{ref_rows}</tbody></table>
  </section>
  <section>
    <h2>Remaining Proofs</h2>
    <ul>{remaining}</ul>
  </section>
  <section>
    <h2>Raw JSON</h2>
    <pre id="json"></pre>
  </section>
</main>
<script>
window.HWANSE_SELECTED_ROOT_LIVE_WRITER_FRONTIER_REVIEW = {payload};
document.getElementById('json').textContent = JSON.stringify(window.HWANSE_SELECTED_ROOT_LIVE_WRITER_FRONTIER_REVIEW, null, 2);
</script>
</body>
</html>
"""


def main() -> int:
    report = build_report()
    OUT.mkdir(exist_ok=True)
    WEB.mkdir(exist_ok=True)
    (OUT / "selected_root_live_writer_frontier_review.json").write_text(
        json.dumps(report, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    (WEB / "selected_root_live_writer_frontier_review.html").write_text(render_html(report), encoding="utf-8")
    print("selected_root_live_writer_frontier_review ok")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
