#!/usr/bin/env python3
"""Build a focused review for battle helper child-spawn/motion effects.

The battle skill timeline now has enough EXE-derived helper data to know when
an effect is not a single overlay frame.  This report narrows the remaining
animation problem to helpers that spawn child visual objects over time, carry
per-child motion, or rely on randomized placement ranges.
"""

from __future__ import annotations

import html
import json
from collections import Counter, defaultdict
from pathlib import Path
from typing import Any


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

EFFECT_PATTERN = OUT / "battle_effect_animation_pattern_review.json"
HELPER_VISUAL = OUT / "battle_helper_visual_behavior_review.json"
HELPER_SYNC = OUT / "battle_helper_sync_timing_review.json"
HELPER_POSITION = OUT / "battle_helper_position_motion_review.json"
HELPER_OBJECT = OUT / "battle_effect_object_review.json"
HELPER_CHILD = OUT / "battle_helper_child_script_review.json"

OUT_JSON = OUT / "battle_child_spawn_stream_review.json"


STREAM_REQUIREMENTS = {
    "runner must instantiate child objects over time",
    "child motion loop present",
    "random placement/range present",
    "palette transform effect; no CNS frame stream",
}


def load(path: Path) -> dict[str, Any]:
    return json.loads(path.read_text(encoding="utf-8"))


def by_helper(data: dict[str, Any], row_key: str) -> dict[int, dict[str, Any]]:
    out: dict[int, dict[str, Any]] = {}
    for row in data.get(row_key, []):
        helper_id = row.get("helperId")
        if isinstance(helper_id, int):
            out[helper_id] = row
    return out


def as_list(value: Any) -> list[Any]:
    return value if isinstance(value, list) else []


def count_frame_script_events(frame_scripts: list[dict[str, Any]]) -> int:
    total = 0
    for script in frame_scripts:
        events = script.get("events")
        if isinstance(events, list):
            total += len(events)
        elif isinstance(script.get("frameSequence"), list):
            total += len(script["frameSequence"])
        elif isinstance(script.get("frameTimeline"), list):
            total += len(script["frameTimeline"])
    return total


def direct_events(helper_anim: dict[str, Any], visual: dict[str, Any], sync: dict[str, Any]) -> list[dict[str, Any]]:
    candidates = [
        (helper_anim.get("directSpawn") or {}).get("events"),
        (visual.get("directSpawn") or {}).get("events"),
        sync.get("directSpawnFrameEvents"),
    ]
    for candidate in candidates:
        if isinstance(candidate, list) and candidate:
            return candidate
    return []


def frame_scripts(helper_anim: dict[str, Any], visual: dict[str, Any], sync: dict[str, Any]) -> list[dict[str, Any]]:
    for candidate in [
        helper_anim.get("frameScripts"),
        visual.get("frameScripts"),
        sync.get("frameScriptTimelines"),
    ]:
        if isinstance(candidate, list) and candidate:
            return candidate
    return []


def root_counter_sets(helper_anim: dict[str, Any], visual: dict[str, Any], obj: dict[str, Any]) -> dict[str, Any]:
    for candidate in [
        helper_anim.get("rootCounterSets"),
        (visual.get("lifecycle") or {}).get("rootCounterSets"),
        (obj.get("root") or {}).get("rootCounterSets"),
    ]:
        if isinstance(candidate, dict) and candidate:
            return candidate
    return {}


def random_ranges(helper_anim: dict[str, Any], obj: dict[str, Any], child: dict[str, Any]) -> list[Any]:
    candidates = [
        helper_anim.get("randomRanges"),
        (obj.get("root") or {}).get("randomRanges"),
        (child.get("featureSummary") or {}).get("randomRanges"),
    ]
    for candidate in candidates:
        if isinstance(candidate, list) and candidate:
            return candidate
    return []


def transform_status_counts(events: list[dict[str, Any]]) -> dict[str, int]:
    counts: Counter[str] = Counter()
    for event in events:
        counts[str(event.get("transformStatus") or "unreported")] += 1
    return dict(sorted(counts.items()))


def render_status(
    requirements: list[str],
    helper_rows: list[dict[str, Any]],
) -> tuple[str, list[str]]:
    problems: list[str] = []
    if "palette transform effect; no CNS frame stream" in requirements:
        problems.append("CNS 프레임이 아니라 팔레트/플래시 분기라 별도 renderer가 필요하다.")
        return "palette-branch-needed", problems

    has_child_stream = any(row["directSpawnEventCount"] > 1 for row in helper_rows)
    has_frame_script = any(row["frameScriptEventCount"] > 0 for row in helper_rows)
    has_init = any(row["initFrameCount"] > 0 for row in helper_rows)
    has_nested = any(row["nestedVisualFrameCount"] > 0 for row in helper_rows)
    has_motion_loop = "child motion loop present" in requirements or any(row["childMotionLoopCount"] > 0 for row in helper_rows)
    has_random = "random placement/range present" in requirements or any(row["randomRangeCount"] > 0 for row in helper_rows)

    if not any([has_child_stream, has_frame_script, has_init, has_nested]):
        return "control-or-motion-only", ["시각 frame 근거보다 actor/helper control 근거가 중심이다."]

    if has_child_stream:
        problems.append("spawn된 child를 단일 overlay가 아니라 독립 visual object 수명으로 유지해야 한다.")
    if has_motion_loop:
        problems.append("child motion loop를 gate 동안 누적 적용해야 한다.")
    if has_random:
        problems.append("위치 범위/난수 분산은 계산식 기반으로 재현해야 한다.")
    if has_nested:
        problems.append("spawn tree의 nested frameScript draw order를 보존해야 한다.")

    if problems:
        return "stream-known-renderer-parity-needed", problems
    return "frame-stream-known", []


def helper_summary(
    helper_anim: dict[str, Any],
    visual_idx: dict[int, dict[str, Any]],
    sync_idx: dict[int, dict[str, Any]],
    position_idx: dict[int, dict[str, Any]],
    object_idx: dict[int, dict[str, Any]],
    child_idx: dict[int, dict[str, Any]],
) -> dict[str, Any]:
    helper_id = int(helper_anim.get("helperId"))
    visual = visual_idx.get(helper_id, {})
    sync = sync_idx.get(helper_id, {})
    position = position_idx.get(helper_id, {})
    obj = object_idx.get(helper_id, {})
    child = child_idx.get(helper_id, {})

    direct = direct_events(helper_anim, visual, sync)
    scripts = frame_scripts(helper_anim, visual, sync)
    nested = as_list(visual.get("nestedVisualFrames"))
    init = as_list(helper_anim.get("initFrames")) or as_list(visual.get("initFrames"))
    loops = as_list(helper_anim.get("childMotionLoops")) or as_list((visual.get("lifecycle") or {}).get("childMotionLoops"))
    ranges = random_ranges(helper_anim, obj, child)

    return {
        "helperId": helper_id,
        "animationClass": helper_anim.get("animationClass"),
        "behaviorClass": helper_anim.get("behaviorClass") or visual.get("behaviorClass") or obj.get("helperClass"),
        "childScriptVaHex": visual.get("childScriptVaHex") or sync.get("childScriptVaHex") or obj.get("childScriptVaHex"),
        "directSpawnEventCount": len(direct),
        "directSpawnTimeline": [event.get("label") or f"{event.get('frame')}@{event.get('tick')}" for event in direct[:18]],
        "directSpawnTickDeltas": (visual.get("lifecycle") or {}).get("tickDeltas")
        or ((helper_anim.get("directSpawn") or {}).get("ticks") or [])[:18],
        "frameScriptCount": len(scripts),
        "frameScriptEventCount": count_frame_script_events(scripts),
        "frameScriptTargets": sorted({str(script.get("targetVaHex")) for script in scripts if script.get("targetVaHex")}),
        "initFrameCount": len(init),
        "initFrames": [
            item.get("label") or f"sprite={item.get('spriteHex')} frame={item.get('frame')}"
            for item in init[:12]
        ],
        "nestedVisualFrameCount": len(nested),
        "nestedVisualFrames": [
            f"{item.get('source')}#{item.get('frame')}@{item.get('targetVaHex')}"
            for item in nested[:12]
        ],
        "rootCounterSets": root_counter_sets(helper_anim, visual, obj),
        "childMotionLoopCount": len(loops),
        "randomRangeCount": len(ranges),
        "positionPatterns": position.get("patterns") or [],
        "motionModeCounts": position.get("motionModeCounts") or {},
        "transformStatusCounts": transform_status_counts(direct),
        "executionRequirements": helper_anim.get("executionRequirements") or [],
        "reviewFlags": helper_anim.get("reviewFlags") or [],
    }


def row_is_relevant(row: dict[str, Any]) -> bool:
    requirements = set(row.get("executionRequirements") or [])
    if requirements & STREAM_REQUIREMENTS:
        return True
    for helper in row.get("helperAnimations") or []:
        if helper.get("animationClass") in {
            "counter-driven-burst-spawn",
            "direct-spawn-stream",
            "target-range-init-frame-effect",
        }:
            return True
        if (helper.get("directSpawn") or {}).get("count", 0) > 1:
            return True
    return False


def build_report() -> dict[str, Any]:
    effect = load(EFFECT_PATTERN)
    visual_idx = by_helper(load(HELPER_VISUAL), "rows")
    sync_idx = by_helper(load(HELPER_SYNC), "rows")
    position_idx = by_helper(load(HELPER_POSITION), "helperRows")
    object_idx = by_helper(load(HELPER_OBJECT), "helperRows")
    child_idx = by_helper(load(HELPER_CHILD), "helperRows")

    rows: list[dict[str, Any]] = []
    for skill in effect.get("skillRows", []):
        if not row_is_relevant(skill):
            continue
        helper_rows = [
            helper_summary(helper, visual_idx, sync_idx, position_idx, object_idx, child_idx)
            for helper in skill.get("helperAnimations") or []
        ]
        requirements = skill.get("executionRequirements") or []
        status, remaining = render_status(requirements, helper_rows)
        rows.append(
            {
                "recordKey": skill.get("recordKey"),
                "ownerName": skill.get("ownerName"),
                "skillName": skill.get("skillName"),
                "skillIdHex": skill.get("skillIdHex"),
                "levelOrFixed": skill.get("levelOrFixed"),
                "renderTrack": skill.get("renderTrack"),
                "effectAnimationClass": skill.get("effectAnimationClass"),
                "helperIds": skill.get("helperIds") or [],
                "helperAnimationClasses": skill.get("helperAnimationClasses") or [],
                "executionRequirements": requirements,
                "reviewFlags": skill.get("reviewFlags") or [],
                "renderStatus": status,
                "remainingRendererWork": remaining,
                "helpers": helper_rows,
            }
        )

    requirement_counts: Counter[str] = Counter()
    render_status_counts: Counter[str] = Counter()
    helper_class_counts: Counter[str] = Counter()
    owner_counts: Counter[str] = Counter()
    for row in rows:
        owner_counts[row["ownerName"]] += 1
        render_status_counts[row["renderStatus"]] += 1
        for req in row["executionRequirements"]:
            requirement_counts[req] += 1
        for helper in row["helpers"]:
            helper_class_counts[str(helper.get("animationClass"))] += 1

    return {
        "version": 1,
        "kind": "hwanse-battle-child-spawn-stream-review",
        "source": "tools/build_battle_child_spawn_stream_review.py",
        "runtimeUsed": False,
        "inputs": [
            str(EFFECT_PATTERN.relative_to(ROOT)),
            str(HELPER_VISUAL.relative_to(ROOT)),
            str(HELPER_SYNC.relative_to(ROOT)),
            str(HELPER_POSITION.relative_to(ROOT)),
            str(HELPER_OBJECT.relative_to(ROOT)),
            str(HELPER_CHILD.relative_to(ROOT)),
        ],
        "status": "static-child-spawn-stream-reviewed",
        "summary": {
            "reviewRows": len(rows),
            "uniqueHelpers": len({helper_id for row in rows for helper_id in row["helperIds"]}),
            "ownerCounts": dict(sorted(owner_counts.items())),
            "requirementCounts": dict(sorted(requirement_counts.items())),
            "renderStatusCounts": dict(sorted(render_status_counts.items())),
            "helperAnimationClassCounts": dict(sorted(helper_class_counts.items())),
        },
        "interpretationNotes": [
            "EXE helper evidence separates actor frames from child visual objects. Child stream skills must create multiple child visual objects over time.",
            "This report does not invent visual behavior. It only combines already decoded helper direct-spawn, frameScript, nested frame, range, counter, and motion rows.",
            "A renderStatus of stream-known-renderer-parity-needed means the source frame/motion evidence is present, but the web renderer still needs exact independent child lifetime/draw-order behavior for parity.",
            "RNG seed capture is not required for static parity; the decoded ranges and repeat counters are sufficient to implement deterministic preview variants.",
        ],
        "rows": rows,
    }


def badge(text: str) -> str:
    return f"<span class='badge'>{html.escape(text)}</span>"


def render_list(items: list[Any], limit: int = 12) -> str:
    if not items:
        return "<span class='muted'>none</span>"
    body = "<br>".join(html.escape(str(item)) for item in items[:limit])
    if len(items) > limit:
        body += f"<br><span class='muted'>+{len(items) - limit} more</span>"
    return body


def render_dict(value: dict[str, Any]) -> str:
    if not value:
        return "<span class='muted'>none</span>"
    return "<br>".join(f"<code>{html.escape(str(k))}</code>: {html.escape(str(v))}" for k, v in value.items())


def render_html(report: dict[str, Any]) -> str:
    summary = report["summary"]
    summary_cards = "\n".join(
        f"<div class='card'><strong>{html.escape(key)}</strong><pre>{html.escape(json.dumps(value, ensure_ascii=False, indent=2))}</pre></div>"
        for key, value in summary.items()
    )
    rows_html = []
    for row in report["rows"]:
        helper_blocks = []
        for helper in row["helpers"]:
            helper_blocks.append(
                "<details class='helper' open>"
                f"<summary>helper #{helper['helperId']} · {html.escape(str(helper.get('animationClass')))} · {html.escape(str(helper.get('behaviorClass')))}</summary>"
                "<div class='helper-grid'>"
                f"<div><b>spawn</b><br>direct {helper['directSpawnEventCount']} / scripts {helper['frameScriptEventCount']} / init {helper['initFrameCount']} / nested {helper['nestedVisualFrameCount']}</div>"
                f"<div><b>timeline</b><br>{render_list(helper['directSpawnTimeline'], 10)}</div>"
                f"<div><b>scripts</b><br>{render_list(helper['frameScriptTargets'], 8)}</div>"
                f"<div><b>motion/range</b><br>loops {helper['childMotionLoopCount']} / ranges {helper['randomRangeCount']}<br>{render_list(helper['positionPatterns'], 8)}</div>"
                f"<div><b>counters</b><br>{render_dict(helper['rootCounterSets'])}</div>"
                f"<div><b>transform</b><br>{render_dict(helper['transformStatusCounts'])}</div>"
                "</div></details>"
            )
        rows_html.append(
            "<tr>"
            f"<td><b>{html.escape(row['ownerName'])}</b><br>{html.escape(row['skillName'])}<br><code>{html.escape(row['skillIdHex'])}</code> Lv={html.escape(str(row['levelOrFixed']))}</td>"
            f"<td>{badge(row['effectAnimationClass'])}<br>{''.join(badge(item) for item in row['helperAnimationClasses'])}</td>"
            f"<td>{render_list(row['executionRequirements'], 8)}<hr>{render_list(row['reviewFlags'], 8)}</td>"
            f"<td><b>{html.escape(row['renderStatus'])}</b><br>{render_list(row['remainingRendererWork'], 8)}</td>"
            f"<td>{''.join(helper_blocks)}</td>"
            "</tr>"
        )
    notes = "".join(f"<li>{html.escape(note)}</li>" for note in report["interpretationNotes"])
    return f"""<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Battle Child Spawn Stream Review</title>
  <style>
    body {{ margin: 0; padding: 24px; background: #111318; color: #eceff4; font: 14px/1.5 system-ui, sans-serif; }}
    a {{ color: #8ec5ff; }}
    h1 {{ margin: 0 0 8px; font-size: 24px; }}
    .muted {{ color: #9aa4b2; }}
    .cards {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; margin: 16px 0; }}
    .card {{ border: 1px solid #2b3342; border-radius: 8px; padding: 12px; background: #171b23; }}
    pre {{ white-space: pre-wrap; margin: 8px 0 0; color: #b8c4d6; }}
    table {{ width: 100%; border-collapse: collapse; margin-top: 18px; }}
    th, td {{ border-top: 1px solid #2b3342; padding: 10px; vertical-align: top; }}
    th {{ text-align: left; position: sticky; top: 0; background: #111318; z-index: 1; }}
    code {{ color: #ffd38a; }}
    hr {{ border: 0; border-top: 1px solid #2b3342; margin: 8px 0; }}
    .badge {{ display: inline-block; margin: 0 4px 4px 0; padding: 2px 6px; border: 1px solid #3a4557; border-radius: 999px; background: #202838; color: #d7e3f4; font-size: 12px; }}
    details.helper {{ margin: 0 0 8px; border: 1px solid #2b3342; border-radius: 8px; background: #151922; }}
    details.helper summary {{ cursor: pointer; padding: 8px 10px; }}
    .helper-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 8px; padding: 0 10px 10px; }}
    .helper-grid > div {{ background: #10141b; border-radius: 6px; padding: 8px; }}
  </style>
</head>
<body>
  <h1>Battle Child Spawn Stream Review</h1>
  <p class="muted">정적 EXE helper 분석에서 child visual spawn, nested frameScript, motion loop, random range가 필요한 기술만 모은 검토표입니다.</p>
  <p><a href="../web/index.html">index</a> · <a href="../web/battle_skill_timeline_review.html">skill timeline review</a></p>
  <div class="cards">{summary_cards}</div>
  <ul>{notes}</ul>
  <table>
    <thead><tr><th>기술</th><th>분류</th><th>요구/플래그</th><th>렌더 상태</th><th>helper 증거</th></tr></thead>
    <tbody>{''.join(rows_html)}</tbody>
  </table>
</body>
</html>
"""


def main() -> None:
    report = build_report()
    OUT_JSON.write_text(json.dumps(report, ensure_ascii=False, separators=(",", ":")) + "\n", encoding="utf-8")
    print(json.dumps(report["summary"], ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()
