#!/usr/bin/env python3
"""Review the remaining RNG selector handler behind field encounter analysis.

The shared RNG call at 0x0040bdcc sits inside handler 0x0040bcc9, reached from
handler table 0x00440720 opcode 0x1c.  This report checks whether the EXE
contains well-formed command streams for that opcode and whether any of them
can be tied to field walking encounter data.
"""
from __future__ import annotations

import html
import json
import struct
import sys
from collections import Counter
from pathlib import Path
from typing import Any

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

sys.path.insert(0, str(ROOT / "tools"))
from probe_exe_scene_tables import offset_to_va, read_sections, va_to_offset  # noqa: E402


HANDLER_TABLE_VA = 0x00440720
OPCODE = 0x1C
HANDLER_ENTRY_VA = HANDLER_TABLE_VA + OPCODE * 4
HANDLER_VA = 0x0040BCC9
RNG_CALL_VA = 0x0040BDCC
ACTIVE_COUNT_GLOBAL = 0x004576E8
ACTIVE_ORDER_GLOBAL = 0x004576E9
ACTIVE_SLOT_WEIGHT_BASE = 0x00457756


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


def hex32(value: int | None) -> str | None:
    return None if value is None else f"0x{value:08x}"


def read_json(path: Path, default: Any) -> Any:
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except FileNotFoundError:
        return default


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


def va_section(sections: list[dict[str, Any]], va: int) -> dict[str, Any] | None:
    for section in sections:
        if section["va"] <= va < section["va"] + section["raw_size"]:
            return section
    return None


def valid_stream_pointer(sections: list[dict[str, Any]], value: int) -> bool:
    section = va_section(sections, value)
    return section is not None and section["name"] in {".text", ".rdata", ".data"}


def bytes_at_va(exe: bytes, sections: list[dict[str, Any]], va: int, size: int) -> bytes:
    offset = va_to_offset(sections, va)
    if offset is None:
        return b""
    return exe[offset : offset + size]


def dword_at_va(exe: bytes, sections: list[dict[str, Any]], va: int) -> int | None:
    data = bytes_at_va(exe, sections, va, 4)
    if len(data) != 4:
        return None
    return struct.unpack_from("<I", data)[0]


def candidate_pointer_count(mode: int, byte2: int, byte3: int) -> int:
    if mode in {0, 2}:
        return byte2
    if mode == 1:
        return byte3 - byte2
    return -1


def scan_well_formed_opcode_blocks(exe: bytes, sections: list[dict[str, Any]]) -> list[dict[str, Any]]:
    """Find byte streams that the opcode 0x1c handler can plausibly consume.

    Layout inferred from handler 0x0040bcc9:
      byte0 opcode 0x1c
      byte1 mode 0/1/2
      byte2 count/range lower byte
      byte3 upper byte used by mode 1
      byte4... dword command pointers
    """
    rows: list[dict[str, Any]] = []
    for section in sections:
        start = section["raw"]
        end = start + section["raw_size"]
        for offset in range(start, max(start, end - 12)):
            if exe[offset] != OPCODE:
                continue
            mode = exe[offset + 1]
            if mode not in {0, 1, 2}:
                continue
            byte2 = exe[offset + 2]
            byte3 = exe[offset + 3]
            count = candidate_pointer_count(mode, byte2, byte3)
            if count <= 0 or count > 64:
                continue
            pointers: list[int] = []
            for index in range(count):
                pointer_offset = offset + 4 + index * 4
                if pointer_offset + 4 > len(exe):
                    break
                pointers.append(struct.unpack_from("<I", exe, pointer_offset)[0])
            if len(pointers) != count:
                continue
            valid_count = sum(1 for pointer in pointers if valid_stream_pointer(sections, pointer))
            if valid_count != count:
                continue
            va = offset_to_va(sections, offset)
            if va is None:
                continue
            rows.append(
                {
                    "streamVa": va,
                    "streamVaHex": hex32(va),
                    "section": section["name"],
                    "mode": mode,
                    "modeHex": f"0x{mode:02x}",
                    "byte2": byte2,
                    "byte2Hex": f"0x{byte2:02x}",
                    "byte3": byte3,
                    "byte3Hex": f"0x{byte3:02x}",
                    "pointerCount": count,
                    "pointerVasHex": [hex32(pointer) for pointer in pointers],
                }
            )
    return rows


def collect_existing_low_byte_refs(exe: bytes, sections: list[dict[str, Any]]) -> list[dict[str, Any]]:
    script_table = read_json(OUT / "script_handler_table.json", {})
    refs = []
    for entry in script_table.get("entries") or []:
        if entry.get("opcodeHex") != "0x1c":
            continue
        for ref in entry.get("references") or []:
            word_va_hex = ref.get("wordVaHex")
            word_va = int(word_va_hex, 16) if isinstance(word_va_hex, str) else None
            raw = bytes_at_va(exe, sections, word_va, 4) if word_va is not None else b""
            mode = raw[1] if len(raw) > 1 else None
            refs.append(
                {
                    "streamKind": ref.get("streamKind"),
                    "streamVaHex": ref.get("streamVaHex"),
                    "wordIndex": ref.get("wordIndex"),
                    "wordVaHex": word_va_hex,
                    "valueHex": ref.get("valueHex"),
                    "rawBytes": raw.hex(" ") if raw else "",
                    "modeByteHex": f"0x{mode:02x}" if mode is not None else None,
                    "modeValidForOpcode1c": mode in {0, 1, 2} if mode is not None else False,
                    "source": ref.get("source"),
                    "target": ref.get("target"),
                    "interpretation": (
                        "low-byte pointer artifact, not a valid opcode 0x1c command"
                        if mode not in {0, 1, 2}
                        else "needs command-stream validation"
                    ),
                }
            )
    return refs


def build_report() -> dict[str, Any]:
    exe = EXE.read_bytes()
    sections = read_sections(exe)
    handler_entry_value = dword_at_va(exe, sections, HANDLER_ENTRY_VA)
    candidates = scan_well_formed_opcode_blocks(exe, sections)
    low_byte_refs = collect_existing_low_byte_refs(exe, sections)
    mode_counts = Counter(row["modeHex"] for row in candidates)
    invalid_low_byte_ref_count = sum(1 for row in low_byte_refs if not row["modeValidForOpcode1c"])

    return {
        "kind": "hwanse-field-rng-selector-opcode1c-review",
        "source": [
            "Hwanse2.exe",
            "out/script_handler_table.json",
            "tools/build_field_rng_selector_opcode1c_review.py",
        ],
        "handler": {
            "handlerTableVaHex": hex32(HANDLER_TABLE_VA),
            "opcodeHex": f"0x{OPCODE:02x}",
            "handlerEntryVaHex": hex32(HANDLER_ENTRY_VA),
            "handlerEntryValueHex": hex32(handler_entry_value),
            "expectedHandlerVaHex": hex32(HANDLER_VA),
            "rngCallVaHex": hex32(RNG_CALL_VA),
            "activeCountGlobalHex": hex32(ACTIVE_COUNT_GLOBAL),
            "activeOrderGlobalHex": hex32(ACTIVE_ORDER_GLOBAL),
            "activeSlotWeightBaseHex": hex32(ACTIVE_SLOT_WEIGHT_BASE),
        },
        "semantics": [
            {
                "mode": 0,
                "summary": "selects by active object count: if active count is nonzero, index = activeCount - 1; pointer count comes from byte2.",
            },
            {
                "mode": 1,
                "summary": "selects by active object/order aggregate. It reads active count 0x4576e8, active order 0x4576e9, and slot word values near 0x457756 before selecting a pointer range byte2..byte3.",
            },
            {
                "mode": 2,
                "summary": "calls RNG helper 0x00427730 with byte2 as range and selects one of the following dword command pointers.",
            },
        ],
        "summary": {
            "handlerEntryMatchesExpected": handler_entry_value == HANDLER_VA,
            "wellFormedOpcode1cBlockCount": len(candidates),
            "wellFormedModeCounts": dict(sorted(mode_counts.items())),
            "scriptHandlerTableLowByteReferenceCount": len(low_byte_refs),
            "invalidLowByteReferenceCount": invalid_low_byte_ref_count,
            "directFieldEncounterBindingFound": False,
            "classification": (
                "handler-only-unbound"
                if not candidates
                else "well-formed-candidates-found-route-unbound"
            ),
            "decision": (
                "Opcode 0x1c remains a handler-grounded selector primitive, but no well-formed command stream using it was found in the current static scan. "
                "The existing script_handler_table refs are low-byte pointer artifacts such as 1c f7 53 00, not executable opcode 0x1c commands. "
                "Do not promote it as field encounter evidence."
            ),
        },
        "wellFormedCandidates": candidates,
        "scriptHandlerTableLowByteRefs": low_byte_refs,
        "knownGaps": [
            "A runtime trace could still prove a dynamically materialized stream reaches opcode 0x1c.",
            "No static stream currently connects opcode 0x1c to walking step, map family, btl background, or monster formation data.",
            "The mode 1 aggregate source is grounded to active object globals, but its game-facing meaning is still not named.",
        ],
    }


def render_html(report: dict[str, Any]) -> str:
    summary = report["summary"]
    handler = report["handler"]
    candidate_rows = []
    for row in report["wellFormedCandidates"]:
        candidate_rows.append(
            "<tr>"
            f"<td><code>{h(row['streamVaHex'])}</code></td>"
            f"<td>{h(row['section'])}</td>"
            f"<td><code>{h(row['modeHex'])}</code></td>"
            f"<td>{h(row['pointerCount'])}</td>"
            f"<td>{' '.join(f'<code>{h(ptr)}</code>' for ptr in row['pointerVasHex'][:8])}</td>"
            "</tr>"
        )
    if not candidate_rows:
        candidate_rows.append('<tr><td colspan="5" class="muted">well-formed opcode 0x1c command block 없음</td></tr>')

    ref_rows = []
    for row in report["scriptHandlerTableLowByteRefs"]:
        status = "ok" if row["modeValidForOpcode1c"] else "bad"
        ref_rows.append(
            "<tr>"
            f"<td><code>{h(row.get('wordVaHex'))}</code></td>"
            f"<td><code>{h(row.get('valueHex'))}</code></td>"
            f"<td><code>{h(row.get('rawBytes'))}</code></td>"
            f"<td><span class=\"tag {status}\">{h(row.get('modeByteHex'))}</span></td>"
            f"<td>{h(row.get('source'))} → {h(row.get('target'))}</td>"
            f"<td>{h(row.get('interpretation'))}</td>"
            "</tr>"
        )

    semantics = "".join(
        f"<li><code>mode {item['mode']}</code>: {h(item['summary'])}</li>"
        for item in report["semantics"]
    )

    return f"""<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Field RNG Selector Opcode 0x1c Review</title>
  <style>
    body {{ margin:0; background:#f6f7f9; color:#20242b; font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; }}
    main {{ width:min(1180px, calc(100vw - 28px)); margin:0 auto; padding:22px 0 36px; }}
    h1 {{ margin:0 0 6px; font-size:26px; }}
    h2 {{ margin:22px 0 10px; font-size:18px; }}
    p {{ color:#606878; }}
    a {{ color:#2459a6; text-decoration:none; font-weight:600; }}
    a:hover {{ text-decoration:underline; }}
    .nav {{ display:flex; flex-wrap:wrap; gap:8px; margin:14px 0 18px; }}
    .chip {{ border:1px solid #d6dce7; background:#fff; border-radius:7px; padding:6px 10px; }}
    .cards {{ display:grid; grid-template-columns:repeat(auto-fit, minmax(190px, 1fr)); gap:10px; }}
    .card {{ background:#fff; border:1px solid #d8dee8; border-radius:8px; padding:12px; }}
    .num {{ font-size:24px; font-weight:800; }}
    .label {{ color:#687080; font-size:13px; }}
    .panel {{ background:#fff; border:1px solid #d8dee8; border-radius:8px; padding:14px; margin-top:12px; }}
    .table-wrap {{ overflow:auto; border:1px solid #d8dee8; border-radius:8px; background:#fff; }}
    table {{ width:100%; border-collapse:collapse; min-width:900px; }}
    th, td {{ border-bottom:1px solid #edf0f4; padding:9px 10px; text-align:left; vertical-align:top; font-size:13px; }}
    th {{ background:#fafbfd; position:sticky; top:0; z-index:1; }}
    code {{ font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; background:#f1f4f8; border-radius:4px; padding:1px 4px; }}
    .muted {{ color:#727b8b; }}
    .tag {{ display:inline-block; border-radius:999px; padding:2px 7px; font-size:12px; font-weight:700; }}
    .ok {{ background:#e8f6ef; color:#176b3a; }}
    .bad {{ background:#fbe7e7; color:#9b2424; }}
    pre {{ white-space:pre-wrap; overflow:auto; background:#101820; color:#edf3fb; border-radius:8px; padding:12px; }}
  </style>
</head>
<body>
<main>
  <h1>Field RNG Selector Opcode 0x1c Review</h1>
  <p>필드 인카운트 쪽에 마지막으로 남았던 RNG selector <code>0x0040bdcc</code>를 handler/opcode 단위로 분리한 리뷰입니다.</p>
  <nav class="nav">
    <a class="chip" href="index.html">index</a>
    <a class="chip" href="field_encounter_static_review.html">field encounter static</a>
    <a class="chip" href="scene_event_vm_review.html">scene/event VM</a>
    <a class="chip" href="../out/field_rng_selector_opcode1c_review.json">artifact json</a>
  </nav>
  <section class="cards">
    {metric("handler entry matches", "yes" if summary["handlerEntryMatchesExpected"] else "no")}
    {metric("well-formed blocks", summary["wellFormedOpcode1cBlockCount"])}
    {metric("low-byte refs", summary["scriptHandlerTableLowByteReferenceCount"])}
    {metric("invalid low-byte refs", summary["invalidLowByteReferenceCount"])}
    {metric("field binding", "no" if not summary["directFieldEncounterBindingFound"] else "yes")}
  </section>
  <section class="panel">
    <h2>결론</h2>
    <p>{h(summary["decision"])}</p>
    <ul>
      <li>handler table: <code>{h(handler["handlerTableVaHex"])}</code>, opcode <code>{h(handler["opcodeHex"])}</code>, handler <code>{h(handler["handlerEntryValueHex"])}</code></li>
      <li>RNG call: <code>{h(handler["rngCallVaHex"])}</code></li>
      <li>active object globals: <code>{h(handler["activeCountGlobalHex"])}</code>, <code>{h(handler["activeOrderGlobalHex"])}</code>, <code>{h(handler["activeSlotWeightBaseHex"])}</code></li>
    </ul>
  </section>
  <section class="panel">
    <h2>Handler Semantics</h2>
    <ul>{semantics}</ul>
  </section>
  <h2>Well-formed Opcode Blocks</h2>
  <div class="table-wrap"><table>
    <thead><tr><th>stream</th><th>section</th><th>mode</th><th>pointer count</th><th>pointers</th></tr></thead>
    <tbody>{''.join(candidate_rows)}</tbody>
  </table></div>
  <h2>기존 low-byte refs 재검토</h2>
  <p>기존 script handler table의 refs는 dword low byte가 <code>0x1c</code>였다는 뜻일 뿐이다. 아래처럼 mode byte가 <code>0xf7</code>이면 opcode 0x1c handler가 정상 소비할 수 없다.</p>
  <div class="table-wrap"><table>
    <thead><tr><th>word</th><th>value</th><th>raw bytes</th><th>mode byte</th><th>source/target metadata</th><th>interpretation</th></tr></thead>
    <tbody>{''.join(ref_rows)}</tbody>
  </table></div>
  <h2>Summary JSON</h2>
  <pre>{h(json.dumps(summary, ensure_ascii=False, indent=2))}</pre>
  <script>
    window.HWANSE_FIELD_RNG_SELECTOR_OPCODE1C_REVIEW_READY = {json.dumps({
        "status": "ok",
        "wellFormedOpcode1cBlockCount": summary["wellFormedOpcode1cBlockCount"],
        "directFieldEncounterBindingFound": summary["directFieldEncounterBindingFound"],
    }, ensure_ascii=False)};
  </script>
</main>
</body>
</html>
"""


def metric(label: str, value: Any) -> str:
    return f'<div class="card"><div class="num">{h(value)}</div><div class="label">{h(label)}</div></div>'


def main() -> None:
    OUT.mkdir(parents=True, exist_ok=True)
    WEB.mkdir(parents=True, exist_ok=True)
    report = build_report()
    write_json(OUT / "field_rng_selector_opcode1c_review.json", report)
    html_text = render_html(report)
    (WEB / "field_rng_selector_opcode1c_review.html").write_text(html_text, encoding="utf-8")
    print("wrote field RNG selector opcode 0x1c review")


if __name__ == "__main__":
    main()
