#!/usr/bin/env python3
"""Build a selector-root opcode matrix for selected-root related handlers.

`selected_root_live_writer_frontier_review` grounds the VM primitives:
selector-byte writer 0x4f and selectedRoot opcodes 0x07/0x08/0x09.  This pass
does not re-prove those handlers.  It scans every known selector root and asks:

* which roots contain the selector-byte writer opcode 0x4f in the byte-writing
  mode,
* whether opcode 0x07 ever selects an exact selector-root start,
* whether opcode 0x09 ever stores an exact selector-root start or crosses into
  another root,
* whether any source/predecessor route roots select the current 2:0 route root.

The strict distinction matters: many dwords have low byte 0x07/0x08/0x09/0x4f
as data, so only exact-root targets are allowed to promote route proof.
"""
from __future__ import annotations

import html
import json
import struct
from collections import Counter
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"

SOURCE_SELECTOR = "0:0"
PREDECESSOR_SELECTOR = "1:0"
CURRENT_SELECTOR = "2:0"


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


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


def selector_key(context: dict[str, Any] | None) -> str:
    if not context:
        return ""
    keys = context.get("selectorKeys") or []
    return ",".join(str(item) for item in keys)


def primary_selector(context: dict[str, Any] | None) -> str:
    if not context:
        return ""
    keys = context.get("selectorKeys") or []
    return str(keys[0]) if keys else ""


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 selector_group_slot(selector: str) -> tuple[int | None, int | None]:
    try:
        group, slot = selector.split(":", 1)
        return int(group), int(slot)
    except ValueError:
        return None, None


def root_contexts() -> list[dict[str, Any]]:
    data = json.loads((OUT / "selector_root_structure_review.json").read_text(encoding="utf-8"))
    contexts = []
    for row in data.get("roots") or []:
        if not isinstance(row.get("rootVa"), int) or not isinstance(row.get("rangeEndVa"), int):
            continue
        contexts.append(
            {
                **row,
                "start": int(row["rootVa"]),
                "end": int(row["rangeEndVa"]),
                "selector": selector_key(row),
                "primarySelector": primary_selector(row),
            }
        )
    contexts.sort(key=lambda row: row["start"])
    return contexts


def context_for_va(contexts: list[dict[str, Any]], va: int | None) -> dict[str, Any] | None:
    if va is None:
        return None
    for context in contexts:
        if context["start"] <= va < context["end"]:
            return context
    return None


def compact(values: list[str], limit: int = 8) -> str:
    if not values:
        return "-"
    if len(values) <= limit:
        return ", ".join(values)
    return ", ".join(values[:limit]) + f" +{len(values) - limit}"


def classify_opcode4f(context: dict[str, Any], va: int, value: int) -> dict[str, Any] | None:
    mode = (value >> 8) & 0xFF
    if mode != 1:
        return None
    target = f"{(value >> 16) & 0xFF}:{(value >> 24) & 0xFF}"
    source = context["primarySelector"]
    source_group, source_slot = selector_group_slot(source)
    target_group, target_slot = selector_group_slot(target)
    if target == source:
        classification = "self-selector-writer"
    elif source_group == target_group and source_slot == 1 and target_slot == 0:
        classification = "same-group-slot1-to-slot0-alias"
    elif source_group == target_group:
        classification = "same-group-other-slot-alias"
    else:
        classification = "cross-group-selector-write-candidate"
    return {
        "kind": "opcode4f-selector-byte-writer",
        "vaHex": hx(va),
        "valueHex": hx(value),
        "selector": source,
        "targetSelector": target,
        "classification": classification,
        "fieldMaps": context.get("fieldMaps") or [],
        "rootVaHex": context.get("rootVaHex"),
        "rootClass": context.get("rootClass"),
    }


def classify_opcode09(
    exe: bytes,
    sections: list[dict[str, Any]],
    contexts: list[dict[str, Any]],
    exact_root_starts: set[int],
    context: dict[str, Any],
    va: int,
    value: int,
) -> dict[str, Any] | None:
    mode = (value >> 8) & 0xFF
    if mode not in {0, 1}:
        return None
    stored = va + 4 if mode == 0 else dword_at(exe, sections, va + 4)
    target_context = context_for_va(contexts, stored)
    target_selector = primary_selector(target_context)
    return {
        "kind": "opcode09-selected-root-store",
        "vaHex": hx(va),
        "valueHex": hx(value),
        "selector": context["primarySelector"],
        "mode": mode,
        "storedPointerHex": hx(stored),
        "storedPointerSelector": target_selector,
        "storedPointerIsExactRoot": stored in exact_root_starts if stored is not None else False,
        "storedPointerCrossesRoot": bool(target_selector and target_selector != context["primarySelector"]),
        "fieldMaps": context.get("fieldMaps") or [],
        "rootVaHex": context.get("rootVaHex"),
    }


def classify_opcode07(
    exe: bytes,
    sections: list[dict[str, Any]],
    contexts: list[dict[str, Any]],
    exact_root_starts: set[int],
    context: dict[str, Any],
    va: int,
    value: int,
) -> dict[str, Any] | None:
    if (value >> 16) & 0xFFFF:
        return None
    index = (value >> 8) & 0xFF
    table = dword_at(exe, sections, va + 4)
    table_context = context_for_va(contexts, table)
    if not table_context:
        return None
    selected_slot = table + index * 4 if table is not None else None
    selected_value = dword_at(exe, sections, selected_slot) if selected_slot is not None else None
    selected_context = context_for_va(contexts, selected_value)
    return {
        "kind": "opcode07-selected-root-indexed-writer-candidate",
        "vaHex": hx(va),
        "valueHex": hx(value),
        "selector": context["primarySelector"],
        "index": index,
        "operandTableHex": hx(table),
        "operandTableSelector": primary_selector(table_context),
        "selectedSlotHex": hx(selected_slot),
        "selectedValueHex": hx(selected_value),
        "selectedValueSelector": primary_selector(selected_context),
        "selectedValueIsExactRoot": selected_value in exact_root_starts if selected_value is not None else False,
        "selectedValueCrossesRoot": bool(
            selected_context and primary_selector(selected_context) != context["primarySelector"]
        ),
        "fieldMaps": context.get("fieldMaps") or [],
        "rootVaHex": context.get("rootVaHex"),
    }


def build_report() -> dict[str, Any]:
    exe = EXE.read_bytes()
    sections = read_sections(exe)
    contexts = root_contexts()
    exact_root_starts = {int(row["start"]) for row in contexts}

    opcode4f_rows: list[dict[str, Any]] = []
    opcode09_rows: list[dict[str, Any]] = []
    opcode07_rows: list[dict[str, Any]] = []
    opcode08_count = 0
    low_byte_counts: Counter[str] = Counter()

    for context in contexts:
        for va in range(context["start"], context["end"], 4):
            value = dword_at(exe, sections, va)
            if value is None:
                continue
            opcode = value & 0xFF
            if opcode not in {0x07, 0x08, 0x09, 0x4F}:
                continue
            low_byte_counts[f"0x{opcode:02x}"] += 1
            if opcode == 0x08:
                opcode08_count += 1
            elif opcode == 0x4F:
                row = classify_opcode4f(context, va, value)
                if row:
                    opcode4f_rows.append(row)
            elif opcode == 0x09:
                row = classify_opcode09(exe, sections, contexts, exact_root_starts, context, va, value)
                if row:
                    opcode09_rows.append(row)
            elif opcode == 0x07:
                row = classify_opcode07(exe, sections, contexts, exact_root_starts, context, va, value)
                if row:
                    opcode07_rows.append(row)

    source_or_predecessor = {SOURCE_SELECTOR, PREDECESSOR_SELECTOR}
    source_current_root_selects = [
        row for row in opcode07_rows
        if row["selector"] in source_or_predecessor
        and row["selectedValueIsExactRoot"]
        and row["selectedValueSelector"] == CURRENT_SELECTOR
    ]
    source_current_root_stores = [
        row for row in opcode09_rows
        if row["selector"] in source_or_predecessor
        and row["storedPointerIsExactRoot"]
        and row["storedPointerSelector"] == CURRENT_SELECTOR
    ]
    opcode4f_class_counts = Counter(row["classification"] for row in opcode4f_rows)
    opcode09_mode_counts = Counter(str(row["mode"]) for row in opcode09_rows)
    cross_group_4f = [row for row in opcode4f_rows if row["classification"] == "cross-group-selector-write-candidate"]
    exact_opcode07 = [row for row in opcode07_rows if row["selectedValueIsExactRoot"]]
    cross_range_opcode07 = [row for row in opcode07_rows if row["selectedValueCrossesRoot"]]
    exact_opcode09 = [row for row in opcode09_rows if row["storedPointerIsExactRoot"]]
    cross_range_opcode09 = [row for row in opcode09_rows if row["storedPointerCrossesRoot"]]

    summary = {
        "selectorRootCount": len(contexts),
        "rootsWithSelectedRootOpcodeCount": len(
            {
                row["rootVaHex"]
                for row in [*opcode4f_rows, *opcode07_rows, *opcode09_rows]
            }
        ),
        "lowByteCountsInRootRanges": dict(low_byte_counts),
        "opcode4fMode1WriterCount": len(opcode4f_rows),
        "opcode4fSelfWriterCount": opcode4f_class_counts.get("self-selector-writer", 0),
        "opcode4fSlotAliasCount": opcode4f_class_counts.get("same-group-slot1-to-slot0-alias", 0),
        "opcode4fCrossGroupCount": len(cross_group_4f),
        "opcode07CandidateCount": len(opcode07_rows),
        "opcode07ExactRootSelectCount": len(exact_opcode07),
        "opcode07CrossRangeSelectCount": len(cross_range_opcode07),
        "opcode07CrossRangeExactRootSelectCount": sum(1 for row in cross_range_opcode07 if row["selectedValueIsExactRoot"]),
        "opcode08LowByteConsumerCount": opcode08_count,
        "opcode09CandidateCount": len(opcode09_rows),
        "opcode09ModeCounts": dict(opcode09_mode_counts),
        "opcode09ExactRootStoreCount": len(exact_opcode09),
        "opcode09CrossRangeStoreCount": len(cross_range_opcode09),
        "sourceOrPredecessorToCurrentExactRootSelectCount": len(source_current_root_selects),
        "sourceOrPredecessorToCurrentExactRootStoreCount": len(source_current_root_stores),
        "routeProducerPromoted": False,
        "decision": (
            "Generalized selector-root opcode scanning finds grounded 0x4f mode1 self/slot-alias writers, "
            "but no opcode 0x07 exact-root selection and no opcode 0x09 exact-root store. "
            "Source/predecessor roots still do not select or store the current 2:0 route root."
        ),
    }

    return {
        "kind": "hwanse-selected-root-opcode-matrix-review",
        "source": "tools/build_selected_root_opcode_matrix_review.py",
        "status": "opcode-matrix-grounded-no-upstream-route-producer",
        "summary": summary,
        "strictInterpretation": [
            "Opcode 0x4f is counted as selector-byte writer only when stream[+1] == 1.",
            "Opcode 0x07 range hits are not promotion proof unless the selected value equals an exact selector-root start.",
            "Opcode 0x09 mode0 stores the continuation pointer; it is not a cross-root producer by itself.",
            "Opcode 0x08 consumes selectedRoot but does not identify who wrote it.",
        ],
        "opcode4fRows": opcode4f_rows,
        "opcode07ExactRootRows": exact_opcode07,
        "opcode07CrossRangeSamples": cross_range_opcode07[:40],
        "opcode09ExactRootRows": exact_opcode09,
        "opcode09CrossRangeRows": cross_range_opcode09,
        "sourceOrPredecessorToCurrentRows": {
            "opcode07ExactRootSelects": source_current_root_selects,
            "opcode09ExactRootStores": source_current_root_stores,
        },
        "remainingProofs": [
            "Find a source/predecessor or higher-level root that writes selectedRoot to the current route root exactly.",
            "Find a live stream/root producer that reaches 0x00406dbb with selector 2:0 outside save/load restore.",
            "Find a non-selector-only event/field dispatcher that chooses the current selector root during normal gameplay.",
        ],
    }


def render_html(report: dict[str, Any]) -> str:
    summary = report["summary"]
    cards = [
        ("status", report["status"]),
        ("roots", summary["selectorRootCount"]),
        ("0x4f mode1", summary["opcode4fMode1WriterCount"]),
        ("0x4f alias", summary["opcode4fSlotAliasCount"]),
        ("0x07 exact root", summary["opcode07ExactRootSelectCount"]),
        ("0x09 exact root", summary["opcode09ExactRootStoreCount"]),
        ("route producer", summary["routeProducerPromoted"]),
    ]
    card_html = "".join(f"<div class='card'><b>{h(k)}</b><span>{h(v)}</span></div>" for k, v in cards)
    opcode4f_rows = "".join(
        "<tr>"
        f"<td><code>{h(row['vaHex'])}</code><br>{h(row['classification'])}</td>"
        f"<td>{h(row['selector'])} -> {h(row['targetSelector'])}<br><code>{h(row['valueHex'])}</code></td>"
        f"<td>{h(compact(row.get('fieldMaps') or [], 6))}</td>"
        "</tr>"
        for row in report["opcode4fRows"]
    )
    cross07_rows = "".join(
        "<tr>"
        f"<td><code>{h(row['vaHex'])}</code><br><code>{h(row['valueHex'])}</code></td>"
        f"<td>{h(row['selector'])} table {h(row['operandTableSelector'])}</td>"
        f"<td><code>{h(row['selectedValueHex'])}</code><br>{h(row['selectedValueSelector'])}</td>"
        f"<td>{h(row['selectedValueIsExactRoot'])}</td>"
        "</tr>"
        for row in report["opcode07CrossRangeSamples"]
    )
    return f"""<!doctype html>
<meta charset="utf-8">
<title>Selected Root Opcode Matrix</title>
<style>
body{{font-family:system-ui,sans-serif;background:#111;color:#eee;margin:24px auto;max-width:1280px;line-height:1.45}}
code{{color:#9bd4ff}} .cards{{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin:16px 0}}
.card{{background:#1c2430;border:1px solid #344154;border-radius:8px;padding:10px}} .card b{{display:block;color:#a6b7cc}} .card span{{font-size:1.25rem}}
table{{border-collapse:collapse;width:100%;margin:12px 0 24px}} th,td{{border:1px solid #38465a;padding:7px 9px;vertical-align:top}} th{{background:#202a38}}
.note{{background:#181f2a;border-left:4px solid #8ab4ff;padding:10px 12px;margin:12px 0}}
</style>
<h1>Selected Root Opcode Matrix</h1>
<p><a href="index.html">index</a> · <a href="selected_root_live_writer_frontier_review.html">selected root writer</a> · <a href="../out/selected_root_opcode_matrix_review.json">json</a></p>
<div class="cards">{card_html}</div>
<div class="note">{h(summary['decision'])}</div>
<h2>Strict Interpretation</h2>
<ul>{''.join(f'<li>{h(item)}</li>' for item in report['strictInterpretation'])}</ul>
<h2>0x4f Mode1 Selector Writers</h2>
<table><thead><tr><th>address</th><th>selector</th><th>field maps</th></tr></thead><tbody>{opcode4f_rows}</tbody></table>
<h2>0x07 Cross-Range Samples</h2>
<p>These are range hits only. None select an exact selector-root start, so they are not route producer proof.</p>
<table><thead><tr><th>address</th><th>source/table</th><th>selected value</th><th>exact root?</th></tr></thead><tbody>{cross07_rows}</tbody></table>
</html>"""


def main() -> None:
    report = build_report()
    (OUT / "selected_root_opcode_matrix_review.json").write_text(
        json.dumps(report, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    (WEB / "selected_root_opcode_matrix_review.html").write_text(render_html(report), encoding="utf-8")
    print("selected root opcode matrix ok")


if __name__ == "__main__":
    main()
