#!/usr/bin/env python3
"""Compare save-selector leaf-table windows across all selector rows."""
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 parse_savedata import collect_linked_cns
from probe_exe_scene_tables import find_cns_strings, read_sections, va_to_offset


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

SOURCE = "map1_01a"
TARGET = "map2_02d"
CURRENT_SELECTOR = "2:0"
CURRENT_ROOT = 0x00540714
CURRENT_TABLE_POINTER = 0x005429DC
FRONTIER_LEAF = 0x00542AE8
FRONTIER_READER = 0x00542B0C
DESCRIPTOR_MARKER = 0x0000003F
INDEX_START = -13
INDEX_END = 9

EVIDENCE_REFS = [
    {
        "path": "out/save_scene_selectors.json",
        "fields": [
            "group",
            "slot",
            "firstDwords",
        ],
    },
    {
        "path": "Hwanse2.exe",
        "fields": [
            "selector leaf-table dwords",
            "descriptor marker/child pointers",
            "CNS linked field maps",
        ],
    },
]
FAILED_LEAF_TABLE_GLOBAL_GATE_IDS = [
    "runtime-negative-wrapper-selection",
    "nonnegative-route-pair-entry-reader-proof",
    "selected-root-execution",
    "strict-hotspot",
]
MISSING_EVIDENCE = [
    "runtime selection of the current negative -12 wrapper entry",
    "normal-route proof that a non-negative current route-pair entry reaches the frontier reader",
    "selected-root/current-root runtime execution proof",
    "strict map1_01a source coordinate or hotspot",
]


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 | None) -> str | None:
    return f"0x{value:08x}" if isinstance(value, int) else None


def dword_at(exe: bytes, sections: list[dict], va: int | None) -> int | None:
    if not isinstance(va, int):
        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 first_pointer(row: dict, index: int) -> int | None:
    first = row.get("firstDwords") or []
    if len(first) <= index:
        return None
    item = first[index]
    return item.get("value") if item.get("pointer") else None


def field_maps_for_pointer(
    exe: bytes,
    sections: list[dict],
    strings: dict[int, str],
    cache: dict[int, list[str]],
    pointer: int | None,
) -> list[str]:
    if not isinstance(pointer, int) or va_to_offset(sections, pointer) is None:
        return []
    if pointer not in cache:
        cache[pointer] = collect_linked_cns(exe, sections, strings, pointer).get("fieldMaps") or []
    return cache[pointer]


def has_route_pair(maps: list[str]) -> bool:
    return SOURCE in maps and TARGET in maps


def entry_row(
    exe: bytes,
    sections: list[dict],
    strings: dict[int, str],
    cache: dict[int, list[str]],
    selector: str,
    table_pointer: int,
    index: int,
) -> dict:
    entry_va = table_pointer + index * 4
    descriptor = dword_at(exe, sections, entry_va)
    marker = dword_at(exe, sections, descriptor)
    child = dword_at(exe, sections, descriptor + 4) if marker == DESCRIPTOR_MARKER else None
    descriptor_maps = field_maps_for_pointer(exe, sections, strings, cache, descriptor)
    child_maps = field_maps_for_pointer(exe, sections, strings, cache, child)
    route_pair_descriptor = has_route_pair(descriptor_maps)
    route_pair_child = has_route_pair(child_maps)
    return {
        "selector": selector,
        "entryIndex": index,
        "entryVaHex": hex32(entry_va),
        "entryIsNonNegative": index >= 0,
        "descriptorHex": hex32(descriptor),
        "descriptorMarkerHex": hex32(marker),
        "descriptorIsMarkerShape": marker == DESCRIPTOR_MARKER,
        "childPointerHex": hex32(child),
        "descriptorFieldMaps": descriptor_maps,
        "childFieldMaps": child_maps,
        "descriptorHasRoutePair": route_pair_descriptor,
        "childHasRoutePair": route_pair_child,
        "hasAnyFieldMapEvidence": bool(descriptor_maps or child_maps),
        "hasRoutePair": route_pair_descriptor or route_pair_child,
        "isCurrentSelector": selector == CURRENT_SELECTOR,
        "isFrontierLeafChild": child == FRONTIER_LEAF,
        "isFrontierLeafDescriptor": descriptor == FRONTIER_LEAF,
    }


def selector_window_rows(
    exe: bytes,
    sections: list[dict],
    strings: dict[int, str],
    cache: dict[int, list[str]],
    selector_rows: list[dict],
) -> list[dict]:
    rows = []
    for selector_row in selector_rows:
        table_pointer = first_pointer(selector_row, 2)
        if table_pointer is None:
            continue
        selector = f"{selector_row.get('group')}:{selector_row.get('slot')}"
        rows.extend(
            entry_row(exe, sections, strings, cache, selector, table_pointer, index)
            for index in range(INDEX_START, INDEX_END + 1)
        )
    return rows


def build_summary(exe: bytes, selector_rows: list[dict]) -> dict:
    sections = read_sections(exe)
    strings = find_cns_strings(exe, sections)
    cache: dict[int, list[str]] = {}
    rows = selector_window_rows(exe, sections, strings, cache, selector_rows)
    selectors_with_table = sorted({row["selector"] for row in rows})
    field_rows = [row for row in rows if row.get("hasAnyFieldMapEvidence")]
    negative_field_rows = [row for row in field_rows if row.get("entryIndex", 0) < 0]
    current_field_rows = [row for row in field_rows if row.get("entryIndex", 0) >= 0]
    selectors_with_negative = sorted({row["selector"] for row in negative_field_rows})
    selectors_with_current = sorted({row["selector"] for row in current_field_rows})
    current_selector_rows = [row for row in rows if row["selector"] == CURRENT_SELECTOR]
    current_route_rows = [row for row in current_selector_rows if row.get("hasRoutePair")]
    current_negative_route_rows = [row for row in current_route_rows if row.get("entryIndex", 0) < 0]
    current_nonnegative_route_rows = [row for row in current_route_rows if row.get("entryIndex", 0) >= 0]
    current_frontier_rows = [
        row for row in current_selector_rows
        if row.get("isFrontierLeafChild") or row.get("isFrontierLeafDescriptor")
    ]
    route_pair_rows = [row for row in rows if row.get("hasRoutePair")]
    route_pair_negative_rows = [row for row in route_pair_rows if row.get("entryIndex", 0) < 0]
    route_pair_nonnegative_rows = [row for row in route_pair_rows if row.get("entryIndex", 0) >= 0]
    conclusion = (
        "Across selector rows, field-map-bearing entries commonly appear on both sides of each selector's "
        "root table pointer. The current reader-bearing frontier leaf 0x00542ae8 appears only as the child "
        "of the current selector's negative root-relative entry -12, while the non-negative current entries "
        "that contain the map1_01a/map2_02d route pair do not by themselves prove the reader path. This "
        "keeps the wrapper as sliding-window context evidence rather than normal-transition proof."
    )
    return {
        "source": SOURCE,
        "target": TARGET,
        "currentSelector": CURRENT_SELECTOR,
        "currentRootHex": hex32(CURRENT_ROOT),
        "currentTablePointerHex": hex32(CURRENT_TABLE_POINTER),
        "frontierLeafHex": hex32(FRONTIER_LEAF),
        "frontierReaderHex": hex32(FRONTIER_READER),
        "evidenceRefs": EVIDENCE_REFS,
        "evidenceRefCount": len(EVIDENCE_REFS),
        "windowIndexRange": [INDEX_START, INDEX_END],
        "selectorTableCount": len(selectors_with_table),
        "fieldEntryRowCount": len(field_rows),
        "negativeFieldEntryRowCount": len(negative_field_rows),
        "nonNegativeFieldEntryRowCount": len(current_field_rows),
        "selectorsWithNegativeFieldEntries": len(selectors_with_negative),
        "selectorsWithNonNegativeFieldEntries": len(selectors_with_current),
        "routePairRowCount": len(route_pair_rows),
        "routePairNegativeRowCount": len(route_pair_negative_rows),
        "routePairNonNegativeRowCount": len(route_pair_nonnegative_rows),
        "currentSelectorRoutePairRowCount": len(current_route_rows),
        "currentSelectorNegativeRoutePairRowCount": len(current_negative_route_rows),
        "currentSelectorNonNegativeRoutePairRowCount": len(current_nonnegative_route_rows),
        "currentSelectorRoutePairIndices": [row["entryIndex"] for row in current_route_rows],
        "currentSelectorFrontierLeafRows": current_frontier_rows,
        "currentFrontierLeafOnlyNegative": bool(current_frontier_rows)
        and all(row.get("entryIndex", 0) < 0 for row in current_frontier_rows),
        "currentSelectorRouteRows": current_route_rows,
        "sampleSelectorRows": [
            row for row in field_rows
            if row["selector"] in {"0:0", "1:0", CURRENT_SELECTOR, "10:0"} and row.get("hasRoutePair")
        ][:24],
        "proofFound": False,
        "leafTableGlobalProofFound": False,
        "failedLeafTableGlobalGateIds": FAILED_LEAF_TABLE_GLOBAL_GATE_IDS,
        "missingEvidence": MISSING_EVIDENCE,
        "runtimeSelectionProven": False,
        "strictHotspotFound": False,
        "promotionStatus": "blocked",
        "remainingProofs": MISSING_EVIDENCE,
        "conclusion": conclusion,
    }


def maps_text(row: dict, key: str) -> str:
    return ", ".join(row.get(key) or []) or "-"


def markdown(summary: dict) -> str:
    lines = [
        "# Save Selector Leaf Table Global Context",
        "",
        f"- route: `{summary['source']} -> {summary['target']}`",
        f"- current selector: `{summary['currentSelector']}` root `{summary['currentRootHex']}`",
        f"- current table pointer: `{summary['currentTablePointerHex']}`",
        f"- frontier leaf: `{summary['frontierLeafHex']}`",
        f"- frontier reader: `{summary['frontierReaderHex']}`",
        f"- selector tables scanned: {summary['selectorTableCount']}",
        f"- field-map entry rows: {summary['fieldEntryRowCount']} ({summary['negativeFieldEntryRowCount']} negative, {summary['nonNegativeFieldEntryRowCount']} non-negative)",
        f"- selectors with negative field entries: {summary['selectorsWithNegativeFieldEntries']}",
        f"- selectors with non-negative field entries: {summary['selectorsWithNonNegativeFieldEntries']}",
        f"- current route-pair rows: {summary['currentSelectorRoutePairRowCount']} at {summary['currentSelectorRoutePairIndices']}",
        f"- current frontier leaf only negative: {summary['currentFrontierLeafOnlyNegative']}",
        f"- proof found: `{summary['proofFound']}`",
        f"- leaf table global proof found: `{summary['leafTableGlobalProofFound']}`",
        f"- promotion status: `{summary['promotionStatus']}`",
        f"- evidence refs: `{summary['evidenceRefCount']}`",
        "",
        summary["conclusion"],
        "",
        "## Evidence Refs",
        "",
    ]
    for ref in summary.get("evidenceRefs") or []:
        lines.append(
            f"- `{ref.get('path')}`: {', '.join(ref.get('fields') or [])}"
        )
    lines.extend([
        "",
        "## Current Selector Route Rows",
        "",
        "| index | entry | descriptor | child | non-negative | desc maps | child maps | route pair | frontier leaf |",
        "| ---: | --- | --- | --- | --- | --- | --- | --- | --- |",
    ])
    for row in summary["currentSelectorRouteRows"]:
        lines.append(
            f"| {row['entryIndex']} | `{row['entryVaHex']}` | `{row.get('descriptorHex')}` | "
            f"`{row.get('childPointerHex') or '-'}` | {row['entryIsNonNegative']} | "
            f"{maps_text(row, 'descriptorFieldMaps')} | {maps_text(row, 'childFieldMaps')} | "
            f"{row.get('hasRoutePair')} | {row.get('isFrontierLeafChild') or row.get('isFrontierLeafDescriptor')} |"
        )
    lines.extend([
        "",
        "## Sample Cross-Selector Route Rows",
        "",
        "| selector | index | entry | descriptor | child | non-negative | desc maps | child maps |",
        "| --- | ---: | --- | --- | --- | --- | --- | --- |",
    ])
    for row in summary["sampleSelectorRows"]:
        lines.append(
            f"| `{row['selector']}` | {row['entryIndex']} | `{row['entryVaHex']}` | "
            f"`{row.get('descriptorHex')}` | `{row.get('childPointerHex') or '-'}` | "
            f"{row['entryIsNonNegative']} | {maps_text(row, 'descriptorFieldMaps')} | "
            f"{maps_text(row, 'childFieldMaps')} |"
        )
    lines.extend(["", "## Remaining Proofs", ""])
    lines.extend(f"- {item}" for item in summary["remainingProofs"])
    lines.extend(["", "## Failed Leaf-Table Global Gates", ""])
    lines.extend(f"- {item}" for item in summary["failedLeafTableGlobalGateIds"])
    lines.extend(["", "## Missing Evidence", ""])
    lines.extend(f"- {item}" for item in summary["missingEvidence"])
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    evidence_refs = "".join(
        "<li>"
        f"<code>{html.escape(str(ref.get('path')))}</code>: "
        f"{html.escape(', '.join(ref.get('fields') or []))}"
        "</li>"
        for ref in summary.get("evidenceRefs") or []
    )

    def row_html(rows: list[dict], include_selector: bool) -> str:
        cells = []
        for row in rows:
            selector_cell = f"<td><code>{html.escape(row['selector'])}</code></td>" if include_selector else ""
            cells.append(
                "<tr>"
                f"{selector_cell}"
                f"<td>{row['entryIndex']}</td>"
                f"<td><code>{html.escape(str(row['entryVaHex']))}</code></td>"
                f"<td><code>{html.escape(str(row.get('descriptorHex')))}</code></td>"
                f"<td><code>{html.escape(str(row.get('childPointerHex') or '-'))}</code></td>"
                f"<td>{row['entryIsNonNegative']}</td>"
                f"<td>{html.escape(maps_text(row, 'descriptorFieldMaps'))}</td>"
                f"<td>{html.escape(maps_text(row, 'childFieldMaps'))}</td>"
                f"<td>{row.get('hasRoutePair')}</td>"
                f"<td>{row.get('isFrontierLeafChild') or row.get('isFrontierLeafDescriptor')}</td>"
                "</tr>"
            )
        return "".join(cells)

    proofs = "".join(f"<li>{html.escape(item)}</li>" for item in summary["remainingProofs"])
    failed_gates = "".join(
        f"<li>{html.escape(item)}</li>"
        for item in summary["failedLeafTableGlobalGateIds"]
    )
    missing = "".join(f"<li>{html.escape(item)}</li>" for item in summary["missingEvidence"])
    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 Leaf Table Global Context</title>",
        "  <style>body{font-family:system-ui,sans-serif;background:#111;color:#eee;margin:24px}table{border-collapse:collapse;width:100%;max-width:1280px}td,th{border:1px solid #333;padding:6px 8px;text-align:left;vertical-align:top}th{background:#1f1f1f}code{color:#9bd4ff}</style>",
        "</head>",
        "<body>",
        "  <h1>Save Selector Leaf Table Global Context</h1>",
        f"  <p>route <code>{summary['source']} -&gt; {summary['target']}</code>; current selector <code>{summary['currentSelector']}</code>; selector tables scanned {summary['selectorTableCount']}; field rows {summary['fieldEntryRowCount']}; current route rows {summary['currentSelectorRoutePairRowCount']} at {summary['currentSelectorRoutePairIndices']}; frontier only negative {summary['currentFrontierLeafOnlyNegative']}; proof found <code>{summary['proofFound']}</code>; leaf table global proof found <code>{summary['leafTableGlobalProofFound']}</code>; promotion <code>{summary['promotionStatus']}</code>.</p>",
        f"  <p>{html.escape(summary['conclusion'])}</p>",
        f"  <p><b>Evidence refs:</b> {summary['evidenceRefCount']}.</p>",
        f"  <h2>Evidence Refs</h2><ul>{evidence_refs}</ul>",
        "  <h2>Current Selector Route Rows</h2>",
        "  <table><thead><tr><th>index</th><th>entry</th><th>descriptor</th><th>child</th><th>non-negative</th><th>desc maps</th><th>child maps</th><th>route pair</th><th>frontier leaf</th></tr></thead>",
        f"  <tbody>{row_html(summary['currentSelectorRouteRows'], False)}</tbody></table>",
        "  <h2>Sample Cross-Selector Route Rows</h2>",
        "  <table><thead><tr><th>selector</th><th>index</th><th>entry</th><th>descriptor</th><th>child</th><th>non-negative</th><th>desc maps</th><th>child maps</th><th>route pair</th><th>frontier leaf</th></tr></thead>",
        f"  <tbody>{row_html(summary['sampleSelectorRows'], True)}</tbody></table>",
        "  <h2>Remaining Proofs</h2>",
        f"  <ul>{proofs}</ul>",
        "  <h2>Failed Leaf-Table Global Gates</h2>",
        f"  <ul>{failed_gates}</ul>",
        "  <h2>Missing Evidence</h2>",
        f"  <ul>{missing}</ul>",
        "</body>",
        "</html>",
        "",
    ])


def write_outputs(summary: dict, out_dir: Path) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "save_selector_leaf_table_global_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("--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 leaf table global context -> {args.out_dir / 'save_selector_leaf_table_global_context.json'}")


if __name__ == "__main__":
    main()
