#!/usr/bin/env python3
"""Summarize selected-pointer opcode 0x07/0x08/0x09 paths in route selector roots."""
from __future__ import annotations

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

sys.path.insert(0, str(Path(__file__).resolve().parent))

from probe_exe_scene_tables import read_sections, va_to_offset
from summarize_script_handler_table import handler_for_opcode, section_name_for_va


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"

SOURCE = "map1_01a"
TARGET = "map2_02d"
SOURCE_SELECTOR = "0:0"
PREDECESSOR_SELECTOR = "1:0"
CURRENT_SELECTOR = "2:0"

OPCODE_HANDLERS = {
    0x07: 0x0040AD9B,
    0x08: 0x0040ADC9,
    0x09: 0x0040AE0E,
}

CONTEXT_RANGES = {
    SOURCE_SELECTOR: {
        "role": "source-side confirmed selector",
        "root": 0x00501808,
        "start": 0x00501808,
        "end": 0x00503570,
    },
    PREDECESSOR_SELECTOR: {
        "role": "target-side predecessor selector",
        "root": 0x00478364,
        "start": 0x00478364,
        "end": 0x0048458C,
    },
    CURRENT_SELECTOR: {
        "role": "current merge/frontier selector",
        "root": 0x00540714,
        "start": 0x00540714,
        "end": 0x00543578,
    },
}


def hex32(value: int) -> str:
    return f"0x{value:08x}"


def range_hex(start: int, end: int) -> str:
    return f"{hex32(start)}..{hex32(end)}"


def load_json(path: Path, fallback: Any) -> Any:
    if not path.exists():
        return fallback
    return json.loads(path.read_text(encoding="utf-8"))


def dword_at(exe: bytes, sections: list[dict], va: int | None) -> int | None:
    if va is None:
        return None
    offset = va_to_offset(sections, va)
    if offset is None or offset + 4 > len(exe):
        return None
    return struct.unpack_from("<I", exe, offset)[0]


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


def context_for_value(value: int | None) -> str | None:
    if value is None:
        return None
    for label, row in CONTEXT_RANGES.items():
        if row["start"] <= value < row["end"]:
            return label
    return None


def exact_root_for_value(value: int | None) -> str | None:
    if value is None:
        return None
    for label, row in CONTEXT_RANGES.items():
        if value == row["root"]:
            return label
    return None


def classify_value(sections: list[dict], value: int | None) -> dict:
    if value is None:
        return {"kind": "unreadable"}
    context = context_for_value(value)
    root = exact_root_for_value(value)
    if root:
        return {"kind": "selector-root", "selector": root}
    if context:
        return {"kind": "selector-range", "selector": context}
    section = section_name_for_va(sections, value)
    if section:
        return {"kind": "pointer", "section": section}
    if (value >> 16) <= 0x400 and (value & 0xFFFF) <= 0x10000:
        return {"kind": "script-scalar-or-pair"}
    return {"kind": "scalar"}


def context_summary(selectors: list[dict], label: str) -> dict:
    row = selector_row(selectors, label)
    context = CONTEXT_RANGES[label]
    return {
        "selector": label,
        "role": context["role"],
        "rootHex": hex32(context["root"]),
        "rangeHex": range_hex(context["start"], context["end"]),
        "fieldMaps": row.get("fieldMaps") or [],
    }


def decode_opcode07(exe: bytes, sections: list[dict], va: int, value: int) -> dict:
    index = (value >> 8) & 0xFF
    table = dword_at(exe, sections, va + 4)
    selected_slot = table + index * 4 if table is not None else None
    selected_value = dword_at(exe, sections, selected_slot)
    selected_class = classify_value(sections, selected_value)
    return {
        "index": index,
        "operandTableHex": hex32(table) if table is not None else None,
        "operandTableClass": classify_value(sections, table),
        "selectedSlotHex": hex32(selected_slot) if selected_slot is not None else None,
        "selectedValueHex": hex32(selected_value) if selected_value is not None else None,
        "selectedValueClass": selected_class,
        "selectedCurrentRoot": selected_value == CONTEXT_RANGES[CURRENT_SELECTOR]["root"],
        "selectedCurrentRange": context_for_value(selected_value) == CURRENT_SELECTOR,
    }


def decode_opcode09(exe: bytes, sections: list[dict], va: int, value: int) -> dict:
    mode = (value >> 8) & 0xFF
    if mode == 0:
        stored = va + 4
        stored_source = "next-stream"
    elif mode == 1:
        stored = dword_at(exe, sections, va + 4)
        stored_source = "stream+4-pointer"
    else:
        stored = None
        stored_source = "unsupported-mode"
    return {
        "mode": mode,
        "modeHex": f"0x{mode:02x}",
        "storedPointerSource": stored_source,
        "storedPointerHex": hex32(stored) if stored is not None else None,
        "storedPointerClass": classify_value(sections, stored),
        "storesCurrentRoot": stored == CONTEXT_RANGES[CURRENT_SELECTOR]["root"],
        "storesCurrentRange": context_for_value(stored) == CURRENT_SELECTOR,
    }


def scan_context(exe: bytes, sections: list[dict], label: str) -> dict:
    context = CONTEXT_RANGES[label]
    rows = []
    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 OPCODE_HANDLERS:
            continue
        handler = handler_for_opcode(exe, sections, opcode)
        row = {
            "selector": label,
            "vaHex": hex32(va),
            "valueHex": hex32(value),
            "opcodeHex": f"0x{opcode:02x}",
            "handlerVaHex": handler.get("handlerVaHex"),
            "validHandler": handler.get("handlerVa") == OPCODE_HANDLERS[opcode],
            "routePromotionCandidate": False,
        }
        if opcode == 0x07:
            row.update(decode_opcode07(exe, sections, va, value))
            row["routePromotionCandidate"] = label != CURRENT_SELECTOR and row["selectedCurrentRange"]
        elif opcode == 0x08:
            row.update({
                "meaning": "activates 0x0059de30 if it is nonzero; static target depends on runtime global",
                "requiresRuntimeSelectedPointer": True,
            })
        elif opcode == 0x09:
            row.update(decode_opcode09(exe, sections, va, value))
            row["routePromotionCandidate"] = label != CURRENT_SELECTOR and row["storesCurrentRange"]
        rows.append(row)
    opcode07_rows = [row for row in rows if row["opcodeHex"] == "0x07"]
    opcode08_rows = [row for row in rows if row["opcodeHex"] == "0x08"]
    opcode09_rows = [row for row in rows if row["opcodeHex"] == "0x09"]
    opcode09_mode0_rows = [row for row in opcode09_rows if row.get("mode") == 0]
    opcode09_mode1_rows = [row for row in opcode09_rows if row.get("mode") == 1]
    return {
        "selector": label,
        "role": context["role"],
        "rangeHex": range_hex(context["start"], context["end"]),
        "opcode07Count": len(opcode07_rows),
        "opcode08Count": len(opcode08_rows),
        "opcode09Count": len(opcode09_rows),
        "opcode09Mode0Count": len(opcode09_mode0_rows),
        "opcode09Mode1Count": len(opcode09_mode1_rows),
        "opcode07SelectsCurrentRootCount": sum(1 for row in opcode07_rows if row.get("selectedCurrentRoot")),
        "opcode07SelectsCurrentRangeCount": sum(1 for row in opcode07_rows if row.get("selectedCurrentRange")),
        "opcode09StoresCurrentRootCount": sum(1 for row in opcode09_rows if row.get("storesCurrentRoot")),
        "opcode09StoresCurrentRangeCount": sum(1 for row in opcode09_rows if row.get("storesCurrentRange")),
        "opcode09StoresOwnRangeCount": sum(1 for row in opcode09_rows if context_for_value(
            int(row["storedPointerHex"], 16) if row.get("storedPointerHex") else None
        ) == label),
        "routePromotionCandidateCount": sum(1 for row in rows if row.get("routePromotionCandidate")),
        "rows": rows,
    }


def build_summary(exe: bytes, selectors: list[dict] | None = None) -> dict:
    sections = read_sections(exe)
    selectors = selectors if selectors is not None else load_json(OUT / "save_scene_selectors.json", [])
    labels = [SOURCE_SELECTOR, PREDECESSOR_SELECTOR, CURRENT_SELECTOR]
    contexts = [context_summary(selectors, label) for label in labels]
    scans = [scan_context(exe, sections, label) for label in labels]
    non_current_scans = [row for row in scans if row["selector"] != CURRENT_SELECTOR]
    source_or_predecessor_current_target_count = sum(
        row["opcode07SelectsCurrentRangeCount"] + row["opcode09StoresCurrentRangeCount"]
        for row in non_current_scans
    )
    source_or_predecessor_current_root_count = sum(
        row["opcode07SelectsCurrentRootCount"] + row["opcode09StoresCurrentRootCount"]
        for row in non_current_scans
    )
    source_or_predecessor_opcode08_count = sum(row["opcode08Count"] for row in non_current_scans)
    current_scan = next(row for row in scans if row["selector"] == CURRENT_SELECTOR)
    conclusion = (
        "The source-side 0:0 and target-side 1:0 roots contain opcode 0x08 activators, but their local opcode "
        "0x07/0x09 selected-pointer writers never select or store the current 2:0 root/range. All opcode 0x09 rows "
        "in those non-current roots are mode 0 next-stream stores back into their own root windows. Current 2:0 has "
        "its own mode 0 continuation stores, but those only preserve internal current-root streams. Therefore the "
        "static selected-pointer opcode paths do not prove a 0:0/1:0 -> 2:0 runtime jump; promotion still requires "
        "a selector 2:0 save, selected-pointer runtime trace, or strict map1_01a hotspot."
    )
    return {
        "source": SOURCE,
        "target": TARGET,
        "sourceSelector": SOURCE_SELECTOR,
        "predecessorSelector": PREDECESSOR_SELECTOR,
        "currentSelector": CURRENT_SELECTOR,
        "contexts": contexts,
        "contextScans": scans,
        "sourceOrPredecessorOpcode08ActivatorCount": source_or_predecessor_opcode08_count,
        "sourceOrPredecessorCurrentRootWriterCount": source_or_predecessor_current_root_count,
        "sourceOrPredecessorCurrentRangeWriterCount": source_or_predecessor_current_target_count,
        "currentInternalOpcode09StoreCount": current_scan["opcode09StoresCurrentRangeCount"],
        "selectedPointerOpcodePathPromotesRoute": False,
        "promotionStatus": "blocked",
        "remainingProofs": [
            "capture a selector 2:0 gameplay savedat or selected-pointer runtime trace",
            "prove opcode 0x08 activates 0x0059de30 == 0x00540714 on the route path",
            "find a strict map1_01a source coordinate or hotspot",
        ],
        "conclusion": conclusion,
    }


def scan_brief(row: dict) -> str:
    return (
        f"op7={row['opcode07Count']} op8={row['opcode08Count']} op9={row['opcode09Count']} "
        f"mode0={row['opcode09Mode0Count']} mode1={row['opcode09Mode1Count']} "
        f"selectCurrent={row['opcode07SelectsCurrentRangeCount']} "
        f"storeCurrent={row['opcode09StoresCurrentRangeCount']} "
        f"promoteCandidates={row['routePromotionCandidateCount']}"
    )


def interesting_rows(scan: dict) -> list[dict]:
    rows = []
    for row in scan["rows"]:
        if row.get("routePromotionCandidate"):
            rows.append(row)
        elif row.get("opcodeHex") == "0x09":
            rows.append(row)
        elif row.get("opcodeHex") == "0x08" and len(rows) < 10:
            rows.append(row)
    return rows[:40]


def markdown(summary: dict) -> str:
    lines = [
        "# Save Selector Selected-Pointer Opcode Paths",
        "",
        f"- route: {summary['source']} -> {summary['target']}",
        f"- selectors: source `{summary['sourceSelector']}`, predecessor `{summary['predecessorSelector']}`, current `{summary['currentSelector']}`",
        f"- source/predecessor opcode 0x08 activators: {summary['sourceOrPredecessorOpcode08ActivatorCount']}",
        f"- source/predecessor current-root writers: {summary['sourceOrPredecessorCurrentRootWriterCount']}",
        f"- source/predecessor current-range writers: {summary['sourceOrPredecessorCurrentRangeWriterCount']}",
        f"- current internal opcode 0x09 stores: {summary['currentInternalOpcode09StoreCount']}",
        f"- selected-pointer opcode path promotes route: {summary['selectedPointerOpcodePathPromotesRoute']}",
        f"- promotion status: `{summary['promotionStatus']}`",
        "",
        summary["conclusion"],
        "",
        "## Context Counts",
        "",
        "| selector | role | range | counts |",
        "| --- | --- | --- | --- |",
    ]
    for scan in summary["contextScans"]:
        lines.append(
            f"| `{scan['selector']}` | {scan['role']} | `{scan['rangeHex']}` | {scan_brief(scan)} |"
        )
    lines.extend([
        "",
        "## Route-Relevant Rows",
        "",
        "| selector | va | opcode | value | selected/stored pointer | kind | promotion candidate |",
        "| --- | --- | --- | --- | --- | --- | --- |",
    ])
    any_rows = False
    for scan in summary["contextScans"]:
        for row in interesting_rows(scan):
            any_rows = True
            pointer_hex = row.get("selectedValueHex") or row.get("storedPointerHex") or "-"
            pointer_class = row.get("selectedValueClass") or row.get("storedPointerClass") or {}
            kind = pointer_class.get("kind") or row.get("meaning") or "-"
            selector = pointer_class.get("selector")
            if selector:
                kind = f"{kind} {selector}"
            lines.append(
                f"| `{scan['selector']}` | `{row['vaHex']}` | `{row['opcodeHex']}` | `{row['valueHex']}` | "
                f"`{pointer_hex}` | {kind} | {row.get('routePromotionCandidate')} |"
            )
    if not any_rows:
        lines.append("| - | - | - | - | - | - | - |")
    lines.extend(["", "## Remaining Proofs", ""])
    lines.extend(f"- {item}" for item in summary["remainingProofs"])
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    count_rows = "\n".join(
        "<tr>"
        f"<td><code>{html.escape(scan['selector'])}</code></td>"
        f"<td>{html.escape(scan['role'])}</td>"
        f"<td><code>{html.escape(scan['rangeHex'])}</code></td>"
        f"<td>{html.escape(scan_brief(scan))}</td>"
        "</tr>"
        for scan in summary["contextScans"]
    )
    route_rows = []
    for scan in summary["contextScans"]:
        for row in interesting_rows(scan):
            pointer_hex = row.get("selectedValueHex") or row.get("storedPointerHex") or "-"
            pointer_class = row.get("selectedValueClass") or row.get("storedPointerClass") or {}
            kind = pointer_class.get("kind") or row.get("meaning") or "-"
            selector = pointer_class.get("selector")
            if selector:
                kind = f"{kind} {selector}"
            route_rows.append(
                "<tr>"
                f"<td><code>{html.escape(scan['selector'])}</code></td>"
                f"<td><code>{html.escape(row['vaHex'])}</code></td>"
                f"<td><code>{html.escape(row['opcodeHex'])}</code></td>"
                f"<td><code>{html.escape(row['valueHex'])}</code></td>"
                f"<td><code>{html.escape(pointer_hex)}</code></td>"
                f"<td>{html.escape(kind)}</td>"
                f"<td>{row.get('routePromotionCandidate')}</td>"
                "</tr>"
            )
    proofs = "".join(f"<li>{html.escape(item)}</li>" for item in summary["remainingProofs"])
    return "\n".join([
        "<!doctype html>",
        '<html lang="en">',
        "<head>",
        '  <meta charset="utf-8">',
        '  <meta name="viewport" content="width=device-width, initial-scale=1">',
        "  <title>Save Selector Selected-Pointer Opcode Paths</title>",
        "  <style>",
        "    :root { color-scheme: dark; font-family: system-ui, sans-serif; background: #101010; color: #eee; }",
        "    body { margin: 0; padding: 24px; }",
        "    table { width: 100%; border-collapse: collapse; margin: 12px 0 20px; font-size: 13px; }",
        "    th, td { border-bottom: 1px solid #303030; padding: 7px 8px; text-align: left; vertical-align: top; }",
        "    th { background: #181818; color: #ddd; }",
        "    code { color: #d7f0ff; }",
        "  </style>",
        "</head>",
        "<body>",
        "  <h1>Save Selector Selected-Pointer Opcode Paths</h1>",
        f"  <p>route {html.escape(summary['source'])} -&gt; {html.escape(summary['target'])}; source/predecessor current-range writers {summary['sourceOrPredecessorCurrentRangeWriterCount']}; selected-pointer opcode path promotes route {summary['selectedPointerOpcodePathPromotesRoute']}; promotion status <code>{html.escape(summary['promotionStatus'])}</code>.</p>",
        f"  <p>source/predecessor opcode 0x08 activators {summary['sourceOrPredecessorOpcode08ActivatorCount']}; source/predecessor current-root writers {summary['sourceOrPredecessorCurrentRootWriterCount']}; source/predecessor current-range writers {summary['sourceOrPredecessorCurrentRangeWriterCount']}; current internal opcode 0x09 stores {summary['currentInternalOpcode09StoreCount']}.</p>",
        f"  <p>{html.escape(summary['conclusion'])}</p>",
        "  <h2>Context Counts</h2>",
        "  <table><thead><tr><th>selector</th><th>role</th><th>range</th><th>counts</th></tr></thead><tbody>",
        count_rows,
        "  </tbody></table>",
        "  <h2>Route-Relevant Rows</h2>",
        "  <table><thead><tr><th>selector</th><th>va</th><th>opcode</th><th>value</th><th>selected/stored pointer</th><th>kind</th><th>promotion candidate</th></tr></thead><tbody>",
        "".join(route_rows),
        "  </tbody></table>",
        f"  <h2>Remaining Proofs</h2><ul>{proofs}</ul>",
        "</body></html>",
    ])


def write_outputs(summary: dict, out_dir: Path, html_out: Path | None = None) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "save_selector_selected_pointer_opcode_paths.json").write_text(
        json.dumps(summary, ensure_ascii=False, separators=(",", ":")) + "\n",
        encoding="utf-8",
    )
    if html_out is not None:
        html_out.parent.mkdir(parents=True, exist_ok=True)
        html_out.write_text(html_page(summary), encoding="utf-8")


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--exe", type=Path, default=ROOT / "Hwanse2.exe")
    parser.add_argument("--selectors", type=Path, default=OUT / "save_scene_selectors.json")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    parser.add_argument("--html-out", type=Path, default=None, help="Optional HTML report output path.")
    args = parser.parse_args()
    summary = build_summary(
        args.exe.read_bytes(),
        load_json(args.selectors, []),
    )
    write_outputs(summary, args.out_dir, args.html_out)
    print(f"wrote save selector selected-pointer opcode paths -> {args.out_dir / 'save_selector_selected_pointer_opcode_paths.json'}")


if __name__ == "__main__":
    main()
