#!/usr/bin/env python3
"""Guard battle skill playback regressions that canonical row checks miss.

The canonical bundle can be internally "confirmed" while a browser preview still
falls back to frame 0 because a frame rect is missing, or while a shifted skill
boundary reintroduces old Rinshan/Runrun regressions.  This verifier checks the
data layer used by the shared battle preview pages, not the original game
runtime.
"""

from __future__ import annotations

import json
from pathlib import Path


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


ASSET_BY_OWNER = {
    "ataho": "btl_at",
    "rinshan": "btl_rs",
    "smashu": "btl_sm",
}


REGRESSION_ROWS: list[tuple[str, str, object, str, list[int]]] = [
    ("rinshan", "0x01", None, "손톱공격", [20, 21, 22, 23, 23, 3]),
    ("rinshan", "0x02", None, "하이킥", [10, 11, 11, 3]),
    ("rinshan", "0x03", None, "미들킥", [12, 13, 13, 3]),
    ("rinshan", "0x04", None, "로 킥", [14, 15, 15, 3]),
    ("rinshan", "0x05", None, "도발", [51, 52, 53, 51, 52, 53, 51, 52, 53, 54, 4, 5, 4]),
    ("rinshan", "0x06", None, "꼬챙이 꿰기", [20, 21, 22, 23, 23, 3]),
    ("rinshan", "0x07", None, "꼬챙이 꿰기", [20, 21, 22, 23, 23, 3]),
    ("rinshan", "0x08", None, "꼬챙이 꿰기", [20, 21, 22, 23, 23, 3]),
    ("rinshan", "0x09", 1, "안면백조권", [6, 7, 8, 9, 9, 16, 17, 18, 19, 19, 6, 7, 8, 9, 9, 3]),
    ("rinshan", "0x0a", 2, "안면백조권", [6, 7, 8, 9, 9, 16, 17, 18, 19, 19, 33, 34, 38, 39, 39, 3]),
    ("rinshan", "0x0b", 3, "안면백조권", [6, 7, 8, 9, 9, 16, 17, 18, 19, 19, 33, 34, 35, 36, 37, 35, 34, 41, 40, 3]),
    ("rinshan", "0x0c", 4, "안면백조권", [6, 7, 8, 9, 9, 16, 17, 18, 19, 19, 33, 34, 38, 39, 39, 33, 34, 35, 36, 37, 35, 34, 42, 42, 41, 40, 3]),
    ("ataho", "0x2f", 1, "맹호룬룬권", [44, 45, 46, 47, 44, 48, 49, 50, 0]),
    ("ataho", "0x30", 2, "맹호룬룬권", [44, 45, 46, 47, 44, 48, 49, 50, 12, 13, 0]),
    ("ataho", "0x31", 3, "맹호룬룬권", [44, 45, 46, 47, 44, 48, 49, 50, 12, 13, 25, 26, 0]),
    ("ataho", "0x32", 4, "맹호룬룬권", [44, 45, 46, 47, 44, 48, 49, 50, 12, 13, 25, 26, 8, 9, 10, 11, 0]),
]


def load_json(path: Path) -> dict:
    return json.loads(path.read_text(encoding="utf-8"))


def fail(message: str) -> None:
    raise AssertionError(message)


def row_key(owner: str, skill: str, level: object) -> tuple[str, str, object]:
    return owner, skill.lower(), level


def canonical_row_index(rows: list[dict]) -> dict[tuple[str, str, object], dict]:
    return {
        row_key(
            str(row.get("ownerKey") or ""),
            str(row.get("skillIdHex") or ""),
            row.get("levelOrFixed"),
        ): row
        for row in rows
    }


def rect_index() -> dict[str, list[dict]]:
    data = load_json(OUT / "cns_rect_review_data.json")
    return {
        str(row.get("asset") or ""): row.get("rects") or []
        for row in data.get("rows") or []
        if row.get("asset")
    }


def assert_shared_pages_bound_to_canonical() -> None:
    required = [
        "BattleAnimation.buildPlayerPlayback",
        "BattleAnimation.mergePlayerActions",
        "battle_skill_timeline_canonical.json",
        "cns_rect_review_data.json",
    ]
    for rel in [
        "battle_skill_timeline_review.html",
        "battle_formula_calculator.html",
        "battle_simulator.html",
    ]:
        text = (WEB / rel).read_text(encoding="utf-8")
        for needle in required[:3]:
            if needle not in text:
                fail(f"web/{rel} no longer uses shared canonical playback: missing {needle}")
    timeline = (WEB / "battle_skill_timeline_review.html").read_text(encoding="utf-8")
    if "renderRawFilmstrip" not in timeline or "renderFilmstrip" not in timeline:
        fail("battle_skill_timeline_review.html must expose raw and playback filmstrips")


def assert_row(row: dict, expected_name: str, expected_frames: list[int], rects: list[dict]) -> None:
    if row.get("status") != "confirmed":
        fail(f"{row.get('ownerKey')} {row.get('skillIdHex')} is not confirmed")
    if row.get("timelineStatus") != "confirmed":
        fail(f"{row.get('ownerKey')} {row.get('skillIdHex')} timeline is not confirmed")
    if row.get("skillName") != expected_name:
        fail(
            f"{row.get('ownerKey')} {row.get('skillIdHex')} name mismatch: "
            f"{row.get('skillName')!r} != {expected_name!r}"
        )

    frame_sequence = row.get("frameSequence") or []
    if frame_sequence != expected_frames:
        fail(
            f"{row.get('ownerKey')} {row.get('skillName')} {row.get('skillIdHex')} "
            f"level={row.get('levelOrFixed')} frameSequence mismatch:\n"
            f"  expected {expected_frames}\n"
            f"  actual   {frame_sequence}"
        )

    raw_frames = [
        int(event.get("frame"))
        for event in (row.get("timeline") or {}).get("frameEvents") or []
        if event.get("kind") == "actor-frame" and event.get("frame") is not None
    ]
    if raw_frames != expected_frames:
        fail(
            f"{row.get('ownerKey')} {row.get('skillName')} raw actor-frame mismatch:\n"
            f"  expected {expected_frames}\n"
            f"  actual   {raw_frames}"
        )

    rect_count = len(rects)
    missing = [frame for frame in expected_frames if frame < 0 or frame >= rect_count]
    if missing:
        fail(
            f"{row.get('ownerKey')} {row.get('skillName')} references frames outside "
            f"rect table ({rect_count}): {missing}"
        )


def main() -> None:
    canonical = load_json(OUT / "battle_skill_timeline_canonical.json")
    summary = canonical.get("summary") or {}
    if summary.get("canonicalRowsReady") is not True:
        fail("canonicalRowsReady must be true before playback regression verification")
    if summary.get("goalReady") is not False:
        fail("goalReady must stay false; this verifier is one gate, not full completion")

    rows = canonical.get("playerActions") or []
    index = canonical_row_index(rows)
    rects_by_asset = rect_index()

    for owner, skill, level, name, frames in REGRESSION_ROWS:
        row = index.get(row_key(owner, skill, level))
        if not row:
            fail(f"missing regression row {owner} {skill} level={level!r}")
        asset = ASSET_BY_OWNER.get(owner)
        assert_row(row, name, frames, rects_by_asset.get(asset, []))

    assert_shared_pages_bound_to_canonical()
    print("battle skill playback regression guard ok")
    print(f"- guarded rows: {len(REGRESSION_ROWS)}")
    print("- checked: canonical frames, raw actor-frame frames, rect bounds, shared page bindings")


if __name__ == "__main__":
    main()
