#!/usr/bin/env python3
"""Classify battle skill visual/effect implementation tracks.

Movement placement is not enough to render a skill faithfully.  This report
combines action payload rows, display VM rows, movement classification, and
helper opcode groups to split skills into actor-only, ranged/effect, screen
effect, display-list cleanup, and child actor/effect tracks.
"""
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"
ACTION_JSON = OUT / "battle_action_mapping.json"
DISPLAY_JSON = OUT / "battle_display_vm_static_decode.json"
MOVEMENT_JSON = OUT / "battle_movement_pattern_review.json"
HELPER_JSON = OUT / "battle_helper_opcode_review.json"


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


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


def vec(values: Any, prefix: str = "") -> str:
    if not values:
        return "-"
    if isinstance(values, list):
        return ", ".join(f"{prefix}{item}" for item in values) or "-"
    return str(values)


def wlk_vec(values: Any) -> str:
    if not values:
        return "-"
    if isinstance(values, list):
        return ", ".join(f"WLK id {int(item):02d}" for item in values) or "-"
    return f"WLK id {int(values):02d}"


def unique_values(*groups: Any) -> list[Any]:
    out: list[Any] = []
    seen: set[Any] = set()
    for group in groups:
        if not group:
            continue
        values = group if isinstance(group, list) else [group]
        for value in values:
            if value is None or value in seen:
                continue
            seen.add(value)
            out.append(value)
    return out


def wlk_list_from(rows: list[dict[str, Any]] | None) -> list[int]:
    out: list[int] = []
    for row in rows or []:
        value = row.get("wlkNo")
        if value is None:
            continue
        out.append(int(value))
    return out


def render_track(row: dict[str, Any]) -> tuple[str, str, str, str]:
    position = row.get("positionClass") or ""
    helper = row.get("helper") or {}
    movement = row.get("movementSignature") or "-"
    if helper.get("childTargets"):
        return (
            "child-actor-effect",
            "high",
            "needs-child-vm-promotion",
            "Child/effect target VA가 있어 actor clone 또는 child effect를 별도 실행해야 한다.",
        )
    if helper.get("spawnChildTargets"):
        return (
            "spawned-child-display-flow",
            "medium",
            "needs-helper-target-promotion",
            "0x07 child display VM spawn target이 있어 actor frame만으로는 부족하다.",
        )
    if helper.get("clearDisplayLists"):
        return (
            "display-list-cleanup-flow",
            "medium",
            "helper-cleanup-evidence-only",
            "0x4b display-list cleanup/free가 포함되어 있으나 이것만으로 시각 리소스가 생기지는 않는다.",
        )
    if position.startswith("single-target-ranged-"):
        return (
            "ranged-projectile-or-beam",
            "medium",
            "needs-child-helper-script-runner",
            "개인기지만 target-front가 아니라 수평 거리에서 helper/effect를 쏘는 계열이다. 0xbd body는 별도 child display script를 붙인다.",
        )
    if position == "all-target-screen-or-ranged-effect":
        return (
            "screen-wide-effect",
            "medium",
            "needs-child-helper-script-runner",
            "전체기 중 actor 위치 변화 없이 화면/대상 전체 helper effect가 본체인 계열이다. 0xbd body는 별도 child display script를 붙인다.",
        )
    if position == "all-target-caster-action-effect":
        return (
            "caster-action-screen-hit",
            "medium",
            "actor-frame-plus-helper",
            "시전자 프레임/로컬 offset은 있지만 피격/화면 효과는 helper/result sound 쪽에 남아 있다.",
        )
    if position in {"self-prep-all-target-screen-effect", "self-prep-all-target-caster-effect"}:
        return (
            "screen-wide-effect",
            "medium",
            "actor-frame-plus-helper",
            "자기 준비/시전 프레임 뒤에 적 전체 helper/result 효과가 이어지는 계열이다.",
        )
    if position == "all-target-clone-placement":
        return (
            "screen-wide-effect",
            "medium",
            "actor-frame-clone-or-spread-placement",
            "selector 5 기반의 화면/분신/확산 배치가 본체인 전체기 계열이다.",
        )
    if position == "all-target-special-placement":
        return (
            "actor-melee-or-dash",
            "medium",
            "actor-frame-special-placement",
            "전체기지만 특수 이동/배치 opcode가 시전자 동작을 직접 제어하는 계열이다.",
        )
    if position == "self-prep-caster-local":
        return (
            "self-prep-actor-hit",
            "medium",
            "actor-frame-self-prep",
            "술/자기 준비 프레임 뒤에 actor-frame 기반 타격이 이어지는 계열이다.",
        )
    if position == "all-target-no-promoted-position" and row.get("resultWlkNos"):
        return (
            "simple-screen-hit",
            "medium",
            "actor-frame-simple-effect",
            "helper id 없이 actor frame과 result sound만 있는 단순 전체 효과 계열이다.",
        )
    if "target-front" in position or "approach" in position or "pierce" in position or "local-combo" in position:
        state = "actor-frame-mostly-promoted" if movement != "-" or "local-combo" in position else "actor-frame-inferred"
        return (
            "actor-melee-or-dash",
            "medium",
            state,
            "시전자 프레임과 위치/이동 VM으로 주 동작은 재생 가능하다. 타격 이펙트는 별도 보정 대상이다.",
        )
    if "support" in position or row.get("targetClass") in {"self", "ally-single", "ally-all"}:
        return (
            "support-or-status",
            "medium",
            "needs-ui/status-effect-policy",
            "피격 대상이 적이 아니므로 회복/버프/상태 아이콘과 UI 효과 쪽을 분리해야 한다.",
        )
    return (
        "unclassified-effect",
        "low",
        "needs-more-evidence",
        "현재 VM 승격만으로 위치/이펙트 의미를 더 나누기 어렵다.",
    )


def build() -> dict[str, Any]:
    action = json.loads(ACTION_JSON.read_text(encoding="utf-8"))
    display = json.loads(DISPLAY_JSON.read_text(encoding="utf-8"))
    movement = json.loads(MOVEMENT_JSON.read_text(encoding="utf-8"))
    helper = json.loads(HELPER_JSON.read_text(encoding="utf-8"))

    action_by_key = {key_for(row): row for row in action.get("playerRows") or []}
    display_by_key = {key_for(row): row for row in display.get("decodedRows") or []}
    movement_by_key = {key_for(row): row for row in movement.get("rows") or []}
    helper_by_key = {key_for(row): row for row in helper.get("rows") or []}

    rows = []
    family_groups: defaultdict[str, list[dict[str, Any]]] = defaultdict(list)
    for key, action_row in sorted(action_by_key.items()):
        if key not in display_by_key:
            continue
        display_row = display_by_key[key]
        movement_row = movement_by_key.get(key, {})
        helper_row = helper_by_key.get(key, {})
        movement_helper = movement_row.get("helperEvidence") or {}
        display_effect_wlks = wlk_list_from(display_row.get("effectSounds"))
        display_result_wlks = wlk_list_from(display_row.get("sounds"))
        effect_wlks = unique_values(display_effect_wlks, helper_row.get("effectWlkNos"), movement_helper.get("effectWlkNos"))
        result_wlks = unique_values(display_result_wlks, helper_row.get("rawResultWlkNos"), helper_row.get("resultWlkNos"), movement_helper.get("resultWlkNos"))
        helper_ids = helper_row.get("helperIds") or movement_helper.get("helperIds") or []
        clear_display_lists = helper_row.get("clearDisplayLists") or movement_helper.get("clearDisplayLists") or []
        child_targets = helper_row.get("childTargets") or movement_helper.get("childTargets") or []
        spawn_child_targets = helper_row.get("spawnChildTargets") or movement_helper.get("spawnChildTargets") or []
        row = {
            "ownerKey": action_row.get("ownerKey"),
            "ownerName": action_row.get("ownerName"),
            "skillName": action_row.get("name"),
            "skillIdHex": action_row.get("skillIdHex"),
            "levelOrFixed": action_row.get("levelOrFixed"),
            "targetClass": movement_row.get("targetClass"),
            "positionClass": movement_row.get("positionClass"),
            "startPlacementPolicy": movement_row.get("startPlacementPolicy"),
            "movementSignature": movement_row.get("movementSignature"),
            "frameSequence": display_row.get("frameSequence") or [],
            "frameCount": len(display_row.get("frames") or []),
            "hitCount": action_row.get("effectCount"),
            "mpCost": action_row.get("mpCost"),
            "effectWlkNos": effect_wlks,
            "resultWlkNos": result_wlks,
            "helperIds": helper_ids,
            "clearDisplayLists": clear_display_lists,
            "childTargets": child_targets,
            "spawnChildTargets": spawn_child_targets,
            "helperRole": helper_row.get("helperRole"),
            "entryStartVaHex": display_row.get("entryStartVaHex"),
        }
        row["helper"] = {
            "effectWlkNos": row["effectWlkNos"],
            "resultWlkNos": row["resultWlkNos"],
            "helperIds": row["helperIds"],
            "clearDisplayLists": row["clearDisplayLists"],
            "childTargets": row["childTargets"],
            "spawnChildTargets": row["spawnChildTargets"],
        }
        track, confidence, implementation_state, note = render_track(row)
        row.update(
            {
                "renderTrack": track,
                "confidence": confidence,
                "implementationState": implementation_state,
                "semanticNote": note,
            }
        )
        rows.append(row)
        family_groups[f"{row['ownerName']}::{row['skillName']}"].append(row)

    semantic_groups = []
    for track, items in sorted(defaultdict(list, {track: [row for row in rows if row["renderTrack"] == track] for track in {row["renderTrack"] for row in rows}}).items()):
        semantic_groups.append(
            {
                "renderTrack": track,
                "count": len(items),
                "positionClasses": sorted({str(item.get("positionClass") or "") for item in items}),
                "helperIds": sorted({helper_id for item in items for helper_id in item["helperIds"]}),
                "effectWlkNos": sorted({wlk_no for item in items for wlk_no in item["effectWlkNos"]}),
                "resultWlkNos": sorted({wlk_no for item in items for wlk_no in item["resultWlkNos"]}),
                "samples": [f"{item['ownerName']} {item['skillName']} {item['skillIdHex']}" for item in items[:12]],
            }
        )

    family_progressions = []
    for family_key, items in sorted(family_groups.items()):
        if len(items) < 2:
            continue
        family_progressions.append(
            {
                "family": family_key,
                "count": len(items),
                "tracks": sorted({item["renderTrack"] for item in items}),
                "positions": sorted({str(item.get("positionClass") or "") for item in items}),
                "skillIds": [item["skillIdHex"] for item in items],
                "helperIdProgression": [item["helperIds"] for item in items],
                "effectWlkProgression": [item["effectWlkNos"] for item in items],
                "frameCounts": [item["frameCount"] for item in items],
                "hitCounts": [item["hitCount"] for item in items],
            }
        )

    return {
        "version": 1,
        "kind": "hwanse-battle-effect-semantics-review",
        "source": [
            "out/battle_action_mapping.json",
            "out/battle_display_vm_static_decode.json",
            "out/battle_movement_pattern_review.json",
            "out/battle_helper_opcode_review.json",
            "out/battle_helper_body_review.json",
            "out/battle_helper_child_script_review.json",
            "out/battle_helper_frame_script_review.json",
            "out/battle_helper_effect_runtime_review.json",
            "out/battle_helper_sync_timing_review.json",
        ],
        "status": "effect-render-track-classification",
        "summary": {
            "rows": len(rows),
            "renderTrackCounts": dict(Counter(row["renderTrack"] for row in rows)),
            "implementationStateCounts": dict(Counter(row["implementationState"] for row in rows)),
            "skillsNeedingHelperScriptRunner": sum(1 for row in rows if "helper" in row["implementationState"] or row["implementationState"].startswith("needs-0xbd")),
            "skillsWithChildTargets": sum(1 for row in rows if row["childTargets"]),
            "skillsWithCleanupOrSpawn": sum(1 for row in rows if row["clearDisplayLists"] or row["spawnChildTargets"]),
        },
        "interpretationNotes": [
            "0x24 is treated as a cast/effect sound cue, not the visual body itself.",
            "0xc2 result WLK remains hit/result sound and should be scheduled around actor-flag hit windows.",
            "0xbd helper ids dispatch through the EXE table at 0x454c10. See battle_helper_body_review for function bodies, battle_helper_child_script_review for spawned child display VM scripts, battle_helper_frame_script_review for 0x20 frameScript bodies, battle_helper_effect_runtime_review for conservative runtime facts, and battle_helper_sync_timing_review for gate-order actor-flag timing.",
            "Ranged personal skills and melee personal skills are split by position/write/helper evidence, not by target scope.",
            "Child target VAs currently appear in 인법·분신술 and require a separate child VM promotion path.",
            "0x4b is display-list cleanup/free, not a visual resource loader or screen effect primitive.",
        ],
        "semanticGroups": semantic_groups,
        "familyProgressions": family_progressions,
        "rows": rows,
    }


def markdown(report: dict[str, Any]) -> str:
    lines = [
        "# Battle Effect Semantics Review",
        "",
        f"- status: `{report['status']}`",
        f"- rows: `{report['summary']['rows']}`",
        f"- render tracks: `{report['summary']['renderTrackCounts']}`",
        "",
        "## Interpretation",
        "",
    ]
    lines.extend(f"- {note}" for note in report["interpretationNotes"])
    lines.extend(
        [
            "",
            "## Render Tracks",
            "",
            "| track | count | position classes | helper ids | 0x24 WLK | 0xc2 WLK | samples |",
            "| --- | ---: | --- | --- | --- | --- | --- |",
        ]
    )
    for group in report["semanticGroups"]:
        lines.append(
            f"| `{group['renderTrack']}` | {group['count']} | {', '.join(group['positionClasses'])} | "
            f"{vec(group['helperIds'])} | {wlk_vec(group['effectWlkNos'])} | {wlk_vec(group['resultWlkNos'])} | "
            f"{'; '.join(group['samples'])} |"
        )
    lines.extend(
        [
            "",
            "## Skill Rows",
            "",
            "| actor | skill | id | track | state | position | helper/effect | frames/hits | note |",
            "| --- | --- | ---: | --- | --- | --- | --- | --- | --- |",
        ]
    )
    for row in report["rows"]:
        helper = (
            f"0xbd {vec(row['helperIds'])}; 0x24 {wlk_vec(row['effectWlkNos'])}; "
            f"0xc2 {wlk_vec(row['resultWlkNos'])}; child {vec(row['childTargets'])}; "
            f"clear {vec(row['clearDisplayLists'])}; spawn {vec(row['spawnChildTargets'])}"
        )
        lines.append(
            f"| {row['ownerName']} | {row['skillName']} | `{row['skillIdHex']}` | `{row['renderTrack']}` | "
            f"`{row['implementationState']}` | {row.get('positionClass') or '-'} / {row.get('startPlacementPolicy') or '-'} | "
            f"{helper} | {row['frameCount']}/{row['hitCount']} | {row['semanticNote']} |"
        )
    return "\n".join(lines) + "\n"


def html_page(report: dict[str, Any]) -> str:
    summary_rows = "".join(
        f"<tr><td>{esc(key)}</td><td><code>{esc(value)}</code></td></tr>"
        for key, value in report["summary"].items()
    )
    notes = "".join(f"<li>{esc(note)}</li>" for note in report["interpretationNotes"])
    group_rows = []
    for group in report["semanticGroups"]:
        group_rows.append(
            "<tr>"
            f"<td><code>{esc(group['renderTrack'])}</code></td>"
            f"<td>{esc(group['count'])}</td>"
            f"<td>{esc(', '.join(group['positionClasses']))}</td>"
            f"<td>{esc(vec(group['helperIds']))}</td>"
            f"<td>{esc(wlk_vec(group['effectWlkNos']))}</td>"
            f"<td>{esc(wlk_vec(group['resultWlkNos']))}</td>"
            f"<td>{esc('; '.join(group['samples']))}</td>"
            "</tr>"
        )
    skill_rows = []
    for row in report["rows"]:
        helper = (
            f"0xbd {esc(vec(row['helperIds']))}<br>"
            f"0x24 {esc(wlk_vec(row['effectWlkNos']))}<br>"
            f"0xc2 {esc(wlk_vec(row['resultWlkNos']))}<br>"
            f"child {esc(vec(row['childTargets']))}<br>"
            f"clear {esc(vec(row['clearDisplayLists']))}<br>"
            f"spawn {esc(vec(row['spawnChildTargets']))}"
        )
        skill_rows.append(
            "<tr>"
            f"<td>{esc(row['ownerName'])}</td>"
            f"<td>{esc(row['skillName'])}<br><code>{esc(row['skillIdHex'])}</code></td>"
            f"<td><code>{esc(row['renderTrack'])}</code><br>{esc(row['confidence'])}</td>"
            f"<td><code>{esc(row['implementationState'])}</code></td>"
            f"<td>{esc(row.get('positionClass') or '-')}<br>{esc(row.get('startPlacementPolicy') or '-')}</td>"
            f"<td>{helper}</td>"
            f"<td>{esc(row['frameCount'])}/{esc(row['hitCount'])}</td>"
            f"<td>{esc(row['semanticNote'])}</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 Effect Semantics 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; margin: 14px 0 24px; }}
    th, td {{ border: 1px solid #30343d; padding: 6px 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; }}
    .grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 12px; }}
    .panel {{ border: 1px solid #30343d; border-radius: 8px; padding: 12px; background: #151821; }}
    .wide {{ overflow: auto; max-height: 78vh; border: 1px solid #30343d; }}
  </style>
</head>
<body>
  <h1>Battle Effect Semantics Review</h1>
  <p><a href="../web/index.html">홈</a> · <a href="../web/battle_simulator.html">전투 기술 실행</a> · <a href="battle_movement_pattern_review.html">위치/이동 패턴</a> · <a href="battle_helper_opcode_review.html">helper opcode</a> · <a href="battle_helper_body_review.html">helper body</a> · <a href="battle_helper_child_script_review.html">helper child script</a> · <a href="battle_helper_frame_script_review.html">helper frameScript</a> · <a href="battle_helper_effect_runtime_review.html">helper effect runtime</a> · <a href="battle_helper_sync_timing_review.html">helper sync timing</a> · <a href="battle_effect_semantics_review.json">JSON</a></p>
  <div class="grid">
    <section class="panel"><h2>Summary</h2><table><tbody>{summary_rows}</tbody></table></section>
    <section class="panel"><h2>Interpretation</h2><ul>{notes}</ul></section>
  </div>
  <h2>Render Tracks</h2>
  <div class="wide"><table><thead><tr><th>track</th><th>count</th><th>positions</th><th>0xbd</th><th>0x24</th><th>0xc2</th><th>samples</th></tr></thead><tbody>{''.join(group_rows)}</tbody></table></div>
  <h2>Skill Rows</h2>
  <div class="wide"><table><thead><tr><th>actor</th><th>skill</th><th>track</th><th>state</th><th>position</th><th>helper/effect</th><th>frames/hits</th><th>note</th></tr></thead><tbody>{''.join(skill_rows)}</tbody></table></div>
</body>
</html>
"""


def main() -> None:
    report = build()
    OUT.mkdir(parents=True, exist_ok=True)
    (OUT / "battle_effect_semantics_review.json").write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    (OUT / "battle_effect_semantics_review.html").write_text(html_page(report), encoding="utf-8")
    print(f"wrote {OUT / 'battle_effect_semantics_review.json'}")
    print(f"wrote {OUT / 'battle_effect_semantics_review.html'}")


if __name__ == "__main__":
    main()
