#!/usr/bin/env python3
"""Explain the selected-pointer value captured by runtime_memory_snapshot."""
from __future__ import annotations

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

from probe_exe_scene_tables import offset_to_va, read_sections


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"
EXE = ROOT / "Hwanse2.exe"
CURRENT_ROUTE_SELECTOR = "2:0"
CURRENT_ROUTE_ROOT_HEX = "0x00540714"


def int_hex(value: str | None) -> int | None:
    if not value:
        return None
    try:
        return int(value, 16)
    except ValueError:
        return None


def hex32(value: int | None) -> str | None:
    if value is None:
        return None
    return f"0x{value & 0xFFFFFFFF: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 walk_objects(value: Any) -> list[dict]:
    found = []
    if isinstance(value, dict):
        found.append(value)
        for child in value.values():
            found.extend(walk_objects(child))
    elif isinstance(value, list):
        for child in value:
            found.extend(walk_objects(child))
    return found


def cns_strings_near(exe: bytes, sections: list[dict], start_va: int, end_va: int) -> list[dict]:
    rows = []
    for match in re.finditer(rb"[a-z0-9_]{2,12}\.cns\0", exe):
        va = offset_to_va(sections, match.start())
        if va is None or not (start_va <= va < end_va):
            continue
        rows.append({
            "vaHex": hex32(va),
            "filename": match.group(0)[:-1].decode("ascii", "replace"),
        })
    return rows


def find_selector_row(selectors: list[dict], label: str) -> dict | None:
    group_text, slot_text = label.split(":", 1)
    group = int(group_text)
    slot = int(slot_text)
    for row in selectors:
        if row.get("group") == group and row.get("slot") == slot:
            return row
    return None


def selector_context_from_writers(selection_writers: dict, selected_va: int) -> dict:
    contexts = []
    writer_rows = []
    for row in walk_objects(selection_writers):
        context = row.get("selectorRootContext")
        if isinstance(context, dict):
            start = int_hex(context.get("rangeStartHex"))
            end = int_hex(context.get("rangeEndHex"))
            if start is not None and end is not None and start <= selected_va < end:
                contexts.append(context)
                if row.get("vaHex"):
                    writer_rows.append(row)
        elif row.get("rangeStartHex") and row.get("rangeEndHex"):
            start = int_hex(row.get("rangeStartHex"))
            end = int_hex(row.get("rangeEndHex"))
            if start is not None and end is not None and start <= selected_va < end:
                contexts.append(row)
    unique_contexts = []
    seen = set()
    for context in contexts:
        key = (tuple(context.get("labels") or []), context.get("rangeStartHex"), context.get("rangeEndHex"))
        if key not in seen:
            unique_contexts.append(context)
            seen.add(key)
    unique_writers = []
    seen_writers = set()
    for row in writer_rows:
        key = row.get("vaHex")
        if key and key not in seen_writers:
            unique_writers.append(row)
            seen_writers.add(key)
    return {
        "contexts": unique_contexts,
        "writerRows": unique_writers,
    }


def build_summary(
    snapshot: dict,
    selectors: list[dict],
    selection_writers: dict,
    exe: bytes,
    sections: list[dict],
) -> dict:
    memory = snapshot.get("memory") or {}
    selected_va = int_hex(memory.get("selectedPointerStaticValueHex"))
    selected_context = selector_context_from_writers(selection_writers, selected_va or 0)
    contexts = selected_context["contexts"]
    primary_context = contexts[0] if contexts else {}
    labels = primary_context.get("labels") or []
    selector_label = labels[0] if labels else None
    selector_row = find_selector_row(selectors, selector_label) if selector_label else None
    nearby_start = (selected_va or 0) - 0x80
    nearby_end = (selected_va or 0) + 0x120
    nearby_cns = cns_strings_near(exe, sections, nearby_start, nearby_end) if selected_va else []
    current_route_selected = memory.get("selectedPointerEqualsCurrentRoot") is True
    samples = {
        row.get("name"): row
        for row in memory.get("samples") or []
    }
    opcode24_runtime_samples = [
        {
            "name": name,
            "staticVaHex": (samples.get(name) or {}).get("staticVaHex"),
            "runtimeVaHex": (samples.get(name) or {}).get("runtimeVaHex"),
            "firstByteHex": (samples.get(name) or {}).get("firstByteHex"),
            "bytesHex": (samples.get(name) or {}).get("bytesHex"),
            "readOk": (samples.get(name) or {}).get("readOk"),
            "role": (samples.get(name) or {}).get("role"),
        }
        for name in [
            "opcode24-current-object-index",
            "opcode24-mode2-source",
            "opcode24-mode1-source",
            "opcode24-runtime-enabled-flag",
            "secondary-branch-state",
        ]
        if name in samples
    ]
    opcode24_idle_all_zero = all(
        row.get("firstByteHex") == "0x00"
        for row in opcode24_runtime_samples
        if row.get("name") != "secondary-branch-state"
    )
    field_maps = selector_row.get("fieldMaps") if selector_row else primary_context.get("fieldRecords")
    if field_maps and field_maps and isinstance(field_maps[0], dict):
        field_maps = [row.get("map") for row in field_maps if row.get("map")]
    writer_brief = [
        {
            "vaHex": row.get("vaHex"),
            "opcodeHex": row.get("opcodeHex"),
            "operation": row.get("operation"),
            "meaning": row.get("meaning"),
            "nextDwordHex": row.get("nextDwordHex"),
            "nextDwordCns": row.get("nextDwordCns"),
        }
        for row in selected_context["writerRows"]
    ]
    return {
        "objective": "classify the live selected-pointer value captured by runtime_memory_snapshot",
        "snapshotStatus": snapshot.get("snapshotStatus"),
        "loadedBaseHex": snapshot.get("loadedBaseHex"),
        "selectedPointerStaticValueHex": memory.get("selectedPointerStaticValueHex"),
        "selectedPointerRuntimeValueHex": memory.get("selectedPointerRuntimeValueHex"),
        "currentRouteSelector": CURRENT_ROUTE_SELECTOR,
        "currentRouteRootHex": CURRENT_ROUTE_ROOT_HEX,
        "selectedPointerEqualsCurrentRouteRoot": current_route_selected,
        "selectedPointerContextSelector": selector_label,
        "selectedPointerContextRangeHex": (
            f"{primary_context.get('rangeStartHex')}..{primary_context.get('rangeEndHex')}"
            if primary_context else None
        ),
        "selectedPointerRelativeOffsetHex": primary_context.get("relativeOffsetHex"),
        "selectedPointerContextFieldMaps": field_maps or [],
        "selectedPointerContextResources": primary_context.get("resources") or (selector_row or {}).get("linkedCns") or [],
        "nearbyCnsStrings": nearby_cns,
        "nearbySelectionWriterRows": writer_brief,
        "opcode24RuntimeSamples": opcode24_runtime_samples,
        "opcode24IdleGlobalBytesAllZero": opcode24_idle_all_zero,
        "contextCount": len(contexts),
        "promotionStatus": "blocked",
        "conclusion": (
            "The live selected-pointer snapshot resolves to selector 8:0 context material, not to the "
            "current blocker selector 2:0 root 0x00540714. Nearby static strings include the title/intro "
            "resource list. The same idle snapshot can read opcode 0x24 globals and currently sees the "
            "runtime enabled flag and mode source bytes as 0x00, but this is still an idle title-context "
            "sample rather than route-path watchpoint proof for map1_01a -> map2_02d."
            if selector_label == "8:0" and not current_route_selected
            else "The live selected-pointer snapshot is not enough to promote the current route."
        ),
    }


def markdown(summary: dict) -> str:
    lines = [
        "# Runtime Memory Snapshot Context",
        "",
        f"- selected pointer: `{summary.get('selectedPointerStaticValueHex')}`",
        f"- selected context selector: `{summary.get('selectedPointerContextSelector')}`",
        f"- selected context range: `{summary.get('selectedPointerContextRangeHex')}`",
        f"- current route selector: `{summary.get('currentRouteSelector')}`",
        f"- current route root: `{summary.get('currentRouteRootHex')}`",
        f"- selected pointer equals current route root: {summary.get('selectedPointerEqualsCurrentRouteRoot')}",
        f"- promotion status: `{summary.get('promotionStatus')}`",
        "",
        summary.get("conclusion") or "",
        "",
        "## Nearby CNS Strings",
        "",
    ]
    lines.extend(
        f"- `{row.get('vaHex')}` {row.get('filename')}"
        for row in summary.get("nearbyCnsStrings") or []
    )
    lines.extend(["", "## Nearby Selection Writers", ""])
    if summary.get("nearbySelectionWriterRows"):
        lines.extend(
            f"- `{row.get('vaHex')}` {row.get('operation')} {row.get('opcodeHex')} "
            f"{row.get('meaning')} next={row.get('nextDwordCns') or row.get('nextDwordHex')}"
            for row in summary.get("nearbySelectionWriterRows") or []
        )
    else:
        lines.append("- none")
    lines.extend([
        "",
        "## Opcode 0x24 Runtime Samples",
        "",
        f"- idle opcode24 global bytes all zero: {summary.get('opcode24IdleGlobalBytesAllZero')}",
        "",
        "| name | static VA | runtime VA | first byte | read | role |",
        "| --- | --- | --- | --- | --- | --- |",
    ])
    for row in summary.get("opcode24RuntimeSamples") or []:
        lines.append(
            f"| {row.get('name')} | `{row.get('staticVaHex')}` | `{row.get('runtimeVaHex')}` | "
            f"`{row.get('firstByteHex')}` | {row.get('readOk')} | {row.get('role')} |"
        )
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    cns_items = "".join(
        f"<li><code>{html.escape(str(row.get('vaHex')))}</code> {html.escape(str(row.get('filename')))}</li>"
        for row in summary.get("nearbyCnsStrings") or []
    )
    writer_items = "".join(
        "<li>"
        f"<code>{html.escape(str(row.get('vaHex')))}</code> "
        f"{html.escape(str(row.get('operation')))} {html.escape(str(row.get('opcodeHex')))} "
        f"{html.escape(str(row.get('meaning')))} next="
        f"{html.escape(str(row.get('nextDwordCns') or row.get('nextDwordHex')))}"
        "</li>"
        for row in summary.get("nearbySelectionWriterRows") or []
    ) or "<li>none</li>"
    opcode_rows = "\n".join(
        "<tr>"
        f"<td>{html.escape(str(row.get('name')))}</td>"
        f"<td><code>{html.escape(str(row.get('staticVaHex')))}</code></td>"
        f"<td><code>{html.escape(str(row.get('runtimeVaHex')))}</code></td>"
        f"<td><code>{html.escape(str(row.get('firstByteHex')))}</code></td>"
        f"<td>{html.escape(str(row.get('readOk')))}</td>"
        f"<td>{html.escape(str(row.get('role')))}</td>"
        "</tr>"
        for row in summary.get("opcode24RuntimeSamples") or []
    )
    return "\n".join([
        "<!doctype html><meta charset=\"utf-8\"><title>Runtime Memory Snapshot Context</title>",
        "<style>body{font-family:system-ui,sans-serif;background:#111;color:#eee;max-width:1000px;margin:24px auto}code{color:#9bd4ff}</style>",
        "<h1>Runtime Memory Snapshot Context</h1>",
        "<ul>",
        f"<li>selected pointer: <code>{html.escape(str(summary.get('selectedPointerStaticValueHex')))}</code></li>",
        f"<li>selected context selector: <code>{html.escape(str(summary.get('selectedPointerContextSelector')))}</code></li>",
        f"<li>current route root: <code>{html.escape(str(summary.get('currentRouteRootHex')))}</code></li>",
        f"<li>selected pointer equals current route root: {html.escape(str(summary.get('selectedPointerEqualsCurrentRouteRoot')))}</li>",
        f"<li>promotion status: <code>{html.escape(str(summary.get('promotionStatus')))}</code></li>",
        "</ul>",
        f"<p>{html.escape(str(summary.get('conclusion') or ''))}</p>",
        "<h2>Nearby CNS Strings</h2>",
        f"<ul>{cns_items}</ul>",
        "<h2>Nearby Selection Writers</h2>",
        f"<ul>{writer_items}</ul>",
        "<h2>Opcode 0x24 Runtime Samples</h2>",
        f"<p>idle opcode24 global bytes all zero: {html.escape(str(summary.get('opcode24IdleGlobalBytesAllZero')))}</p>",
        "<table><thead><tr><th>name</th><th>static VA</th><th>runtime VA</th><th>first byte</th><th>read</th><th>role</th></tr></thead><tbody>",
        opcode_rows,
        "</tbody></table>",
    ])


def write_outputs(summary: dict, out_dir: Path = OUT) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "runtime_memory_snapshot_context.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--out-dir", type=Path, default=OUT)
    args = parser.parse_args()
    snapshot = load_json(args.out_dir / "runtime_memory_snapshot.json", {})
    selectors = load_json(args.out_dir / "save_scene_selectors.json", [])
    selection_writers = load_json(args.out_dir / "save_selector_selection_writers.json", {})
    exe = EXE.read_bytes()
    sections = read_sections(exe)
    summary = build_summary(snapshot, selectors, selection_writers, exe, sections)
    write_outputs(summary, args.out_dir)
    print(f"wrote runtime memory snapshot context -> {args.out_dir / 'runtime_memory_snapshot_context.json'}")


if __name__ == "__main__":
    main()
