#!/usr/bin/env python3
"""Build a browser-readable overview of CNS payload status."""
from __future__ import annotations

import argparse
import html
import json
import re
from collections import Counter
from pathlib import Path


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


def load_json(path: Path, fallback):
    if not path.exists():
        return fallback
    return json.loads(path.read_text(encoding="utf-8"))


def category_for(name: str, kind: str) -> str:
    if re.match(r"map\d+_", name):
        return "field maps"
    if re.match(r"map_[a-z]\d", name):
        return "map tilesets"
    if name.startswith("btl_") and kind == "tilemap":
        return "battle backgrounds"
    if name.startswith("btl_"):
        return "battle sprites"
    if name.startswith("cara_"):
        return "character sprites"
    if name.startswith("face_"):
        return "face sprites"
    if name.startswith(("z", "boss")):
        return "enemy/object sprites"
    return "ui/title/misc images"


def category_description(category: str) -> str:
    descriptions = {
        "field maps": "필드 맵 tilemap. 웹 맵 chunk 와 맵 리뷰에 사용한다.",
        "battle backgrounds": "전투 배경 tilemap. 전투 검토 화면에서 사용한다.",
        "map tilesets": "16x16 타일셋 이미지. 필드/전투 배경 렌더링에 필수다.",
        "character sprites": "아타호/동료/NPC 등 캐릭터 스프라이트 후보.",
        "battle sprites": "전투 화면 캐릭터 스프라이트.",
        "face sprites": "대화 얼굴 리소스.",
        "enemy/object sprites": "적, 오브젝트, 이벤트 스프라이트 후보.",
        "ui/title/misc images": "타이틀, 창, 아이콘, 숫자, 아이템, 상태 UI 등.",
    }
    return descriptions.get(category, "-")


def dimensions(row: dict) -> str:
    width = row.get("width")
    height = row.get("height")
    if isinstance(width, int) and isinstance(height, int):
        return f"{width}x{height}"
    return "-"


def build_summary(cns_payloads: list[dict], cns_text: dict, scene_manifest: list[dict]) -> dict:
    rows = []
    for row in cns_payloads:
        rows.append({
            **row,
            "category": category_for(row.get("name", ""), row.get("kind", "")),
            "dimensions": dimensions(row),
        })
    kind_counts = Counter(row.get("kind", "unknown") for row in rows)
    category_counts = Counter(row["category"] for row in rows)
    tilemap_category_counts = Counter(row["category"] for row in rows if row.get("kind") == "tilemap")
    image_category_counts = Counter(row["category"] for row in rows if row.get("kind") == "image")
    unknown_rows = [row for row in rows if row.get("kind") == "unknown"]
    unique_scene_maps = sorted({row.get("map") for row in scene_manifest if row.get("map")})
    return {
        "scope": "CNS payload overview for browser review.",
        "cnsTotal": len(rows),
        "kindCounts": dict(sorted(kind_counts.items())),
        "categoryCounts": dict(sorted(category_counts.items())),
        "tilemapCategoryCounts": dict(sorted(tilemap_category_counts.items())),
        "imageCategoryCounts": dict(sorted(image_category_counts.items())),
        "unknownCount": len(unknown_rows),
        "decodedFileCount": sum(1 for row in rows if row.get("decodedSize") is not None),
        "sceneRecordCount": len(scene_manifest),
        "sceneManifestMapCount": len(unique_scene_maps),
        "plainCnsTextCandidateCount": cns_text.get("candidateCount", 0),
        "plainCnsTextFilesWithCandidates": cns_text.get("filesWithCandidates", 0),
        "conclusion": (
            "CNS compression, top-level payload classification, field/battle map assembly, and field "
            "collision/occlusion flags are broadly handled: all 377 payloads decode and currently classify "
            "as image or tilemap. The remaining work is semantic: event triggers, dialogue VM behavior, "
            "battle data, and save-point interactions still need EXE/runtime evidence."
        ),
        "completed": [
            "377 CNS files are extracted from GENSE.FLD and decompressed.",
            "Payload classification has no current unknown rows.",
            "177 image payloads convert to palette PNG assets.",
            "174 field map tilemaps and 26 battle background tilemaps are exported for the web runtime/review tools.",
            "Field map layer1 low bits act as directional collision flags; high bits act as foreground occlusion masks.",
            "CNS plain CP949 text scan found no direct dialogue strings, pushing dialogue work toward EXE event streams.",
        ],
        "openQuestions": [
            "Strict map transition hotspot and spawn coordinates.",
            "Full event/object VM semantics, including dialogue and save-point interactions.",
            "Original battle entry, enemy data, and battle formulas.",
            "When each EXE scene tileset variant is selected in normal gameplay.",
        ],
        "relatedReports": [
            {"label": "CNS payload classification", "href": "cns_payloads.json"},
            {"label": "CNS text candidate scan", "href": "cns_text_candidates.html"},
            {"label": "Map tileset candidates", "href": "map_tileset_candidates.md"},
            {"label": "Scene manifest JSON", "href": "scene_manifest.json"},
            {"label": "Map review", "href": "../web/map_review.html"},
            {"label": "Map gallery", "href": "../web/map_gallery.html"},
            {"label": "Battle background review", "href": "../web/battle_background_review.html"},
            {"label": "CNS format notes", "href": "../docs/CNS_FORMAT.md"},
            {"label": "CNS overview notes", "href": "../docs/CNS_OVERVIEW.md"},
        ],
        "rows": rows,
    }


def category_rows(summary: dict) -> list[dict]:
    counts = summary.get("categoryCounts") or {}
    return [
        {
            "category": category,
            "count": count,
            "description": category_description(category),
        }
        for category, count in sorted(counts.items(), key=lambda item: (-item[1], item[0]))
    ]


def markdown(summary: dict) -> str:
    lines = [
        "# CNS Overview",
        "",
        summary["conclusion"],
        "",
        "## Counts",
        "",
        f"- CNS files: {summary['cnsTotal']}",
        f"- decoded files: {summary['decodedFileCount']}",
        f"- payload kinds: {json.dumps(summary['kindCounts'], ensure_ascii=False)}",
        f"- unknown payloads: {summary['unknownCount']}",
        f"- scene manifest records: {summary['sceneRecordCount']}",
        f"- CNS plain text candidates: {summary['plainCnsTextCandidateCount']}",
        "",
        "## Category Counts",
        "",
        "| category | count | meaning |",
        "| --- | ---: | --- |",
    ]
    for row in category_rows(summary):
        lines.append(f"| {row['category']} | {row['count']} | {row['description']} |")
    lines.extend([
        "",
        "## Completed",
        "",
    ])
    lines.extend(f"- {item}" for item in summary["completed"])
    lines.extend([
        "",
        "## Open Questions",
        "",
    ])
    lines.extend(f"- {item}" for item in summary["openQuestions"])
    lines.extend([
        "",
        "## Related Reports",
        "",
    ])
    lines.extend(f"- [{row['label']}]({row['href']})" for row in summary["relatedReports"])
    lines.extend([
        "",
        "## CNS Files",
        "",
        "| file | kind | category | size | decoded bytes | details |",
        "| --- | --- | --- | ---: | ---: | --- |",
    ])
    for row in summary["rows"]:
        detail = []
        for key in ["bpp", "paletteColors", "tileCount", "headerBytes"]:
            if row.get(key) is not None:
                detail.append(f"{key}={row[key]}")
        lines.append(
            f"| {row.get('name')} | {row.get('kind')} | {row.get('category')} | "
            f"{row.get('dimensions')} | {row.get('decodedSize') or '-'} | {', '.join(detail) or '-'} |"
        )
    return "\n".join(lines) + "\n"


def html_page(summary: dict) -> str:
    cards = [
        ("CNS files", summary["cnsTotal"]),
        ("decoded", summary["decodedFileCount"]),
        ("images", summary["kindCounts"].get("image", 0)),
        ("tilemaps", summary["kindCounts"].get("tilemap", 0)),
        ("unknown", summary["unknownCount"]),
        ("scene records", summary["sceneRecordCount"]),
        ("plain text hits", summary["plainCnsTextCandidateCount"]),
    ]
    category_table = "".join(
        "<tr>"
        f"<td>{html.escape(row['category'])}</td>"
        f"<td>{row['count']}</td>"
        f"<td>{html.escape(row['description'])}</td>"
        "</tr>"
        for row in category_rows(summary)
    )
    cns_rows = []
    for row in summary["rows"]:
        details = []
        for key in ["bpp", "paletteColors", "tileCount", "headerBytes"]:
            if row.get(key) is not None:
                details.append(f"{key}={row[key]}")
        cns_rows.append(
            "<tr>"
            f"<td><code>{html.escape(row.get('name', ''))}</code></td>"
            f"<td>{html.escape(row.get('kind', ''))}</td>"
            f"<td>{html.escape(row.get('category', ''))}</td>"
            f"<td>{html.escape(row.get('dimensions', '-'))}</td>"
            f"<td>{row.get('decodedSize') or '-'}</td>"
            f"<td>{html.escape(', '.join(details) or '-')}</td>"
            "</tr>"
        )
    related = "".join(
        f"<li><a href=\"{html.escape(row['href'])}\">{html.escape(row['label'])}</a></li>"
        for row in summary["relatedReports"]
    )
    completed = "".join(f"<li>{html.escape(item)}</li>" for item in summary["completed"])
    open_questions = "".join(f"<li>{html.escape(item)}</li>" for item in summary["openQuestions"])
    card_html = "".join(
        f"<div class=\"card\"><strong>{value}</strong><span>{html.escape(label)}</span></div>"
        for label, value in cards
    )
    return "\n".join([
        "<!doctype html>",
        '<html lang="ko">',
        "<head>",
        '  <meta charset="utf-8">',
        '  <meta name="viewport" content="width=device-width, initial-scale=1">',
        "  <title>CNS Overview</title>",
        "  <style>",
        "    body{font-family:system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#101010;color:#eee;margin:24px;line-height:1.5}",
        "    a{color:#8ab4f8} code{color:#f5d76e} .cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:10px;margin:16px 0 22px}",
        "    .card{border:1px solid #333;background:#171717;padding:10px;border-radius:6px}.card strong{display:block;font-size:22px}.card span{color:#bbb}",
        "    table{border-collapse:collapse;width:100%;margin:12px 0 24px}td,th{border:1px solid #333;padding:6px 8px;vertical-align:top}th{background:#1d1d1d;position:sticky;top:0}",
        "    details{border:1px solid #333;background:#151515;padding:10px;margin:12px 0 24px}summary{cursor:pointer;font-weight:700} .muted{color:#bbb}",
        "  </style>",
        "</head>",
        "<body>",
        "  <h1>CNS Overview</h1>",
        f"  <p>{html.escape(summary['conclusion'])}</p>",
        f"  <div class=\"cards\">{card_html}</div>",
        "  <h2>Category Counts</h2>",
        "  <table><thead><tr><th>category</th><th>count</th><th>meaning</th></tr></thead>",
        f"  <tbody>{category_table}</tbody></table>",
        "  <h2>Completed</h2>",
        f"  <ul>{completed}</ul>",
        "  <h2>Open Questions</h2>",
        f"  <ul>{open_questions}</ul>",
        "  <h2>Related Reports</h2>",
        f"  <ul>{related}</ul>",
        "  <details open><summary>All CNS files</summary>",
        "  <p class=\"muted\">This table is generated from <code>out/cns_payloads.json</code>.</p>",
        "  <table><thead><tr><th>file</th><th>kind</th><th>category</th><th>size</th><th>decoded bytes</th><th>details</th></tr></thead>",
        f"  <tbody>{''.join(cns_rows)}</tbody></table>",
        "  </details>",
        "</body>",
        "</html>",
        "",
    ])


def write_outputs(summary: dict, out_dir: Path = OUT) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "cns_overview.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    (out_dir / "cns_overview.md").write_text(markdown(summary), encoding="utf-8")
    (out_dir / "cns_overview.html").write_text(html_page(summary), encoding="utf-8")


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--cns-payloads", type=Path, default=OUT / "cns_payloads.json")
    parser.add_argument("--cns-text", type=Path, default=OUT / "cns_text_candidates.json")
    parser.add_argument("--scene-manifest", type=Path, default=OUT / "scene_manifest.json")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    return parser.parse_args()


def main() -> None:
    args = parse_args()
    summary = build_summary(
        load_json(args.cns_payloads, []),
        load_json(args.cns_text, {}),
        load_json(args.scene_manifest, []),
    )
    write_outputs(summary, args.out_dir)


if __name__ == "__main__":
    main()
