#!/usr/bin/env python3
"""Summarize script-local sources for the gate offsets before the current frontier."""
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


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"
SOURCE = "map1_01a"
TARGET = "map2_02d"
FAILED_GATE_OFFSET_SOURCE_GATE_IDS = [
    "script-local-gate-byte-writer",
    "global-script-gate-byte-writer",
    "runtime-inherited-selection-buffer-state",
    "control-path-fallthrough-proof",
]
GATE_OFFSET_SOURCE_MISSING_EVIDENCE = [
    "opcode 0x12/0x13 writer to selectionBuffer[0xe8] or [0xea] before the current gate",
    "runtime sample showing inherited gate bytes on the current selector path",
    "control-flow proof that the gate fallthrough reaches the frontier reader",
    "strict hotspot or selected-root context tying the fallthrough to map1_01a -> map2_02d",
]
GATE_OFFSET_SOURCE_EVIDENCE_REFS = [
    {
        "path": "Hwanse2.exe",
        "description": "dword-aligned save-selector script scan for gate-offset readers/writers",
    },
    {
        "path": "out/save_scene_selectors.json",
        "description": "selector root ranges used to isolate the current 2:0 script root",
    },
    {
        "path": "out/save_selector_branch_gate_consistency.json",
        "description": "nearest current writer/frontier reader gate rows that define offsets 0xe8 and 0xea",
    },
]


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


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


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


def data_section(sections: list[dict]) -> dict:
    for section in sections:
        if section.get("name") == ".data":
            return section
    raise ValueError("Hwanse2.exe has no .data section")


def dword_at(exe: bytes, sections: list[dict], va: int) -> int | 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 operation_for_opcode(opcode: int) -> str:
    return {
        0x10: "fillBranchStateTable",
        0x11: "readSelectedStateAndBranch",
        0x12: "selectActiveStateSlot",
        0x13: "selectMatchingRuntimeSlot",
    }.get(opcode, "other")


def state_table(stream_plus_1: int) -> str:
    return "primaryBranchState" if stream_plus_1 == 0 else "secondaryBranchState"


def decode_row(va: int, value: int) -> dict | None:
    opcode = value & 0xFF
    if opcode not in {0x10, 0x11, 0x12, 0x13}:
        return None
    stream_plus_1 = (value >> 8) & 0xFF
    stream_plus_2 = (value >> 16) & 0xFF
    row = {
        "va": va,
        "vaHex": hex32(va),
        "value": value,
        "valueHex": hex32(value),
        "opcode": opcode,
        "opcodeHex": hex8(opcode),
        "operation": operation_for_opcode(opcode),
        "stateTable": state_table(stream_plus_1),
        "streamPlus1Hex": hex8(stream_plus_1),
        "selectionBufferOffset": stream_plus_2,
        "selectionBufferOffsetHex": hex8(stream_plus_2),
        "writesSelectionBuffer": opcode in {0x12, 0x13},
        "readsSelectionBuffer": opcode == 0x11,
    }
    if opcode == 0x10:
        row["validHelperDispatch"] = stream_plus_2 <= 0x0B
    return row


def selector_root_range(selectors: list[dict], group: int, slot: int) -> tuple[int, int, str]:
    selected = None
    label = f"{group}:{slot}"
    for row in selectors:
        if row.get("group") == group and row.get("slot") == slot:
            selected = row.get("selectedPointer")
            break
    if not isinstance(selected, int):
        raise ValueError(f"missing selector {label}")
    roots = sorted({row["selectedPointer"] for row in selectors if isinstance(row.get("selectedPointer"), int)})
    next_roots = [root for root in roots if root > selected]
    return selected, next_roots[0] if next_roots else selected + 0x4000, label


def scan_data_rows(exe: bytes, sections: list[dict], offsets: set[int]) -> list[dict]:
    section = data_section(sections)
    rows = []
    for file_offset in range(section["raw"], section["raw"] + section["raw_size"] - 3, 4):
        value = struct.unpack_from("<I", exe, file_offset)[0]
        va = section["va"] + file_offset - section["raw"]
        row = decode_row(va, value)
        if row and row["selectionBufferOffset"] in offsets:
            rows.append(row)
    return rows


def summarize_gate(offset: int, gate_va: int, rows: list[dict], root_start: int, root_end: int) -> dict:
    global_rows = [row for row in rows if row["selectionBufferOffset"] == offset]
    root_rows = [row for row in global_rows if root_start <= row["va"] < root_end]
    root_rows_before_gate = [row for row in root_rows if row["va"] < gate_va]
    global_writers = [row for row in global_rows if row["writesSelectionBuffer"]]
    root_writers = [row for row in root_rows if row["writesSelectionBuffer"]]
    root_writers_before_gate = [row for row in root_rows_before_gate if row["writesSelectionBuffer"]]
    return {
        "gateVaHex": hex32(gate_va),
        "selectionBufferOffset": offset,
        "selectionBufferOffsetHex": hex8(offset),
        "globalRowCount": len(global_rows),
        "globalReaderCount": sum(1 for row in global_rows if row["readsSelectionBuffer"]),
        "globalWriterCount": len(global_writers),
        "globalFillLikeCount": sum(1 for row in global_rows if row["opcode"] == 0x10),
        "currentRootRowCount": len(root_rows),
        "currentRootReaderCount": sum(1 for row in root_rows if row["readsSelectionBuffer"]),
        "currentRootWriterCount": len(root_writers),
        "currentRootRowsBeforeGateCount": len(root_rows_before_gate),
        "currentRootWriterBeforeGateCount": len(root_writers_before_gate),
        "currentRootRows": root_rows,
        "currentRootRowsBeforeGate": root_rows_before_gate,
        "globalSamples": global_rows[:12],
        "scriptLocalSelectionWriterFound": bool(root_writers_before_gate),
        "globalScriptSelectionWriterFound": bool(global_writers),
        "sourceClassification": (
            "script-local writer"
            if root_writers_before_gate
            else "global script writer outside current root"
            if global_writers
            else "no opcode 0x12/0x13 script writer; inherited/runtime selection-buffer byte"
        ),
    }


def gate_offsets_from_consistency(gate_consistency: dict) -> list[tuple[int, int]]:
    rows = []
    for row in gate_consistency.get("selectionOpcodeRowsBetween") or []:
        if row.get("opcodeHex") != "0x11":
            continue
        va = int(row["vaHex"], 16)
        offset = row.get("selectionBufferOffset")
        if not isinstance(offset, int):
            offset = int(row["selectionBufferOffsetHex"], 16)
        rows.append((offset, va))
    return rows


def build_summary(exe: bytes, selectors: list[dict], gate_consistency: dict) -> dict:
    sections = read_sections(exe)
    root_start, root_end, selector = selector_root_range(selectors, 2, 0)
    gates = gate_offsets_from_consistency(gate_consistency)
    offsets = {offset for offset, _va in gates}
    rows = scan_data_rows(exe, sections, offsets)
    gate_rows = [
        summarize_gate(offset, gate_va, rows, root_start, root_end)
        for offset, gate_va in gates
    ]
    any_script_local_writer = any(row["scriptLocalSelectionWriterFound"] for row in gate_rows)
    any_global_writer = any(row["globalScriptSelectionWriterFound"] for row in gate_rows)
    conclusion = (
        "The control gates before the current frontier reader use selectionBuffer offsets 0xe8 and 0xea, "
        "but the dword-aligned save-selector script scan finds no opcode 0x12/0x13 writer for either offset "
        "globally or inside the current 2:0 root. In the current root these offsets appear only as reader gates. "
        "That means the fallthrough path depends on inherited/runtime selection-buffer bytes rather than a "
        "script-local setup in this root, so control-flow proof remains open."
    )
    return {
        "source": SOURCE,
        "target": TARGET,
        "selector": selector,
        "rootHex": hex32(root_start),
        "rootRangeHex": f"{hex32(root_start)}..{hex32(root_end)}",
        "gateCount": len(gate_rows),
        "gateOffsetsHex": [hex8(offset) for offset, _va in gates],
        "anyScriptLocalSelectionWriter": any_script_local_writer,
        "anyGlobalScriptSelectionWriter": any_global_writer,
        "controlPathGateStatus": "inherited-runtime-state",
        "controlPathProofStatus": "blocked",
        "proofFound": False,
        "gateOffsetSourceProofFound": False,
        "failedGateOffsetSourceGateIds": FAILED_GATE_OFFSET_SOURCE_GATE_IDS,
        "missingEvidence": GATE_OFFSET_SOURCE_MISSING_EVIDENCE,
        "evidenceRefs": GATE_OFFSET_SOURCE_EVIDENCE_REFS,
        "evidenceRefCount": len(GATE_OFFSET_SOURCE_EVIDENCE_REFS),
        "promotionStatus": "blocked",
        "gates": gate_rows,
        "conclusion": conclusion,
    }


def markdown(summary: dict) -> str:
    lines = [
        "# Save Selector Gate Offset Sources",
        "",
        f"- route: {summary['source']} -> {summary['target']}",
        f"- selector: `{summary['selector']}` root `{summary['rootHex']}` (`{summary['rootRangeHex']}`)",
        f"- gate offsets: {', '.join(f'`{item}`' for item in summary['gateOffsetsHex'])}",
        f"- any script-local selection writer: {summary['anyScriptLocalSelectionWriter']}",
        f"- any global script selection writer: {summary['anyGlobalScriptSelectionWriter']}",
        f"- control path gate status: `{summary['controlPathGateStatus']}`",
        f"- control path proof status: `{summary['controlPathProofStatus']}`",
        f"- proofFound: `{summary['proofFound']}`",
        f"- gateOffsetSourceProofFound: `{summary['gateOffsetSourceProofFound']}`",
        f"- promotion status: `{summary['promotionStatus']}`",
        "",
        summary["conclusion"],
        "",
        "## Failed Gates",
        "",
    ]
    lines.extend(f"- `{item}`" for item in summary["failedGateOffsetSourceGateIds"])
    lines.extend([
        "",
        "## Missing Evidence",
        "",
    ])
    lines.extend(f"- {item}" for item in summary["missingEvidence"])
    lines.extend([
        "",
        "## Evidence Refs",
        "",
    ])
    lines.extend(
        f"- `{row['path']}`: {row['description']}"
        for row in summary["evidenceRefs"]
    )
    lines.extend([
        "",
        "## Gates",
        "",
        "| gate | offset | root rows | root writers before gate | global readers | global writers | source |",
        "| --- | --- | ---: | ---: | ---: | ---: | --- |",
    ])
    for gate in summary["gates"]:
        lines.append(
            f"| `{gate['gateVaHex']}` | `{gate['selectionBufferOffsetHex']}` | "
            f"{gate['currentRootRowCount']} | {gate['currentRootWriterBeforeGateCount']} | "
            f"{gate['globalReaderCount']} | {gate['globalWriterCount']} | "
            f"{gate['sourceClassification']} |"
        )
    lines.extend(["", "## Current Root Rows", ""])
    for gate in summary["gates"]:
        lines.extend([
            f"### Gate `{gate['gateVaHex']}` offset `{gate['selectionBufferOffsetHex']}`",
            "",
            "| va | value | op | operation | table | before gate? |",
            "| --- | --- | --- | --- | --- | --- |",
        ])
        for row in gate["currentRootRows"]:
            lines.append(
                f"| `{row['vaHex']}` | `{row['valueHex']}` | `{row['opcodeHex']}` | "
                f"{row['operation']} | {row['stateTable']} | {row['va'] < int(gate['gateVaHex'], 16)} |"
            )
        if not gate["currentRootRows"]:
            lines.append("| - | - | - | - | - | - |")
        lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    gate_rows = []
    detail_parts = []
    failed_gates = "".join(
        f"<li><code>{html.escape(item)}</code></li>"
        for item in summary["failedGateOffsetSourceGateIds"]
    )
    missing_evidence = "".join(
        f"<li>{html.escape(item)}</li>"
        for item in summary["missingEvidence"]
    )
    evidence_refs = "".join(
        f"<li><code>{html.escape(row['path'])}</code>: {html.escape(row['description'])}</li>"
        for row in summary["evidenceRefs"]
    )
    for gate in summary["gates"]:
        gate_rows.append(
            "<tr>"
            f"<td><code>{html.escape(gate['gateVaHex'])}</code></td>"
            f"<td><code>{html.escape(gate['selectionBufferOffsetHex'])}</code></td>"
            f"<td>{gate['currentRootRowCount']}</td>"
            f"<td>{gate['currentRootWriterBeforeGateCount']}</td>"
            f"<td>{gate['globalReaderCount']}</td>"
            f"<td>{gate['globalWriterCount']}</td>"
            f"<td>{html.escape(gate['sourceClassification'])}</td>"
            "</tr>"
        )
        rows = []
        gate_va = int(gate["gateVaHex"], 16)
        for row in gate["currentRootRows"]:
            rows.append(
                "<tr>"
                f"<td><code>{html.escape(row['vaHex'])}</code></td>"
                f"<td><code>{html.escape(row['valueHex'])}</code></td>"
                f"<td><code>{html.escape(row['opcodeHex'])}</code></td>"
                f"<td>{html.escape(row['operation'])}</td>"
                f"<td>{html.escape(row['stateTable'])}</td>"
                f"<td>{row['va'] < gate_va}</td>"
                "</tr>"
            )
        detail_parts.extend([
            f"<h2>Gate <code>{html.escape(gate['gateVaHex'])}</code> Offset <code>{html.escape(gate['selectionBufferOffsetHex'])}</code></h2>",
            "<table><thead><tr><th>va</th><th>value</th><th>op</th><th>operation</th><th>table</th><th>before gate?</th></tr></thead><tbody>",
            *rows,
            "</tbody></table>",
        ])
    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 Gate Offset Sources</title>",
        "  <style>",
        "    :root { color-scheme: dark; font-family: system-ui, sans-serif; background: #101010; color: #eee; }",
        "    body { margin: 0; padding: 24px; }",
        "    h1 { margin: 0 0 8px; font-size: 24px; }",
        "    h2 { margin: 24px 0 8px; font-size: 18px; }",
        "    p { max-width: 1120px; color: #bbb; line-height: 1.45; }",
        "    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 { position: sticky; top: 0; background: #181818; color: #ddd; }",
        "    code { color: #d7f0ff; }",
        "  </style>",
        "</head>",
        "<body>",
        "  <h1>Save Selector Gate Offset Sources</h1>",
        f"  <p>route <code>{html.escape(summary['source'])}</code> -&gt; <code>{html.escape(summary['target'])}</code>; selector <code>{html.escape(summary['selector'])}</code>; root <code>{html.escape(summary['rootRangeHex'])}</code>.</p>",
        f"  <p>gate offsets: {html.escape(', '.join(summary['gateOffsetsHex']))}; any script-local selection writer: {summary['anyScriptLocalSelectionWriter']}; any global script selection writer: {summary['anyGlobalScriptSelectionWriter']}; control path gate status: <code>{html.escape(summary['controlPathGateStatus'])}</code>; proofFound <code>{summary['proofFound']}</code>; promotion status: <code>{html.escape(summary['promotionStatus'])}</code>.</p>",
        f"  <p>{html.escape(summary['conclusion'])}</p>",
        "  <h2>Failed Gates</h2>",
        f"  <ul>{failed_gates}</ul>",
        "  <h2>Missing Evidence</h2>",
        f"  <ul>{missing_evidence}</ul>",
        "  <h2>Evidence Refs</h2>",
        f"  <ul>{evidence_refs}</ul>",
        "  <h2>Gates</h2>",
        "  <table><thead><tr><th>gate</th><th>offset</th><th>root rows</th><th>root writers before gate</th><th>global readers</th><th>global writers</th><th>source</th></tr></thead><tbody>",
        *gate_rows,
        "  </tbody></table>",
        *detail_parts,
        "</body>",
        "</html>",
        "",
    ])


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


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    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("--gate-consistency", type=Path, default=OUT / "save_selector_branch_gate_consistency.json")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    args = parser.parse_args()
    summary = build_summary(
        args.exe.read_bytes(),
        load_json(args.selectors, []),
        load_json(args.gate_consistency, {}),
    )
    write_outputs(summary, args.out_dir)
    print(f"wrote save selector gate offset sources -> {args.out_dir / 'save_selector_gate_offset_sources.html'}")


if __name__ == "__main__":
    main()
