#!/usr/bin/env python3
"""Build a focused proof report for Ataho's 폭전축/퍽전축 helper effect.

The skill family is a compact calibration case for moving helper effects:
levels 2..4 all attach the same btl_efc blade frame, but the helper child
script initializes different motion parameters.  This report keeps the raw EXE
evidence and a small web-preview projection separate.
"""

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 effect_rect(rect_review: dict[str, Any], frame: int) -> dict[str, Any] | None:
    for row in rect_review.get("rows") or []:
        if row.get("asset") != "btl_efc":
            continue
        for rect in row.get("rects") or []:
            if rect.get("index") == frame:
                out = dict(rect)
                out["asset"] = "btl_efc"
                out["frame"] = frame
                return out
    return None


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


def helper_rows_by_id(child_scripts: dict[str, Any]) -> dict[int, dict[str, Any]]:
    return {
        int(row["helperId"]): row
        for row in child_scripts.get("helperRows") or []
        if isinstance(row.get("helperId"), int)
    }


def init_write_values(helper: dict[str, Any]) -> dict[str, dict[str, Any]]:
    values: dict[str, dict[str, Any]] = {}
    for write in (helper.get("decoded") or {}).get("initWrites") or []:
        dest = write.get("destHex")
        if not dest or write.get("category") == "init-end":
            continue
        # Keep the first write for each field.  The later 0x004bb010 block is
        # the common sprite/display initialization, not a motion override.
        values.setdefault(dest, write)
    return values


def common_frame_write(helper: dict[str, Any]) -> dict[str, Any] | None:
    for write in (helper.get("decoded") or {}).get("initWrites") or []:
        frame = write.get("frame")
        if isinstance(frame, dict) and frame.get("spriteHex") == "0x1a":
            return write
    return None


def result_sounds(row: dict[str, Any]) -> list[int]:
    out: list[int] = []
    for event in row.get("soundCalls") or []:
        if event.get("opcode") != "0xc2":
            continue
        value = event.get("normalWlkNo", event.get("wlkNo"))
        if isinstance(value, int):
            out.append(value)
    if out:
        return out
    return [v for v in row.get("resultWlkNos") or [] if isinstance(v, int)]


def classify_motion(level: int, helper_id: int | None, values: dict[str, dict[str, Any]]) -> tuple[str, str, int, int, int]:
    if not helper_id:
        return ("actor-only", "1단계는 helper가 없고 actor 프레임만 재생된다.", 0, 0, 0)
    param_x = int(values.get("0x8c", {}).get("value") or 1)
    dx = float(values.get("0x74", {}).get("valueFixed") or 0)
    target_y = values.get("0x84", {}).get("valueFixed")
    field_8e = int(values.get("0x8e", {}).get("value") or 0)
    param_y = int(values.get("0x90", {}).get("value") or 0)

    if target_y is not None and float(target_y) < 0:
        motion_x = int(round(dx * max(1, param_x)))
        # The absolute targetY is a VM-space target.  For preview we project the
        # EXE's asymmetric 0x8e/0x90 pair into a visible upward travel span.
        motion_y = -int(round((field_8e + param_y) * 4))
        return (
            "rising-blade-helper",
            f"helper {helper_id}는 btl_efc#179에 dx={dx:g}px, paramX={param_x}, targetY={target_y:g}px를 붙인다. 상승형 칼날 이펙트로 해석한다.",
            param_x,
            motion_x,
            motion_y,
        )
    if dx:
        motion_x = int(round(dx * max(1, param_x)))
        return (
            "side-blade-helper",
            f"helper {helper_id}는 btl_efc#179에 dx={dx:g}px, paramX={param_x}, 0x8e={field_8e}, paramY={param_y}를 붙인다. 옆으로 흐르는 칼날 이펙트로 해석한다.",
            param_x,
            motion_x,
            0,
        )
    return (
        "single-position-blade-helper",
        f"helper {helper_id}는 btl_efc#179를 표시하지만 dx/targetY가 없다. 같은 위치에 머무는 칼날 이펙트로 해석한다.",
        param_x,
        0,
        0,
    )


def build() -> dict[str, Any]:
    complete = load_json("battle_skill_complete_pattern_review.json")
    child_scripts = load_json("battle_helper_child_script_review.json")
    rect_review = load_json("cns_rect_review_data.json")
    helper_by_id = helper_rows_by_id(child_scripts)
    blade_rect = effect_rect(rect_review, 179)

    source_rows = [
        row
        for row in complete.get("rows") or []
        if row.get("ownerName") == "아타호"
        and row.get("familyName") == "폭전축/퍽전축"
    ]
    source_rows.sort(key=lambda row: int(str(row.get("skillIdHex") or "0").replace("0x", ""), 16))

    rows: list[dict[str, Any]] = []
    for row in source_rows:
        helper_id = (row.get("helperIds") or [None])[0]
        helper = helper_by_id.get(helper_id) if isinstance(helper_id, int) else None
        values = init_write_values(helper or {})
        frame_write = common_frame_write(helper or {})
        level = int(row.get("levelOrFixed") or len(rows) + 1)
        motion_class, interpretation, gate, motion_x, motion_y = classify_motion(level, helper_id, values)
        rows.append(
            {
                "level": level,
                "skillIdHex": row.get("skillIdHex"),
                "skillName": row.get("skillName"),
                "mpCost": row.get("mpCost"),
                "actorFrames": actor_gate_labels(row),
                "helperId": helper_id,
                "helperScriptVaHex": (helper or {}).get("childScriptVaHex"),
                "helperCallTicks": [
                    event.get("tick")
                    for event in row.get("timelineEvents") or []
                    if event.get("kind") == "helper-call"
                ],
                "effectFrame": frame_write.get("frame") if frame_write else None,
                "effectRect": blade_rect if frame_write else None,
                "motionInitWrites": [
                    write
                    for key, write in values.items()
                    if key in {"0x8c", "0x8e", "0x90", "0x74", "0x6c", "0x84"}
                ],
                "resultWlkNos": result_sounds(row),
                "hitTicks": [
                    hit.get("tick")
                    for hit in row.get("hitEvents") or []
                    if isinstance(hit.get("tick"), int)
                ],
                "motionClass": motion_class,
                "previewGate": gate,
                "previewMotionX": motion_x,
                "previewMotionY": motion_y,
                "interpretation": interpretation,
            }
        )

    return {
        "version": 1,
        "kind": "hwanse-battle-bakuten-effect-review",
        "status": "static-exe-proof-for-bakuten-blade-helper-motion",
        "source": [
            "out/battle_skill_complete_pattern_review.json",
            "out/battle_helper_child_script_review.json",
            "out/cns_rect_review_data.json",
        ],
        "summary": {
            "levels": len(rows),
            "levelsWithHelper": sum(1 for row in rows if row["helperId"]),
            "effectFrame": "btl_efc#179",
            "effectRect": blade_rect,
        },
        "interpretationNotes": [
            "btl_efc#179는 helper child script의 display.spriteFrame +0x28 = 0x001a00b3에서 직접 나온다.",
            "0x8c는 helper 루프 카운터/수명으로 쓰이며, 3/4단계는 8 gate, 2단계는 12 gate로 초기화된다.",
            "3단계 helper #116은 motion.dx +0x74 = 4px를 가진다. 사용자가 관찰한 옆 이동과 일치한다.",
            "4단계 helper #117은 motion.dx +0x74 = 8px와 motion.targetY +0x84 = -2000px를 가진다. 사용자가 관찰한 상승 이동과 일치한다.",
            "previewMotionX/Y는 EXE motion 파라미터를 타임라인 리뷰용으로 투영한 값이다. 원본 init write는 motionInitWrites에 그대로 남긴다.",
        ],
        "rows": rows,
    }


def markdown(report: dict[str, Any]) -> str:
    lines = [
        "# 폭전축/퍽전축 이펙트 해석",
        "",
        f"status: `{report['status']}`",
        "",
        "## 결론",
        "",
        "- 1단계: helper 없음. actor 프레임만 재생.",
        "- 2단계: helper #115가 `btl_efc#179`를 같은 위치에 표시.",
        "- 3단계(퍽전축): helper #116이 `btl_efc#179`에 `dx=4px`를 붙여 옆으로 움직임.",
        "- 4단계(폭전축): helper #117이 `dx=8px`와 `targetY=-2000px`를 붙여 상승형 움직임.",
        "",
        "## 레벨별 증거",
        "",
        "| Lv | skill | helper | call | effect | motion init | WLK | motion |",
        "|---:|---|---|---|---|---|---|---|",
    ]
    for row in report["rows"]:
        writes = [write.get("summary") for write in row["motionInitWrites"]]
        effect = row["effectFrame"] or {}
        effect_label = "-"
        if effect:
            effect_label = f"sprite {effect.get('spriteHex')} frame {effect.get('frame')}"
        lines.append(
            "| "
            + " | ".join(
                [
                    str(row["level"]),
                    f"{row['skillName']} `{row['skillIdHex']}`",
                    f"`{row['helperId'] or '-'}` `{row['helperScriptVaHex'] or '-'}`",
                    "`" + compact(row["helperCallTicks"]) + "`",
                    f"`{effect_label}`",
                    "`" + compact(writes, 8) + "`",
                    "`" + compact(row["resultWlkNos"]) + "`",
                    f"{row['motionClass']} ({row['previewMotionX']},{row['previewMotionY']})",
                ]
            )
            + " |"
        )
    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"]:
        writes = [write.get("summary") for write in row["motionInitWrites"]]
        effect = row["effectFrame"] or {}
        effect_label = "-"
        if effect:
            effect_label = f"sprite {effect.get('spriteHex')} frame {effect.get('frame')}"
        rows_html.append(
            "<tr>"
            f"<td>{row['level']}</td>"
            f"<td>{esc(row['skillName'])}<br><code>{esc(row['skillIdHex'])}</code></td>"
            f"<td><code>{esc(row['helperId'] or '-')}</code><br><small>{esc(row['helperScriptVaHex'] or '-')}</small></td>"
            f"<td><code>{esc(compact(row['helperCallTicks']))}</code></td>"
            f"<td><code>{esc(effect_label)}</code></td>"
            f"<td><code>{esc(compact(writes, 10))}</code></td>"
            f"<td><code>{esc(compact(row['resultWlkNos']))}</code></td>"
            f"<td><strong>{esc(row['motionClass'])}</strong><br><code>preview {row['previewMotionX']},{row['previewMotionY']}</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; }}
  </style>
</head>
<body>
  <p><a href="../web/index.html">홈</a> · <a href="../web/battle_skill_timeline_review.html">skill timeline</a> · <a href="battle_bakuten_effect_review.json">JSON</a></p>
  <h1>폭전축/퍽전축 이펙트 해석</h1>
  <p class="lead">레벨별 actor VM, helper child script init, result WLK, btl_efc source rect를 결합한 정적 EXE 근거입니다. 3단계와 4단계는 같은 <code>btl_efc#179</code>를 쓰지만 motion init 값이 달라져 각각 옆 이동과 상승 이동으로 갈라집니다.</p>
  <table>
    <thead><tr><th>Lv</th><th>skill</th><th>helper</th><th>call tick</th><th>effect</th><th>motion init</th><th>WLK</th><th>motion</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_bakuten_effect_review.json").write_text(
        json.dumps(report, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    print("wrote out/battle_bakuten_effect_review.json")


if __name__ == "__main__":
    main()
