#!/usr/bin/env python3
"""Summarize battle effect runner binding against the static EXE evidence.

This report is intentionally about web runner implementation boundaries, not
about promoting or demoting EXE evidence.  The source of truth for effect
requirements is battle_effect_animation_pattern_review.json; this script checks
whether the active web pages are wired to the shared BattleAnimation helper
pipeline that consumes helper sync/visual/position-motion data.
"""

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]
WEB = ROOT / "web"
OUT = ROOT / "out"

EFFECT_PATTERN = OUT / "battle_effect_animation_pattern_review.json"
ENGINE = WEB / "engine" / "battle" / "animation.js"
PAGES = [
    WEB / "battle_skill_timeline_review.html",
    WEB / "battle_formula_calculator.html",
    WEB / "battle_simulator.html",
]

OUT_JSON = OUT / "battle_effect_runner_binding_review.json"
OUT_HTML = OUT / "battle_effect_runner_binding_review.html"


def read(path: Path) -> str:
    return path.read_text(encoding="utf-8")


def load_json(path: Path) -> dict[str, Any]:
    return json.loads(read(path))


def present(text: str, snippet: str) -> bool:
    return snippet in text


def source_checks() -> dict[str, Any]:
    engine = read(ENGINE)
    engine_checks = {
        "helperEffectFrames exported": present(engine, "helperEffectFrames,"),
        "helper effect end extends actor hold": present(engine, "function extendFramesForHelperEffects")
        and present(engine, "helperEffectEndTick"),
        "direct/frameScript/init/nested visual merge": all(
            present(engine, snippet)
            for snippet in [
                "directSource",
                "helper.frameScriptTimelines",
                "visualInitEffectFrames",
                "nestedVisualEffectFrames",
            ]
        ),
        "transform offsets and motion carried": all(
            present(engine, snippet)
            for snippet in ["offsetX", "offsetY", "motionX", "motionY", "helperEffectTransform"]
        ),
        "helper position/motion VM projection": all(
            present(engine, snippet)
            for snippet in [
                "evaluateHelperVmTransform",
                "projectHelperEffectFrames",
                "helperPositionRow",
                "originAnchor",
            ]
        ),
        "shared playback builder exported": present(engine, "buildPlayerPlayback,"),
    }
    page_checks = []
    for path in PAGES:
        text = read(path)
        uses_shared_playback = present(text, "BattleAnimation.buildPlayerPlayback")
        passes_helper_sync = present(text, "helperSyncIndex:")
        passes_helper_visual = present(text, "helperVisualIndex:")
        passes_helper_position = present(text, "helperPositionIndex:")
        page_checks.append(
            {
                "page": str(path.relative_to(ROOT)),
                "loadsCanonicalTimeline": present(text, "battle_skill_timeline_canonical.json"),
                "loadsHelperSync": present(text, "battle_helper_sync_timing_review.json"),
                "loadsHelperVisual": present(text, "battle_helper_visual_behavior_review.json"),
                "loadsHelperPositionMotion": present(text, "battle_helper_position_motion_review.json"),
                "usesSharedPlayback": uses_shared_playback,
                "passesHelperSyncIndex": passes_helper_sync,
                "passesHelperVisualIndex": passes_helper_visual,
                "passesHelperPositionIndex": passes_helper_position,
                "delegatesHelperFrames": present(text, "BattleAnimation.helperEffectFrames")
                or present(text, "buildEffectFrames:")
                or present(text, "helperEffectFrames(")
                or (uses_shared_playback and passes_helper_sync and passes_helper_visual),
                "usesSharedProjection": present(text, "BattleAnimation.projectHelperEffectFrames")
                or (uses_shared_playback and passes_helper_position),
                "usesPlaybackEffectFrames": present(text, "playback.effectFrames"),
                "rendersHelperEffectLayer": present(text, "drawEffectLayer")
                or present(text, "renderEffectStrip")
                or present(text, "effectFramesAtTick")
                or present(text, "pushPlayerHelperEffectFrames")
                or present(text, "playPlayerHelperEffectFrames"),
                "hasOnlyGenericResultEffects": present(text, "state.effects")
                and not present(text, "playback.effectFrames"),
            }
        )
    return {"engine": engine_checks, "pages": page_checks}


def requirement_summary(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
    buckets: dict[str, list[dict[str, Any]]] = defaultdict(list)
    for row in rows:
        for requirement in row.get("executionRequirements") or []:
            buckets[requirement].append(row)

    support_notes = {
        "random placement/range present": {
            "runnerStatus": "shared-engine-vm-projected-preview",
            "evidence": "BattleAnimation now consumes battle_helper_position_motion_review and projects display.x/y, target-cache, delta, and originAnchor.",
            "remaining": "Exact original RNG seed is not required; visual parity still depends on per-helper spawn cadence and child draw order.",
        },
        "runner must instantiate child objects over time": {
            "runnerStatus": "effect-frame-stream-present-needs-per-helper-parity-review",
            "evidence": "helperEffectFrames merges direct spawn, frameScript, init frames, and nested spawn tree frames.",
            "remaining": "The renderer must keep every spawned child as an independent object over its lifetime. This is implemented for timeline preview, but complex burst skills still need visual review.",
        },
        "child motion loop present": {
            "runnerStatus": "motion-fields-carried-preview-sensitive",
            "evidence": "Nested/direct effect frames carry motionX/motionY, motionBasis, and helper VM projection metadata.",
            "remaining": "The page renderer applies motion during the gate, but exact child loop integration and draw-order parity remain review targets.",
        },
        "palette transform effect; no CNS frame stream": {
            "runnerStatus": "classified-separately-no-cns-frame-stream",
            "evidence": "Static pattern marks palette transform as an effect requirement without a CNS frame stream.",
            "remaining": "Renderer needs a palette/flash branch; it should not invent CNS frames.",
        },
    }

    out = []
    for requirement, bucket_rows in sorted(buckets.items()):
        owners = Counter(row.get("ownerName") for row in bucket_rows)
        samples = [
            {
                "owner": row.get("ownerName"),
                "skill": row.get("skillName"),
                "skillIdHex": row.get("skillIdHex"),
                "levelOrFixed": row.get("levelOrFixed"),
                "helperIds": row.get("helperIds") or [],
            }
            for row in bucket_rows[:12]
        ]
        out.append(
            {
                "requirement": requirement,
                "count": len(bucket_rows),
                "owners": dict(sorted(owners.items())),
                "samples": samples,
                **support_notes.get(
                    requirement,
                    {
                        "runnerStatus": "unclassified-runner-requirement",
                        "evidence": "No explicit runner support note yet.",
                        "remaining": "Classify this requirement before treating it as implemented.",
                    },
                ),
            }
        )
    return out


def build_report() -> dict[str, Any]:
    effect = load_json(EFFECT_PATTERN)
    rows = effect.get("skillRows") or []
    checks = source_checks()
    report = {
        "version": 1,
        "kind": "hwanse-battle-effect-runner-binding-review",
        "source": "tools/build_battle_effect_runner_binding_review.py",
        "inputs": [
            "out/battle_effect_animation_pattern_review.json",
            "web/engine/battle/animation.js",
            "web/battle_skill_timeline_review.html",
            "web/battle_formula_calculator.html",
            "web/battle_simulator.html",
        ],
        "status": "runner-binding-reviewed",
        "summary": {
            "effectSkillRows": effect.get("summary", {}).get("skillRows"),
            "skillsWithEffectFrames": effect.get("summary", {}).get("skillsWithEffectFrames"),
            "skillsWithReviewFlags": effect.get("summary", {}).get("skillsWithReviewFlags"),
            "skillsWithExecutionRequirements": effect.get("summary", {}).get("skillsWithExecutionRequirements"),
            "requirementBuckets": len(effect.get("summary", {}).get("executionRequirementCounts") or {}),
            "engineChecksPassed": sum(1 for value in checks["engine"].values() if value),
            "engineChecksTotal": len(checks["engine"]),
            "pageChecksPassed": sum(
                1
                for page in checks["pages"]
                for key, value in page.items()
                if key not in {"page", "usesPlaybackEffectFrames", "rendersHelperEffectLayer", "hasOnlyGenericResultEffects"} and value
            ),
            "pageChecksTotal": sum(len(page) - 4 for page in checks["pages"]),
            "pagesUsingPlaybackEffectFrames": sum(1 for page in checks["pages"] if page.get("usesPlaybackEffectFrames")),
            "pagesRenderingHelperEffectLayer": sum(1 for page in checks["pages"] if page.get("rendersHelperEffectLayer")),
            "pagesWithGenericResultEffectsOnly": sum(1 for page in checks["pages"] if page.get("hasOnlyGenericResultEffects")),
        },
        "engineChecks": checks["engine"],
        "pageChecks": checks["pages"],
        "requirements": requirement_summary(rows),
        "notes": [
            "This report separates EXE-confirmed effect requirements from web runner visual completeness.",
            "Runtime is not used here; all requirement rows come from static EXE-derived helper reports.",
            "A passing binding check means the pages are wired to shared helper data, not that every visual effect is pixel-perfect.",
            "Only pages that use playback.effectFrames and draw an effect layer are full helper-effect visual previews. Other pages may intentionally use actor-only animation plus damage/miss/heal overlays.",
        ],
    }
    return report


def badge(text: str, good: bool | None = None) -> str:
    cls = "neutral" if good is None else ("ok" if good else "bad")
    return f"<span class='badge {cls}'>{html.escape(text)}</span>"


def html_page(report: dict[str, Any]) -> str:
    summary = report["summary"]
    engine_rows = "\n".join(
        f"<tr><td>{html.escape(name)}</td><td>{badge('ok' if value else 'missing', bool(value))}</td></tr>"
        for name, value in report["engineChecks"].items()
    )
    page_rows = []
    for page in report["pageChecks"]:
        binding_states = [
            badge(name.replace("loads", "load ").replace("uses", "use "), value)
            for name, value in page.items()
            if name not in {"page", "usesPlaybackEffectFrames", "rendersHelperEffectLayer", "hasOnlyGenericResultEffects"}
        ]
        render_states = [
            badge("uses playback.effectFrames", page.get("usesPlaybackEffectFrames")),
            badge("renders helper effect layer", page.get("rendersHelperEffectLayer")),
        ]
        if page.get("hasOnlyGenericResultEffects"):
            render_states.append(badge("generic result overlays only"))
        page_rows.append(
            f"<tr><td><code>{html.escape(page['page'])}</code></td>"
            f"<td>{''.join(binding_states)}</td><td>{''.join(render_states)}</td></tr>"
        )
    req_rows = []
    for req in report["requirements"]:
        samples = "<br>".join(
            html.escape(
                f"{item['owner']} {item['skill']} {item['skillIdHex']} "
                f"Lv={item['levelOrFixed']} helpers={item['helperIds']}"
            )
            for item in req["samples"]
        )
        owners = ", ".join(f"{owner}: {count}" for owner, count in req["owners"].items())
        req_rows.append(
            "<tr>"
            f"<td>{html.escape(req['requirement'])}</td>"
            f"<td>{req['count']}</td>"
            f"<td>{html.escape(owners)}</td>"
            f"<td>{badge(req['runnerStatus'])}<p>{html.escape(req['evidence'])}</p><p>{html.escape(req['remaining'])}</p></td>"
            f"<td>{samples}</td>"
            "</tr>"
        )
    notes = "".join(f"<li>{html.escape(note)}</li>" for note in report["notes"])
    return f"""<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Battle Effect Runner Binding Review</title>
  <style>
    body {{ margin: 0; padding: 24px; font-family: system-ui, sans-serif; background: #f8fafc; color: #0f172a; }}
    h1 {{ margin: 0 0 8px; font-size: 24px; }}
    h2 {{ margin: 24px 0 8px; font-size: 18px; }}
    .muted {{ color: #64748b; }}
    .cards {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 10px; margin: 16px 0; }}
    .card {{ border: 1px solid #dbe3ef; background: white; border-radius: 8px; padding: 12px; }}
    .value {{ font-size: 22px; font-weight: 700; }}
    table {{ width: 100%; border-collapse: collapse; background: white; border: 1px solid #dbe3ef; }}
    th, td {{ padding: 8px 10px; border-bottom: 1px solid #e2e8f0; text-align: left; vertical-align: top; }}
    th {{ background: #eaf0f8; font-size: 12px; text-transform: uppercase; letter-spacing: .04em; }}
    code {{ font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }}
    .badge {{ display: inline-block; margin: 2px 4px 2px 0; padding: 3px 7px; border-radius: 999px; font-size: 12px; font-weight: 650; }}
    .badge.ok {{ background: #dcfce7; color: #166534; }}
    .badge.bad {{ background: #fee2e2; color: #991b1b; }}
    .badge.neutral {{ background: #e0f2fe; color: #075985; }}
  </style>
</head>
<body>
  <h1>Battle Effect Runner Binding Review</h1>
  <p class="muted">EXE 정적 이펙트 요구와 현재 웹 러너의 공통 엔진 결속 상태를 분리해서 보여준다.</p>
  <p><a href="../web/index.html">홈</a> · <a href="../web/battle_effect_animation_pattern_review.html">effect animation patterns</a> · <a href="../web/battle_skill_timeline_review.html">skill timeline</a></p>
  <div class="cards">
    <div class="card"><div class="muted">effect skill rows</div><div class="value">{summary['effectSkillRows']}</div></div>
    <div class="card"><div class="muted">effect frames</div><div class="value">{summary['skillsWithEffectFrames']}</div></div>
    <div class="card"><div class="muted">review flags</div><div class="value">{summary['skillsWithReviewFlags']}</div></div>
    <div class="card"><div class="muted">execution requirements</div><div class="value">{summary['skillsWithExecutionRequirements']}</div></div>
    <div class="card"><div class="muted">engine checks</div><div class="value">{summary['engineChecksPassed']}/{summary['engineChecksTotal']}</div></div>
    <div class="card"><div class="muted">page checks</div><div class="value">{summary['pageChecksPassed']}/{summary['pageChecksTotal']}</div></div>
    <div class="card"><div class="muted">effect render pages</div><div class="value">{summary['pagesRenderingHelperEffectLayer']}/{len(report['pageChecks'])}</div></div>
  </div>
  <h2>해석 기준</h2>
  <ul>{notes}</ul>
  <h2>공통 엔진 결속</h2>
  <table><thead><tr><th>check</th><th>state</th></tr></thead><tbody>{engine_rows}</tbody></table>
  <h2>페이지 결속</h2>
  <table><thead><tr><th>page</th><th>binding checks</th><th>render checks</th></tr></thead><tbody>{''.join(page_rows)}</tbody></table>
  <h2>실행 요구 버킷</h2>
  <table><thead><tr><th>requirement</th><th>count</th><th>owners</th><th>runner status</th><th>samples</th></tr></thead><tbody>{''.join(req_rows)}</tbody></table>
</body>
</html>
"""


def main() -> int:
    report = build_report()
    OUT_JSON.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    OUT_HTML.write_text(html_page(report), encoding="utf-8")
    print("wrote out/battle_effect_runner_binding_review.{json,html}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
