#!/usr/bin/env python3
"""Classify promoted battle skill VM scaling patterns.

This report is for avoiding a common preview mistake: not every four-level skill
gets stronger by adding visible hit frames.  Some keep the same frame sequence
and only reduce the gate/movement divisor, while others only add visual effects.
"""
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"
DISPLAY_JSON = OUT / "battle_display_vm_static_decode.json"


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


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


def monotonic_decreasing(values: list[int]) -> bool:
    return len(values) >= 2 and all(a > b for a, b in zip(values, values[1:]))


def vector_signature(values: list[Any]) -> str:
    return ", ".join(str(value) for value in values) or "-"


def hit_windows(rows: list[dict[str, Any]]) -> int:
    count = 0
    pending = False
    for row in rows:
        if row.get("category") == "actor-flags":
            if row.get("mode") == 2 and row.get("mask") == 0x0C:
                pending = True
            elif pending and row.get("mode") == 1 and row.get("mask") == 0x08:
                pass
        elif pending and row.get("category") == "wait":
            count += 1
            pending = False
    if pending:
        count += 1
    return count


def row_summary(row: dict[str, Any]) -> dict[str, Any]:
    frames = row.get("frames") or []
    movements = [movement for movement in (row.get("movements") or []) if movement.get("movementMode")]
    gates = [frame.get("gate") for frame in frames]
    movement_divisors = [movement.get("divisor") for movement in movements]
    movement_modes = [movement.get("movementMode") for movement in movements]
    direct_frames = [frame for frame in frames if any(
        instr.get("directFrameSelector") and instr.get("vaHex") == frame.get("vaHex")
        for instr in row.get("rows") or []
    )]
    return {
        "ownerKey": row.get("ownerKey"),
        "ownerName": row.get("ownerName"),
        "familyName": row.get("familyName"),
        "skillName": row.get("skillName"),
        "skillId": row.get("skillId"),
        "skillIdHex": row.get("skillIdHex"),
        "phaseHex": row.get("phaseHex"),
        "effectCount": row.get("effectCount"),
        "frameSequence": row.get("frameSequence") or [],
        "frameCount": len(row.get("frameSequence") or []),
        "gates": gates,
        "movementDivisors": movement_divisors,
        "movementModes": movement_modes,
        "soundCount": len(row.get("sounds") or []),
        "effectSoundCount": len(row.get("effectSounds") or []),
        "hitWindowCount": hit_windows(row.get("rows") or []),
        "repeatLoopCount": len(row.get("repeatLoops") or []),
        "repeatLoops": row.get("repeatLoops") or [],
        "directFrameSelectorCount": len(direct_frames),
        "stopReason": row.get("stopReason"),
    }


def classify_group(rows: list[dict[str, Any]]) -> tuple[str, list[str]]:
    rows = sorted(rows, key=lambda item: int(item.get("skillId") or 0))
    name_counts = Counter(row["skillName"] for row in rows)
    names = set(name_counts)
    effect_counts = [int(row.get("effectCount") or 0) for row in rows]
    frame_sequences = [row["frameSequence"] for row in rows]
    frame_counts = [row["frameCount"] for row in rows]
    gate_vectors = [row["gates"] for row in rows]
    first_gates = [vector[0] for vector in gate_vectors if vector]
    movement_vectors = [row["movementDivisors"] for row in rows]
    first_movement_divisors = [vector[0] for vector in movement_vectors if vector]
    sound_counts = [row["soundCount"] for row in rows]
    effect_sound_counts = [row["effectSoundCount"] for row in rows]
    repeat_loop_counts = [row["repeatLoopCount"] for row in rows]
    notes: list[str] = []

    same_effect_count = len(set(effect_counts)) == 1
    same_frame_sequence = len(unique(frame_sequences)) == 1
    if len(names) > 1 and max(name_counts.values()) < 3:
        notes.append("한 패밀리에 서로 다른 고정/장비 기술명이 섞여 있어 4단계 숙련도 비교로 보면 안 된다.")
        notes.append(f"skill names: {', '.join(name_counts)}")
        if any(repeat_loop_counts):
            notes.append("repeat-loop 포함 기술이 있어 별도 수동 검토가 필요하다.")
        return "mixed-fixed-family", notes

    speed_by_movement = same_effect_count and same_frame_sequence and monotonic_decreasing(first_movement_divisors)
    speed_by_gate = same_effect_count and same_frame_sequence and monotonic_decreasing(first_gates)
    if speed_by_movement or speed_by_gate:
        if speed_by_movement:
            notes.append(f"movement divisor descends: {vector_signature(first_movement_divisors)}")
        if speed_by_gate:
            notes.append(f"first frame gate descends: {vector_signature(first_gates)}")
        notes.append("타수 증가가 아니라 같은 동작의 속도/관통 시간 변화로 처리해야 한다.")
        return "speed-scaling", notes

    if len(set(effect_counts)) > 1:
        notes.append(f"payload effectCount changes: {vector_signature(effect_counts)}")
        if len(set(sound_counts)) > 1:
            notes.append(f"0xc2 result sound count changes: {vector_signature(sound_counts)}")
        if len(set(frame_counts)) > 1:
            notes.append(f"frame count changes: {vector_signature(frame_counts)}")
        if any(repeat_loop_counts):
            notes.append("repeat-loop가 있어 단순 frameSequence 길이만으로 타수를 판단하면 안 된다.")
        return "hit-count-scaling", notes

    if same_effect_count and (len(set(frame_counts)) > 1 or len(set(effect_sound_counts)) > 1 or len(unique(frame_sequences)) > 1):
        notes.append("payload 타수는 같지만 frame/effect sound가 숙련도에 따라 달라진다.")
        notes.append(f"frame count: {vector_signature(frame_counts)}, effect WLK count: {vector_signature(effect_sound_counts)}")
        return "visual-scaling", notes

    if any(repeat_loop_counts):
        notes.append("표면 패턴은 거의 같지만 repeat-loop가 있어 프리뷰 타수 보정 대상이다.")
        return "repeat-loop-caution", notes

    notes.append("현재 승격된 actor VM 기준으로 숙련도별 frame/gate/hit 변화가 없다.")
    notes.append("이는 미해석이 아니라 actor 동작은 고정이고, 숙련도 차이는 MP/헬퍼/효과 크기/결과음 등 다른 레이어에서 확인해야 하는 패밀리다.")
    return "constant-fixed-family", notes


def build() -> dict[str, Any]:
    display = json.loads(DISPLAY_JSON.read_text(encoding="utf-8"))
    grouped: defaultdict[tuple[str, str], list[dict[str, Any]]] = defaultdict(list)
    for row in display.get("decodedRows") or []:
        summary = row_summary(row)
        grouped[(str(summary["ownerName"]), str(summary["familyName"]))].append(summary)

    groups = []
    for (owner_name, family_name), rows in sorted(grouped.items(), key=lambda item: (item[0][0], item[0][1])):
        rows = sorted(rows, key=lambda item: int(item.get("skillId") or 0))
        classification, notes = classify_group(rows)
        groups.append({
            "ownerName": owner_name,
            "familyName": family_name,
            "classification": classification,
            "notes": notes,
            "skillIds": [row["skillIdHex"] for row in rows],
            "skillNames": [row["skillName"] for row in rows],
            "effectCounts": [row["effectCount"] for row in rows],
            "frameCounts": [row["frameCount"] for row in rows],
            "soundCounts": [row["soundCount"] for row in rows],
            "effectSoundCounts": [row["effectSoundCount"] for row in rows],
            "hitWindowCounts": [row["hitWindowCount"] for row in rows],
            "movementDivisors": [row["movementDivisors"] for row in rows],
            "gates": [row["gates"] for row in rows],
            "repeatLoopCounts": [row["repeatLoopCount"] for row in rows],
            "directFrameSelectorCounts": [row["directFrameSelectorCount"] for row in rows],
            "rows": rows,
        })

    classification_counts = Counter(group["classification"] for group in groups)
    caution_groups = [
        group for group in groups
        if group["classification"] in {"speed-scaling", "visual-scaling", "mixed-fixed-family", "repeat-loop-caution"}
        or any(group["repeatLoopCounts"])
    ]
    return {
        "version": 1,
        "kind": "hwanse-battle-skill-pattern-review",
        "source": ["out/battle_display_vm_static_decode.json"],
        "status": "classifies-hit-count-speed-and-visual-scaling-patterns",
        "summary": {
            "decodedSkillRows": len(display.get("decodedRows") or []),
            "familyGroups": len(groups),
            "classificationCounts": dict(classification_counts),
            "cautionGroupCount": len(caution_groups),
        },
        "interpretationNotes": [
            "speed-scaling은 payload 타수와 frame sequence가 그대로인데 gate/movement divisor만 줄어드는 계열이다. 프리뷰에서 타수를 추가하면 틀린다.",
            "hit-count-scaling은 effectCount와 0xc2/result sound 또는 hit frame 수가 함께 증가하는 계열이다.",
            "visual-scaling은 payload 타수는 그대로인데 시전 이펙트/프레임만 늘어나는 계열이다.",
            "constant-fixed-family는 actor VM frame/gate/hit가 숙련도별로 고정인 패밀리다. 헬퍼 VM이나 WLK/result 레이어의 차이는 통합 리포트에서 따로 본다.",
            "repeat-loop가 있는 기술은 정적 frameSequence를 단순 나열한 값과 실제 반복 타격/연출 수가 다를 수 있다.",
        ],
        "groups": groups,
        "cautionGroups": caution_groups,
    }


def markdown(report: dict[str, Any]) -> str:
    lines = [
        "# Battle Skill Pattern Review",
        "",
        f"- status: `{report['status']}`",
        f"- decoded skill rows: `{report['summary']['decodedSkillRows']}`",
        f"- family groups: `{report['summary']['familyGroups']}`",
        "",
        "## Interpretation",
        "",
    ]
    lines.extend(f"- {note}" for note in report["interpretationNotes"])
    lines += [
        "",
        "## Group Summary",
        "",
        "| actor | family | class | skill ids | effectCount | frames | movement divisor | sounds | effect WLK | repeat | notes |",
        "| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |",
    ]
    for group in report["groups"]:
        lines.append(
            f"| {group['ownerName']} | {group['familyName']} | `{group['classification']}` | "
            f"{', '.join('`'+value+'`' for value in group['skillIds'])} | "
            f"{vector_signature(group['effectCounts'])} | {vector_signature(group['frameCounts'])} | "
            f"{vector_signature(group['movementDivisors'])} | {vector_signature(group['soundCounts'])} | "
            f"{vector_signature(group['effectSoundCounts'])} | {vector_signature(group['repeatLoopCounts'])} | "
            f"{'; '.join(group['notes'])} |"
        )
    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()
    )
    class_rows = "".join(
        f"<tr><td><code>{esc(key)}</code></td><td>{esc(value)}</td></tr>"
        for key, value in report["summary"].get("classificationCounts", {}).items()
    )
    group_rows = []
    for group in report["groups"]:
        notes = "<br>".join(esc(note) for note in group["notes"])
        details = "<br>".join(
            f"<code>{esc(row['skillIdHex'])}</code> {esc(row['skillName'])}: "
            f"eff {esc(row['effectCount'])}, frames {esc(vector_signature(row['frameSequence']))}, "
            f"gates {esc(vector_signature(row['gates']))}, mov {esc(vector_signature(row['movementDivisors']))}, "
            f"snd {esc(row['soundCount'])}, efx {esc(row['effectSoundCount'])}, repeat {esc(row['repeatLoopCount'])}"
            for row in group["rows"]
        )
        group_rows.append(
            "<tr>"
            f"<td>{esc(group['ownerName'])}</td>"
            f"<td>{esc(group['familyName'])}</td>"
            f"<td><code>{esc(group['classification'])}</code></td>"
            f"<td>{esc(vector_signature(group['effectCounts']))}</td>"
            f"<td>{esc(vector_signature(group['frameCounts']))}</td>"
            f"<td>{esc(vector_signature(group['movementDivisors']))}</td>"
            f"<td>{esc(vector_signature(group['soundCounts']))}</td>"
            f"<td>{esc(vector_signature(group['effectSoundCounts']))}</td>"
            f"<td>{esc(vector_signature(group['repeatLoopCounts']))}</td>"
            f"<td>{notes}</td>"
            f"<td>{details}</td>"
            "</tr>"
        )
    notes = "".join(f"<li>{esc(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">
  <link rel="icon" href="../favicon.ico">
  <title>Battle Skill 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; 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(260px, 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 Skill Pattern Review</h1>
  <p><a href="../web/index.html">홈</a> · <a href="../web/battle_simulator.html">전투 기술 실행</a> · <a href="battle_display_vm_static_decode.html">VM 정적 디코드</a> · <a href="battle_skill_pattern_review.json">JSON</a> · <a href="battle_skill_pattern_review.md">MD</a></p>
  <div class="grid">
    <section class="panel"><h2>Summary</h2><table><tbody>{summary_rows}</tbody></table></section>
    <section class="panel"><h2>Classes</h2><table><tbody>{class_rows}</tbody></table></section>
  </div>
  <h2>Interpretation</h2>
  <ul>{notes}</ul>
  <h2>All Promoted Families</h2>
  <div class="wide">
    <table>
      <thead><tr><th>actor</th><th>family</th><th>class</th><th>effectCount</th><th>frame count</th><th>movement divisor</th><th>sound count</th><th>effect WLK count</th><th>repeat</th><th>notes</th><th>detail</th></tr></thead>
      <tbody>{''.join(group_rows)}</tbody>
    </table>
  </div>
</body>
</html>
"""


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


if __name__ == "__main__":
    main()
