#!/usr/bin/env python3
"""Summarize save-selector frontier leaf stream contents."""
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


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


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


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 dword_rows(exe: bytes, sections: list[dict], strings: dict[int, str], va: int, count: int) -> list[dict]:
    rows = []
    for index in range(count):
        item_va = va + index * 4
        value = dword_at(exe, sections, item_va)
        if value is None:
            break
        item = {
            "index": index,
            "vaHex": f"0x{item_va:08x}",
            "valueHex": f"0x{value:08x}",
            "u16Lo": value & 0xffff,
            "u16Hi": value >> 16,
        }
        if value in strings:
            item["cns"] = strings[value]
            if strings[value].startswith("map") and not strings[value].startswith("map_"):
                scene_id = dword_at(exe, sections, item_va + 4)
                item["fieldMapRecordStart"] = True
                item["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
        rows.append(item)
    return rows


def linked_cns(rows: list[dict]) -> list[str]:
    names = []
    for row in rows:
        name = row.get("cns")
        if name and name not in names:
            names.append(name)
    return names


def field_records(rows: list[dict]) -> list[dict]:
    result = []
    for row in rows:
        name = row.get("cns")
        if name and name.startswith("map") and not name.startswith("map_"):
            result.append({
                "recordVaHex": row["vaHex"],
                "filename": name,
                "map": name[:-4],
                "sceneIdHex": row.get("sceneIdHex"),
            })
    return result


def build_rows(exe: bytes, frontier_rows: list[dict], word_count: int = 80) -> list[dict]:
    sections = read_sections(exe)
    strings = find_cns_strings(exe, sections)
    rows = []
    for frontier in frontier_rows:
        for pair in frontier.get("scenePairs") or []:
            leaf_va = parse_hex(pair.get("leafPointerHex"))
            if leaf_va is None:
                continue
            words = dword_rows(exe, sections, strings, leaf_va, word_count)
            nested = words[1]["valueHex"] if len(words) > 1 and words[0]["valueHex"] == "0x0000003f" else None
            nested_words = []
            if nested and parse_hex(nested) is not None:
                nested_words = dword_rows(exe, sections, strings, parse_hex(nested), 16)
            rows.append({
                "source": frontier["source"],
                "target": frontier["target"],
                "selector": pair.get("selector"),
                "leafPointerHex": pair.get("leafPointerHex"),
                "nestedPointerHex": nested,
                "sourceRecordVaHex": pair.get("sourceRecordVaHex"),
                "targetRecordVaHex": pair.get("targetRecordVaHex"),
                "linkedCns": linked_cns(words),
                "fieldRecords": field_records(words),
                "nestedLinkedCns": linked_cns(nested_words),
                "nestedFieldRecords": field_records(nested_words),
                "words": words,
                "nestedWords": nested_words,
            })
    return rows


def markdown(rows: list[dict]) -> str:
    lines = [
        "# Save Selector Leaf Streams",
        "",
        "Leaf stream summaries for save-selector frontier candidates. These streams are script/event data candidates, not confirmed tile transitions.",
        "",
        f"Leaf streams: {len(rows)}.",
        "",
        "| leaf | nested | source | target | field records in leaf | nested field records | linked CNS |",
        "| --- | --- | --- | --- | --- | --- | --- |",
    ]
    for row in rows:
        fields = ", ".join(f"{item['map']} {item.get('sceneIdHex')}" for item in row["fieldRecords"]) or "-"
        nested_fields = ", ".join(f"{item['map']} {item.get('sceneIdHex')}" for item in row["nestedFieldRecords"]) or "-"
        lines.append(
            f"| {row['leafPointerHex']} | {row.get('nestedPointerHex') or '-'} | {row['sourceRecordVaHex']} | "
            f"{row['targetRecordVaHex']} | {fields} | {nested_fields} | {', '.join(row['linkedCns']) or '-'} |"
        )
    lines.append("")
    return "\n".join(lines)


def html_page(rows: list[dict]) -> str:
    body_rows = []
    for row in rows:
        fields = "<br>".join(
            html.escape(f"{item['recordVaHex']} {item['map']} {item.get('sceneIdHex')}")
            for item in row["fieldRecords"]
        ) or "-"
        nested_fields = "<br>".join(
            html.escape(f"{item['recordVaHex']} {item['map']} {item.get('sceneIdHex')}")
            for item in row["nestedFieldRecords"]
        ) or "-"
        body_rows.append(
            "<tr>"
            f"<td>{html.escape(row['leafPointerHex'])}</td>"
            f"<td>{html.escape(row.get('nestedPointerHex') or '-')}</td>"
            f"<td>{html.escape(row['sourceRecordVaHex'] or '-')}</td>"
            f"<td>{html.escape(row['targetRecordVaHex'] or '-')}</td>"
            f"<td>{fields}</td>"
            f"<td>{nested_fields}</td>"
            f"<td>{html.escape(', '.join(row['linkedCns']) 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 Leaf Streams</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; }",
        "  </style>",
        "</head>",
        "<body>",
        "  <h1>Save Selector Leaf Streams</h1>",
        "  <p>Leaf stream summaries for save-selector frontier candidates. These streams are script/event data candidates, not confirmed tile transitions.</p>",
        f"  <p>Leaf streams: {len(rows)}.</p>",
        "  <table>",
        "    <thead><tr><th>leaf</th><th>nested</th><th>source record</th><th>target record</th><th>field records in leaf</th><th>nested field records</th><th>linked CNS</th></tr></thead>",
        "    <tbody>",
        "\n".join(body_rows) or '<tr><td colspan="7">No leaf streams.</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_leaf_streams.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", type=Path, default=OUT / "save_selector_frontier.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.read_text(encoding="utf-8")),
    )
    write_outputs(rows, args.out_dir)
    print(f"wrote {len(rows)} save selector leaf stream rows -> {args.out_dir / 'save_selector_leaf_streams.json'}")


if __name__ == "__main__":
    main()
