#!/usr/bin/env python3
"""Summarize secondaryBranchState reset/write scope around the current blocker."""
from __future__ import annotations

import argparse
import html
import json
import struct
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))

from probe_exe_scene_tables import offset_to_va, read_sections, va_to_offset


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"
SECONDARY_TABLE = 0x0059E360
HELPER_VA = 0x00410C90
OPCODE10_HANDLER = 0x0040B49E


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


def call_refs(exe: bytes, sections: list[dict], target: int) -> list[dict]:
    rows = []
    for section in sections:
        if section.get("name") != ".text":
            continue
        start = section["raw"]
        end = start + section["raw_size"]
        data = exe[start:end]
        for index in range(0, max(0, len(data) - 4)):
            if data[index] != 0xE8:
                continue
            va = offset_to_va(sections, start + index)
            if va is None:
                continue
            rel = struct.unpack_from("<i", data, index + 1)[0]
            dest = va + 5 + rel
            if dest == target:
                rows.append({
                    "callVa": va,
                    "callVaHex": hex32(va),
                    "targetVaHex": hex32(dest),
                    "insideOpcode10Handler": OPCODE10_HANDLER <= va < OPCODE10_HANDLER + 0x80,
                })
    return rows


def bytes_before(exe: bytes, offset: int, count: int) -> bytes:
    return exe[max(0, offset - count):offset]


def classify_secondary_ref(exe: bytes, sections: list[dict], ref: dict) -> dict:
    row = dict(ref)
    offset = ref.get("fileOffset")
    if not isinstance(offset, int):
        return row
    before = bytes_before(exe, offset, 4)
    if ref.get("kind") == "unknown" and len(before) >= 3 and before[-3:-1] == b"\xc7\x45":
        row["kind"] = "localTablePointerImmediate"
        row["writeToSecondaryTable"] = False
        row["meaning"] = "loads the secondary table address into a stack local; not a table write"
    elif ref.get("kind") == "baseAddressImmediate":
        row["writeToSecondaryTable"] = False
        row["meaning"] = "loads secondary table base for opcode 0x10 helper dispatch"
    elif ref.get("kind") == "indexedReadByte":
        row["writeToSecondaryTable"] = False
        row["meaning"] = "reads secondaryBranchState by selected slot"
    elif ref.get("kind") == "indexedWriteImmediate":
        row["writeToSecondaryTable"] = True
        row["meaning"] = "direct indexed write to secondaryBranchState"
    else:
        row["writeToSecondaryTable"] = False
        row["meaning"] = row.get("kind") or "unknown reference"
    return row


def secondary_table_summary(exe: bytes, sections: list[dict], branch_state: dict) -> dict:
    tables = {table.get("name"): table for table in branch_state.get("tables") or []}
    secondary = tables.get("secondaryBranchState") or {}
    refs = [
        classify_secondary_ref(exe, sections, ref)
        for ref in secondary.get("refs") or []
    ]
    kinds: dict[str, int] = {}
    direct_writers = []
    unresolved = []
    for ref in refs:
        kind = ref.get("kind") or "unknown"
        kinds[kind] = kinds.get(kind, 0) + 1
        if ref.get("writeToSecondaryTable"):
            direct_writers.append(ref)
        if kind == "unknown":
            unresolved.append(ref)
    return {
        "baseVaHex": secondary.get("baseVaHex") or hex32(SECONDARY_TABLE),
        "rangeHex": secondary.get("rangeHex") or f"{hex32(SECONDARY_TABLE)}..{hex32(SECONDARY_TABLE + 11)}",
        "slots": secondary.get("slots"),
        "refCount": len(refs),
        "refKindCounts": dict(sorted(kinds.items())),
        "directIndexedWriterCount": len(direct_writers),
        "unresolvedRefCount": len(unresolved),
        "refs": refs,
    }


def build_summary(
    exe: bytes,
    branch_state: dict,
    secondary_state_sources: dict,
    predecessor_tail_reset: dict,
    persistence_gap: dict,
) -> dict:
    sections = read_sections(exe)
    secondary = secondary_table_summary(exe, sections, branch_state)
    helper_calls = call_refs(exe, sections, HELPER_VA)
    helper_only_opcode10 = len(helper_calls) == 1 and all(row["insideOpcode10Handler"] for row in helper_calls)
    current_before = secondary_state_sources.get("validBeforeFrontierCount")
    current_after = secondary_state_sources.get("validAfterFrontierCount")
    tail_valid = predecessor_tail_reset.get("tailValidSecondaryFillCount")
    no_direct_global_writer = secondary["directIndexedWriterCount"] == 0 and secondary["unresolvedRefCount"] == 0
    no_known_pre_frontier_reset = (
        no_direct_global_writer
        and helper_only_opcode10
        and current_before == 0
        and tail_valid == 0
    )
    conclusion = (
        "The secondaryBranchState table has no direct indexed writes in .text; its direct references are table-base "
        "loads, stack-local table pointer loads, and indexed reads. Helper 0x00410c90 is reached by a single direct "
        "call inside the opcode 0x10 handler, so secondary resets/overwrites are driven by script opcode 0x10 rows. "
        "The current root has no valid secondary opcode 0x10 fill before the 0x00542b0c blocker and the 1:0 tail has "
        "no post-fill secondary fill. This narrows reset risk to unproven runtime order or still-untraced bytecode paths; "
        "it still does not promote map1_01a->map2_02d without a strict source hotspot."
    )
    return {
        "secondaryBranchState": secondary,
        "helper": {
            "helperVaHex": hex32(HELPER_VA),
            "opcode10HandlerHex": hex32(OPCODE10_HANDLER),
            "directCallCount": len(helper_calls),
            "directCalls": helper_calls,
            "onlyDirectCallInsideOpcode10Handler": helper_only_opcode10,
        },
        "routeScope": {
            "currentRootHex": secondary_state_sources.get("rootRangeHex", "").split("..", 1)[0],
            "frontierReaderHex": secondary_state_sources.get("frontierReaderHex"),
            "currentRootValidBeforeFrontierFillCount": current_before,
            "currentRootValidAfterFrontierFillCount": current_after,
            "predecessorRootHex": predecessor_tail_reset.get("predecessorRootHex"),
            "predecessorTailValidSecondaryFillCount": tail_valid,
            "predecessorLocalTailResetFound": predecessor_tail_reset.get("localTailResetFound"),
            "persistenceProven": persistence_gap.get("persistenceProven"),
            "strictHotspotFound": persistence_gap.get("strictHotspotFound"),
        },
        "noDirectGlobalSecondaryWriter": no_direct_global_writer,
        "noKnownPreFrontierResetInScopedEvidence": no_known_pre_frontier_reset,
        "globalResetRuledOut": False,
        "promotionStatus": "blocked",
        "remainingProofs": [
            "prove selector 1:0 executes before selector 2:0 in the normal runtime path",
            "prove no untraced bytecode path runs another secondary opcode 0x10 fill between 1:0 and the current reader",
            "find strict map1_01a source coordinate or hotspot",
        ],
        "conclusion": conclusion,
    }


def format_counts(counts: dict[str, int]) -> str:
    return ", ".join(f"{key}:{value}" for key, value in counts.items()) or "-"


def markdown(summary: dict) -> str:
    secondary = summary["secondaryBranchState"]
    helper = summary["helper"]
    route = summary["routeScope"]
    lines = [
        "# Save Selector Secondary Reset Scope",
        "",
        f"- secondaryBranchState: `{secondary['rangeHex']}`",
        f"- secondary refs: {secondary['refCount']} ({format_counts(secondary['refKindCounts'])})",
        f"- direct secondary indexed writers: {secondary['directIndexedWriterCount']}",
        f"- unresolved secondary refs: {secondary['unresolvedRefCount']}",
        f"- helper: `{helper['helperVaHex']}` direct calls {helper['directCallCount']}",
        f"- helper only called inside opcode 0x10 handler: {helper['onlyDirectCallInsideOpcode10Handler']}",
        f"- current valid before-frontier secondary fills: {route['currentRootValidBeforeFrontierFillCount']}",
        f"- current valid after-frontier secondary fills: {route['currentRootValidAfterFrontierFillCount']}",
        f"- predecessor tail valid secondary fills: {route['predecessorTailValidSecondaryFillCount']}",
        f"- no direct global secondary writer: {summary['noDirectGlobalSecondaryWriter']}",
        f"- no known pre-frontier reset in scoped evidence: {summary['noKnownPreFrontierResetInScopedEvidence']}",
        f"- global reset ruled out: {summary['globalResetRuledOut']}",
        f"- promotion status: {summary['promotionStatus']}",
        "",
        summary["conclusion"],
        "",
        "## Helper Direct Calls",
        "",
        "| call | target | inside opcode10 handler |",
        "| --- | --- | --- |",
    ]
    for row in helper["directCalls"]:
        lines.append(
            f"| `{row['callVaHex']}` | `{row['targetVaHex']}` | {row['insideOpcode10Handler']} |"
        )
    lines.extend([
        "",
        "## Secondary Table Refs",
        "",
        "| ref | kind | writes table | meaning |",
        "| --- | --- | --- | --- |",
    ])
    for row in secondary["refs"]:
        lines.append(
            f"| `{row['refVaHex']}` | {row.get('kind') or '-'} | {row.get('writeToSecondaryTable')} | "
            f"{row.get('meaning') or '-'} |"
        )
    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:
    secondary = summary["secondaryBranchState"]
    helper = summary["helper"]
    route = summary["routeScope"]
    call_rows = "\n".join(
        "<tr>"
        f"<td><code>{html.escape(row['callVaHex'])}</code></td>"
        f"<td><code>{html.escape(row['targetVaHex'])}</code></td>"
        f"<td>{row['insideOpcode10Handler']}</td>"
        "</tr>"
        for row in helper["directCalls"]
    )
    ref_rows = "\n".join(
        "<tr>"
        f"<td><code>{html.escape(row['refVaHex'])}</code></td>"
        f"<td>{html.escape(row.get('kind') or '-')}</td>"
        f"<td>{html.escape(str(row.get('writeToSecondaryTable')))}</td>"
        f"<td>{html.escape(row.get('meaning') or '-')}</td>"
        "</tr>"
        for row in secondary["refs"]
    )
    proofs = "".join(f"<li>{html.escape(item)}</li>" for item in summary["remainingProofs"])
    return "\n".join([
        "<!doctype html><meta charset=\"utf-8\"><title>Save Selector Secondary Reset Scope</title>",
        "<style>body{font-family:system-ui,sans-serif;background:#111;color:#eee;max-width:1100px;margin:24px auto}table{border-collapse:collapse;width:100%;margin:18px 0}td,th{border:1px solid #444;padding:6px 8px;vertical-align:top}code{color:#9bd4ff}</style>",
        "<h1>Save Selector Secondary Reset Scope</h1>",
        f"<p>secondaryBranchState <code>{html.escape(secondary['rangeHex'])}</code>; refs {secondary['refCount']} ({html.escape(format_counts(secondary['refKindCounts']))}); direct secondary indexed writers: {secondary['directIndexedWriterCount']}; unresolved refs: {secondary['unresolvedRefCount']}.</p>",
        f"<p>helper <code>{helper['helperVaHex']}</code>; direct calls {helper['directCallCount']}; helper only called inside opcode 0x10 handler: {helper['onlyDirectCallInsideOpcode10Handler']}.</p>",
        f"<p>current before-frontier fills: {route['currentRootValidBeforeFrontierFillCount']}; current after-frontier fills: {route['currentRootValidAfterFrontierFillCount']}; predecessor tail valid fills: {route['predecessorTailValidSecondaryFillCount']}.</p>",
        f"<p>no direct global secondary writer: {summary['noDirectGlobalSecondaryWriter']}; no known pre-frontier reset in scoped evidence: {summary['noKnownPreFrontierResetInScopedEvidence']}; global reset ruled out: {summary['globalResetRuledOut']}; promotion status: {html.escape(summary['promotionStatus'])}</p>",
        f"<p>{html.escape(summary['conclusion'])}</p>",
        "<h2>Helper Direct Calls</h2><table><thead><tr><th>call</th><th>target</th><th>inside opcode10 handler</th></tr></thead><tbody>",
        call_rows,
        "</tbody></table>",
        "<h2>Secondary Table Refs</h2><table><thead><tr><th>ref</th><th>kind</th><th>writes table</th><th>meaning</th></tr></thead><tbody>",
        ref_rows,
        "</tbody></table>",
        f"<h2>Remaining Proofs</h2><ul>{proofs}</ul>",
    ])


def write_outputs(summary: dict, out_dir: Path) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "save_selector_secondary_reset_scope.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    (out_dir / "save_selector_secondary_reset_scope.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("--branch-state", type=Path, default=OUT / "save_selector_branch_state.json")
    parser.add_argument("--secondary-state-sources", type=Path, default=OUT / "save_selector_secondary_state_sources.json")
    parser.add_argument("--tail-reset", type=Path, default=OUT / "save_selector_predecessor_tail_reset.json")
    parser.add_argument("--persistence-gap", type=Path, default=OUT / "save_selector_predecessor_persistence_gap.json")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    args = parser.parse_args()
    summary = build_summary(
        args.exe.read_bytes(),
        json.loads(args.branch_state.read_text(encoding="utf-8")),
        json.loads(args.secondary_state_sources.read_text(encoding="utf-8")),
        json.loads(args.tail_reset.read_text(encoding="utf-8")),
        json.loads(args.persistence_gap.read_text(encoding="utf-8")),
    )
    write_outputs(summary, args.out_dir)
    print(f"wrote secondary reset scope -> {args.out_dir / 'save_selector_secondary_reset_scope.html'}")


if __name__ == "__main__":
    main()
