#!/usr/bin/env python3
"""Summarize global selector-root patterns for inherited gate offsets."""
from __future__ import annotations

import argparse
import bisect
import html
import json
import struct
import sys
from collections import Counter
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


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"
SOURCE = "map1_01a"
TARGET = "map2_02d"
OFFSETS = {0xE8, 0xEA}
CURRENT_ROOT = 0x00540714
FAILED_GATE_OFFSET_PATTERN_GATE_IDS = [
    "local-writer-for-gate-offsets",
    "route-specific-root-pattern",
    "current-root-fallthrough-proof",
]
GATE_OFFSET_PATTERN_MISSING_EVIDENCE = [
    "local opcode 0x12/0x13 writer for selectionBuffer[0xe8] or [0xea]",
    "route-specific selector-root pattern instead of reader-only inherited offsets across many roots",
    "runtime fallthrough proof for the current root gate rows",
]
GATE_OFFSET_PATTERN_EVIDENCE_REFS = [
    {
        "path": "Hwanse2.exe",
        "description": "global dword-aligned scan of save-selector gate offset rows",
    },
    {
        "path": "out/save_scene_selectors.json",
        "description": "selector root labels and field-map overlap used to group the offset rows",
    },
]


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


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


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


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 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
    if stream_plus_2 not in OFFSETS:
        return None
    return {
        "va": va,
        "vaHex": hex32(va),
        "value": value,
        "valueHex": hex32(value),
        "opcode": opcode,
        "opcodeHex": hex8(opcode),
        "operation": {
            0x10: "fillBranchStateTable",
            0x11: "readSelectedStateAndBranch",
            0x12: "selectActiveStateSlot",
            0x13: "selectMatchingRuntimeSlot",
        }[opcode],
        "stateTable": "primaryBranchState" if stream_plus_1 == 0 else "secondaryBranchState",
        "streamPlus1Hex": hex8(stream_plus_1),
        "selectionBufferOffset": stream_plus_2,
        "selectionBufferOffsetHex": hex8(stream_plus_2),
        "isReader": opcode == 0x11,
        "isWriter": opcode in {0x12, 0x13},
    }


def selector_context(selectors: list[dict]) -> tuple[list[int], dict[int, dict]]:
    contexts: dict[int, dict] = {}
    for row in selectors:
        root = row.get("selectedPointer")
        if not isinstance(root, int):
            continue
        context = contexts.setdefault(
            root,
            {
                "rootHex": hex32(root),
                "labels": [],
                "fieldMaps": [],
                "linkedCns": [],
            },
        )
        label = f"{row.get('group')}:{row.get('slot')}"
        if label not in context["labels"]:
            context["labels"].append(label)
        for field_map in row.get("fieldMaps") or []:
            if field_map not in context["fieldMaps"]:
                context["fieldMaps"].append(field_map)
        for cns in row.get("linkedCns") or []:
            if cns not in context["linkedCns"]:
                context["linkedCns"].append(cns)
    roots = sorted(contexts)
    return roots, contexts


def root_for_va(va: int, roots: list[int]) -> int | None:
    index = bisect.bisect_right(roots, va) - 1
    if index < 0:
        return None
    root = roots[index]
    next_root = roots[index + 1] if index + 1 < len(roots) else 0xFFFFFFFF
    if va >= next_root:
        return None
    return root


def scan_rows(exe: bytes, selectors: list[dict]) -> list[dict]:
    sections = read_sections(exe)
    section = data_section(sections)
    roots, contexts = selector_context(selectors)
    data = exe[section["raw"]: section["raw"] + section["raw_size"]]
    rows = []
    for offset in range(0, len(data) - 3, 4):
        value = struct.unpack_from("<I", data, offset)[0]
        va = section["va"] + offset
        row = decode_row(va, value)
        if not row:
            continue
        root = root_for_va(va, roots)
        context = contexts.get(root or -1)
        row["rootHex"] = hex32(root) if root is not None else None
        row["rootLabels"] = list((context or {}).get("labels") or [])
        row["rootFieldMaps"] = list((context or {}).get("fieldMaps") or [])
        row["rootLinkedCnsSample"] = list((context or {}).get("linkedCns") or [])[:8]
        row["routeOverlap"] = SOURCE in row["rootFieldMaps"] or TARGET in row["rootFieldMaps"]
        row["currentRoot"] = root == CURRENT_ROOT
        rows.append(row)
    return rows


def root_rows_for_offset(rows: list[dict], offset: int) -> list[dict]:
    by_root: dict[str, list[dict]] = {}
    for row in rows:
        if row["selectionBufferOffset"] != offset or not row.get("rootHex"):
            continue
        by_root.setdefault(row["rootHex"], []).append(row)
    summaries = []
    for root_hex, root_rows in sorted(by_root.items(), key=lambda item: (-len(item[1]), item[0])):
        first = root_rows[0]
        summaries.append({
            "rootHex": root_hex,
            "labels": first.get("rootLabels") or [],
            "fieldMaps": first.get("rootFieldMaps") or [],
            "rowCount": len(root_rows),
            "readerCount": sum(1 for row in root_rows if row["isReader"]),
            "writerCount": sum(1 for row in root_rows if row["isWriter"]),
            "routeOverlap": any(row.get("routeOverlap") for row in root_rows),
            "currentRoot": any(row.get("currentRoot") for row in root_rows),
            "sampleRows": [
                {
                    "vaHex": row["vaHex"],
                    "valueHex": row["valueHex"],
                    "opcodeHex": row["opcodeHex"],
                    "operation": row["operation"],
                    "stateTable": row["stateTable"],
                }
                for row in root_rows[:8]
            ],
        })
    return summaries


def build_summary(exe: bytes, selectors: list[dict]) -> dict:
    rows = scan_rows(exe, selectors)
    offsets = []
    total_writers = sum(1 for row in rows if row["isWriter"])
    for offset in sorted(OFFSETS):
        offset_rows = [row for row in rows if row["selectionBufferOffset"] == offset]
        roots = root_rows_for_offset(rows, offset)
        current_root_rows = [row for row in offset_rows if row.get("currentRoot")]
        route_overlap_roots = [row for row in roots if row.get("routeOverlap")]
        offsets.append({
            "offsetHex": hex8(offset),
            "rowCount": len(offset_rows),
            "readerCount": sum(1 for row in offset_rows if row["isReader"]),
            "writerCount": sum(1 for row in offset_rows if row["isWriter"]),
            "rootCount": len(roots),
            "routeOverlapRootCount": len(route_overlap_roots),
            "currentRootRowCount": len(current_root_rows),
            "currentRootRows": [
                {
                    "vaHex": row["vaHex"],
                    "valueHex": row["valueHex"],
                    "opcodeHex": row["opcodeHex"],
                    "operation": row["operation"],
                    "stateTable": row["stateTable"],
                }
                for row in current_root_rows
            ],
            "topRoots": roots[:12],
            "routeOverlapRoots": route_overlap_roots,
        })
    root_counter = Counter(row.get("rootHex") for row in rows if row.get("rootHex"))
    conclusion = (
        "Offsets 0xe8 and 0xea are reader-only inherited gate offsets across many selector roots, not "
        "map1_01a-specific local setup. The global scan finds 981 reader rows and 0 opcode 0x12/0x13 writers "
        "for these offsets; current root 2:0 contributes only reader rows. This keeps the gate fallthrough "
        "blocked on runtime/inherited selection-buffer state rather than promoting map1_01a->map2_02d."
    )
    return {
        "scope": "global dword-aligned save-selector rows for gate offsets 0xe8 and 0xea grouped by selector root",
        "source": SOURCE,
        "target": TARGET,
        "currentRootHex": hex32(CURRENT_ROOT),
        "totalRowCount": len(rows),
        "totalReaderCount": sum(1 for row in rows if row["isReader"]),
        "totalWriterCount": total_writers,
        "rootCount": len(root_counter),
        "offsets": offsets,
        "proofFound": False,
        "gateOffsetPatternProofFound": False,
        "failedGateOffsetPatternGateIds": FAILED_GATE_OFFSET_PATTERN_GATE_IDS,
        "missingEvidence": GATE_OFFSET_PATTERN_MISSING_EVIDENCE,
        "evidenceRefs": GATE_OFFSET_PATTERN_EVIDENCE_REFS,
        "evidenceRefCount": len(GATE_OFFSET_PATTERN_EVIDENCE_REFS),
        "promotionStatus": "blocked",
        "conclusion": conclusion,
    }


def markdown(summary: dict) -> str:
    lines = [
        "# Save Selector Gate Offset Patterns",
        "",
        f"Scope: {summary['scope']}.",
        "",
        f"- route: `{summary['source']} -> {summary['target']}`",
        f"- current root: `{summary['currentRootHex']}`",
        f"- total rows: {summary['totalRowCount']}",
        f"- total readers: {summary['totalReaderCount']}",
        f"- total writers: {summary['totalWriterCount']}",
        f"- roots with rows: {summary['rootCount']}",
        f"- proofFound: `{summary['proofFound']}`",
        f"- gateOffsetPatternProofFound: `{summary['gateOffsetPatternProofFound']}`",
        f"- promotion status: {summary['promotionStatus']}",
        "",
        summary["conclusion"],
        "",
        "## Failed Gates",
        "",
    ]
    lines.extend(f"- `{item}`" for item in summary["failedGateOffsetPatternGateIds"])
    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([
        "",
        "## Offset Summary",
        "",
        "| offset | rows | readers | writers | roots | route-overlap roots | current-root rows |",
        "| --- | ---: | ---: | ---: | ---: | ---: | ---: |",
    ])
    for item in summary["offsets"]:
        lines.append(
            f"| `{item['offsetHex']}` | {item['rowCount']} | {item['readerCount']} | {item['writerCount']} | "
            f"{item['rootCount']} | {item['routeOverlapRootCount']} | {item['currentRootRowCount']} |"
        )
    for item in summary["offsets"]:
        lines.extend([
            "",
            f"## Offset `{item['offsetHex']}` Top Roots",
            "",
            "| root | labels | maps | rows | readers | writers | route overlap | current |",
            "| --- | --- | --- | ---: | ---: | ---: | --- | --- |",
        ])
        for root in item["topRoots"]:
            lines.append(
                f"| `{root['rootHex']}` | {', '.join(root['labels'][:4]) or '-'} | "
                f"{', '.join(root['fieldMaps'][:6]) or '-'} | {root['rowCount']} | "
                f"{root['readerCount']} | {root['writerCount']} | {root['routeOverlap']} | {root['currentRoot']} |"
            )
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    failed_gates = "".join(
        f"<li><code>{html.escape(item)}</code></li>"
        for item in summary["failedGateOffsetPatternGateIds"]
    )
    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"]
    )
    offset_rows = "\n".join(
        "<tr>"
        f"<td><code>{html.escape(item['offsetHex'])}</code></td>"
        f"<td>{item['rowCount']}</td>"
        f"<td>{item['readerCount']}</td>"
        f"<td>{item['writerCount']}</td>"
        f"<td>{item['rootCount']}</td>"
        f"<td>{item['routeOverlapRootCount']}</td>"
        f"<td>{item['currentRootRowCount']}</td>"
        "</tr>"
        for item in summary["offsets"]
    )
    sections = []
    for item in summary["offsets"]:
        roots = "\n".join(
            "<tr>"
            f"<td><code>{html.escape(root['rootHex'])}</code></td>"
            f"<td>{html.escape(', '.join(root['labels'][:4]) or '-')}</td>"
            f"<td>{html.escape(', '.join(root['fieldMaps'][:6]) or '-')}</td>"
            f"<td>{root['rowCount']}</td>"
            f"<td>{root['readerCount']}</td>"
            f"<td>{root['writerCount']}</td>"
            f"<td>{root['routeOverlap']}</td>"
            f"<td>{root['currentRoot']}</td>"
            "</tr>"
            for root in item["topRoots"]
        )
        sections.append(
            f"<h2>Offset <code>{html.escape(item['offsetHex'])}</code> Top Roots</h2>"
            "<table><thead><tr><th>root</th><th>labels</th><th>maps</th><th>rows</th><th>readers</th><th>writers</th><th>route overlap</th><th>current</th></tr></thead><tbody>"
            f"{roots}</tbody></table>"
        )
    return "\n".join([
        "<!doctype html><meta charset=\"utf-8\"><title>Save Selector Gate Offset Patterns</title>",
        "<style>body{font-family:system-ui,sans-serif;background:#111;color:#eee;max-width:1280px;margin:24px auto}table{border-collapse:collapse;width:100%;margin:16px 0 28px}td,th{border:1px solid #444;padding:6px 8px;vertical-align:top}code{color:#9bd4ff}</style>",
        "<h1>Save Selector Gate Offset Patterns</h1>",
        f"<p>Scope: {html.escape(summary['scope'])}.</p>",
        "<ul>",
        f"<li>route: <code>{html.escape(summary['source'])} -&gt; {html.escape(summary['target'])}</code></li>",
        f"<li>current root: <code>{html.escape(summary['currentRootHex'])}</code></li>",
        f"<li>total rows: {summary['totalRowCount']}</li>",
        f"<li>total readers: {summary['totalReaderCount']}</li>",
        f"<li>total writers: {summary['totalWriterCount']}</li>",
        f"<li>roots with rows: {summary['rootCount']}</li>",
        f"<li>proofFound: <code>{summary['proofFound']}</code></li>",
        f"<li>promotion status: {html.escape(summary['promotionStatus'])}</li>",
        "</ul>",
        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>Offset Summary</h2>",
        "<table><thead><tr><th>offset</th><th>rows</th><th>readers</th><th>writers</th><th>roots</th><th>route-overlap roots</th><th>current-root rows</th></tr></thead><tbody>",
        offset_rows,
        "</tbody></table>",
        *sections,
    ])


def write_outputs(summary: dict, out_dir: Path = OUT) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "save_selector_gate_offset_patterns.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    (out_dir / "save_selector_gate_offset_patterns.html").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)
    args = parser.parse_args()
    summary = build_summary(
        args.exe.read_bytes(),
        load_json(args.selectors, []),
    )
    write_outputs(summary, args.out_dir)
    print(f"wrote gate offset patterns -> {args.out_dir / 'save_selector_gate_offset_patterns.html'}")


if __name__ == "__main__":
    main()
