#!/usr/bin/env python3
"""Summarize branch path contents for save-selector frontier streams."""
from __future__ import annotations

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

from probe_exe_scene_tables import find_cns_strings, read_sections, va_to_offset
from summarize_save_selector_frontier_branches import branch_target_kind


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


def dword_at(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 scan_path(exe: bytes, sections: list[dict], strings: dict[int, str], start_va_hex: str | None, count: int) -> dict:
    if not start_va_hex:
        return {"startVaHex": None, "words": [], "linkedCns": [], "fieldRecords": []}
    start_va = int(start_va_hex, 16)
    words = []
    linked = []
    fields = []
    if start_va in strings:
        linked.append(strings[start_va])
    for index in range(count):
        va = start_va + index * 4
        value = dword_at(exe, sections, va)
        if value is None:
            break
        item = {
            "index": index,
            "vaHex": f"0x{va:08x}",
            "valueHex": f"0x{value:08x}",
            "u16Lo": value & 0xFFFF,
            "u16Hi": value >> 16,
        }
        cns = strings.get(value)
        if cns:
            item["cns"] = cns
            if cns not in linked:
                linked.append(cns)
            if cns.startswith("map") and not cns.startswith("map_"):
                scene_id = dword_at(exe, sections, va + 4)
                fields.append({
                    "recordVaHex": item["vaHex"],
                    "filename": cns,
                    "map": cns[:-4],
                    "sceneIdHex": f"0x{scene_id:04x}" if scene_id is not None else None,
                })
        elif va_to_offset(sections, value) is not None:
            item["pointer"] = True
        words.append(item)
    return {
        "startVaHex": start_va_hex,
        "words": words,
        "linkedCns": linked,
        "fieldRecords": fields,
    }


def build_rows(
    exe: bytes,
    frontier_branches: list[dict],
    leaf_streams: list[dict],
    scan_words: int = 48,
) -> list[dict]:
    sections = read_sections(exe)
    strings = find_cns_strings(exe, sections)
    leaves_by_pointer = {leaf.get("leafPointerHex"): leaf for leaf in leaf_streams}
    rows = []
    for edge in frontier_branches:
        for step in edge.get("branchSteps") or []:
            leaf = leaves_by_pointer.get(step.get("leafPointerHex")) or {}
            fallthrough = scan_path(exe, sections, strings, step.get("fallthroughVaHex"), scan_words)
            branch_target = scan_path(exe, sections, strings, step.get("branchTargetHex"), 16)
            source_maps = {edge.get("source")}
            target_maps = {edge.get("target")}
            fallthrough_maps = {record["map"] for record in fallthrough["fieldRecords"]}
            rows.append({
                "source": edge.get("source"),
                "target": edge.get("target"),
                "leafPointerHex": step.get("leafPointerHex"),
                "streamVaHex": step.get("streamVaHex"),
                "condition": step.get("condition"),
                "truePath": "fallthrough",
                "falsePath": "branchTarget",
                "fallthroughVaHex": step.get("fallthroughVaHex"),
                "branchTargetHex": step.get("branchTargetHex"),
                "branchTargetKind": branch_target_kind(leaf, step.get("branchTargetHex")),
                "fallthroughLinkedCns": fallthrough["linkedCns"],
                "fallthroughFieldRecords": fallthrough["fieldRecords"],
                "branchTargetLinkedCns": branch_target["linkedCns"],
                "branchTargetFieldRecords": branch_target["fieldRecords"],
                "fallthroughContainsSource": bool(source_maps & fallthrough_maps),
                "fallthroughContainsTarget": bool(target_maps & fallthrough_maps),
                "sourceRecordTilesets": edge.get("sourceRecordTilesets") or [],
                "targetRecordTilesets": edge.get("targetRecordTilesets") or [],
                "sourceAcceptedRender": edge.get("sourceAcceptedRender") or {},
                "targetAcceptedRender": edge.get("targetAcceptedRender") or {},
                "note": (
                    "The fallthrough path contains scene records, but this is still not a tile transition. "
                    "The missing piece is the runtime branch-state value and a source hotspot/spawn."
                ),
            })
    return rows


def markdown(rows: list[dict]) -> str:
    lines = [
        "# Save Selector Branch Paths",
        "",
        "Branch-path scan for save-selector frontier streams. A path containing map records is a script-resource path, not a confirmed tile hotspot.",
        "",
        "| source | target | condition | true path records | false path | render mismatch | note |",
        "| --- | --- | --- | --- | --- | --- | --- |",
    ]
    for row in rows:
        true_records = ", ".join(
            f"{item['map']} {item.get('sceneIdHex')}"
            for item in row.get("fallthroughFieldRecords") or []
        ) or "-"
        false_records = ", ".join(row.get("branchTargetLinkedCns") or []) or row.get("branchTargetKind") or "-"
        mismatch = (
            f"record {','.join(row.get('sourceRecordTilesets') or []) or '-'}; "
            f"accepted {','.join((row.get('sourceAcceptedRender') or {}).get('tilesets') or []) or '-'}"
        )
        lines.append(
            f"| {row.get('source')} | {row.get('target')} | `{row.get('condition')}` | "
            f"{row.get('fallthroughVaHex')} -> {true_records} | "
            f"{row.get('branchTargetHex')} -> {false_records} | {mismatch} | {row.get('note')} |"
        )
    if not rows:
        lines.append("| - | - | - | - | - | - | - |")
    lines.append("")
    return "\n".join(lines)


def html_page(rows: list[dict]) -> str:
    body = []
    for row in rows:
        true_records = "<br>".join(
            html.escape(f"{item['recordVaHex']} {item['map']} {item.get('sceneIdHex')}")
            for item in row.get("fallthroughFieldRecords") or []
        ) or "-"
        false_records = html.escape(", ".join(row.get("branchTargetLinkedCns") or []) or row.get("branchTargetKind") or "-")
        mismatch = (
            f"record <code>{html.escape(','.join(row.get('sourceRecordTilesets') or []) or '-')}</code><br>"
            f"accepted <code>{html.escape(','.join((row.get('sourceAcceptedRender') or {}).get('tilesets') or []) or '-')}</code>"
        )
        body.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(row.get('condition') or '-')}</code></td>"
            f"<td><code>{html.escape(row.get('fallthroughVaHex') or '-')}</code><br>{true_records}</td>"
            f"<td><code>{html.escape(row.get('branchTargetHex') or '-')}</code><br>{false_records}</td>"
            f"<td>{mismatch}</td>"
            f"<td>{html.escape(row.get('note') or '')}</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 Branch Paths</title>",
        "  <style>",
        "    body { margin: 24px; background: #101010; color: #eee; font: 14px system-ui, sans-serif; }",
        "    table { border-collapse: collapse; width: 100%; }",
        "    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 Branch Paths</h1>",
        "  <p>Branch-path scan for save-selector frontier streams. A path containing map records is a script-resource path, not a confirmed tile hotspot.</p>",
        "  <table><thead><tr><th>source</th><th>target</th><th>condition</th><th>true/fallthrough path</th><th>false/branch path</th><th>render mismatch</th><th>note</th></tr></thead>",
        f"  <tbody>{''.join(body) or '<tr><td colspan=\"7\">No rows.</td></tr>'}</tbody></table>",
        "</body>",
        "</html>",
        "",
    ])


def write_outputs(rows: list[dict], out_dir: Path = OUT) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "save_selector_branch_paths.json").write_text(
        json.dumps(rows, 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("--frontier-branches", type=Path, default=OUT / "save_selector_frontier_branches.json")
    parser.add_argument("--leaf-streams", type=Path, default=OUT / "save_selector_leaf_streams.json")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    args = parser.parse_args()
    rows = build_rows(
        args.exe.read_bytes(),
        json.loads(args.frontier_branches.read_text(encoding="utf-8")),
        json.loads(args.leaf_streams.read_text(encoding="utf-8")),
    )
    write_outputs(rows, args.out_dir)
    print(f"wrote {len(rows)} save selector branch path rows -> {args.out_dir / 'save_selector_branch_paths.json'}")


if __name__ == "__main__":
    main()
