#!/usr/bin/env python3
"""Build a focused proof report for Ataho's 호격권 target-overlaid effect.

호격권 is a useful contrast case with 맹호의 울부짖음.  Both families reuse
the tiger-face btl_efc frames, but 호격권 is an enemy-single action and its
helper body is a target-range child visual spawner.  This report keeps that
target-overlay evidence explicit instead of baking it into a visual guess.
"""

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 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 effect_rects(rect_review: dict[str, Any], frames: set[int]) -> list[dict[str, Any]]:
    out: list[dict[str, Any]] = []
    for row in rect_review.get("rows") or []:
        if row.get("asset") != "btl_efc":
            continue
        for rect in row.get("rects") or []:
            index = rect.get("index")
            if index in frames:
                item = dict(rect)
                item["asset"] = "btl_efc"
                item["frame"] = index
                out.append(item)
    return sorted(out, key=lambda item: int(item["frame"]))


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_frames(helper: dict[str, Any]) -> set[int]:
    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 frames


def helper_call_ticks(row: dict[str, Any]) -> list[int]:
    ticks: list[int] = []
    for event in row.get("timelineEvents") or []:
        if event.get("kind") == "helper-call" and isinstance(event.get("tick"), int):
            ticks.append(event["tick"])
    return ticks


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 target_evidence(helper_row: dict[str, Any] | None, source_row: dict[str, Any]) -> dict[str, Any]:
    summary = (helper_row or {}).get("featureSummary") or {}
    return {
        "actionTargetClass": source_row.get("targetClass"),
        "actionPositionClass": source_row.get("positionClass"),
        "helperBodyClass": (helper_row or {}).get("bodyClass"),
        "targetRangeLoop": bool(summary.get("targetRangeLoop")),
        "writesDisplayXy": bool(summary.get("writesDisplayXy")),
        "knownCalls": summary.get("knownCalls") or [],
        "scriptTableOffsets": summary.get("scriptTableOffsets") or [],
        "basis": [
            "battle action row targetClass=enemy-single",
            "battle action row positionClass=single-target-ranged-caster-effect",
            "helper bodyClass=target-range child visual spawner",
            "helper featureSummary.targetRangeLoop=true",
            "helper featureSummary.writesDisplayXy=true",
        ],
    }


def effect_verdict(row: dict[str, Any], helper: dict[str, Any]) -> tuple[str, str]:
    helper_id = helper.get("helperId")
    if not helper_id:
        return ("actor-only", "1단계는 visual helper가 없다. actor 프레임과 WLK만 존재한다.")
    behavior = helper.get("behaviorClass")
    if behavior == "single-frameScript-flash":
        return (
            "target-single-flash",
            "helper가 target-range child visual spawner로 btl_efc#190 한 프레임을 대상 위치에 표시한다.",
        )
    if behavior == "display-flag-blink-frameScript":
        frame = 191 if row.get("skillIdHex") == "0x0d" else 190
        return (
            "target-display-flag-blink",
            f"helper가 대상 위치에 btl_efc#{frame}을 만들고 display flag 0x10000000을 op6/op7로 토글한다.",
        )
    return (behavior or "unclassified-helper", "helper 근거는 있으나 시각 의미는 추가 검토 대상이다.")


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)

    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))

    rows: list[dict[str, Any]] = []
    for index, row in enumerate(source_rows, start=1):
        helper_fact = (row.get("helperFacts") or [{}])[0]
        helper_id = helper_fact.get("helperId")
        helper_row = helper_by_id.get(helper_id) if isinstance(helper_id, int) else None
        blink_segments = helper_blink_segments(helper_fact)
        frames = helper_frames(helper_fact)
        rects = effect_rects(rect_review, frames)
        verdict, interpretation = effect_verdict(row, helper_fact)
        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 [],
                "helperId": helper_id,
                "helperBehavior": helper_fact.get("behaviorClass") if helper_fact else None,
                "helperScriptVaHex": (helper_row or {}).get("childScriptVaHex"),
                "helperFunctionVaHex": (helper_row or {}).get("functionVaHex"),
                "helperBodyClass": (helper_row or {}).get("bodyClass"),
                "helperCallTicks": helper_call_ticks(row),
                "helperTargets": helper_targets(helper_fact),
                "helperFrames": helper_frame_labels(helper_fact),
                "effectRects": rects,
                "actorHoldFrame": 26 if helper_id else None,
                "actorHoldBasis": "0xc1 actor flag wait after #26 frame" if helper_id else None,
                "displayFlagBlink": bool(blink_segments),
                "blinkFlagMaskHex": "0x10000000" if blink_segments else None,
                "blinkSegments": blink_segments,
                "actorFlagWaits": c1_waits(row),
                "targetOverlayEvidence": target_evidence(helper_row, row) if helper_id else {
                    "actionTargetClass": row.get("targetClass"),
                    "actionPositionClass": row.get("positionClass"),
                    "basis": ["battle action row targetClass=enemy-single"],
                },
                "effectVerdict": verdict,
                "interpretation": interpretation,
            }
        )

    return {
        "version": 1,
        "kind": "hwanse-battle-hogyeok-effect-review",
        "status": "static-exe-proof-for-hogyeok-target-overlaid-effect",
        "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),
            "levelsWithVisualHelper": sum(1 for row in rows if row["helperIds"]),
            "levelsWithBlink": sum(1 for row in rows if row["displayFlagBlink"]),
            "targetOverlayProofRows": sum(
                1
                for row in rows
                if (row.get("targetOverlayEvidence") or {}).get("targetRangeLoop")
                and (row.get("targetOverlayEvidence") or {}).get("writesDisplayXy")
            ),
        },
        "interpretationNotes": [
            "호격권은 action row에서 targetClass=enemy-single, positionClass=single-target-ranged-caster-effect로 분류된다.",
            "2~4단 helper #112/#113/#114는 모두 function 0x00414a79, bodyClass target-range child visual spawner다.",
            "해당 helper featureSummary에는 targetRangeLoop=true, writesDisplayXy=true, knownCalls=[allocate display object, target range begin]가 있다.",
            "따라서 울부짖음처럼 화면 중앙 고정 효과가 아니라, 공격 대상 range에서 display 좌표를 잡아 btl_efc 호랑이 얼굴을 대상 위에 띄우는 해석이 EXE 근거와 맞다.",
            "2~4단은 helper 호출 뒤 actor frame #26@6 다음에 0xc1 actor flag wait(mode=0, mask=0x00000002)가 있다. 웹 프리뷰에서는 #26 자세를 helper 종료/hit까지 유지한다.",
            "정확한 몬스터 위 픽셀 오프셋 이름은 아직 미명명이다. 현재 타임라인 프리뷰는 target-overlay anchor로 표시한다.",
        ],
        "rows": rows,
    }


def markdown(report: dict[str, Any]) -> str:
    lines = [
        "# 호격권 이펙트 해석",
        "",
        f"status: `{report['status']}`",
        "",
        "## 결론",
        "",
        "- 1단계: visual helper 없음.",
        "- 2단계: helper #112가 `btl_efc#190@8`을 target range 위치에 단일 표시.",
        "- 3단계: helper #113이 `btl_efc#190`을 target range 위치에 표시하고 `0x10000000` 표시 플래그를 토글.",
        "- 4단계: helper #114가 더 큰 `btl_efc#191`을 target range 위치에 표시하고 같은 플래그를 토글.",
        "",
        "## 타겟 오버레이 근거",
        "",
        "- action: `targetClass=enemy-single`, `positionClass=single-target-ranged-caster-effect`",
        "- helper body: `target-range child visual spawner`",
        "- helper features: `targetRangeLoop=true`, `writesDisplayXy=true`, `target range begin` call",
        "",
        "## 레벨별 증거",
        "",
        "| Lv | skill | actor frames | WLK | helper | target evidence | effect frames | rect | blink |",
        "|---:|---|---|---|---|---|---|---|---|",
    ]
    for row in report["rows"]:
        rects = [f"#{r['frame']} {r['x']},{r['y']} {r['w']}x{r['h']}" for r in row["effectRects"]]
        evidence = row.get("targetOverlayEvidence") or {}
        target = [
            evidence.get("actionTargetClass"),
            evidence.get("actionPositionClass"),
            evidence.get("helperBodyClass"),
            "targetRangeLoop" if evidence.get("targetRangeLoop") else None,
            "writesDisplayXy" if evidence.get("writesDisplayXy") else None,
        ]
        lines.append(
            "| "
            + " | ".join(
                [
                    str(row["level"]),
                    f"`{row['skillIdHex']}`",
                    "`" + compact(row["actorFrames"], 10) + "`",
                    "`effect " + compact(row["effectWlkNos"]) + " / result " + compact(row["resultWlkNos"]) + "`",
                    f"`{compact(row['helperIds'])}` `{row.get('helperScriptVaHex') or '-'}`",
                    "`" + compact(target, 6) + "`",
                    "`" + compact(row["helperFrames"], 20) + "`",
                    "`" + compact(rects, 4) + "`",
                    "yes" if row["displayFlagBlink"] else "-",
                ]
            )
            + " |"
        )
    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"]]
        evidence = row.get("targetOverlayEvidence") or {}
        target = [
            evidence.get("actionTargetClass"),
            evidence.get("actionPositionClass"),
            evidence.get("helperBodyClass"),
            "targetRangeLoop" if evidence.get("targetRangeLoop") else None,
            "writesDisplayXy" if evidence.get("writesDisplayXy") else None,
            compact(evidence.get("knownCalls") or [], 3),
        ]
        rows_html.append(
            "<tr>"
            f"<td>{esc(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.get('helperBehavior'))}</small><br><code>{esc(row.get('helperScriptVaHex'))}</code></td>"
            f"<td><code>{esc(compact(target, 7))}</code></td>"
            f"<td><code>{esc(compact(row['helperFrames'], 24))}</code></td>"
            f"<td><code>{esc(compact(rects, 4))}</code></td>"
            f"<td>{'yes' if row['displayFlagBlink'] else '-'}</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: 980px; 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_hogyeok_effect_review.json">JSON</a></p>
  <h1>호격권 이펙트 해석</h1>
  <p class="lead">호격권은 울부짖음과 같은 호랑이 얼굴 프레임을 재사용하지만, EXE action row와 helper body가 단일 타겟 range에 display object를 생성하는 형태로 잡힙니다. 따라서 타겟 몬스터 위 오버레이로 해석합니다.</p>
  <table>
    <thead><tr><th>Lv</th><th>skill</th><th>actor frames</th><th>WLK</th><th>helper</th><th>target evidence</th><th>effect frames</th><th>rect</th><th>blink</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_hogyeok_effect_review.json").write_text(
        json.dumps(report, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    print("wrote out/battle_hogyeok_effect_review.json")


if __name__ == "__main__":
    main()
