#!/usr/bin/env python3
"""Aggregate all promoted battle skill pattern evidence by skill.

The lower-level reports intentionally stay close to one subsystem: actor VM
timeline, helper visual behavior, sound roles, caster placement, and render
track classification.  This report joins those promoted facts so a single skill
row shows every EXE-derived pattern currently known.
"""

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"
WEB = ROOT / "web"
OUT_JSON = OUT / "battle_skill_complete_pattern_review.json"
OUT_MD = OUT / "battle_skill_complete_pattern_review.md"
WEB_HTML = WEB / "battle_skill_complete_pattern_review.html"


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


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


def key_for(row: dict[str, Any]) -> str:
    return f"{row.get('ownerKey')}:{str(row.get('skillIdHex')).lower()}"


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


def unique(values: list[Any]) -> list[Any]:
    out: list[Any] = []
    seen: set[str] = set()
    for value in values:
        marker = json.dumps(value, ensure_ascii=False, sort_keys=True)
        if marker in seen:
            continue
        seen.add(marker)
        out.append(value)
    return out


def index_rows(report: dict[str, Any], row_key: str = "rows") -> dict[str, dict[str, Any]]:
    return {
        key_for(row): row
        for row in report.get(row_key) or []
        if row.get("ownerKey") and row.get("skillIdHex")
    }


def canonical_index(report: dict[str, Any]) -> dict[str, dict[str, Any]]:
    out: dict[str, dict[str, Any]] = {}
    for row in report.get("playerActions") or []:
        key = str(row.get("key") or "")
        if key:
            out[key] = row
        if row.get("ownerKey") and row.get("skillIdHex"):
            out[f"player:{row.get('ownerKey')}:{str(row.get('skillIdHex')).lower()}"] = row
            out[f"{row.get('ownerKey')}:{str(row.get('skillIdHex')).lower()}"] = row
    return out


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


def family_pattern_index(report: dict[str, Any]) -> dict[tuple[str, str], dict[str, Any]]:
    out: dict[tuple[str, str], dict[str, Any]] = {}
    for group in report.get("groups") or []:
        owner = str(group.get("ownerName") or "")
        for skill_id in group.get("skillIds") or []:
            out[(owner, str(skill_id).lower())] = group
    return out


def helper_facts(helper_ids: list[Any], helper_by_id: dict[int, dict[str, Any]]) -> list[dict[str, Any]]:
    facts: list[dict[str, Any]] = []
    for raw_id in helper_ids or []:
        try:
            helper_id = int(raw_id)
        except (TypeError, ValueError):
            continue
        helper = helper_by_id.get(helper_id)
        if not helper:
            facts.append({"helperId": helper_id, "status": "missing-helper-visual-row"})
            continue
        lifecycle = helper.get("lifecycle") or {}
        facts.append(
            {
                "helperId": helper_id,
                "behaviorClass": helper.get("behaviorClass"),
                "frameScripts": helper.get("frameScripts") or [],
                "directSpawnCount": (helper.get("directSpawn") or {}).get("count") or 0,
                "directSpawnFrames": (helper.get("directSpawn") or {}).get("frameTimeline") or [],
                "emitFrames": lifecycle.get("emitFrames") or [],
                "rootCounterSets": lifecycle.get("rootCounterSets") or {},
                "rootBranches": lifecycle.get("rootBranches") or [],
                "childMotionLoops": lifecycle.get("childMotionLoops") or [],
                "interpretation": lifecycle.get("interpretation") or [],
                "notes": helper.get("notes") or [],
            }
        )
    return facts


def pattern_facts(row: dict[str, Any]) -> list[str]:
    facts: list[str] = []
    if row.get("canonicalTimelineStatus"):
        facts.append(f"canonical:{row.get('canonicalTimelineStatus')}")
    if row.get("canonicalAlignmentClass"):
        facts.append(f"alignment:{row.get('canonicalAlignmentClass')}")
    if row.get("frameSequence"):
        facts.append("actor-frame-sequence")
    if row.get("expandedFrameGateSequence"):
        facts.append("actor-frame-gates")
    if row.get("repeatLoopCount") or row.get("expandedRepeatLoopCount"):
        facts.append("repeat-loop-expanded")
    if row.get("effectWlkNos"):
        facts.append("0x24-effect/cast-wlk")
    if row.get("soundWlkNos") or row.get("resultWlkNos"):
        facts.append("0xc2-result-wlk")
    if row.get("helperIds"):
        facts.append("0xbd-helper-call")
    if row.get("hitEventCount"):
        facts.append("confirmed-hit-window")
    if row.get("casterRule"):
        facts.append("caster-position-rule")
    if row.get("renderTrack"):
        facts.append(f"render:{row.get('renderTrack')}")
    if row.get("soundModel"):
        facts.append(f"sound:{row.get('soundModel')}")
    for helper in row.get("helperFacts") or []:
        behavior = helper.get("behaviorClass")
        if behavior:
            facts.append(f"helper:{behavior}")
        if helper.get("childMotionLoops"):
            facts.append("helper-child-motion-loop")
        if helper.get("rootCounterSets"):
            facts.append("helper-root-counters")
    return unique(facts)


def canonical_confirmed(row: dict[str, Any]) -> bool:
    return (
        row.get("canonicalTimelineStatus") == "confirmed"
        and row.get("canonicalAlignmentClass") in {"visual-confirmed", "start-confirmed"}
    )


def effective_implementation_state(row: dict[str, Any]) -> str:
    state = str(row.get("implementationState") or "")
    if canonical_confirmed(row) and state in {
        "needs-child-helper-script-runner",
        "needs-helper-target-promotion",
    }:
        return "canonical-confirmed; runner consumes canonical timeline"
    if canonical_confirmed(row) and state == "needs-ui/status-effect-policy":
        return "canonical-confirmed; UI/status policy"
    return state or "-"


def unresolved_flags(row: dict[str, Any]) -> list[str]:
    flags: list[str] = []
    state = str(row.get("implementationState") or "")
    confirmed = canonical_confirmed(row)
    if state.startswith("needs-") and not confirmed:
        flags.append(state)
    if row.get("helperIds") and not row.get("helperFacts") and not confirmed:
        flags.append("helper visual row missing")
    if (
        any((helper.get("behaviorClass") == "no-visual-frame-detected") for helper in row.get("helperFacts") or [])
        and not confirmed
    ):
        flags.append("helper has no visual frame detected")
    if not row.get("positionBucket"):
        flags.append("position bucket missing")
    if (
        not row.get("hitEventCount")
        and row.get("targetClass") not in {"ally-single", "ally-all", "self"}
        and not confirmed
    ):
        flags.append("no hit window in actor VM")
    return unique(flags)


def policy_flags(row: dict[str, Any]) -> list[str]:
    flags: list[str] = []
    if row.get("renderTrack") in {"support-or-status"}:
        flags.append("support/status UI policy")
    return unique(flags)


def build() -> dict[str, Any]:
    action = load_json("battle_action_event_timeline_review.json")
    effect = load_json("battle_effect_semantics_review.json")
    position = load_json("battle_caster_position_rule_review.json")
    frame_gate = load_json("battle_frame_gate_review.json")
    sound = load_json("battle_sound_role_review.json")
    skill_pattern = load_json("battle_skill_pattern_review.json")
    helper_visual = load_json("battle_helper_visual_behavior_review.json")
    canonical = load_json("battle_skill_timeline_canonical.json")

    action_by_key = index_rows(action)
    effect_by_key = index_rows(effect)
    position_by_key = index_rows(position, "rules")
    frame_by_key = index_rows(frame_gate)
    sound_by_key = index_rows(sound, "skillRows")
    helper_by_id = helper_index(helper_visual)
    pattern_by_owner_skill = family_pattern_index(skill_pattern)
    canonical_by_key = canonical_index(canonical)

    keys = sorted(set(action_by_key) | set(effect_by_key) | set(position_by_key) | set(frame_by_key) | set(sound_by_key))
    rows: list[dict[str, Any]] = []
    for key in keys:
        action_row = action_by_key.get(key, {})
        effect_row = effect_by_key.get(key, {})
        position_row = position_by_key.get(key, {})
        frame_row = frame_by_key.get(key, {})
        sound_row = sound_by_key.get(key, {})
        owner_name = action_row.get("ownerName") or effect_row.get("ownerName") or position_row.get("ownerName") or sound_row.get("ownerName")
        skill_id = action_row.get("skillIdHex") or effect_row.get("skillIdHex") or position_row.get("skillIdHex") or sound_row.get("skillIdHex")
        owner_key = action_row.get("ownerKey") or effect_row.get("ownerKey") or position_row.get("ownerKey") or sound_row.get("ownerKey")
        canonical_row = canonical_by_key.get(f"player:{owner_key}:{str(skill_id).lower()}") or canonical_by_key.get(key) or {}
        family_group = pattern_by_owner_skill.get((str(owner_name or ""), str(skill_id).lower()), {})
        helper_ids = (
            action_row.get("helperIds")
            or effect_row.get("helperIds")
            or sound_row.get("helperIds")
            or position_row.get("evidence", {}).get("helperIds")
            or []
        )
        helper_rows = helper_facts(helper_ids, helper_by_id)
        merged = {
            "recordKey": key,
            "ownerKey": owner_key,
            "ownerName": owner_name,
            "skillName": action_row.get("skillName") or effect_row.get("skillName") or position_row.get("skillName") or sound_row.get("skillName"),
            "familyName": action_row.get("familyName") or effect_row.get("familyName") or position_row.get("familyName") or sound_row.get("familyName"),
            "skillIdHex": skill_id,
            "levelOrFixed": action_row.get("levelOrFixed") if action_row.get("levelOrFixed") is not None else effect_row.get("levelOrFixed"),
            "targetClass": effect_row.get("targetClass") or position_row.get("targetClass"),
            "mpCost": effect_row.get("mpCost"),
            "hitCount": effect_row.get("hitCount") or len((action_row.get("timeline") or {}).get("hitEvents") or []),
            "frameSequence": action_row.get("frameSequence") or effect_row.get("frameSequence") or [],
            "expandedFrameGateSequence": frame_row.get("expandedFrameGateSequence") or [],
            "durationGate": action_row.get("durationGateFromFrames") or frame_row.get("expandedDurationGate"),
            "repeatLoopCount": action_row.get("repeatLoopCount") or frame_row.get("expandedRepeatLoopCount") or 0,
            "hitEventCount": action_row.get("hitEventCount") or frame_row.get("confirmedHitEventCount") or 0,
            "hitClassCounts": action_row.get("hitClassCounts") or {},
            "effectWlkNos": action_row.get("effectWlkNos") or effect_row.get("effectWlkNos") or [],
            "resultWlkNos": action_row.get("soundWlkNos") or effect_row.get("resultWlkNos") or [],
            "soundModel": sound_row.get("soundModel"),
            "soundCalls": sound_row.get("calls") or {},
            "helperIds": helper_ids,
            "helperFacts": helper_rows,
            "helperRole": effect_row.get("helperRole") or action_row.get("helperRole"),
            "renderTrack": effect_row.get("renderTrack"),
            "implementationState": effect_row.get("implementationState"),
            "effectiveImplementationState": None,
            "semanticNote": effect_row.get("semanticNote"),
            "canonicalKey": canonical_row.get("key"),
            "canonicalTimelineStatus": canonical_row.get("timelineStatus") or canonical_row.get("status"),
            "canonicalVisualAlignmentStatus": canonical_row.get("visualAlignmentStatus"),
            "canonicalAlignmentClass": canonical_row.get("alignmentClass"),
            "canonicalAlignmentLabel": canonical_row.get("alignmentLabel"),
            "canonicalDisplayVmStartVaHex": canonical_row.get("displayVmStartVaHex"),
            "canonicalDisplayStartSource": canonical_row.get("displayStartSource"),
            "canonicalWlkEvidenceStatus": canonical_row.get("wlkEvidenceStatus"),
            "canonicalHelperBodyOnlyIds": canonical_row.get("helperBodyOnlyIds") or [],
            "positionClass": effect_row.get("positionClass"),
            "casterRule": position_row.get("casterRule"),
            "startAnchor": position_row.get("startAnchor"),
            "motionKind": position_row.get("motionKind"),
            "motionTarget": position_row.get("motionTarget"),
            "positionConfidence": position_row.get("confidence"),
            "positionBucket": position_row.get("positionBucket"),
            "positionBucketLabel": position_row.get("positionBucketLabel"),
            "battleMapPlacementSummary": position_row.get("battleMapPlacementSummary"),
            "evidenceTier": position_row.get("evidenceTier"),
            "evidenceTierLabel": position_row.get("evidenceTierLabel"),
            "hasExplicitMovementOpcode": position_row.get("hasExplicitMovementOpcode"),
            "positionNotes": position_row.get("notes") or [],
            "familyPatternClass": family_group.get("classification"),
            "familyPatternNotes": family_group.get("notes") or [],
            "timelineEvents": (action_row.get("timeline") or {}).get("events") or [],
            "hitEvents": (action_row.get("timeline") or {}).get("hitEvents") or [],
        }
        merged["effectiveImplementationState"] = effective_implementation_state(merged)
        merged["patternFacts"] = pattern_facts(merged)
        merged["policyFlags"] = policy_flags(merged)
        merged["unresolvedFlags"] = unresolved_flags(merged)
        rows.append(merged)

    summary = {
        "rows": len(rows),
        "rowsWithHelper": sum(1 for row in rows if row["helperIds"]),
        "rowsWithHelperLifecycle": sum(1 for row in rows if any(helper.get("interpretation") for helper in row["helperFacts"])),
        "rowsWithChildMotionLoop": sum(1 for row in rows if any(helper.get("childMotionLoops") for helper in row["helperFacts"])),
        "rowsWithBlinkHelper": sum(1 for row in rows if any(helper.get("behaviorClass") == "display-flag-blink-frameScript" for helper in row["helperFacts"])),
        "rowsWithRepeatLoop": sum(1 for row in rows if row["repeatLoopCount"]),
        "rowsWithSupportPolicy": sum(1 for row in rows if row["policyFlags"]),
        "rowsWithUnresolvedFlags": sum(1 for row in rows if row["unresolvedFlags"]),
        "canonicalConfirmedRows": sum(1 for row in rows if canonical_confirmed(row)),
        "legacyNeedsOverriddenByCanonical": sum(
            1
            for row in rows
            if canonical_confirmed(row)
            and str(row.get("implementationState") or "") in {"needs-child-helper-script-runner", "needs-helper-target-promotion"}
        ),
        "renderTrackCounts": dict(Counter(row.get("renderTrack") or "-" for row in rows)),
        "implementationStateCounts": dict(Counter(row.get("implementationState") or "-" for row in rows)),
        "effectiveImplementationStateCounts": dict(Counter(row.get("effectiveImplementationState") or "-" for row in rows)),
        "canonicalTimelineStatusCounts": dict(Counter(row.get("canonicalTimelineStatus") or "-" for row in rows)),
        "canonicalAlignmentClassCounts": dict(Counter(row.get("canonicalAlignmentClass") or "-" for row in rows)),
        "casterRuleCounts": dict(Counter(row.get("casterRule") or "-" for row in rows)),
        "positionBucketCounts": dict(Counter(row.get("positionBucketLabel") or "-" for row in rows)),
        "positionEvidenceTierCounts": dict(Counter(row.get("evidenceTierLabel") or "-" for row in rows)),
        "familyPatternCounts": dict(Counter(row.get("familyPatternClass") or "-" for row in rows)),
        "helperBehaviorCounts": dict(Counter(helper.get("behaviorClass") or "-" for row in rows for helper in row["helperFacts"])),
        "soundModelCounts": dict(Counter(row.get("soundModel") or "-" for row in rows)),
    }
    return {
        "version": 1,
        "kind": "hwanse-battle-skill-complete-pattern-review",
        "source": [
            "out/battle_action_event_timeline_review.json",
            "out/battle_effect_semantics_review.json",
            "out/battle_caster_position_rule_review.json",
            "out/battle_frame_gate_review.json",
            "out/battle_sound_role_review.json",
            "out/battle_skill_pattern_review.json",
            "out/battle_helper_visual_behavior_review.json",
            "out/battle_skill_timeline_canonical.json",
        ],
        "status": "aggregated-skill-pattern-evidence",
        "summary": summary,
        "interpretationNotes": [
            "이 파일은 새 분석을 invent하지 않고, 이미 EXE에서 승격된 actor VM, helper VM, WLK, 위치 규칙, render track 정보를 기술 단위로 결합한다.",
            "helper lifecycle은 반복 child spawn, root counter, child motion loop 등 helper 내부 흐름을 의미한다.",
            "battle_skill_timeline_canonical.json이 confirmed로 승격한 기술은 구 implementationState의 needs-child-helper-script-runner/needs-helper-target-promotion보다 우선한다.",
            "support/status UI policy는 공격 타임라인 누락이 아니라 도주, 방어, 회복, 취기/도발/눈요기 같은 UI/상태 처리 분기다.",
            "positionConfidence가 medium인 항목은 EXE 근거는 있으나 최종 화면 좌표/상황별 배치가 더 필요할 수 있다.",
        ],
        "rows": rows,
        "supportPolicyRows": [row for row in rows if row["policyFlags"]],
        "unresolvedRows": [row for row in rows if row["unresolvedFlags"]],
    }


def markdown(report: dict[str, Any]) -> str:
    lines = [
        "# Battle Skill Complete Pattern Review",
        "",
        f"- status: `{report['status']}`",
        f"- rows: `{report['summary']['rows']}`",
        f"- rows with helper lifecycle: `{report['summary']['rowsWithHelperLifecycle']}`",
        f"- rows with child motion loop: `{report['summary']['rowsWithChildMotionLoop']}`",
        f"- rows with support/status policy: `{report['summary']['rowsWithSupportPolicy']}`",
        f"- rows with unresolved flags: `{report['summary']['rowsWithUnresolvedFlags']}`",
        "",
        "## Interpretation",
        "",
    ]
    lines.extend(f"- {note}" for note in report["interpretationNotes"])
    lines.extend(
        [
            "",
            "## Skill Rows",
            "",
            "| actor | skill | id | family pattern | render | position | frames | hit | WLK | helpers | policy | unresolved |",
            "|---|---|---:|---|---|---|---|---:|---|---|---|---|",
        ]
    )
    for row in report["rows"]:
        helper = compact([f"#{hid}" for hid in row["helperIds"]], 6)
        wlk = f"fx {compact(row['effectWlkNos'], 4)} / hit {compact(row['resultWlkNos'], 4)}"
        lines.append(
            f"| {row['ownerName']} | {row['skillName']} | `{row['skillIdHex']}` | "
            f"`{row.get('familyPatternClass') or '-'}` | `{row.get('renderTrack') or '-'}` | "
            f"`{row.get('positionBucketLabel') or row.get('casterRule') or '-'}` | {compact(row['frameSequence'], 10)} | {row['hitEventCount']} | "
            f"{wlk} | {helper} | {compact(row['policyFlags'], 4)} | "
            f"{compact(row['unresolvedFlags'], 4)} / `{row.get('effectiveImplementationState') or '-'}` |"
        )
    return "\n".join(lines) + "\n"


def dict_table(payload: dict[str, Any]) -> str:
    return "".join(f"<tr><td>{esc(key)}</td><td><code>{esc(value)}</code></td></tr>" for key, value in payload.items())


def helper_html(row: dict[str, Any]) -> str:
    if not row.get("helperFacts"):
        return "-"
    parts = []
    for helper in row["helperFacts"]:
        lifecycle = compact(helper.get("interpretation") or [], 3)
        direct = compact(helper.get("directSpawnFrames") or [], 6)
        parts.append(
            f"<details><summary>helper {esc(helper.get('helperId'))} · {esc(helper.get('behaviorClass'))}</summary>"
            f"<p>direct: {esc(direct)}</p><p>{esc(lifecycle)}</p></details>"
        )
    return "".join(parts)


def row_detail_html(row: dict[str, Any]) -> str:
    facts = "".join(f"<span class='badge'>{esc(item)}</span>" for item in row["patternFacts"])
    policy = "".join(f"<span class='badge policy'>{esc(item)}</span>" for item in row["policyFlags"]) or "-"
    unresolved = "".join(f"<span class='badge warn'>{esc(item)}</span>" for item in row["unresolvedFlags"]) or "-"
    canonical = (
        f"{row.get('canonicalTimelineStatus') or '-'} / "
        f"{row.get('canonicalAlignmentClass') or '-'} / "
        f"{row.get('canonicalDisplayVmStartVaHex') or '-'}"
    )
    gates = compact([f"{item.get('frame')}@{item.get('gate')}" for item in row.get("expandedFrameGateSequence") or []], 16)
    hits = compact([f"t{hit.get('tick')} {hit.get('classification')}" for hit in row.get("hitEvents") or []], 8)
    return (
        "<div class='detail-grid'>"
        f"<div><b>facts</b><br>{facts}</div>"
        f"<div><b>policy</b><br>{policy}</div>"
        f"<div><b>unresolved</b><br>{unresolved}</div>"
        f"<div><b>canonical</b><br>{esc(canonical)}</div>"
        f"<div><b>gates</b><br>{esc(gates)}</div>"
        f"<div><b>hit windows</b><br>{esc(hits)}</div>"
        f"<div><b>helper lifecycle</b><br>{helper_html(row)}</div>"
        f"<div><b>notes</b><br>{esc(compact(row.get('familyPatternNotes') or row.get('positionNotes') or [], 6))}</div>"
        "</div>"
    )


def html_page(report: dict[str, Any]) -> str:
    note_html = "".join(f"<li>{esc(note)}</li>" for note in report["interpretationNotes"])
    rows_html = []
    for row in report["rows"]:
        rows_html.append(
            "<tr>"
            f"<td>{esc(row['ownerName'])}</td>"
            f"<td><b>{esc(row['skillName'])}</b><br><code>{esc(row['skillIdHex'])}</code></td>"
            f"<td><code>{esc(row.get('familyPatternClass') or '-')}</code></td>"
            f"<td>{esc(row.get('renderTrack') or '-')}<br><small>{esc(row.get('effectiveImplementationState') or '-')}</small><br><small>legacy: {esc(row.get('implementationState') or '-')}</small></td>"
            f"<td>{esc(row.get('positionBucketLabel') or '-')}<br><small>{esc(row.get('casterRule') or '-')} / {esc(row.get('motionKind') or '-')} / {esc(row.get('evidenceTierLabel') or '-')}</small></td>"
            f"<td>{esc(compact(row['frameSequence'], 14))}</td>"
            f"<td>{esc(row['hitEventCount'])}</td>"
            f"<td>fx {esc(compact(row['effectWlkNos'], 4))}<br>hit {esc(compact(row['resultWlkNos'], 4))}</td>"
            f"<td>{esc(compact([f'#{hid}' for hid in row['helperIds']], 8))}</td>"
            f"<td>{esc(compact(row['policyFlags'], 4))}</td>"
            f"<td>{esc(compact(row['unresolvedFlags'], 4))}</td>"
            f"<td><details><summary>detail</summary>{row_detail_html(row)}</details></td>"
            "</tr>"
        )
    return f"""<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" href="../favicon.ico">
  <title>Battle Skill Complete Pattern Review</title>
  <style>
    body {{ margin: 20px; background: #101114; color: #f1f3f5; font-family: system-ui, sans-serif; }}
    a {{ color: #9ecbff; }} code {{ color: #ffd37a; }}
    table {{ width: 100%; border-collapse: collapse; }}
    th, td {{ border: 1px solid #30343d; padding: 7px 8px; font-size: 12px; vertical-align: top; }}
    th {{ background: #1a1d24; color: #bac2cf; position: sticky; top: 0; z-index: 2; }}
    tr:nth-child(even) td {{ background: #141820; }}
    .wide {{ overflow: auto; max-height: 78vh; border: 1px solid #30343d; }}
    .summary {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 12px; margin: 16px 0; }}
    .panel {{ border: 1px solid #30343d; border-radius: 8px; padding: 12px; background: #151821; }}
    .badge {{ display: inline-block; margin: 2px; padding: 2px 5px; border-radius: 4px; background: #263349; color: #dce8ff; }}
    .badge.policy {{ background: #244436; color: #bbf7d0; }}
    .badge.warn {{ background: #4a3420; color: #ffd8a8; }}
    .detail-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 10px; min-width: 720px; }}
    details > summary {{ cursor: pointer; color: #ffd37a; }}
  </style>
</head>
<body>
  <h1>Battle Skill Complete Pattern Review</h1>
  <p><a href="../web/index.html">홈</a> · <a href="../web/battle_skill_timeline_review.html">timeline review</a> · <a href="../web/battle_simulator.html">battle simulator</a> · <a href="../out/battle_skill_complete_pattern_review.json">JSON</a> · <a href="../out/battle_skill_complete_pattern_review.md">MD</a></p>
  <div class="summary">
    <section class="panel"><h2>Summary</h2><table><tbody>{dict_table(report['summary'])}</tbody></table></section>
    <section class="panel"><h2>Interpretation</h2><ul>{note_html}</ul></section>
    <section class="panel"><h2>지원/상태 정책</h2><p><code>policyFlags</code> marks support/status UI policy rows without treating them as animation gaps. <code>effectiveImplementationState</code> keeps the canonical EXE implementation state visible.</p></section>
  </div>
  <div class="wide">
    <table>
      <thead>
        <tr><th>actor</th><th>skill</th><th>family pattern</th><th>render</th><th>position</th><th>frames</th><th>hit</th><th>WLK</th><th>helpers</th><th>policy</th><th>unresolved</th><th>detail</th></tr>
      </thead>
      <tbody>{''.join(rows_html)}</tbody>
    </table>
  </div>
</body>
</html>
"""


def main() -> None:
    report = build()
    OUT_JSON.write_text(json.dumps(report, ensure_ascii=False, separators=(",", ":")) + "\n", encoding="utf-8")
    OUT_MD.write_text(markdown(report), encoding="utf-8")
    WEB_HTML.write_text(html_page(report), encoding="utf-8")
    print("wrote out/battle_skill_complete_pattern_review.{json,md}")
    print(f"wrote {WEB_HTML}")


if __name__ == "__main__":
    main()
