#!/usr/bin/env python3
"""Classify unresolved child target scripts from battle child lifecycle review.

The previous lifecycle report intentionally left direct child targets with
``target-script-stopReason`` when their lifetime was not a simple frameScript
duration.  This pass resolves those targets into renderer-facing classes:
motion-boundary loops, nested child spawners, and counter-held direct frames.
"""

from __future__ import annotations

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"

LIFECYCLE = OUT / "battle_child_lifecycle_draw_order_review.json"
SPAWN_TREE = OUT / "battle_helper_spawn_tree_review.json"
VISUAL = OUT / "battle_helper_visual_behavior_review.json"
POSITION = OUT / "battle_helper_position_motion_review.json"

OUT_JSON = OUT / "battle_child_target_script_expansion_review.json"


def load(path: Path) -> dict[str, Any]:
    return json.loads(path.read_text(encoding="utf-8"))


def as_list(value: Any) -> list[Any]:
    return value if isinstance(value, list) else []


def index_rows(path: Path, key: str) -> dict[int, dict[str, Any]]:
    return {row["helperId"]: row for row in load(path).get(key, []) if isinstance(row.get("helperId"), int)}


def spawn_node_index() -> dict[tuple[int, str], dict[str, Any]]:
    out: dict[tuple[int, str], dict[str, Any]] = {}
    for row in load(SPAWN_TREE).get("helperRows", []):
        helper_id = row.get("helperId")
        for node in row.get("flatNodes") or row.get("nodes") or []:
            target = node.get("targetVaHex")
            if isinstance(helper_id, int) and target:
                out[(helper_id, str(target))] = node
    return out


def first_counter_initial(node: dict[str, Any], visual: dict[str, Any], field_hex: str) -> int | None:
    for source in [node, visual.get("lifecycle") or {}]:
        for step in as_list(source.get("motionWrites")) + as_list(source.get("rootCounterSteps")):
            if step.get("fieldHex") == field_hex or step.get("destHex") == field_hex:
                if step.get("operation") == "set" and isinstance(step.get("rawValue"), int):
                    return int(step["rawValue"])
    counters = (visual.get("lifecycle") or {}).get("rootCounterSets") or {}
    value = counters.get(field_hex)
    return int(value) if isinstance(value, int) else None


def branch_fields(node: dict[str, Any]) -> list[str]:
    fields = []
    for branch in as_list(node.get("branchTargets")):
        summary = str(branch.get("summary") or "")
        for field in ["0x8c", "0x8e", "0x90", "0x92", "0x94", "0x96"]:
            if f"[+{field}]" in summary or f"field +{field}" in summary:
                fields.append(field)
    return sorted(set(fields))


def classify_target(node: dict[str, Any], visual: dict[str, Any]) -> tuple[str, dict[str, Any]]:
    tags = set(node.get("tags") or [])
    primary = node.get("primaryClass")
    branches = as_list(node.get("branchTargets"))
    branch_summaries = [str(branch.get("summary") or "") for branch in branches]
    fields = branch_fields(node)
    motion_steps = as_list(node.get("motionSteps"))
    spawn_rows = as_list(node.get("spawnRows"))
    random_ranges = as_list(node.get("randomRanges"))
    resolved = as_list(node.get("resolvedFrameScripts"))

    if "spawns-children" in tags or primary == "spawns-children" or spawn_rows:
        repeat_field = fields[0] if fields else None
        repeat_count = first_counter_initial(node, visual, repeat_field) if repeat_field else None
        return "nested-child-spawner", {
            "spawnTargets": [row.get("targetVaHex") for row in spawn_rows],
            "randomRanges": random_ranges,
            "repeatField": repeat_field,
            "repeatCountCandidate": repeat_count,
            "resolvedFrameScriptDurations": [item.get("durationGate") for item in resolved if item.get("durationGate") is not None],
        }

    if motion_steps:
        return "motion-boundary-loop", {
            "motionSteps": motion_steps,
            "boundaryBranches": branch_summaries,
            "boundaryTargets": [branch.get("targetVaHex") for branch in branches],
            "initFrames": node.get("initFrames") or [],
        }

    if any("local-backward-loop" in summary for summary in branch_summaries):
        fields_for_counter = fields or ["0x96"]
        counter_candidates = {
            field: first_counter_initial(node, visual, field)
            for field in fields_for_counter
        }
        return "counter-held-direct-frame", {
            "counterFields": fields_for_counter,
            "counterCandidates": counter_candidates,
            "initFrames": node.get("initFrames") or [],
            "directFrames": node.get("directFrames") or [],
            "branchSummaries": branch_summaries,
        }

    if resolved:
        return "frameScript-backed-target", {
            "resolvedFrameScriptDurations": [item.get("durationGate") for item in resolved if item.get("durationGate") is not None],
            "frameLabels": [label for item in resolved for label in (item.get("frameLabels") or [])],
        }

    return "unclassified-target-script", {
        "primaryClass": primary,
        "tags": sorted(tags),
        "branchSummaries": branch_summaries,
    }


def build_report() -> dict[str, Any]:
    lifecycle = load(LIFECYCLE)
    node_idx = spawn_node_index()
    visual_idx = index_rows(VISUAL, "rows")
    position_idx = index_rows(POSITION, "helperRows")

    rows = []
    for skill in lifecycle.get("rows", []):
        for helper in skill.get("helpers", []):
            helper_id = helper.get("helperId")
            if not isinstance(helper_id, int):
                continue
            visual = visual_idx.get(helper_id, {})
            position = position_idx.get(helper_id, {})
            for event in helper.get("spawnPlan", []):
                if (event.get("lifetime") or {}).get("class") != "target-script-stopReason":
                    continue
                target = str(event.get("targetVaHex"))
                node = node_idx.get((helper_id, target), {})
                target_class, details = classify_target(node, visual)
                rows.append(
                    {
                        "ownerName": skill.get("ownerName"),
                        "skillName": skill.get("skillName"),
                        "skillIdHex": skill.get("skillIdHex"),
                        "levelOrFixed": skill.get("levelOrFixed"),
                        "helperId": helper_id,
                        "targetVaHex": target,
                        "tick": event.get("tick"),
                        "frame": event.get("label") or event.get("frame"),
                        "previousLifetimeClass": event["lifetime"]["class"],
                        "targetScriptClass": target_class,
                        "nodePrimaryClass": node.get("primaryClass"),
                        "nodeTags": node.get("tags") or [],
                        "stopReason": node.get("stopReason") or event["lifetime"].get("source"),
                        "nodeOpcodeCounts": node.get("opcodeCounts") or {},
                        "positionPatterns": position.get("patterns") or [],
                        "transform": event.get("transform") or {},
                        "details": details,
                    }
                )

    class_counts = Counter(row["targetScriptClass"] for row in rows)
    helper_counts = Counter(row["helperId"] for row in rows)
    skill_counts = Counter(f"{row['ownerName']} {row['skillName']} {row['skillIdHex']}" for row in rows)
    promoted = sum(1 for row in rows if row["targetScriptClass"] != "unclassified-target-script")

    return {
        "version": 1,
        "kind": "hwanse-battle-child-target-script-expansion-review",
        "source": "tools/build_battle_child_target_script_expansion_review.py",
        "runtimeUsed": False,
        "inputs": [
            str(LIFECYCLE.relative_to(ROOT)),
            str(SPAWN_TREE.relative_to(ROOT)),
            str(VISUAL.relative_to(ROOT)),
            str(POSITION.relative_to(ROOT)),
        ],
        "status": "static-target-script-expanded",
        "summary": {
            "targetScriptStopReasonEvents": len(rows),
            "promotedTargetScriptEvents": promoted,
            "remainingUnclassifiedEvents": len(rows) - promoted,
            "targetScriptClassCounts": dict(sorted(class_counts.items())),
            "helperCounts": dict(sorted((str(k), v) for k, v in helper_counts.items())),
            "skillCounts": dict(sorted(skill_counts.items())),
        },
        "interpretationNotes": [
            "nested-child-spawner means the target script itself creates child display VMs; renderer must recurse one level instead of treating it as a static frame.",
            "motion-boundary-loop means an init frame persists while display position is advanced by motion-step opcode until boundary branches end the child.",
            "counter-held-direct-frame means the child is held by a decrementing counter field; the counter candidate is taken from the helper lifecycle/root counter writes.",
            "No runtime capture is used; all rows are from static spawn-tree and helper motion reports.",
        ],
        "rows": rows,
    }


def render_list(items: list[Any], limit: int = 10) -> str:
    if not items:
        return "<span class='muted'>none</span>"
    body = "<br>".join(html.escape(str(item)) for item in items[:limit])
    if len(items) > limit:
        body += f"<br><span class='muted'>+{len(items)-limit} more</span>"
    return body


def render_dict(value: dict[str, Any]) -> str:
    if not value:
        return "<span class='muted'>none</span>"
    return "<br>".join(f"<code>{html.escape(str(k))}</code>: {html.escape(str(v))}" for k, v in value.items())


def render_html(report: dict[str, Any]) -> str:
    cards = "".join(
        f"<div class='card'><b>{html.escape(k)}</b><pre>{html.escape(json.dumps(v, ensure_ascii=False, indent=2))}</pre></div>"
        for k, v in report["summary"].items()
    )
    notes = "".join(f"<li>{html.escape(note)}</li>" for note in report["interpretationNotes"])
    rows = []
    for row in report["rows"]:
        rows.append(
            "<tr>"
            f"<td><b>{html.escape(str(row['ownerName']))}</b><br>{html.escape(str(row['skillName']))}<br><code>{html.escape(str(row['skillIdHex']))}</code> Lv={html.escape(str(row.get('levelOrFixed')))}</td>"
            f"<td>helper #{row['helperId']}<br><code>{html.escape(row['targetVaHex'])}</code><br>tick {html.escape(str(row.get('tick')))} / frame {html.escape(str(row.get('frame')))}</td>"
            f"<td><b>{html.escape(row['targetScriptClass'])}</b><br>{html.escape(str(row.get('nodePrimaryClass')))}<br>{render_list(row.get('nodeTags') or [], 8)}</td>"
            f"<td>{html.escape(str(row.get('stopReason')))}<hr>{render_dict(row.get('nodeOpcodeCounts') or {})}</td>"
            f"<td>{render_dict(row.get('details') or {})}<hr>{render_list(row.get('positionPatterns') or [], 8)}</td>"
            "</tr>"
        )
    return f"""<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Battle Child Target Script Expansion Review</title>
  <style>
    body {{ margin: 0; padding: 24px; background: #101218; color: #edf1f7; font: 14px/1.5 system-ui, sans-serif; }}
    a {{ color: #8ec5ff; }}
    h1 {{ margin: 0 0 8px; font-size: 24px; }}
    .muted {{ color: #9aa4b2; }}
    .cards {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 12px; margin: 16px 0; }}
    .card {{ border: 1px solid #2c3444; border-radius: 8px; padding: 12px; background: #171c25; }}
    pre {{ white-space: pre-wrap; margin: 8px 0 0; color: #b9c4d6; }}
    table {{ width: 100%; border-collapse: collapse; margin-top: 16px; }}
    th, td {{ border-top: 1px solid #2c3444; padding: 10px; vertical-align: top; }}
    th {{ text-align: left; position: sticky; top: 0; background: #101218; }}
    code {{ color: #ffd58c; }}
    hr {{ border: 0; border-top: 1px solid #2c3444; margin: 8px 0; }}
  </style>
</head>
<body>
  <h1>Battle Child Target Script Expansion Review</h1>
  <p class="muted">target-script-stopReason으로 남은 child target을 nested spawner, motion boundary loop, counter held frame으로 재분류한 정적 분석입니다.</p>
  <p><a href="../web/index.html">index</a> · <a href="battle_child_lifecycle_draw_order_review.json">child lifecycle order JSON</a> · <a href="../web/battle_skill_timeline_review.html">skill timeline</a></p>
  <div class="cards">{cards}</div>
  <ul>{notes}</ul>
  <table>
    <thead><tr><th>기술</th><th>target</th><th>승격 분류</th><th>stop/opcode</th><th>renderer 근거</th></tr></thead>
    <tbody>{''.join(rows)}</tbody>
  </table>
</body>
</html>
"""


def main() -> None:
    report = build_report()
    OUT_JSON.write_text(json.dumps(report, ensure_ascii=False, separators=(",", ":")) + "\n", encoding="utf-8")
    print(json.dumps(report["summary"], ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()
