#!/usr/bin/env python3
"""Build a focused proof report for Ataho's "맹호의 울부짖음" effect.

This report deliberately stays narrow.  The skill is a useful calibration case
for battle effect animation because its four proficiency levels differ only by
helper effect behavior while the actor-side frame sequence stays stable.
"""

from __future__ import annotations

import html
import json
from pathlib import Path
from typing import Any


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


def load_json(name: str) -> dict[str, Any]:
    return json.loads((OUT / name).read_text(encoding="utf-8"))


def esc(value: Any) -> str:
    return html.escape(str(value if value is not None else ""))


def compact(values: list[Any] | tuple[Any, ...], limit: int = 12) -> str:
    items = [str(value) for value in values if value not in (None, "")]
    if not items:
        return "-"
    if len(items) > limit:
        items = items[:limit] + [f"... +{len(items) - limit}"]
    return ", ".join(items)


def btl_efc_rects(rect_scan: dict[str, Any]) -> dict[int, dict[str, Any]]:
    for row in rect_scan.get("rows") or []:
        if row.get("asset") != "btl_efc":
            continue
        rects = (row.get("bestTable") or {}).get("rects") or []
        out: dict[int, dict[str, Any]] = {}
        for index in (190, 191):
            if index < len(rects):
                rect = dict(rects[index])
                rect["frame"] = index
                rect["asset"] = "btl_efc"
                out[index] = rect
        return out
    return {}


def actor_gate_labels(row: dict[str, Any]) -> list[str]:
    out: list[str] = []
    for item in row.get("expandedFrameGateSequence") or row.get("actorExpandedFrameGateSequence") or []:
        frame = item.get("frame")
        gate = item.get("gate")
        tick = item.get("tick")
        out.append(f"t{tick}:#{frame}@{gate}")
    return out


def helper_frame_labels(helper: dict[str, Any]) -> list[str]:
    labels: list[str] = []
    for script in helper.get("frameScripts") or []:
        labels.extend(script.get("frames") or [])
    return labels


def helper_targets(helper: dict[str, Any]) -> list[str]:
    return [str(script.get("targetVaHex")) for script in helper.get("frameScripts") or [] if script.get("targetVaHex")]


def helper_blink_segments(helper: dict[str, Any]) -> list[dict[str, Any]]:
    out: list[dict[str, Any]] = []
    for script in helper.get("frameScripts") or []:
        out.extend(script.get("blinkSegments") or [])
    return out


def helper_effect_rects(helper: dict[str, Any], rect_by_frame: dict[int, dict[str, Any]]) -> list[dict[str, Any]]:
    frames: set[int] = set()
    for script in helper.get("frameScripts") or []:
        for segment in script.get("blinkSegments") or []:
            if isinstance(segment.get("frame"), int):
                frames.add(segment["frame"])
        for label in script.get("frames") or []:
            raw = str(label).split("@", 1)[0]
            try:
                frames.add(int(raw))
            except ValueError:
                pass
    return [rect_by_frame[frame] for frame in sorted(frames) if frame in rect_by_frame]


def c1_waits(row: dict[str, Any]) -> list[dict[str, Any]]:
    waits: list[dict[str, Any]] = []
    for event in row.get("timelineEvents") or []:
        if event.get("kind") == "wait" and event.get("opcode") == "0xc1":
            waits.append(
                {
                    "tick": event.get("tick"),
                    "vaHex": event.get("vaHex"),
                    "summary": event.get("summary"),
                }
            )
    return waits


def result_actor_reset(row: dict[str, Any]) -> list[dict[str, Any]]:
    resets: list[dict[str, Any]] = []
    for event in row.get("timelineEvents") or []:
        if event.get("kind") == "actor-frame" and event.get("frame") == 0:
            resets.append(
                {
                    "tick": event.get("tick"),
                    "vaHex": event.get("vaHex"),
                    "summary": event.get("summary"),
                }
            )
    return resets


def build() -> dict[str, Any]:
    complete = load_json("battle_skill_complete_pattern_review.json")
    rect_scan = load_json("cns_frame_rect_exe_scan.json")
    rect_by_frame = btl_efc_rects(rect_scan)

    rows: list[dict[str, Any]] = []
    source_rows = [
        row
        for row in complete.get("rows") or []
        if row.get("ownerName") == "아타호" and row.get("skillName") == "맹호의 울부짖음"
    ]
    source_rows.sort(key=lambda row: int(str(row.get("skillIdHex") or "0").replace("0x", ""), 16))

    for index, row in enumerate(source_rows, start=1):
        helpers = row.get("helperFacts") or []
        helper = helpers[0] if helpers else {}
        helper_rects = helper_effect_rects(helper, rect_by_frame)
        blink_segments = helper_blink_segments(helper)
        c1 = c1_waits(row)
        reset = result_actor_reset(row)
        if not helpers:
            effect_verdict = "no-visual-helper"
            interpretation = "1단계는 0xbd helper 호출이 없고 WLK 6 시전음만 있다."
        elif helper.get("behaviorClass") == "single-frameScript-flash":
            effect_verdict = "single-flash"
            interpretation = "2단계는 helper가 btl_efc#190 한 프레임을 gate 8 동안 표시한다."
        elif helper.get("behaviorClass") == "display-flag-blink-frameScript":
            frames = compact(helper_frame_labels(helper), 20)
            effect_verdict = "display-flag-blink"
            interpretation = (
                "helper frameScript가 같은 btl_efc 프레임을 반복 표시하면서 "
                "display/actor +0x00 bit 0x10000000을 op6/op7로 토글한다. "
                f"프레임열: {frames}"
            )
        else:
            effect_verdict = helper.get("behaviorClass") or "unclassified"
            interpretation = "helper 근거는 있으나 별도 시각 의미는 추가 검토 대상이다."

        rows.append(
            {
                "level": row.get("levelOrFixed") or index,
                "skillIdHex": row.get("skillIdHex"),
                "mpCost": row.get("mpCost"),
                "actorFrames": actor_gate_labels(row),
                "effectWlkNos": row.get("effectWlkNos") or [],
                "resultWlkNos": row.get("resultWlkNos") or [],
                "helperIds": row.get("helperIds") or [],
                "helperBehavior": helper.get("behaviorClass") if helper else None,
                "helperTargets": helper_targets(helper),
                "helperFrames": helper_frame_labels(helper),
                "effectRects": helper_rects,
                "displayFlagBlink": bool(blink_segments),
                "blinkFlagMaskHex": "0x10000000" if blink_segments else None,
                "blinkSegments": blink_segments,
                "actorFlagWaits": c1,
                "actorFrame0Reset": reset,
                "effectVerdict": effect_verdict,
                "interpretation": interpretation,
            }
        )

    return {
        "version": 1,
        "kind": "hwanse-battle-roar-effect-review",
        "status": "static-exe-proof-for-mengho-roar-effect",
        "source": [
            "out/battle_skill_complete_pattern_review.json",
            "out/battle_helper_visual_behavior_review.json",
            "out/battle_helper_frame_script_review.json",
            "out/cns_frame_rect_exe_scan.json",
        ],
        "summary": {
            "levels": len(rows),
            "levelsWithVisualHelper": sum(1 for row in rows if row["helperIds"]),
            "levelsWithBlink": sum(1 for row in rows if row["displayFlagBlink"]),
            "btlEfcFrame190Rect": rect_by_frame.get(190),
            "btlEfcFrame191Rect": rect_by_frame.get(191),
        },
        "interpretationNotes": [
            "actor frame #38 유지의 정적 근거는 helper 호출 레벨에서 result/reset 전 0xc1 actor flag wait가 있는 점이다.",
            "helper frameScript 끝의 actor flag mode=0 mask=0x00000002는 0xc1 wait의 해제 신호로 취급한다.",
            "display/actor +0x00 bit 0x10000000의 정확한 내부 이름은 미확정이지만, 3/4단 frameScript의 op6/op7 반복 토글은 EXE에서 직접 나온다.",
            "btl_efc#190은 EXE rect 기준 80x80, btl_efc#191은 128x112로 4단 효과가 커지는 근거다.",
        ],
        "rows": rows,
    }


def markdown(report: dict[str, Any]) -> str:
    lines = [
        "# 맹호의 울부짖음 이펙트 해석",
        "",
        f"status: `{report['status']}`",
        "",
        "## 결론",
        "",
        "- 1단계: visual helper 없음. WLK 6 시전음만 존재.",
        "- 2단계: helper 112가 `btl_efc#190@8` 단일 표시.",
        "- 3단계: helper 113이 `btl_efc#190`을 반복하고 `0x10000000` 표시 플래그를 토글.",
        "- 4단계: helper 114가 더 큰 `btl_efc#191`을 반복하고 같은 표시 플래그를 토글.",
        "",
        "## 레벨별 증거",
        "",
        "| Lv | skill | actor frames | WLK | helper | effect frames | rect | blink | wait |",
        "|---:|---|---|---|---|---|---|---|---|",
    ]
    for row in report["rows"]:
        rects = [f"#{r['frame']} {r['x']},{r['y']} {r['w']}x{r['h']}" for r in row["effectRects"]]
        waits = [f"{w['vaHex']} {w['summary']}" for w in row["actorFlagWaits"]]
        lines.append(
            "| "
            + " | ".join(
                [
                    str(row["level"]),
                    f"`{row['skillIdHex']}`",
                    "`" + compact(row["actorFrames"], 8) + "`",
                    "`effect " + compact(row["effectWlkNos"]) + " / result " + compact(row["resultWlkNos"]) + "`",
                    compact(row["helperIds"]),
                    "`" + compact(row["helperFrames"], 18) + "`",
                    "`" + compact(rects, 4) + "`",
                    "yes" if row["displayFlagBlink"] else "-",
                    "`" + compact(waits, 2) + "`",
                ]
            )
            + " |"
        )
    lines.append("")
    lines.append("## 해석 주석")
    lines.extend(f"- {note}" for note in report["interpretationNotes"])
    return "\n".join(lines) + "\n"


def html_page(report: dict[str, Any]) -> str:
    rows_html: list[str] = []
    for row in report["rows"]:
        rects = [f"#{r['frame']} {r['x']},{r['y']} {r['w']}x{r['h']}" for r in row["effectRects"]]
        waits = [f"{w['vaHex']} {w['summary']}" for w in row["actorFlagWaits"]]
        blink = "yes" if row["displayFlagBlink"] else "-"
        rows_html.append(
            "<tr>"
            f"<td>{row['level']}</td>"
            f"<td><code>{esc(row['skillIdHex'])}</code><br><small>MP {esc(row['mpCost'])}</small></td>"
            f"<td><code>{esc(compact(row['actorFrames'], 10))}</code></td>"
            f"<td><code>effect {esc(compact(row['effectWlkNos']))}</code><br><code>result {esc(compact(row['resultWlkNos']))}</code></td>"
            f"<td>{esc(compact(row['helperIds']))}<br><small>{esc(row['helperBehavior'])}</small></td>"
            f"<td><code>{esc(compact(row['helperFrames'], 24))}</code></td>"
            f"<td><code>{esc(compact(rects, 4))}</code></td>"
            f"<td>{blink}</td>"
            f"<td><code>{esc(compact(waits, 2))}</code></td>"
            f"<td>{esc(row['interpretation'])}</td>"
            "</tr>"
        )
    notes = "".join(f"<li>{esc(note)}</li>" for note in report["interpretationNotes"])
    return f"""<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <title>맹호의 울부짖음 이펙트 해석</title>
  <style>
    body {{ font-family: system-ui, sans-serif; margin: 24px; background:#f7f5ef; color:#201d18; }}
    a {{ color:#744100; }}
    table {{ border-collapse: collapse; width: 100%; background:#fffdf8; }}
    th, td {{ border:1px solid #d8cfc0; padding:8px; vertical-align: top; font-size:13px; }}
    th {{ background:#efe3cf; text-align:left; }}
    code {{ background:#f1eadf; padding:1px 4px; border-radius:3px; }}
    .lead {{ max-width: 960px; line-height:1.55; }}
    .good {{ color:#14612c; font-weight:700; }}
  </style>
</head>
<body>
  <p><a href="../web/index.html">홈</a> · <a href="../web/battle_skill_timeline_review.html">skill timeline</a> · <a href="battle_roar_effect_review.json">JSON</a></p>
  <h1>맹호의 울부짖음 이펙트 해석</h1>
  <p class="lead">레벨별 actor VM, helper frameScript, WLK, btl_efc source rect를 결합한 정적 EXE 근거입니다. 3/4단의 깜빡임은 같은 프레임 반복 사이에 display/actor +0x00 bit <code>0x10000000</code>을 AND/OR로 토글하는 명령에서 직접 확인됩니다.</p>
  <table>
    <thead><tr><th>Lv</th><th>skill</th><th>actor frames</th><th>WLK</th><th>helper</th><th>effect frames</th><th>rect</th><th>blink</th><th>actor wait</th><th>interpretation</th></tr></thead>
    <tbody>{''.join(rows_html)}</tbody>
  </table>
  <h2>해석 주석</h2>
  <ul>{notes}</ul>
</body>
</html>
"""


def main() -> None:
    report = build()
    (OUT / "battle_roar_effect_review.json").write_text(
        json.dumps(report, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    print("wrote out/battle_roar_effect_review.json")


if __name__ == "__main__":
    main()
