#!/usr/bin/env python3
"""Build the remaining original-runtime pixel-oracle capture plan.

The current browser assertions prove that the web runner draws every
EXE-derived effect requirement class.  They do not prove that the output is
pixel-identical to the original game.  This handoff narrows that remaining
gap to a concrete capture set.
"""

from __future__ import annotations

import argparse
import html
import json
from collections import Counter
from pathlib import Path
from typing import Any


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

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


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


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


def row_name(row: dict[str, Any]) -> str:
    return str(row.get("skillName") or row.get("name") or "")


def row_id(row: dict[str, Any]) -> str:
    level = row.get("levelOrFixed")
    level_text = f":Lv{level}" if level is not None else ""
    return f"{row.get('ownerKey')}:{str(row.get('skillIdHex')).lower()}{level_text}"


def helper_behavior_classes(row: dict[str, Any]) -> list[str]:
    return sorted({
        str(helper.get("behaviorClass"))
        for helper in row.get("helperAnimations") or []
        if helper.get("behaviorClass")
    })


def helper_animation_classes(row: dict[str, Any]) -> list[str]:
    return sorted({
        str(helper.get("animationClass"))
        for helper in row.get("helperAnimations") or []
        if helper.get("animationClass")
    })


def effect_frames(row: dict[str, Any]) -> list[str]:
    return sorted(set(str(label) for label in row.get("effectFrameLabels") or []))


def has_palette_events(row: dict[str, Any]) -> bool:
    return any(helper.get("paletteEvents") for helper in row.get("helperAnimations") or [])


def direct_spawn_total(row: dict[str, Any]) -> int:
    total = 0
    for helper in row.get("helperAnimations") or []:
        total += int((helper.get("directSpawn") or {}).get("count") or 0)
    return total


def frame_script_total(row: dict[str, Any]) -> int:
    return sum(len(helper.get("frameScripts") or []) for helper in row.get("helperAnimations") or [])


def motion_loop_total(row: dict[str, Any]) -> int:
    return sum(len(helper.get("childMotionLoops") or []) for helper in row.get("helperAnimations") or [])


def row_tokens(row: dict[str, Any]) -> set[str]:
    tokens = {
        f"class:{row.get('effectAnimationClass')}",
        f"owner:{row.get('ownerKey')}",
        f"track:{row.get('renderTrack')}",
    }
    for requirement in row.get("executionRequirements") or []:
        tokens.add(f"requirement:{requirement}")
    for behavior in helper_behavior_classes(row):
        tokens.add(f"behavior:{behavior}")
    for animation in helper_animation_classes(row):
        tokens.add(f"helper-animation:{animation}")
    if direct_spawn_total(row) > 0:
        tokens.add("feature:direct-spawn")
    if frame_script_total(row) > 0:
        tokens.add("feature:frame-script")
    if motion_loop_total(row) > 0:
        tokens.add("feature:motion-loop")
    if has_palette_events(row):
        tokens.add("feature:palette-event")
    return {token for token in tokens if not token.endswith(":None")}


def normalize_row(row: dict[str, Any]) -> dict[str, Any]:
    return {
        "id": row_id(row),
        "ownerKey": row.get("ownerKey"),
        "ownerName": row.get("ownerName"),
        "skillIdHex": str(row.get("skillIdHex")).lower(),
        "levelOrFixed": row.get("levelOrFixed"),
        "skillName": row_name(row),
        "renderTrack": row.get("renderTrack"),
        "implementationState": row.get("implementationState"),
        "effectAnimationClass": row.get("effectAnimationClass"),
        "helperAnimationClasses": helper_animation_classes(row),
        "helperBehaviorClasses": helper_behavior_classes(row),
        "executionRequirements": row.get("executionRequirements") or [],
        "effectFrameCount": len(effect_frames(row)),
        "effectFrames": effect_frames(row),
        "directSpawnTotal": direct_spawn_total(row),
        "frameScriptTotal": frame_script_total(row),
        "motionLoopTotal": motion_loop_total(row),
        "hasPaletteEvents": has_palette_events(row),
        "coverageTokens": sorted(row_tokens(row)),
    }


def greedy_capture_set(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
    universe: set[str] = set()
    for row in rows:
        universe |= set(row["coverageTokens"])

    remaining = set(universe)
    selected: list[dict[str, Any]] = []
    pool = rows[:]
    while remaining:
        best = max(
            pool,
            key=lambda row: (
                len(set(row["coverageTokens"]) & remaining),
                len(row["executionRequirements"]),
                row["effectFrameCount"],
                row["directSpawnTotal"],
                row["skillName"],
            ),
        )
        covered = set(best["coverageTokens"]) & remaining
        if not covered:
            break
        chosen = dict(best)
        chosen["newlyCoveredTokens"] = sorted(covered)
        selected.append(chosen)
        remaining -= covered
        pool.remove(best)
    return selected


def build_report() -> dict[str, Any]:
    pattern = load_json("battle_effect_animation_pattern_review.json")
    visual = load_json("battle_effect_visual_assertion_review.json")
    rows = [
        normalize_row(row)
        for row in pattern.get("skillRows") or []
        if set(row.get("executionRequirements") or []) & CORE_REQUIREMENTS
    ]
    rows.sort(key=lambda row: (str(row["ownerKey"]), str(row["skillIdHex"]), str(row["levelOrFixed"])))
    required_full = rows
    recommended = greedy_capture_set(rows)
    requirement_counts = Counter(req for row in rows for req in row["executionRequirements"])
    class_counts = Counter(row["effectAnimationClass"] for row in rows)
    behavior_counts = Counter(behavior for row in rows for behavior in row["helperBehaviorClasses"])
    return {
        "version": 1,
        "kind": "hwanse-battle-effect-pixel-oracle-plan",
        "source": "tools/build_battle_effect_pixel_oracle_plan.py",
        "status": "capture-oracle-not-built",
        "inputs": [
            "out/battle_effect_animation_pattern_review.json",
            "out/battle_effect_visual_assertion_review.json",
        ],
        "summary": {
            "remainingGap": "original-runtime pixel/particle choreography oracle",
            "webVisualAssertionStatus": visual.get("status"),
            "webVisualAssertionRows": (visual.get("summary") or {}).get("requirementRows"),
            "requiredFullOracleRows": len(required_full),
            "recommendedFirstPassRows": len(recommended),
            "requirementCounts": dict(sorted(requirement_counts.items())),
            "effectClassCounts": dict(sorted(class_counts.items())),
            "helperBehaviorCounts": dict(sorted(behavior_counts.items())),
            "completionDecision": "do-not-call-update-goal",
        },
        "captureProtocol": [
            "Capture original game output at 640x480 without scaling or filtering.",
            "For each capture row, record from the first visible actor frame before the skill effect until all helper/effect pixels disappear.",
            "Record the selected actor, skill id, skill level, target monster, and whether the attack hit/missed/guarded.",
            "Compare against the web runner at the same canonical row using frame/tick alignment; browser-only non-empty pixel assertions are not sufficient for goal completion.",
        ],
        "recommendedFirstPassRows": recommended,
        "fullOracleRows": required_full,
    }


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


def row_table(rows: list[dict[str, Any]], include_tokens: bool = False) -> str:
    out = []
    for row in rows:
        reqs = " ".join(badge(req) for req in row["executionRequirements"])
        behaviors = " ".join(badge(item) for item in row["helperBehaviorClasses"])
        tokens = ""
        if include_tokens:
            tokens = "<td>" + "<br>".join(html.escape(token) for token in row.get("newlyCoveredTokens") or row["coverageTokens"]) + "</td>"
        out.append(
            "<tr>"
            f"<td><code>{html.escape(row['id'])}</code></td>"
            f"<td>{html.escape(str(row['ownerName'] or row['ownerKey']))}</td>"
            f"<td>{html.escape(row['skillName'])}</td>"
            f"<td>{html.escape(str(row['effectAnimationClass']))}</td>"
            f"<td>{reqs}</td>"
            f"<td>{behaviors}</td>"
            f"<td>{row['effectFrameCount']}</td>"
            f"<td>{row['directSpawnTotal']}</td>"
            f"<td>{row['motionLoopTotal']}</td>"
            f"{tokens}"
            "</tr>"
        )
    return "\n".join(out)


def html_page(report: dict[str, Any]) -> str:
    summary = report["summary"]
    protocol = "".join(f"<li>{html.escape(item)}</li>" for item in report["captureProtocol"])
    req_cards = "".join(
        f"<div class='card'><div class='muted'>{html.escape(key)}</div><div class='value'>{value}</div></div>"
        for key, value in summary["requirementCounts"].items()
    )
    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 Pixel Oracle Plan</title>
  <style>
    body {{ margin: 0; padding: 24px; font-family: system-ui, sans-serif; background: #f8fafc; color: #172033; }}
    h1, h2 {{ margin: 0 0 12px; }}
    .muted {{ color: #64748b; font-size: 12px; }}
    .cards {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 10px; margin: 16px 0 24px; }}
    .card {{ background: white; border: 1px solid #dbe3ef; border-radius: 8px; padding: 12px; }}
    .value {{ font-size: 24px; font-weight: 800; }}
    table {{ width: 100%; border-collapse: collapse; background: white; border: 1px solid #dbe3ef; margin: 12px 0 28px; }}
    th, td {{ border-bottom: 1px solid #e2e8f0; padding: 8px 10px; 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, Consolas, monospace; }}
    .badge {{ display: inline-block; margin: 2px; padding: 2px 6px; border-radius: 999px; background: #e0f2fe; color: #075985; font-size: 12px; font-weight: 700; }}
    .warn {{ background: #fff7ed; border: 1px solid #fed7aa; padding: 12px; border-radius: 8px; }}
  </style>
</head>
<body>
  <h1>Battle Effect Pixel Oracle Plan</h1>
  <p class="muted">source: <code>{html.escape(report['source'])}</code> · status: <code>{html.escape(report['status'])}</code></p>
  <div class="warn">웹 assertion은 통과했지만, 이 문서는 원작 런타임 캡처와 비교하기 전까지 goal을 완료하지 않는다는 경계를 고정한다.</div>
  <div class="cards">
    <div class="card"><div class="muted">web visual assertions</div><div class="value">{html.escape(str(summary['webVisualAssertionStatus']))}</div></div>
    <div class="card"><div class="muted">full oracle rows</div><div class="value">{summary['requiredFullOracleRows']}</div></div>
    <div class="card"><div class="muted">first-pass rows</div><div class="value">{summary['recommendedFirstPassRows']}</div></div>
    <div class="card"><div class="muted">completion</div><div class="value">{html.escape(summary['completionDecision'])}</div></div>
  </div>
  <h2>Requirement Counts</h2>
  <div class="cards">{req_cards}</div>
  <h2>Capture Protocol</h2>
  <ul>{protocol}</ul>
  <h2>Recommended First-Pass Capture Rows</h2>
  <table>
    <thead><tr><th>row</th><th>actor</th><th>skill</th><th>class</th><th>requirements</th><th>helper behavior</th><th>frames</th><th>spawns</th><th>loops</th><th>new coverage</th></tr></thead>
    <tbody>{row_table(report['recommendedFirstPassRows'], include_tokens=True)}</tbody>
  </table>
  <h2>Full Oracle Rows</h2>
  <table>
    <thead><tr><th>row</th><th>actor</th><th>skill</th><th>class</th><th>requirements</th><th>helper behavior</th><th>frames</th><th>spawns</th><th>loops</th></tr></thead>
    <tbody>{row_table(report['fullOracleRows'])}</tbody>
  </table>
</body>
</html>
"""


def write_outputs(report: dict[str, Any], json_out: Path = OUT_JSON, html_out: Path | None = None) -> None:
    json_out.parent.mkdir(parents=True, exist_ok=True)
    json_out.write_text(json.dumps(report, ensure_ascii=False, separators=(",", ":")), encoding="utf-8")
    if html_out is not None:
        html_out.parent.mkdir(parents=True, exist_ok=True)
        html_out.write_text(html_page(report), encoding="utf-8")


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--json-out", type=Path, default=OUT_JSON)
    parser.add_argument("--html-out", type=Path)
    args = parser.parse_args()
    report = build_report()
    write_outputs(report, args.json_out, args.html_out)
    print(json.dumps(report["summary"], ensure_ascii=False))
    return 0


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