#!/usr/bin/env python3
"""Summarize why the current save-selector frontier is a scene/resource list."""
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 find_cns_strings, read_sections, va_to_offset
from summarize_script_handler_table import HANDLER_TABLE_VA, dword_at, section_name_for_va


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"
FAILED_SCENE_LIST_GATE_IDS = [
    "resource-branch-not-field-map",
    "selector-adjacency-only",
    "strict-hotspot-missing",
]
SCENE_LIST_MISSING_EVIDENCE = [
    "direct field-map branch target for map1_01a -> map2_02d",
    "strict or confirmed transition backing selector scene adjacency",
    "strict map1_01a source coordinate or hotspot",
]
SCENE_LIST_REMAINING_PROOFS = [
    "prove the 0x00542b0c branch targets a field-map transition, not a CNS resource gate",
    "back selector scene-record adjacency with a strict or confirmed transition",
    "find a strict map1_01a source coordinate or hotspot",
]
SCENE_LIST_EVIDENCE_REFS = [
    {
        "path": "Hwanse2.exe",
        "fields": [".text", ".rdata", ".data"],
    },
    {
        "path": "out/save_selector_frontier_branches.json",
        "fields": ["branchSteps", "source", "target"],
    },
    {
        "path": "out/scene_manifest.json",
        "fields": ["map", "recordVa", "sceneIdHex", "tilesets"],
    },
]


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


def hex32(value: int | None) -> str | None:
    return f"0x{value:08x}" if value is not None else None


def read_u32(exe: bytes, sections: list[dict], va: int) -> int | 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 value_kind(
    exe: bytes,
    sections: list[dict],
    strings: dict[int, str],
    value: int | None,
) -> dict:
    if value is None:
        return {"kind": "unreadable"}
    if value in strings:
        name = strings[value]
        return {
            "kind": "cns",
            "cns": name,
            "isFieldMap": name.startswith("map") and name.endswith(".cns") and not name.startswith("map_"),
            "isResource": not (name.startswith("map") and name.endswith(".cns") and not name.startswith("map_")),
        }
    if va_to_offset(sections, value) is not None:
        return {"kind": "pointer", "targetVaHex": hex32(value)}
    return {"kind": "scalar"}


def handler_kind(exe: bytes, sections: list[dict], opcode: int) -> dict:
    entry_va = HANDLER_TABLE_VA + opcode * 4
    handler_va = dword_at(exe, sections, entry_va)
    section = section_name_for_va(sections, handler_va) if handler_va is not None else None
    return {
        "opcodeHex": f"0x{opcode:02x}",
        "entryVaHex": hex32(entry_va),
        "handlerVaHex": hex32(handler_va),
        "handlerSection": section,
        "isCodeHandler": section == ".text",
    }


def manifest_records_for(manifest: list[dict], map_name: str) -> list[dict]:
    rows = []
    for row in manifest:
        if row.get("map") != map_name:
            continue
        record_va = row.get("recordVa")
        if not isinstance(record_va, int):
            continue
        rows.append({
            "recordVa": record_va,
            "recordVaHex": hex32(record_va),
            "map": row.get("map"),
            "sceneIdHex": row.get("sceneIdHex"),
            "tilesets": row.get("tilesets") or [],
            "resourceSource": row.get("resourceSource"),
        })
    return sorted(rows, key=lambda item: item["recordVa"])


def nearest_record_after(records: list[dict], va: int | None, limit: int = 0x200) -> dict | None:
    if va is None:
        return None
    candidates = [
        {**record, "distanceHex": f"0x{record['recordVa'] - va:x}"}
        for record in records
        if record["recordVa"] >= va and record["recordVa"] - va <= limit
    ]
    return candidates[0] if candidates else None


def dword_window(
    exe: bytes,
    sections: list[dict],
    strings: dict[int, str],
    start: int,
    end: int,
) -> list[dict]:
    rows = []
    for va in range(start, end, 4):
        value = read_u32(exe, sections, va)
        if value is None:
            continue
        row = {
            "vaHex": hex32(va),
            "valueHex": hex32(value),
            "lowByteHex": f"0x{value & 0xff:02x}",
            "valueKind": value_kind(exe, sections, strings, value),
        }
        if (value & 0xff) <= 0xff:
            row["handlerCandidate"] = handler_kind(exe, sections, value & 0xff)
        rows.append(row)
    return rows


def branch_context(
    exe: bytes,
    sections: list[dict],
    strings: dict[int, str],
    edge: dict,
    manifest: list[dict],
) -> dict:
    source_records = manifest_records_for(manifest, edge.get("source"))
    target_records = manifest_records_for(manifest, edge.get("target"))
    steps = []
    for step in edge.get("branchSteps") or []:
        branch_va = parse_hex(step.get("streamVaHex"))
        fallthrough_va = parse_hex(step.get("fallthroughVaHex"))
        target_va = parse_hex(step.get("branchTargetHex"))
        fallthrough_value = read_u32(exe, sections, fallthrough_va) if fallthrough_va is not None else None
        target_kind = value_kind(exe, sections, strings, target_va)
        fallthrough_kind = value_kind(exe, sections, strings, fallthrough_value)
        fallthrough_handler = handler_kind(exe, sections, fallthrough_value & 0xff) if fallthrough_value is not None else {}
        source_record = nearest_record_after(source_records, branch_va)
        target_record = nearest_record_after(target_records, branch_va)
        steps.append({
            **step,
            "branchTargetValueKind": target_kind,
            "branchTargetIsResource": target_kind.get("kind") == "cns" and target_kind.get("isResource") is True,
            "fallthroughValueHex": hex32(fallthrough_value),
            "fallthroughValueKind": fallthrough_kind,
            "fallthroughHandlerCandidate": fallthrough_handler,
            "fallthroughLooksExecutable": fallthrough_handler.get("isCodeHandler") is True,
            "nearestSourceRecordAfterBranch": source_record,
            "nearestTargetRecordAfterBranch": target_record,
            "classification": (
                "resource-gate-before-scene-record"
                if target_kind.get("kind") == "cns"
                and target_kind.get("isResource") is True
                and source_record
                else "unresolved"
            ),
        })
    window_start = min(
        [parse_hex(step.get("streamVaHex")) for step in edge.get("branchSteps") or [] if parse_hex(step.get("streamVaHex")) is not None]
        or [source_records[0]["recordVa"] if source_records else 0]
    )
    first_target_record = target_records[0]["recordVa"] if target_records else window_start + 0x80
    window_end = min(max(first_target_record + 0x50, window_start + 0x80), window_start + 0x180)
    return {
        "source": edge.get("source"),
        "target": edge.get("target"),
        "proofFound": False,
        "sceneListResourceGateProofFound": False,
        "strictTransitionProofFound": False,
        "failedSceneListGateIds": FAILED_SCENE_LIST_GATE_IDS,
        "missingEvidence": SCENE_LIST_MISSING_EVIDENCE,
        "remainingProofs": SCENE_LIST_REMAINING_PROOFS,
        "evidenceRefs": SCENE_LIST_EVIDENCE_REFS,
        "evidenceRefCount": len(SCENE_LIST_EVIDENCE_REFS),
        "promotionStatus": "blocked",
        "selectorOnlySceneList": True,
        "branchSteps": steps,
        "sourceRecords": source_records,
        "targetRecords": target_records,
        "window": dword_window(exe, sections, strings, window_start, window_end),
        "conclusion": (
            "The save-selector branch around the current frontier gates CNS resource loading before field-map "
            "scene records. For map1_01a -> map2_02d, the 0x00542b0c branch target is cara_01.cns, not the "
            "map2_02d field map. map2_02d appears later as another scene record in the same selector list, so "
            "this remains scene-list evidence, not a tile transition."
        ),
    }


def build_summary(
    exe: bytes,
    frontier_branches: list[dict],
    manifest: list[dict],
) -> dict:
    sections = read_sections(exe)
    strings = find_cns_strings(exe, sections)
    rows = [
        branch_context(exe, sections, strings, edge, manifest)
        for edge in frontier_branches
    ]
    current = next(
        (
            row for row in rows
            if row.get("source") == "map1_01a" and row.get("target") == "map2_02d"
        ),
        rows[0] if rows else {},
    )
    return {
        "scope": "save-selector scene-list/resource context for confirmed-route blockers",
        "proofFound": False,
        "sceneListResourceGateProofFound": False,
        "strictTransitionProofFound": False,
        "failedSceneListGateIds": FAILED_SCENE_LIST_GATE_IDS,
        "missingEvidence": SCENE_LIST_MISSING_EVIDENCE,
        "remainingProofs": SCENE_LIST_REMAINING_PROOFS,
        "evidenceRefs": SCENE_LIST_EVIDENCE_REFS,
        "evidenceRefCount": len(SCENE_LIST_EVIDENCE_REFS),
        "rowCount": len(rows),
        "rows": rows,
        "currentFrontier": current,
        "promotionStatus": "blocked",
        "conclusion": (
            "Save-selector field-map adjacency is kept out of normal gameplay because the nearby branches "
            "resolve to resource gates and scene-record lists rather than strict source coordinates."
        ),
    }


def format_record(record: dict | None) -> str:
    if not record:
        return "-"
    tilesets = ",".join(record.get("tilesets") or []) or "-"
    return f"{record.get('recordVaHex')} {record.get('map')} {record.get('sceneIdHex')} {tilesets}"


def markdown(summary: dict) -> str:
    lines = [
        "# Save Selector Scene List Context",
        "",
        summary["conclusion"],
        "",
        "| source | target | branch | target value | fallthrough value | nearest source record | nearest target record | classification |",
        "| --- | --- | --- | --- | --- | --- | --- | --- |",
    ]
    for row in summary.get("rows") or []:
        for step in row.get("branchSteps") or []:
            target_kind = step.get("branchTargetValueKind") or {}
            fall_kind = step.get("fallthroughValueKind") or {}
            target_text = target_kind.get("cns") or target_kind.get("targetVaHex") or target_kind.get("kind")
            fall_text = (
                f"{step.get('fallthroughValueHex')} {fall_kind.get('cns') or fall_kind.get('targetVaHex') or fall_kind.get('kind')}"
            )
            lines.append(
                f"| {row.get('source')} | {row.get('target')} | "
                f"`{step.get('streamVaHex')}` `{step.get('condition')}` | "
                f"`{step.get('branchTargetHex')}` {target_text} | "
                f"{fall_text} | {format_record(step.get('nearestSourceRecordAfterBranch'))} | "
                f"{format_record(step.get('nearestTargetRecordAfterBranch'))} | {step.get('classification')} |"
            )
    if not any(row.get("branchSteps") for row in summary.get("rows") or []):
        lines.append("| - | - | - | - | - | - | - | - |")
    lines.extend(["", "## Current Frontier Window", ""])
    current = summary.get("currentFrontier") or {}
    lines.extend([
        f"- source: `{current.get('source')}`",
        f"- target: `{current.get('target')}`",
        f"- proof found: {current.get('proofFound')}",
        f"- scene-list resource gate proof found: {current.get('sceneListResourceGateProofFound')}",
        f"- strict transition proof found: {current.get('strictTransitionProofFound')}",
        f"- failed scene-list gates: {', '.join(current.get('failedSceneListGateIds') or []) or '-'}",
        f"- missing evidence count: {len(current.get('missingEvidence') or [])}",
        f"- evidence refs: {current.get('evidenceRefCount') or summary.get('evidenceRefCount')}",
        f"- promotion status: `{current.get('promotionStatus')}`",
        f"- selector-only scene list: {current.get('selectorOnlySceneList')}",
        "",
        current.get("conclusion") or "",
        "",
        "## Missing Evidence",
        "",
    ])
    lines.extend(f"- {item}" for item in current.get("missingEvidence") or [])
    lines.extend([
        "",
        "| va | value | low byte | value kind | handler candidate |",
        "| --- | --- | --- | --- | --- |",
    ])
    for word in (current.get("window") or [])[:48]:
        kind = word.get("valueKind") or {}
        handler = word.get("handlerCandidate") or {}
        kind_text = kind.get("cns") or kind.get("targetVaHex") or kind.get("kind")
        handler_text = f"{handler.get('handlerVaHex') or '-'} {handler.get('handlerSection') or '-'}"
        lines.append(
            f"| `{word.get('vaHex')}` | `{word.get('valueHex')}` | `{word.get('lowByteHex')}` | "
            f"{kind_text} | {handler_text} |"
        )
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    rows = []
    for row in summary.get("rows") or []:
        for step in row.get("branchSteps") or []:
            target_kind = step.get("branchTargetValueKind") or {}
            fall_kind = step.get("fallthroughValueKind") or {}
            target_text = target_kind.get("cns") or target_kind.get("targetVaHex") or target_kind.get("kind")
            fall_text = f"{step.get('fallthroughValueHex')} {fall_kind.get('cns') or fall_kind.get('targetVaHex') or fall_kind.get('kind')}"
            rows.append(
                "<tr>"
                f"<td>{html.escape(row.get('source') or '-')}</td>"
                f"<td>{html.escape(row.get('target') or '-')}</td>"
                f"<td><code>{html.escape(step.get('streamVaHex') or '-')}</code><br>{html.escape(step.get('condition') or '-')}</td>"
                f"<td><code>{html.escape(step.get('branchTargetHex') or '-')}</code><br>{html.escape(str(target_text))}</td>"
                f"<td>{html.escape(fall_text)}</td>"
                f"<td>{html.escape(format_record(step.get('nearestSourceRecordAfterBranch')))}</td>"
                f"<td>{html.escape(format_record(step.get('nearestTargetRecordAfterBranch')))}</td>"
                f"<td>{html.escape(step.get('classification') or '-')}</td>"
                "</tr>"
            )
    current = summary.get("currentFrontier") or {}
    failed_gates = "".join(
        f"<li>{html.escape(item)}</li>" for item in current.get("failedSceneListGateIds") or []
    )
    missing = "".join(f"<li>{html.escape(item)}</li>" for item in current.get("missingEvidence") or [])
    window_rows = []
    for word in (current.get("window") or [])[:48]:
        kind = word.get("valueKind") or {}
        handler = word.get("handlerCandidate") or {}
        kind_text = kind.get("cns") or kind.get("targetVaHex") or kind.get("kind")
        handler_text = f"{handler.get('handlerVaHex') or '-'} {handler.get('handlerSection') or '-'}"
        window_rows.append(
            "<tr>"
            f"<td><code>{html.escape(word.get('vaHex') or '-')}</code></td>"
            f"<td><code>{html.escape(word.get('valueHex') or '-')}</code></td>"
            f"<td><code>{html.escape(word.get('lowByteHex') or '-')}</code></td>"
            f"<td>{html.escape(str(kind_text))}</td>"
            f"<td>{html.escape(handler_text)}</td>"
            "</tr>"
        )
    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 Scene List Context</title>",
        "  <style>",
        "    body { margin: 24px; background: #101010; color: #eee; font: 14px system-ui, sans-serif; }",
        "    table { border-collapse: collapse; width: 100%; margin-bottom: 24px; }",
        "    th, td { border: 1px solid #333; padding: 6px 8px; vertical-align: top; }",
        "    th { background: #1d1d1d; position: sticky; top: 0; }",
        "    code { color: #f5d76e; }",
        "  </style>",
        "</head>",
        "<body>",
        "  <h1>Save Selector Scene List Context</h1>",
        f"  <p>{html.escape(summary.get('conclusion') or '')}</p>",
        f"  <p>proofFound <code>{current.get('proofFound')}</code>; sceneListResourceGateProofFound <code>{current.get('sceneListResourceGateProofFound')}</code>; strictTransitionProofFound <code>{current.get('strictTransitionProofFound')}</code>; missingEvidenceCount <code>{len(current.get('missingEvidence') or [])}</code>; evidence refs <code>{current.get('evidenceRefCount') or summary.get('evidenceRefCount')}</code>; promotion status <code>{html.escape(str(current.get('promotionStatus') or summary.get('promotionStatus')))}</code>.</p>",
        f"  <h2>Failed Scene-List Gates</h2><ul>{failed_gates}</ul>",
        f"  <h2>Missing Evidence</h2><ul>{missing}</ul>",
        "  <table><thead><tr><th>source</th><th>target</th><th>branch</th><th>target value</th><th>fallthrough value</th><th>nearest source record</th><th>nearest target record</th><th>classification</th></tr></thead>",
        f"  <tbody>{''.join(rows) or '<tr><td colspan=\"8\">No rows.</td></tr>'}</tbody></table>",
        "  <h2>Current Frontier Window</h2>",
        f"  <p>{html.escape(current.get('conclusion') or '')}</p>",
        "  <table><thead><tr><th>va</th><th>value</th><th>low byte</th><th>value kind</th><th>handler candidate</th></tr></thead>",
        f"  <tbody>{''.join(window_rows) or '<tr><td colspan=\"5\">No rows.</td></tr>'}</tbody></table>",
        "</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_scene_list_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("--frontier-branches", type=Path, default=OUT / "save_selector_frontier_branches.json")
    parser.add_argument("--scene-manifest", type=Path, default=OUT / "scene_manifest.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(),
        json.loads(args.frontier_branches.read_text(encoding="utf-8")),
        json.loads(args.scene_manifest.read_text(encoding="utf-8")),
    )
    json_out = write_outputs(summary, args.out_dir, args.html_out)
    print(f"wrote save selector scene-list context -> {json_out}")


if __name__ == "__main__":
    main()
