#!/usr/bin/env python3
"""Summarize the grounded selected-root scenario VM pattern.

This report consolidates the scattered selected-root/selector-root evidence into
one narrow page.  It intentionally distinguishes proven VM primitives from the
still-missing field/scene trigger producer.
"""
from __future__ import annotations

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

from probe_exe_scene_tables import 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

VM_DISPATCHER_VA = 0x00402321
VM_NESTED_RUNNER_VA = 0x00402360
SELECTOR_BYTE_WRITER_VA = 0x00406DBB
SELECTED_ROOT_INDEXED_WRITER_VA = 0x0040AD9B
SELECTED_ROOT_EXECUTOR_VA = 0x0040ADC9
SELECTED_ROOT_DIRECT_WRITER_VA = 0x0040AE0E
OBJECT_POOL_INIT_VA = 0x004359D6

SELECTED_ROOT_VA = 0x0059DE30
ROOT_OBJECT_VA = 0x0055BE00
CURRENT_STREAM_OFFSET = 0x40
RETURN_STACK_OFFSET = 0x44
RETURN_DEPTH_OFFSET = 0x5C
VM_CONTEXT_OFFSET_CANDIDATE = 0xA8
SCRIPT_POINTER_OFFSET_CANDIDATE = 0xB0

RELATED_REPORTS = [
    "selected_root_live_writer_frontier_review",
    "selected_root_opcode_matrix_review",
    "selector_root_structure_review",
    "scene_event_vm_execution_route_review",
    "scene_event_vm_command_stream_candidates",
]


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


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


def load_json(name: str) -> dict[str, Any]:
    path = OUT / f"{name}.json"
    if not path.exists():
        return {}
    return json.loads(path.read_text(encoding="utf-8"))


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


def dword_refs(exe: bytes, sections: list[dict[str, Any]], value: int) -> list[str]:
    needle = struct.pack("<I", value)
    refs: list[str] = []
    pos = exe.find(needle)
    while pos >= 0:
        va = None
        for section in sections:
            raw = int(section["raw"])
            size = int(section["raw_size"])
            if raw <= pos < raw + size:
                va = int(section["va"]) + (pos - raw)
                break
        if va is not None:
            refs.append(hx(va) or "")
        pos = exe.find(needle, pos + 1)
    return refs


def dword_at(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 dispatch_entry(exe: bytes, sections: list[dict[str, Any]], handler_va: int) -> dict[str, Any]:
    refs = dword_refs(exe, sections, handler_va)
    dispatch_refs = []
    for ref in refs:
        ref_int = int(ref, 16)
        if GENERAL_DISPATCH_TABLE_VA <= ref_int < GENERAL_DISPATCH_TABLE_VA + 0x500:
            index = (ref_int - GENERAL_DISPATCH_TABLE_VA) // 4
            dispatch_refs.append(
                {
                    "entryVaHex": ref,
                    "opcodeIndex": index,
                    "opcodeIndexHex": hx(index, 2),
                }
            )
    return {
        "handlerVaHex": hx(handler_va),
        "dispatchRefs": dispatch_refs,
        "allDwordRefs": refs,
    }


def summarize_related() -> dict[str, Any]:
    related = {name: load_json(name) for name in RELATED_REPORTS}
    selector_structure = related["selector_root_structure_review"]
    opcode_matrix = related["selected_root_opcode_matrix_review"]
    live_writer = related["selected_root_live_writer_frontier_review"]
    route_review = related["scene_event_vm_execution_route_review"]
    command_candidates = related["scene_event_vm_command_stream_candidates"]
    selector_summary = selector_structure.get("summary", {})
    opcode_summary = opcode_matrix.get("summary", {})
    route_summary = route_review.get("summary", {})
    command_summary = command_candidates.get("summary", {})

    return {
        "selectorRootCount": selector_summary.get("selectorRootCount"),
        "selectorRows": selector_summary.get("selectorRows"),
        "rootsWithSceneSeqCount": selector_summary.get("rootsWithSceneSeqCount"),
        "rootsWithResourceRefCount": selector_summary.get("rootsWithResourceRefCount"),
        "sequenceRootWithResourceStructureCount": selector_summary.get("sequenceRootWithResourceStructureCount"),
        "unclassifiedSelectorRootCount": selector_summary.get("unclassifiedSelectorRootCount"),
        "rootsWithSelectedRootOpcodeCount": opcode_summary.get("rootsWithSelectedRootOpcodeCount"),
        "opcode4fMode1WriterCount": opcode_summary.get("opcode4fMode1WriterCount"),
        "opcode4fSelfWriterCount": opcode_summary.get("opcode4fSelfWriterCount"),
        "opcode4fSlotAliasCount": opcode_summary.get("opcode4fSlotAliasCount"),
        "selectedRootDirectRefCount": live_writer.get("summary", {}).get("directRefCount"),
        "selectedRootExecutorFound": live_writer.get("summary", {}).get("selectedRootExecutorFound"),
        "liveWriterStatus": live_writer.get("status"),
        "opcodeMatrixStatus": opcode_matrix.get("status"),
        "selectorStructureStatus": selector_structure.get("promotionStatus"),
        "routeReviewStatus": route_review.get("promotionStatus"),
        "directExecutionRootFound": route_summary.get("directExecutionRootFound"),
        "commandBlockCount": command_summary.get("commandBlockCount"),
        "dialogueLikeBlockCount": command_summary.get("dialogueLikeBlockCount"),
        "routeLinkedCommandBlockCount": command_summary.get("routeLinkedCommandBlockCount"),
        "mapLinkedCommandBlockCount": command_summary.get("mapLinkedCommandBlockCount"),
    }


def build_report() -> dict[str, Any]:
    exe = EXE.read_bytes()
    sections = read_sections(exe)
    related_summary = summarize_related()

    dispatch = {
        "vmDispatcher": dispatch_entry(exe, sections, VM_DISPATCHER_VA),
        "selectorByteWriter": dispatch_entry(exe, sections, SELECTOR_BYTE_WRITER_VA),
        "selectedRootIndexedWriter": dispatch_entry(exe, sections, SELECTED_ROOT_INDEXED_WRITER_VA),
        "selectedRootExecutor": dispatch_entry(exe, sections, SELECTED_ROOT_EXECUTOR_VA),
        "selectedRootDirectWriter": dispatch_entry(exe, sections, SELECTED_ROOT_DIRECT_WRITER_VA),
    }

    byte_evidence = [
        {
            "label": "generic VM dispatcher",
            "vaHex": hx(VM_DISPATCHER_VA),
            "bytes": read_bytes(exe, sections, VM_DISPATCHER_VA, 0x3F),
            "decoded": "reads object+0x40 as current stream, reads opcode byte, dispatches through table 0x00440538.",
            "status": "confirmed",
        },
        {
            "label": "nested VM stream runner",
            "vaHex": hx(VM_NESTED_RUNNER_VA),
            "bytes": read_bytes(exe, sections, VM_NESTED_RUNNER_VA, 0x3F),
            "decoded": "temporarily replaces object+0x40 with an argument stream, runs the dispatcher, then restores the old stream.",
            "status": "confirmed",
        },
        {
            "label": "selector byte writer",
            "vaHex": hx(SELECTOR_BYTE_WRITER_VA),
            "bytes": read_bytes(exe, sections, SELECTOR_BYTE_WRITER_VA, 0xF3),
            "decoded": "mode 1 writes selector bytes from stream operands and jumps to a fixed root pointer; modes 2/3 delegate save/load restore.",
            "status": "confirmed",
        },
        {
            "label": "selected root indexed writer",
            "vaHex": hx(SELECTED_ROOT_INDEXED_WRITER_VA),
            "bytes": read_bytes(exe, sections, SELECTED_ROOT_INDEXED_WRITER_VA, 0x2E),
            "decoded": "selected_root = dword[streamTable + stream[1] * 4], where streamTable is dword at stream+4; advances stream by 8.",
            "status": "confirmed",
        },
        {
            "label": "selected root executor",
            "vaHex": hx(SELECTED_ROOT_EXECUTOR_VA),
            "bytes": read_bytes(exe, sections, SELECTED_ROOT_EXECUTOR_VA, 0x45),
            "decoded": "if selected_root is nonzero, pushes current continuation to object+0x44 return stack, increments object+0x5c, and writes selected_root to object+0x40.",
            "status": "confirmed",
        },
        {
            "label": "selected root direct writer",
            "vaHex": hx(SELECTED_ROOT_DIRECT_WRITER_VA),
            "bytes": read_bytes(exe, sections, SELECTED_ROOT_DIRECT_WRITER_VA, 0x70),
            "decoded": "mode 0 stores inline stream+4 as selected_root; mode 1 stores dword at stream+4 as selected_root.",
            "status": "confirmed",
        },
        {
            "label": "root object pool initializer",
            "vaHex": hx(OBJECT_POOL_INIT_VA),
            "bytes": read_bytes(exe, sections, OBJECT_POOL_INIT_VA, 0x4A),
            "decoded": "initializes the object pool/list around 0x0055be00, matching the VM object base seen in selected-root handlers.",
            "status": "confirmed-base",
        },
    ]

    global_refs = {
        "selectedRoot": {
            "vaHex": hx(SELECTED_ROOT_VA),
            "dwordRefs": dword_refs(exe, sections, SELECTED_ROOT_VA),
        },
        "rootObject": {
            "vaHex": hx(ROOT_OBJECT_VA),
            "dwordRefs": dword_refs(exe, sections, ROOT_OBJECT_VA),
        },
    }

    confirmed = [
        "The VM dispatcher at 0x00402321 executes bytecode streams from object+0x40.",
        "selected_root at 0x0059de30 is a real global consumed by the selected-root executor.",
        "0x0040ad9b writes selected_root by table index; 0x0040ae0e writes it directly or as an inline continuation.",
        "0x0040adc9 pushes the current stream to object+0x44/object+0x5c and jumps object+0x40 to selected_root.",
        "0x00406dbb writes selector bytes and jumps to fixed roots, so selector bytes and selected roots are part of the same VM family.",
        "Existing selector-root reports already expose dozens of roots and command blocks that fit this mechanism.",
    ]
    candidates = [
        "field/NPC/region triggers likely enter this VM through active descriptors or inline stream pointers, but the exact live producer is not promoted here.",
        "object+0xa8 appears repeatedly in VM handlers as a context/base pointer family; object+0xb0 remains a script-pointer candidate, not a confirmed stream slot in this review.",
        "selectedRoot indexed tables and direct inline roots are good static enumeration targets, but each entry still needs owner/trigger binding.",
    ]
    blocked = [
        "No static proof yet maps a specific field entrance, talk target, or region descriptor to one selected_root write in normal gameplay.",
        "scene/event command streams are found, but route-linked command block count remains zero in the current command-stream candidate report.",
        "automatic map transition and scenario branch producer remain separate problems from the selected-root executor mechanism.",
    ]

    return {
        "kind": "hwanse-scenario-selected-root-pattern-review",
        "source": "tools/build_scenario_selected_root_pattern_review.py",
        "status": "scenario-selected-root-vm-pattern-grounded-producer-pending",
        "decision": (
            "The web-runtime observation is consistent with the EXE.  A common VM runner, selected-root writers, "
            "and selected-root executor are statically grounded.  The remaining missing evidence is the upstream "
            "producer that chooses those streams from concrete field/NPC/region conditions."
        ),
        "globals": {
            "selectedRootVaHex": hx(SELECTED_ROOT_VA),
            "rootObjectVaHex": hx(ROOT_OBJECT_VA),
            "currentStreamOffsetHex": hx(CURRENT_STREAM_OFFSET, 2),
            "returnStackOffsetHex": hx(RETURN_STACK_OFFSET, 2),
            "returnDepthOffsetHex": hx(RETURN_DEPTH_OFFSET, 2),
            "vmContextOffsetCandidateHex": hx(VM_CONTEXT_OFFSET_CANDIDATE, 2),
            "scriptPointerOffsetCandidateHex": hx(SCRIPT_POINTER_OFFSET_CANDIDATE, 2),
        },
        "dispatch": dispatch,
        "byteEvidence": byte_evidence,
        "globalRefs": global_refs,
        "confirmed": confirmed,
        "candidates": candidates,
        "blocked": blocked,
        "relatedSummary": related_summary,
        "relatedReports": [{"json": f"out/{name}.json", "html": f"web/{name}.html"} for name in RELATED_REPORTS],
        "nextStaticActions": [
            "Enumerate opcode 0x81 indexed-writer command rows and group them by owner root.",
            "Enumerate opcode 0x83 direct-writer inline roots and split mode 0 inline continuation from mode 1 dword-root writes.",
            "Cross-link selected-root writer rows to prompt/text groups and resource records, but keep them below route-proof until a producer is found.",
            "Use future runtime traces to tag which writer row fired before a known talk/region/event interaction.",
        ],
    }


def render_table(rows: list[dict[str, Any]], columns: list[tuple[str, str]]) -> str:
    head = "".join(f"<th>{h(label)}</th>" for _, label in columns)
    body = []
    for row in rows:
        cells = "".join(f"<td>{h(row.get(key))}</td>" for key, _ in columns)
        body.append(f"<tr>{cells}</tr>")
    return f"<table><thead><tr>{head}</tr></thead><tbody>{''.join(body)}</tbody></table>"


def render_html(report: dict[str, Any]) -> str:
    evidence_rows = [
        {
            "label": row["label"],
            "va": row["vaHex"],
            "status": row["status"],
            "decoded": row["decoded"],
        }
        for row in report["byteEvidence"]
    ]
    related = [
        {"key": k, "value": v}
        for k, v in report["relatedSummary"].items()
    ]
    dispatch_rows = []
    for name, row in report["dispatch"].items():
        refs = row.get("dispatchRefs") or []
        opcodes = ", ".join(ref.get("opcodeIndexHex") or "" for ref in refs) or "-"
        entries = ", ".join(ref.get("entryVaHex") or "" for ref in refs) or "-"
        dispatch_rows.append({"name": name, "handler": row["handlerVaHex"], "opcodes": opcodes, "entries": entries})

    def bullets(items: list[str]) -> str:
        return "<ul>" + "".join(f"<li>{h(item)}</li>" for item in items) + "</ul>"

    return f"""<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Scenario selected-root pattern review</title>
  <style>
    body {{ margin: 0; background: #f6f7f9; color: #20242b; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; line-height: 1.45; }}
    main {{ width: min(1180px, calc(100vw - 28px)); margin: 0 auto; padding: 22px 0 36px; }}
    h1 {{ margin: 0 0 8px; font-size: 26px; }}
    h2 {{ margin: 18px 0 8px; font-size: 18px; }}
    .panel {{ background: #fff; border: 1px solid #d9dee7; border-radius: 8px; padding: 14px; margin: 12px 0; }}
    .status {{ display: inline-block; padding: 3px 8px; border-radius: 999px; background: #fff3cd; color: #7a5600; font-size: 12px; font-weight: 700; }}
    code {{ background: #eef2f7; padding: 1px 4px; border-radius: 4px; }}
    table {{ border-collapse: collapse; width: 100%; background: #fff; font-size: 13px; }}
    th, td {{ border: 1px solid #dfe4ec; padding: 7px 8px; vertical-align: top; text-align: left; }}
    th {{ background: #f0f3f8; }}
    .grid {{ display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }}
    .small {{ color: #687080; font-size: 13px; }}
    @media (max-width: 900px) {{ .grid {{ grid-template-columns: 1fr; }} table {{ display: block; overflow-x: auto; }} }}
  </style>
</head>
<body>
<main>
  <h1>Scenario selected-root pattern review</h1>
  <p><span class="status">{h(report["status"])}</span></p>
  <div class="panel">
    <p>{h(report["decision"])}</p>
    <p class="small">JSON: <code>out/scenario_selected_root_pattern_review.json</code></p>
  </div>

  <div class="grid">
    <section class="panel">
      <h2>확정</h2>
      {bullets(report["confirmed"])}
    </section>
    <section class="panel">
      <h2>후보</h2>
      {bullets(report["candidates"])}
    </section>
    <section class="panel">
      <h2>차단점</h2>
      {bullets(report["blocked"])}
    </section>
  </div>

  <h2>전역/오프셋</h2>
  {render_table([{"key": k, "value": v} for k, v in report["globals"].items()], [("key", "name"), ("value", "value")])}

  <h2>디스패치 엔트리</h2>
  {render_table(dispatch_rows, [("name", "name"), ("handler", "handler VA"), ("opcodes", "opcode index"), ("entries", "dispatch entry")])}

  <h2>바이트 근거</h2>
  {render_table(evidence_rows, [("label", "label"), ("va", "VA"), ("status", "status"), ("decoded", "decoded semantics")])}

  <h2>관련 산출물 요약</h2>
  {render_table(related, [("key", "metric"), ("value", "value")])}

  <h2>다음 정적 분석</h2>
  {bullets(report["nextStaticActions"])}
</main>
</body>
</html>"""


def main() -> None:
    report = build_report()
    OUT.mkdir(exist_ok=True)
    WEB.mkdir(exist_ok=True)
    (OUT / "scenario_selected_root_pattern_review.json").write_text(
        json.dumps(report, ensure_ascii=False, indent=2),
        encoding="utf-8",
    )
    html_text = render_html(report)
    (OUT / "scenario_selected_root_pattern_review.html").write_text(html_text, encoding="utf-8")
    (WEB / "scenario_selected_root_pattern_review.html").write_text(html_text, encoding="utf-8")
    print(report["status"])


if __name__ == "__main__":
    main()
