#!/usr/bin/env python3
"""Summarize route selector root references for the current merge/order gap."""
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 offset_to_va, read_sections


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"
SOURCE = "map1_01a"
TARGET = "map2_02d"
ROUTE_SELECTORS = ("0:0", "1:0", "2:0")
FAILED_ROUTE_ROOT_REF_GATE_IDS = [
    "selector-root-code-ref",
    "predecessor-current-root-link",
    "route-order-execution-proof",
    "strict-hotspot-or-real-selector-proof",
]
ROUTE_ROOT_REF_MISSING_EVIDENCE = [
    "code or runtime reference executing route selector root 2:0",
    "predecessor 1:0 root/control-flow link to current selector root 2:0",
    "route-order proof from source/predecessor selectors into current selector",
    "strict source hotspot or captured selector 2:0 save/runtime evidence",
]
ROUTE_ROOT_REF_EVIDENCE_REFS = [
    {
        "path": "Hwanse2.exe",
        "fields": [
            "selector row-pointer dword refs",
            "selected-root dword refs",
            ".text reference classification",
        ],
    },
    {
        "path": "out/save_scene_selectors.json",
        "fields": ["group", "slot", "rowPointerHex", "selectedPointerHex", "fieldMaps"],
    },
]


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


def parse_hex(value: str | None) -> int | None:
    return int(value, 16) if value else None


def selector_key(row: dict) -> str:
    return f"{row.get('group')}:{row.get('slot')}"


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


def section_for_va(sections: list[dict], va: int) -> str | None:
    for section in sections:
        start = int(section["va"])
        end = start + int(section["raw_size"])
        if start <= va < end:
            return str(section.get("name") or "")
    return None


def dword_refs(exe: bytes, sections: list[dict], target: int) -> list[dict]:
    pattern = struct.pack("<I", target)
    refs = []
    start = 0
    while True:
        index = exe.find(pattern, start)
        if index < 0:
            break
        va = offset_to_va(sections, index)
        if va is not None:
            refs.append({
                "refVa": va,
                "refVaHex": hex32(va),
                "targetVaHex": hex32(target),
                "section": section_for_va(sections, va),
            })
        start = index + 1
    return refs


def selector_row(selectors: list[dict], key: str) -> dict:
    for row in selectors:
        if selector_key(row) == key:
            return row
    raise ValueError(f"selector {key} not found")


def classify_ref(row: dict, selector: dict, ref_kind: str) -> dict:
    ref_va = parse_hex(row.get("refVaHex"))
    role = "other"
    if ref_kind == "rowPointer" and row.get("refVaHex") == selector.get("rowPointerVaHex"):
        role = "selector-table-row-pointer-entry"
    elif ref_kind == "selectedRoot" and row.get("refVaHex") == selector.get("selectedPointerVaHex"):
        role = "selector-row-selected-root-entry"
    return {
        **row,
        "refKind": ref_kind,
        "role": role,
        "isTextRef": row.get("section") == ".text",
        "offsetFromRowPointerVa": (
            ref_va - parse_hex(selector.get("rowPointerVaHex"))
            if ref_va is not None and parse_hex(selector.get("rowPointerVaHex")) is not None
            else None
        ),
    }


def build_selector_summary(exe: bytes, sections: list[dict], selector: dict) -> dict:
    row_pointer = parse_hex(selector.get("rowPointerHex"))
    selected_root = parse_hex(selector.get("selectedPointerHex"))
    if row_pointer is None or selected_root is None:
        raise ValueError(f"selector {selector_key(selector)} missing pointer fields")
    row_pointer_refs = [
        classify_ref(ref, selector, "rowPointer")
        for ref in dword_refs(exe, sections, row_pointer)
    ]
    selected_root_refs = [
        classify_ref(ref, selector, "selectedRoot")
        for ref in dword_refs(exe, sections, selected_root)
    ]
    text_refs = [ref for ref in row_pointer_refs + selected_root_refs if ref.get("isTextRef")]
    table_only_chain = (
        len(row_pointer_refs) == 1
        and row_pointer_refs[0].get("role") == "selector-table-row-pointer-entry"
        and len(selected_root_refs) == 1
        and selected_root_refs[0].get("role") == "selector-row-selected-root-entry"
        and not text_refs
    )
    field_maps = selector.get("fieldMaps") or []
    return {
        "selector": selector_key(selector),
        "rowPointerTableEntryHex": selector.get("rowPointerVaHex"),
        "rowPointerHex": selector.get("rowPointerHex"),
        "selectedRootEntryHex": selector.get("selectedPointerVaHex"),
        "selectedRootHex": selector.get("selectedPointerHex"),
        "fieldMaps": field_maps,
        "containsSource": SOURCE in field_maps,
        "containsTarget": TARGET in field_maps,
        "rowPointerRefCount": len(row_pointer_refs),
        "selectedRootRefCount": len(selected_root_refs),
        "textRefCount": len(text_refs),
        "rowPointerRefs": row_pointer_refs,
        "selectedRootRefs": selected_root_refs,
        "tableOnlyChain": table_only_chain,
    }


def build_summary(exe: bytes, selectors: list[dict] | None = None) -> dict:
    selectors = selectors if selectors is not None else load_json(OUT / "save_scene_selectors.json", [])
    sections = read_sections(exe)
    rows = [
        build_selector_summary(exe, sections, selector_row(selectors, key))
        for key in ROUTE_SELECTORS
    ]
    source_row = rows[0]
    predecessor_row = rows[1]
    current_row = rows[2]
    all_table_only = all(row["tableOnlyChain"] for row in rows)
    any_text_refs = any(row["textRefCount"] for row in rows)
    predecessor_to_current_root_ref = any(
        ref.get("targetVaHex") == current_row["selectedRootHex"]
        for ref in predecessor_row["rowPointerRefs"] + predecessor_row["selectedRootRefs"]
    )
    current_contains_pair = current_row["containsSource"] and current_row["containsTarget"]
    source_target_split = (
        source_row["containsSource"]
        and not source_row["containsTarget"]
        and predecessor_row["containsTarget"]
        and not predecessor_row["containsSource"]
        and current_contains_pair
    )
    conclusion = (
        "The route-relevant selector roots 0:0, 1:0, and 2:0 each have a selector-table-only pointer chain: "
        "one table entry points to a selector row, and that row points to the selected root. No .text dword ref "
        "targets these row pointers or roots, and the predecessor root has no direct pointer to the current root. "
        "This reinforces the selector-merge gap: 0:0 is source-side, 1:0 is target-side, and 2:0 carries both maps, "
        "but the static root references are table membership, not execution order proof."
    )
    return {
        "source": SOURCE,
        "target": TARGET,
        "selectors": rows,
        "allRouteSelectorRootsTableOnly": all_table_only,
        "anyRouteSelectorRootTextRefs": any_text_refs,
        "sourceTargetSplitAcrossPreviousSelectors": source_target_split,
        "currentSelectorContainsRoutePair": current_contains_pair,
        "predecessorToCurrentRootRefFound": predecessor_to_current_root_ref,
        "routeOrderProven": False,
        "proofFound": False,
        "routeRootRefProofFound": False,
        "failedRouteRootRefGateIds": FAILED_ROUTE_ROOT_REF_GATE_IDS,
        "missingEvidence": ROUTE_ROOT_REF_MISSING_EVIDENCE,
        "evidenceRefs": ROUTE_ROOT_REF_EVIDENCE_REFS,
        "evidenceRefCount": len(ROUTE_ROOT_REF_EVIDENCE_REFS),
        "promotionStatus": "blocked",
        "remainingProofs": [
            "find runtime/control-flow proof that target-side selector 1:0 state reaches selector 2:0",
            "find a strict map1_01a source hotspot or equivalent runtime trigger",
            "replace selector table adjacency with captured selector 2:0 save/runtime evidence",
        ],
        "conclusion": conclusion,
    }


def markdown(summary: dict) -> str:
    lines = [
        "# Save Selector Route Root Reference Context",
        "",
        f"- route: `{summary['source']} -> {summary['target']}`",
        f"- all route selector roots table-only: {summary['allRouteSelectorRootsTableOnly']}",
        f"- any route selector root .text refs: {summary['anyRouteSelectorRootTextRefs']}",
        f"- source/target split across previous selectors: {summary['sourceTargetSplitAcrossPreviousSelectors']}",
        f"- current selector contains route pair: {summary['currentSelectorContainsRoutePair']}",
        f"- predecessor -> current root ref found: {summary['predecessorToCurrentRootRefFound']}",
        f"- route order proven: {summary['routeOrderProven']}",
        f"- proof found: {summary['proofFound']}",
        f"- route root-ref proof found: {summary['routeRootRefProofFound']}",
        f"- failed route root-ref gates: {', '.join(summary.get('failedRouteRootRefGateIds') or []) or '-'}",
        f"- missing evidence count: {len(summary.get('missingEvidence') or [])}",
        f"- evidence refs: {summary.get('evidenceRefCount')}",
        f"- promotion status: `{summary['promotionStatus']}`",
        "",
        summary["conclusion"],
        "",
        "## Missing Evidence",
        "",
    ]
    lines.extend(f"- {item}" for item in summary["missingEvidence"])
    lines.extend([
        "",
        "## Selector Roots",
        "",
        "| selector | row pointer entry | row pointer | root entry | selected root | field maps | refs row/root | text refs | table-only |",
        "| --- | --- | --- | --- | --- | --- | ---: | ---: | --- |",
    ])
    for row in summary["selectors"]:
        maps = ", ".join(row["fieldMaps"])
        lines.append(
            f"| `{row['selector']}` | `{row['rowPointerTableEntryHex']}` | `{row['rowPointerHex']}` | "
            f"`{row['selectedRootEntryHex']}` | `{row['selectedRootHex']}` | {maps} | "
            f"{row['rowPointerRefCount']}/{row['selectedRootRefCount']} | {row['textRefCount']} | "
            f"{row['tableOnlyChain']} |"
        )
    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:
    selector_rows = []
    for row in summary["selectors"]:
        selector_rows.append(
            "<tr>"
            f"<td><code>{html.escape(row['selector'])}</code></td>"
            f"<td><code>{html.escape(row['rowPointerTableEntryHex'])}</code></td>"
            f"<td><code>{html.escape(row['rowPointerHex'])}</code></td>"
            f"<td><code>{html.escape(row['selectedRootEntryHex'])}</code></td>"
            f"<td><code>{html.escape(row['selectedRootHex'])}</code></td>"
            f"<td>{html.escape(', '.join(row['fieldMaps']))}</td>"
            f"<td>{row['rowPointerRefCount']}/{row['selectedRootRefCount']}</td>"
            f"<td>{row['textRefCount']}</td>"
            f"<td>{row['tableOnlyChain']}</td>"
            "</tr>"
        )
    failed_gates = "".join(
        f"<li>{html.escape(item)}</li>" for item in summary.get("failedRouteRootRefGateIds") or []
    )
    missing = "".join(f"<li>{html.escape(item)}</li>" for item in summary.get("missingEvidence") or [])
    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 Route Root Reference Context</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: 1180px; 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 Route Root Reference Context</h1>",
        f"  <p>route <code>{html.escape(summary['source'])} -&gt; {html.escape(summary['target'])}</code>; "
        f"all route selector roots table-only: {summary['allRouteSelectorRootsTableOnly']}; "
        f"any route selector root .text refs: {summary['anyRouteSelectorRootTextRefs']}; "
        f"source/target split across previous selectors: {summary['sourceTargetSplitAcrossPreviousSelectors']}; "
        f"predecessor -> current root ref found: {summary['predecessorToCurrentRootRefFound']}; "
        f"proof found: {summary['proofFound']}; "
        f"missing evidence count: {len(summary.get('missingEvidence') or [])}; "
        f"evidence refs: {summary.get('evidenceRefCount')}; "
        f"promotion status <code>{html.escape(summary['promotionStatus'])}</code>.</p>",
        f"  <p>{html.escape(summary['conclusion'])}</p>",
        f"  <h2>Failed Route Root-Ref Gates</h2><ul>{failed_gates}</ul>",
        f"  <h2>Missing Evidence</h2><ul>{missing}</ul>",
        "  <h2>Selector Roots</h2>",
        "  <table><thead><tr><th>selector</th><th>row pointer entry</th><th>row pointer</th><th>root entry</th><th>selected root</th><th>field maps</th><th>refs row/root</th><th>text refs</th><th>table-only</th></tr></thead><tbody>",
        *selector_rows,
        "  </tbody></table>",
        "  <h2>Remaining Proofs</h2>",
        f"  <ul>{proofs}</ul>",
        "</body>",
        "</html>",
        "",
    ])


def write_outputs(summary: dict, out_dir: Path = OUT, html_out: Path | None = None) -> Path:
    out_dir.mkdir(parents=True, exist_ok=True)
    json_out = out_dir / "save_selector_route_root_ref_context.json"
    json_out.write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\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")
    return json_out


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)
    parser.add_argument("--html-out", type=Path)
    args = parser.parse_args()
    summary = build_summary(args.exe.read_bytes(), load_json(args.selectors, []))
    json_out = write_outputs(summary, args.out_dir, args.html_out)
    print(f"wrote save selector route root reference context -> {json_out}")


if __name__ == "__main__":
    main()
