#!/usr/bin/env python3
"""Classify remaining battle skill presentation gaps.

`battle_skill_complete_pattern_review.json` intentionally keeps every
needs-* marker close to the source reports.  That is useful for auditing but
too broad for planning: some rows still need EXE/static analysis, while others
only need browser runner/UI implementation.  This report separates those two
cases without promoting any new visual behavior.
"""

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"
OUT_JSON = OUT / "battle_skill_implementation_gap_review.json"
OUT_MD = OUT / "battle_skill_implementation_gap_review.md"
OUT_HTML = OUT / "battle_skill_implementation_gap_review.html"


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


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


def compact(values: list[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 helper_behaviors(row: dict[str, Any]) -> list[str]:
    out: list[str] = []
    for helper in row.get("helperFacts") or []:
        behavior = helper.get("behaviorClass")
        helper_id = helper.get("helperId")
        if behavior:
            out.append(f"#{helper_id}:{behavior}")
        elif helper_id is not None:
            out.append(f"#{helper_id}:missing-or-resource-only")
    return out


def classify(row: dict[str, Any]) -> tuple[str, str, list[str], list[str]]:
    flags = set(row.get("unresolvedFlags") or [])
    render = row.get("renderTrack")
    state = row.get("implementationState")
    reasons: list[str] = []
    next_actions: list[str] = []

    if "needs-ui/status-effect-policy" in flags or render == "support-or-status":
        reasons.append("타격 연출이 아니라 도주/방어/회복/상태/취기 같은 UI 또는 상태 처리 계열이다.")
        next_actions.append("battle runner가 공격 애니메이션으로 처리하지 않고 formula/status UI 경로로 분기한다.")
        return "presentation-policy", "EXE 타임라인 누락 아님", reasons, next_actions

    if "needs-helper-target-promotion" in flags:
        position_evidence = [
            value
            for value in (
                row.get("positionClass"),
                row.get("casterRule"),
                row.get("startAnchor"),
                row.get("positionBucket"),
                row.get("positionBucketLabel"),
            )
            if value
        ]
        if position_evidence:
            reasons.append("target/front/screen/clone-spread 계열 배치 분류는 이미 EXE-derived position row에 있다.")
            reasons.append(f"position evidence: {compact(position_evidence, 5)}")
            behaviors = helper_behaviors(row)
            if behaviors:
                reasons.append(f"helper behavior: {compact(behaviors, 8)}")
            next_actions.append("browser runner가 screen scatter, target-side, clone-spread child display flow를 실행한다.")
            return "runner-implementation-gap", "EXE 위치 근거 있음, 웹 실행기 미반영", reasons, next_actions
        reasons.append("helper id는 잡혔지만 child/spawn 대상 위치 승격이 아직 부족하다.")
        next_actions.append("helper body/child script에서 target slot, screen scatter, clone spread 좌표를 추가 해석한다.")
        return "static-analysis-gap", "helper target/placement 미확정", reasons, next_actions

    if "helper has no visual frame detected" in flags:
        reasons.append("0xbd helper 호출은 잡혔지만 helper body에서 btl_efc 등 시각 frame source가 아직 직접 검출되지 않았다.")
        if row.get("helperIds"):
            reasons.append(f"대상 helper: {compact([f'#{hid}' for hid in row.get('helperIds') or []], 10)}")
        next_actions.append("해당 helper body를 opcode 단위로 재분해해 field write, indirect frame table, cleanup-only 여부를 분리한다.")
        return "static-analysis-gap", "helper visual frame 미확정", reasons, next_actions

    if "needs-child-helper-script-runner" in flags:
        behaviors = helper_behaviors(row)
        if behaviors:
            reasons.append("helper frameScript/direct spawn 근거는 이미 있다.")
            reasons.append(f"helper behavior: {compact(behaviors, 8)}")
        else:
            reasons.append("저숙련/무이펙트 행이거나 상위 숙련도 helper family와 묶인 구현 상태다.")
        next_actions.append("browser timeline/runner가 frameScript, direct spawn, cast/result WLK를 같은 시간축으로 실행한다.")
        return "runner-implementation-gap", "EXE 근거 있음, 웹 실행기 미반영", reasons, next_actions

    if flags:
        reasons.append(f"분류되지 않은 flag: {compact(sorted(flags), 8)}")
        next_actions.append("source report의 unresolved flag 생성 조건을 재검토한다.")
        return "unclassified-gap", "분류 필요", reasons, next_actions

    return "no-gap", "추가 gap 없음", reasons, next_actions


def build() -> dict[str, Any]:
    complete = load_json("battle_skill_complete_pattern_review.json")
    support_policy_count = len(complete.get("supportPolicyRows") or [])
    rows: list[dict[str, Any]] = []
    for row in complete.get("rows") or []:
        if not row.get("unresolvedFlags"):
            continue
        gap_class, label, reasons, next_actions = classify(row)
        rows.append(
            {
                "ownerKey": row.get("ownerKey"),
                "ownerName": row.get("ownerName"),
                "skillName": row.get("skillName"),
                "familyName": row.get("familyName"),
                "skillIdHex": row.get("skillIdHex"),
                "renderTrack": row.get("renderTrack"),
                "implementationState": row.get("implementationState"),
                "positionBucketLabel": row.get("positionBucketLabel"),
                "positionClass": row.get("positionClass"),
                "casterRule": row.get("casterRule"),
                "startAnchor": row.get("startAnchor"),
                "positionBucket": row.get("positionBucket"),
                "unresolvedFlags": row.get("unresolvedFlags") or [],
                "gapClass": gap_class,
                "gapLabel": label,
                "reasons": reasons,
                "nextActions": next_actions,
                "helperIds": row.get("helperIds") or [],
                "helperBehaviors": helper_behaviors(row),
                "effectWlkNos": row.get("effectWlkNos") or [],
                "resultWlkNos": row.get("resultWlkNos") or [],
                "frameSequence": row.get("frameSequence") or [],
                "hitEventCount": row.get("hitEventCount") or 0,
                "evidenceTierLabel": row.get("evidenceTierLabel"),
            }
        )

    class_counts = Counter(row["gapClass"] for row in rows)
    flag_counts = Counter(flag for row in rows for flag in row["unresolvedFlags"])
    by_class: dict[str, list[dict[str, Any]]] = defaultdict(list)
    for row in rows:
        by_class[row["gapClass"]].append(row)

    static_gap_rows = [row for row in rows if row["gapClass"] == "static-analysis-gap"]
    runner_gap_rows = [row for row in rows if row["gapClass"] == "runner-implementation-gap"]
    policy_rows = [row for row in rows if row["gapClass"] == "presentation-policy"]
    status = "no-unresolved-implementation-gaps" if not rows else "unresolved-flags-classified"
    report = {
        "version": 1,
        "kind": "hwanse-battle-skill-implementation-gap-review",
        "source": [
            "out/battle_skill_complete_pattern_review.json",
            "out/battle_helper_visual_behavior_review.json",
            "out/battle_effect_animation_pattern_review.json",
        ],
        "status": status,
        "summary": {
            "rowsWithFlags": len(rows),
            "supportPolicyRowsOutsideGap": support_policy_count,
            "presentationPolicyRows": len(policy_rows),
            "runnerImplementationGapRows": len(runner_gap_rows),
            "staticAnalysisGapRows": len(static_gap_rows),
            "unclassifiedGapRows": sum(1 for row in rows if row["gapClass"] == "unclassified-gap"),
            "gapClassCounts": dict(class_counts),
            "unresolvedFlagCounts": dict(flag_counts),
        },
        "interpretationNotes": [
            "이 파일은 새 연출을 승격하지 않고, complete pattern 리포트의 unresolved flag만 작업 성격별로 나눈다.",
            "support/status UI policy는 이제 unresolved flag가 아니며 complete pattern의 policyFlags/supportPolicyRows로 별도 관리한다.",
            "runner-implementation-gap은 EXE 근거가 있으나 웹 timeline/runner가 helper script를 충분히 실행하지 못하는 영역이다.",
            "static-analysis-gap은 helper target/placement 또는 helper visual frame을 더 파야 하는 영역이다.",
        ],
        "recommendedOrder": [
            {
                "step": "1",
                "title": "static-analysis-gap 먼저 축소",
                "reason": "helper visual frame/target placement는 EXE 근거 자체가 아직 부족하므로 runner 구현 전에 더 파야 한다.",
                "rowCount": len(static_gap_rows),
            },
            {
                "step": "2",
                "title": "runner-implementation-gap 반영",
                "reason": "frameScript/direct spawn/WLK 근거가 있으므로 battle timeline/runner의 실행 모델을 보강한다.",
                "rowCount": len(runner_gap_rows),
            },
            {
                "step": "3",
                "title": "presentation-policy는 전투 UI/status 작업으로 분리",
                "reason": "도주, 방어, 회복, 취기/도발/눈요기류는 공격 이펙트 재생 문제가 아니다.",
                "rowCount": len(policy_rows),
            },
        ],
        "rows": rows,
        "rowsByGapClass": {key: value for key, value in sorted(by_class.items())},
    }
    return report


def markdown(report: dict[str, Any]) -> str:
    lines = [
        "# Battle Skill Implementation Gap Review",
        "",
        f"- status: `{report['status']}`",
        "",
        "## Summary",
        "",
    ]
    for key, value in report["summary"].items():
        lines.append(f"- {key}: `{value}`")
    lines.extend(["", "## Recommended Order", ""])
    for step in report["recommendedOrder"]:
        lines.append(f"- {step['step']}. {step['title']} (`{step['rowCount']}` rows): {step['reason']}")
    lines.extend(
        [
            "",
            "## Rows",
            "",
            "| class | actor | skill | id | render | flags | helpers | next |",
            "|---|---|---|---:|---|---|---|---|",
        ]
    )
    for row in report["rows"]:
        helpers = compact(row["helperBehaviors"] or [f"#{hid}" for hid in row["helperIds"]], 5)
        lines.append(
            f"| `{row['gapClass']}` | {row['ownerName']} | {row['skillName']} | `{row['skillIdHex']}` | "
            f"`{row.get('renderTrack') or '-'}` | {compact(row['unresolvedFlags'], 4)} | {helpers} | {compact(row['nextActions'], 2)} |"
        )
    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 html_page(report: dict[str, Any]) -> str:
    notes = "".join(f"<li>{esc(note)}</li>" for note in report["interpretationNotes"])
    order = "".join(
        f"<li><b>{esc(step['title'])}</b> <code>{esc(step['rowCount'])}</code><br>{esc(step['reason'])}</li>"
        for step in report["recommendedOrder"]
    )
    rows = []
    for row in report["rows"]:
        helpers = compact(row["helperBehaviors"] or [f"#{hid}" for hid in row["helperIds"]], 8)
        rows.append(
            "<tr>"
            f"<td><span class='badge {esc(row['gapClass'])}'>{esc(row['gapClass'])}</span><br><small>{esc(row['gapLabel'])}</small></td>"
            f"<td>{esc(row['ownerName'])}</td>"
            f"<td><b>{esc(row['skillName'])}</b><br><code>{esc(row['skillIdHex'])}</code></td>"
            f"<td>{esc(row.get('renderTrack') or '-')}<br><small>{esc(row.get('implementationState') or '-')}</small></td>"
            f"<td>{esc(compact(row['unresolvedFlags'], 5))}</td>"
            f"<td>{esc(helpers)}</td>"
            f"<td>{esc(compact(row['reasons'], 3))}</td>"
            f"<td>{esc(compact(row['nextActions'], 3))}</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 Implementation Gap 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; padding: 2px 6px; border-radius: 4px; background: #263349; color: #dce8ff; }}
    .presentation-policy {{ background: #243b33; color: #b7f4d2; }}
    .runner-implementation-gap {{ background: #35314e; color: #ded6ff; }}
    .static-analysis-gap {{ background: #4a3420; color: #ffd8a8; }}
  </style>
</head>
<body>
  <h1>Battle Skill Implementation Gap Review</h1>
  <p><a href="../web/index.html">홈</a> · <a href="battle_skill_implementation_gap_review.json">JSON</a> · <a href="battle_skill_implementation_gap_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>{notes}</ul></section>
    <section class="panel"><h2>Recommended Order</h2><ol>{order}</ol></section>
  </div>
  <div class="wide">
    <table>
      <thead><tr><th>class</th><th>actor</th><th>skill</th><th>render</th><th>flags</th><th>helpers</th><th>reason</th><th>next</th></tr></thead>
      <tbody>{''.join(rows)}</tbody>
    </table>
  </div>
</body>
</html>
"""


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


if __name__ == "__main__":
    main()
