#!/usr/bin/env python3
"""Scan direct bridge refs between predecessor 1:0 and current 2:0 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


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

PREDECESSOR_SELECTOR = "1:0"
CURRENT_SELECTOR = "2:0"
PREDECESSOR_ROOT = 0x00478364
PREDECESSOR_END = 0x0048458C
CURRENT_ROOT = 0x00540714
CURRENT_END = 0x00543578
CURRENT_FRONTIER_READER = 0x00542B0C
CURRENT_SOURCE_RECORD = 0x00542B44
CURRENT_TARGET_RECORD = 0x00542BAC
PREDECESSOR_FILL_SITES = [0x004844D0, 0x004844D8]


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_va(data: bytes, sections: list[dict], va: int) -> int | None:
    offset = va_to_offset(sections, va)
    if offset is None or offset + 4 > len(data):
        return None
    return struct.unpack_from("<I", data, offset)[0]


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


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 collect_path_targets(select_refs: list[dict], label: str) -> dict[int, str]:
    targets: dict[int, str] = {}
    for row in select_refs:
        if row.get("label") != label:
            continue
        for path_va in row.get("path") or []:
            targets.setdefault(int(path_va), f"{label} selector path")
        ref_va = parse_hex(row.get("refVaHex"))
        if ref_va is not None:
            targets.setdefault(ref_va, f"{label} resource ref")
    return targets


def exact_target_rows(targets: dict[int, str]) -> list[dict]:
    return [
        {"va": va, "vaHex": hex32(va), "label": label}
        for va, label in sorted(targets.items())
    ]


def scan_dwords_for_bridge(
    exe: bytes,
    sections: list[dict],
    source_start: int,
    source_end: int,
    target_start: int,
    target_end: int,
    exact_targets: dict[int, str],
) -> dict:
    hits = []
    readable_count = 0
    unreadable_count = 0
    for va in range(source_start, source_end, 4):
        value = dword_at_va(exe, sections, va)
        if value is None:
            unreadable_count += 1
            continue
        readable_count += 1
        inside_target_range = target_start <= value < target_end
        exact_label = exact_targets.get(value)
        if not inside_target_range and exact_label is None:
            continue
        hits.append({
            "sourceVa": va,
            "sourceVaHex": hex32(va),
            "value": value,
            "valueHex": hex32(value),
            "insideTargetRootRange": inside_target_range,
            "exactLabel": exact_label,
        })
    return {
        "sourceRangeHex": range_hex(source_start, source_end),
        "targetRootRangeHex": range_hex(target_start, target_end),
        "readableDwordCount": readable_count,
        "unreadableDwordCount": unreadable_count,
        "targetExactValues": exact_target_rows(exact_targets),
        "hitCount": len(hits),
        "hits": hits,
    }


def build_summary(
    exe: bytes,
    save_scene_selectors: list[dict] | None = None,
    save_scene_selector_refs: list[dict] | None = None,
    predecessor_tail_reset: dict | None = None,
    current_root_paths: dict | None = None,
) -> dict:
    sections = read_sections(exe)
    save_scene_selectors = save_scene_selectors if save_scene_selectors is not None else load_json(
        OUT / "save_scene_selectors.json",
        [],
    )
    save_scene_selector_refs = save_scene_selector_refs if save_scene_selector_refs is not None else load_json(
        OUT / "save_scene_selector_references.json",
        [],
    )
    predecessor_tail_reset = predecessor_tail_reset if predecessor_tail_reset is not None else load_json(
        OUT / "save_selector_predecessor_tail_reset.json",
        {},
    )
    current_root_paths = current_root_paths if current_root_paths is not None else load_json(
        OUT / "save_selector_current_root_frontier_paths.json",
        {},
    )

    predecessor_row = selector_row(save_scene_selectors, PREDECESSOR_SELECTOR)
    current_row = selector_row(save_scene_selectors, CURRENT_SELECTOR)
    predecessor_root = parse_hex(predecessor_row.get("selectedPointerHex")) or PREDECESSOR_ROOT
    current_root = parse_hex(current_row.get("selectedPointerHex")) or CURRENT_ROOT
    predecessor_end = parse_hex(predecessor_tail_reset.get("nextRootHex")) or PREDECESSOR_END
    current_end = CURRENT_END

    predecessor_exact_targets = collect_path_targets(save_scene_selector_refs, PREDECESSOR_SELECTOR)
    predecessor_exact_targets.update({
        predecessor_root: "predecessor root 1:0",
    })
    for fill_va in PREDECESSOR_FILL_SITES:
        predecessor_exact_targets[fill_va] = "predecessor secondaryBranchState fill"
    current_exact_targets = collect_path_targets(save_scene_selector_refs, CURRENT_SELECTOR)
    current_exact_targets.update({
        current_root: "current root 2:0",
        CURRENT_FRONTIER_READER: "current frontier reader/resource gate",
        CURRENT_SOURCE_RECORD: "current source scene record map1_01a",
        CURRENT_TARGET_RECORD: "current target scene record map2_02d",
    })
    for leaf in current_root_paths.get("leafPaths") or current_root_paths.get("leaves") or []:
        leaf_va = parse_hex(leaf.get("leafPointerHex"))
        if leaf_va is not None:
            current_exact_targets.setdefault(leaf_va, "current leaf path")

    predecessor_to_current = scan_dwords_for_bridge(
        exe,
        sections,
        predecessor_root,
        predecessor_end,
        current_root,
        current_end,
        current_exact_targets,
    )
    current_to_predecessor = scan_dwords_for_bridge(
        exe,
        sections,
        current_root,
        current_end,
        predecessor_root,
        predecessor_end,
        predecessor_exact_targets,
    )
    forward_execution_bridge_found = predecessor_to_current["hitCount"] > 0
    reverse_reuse_hits = current_to_predecessor["hits"]
    reverse_hits_to_fill = [
        row for row in reverse_reuse_hits
        if row["value"] in PREDECESSOR_FILL_SITES
    ]
    first_fill = min(PREDECESSOR_FILL_SITES)
    reverse_hits_before_fill = [
        row for row in reverse_reuse_hits
        if row["value"] < first_fill
    ]
    reverse_hits_after_or_at_fill = [
        row for row in reverse_reuse_hits
        if row["value"] >= first_fill
    ]
    reverse_hits_before_current_writer = [
        row for row in reverse_reuse_hits
        if row["sourceVa"] < 0x005428BC
    ]
    bridge_found = forward_execution_bridge_found or bool(reverse_reuse_hits)
    reverse_hits_only_before_fill = (
        bool(reverse_reuse_hits)
        and len(reverse_hits_before_fill) == len(reverse_reuse_hits)
    )
    conclusion = (
        "The direct bridge scan found no predecessor->current dword edge. The reverse direction has current-root dwords "
        "that point back into the predecessor root, but all of those targets are before the predecessor secondary fill "
        "sites 0x004844d0/0x004844d8 and none targets the fill sites themselves. This looks like shared/reused script "
        "substructure rather than proof that selector 1:0 executes into selector 2:0 or leaves its fill state for the "
        "current frontier. Promotion remains blocked until runtime order/state persistence or a strict map1_01a hotspot "
        "is proven."
    )
    return {
        "predecessorSelector": PREDECESSOR_SELECTOR,
        "predecessorRootHex": hex32(predecessor_root),
        "predecessorRootRangeHex": range_hex(predecessor_root, predecessor_end),
        "predecessorFieldMaps": predecessor_row.get("fieldMaps") or [],
        "currentSelector": CURRENT_SELECTOR,
        "currentRootHex": hex32(current_root),
        "currentRootRangeHex": range_hex(current_root, current_end),
        "currentFieldMaps": current_row.get("fieldMaps") or [],
        "predecessorToCurrent": predecessor_to_current,
        "currentToPredecessor": current_to_predecessor,
        "bridgeFound": bridge_found,
        "forwardExecutionBridgeFound": forward_execution_bridge_found,
        "reverseReuseHitCount": len(reverse_reuse_hits),
        "reverseHitsToFillSiteCount": len(reverse_hits_to_fill),
        "reverseHitsBeforeFillCount": len(reverse_hits_before_fill),
        "reverseHitsAfterOrAtFillCount": len(reverse_hits_after_or_at_fill),
        "reverseHitsBeforeCurrentWriterCount": len(reverse_hits_before_current_writer),
        "reverseHitsOnlyBeforeFill": reverse_hits_only_before_fill,
        "routeOrderProven": False,
        "promotionStatus": "blocked",
        "limitations": [
            "This scan covers direct dword values in the known selector root ranges only.",
            "It does not rule out VM-dispatched execution order, save flag progression, or indirect runtime state transitions.",
            "It does not replace the strict hotspot/source-coordinate requirement.",
        ],
        "remainingProofs": [
            "prove selector 1:0 executes before selector 2:0 in the normal runtime path",
            "prove secondaryBranchState persists from predecessor fill to current reader",
            "find strict map1_01a source coordinate or hotspot",
        ],
        "conclusion": conclusion,
    }


def markdown(summary: dict) -> str:
    p2c = summary["predecessorToCurrent"]
    c2p = summary["currentToPredecessor"]
    lines = [
        "# Save Selector Predecessor Bridge Refs",
        "",
        f"- predecessor: `{summary['predecessorSelector']}` root `{summary['predecessorRootHex']}` range `{summary['predecessorRootRangeHex']}`",
        f"- current: `{summary['currentSelector']}` root `{summary['currentRootHex']}` range `{summary['currentRootRangeHex']}`",
        f"- predecessor -> current hits: {p2c['hitCount']}",
        f"- current -> predecessor hits: {c2p['hitCount']}",
        f"- bridge found: {summary['bridgeFound']}",
        f"- forward execution bridge found: {summary['forwardExecutionBridgeFound']}",
        f"- reverse hits to fill sites: {summary['reverseHitsToFillSiteCount']}",
        f"- reverse hits only before fill: {summary['reverseHitsOnlyBeforeFill']}",
        f"- route order proven: {summary['routeOrderProven']}",
        f"- promotion status: `{summary['promotionStatus']}`",
        "",
        summary["conclusion"],
        "",
        "## Scan Summary",
        "",
        "| direction | source range | target root range | readable dwords | hits |",
        "| --- | --- | --- | ---: | ---: |",
        f"| predecessor -> current | `{p2c['sourceRangeHex']}` | `{p2c['targetRootRangeHex']}` | {p2c['readableDwordCount']} | {p2c['hitCount']} |",
        f"| current -> predecessor | `{c2p['sourceRangeHex']}` | `{c2p['targetRootRangeHex']}` | {c2p['readableDwordCount']} | {c2p['hitCount']} |",
        "",
        "## Reverse Hit Classification",
        "",
        "| metric | count/status |",
        "| --- | ---: |",
        f"| reverse reuse hits | {summary['reverseReuseHitCount']} |",
        f"| reverse hits before predecessor fill | {summary['reverseHitsBeforeFillCount']} |",
        f"| reverse hits at/after predecessor fill | {summary['reverseHitsAfterOrAtFillCount']} |",
        f"| reverse hits to exact fill sites | {summary['reverseHitsToFillSiteCount']} |",
        f"| reverse hits before current writer 0x005428bc | {summary['reverseHitsBeforeCurrentWriterCount']} |",
        f"| reverse hits only before fill | {summary['reverseHitsOnlyBeforeFill']} |",
        "",
        "## Hits",
        "",
        "### Predecessor -> Current",
        "",
        "| source | value | inside target range | exact label |",
        "| --- | --- | --- | --- |",
    ]
    if p2c["hits"]:
        for row in p2c["hits"]:
            lines.append(
                f"| `{row['sourceVaHex']}` | `{row['valueHex']}` | {row['insideTargetRootRange']} | {row.get('exactLabel') or '-'} |"
            )
    else:
        lines.append("| - | - | - | - |")
    lines.extend([
        "",
        "### Current -> Predecessor",
        "",
        "| source | value | inside target range | exact label |",
        "| --- | --- | --- | --- |",
    ])
    if c2p["hits"]:
        for row in c2p["hits"]:
            lines.append(
                f"| `{row['sourceVaHex']}` | `{row['valueHex']}` | {row['insideTargetRootRange']} | {row.get('exactLabel') or '-'} |"
            )
    else:
        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:
    def hit_rows(rows: list[dict]) -> str:
        if not rows:
            return '<tr><td colspan="4">No hits.</td></tr>'
        return "\n".join(
            "<tr>"
            f"<td><code>{html.escape(row['sourceVaHex'])}</code></td>"
            f"<td><code>{html.escape(row['valueHex'])}</code></td>"
            f"<td>{row['insideTargetRootRange']}</td>"
            f"<td>{html.escape(row.get('exactLabel') or '-')}</td>"
            "</tr>"
            for row in rows
        )

    p2c = summary["predecessorToCurrent"]
    c2p = summary["currentToPredecessor"]
    remaining = "".join(f"<li>{html.escape(item)}</li>" for item in summary["remainingProofs"])
    limitations = "".join(f"<li>{html.escape(item)}</li>" for item in summary["limitations"])
    return "\n".join([
        "<!doctype html><meta charset=\"utf-8\"><title>Save Selector Predecessor Bridge Refs</title>",
        "<style>body{font-family:system-ui,sans-serif;background:#111;color:#eee;max-width:1120px;margin:24px auto;line-height:1.45}table{border-collapse:collapse;width:100%;margin:16px 0 28px}td,th{border:1px solid #444;padding:6px 8px;vertical-align:top}th{background:#202020}code{color:#9bd4ff}</style>",
        "<h1>Save Selector Predecessor Bridge Refs</h1>",
        f"<p>predecessor <code>{summary['predecessorSelector']}</code> root <code>{summary['predecessorRootHex']}</code> range <code>{summary['predecessorRootRangeHex']}</code>; current <code>{summary['currentSelector']}</code> root <code>{summary['currentRootHex']}</code> range <code>{summary['currentRootRangeHex']}</code>.</p>",
        f"<p>predecessor -> current hits: {p2c['hitCount']}; current -> predecessor hits: {c2p['hitCount']}; bridge found: {summary['bridgeFound']}; promotion status <code>{summary['promotionStatus']}</code>.</p>",
        f"<p>forward execution bridge found: {summary['forwardExecutionBridgeFound']}; reverse hits to fill sites: {summary['reverseHitsToFillSiteCount']}; reverse hits only before fill: {summary['reverseHitsOnlyBeforeFill']}.</p>",
        f"<p>{html.escape(summary['conclusion'])}</p>",
        "<h2>Scan Summary</h2>",
        "<table><thead><tr><th>direction</th><th>source range</th><th>target root range</th><th>readable dwords</th><th>hits</th></tr></thead><tbody>",
        f"<tr><td>predecessor -&gt; current</td><td><code>{p2c['sourceRangeHex']}</code></td><td><code>{p2c['targetRootRangeHex']}</code></td><td>{p2c['readableDwordCount']}</td><td>{p2c['hitCount']}</td></tr>",
        f"<tr><td>current -&gt; predecessor</td><td><code>{c2p['sourceRangeHex']}</code></td><td><code>{c2p['targetRootRangeHex']}</code></td><td>{c2p['readableDwordCount']}</td><td>{c2p['hitCount']}</td></tr>",
        "</tbody></table>",
        "<h2>Reverse Hit Classification</h2>",
        "<table><thead><tr><th>metric</th><th>count/status</th></tr></thead><tbody>",
        f"<tr><td>reverse reuse hits</td><td>{summary['reverseReuseHitCount']}</td></tr>",
        f"<tr><td>reverse hits before predecessor fill</td><td>{summary['reverseHitsBeforeFillCount']}</td></tr>",
        f"<tr><td>reverse hits at/after predecessor fill</td><td>{summary['reverseHitsAfterOrAtFillCount']}</td></tr>",
        f"<tr><td>reverse hits to exact fill sites</td><td>{summary['reverseHitsToFillSiteCount']}</td></tr>",
        f"<tr><td>reverse hits before current writer 0x005428bc</td><td>{summary['reverseHitsBeforeCurrentWriterCount']}</td></tr>",
        f"<tr><td>reverse hits only before fill</td><td>{summary['reverseHitsOnlyBeforeFill']}</td></tr>",
        "</tbody></table>",
        "<h2>Predecessor -&gt; Current Hits</h2>",
        "<table><thead><tr><th>source</th><th>value</th><th>inside target range</th><th>exact label</th></tr></thead><tbody>",
        hit_rows(p2c["hits"]),
        "</tbody></table>",
        "<h2>Current -&gt; Predecessor Hits</h2>",
        "<table><thead><tr><th>source</th><th>value</th><th>inside target range</th><th>exact label</th></tr></thead><tbody>",
        hit_rows(c2p["hits"]),
        "</tbody></table>",
        "<h2>Remaining Proofs</h2>",
        f"<ul>{remaining}</ul>",
        "<h2>Limitations</h2>",
        f"<ul>{limitations}</ul>",
    ])


def write_outputs(summary: dict, out_dir: Path) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "save_selector_predecessor_bridge_refs.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    (out_dir / "save_selector_predecessor_bridge_refs.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("--out-dir", type=Path, default=OUT)
    args = parser.parse_args()
    summary = build_summary(args.exe.read_bytes())
    write_outputs(summary, args.out_dir)
    print(f"wrote predecessor bridge refs -> {args.out_dir / 'save_selector_predecessor_bridge_refs.html'}")


if __name__ == "__main__":
    main()
