#!/usr/bin/env python3
"""Build a focused Scene/Event VM random-gate review.

This is the next boundary after the choice-target review.  The variable that
was initially suspected to be a selected choice index (`0x59db1f`) is actually
incremented by an internal random-threshold helper.  This builder records that
cluster so later scene/choice work does not promote it as a user selection
cursor by mistake.
"""
from __future__ import annotations

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


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

TEXT_START = 0x00401000
TEXT_FILE_OFFSET = 0x00000400
TEXT_SIZE = 0x0003978C
DATA_START = 0x0043C000
DATA_FILE_OFFSET = 0x0003A000

HANDLER_TABLE_VA = 0x00440720
RAND16_MOD_HANDLER = 0x00427730
RANDOM_GATE_TICK = 0x004330E0
RANDOM_GATE_TICK_CALLSITE = 0x00430E07

OP16_TARGET_PRODUCER = 0x0040BA3F
OP17_GATE_INIT = 0x0040BA5F
OP18_TARGET_GATE = 0x0040BAB2
OP19_FALLBACK_GATE = 0x0040BB2B
OP1A_ACTIVE_TABLE_GATE = 0x0040BB70

TARGET_GLOBAL = 0x0059E2A0
FALLBACK_GLOBAL = 0x0059DB18
THRESHOLD_GLOBAL = 0x0059DB1C
REQUIRED_COUNT_GLOBAL = 0x0059DB1E
SUCCESS_COUNT_GLOBAL = 0x0059DB1F
ACTIVE_FLAG_GLOBAL = 0x00457749
BRANCH_STATE_GLOBAL = 0x0059E358
DISPLAY_LOCK_GLOBAL = 0x0059E334
RAND_SEED_GLOBAL = 0x004AAAFC


def hx(value: int | None, width: int = 8) -> str | None:
    if value is None:
        return None
    return f"0x{value:0{width}x}"


def va_to_offset(va: int) -> int | None:
    if TEXT_START <= va < TEXT_START + TEXT_SIZE:
        return TEXT_FILE_OFFSET + (va - TEXT_START)
    if DATA_START <= va:
        return DATA_FILE_OFFSET + (va - DATA_START)
    return None


def read_bytes(exe: bytes, va: int, size: int) -> str:
    offset = va_to_offset(va)
    if offset is None:
        return ""
    return exe[offset : offset + size].hex(" ")


def table_entry(exe: bytes, opcode: int) -> dict[str, Any]:
    entry_va = HANDLER_TABLE_VA + opcode * 4
    offset = va_to_offset(entry_va)
    handler = struct.unpack_from("<I", exe, offset)[0] if offset is not None else None
    return {
        "opcodeHex": f"0x{opcode:02x}",
        "entryVaHex": hx(entry_va),
        "handlerVaHex": hx(handler),
        "handlerBytes": read_bytes(exe, handler or 0, 48),
    }


def find_direct_calls(exe: bytes, target: int) -> list[str]:
    rows: list[str] = []
    start = TEXT_FILE_OFFSET
    end = TEXT_FILE_OFFSET + TEXT_SIZE - 5
    for offset in range(start, end):
        if exe[offset] != 0xE8:
            continue
        call_va = TEXT_START + (offset - TEXT_FILE_OFFSET)
        rel = struct.unpack_from("<i", exe, offset + 1)[0]
        dest = call_va + 5 + rel
        if dest == target:
            rows.append(hx(call_va) or "")
    return rows


def global_refs(exe: bytes, value: int) -> list[dict[str, Any]]:
    needle = struct.pack("<I", value)
    rows: list[dict[str, Any]] = []
    start = 0
    while True:
        offset = exe.find(needle, start)
        if offset < 0:
            break
        if TEXT_FILE_OFFSET <= offset < TEXT_FILE_OFFSET + TEXT_SIZE:
            va = TEXT_START + (offset - TEXT_FILE_OFFSET)
            near = exe[max(TEXT_FILE_OFFSET, offset - 4) : min(TEXT_FILE_OFFSET + TEXT_SIZE, offset + 8)]
            rows.append({"immediateVaHex": hx(va), "nearBytes": near.hex(" ")})
        start = offset + 1
    return rows


def build_payload(exe: bytes) -> dict[str, Any]:
    handlers = [table_entry(exe, opcode) for opcode in range(0x16, 0x1B)]
    refs = {
        "targetGlobalRefs": global_refs(exe, TARGET_GLOBAL),
        "fallbackGlobalRefs": global_refs(exe, FALLBACK_GLOBAL),
        "thresholdGlobalRefs": global_refs(exe, THRESHOLD_GLOBAL),
        "requiredCountGlobalRefs": global_refs(exe, REQUIRED_COUNT_GLOBAL),
        "successCountGlobalRefs": global_refs(exe, SUCCESS_COUNT_GLOBAL),
        "activeFlagGlobalRefs": global_refs(exe, ACTIVE_FLAG_GLOBAL),
        "branchStateGlobalRefs": global_refs(exe, BRANCH_STATE_GLOBAL),
    }
    return {
        "scope": "scene/event VM random gate cluster",
        "promotionStatus": "random-gate-grounded-choice-index-rejected",
        "summary": {
            "randomGateReviewImplemented": True,
            "choiceIndexInterpretationRejected": True,
            "successCounterGlobalHex": hx(SUCCESS_COUNT_GLOBAL),
            "successThresholdGlobalHex": hx(THRESHOLD_GLOBAL),
            "requiredSuccessCountGlobalHex": hx(REQUIRED_COUNT_GLOBAL),
            "targetGlobalHex": hx(TARGET_GLOBAL),
            "fallbackTargetGlobalHex": hx(FALLBACK_GLOBAL),
            "activeFlagGlobalHex": hx(ACTIVE_FLAG_GLOBAL),
            "branchStateGlobalHex": hx(BRANCH_STATE_GLOBAL),
            "randFunctionHex": hx(RAND16_MOD_HANDLER),
            "randSeedGlobalHex": hx(RAND_SEED_GLOBAL),
            "randomGateTickHex": hx(RANDOM_GATE_TICK),
            "randomGateTickCallsiteHex": hx(RANDOM_GATE_TICK_CALLSITE),
            "randModulo": 10000,
            "randomGateTickCallCount": len(find_direct_calls(exe, RANDOM_GATE_TICK)),
            "randFunctionCallCount": len(find_direct_calls(exe, RAND16_MOD_HANDLER)),
        },
        "decisions": [
            {
                "item": "0x0059db1f",
                "promotion": "reclassified-grounded",
                "meaning": "random-gate success counter, not a user selected choice cursor",
                "evidence": "0x004330e0 calls rand16_mod(10000), compares the result with word 0x59db1c, and increments 0x59db1f only on success.",
            },
            {
                "item": "opcode 0x17",
                "promotion": "grounded-handler",
                "meaning": "random gate initializer",
                "evidence": "stores fallback target 0x59db18, success threshold 0x59db1c, required count 0x59db1e, and resets 0x59db1f.",
            },
            {
                "item": "opcode 0x18",
                "promotion": "grounded-handler",
                "meaning": "target jump when accumulated random successes reach required count",
                "evidence": "compares 0x59db1e <= 0x59db1f, resets the counter, and jumps context+0x40 to 0x59e2a0 when active.",
            },
            {
                "item": "selected option -> next prompt",
                "promotion": "still-blocked",
                "meaning": "not solved by 0x59db1f",
                "evidence": "the counter is random-gated, so the direct user-choice producer remains elsewhere.",
            },
        ],
        "formula": {
            "rand16Mod": "seed = seed * 0x41c64e6d + 0x3039; result = ((seed >> 16) & 0xffff) % arg",
            "gateTick": "if rand16_mod(10000) < threshold_0x59db1c: successCounter_0x59db1f++",
            "op18Gate": "if required_0x59db1e <= successCounter_0x59db1f and active_0x457749 != 0: context.stream = target_0x59e2a0",
        },
        "handlers": handlers,
        "callSites": {
            "randomGateTickCalls": find_direct_calls(exe, RANDOM_GATE_TICK),
            "randFunctionCalls": find_direct_calls(exe, RAND16_MOD_HANDLER),
        },
        "globalRefs": refs,
        "remainingGaps": [
            "The real user selection cursor/confirm path is not proven here.",
            "Which scene scripts use opcode 0x17/0x18 as probability gates versus display timing still needs per-scene mapping.",
        ],
    }


def write_json(path: Path, payload: Any) -> None:
    path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")


def html_page(payload: dict[str, Any]) -> str:
    s = payload["summary"]
    decision_rows = "\n".join(
        "<tr><td>{}</td><td>{}</td><td>{}</td><td>{}</td></tr>".format(
            html.escape(row["item"]),
            html.escape(row["promotion"]),
            html.escape(row["meaning"]),
            html.escape(row["evidence"]),
        )
        for row in payload["decisions"]
    )
    handler_rows = "\n".join(
        "<tr><td>{}</td><td>{}</td><td>{}</td><td><code>{}</code></td></tr>".format(
            html.escape(row["opcodeHex"]),
            html.escape(row["entryVaHex"] or ""),
            html.escape(row["handlerVaHex"] or ""),
            html.escape(row["handlerBytes"] or ""),
        )
        for row in payload["handlers"]
    )
    formula_rows = "\n".join(
        f"<tr><td>{html.escape(key)}</td><td><code>{html.escape(value)}</code></td></tr>"
        for key, value in payload["formula"].items()
    )
    return f"""<!doctype html>
<html lang=\"ko\">
<head>
  <meta charset=\"utf-8\" />
  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />
  <link rel=\"icon\" href=\"../favicon.ico\" />
  <title>Scene/Event VM Random Gate Review</title>
  <style>
    body {{ margin: 0; font-family: system-ui, sans-serif; color: #1f2933; background: #f7f8fa; }}
    header, main {{ max-width: 1180px; margin: 0 auto; padding: 20px; }}
    nav a {{ margin-right: 12px; color: #24527a; }}
    section {{ background: white; border: 1px solid #d8dee7; border-radius: 8px; margin: 14px 0; padding: 14px; }}
    table {{ width: 100%; border-collapse: collapse; font-size: 13px; }}
    th, td {{ border-bottom: 1px solid #e3e8ef; padding: 8px; text-align: left; vertical-align: top; }}
    code {{ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; }}
    .pill {{ display: inline-block; padding: 3px 7px; border-radius: 999px; background: #e8f2ff; color: #1d4f7a; font-size: 12px; }}
  </style>
</head>
<body>
  <header>
    <nav>
      <a href=\"index.html\">home</a>
      <a href=\"scene_event_vm_review.html\">VM root</a>
      <a href=\"scene_event_vm_choice_target_review.html\">choice target</a>
      <a href=\"scene_event_vm_prompt_sequence_review.html\">prompt sequence</a>
      <a href=\"../out/scene_event_vm_random_gate_review.json\">artifact json</a>
    </nav>
    <h1>Scene/Event VM Random Gate Review</h1>
    <p><span class=\"pill\">{html.escape(payload['promotionStatus'])}</span></p>
  </header>
  <main>
    <section>
      <h2>Summary</h2>
      <table>
        <tr><th>success counter</th><td><code>{s['successCounterGlobalHex']}</code></td></tr>
        <tr><th>threshold</th><td><code>{s['successThresholdGlobalHex']}</code></td></tr>
        <tr><th>required count</th><td><code>{s['requiredSuccessCountGlobalHex']}</code></td></tr>
        <tr><th>target global</th><td><code>{s['targetGlobalHex']}</code></td></tr>
        <tr><th>random function</th><td><code>{s['randFunctionHex']}</code> · modulo <code>{s['randModulo']}</code></td></tr>
      </table>
    </section>
    <section><h2>Formula</h2><table><tbody>{formula_rows}</tbody></table></section>
    <section><h2>Decisions</h2><table><thead><tr><th>item</th><th>promotion</th><th>meaning</th><th>evidence</th></tr></thead><tbody>{decision_rows}</tbody></table></section>
    <section><h2>Handler Cluster</h2><table><thead><tr><th>opcode</th><th>entry</th><th>handler</th><th>first bytes</th></tr></thead><tbody>{handler_rows}</tbody></table></section>
  </main>
  <script>
    window.HWANSE_SCENE_EVENT_VM_RANDOM_GATE_REVIEW_READY = {{
      randomGateReviewImplemented: true,
      choiceIndexInterpretationRejected: true,
      successCounterGlobalHex: \"{s['successCounterGlobalHex']}\",
      successThresholdGlobalHex: \"{s['successThresholdGlobalHex']}\",
      requiredSuccessCountGlobalHex: \"{s['requiredSuccessCountGlobalHex']}\",
      targetGlobalHex: \"{s['targetGlobalHex']}\",
      randFunctionHex: \"{s['randFunctionHex']}\",
      randomGateTickHex: \"{s['randomGateTickHex']}\",
      randModulo: {s['randModulo']},
      selectedOptionNextPromptStillBlocked: true
    }};
  </script>
</body>
</html>
"""


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--exe", type=Path, default=EXE)
    args = parser.parse_args()
    payload = build_payload(args.exe.read_bytes())
    write_json(OUT / "scene_event_vm_random_gate_review.json", payload)
    page = html_page(payload)
    (WEB / "scene_event_vm_random_gate_review.html").write_text(page, encoding="utf-8")
    print("wrote scene_event_vm_random_gate_review artifacts")


if __name__ == "__main__":
    main()
