#!/usr/bin/env python3
"""Summarize runtime evidence for the opening logo fade path.

The intro capture is useful because it separates three things that were
previously easy to conflate in static analysis:

* a full-screen CNS surface already present at trace start (`compile.cns`),
* a later full-screen CNS load/decode/upload (`aaa.cns`),
* the actual fade, which is observed as repeated DirectDraw palette updates on
  the primary surface rather than alpha blended image draws.
"""
from __future__ import annotations

import html
import json
import re
from collections import Counter
from pathlib import Path
from typing import Any


ROOT = Path(__file__).resolve().parents[1]
CAPTURES = ROOT / "captures" / "runtime"
OUT = ROOT / "out"
WEB = ROOT / "web"

JSON_OUT = OUT / "intro_runtime_fade_review.json"
HTML_OUT = WEB / "intro_runtime_fade_review.html"
PALETTE_PIPELINE = OUT / "palette_pipeline_review.json"


def h(value: Any) -> str:
    return html.escape("" if value is None else str(value), quote=True)


def read_json(path: Path) -> dict[str, Any]:
    if not path.exists():
        return {}
    return json.loads(path.read_text(encoding="utf-8"))


def load_events(path: Path) -> list[dict[str, Any]]:
    events = []
    for line_no, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
        if not line.strip():
            continue
        event = json.loads(line)
        event["_line"] = line_no
        events.append(event)
    return events


def latest_intro_trace() -> tuple[Path, Path] | None:
    manifests = sorted(CAPTURES.glob("*intro*.manifest.json"))
    if not manifests:
        return None
    pairs = []
    for manifest in manifests:
        jsonl = manifest.with_suffix("").with_suffix(".jsonl")
        if jsonl.exists():
            pairs.append((manifest, jsonl))
    if not pairs:
        return None
    return pairs[-1]


def parse_draw_detail(detail: str | None) -> dict[str, Any]:
    if not detail:
        return {}
    parsed: dict[str, Any] = {}
    for key, pattern in {
        "caller": r"caller=(0x[0-9a-fA-F]+)",
        "srcRect": r"srcRect=([0-9,\-]+)",
        "dstRect": r"dstRect=([0-9,\-]+)",
        "srcColorKey": r"srcColorKey=([^ ]+)",
        "appliedColorKey": r"appliedColorKey=([^ ]+)",
    }.items():
        match = re.search(pattern, detail)
        if match:
            parsed[key] = match.group(1)
    at = re.search(r" at ([0-9\-]+),([0-9\-]+)", detail)
    if at:
        parsed["dstPoint"] = f"{at.group(1)},{at.group(2)}"
    return parsed


def compact_event(event: dict[str, Any]) -> dict[str, Any]:
    keys = [
        "seq",
        "frame",
        "tick",
        "step",
        "event",
        "pc",
        "caller",
        "runner",
        "object",
        "stream",
        "opcode",
        "previous_stream",
        "next_stream",
        "previous_opcode",
        "next_opcode",
        "surfaceHandle",
        "source",
        "destination",
        "sourceLabel",
        "destinationLabel",
        "sourceResourceName",
        "destinationResourceName",
        "resourceName",
        "resourceType",
        "archiveOffset",
        "imageWidth",
        "imageHeight",
        "colorKey",
        "paletteHash",
        "pixelHash",
        "byteLength",
        "writeRect",
        "flags",
        "name",
        "detail",
        "raw",
    ]
    out = {key: event.get(key) for key in keys if key in event}
    if event.get("detail"):
        parsed = parse_draw_detail(event.get("detail"))
        if parsed:
            out["parsed"] = parsed
    return out


def group_palette_runs(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
    runs: list[list[dict[str, Any]]] = []
    current: list[dict[str, Any]] = []
    previous_frame: int | None = None
    for row in rows:
        frame = row.get("frame")
        if not isinstance(frame, int):
            continue
        if previous_frame is None or frame <= previous_frame + 1:
            current.append(row)
        else:
            if current:
                runs.append(current)
            current = [row]
        previous_frame = frame
    if current:
        runs.append(current)

    out = []
    for index, run in enumerate(runs, 1):
        first = run[0]
        last = run[-1]
        tick_span = None
        if isinstance(first.get("tick"), int) and isinstance(last.get("tick"), int):
            tick_span = last["tick"] - first["tick"]
        out.append(
            {
                "runIndex": index,
                "firstSeq": first.get("seq"),
                "lastSeq": last.get("seq"),
                "firstFrame": first.get("frame"),
                "lastFrame": last.get("frame"),
                "eventCount": len(run),
                "frameSpan": (last.get("frame") - first.get("frame") + 1)
                if isinstance(first.get("frame"), int) and isinstance(last.get("frame"), int)
                else None,
                "tickSpan": tick_span,
                "surfaceHandle": first.get("surfaceHandle"),
                "sourceLabel": first.get("sourceLabel"),
                "pc": first.get("pc"),
                "caller": first.get("caller"),
                "firstPaletteHash": first.get("paletteHash"),
                "lastPaletteHash": last.get("paletteHash"),
                "paletteHashes": [row.get("paletteHash") for row in run],
                "sampleRows": [compact_event(row) for row in run[:6]],
            }
        )
    return out


def build() -> dict[str, Any]:
    trace_pair = latest_intro_trace()
    if not trace_pair:
        return {
            "kind": "hwanse-intro-runtime-fade-review",
            "status": "missing-runtime-trace",
            "summary": {"traceFound": False},
            "traces": [],
        }

    manifest_path, jsonl_path = trace_pair
    manifest = read_json(manifest_path)
    events = load_events(jsonl_path)
    palette_static = read_json(PALETTE_PIPELINE)

    counts = Counter(event.get("event") for event in events)
    surface_snapshots = [event for event in events if event.get("event") == "surface-snapshot"]
    resource_labels = [event for event in events if event.get("event") == "resource-surface-label"]
    resource_lifecycle = [
        event
        for event in events
        if event.get("event")
        in {
            "resource-open",
            "resource-read",
            "resource-decode",
            "surface-create",
            "surface-colorkey",
            "resource-surface-label",
            "surface-upload",
        }
        and (
            event.get("resourceName") in {"compile.cns", "aaa.cns"}
            or event.get("sourceResourceName") in {"compile.cns", "aaa.cns"}
        )
    ]

    palette_rows = [event for event in events if event.get("event") == "surface-palette"]
    draw_rows = [event for event in events if event.get("event") == "draw-surface"]
    full_frame_draws = [
        event
        for event in draw_rows
        if event.get("source") == "0x1004C438" and event.get("destination") == "0x10001330"
    ]
    logo_draws = [
        event
        for event in draw_rows
        if event.get("sourceResourceName") in {"compile.cns", "aaa.cns"}
        or event.get("sourceLabel") in {"compile.cns", "aaa.cns"}
    ]

    interesting_frames = set()
    for row in palette_rows + resource_lifecycle + logo_draws:
        frame = row.get("frame")
        if isinstance(frame, int):
            interesting_frames.update(range(frame - 2, frame + 3))
    vm_rows = [
        event
        for event in events
        if event.get("event") in {"vm-op", "vm-op-transition", "object-stream-change"}
        and event.get("frame") in interesting_frames
    ]

    draw_pairs = []
    for key, count in Counter(
        (
            row.get("source"),
            row.get("destination"),
            row.get("sourceLabel"),
            row.get("destinationLabel"),
            row.get("pc"),
            row.get("name"),
        )
        for row in draw_rows
    ).most_common():
        draw_pairs.append(
            {
                "source": key[0],
                "destination": key[1],
                "sourceLabel": key[2],
                "destinationLabel": key[3],
                "pc": key[4],
                "name": key[5],
                "count": count,
            }
        )

    compile_snapshot = next(
        (
            row
            for row in resource_labels
            if row.get("surfaceHandle") == "0x1025D128" and row.get("resourceName") == "compile.cns"
        ),
        None,
    )
    aaa_label = next(
        (
            row
            for row in resource_labels
            if row.get("surfaceHandle") == "0x1025D128" and row.get("resourceName") == "aaa.cns"
        ),
        None,
    )

    palette_runs = group_palette_runs(palette_rows)
    status = "palette-fade-runtime-confirmed"
    if not palette_runs:
        status = "logo-surface-only-no-palette-run"

    conclusions = [
        {
            "id": "fade-rendering-model",
            "status": "confirmed",
            "text": "The observed intro fade is driven by repeated palette updates on the primary 8-bit surface, not by per-frame alpha Blt opacity.",
            "evidence": {
                "surfacePaletteEventCount": len(palette_rows),
                "paletteRuns": [
                    {
                        "firstFrame": run.get("firstFrame"),
                        "lastFrame": run.get("lastFrame"),
                        "eventCount": run.get("eventCount"),
                        "caller": run.get("caller"),
                    }
                    for run in palette_runs
                ],
            },
        },
        {
            "id": "logo-surface-swap",
            "status": "confirmed-partial",
            "text": "The trace starts with the shared full-screen logo surface already labeled compile.cns, then reloads the same handle as aaa.cns at frame 134.",
            "evidence": {
                "sharedLogoSurface": "0x1025D128",
                "compileSnapshotSeq": compile_snapshot.get("seq") if compile_snapshot else None,
                "aaaLabelSeq": aaa_label.get("seq") if aaa_label else None,
                "aaaResourceFrame": aaa_label.get("frame") if aaa_label else None,
            },
        },
        {
            "id": "animation-boundary",
            "status": "supported-boundary",
            "text": "This capture can ground full-screen CNS fade timing and surface replacement. It does not yet prove battle/helper sprite motion; those still need object/helper draw traces during the relevant action.",
            "evidence": {
                "logoDrawCount": len(logo_draws),
                "fullFramePresentCount": len(full_frame_draws),
                "vmRowsNearFadeOrLoad": len(vm_rows),
            },
        },
    ]

    return {
        "kind": "hwanse-intro-runtime-fade-review",
        "status": status,
        "source": {
            "manifest": str(manifest_path.relative_to(ROOT)),
            "jsonl": str(jsonl_path.relative_to(ROOT)),
            "palettePipeline": str(PALETTE_PIPELINE.relative_to(ROOT)) if PALETTE_PIPELINE.exists() else None,
        },
        "manifest": manifest,
        "summary": {
            "traceFound": True,
            "eventCount": len(events),
            "eventCounts": dict(counts),
            "frameRange": [
                min((event.get("frame") for event in events if isinstance(event.get("frame"), int)), default=None),
                max((event.get("frame") for event in events if isinstance(event.get("frame"), int)), default=None),
            ],
            "surfacePaletteEventCount": len(palette_rows),
            "paletteRunCount": len(palette_runs),
            "drawSurfaceEventCount": len(draw_rows),
            "fullFramePresentCount": len(full_frame_draws),
            "logoDrawCount": len(logo_draws),
            "resourceLifecycleEventCount": len(resource_lifecycle),
            "vmRowsNearFadeOrLoad": len(vm_rows),
            "staticPaletteSetEntriesHelpers": (palette_static.get("summary") or {}).get("setEntriesHelpers"),
            "staticPaletteCopyThenApplyHelper": (palette_static.get("summary") or {}).get("copyThenApplyHelper"),
        },
        "surfaceSnapshots": [compact_event(row) for row in surface_snapshots],
        "resourceLabels": [compact_event(row) for row in resource_labels],
        "resourceLifecycle": [compact_event(row) for row in resource_lifecycle],
        "paletteRuns": palette_runs,
        "drawPairs": draw_pairs,
        "logoDrawRows": [compact_event(row) for row in logo_draws],
        "fullFramePresentSamples": [compact_event(row) for row in full_frame_draws[:6]],
        "vmRowsNearFadeOrLoad": [compact_event(row) for row in vm_rows[:120]],
        "conclusions": conclusions,
    }


def table(headers: list[str], rows: list[list[Any]]) -> str:
    head = "".join(f"<th>{h(header)}</th>" for header in headers)
    body = "\n".join(
        "<tr>" + "".join(f"<td>{h(value)}</td>" for value in row) + "</tr>"
        for row in rows
    )
    return f"<table><thead><tr>{head}</tr></thead><tbody>{body}</tbody></table>"


def render_html(data: dict[str, Any]) -> str:
    summary = data.get("summary") or {}
    palette_runs = data.get("paletteRuns") or []
    resource_lifecycle = data.get("resourceLifecycle") or []
    draw_pairs = data.get("drawPairs") or []
    conclusions = data.get("conclusions") or []
    vm_rows = data.get("vmRowsNearFadeOrLoad") or []

    palette_table = table(
        ["run", "frames", "events", "caller", "first hash", "last hash"],
        [
            [
                row.get("runIndex"),
                f"{row.get('firstFrame')}..{row.get('lastFrame')}",
                row.get("eventCount"),
                row.get("caller"),
                row.get("firstPaletteHash"),
                row.get("lastPaletteHash"),
            ]
            for row in palette_runs
        ],
    )
    lifecycle_table = table(
        ["seq", "frame", "event", "resource", "surface", "pc", "caller", "rect/hash"],
        [
            [
                row.get("seq"),
                row.get("frame"),
                row.get("event"),
                row.get("resourceName") or row.get("sourceResourceName"),
                row.get("surfaceHandle"),
                row.get("pc"),
                row.get("caller"),
                row.get("writeRect") or row.get("pixelHash") or row.get("paletteHash"),
            ]
            for row in resource_lifecycle
        ],
    )
    draw_table = table(
        ["source", "destination", "source label", "destination label", "pc", "method", "count"],
        [
            [
                row.get("source"),
                row.get("destination"),
                row.get("sourceLabel"),
                row.get("destinationLabel"),
                row.get("pc"),
                row.get("name"),
                row.get("count"),
            ]
            for row in draw_pairs
        ],
    )
    vm_table = table(
        ["seq", "frame", "event", "object", "stream", "opcode", "next", "pc"],
        [
            [
                row.get("seq"),
                row.get("frame"),
                row.get("event"),
                row.get("object"),
                row.get("stream") or row.get("previous_stream"),
                row.get("opcode") or row.get("previous_opcode"),
                row.get("next_stream") or row.get("new"),
                row.get("pc"),
            ]
            for row in vm_rows[:60]
        ],
    )
    conclusion_cards = "\n".join(
        f"<article><h3>{h(row.get('id'))} <span>{h(row.get('status'))}</span></h3><p>{h(row.get('text'))}</p><pre>{h(json.dumps(row.get('evidence') or {}, ensure_ascii=False, indent=2))}</pre></article>"
        for row in conclusions
    )

    return f"""<!doctype html>
<html lang=\"ko\">
<head>
  <meta charset=\"utf-8\">
  <title>Intro Runtime Fade Review</title>
  <style>
    body {{ margin: 0; padding: 24px; background: #f7f7f4; color: #1f2328; font: 14px/1.55 system-ui, sans-serif; }}
    main {{ max-width: 1180px; margin: 0 auto; }}
    h1 {{ margin: 0 0 8px; font-size: 26px; }}
    h2 {{ margin: 28px 0 10px; font-size: 18px; }}
    .lead {{ color: #4f5b66; margin: 0 0 16px; }}
    .grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 10px; margin: 16px 0; }}
    .metric, article {{ background: #fff; border: 1px solid #d8d8d0; border-radius: 8px; padding: 12px; }}
    .metric strong {{ display: block; font-size: 20px; }}
    article h3 {{ margin: 0 0 8px; font-size: 15px; }}
    article h3 span {{ color: #6a5a00; font-weight: 600; }}
    table {{ border-collapse: collapse; width: 100%; background: #fff; border: 1px solid #d8d8d0; }}
    th, td {{ border: 1px solid #e3e3dc; padding: 6px 8px; vertical-align: top; }}
    th {{ background: #efefe8; text-align: left; }}
    code, pre {{ font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }}
    pre {{ overflow: auto; background: #f3f3ee; padding: 10px; border-radius: 6px; }}
    .note {{ border-left: 4px solid #a56b00; padding: 8px 12px; background: #fff8e9; }}
  </style>
</head>
<body>
<main>
  <h1>Intro Runtime Fade Review</h1>
  <p class=\"lead\">게임 시작부의 <code>compile.cns</code> / <code>aaa.cns</code> 로고 페이드가 런타임에서 어떤 draw/palette 단위로 보이는지 정리합니다.</p>
  <p><a href=\"index.html\">index</a> · <a href=\"palette_pipeline_review.html\">palette pipeline</a> · <a href=\"render_pipeline_model_review.html\">render pipeline</a></p>
  <div class=\"grid\">
    <div class=\"metric\"><span>status</span><strong>{h(data.get('status'))}</strong></div>
    <div class=\"metric\"><span>events</span><strong>{h(summary.get('eventCount'))}</strong></div>
    <div class=\"metric\"><span>palette updates</span><strong>{h(summary.get('surfacePaletteEventCount'))}</strong></div>
    <div class=\"metric\"><span>palette runs</span><strong>{h(summary.get('paletteRunCount'))}</strong></div>
    <div class=\"metric\"><span>logo draws</span><strong>{h(summary.get('logoDrawCount'))}</strong></div>
  </div>
  <section class=\"note\">
    <p>현재 캡처는 trace-start 시점에 <code>compile.cns</code>가 이미 <code>0x1025D128</code> 서피스로 존재하고, frame 134에서 같은 핸들이 <code>aaa.cns</code>로 재라벨/업로드되는 흐름을 잡습니다. 따라서 첫 로고의 로드 자체는 부분 캡처이고, 페이드와 두 번째 로고 로드는 런타임 근거가 있습니다.</p>
  </section>
  <h2>Conclusions</h2>
  <div class=\"grid\">{conclusion_cards}</div>
  <h2>Palette Fade Runs</h2>
  {palette_table}
  <h2>Logo Resource Lifecycle</h2>
  {lifecycle_table}
  <h2>Draw Surface Pairs</h2>
  {draw_table}
  <h2>VM Rows Near Fade/Load</h2>
  {vm_table}
  <script>window.HWANSE_INTRO_RUNTIME_FADE_READY = {json.dumps(summary, ensure_ascii=False)};</script>
</main>
</body>
</html>
"""


def main() -> int:
    data = build()
    JSON_OUT.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    HTML_OUT.write_text(render_html(data), encoding="utf-8")
    print(f"wrote {JSON_OUT.relative_to(ROOT)}")
    print(f"wrote {HTML_OUT.relative_to(ROOT)}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
