#!/usr/bin/env python3
"""Summarize confirmed static facts for battle helper effects.

This report intentionally stays conservative.  It does not decide that an
effect is a beam, slash, shockwave, or aura unless the VM stream already proves
the relevant primitive.  It joins the helper body, child script, and frameScript
reports and exposes only confirmed runtime ingredients: RNG ranges, parent
actor linkage, child/parent linkage, placement/write opcodes, frame/gate
sequences, actor flag writes, waits, loops, and screen helper calls.
"""
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"
HELPER_BODY_JSON = OUT / "battle_helper_body_review.json"
HELPER_CHILD_JSON = OUT / "battle_helper_child_script_review.json"
HELPER_FRAME_JSON = OUT / "battle_helper_frame_script_review.json"


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


def vec(values: list[Any] | None, prefix: str = "") -> str:
    if not values:
        return "-"
    return ", ".join(f"{prefix}{value}" for value in values)


def compact_skills(skills: list[dict[str, Any]], limit: int = 8) -> str:
    values = [
        f"{skill.get('ownerName')} {skill.get('skillName')} {skill.get('skillIdHex')}"
        for skill in skills
    ]
    if len(values) > limit:
        values = values[:limit] + [f"... +{len(values) - limit}"]
    return "; ".join(values) or "-"


def rows_for(decoded: dict[str, Any], *categories: str) -> list[dict[str, Any]]:
    wanted = set(categories)
    return [row for row in decoded.get("rows") or [] if row.get("category") in wanted]


def summaries(rows: list[dict[str, Any]], limit: int | None = None) -> list[str]:
    values = [f"{row.get('vaHex')}: {row.get('summary')}" for row in rows]
    if limit is not None and len(values) > limit:
        return values[:limit] + [f"... +{len(values) - limit}"]
    return values


def random_ranges(decoded: dict[str, Any]) -> list[int]:
    return [item.get("randomRange") for item in decoded.get("randomRanges") or [] if item.get("randomRange") is not None]


def actor_flag_masks(decoded: dict[str, Any]) -> list[str]:
    masks = []
    for row in rows_for(decoded, "actor-flags"):
        mask = row.get("maskHex")
        if mask:
            masks.append(mask)
    return masks


def frame_target_map(frame_report: dict[str, Any]) -> dict[str, dict[str, Any]]:
    return {
        row.get("targetVaHex"): row
        for row in frame_report.get("frameScriptRows") or []
        if row.get("targetVaHex")
    }


def skill_key(skill: dict[str, Any]) -> str:
    return f"{skill.get('ownerName')}::{skill.get('skillName')}::{skill.get('skillIdHex')}"


def build() -> dict[str, Any]:
    body_report = json.loads(HELPER_BODY_JSON.read_text(encoding="utf-8"))
    child_report = json.loads(HELPER_CHILD_JSON.read_text(encoding="utf-8"))
    frame_report = json.loads(HELPER_FRAME_JSON.read_text(encoding="utf-8"))
    frame_by_target = frame_target_map(frame_report)

    body_by_id = {int(row["helperId"]): row for row in body_report.get("helperRows") or []}
    runtime_rows = []
    skill_index: defaultdict[str, set[int]] = defaultdict(set)
    signal_counts: Counter[str] = Counter()

    for child in child_report.get("helperRows") or []:
        helper_id = int(child["helperId"])
        body = body_by_id.get(helper_id, {})
        decoded = child.get("decoded") or {}
        child_rows = decoded.get("rows") or []
        targets = [
            item.get("targetVaHex")
            for item in decoded.get("frameScriptTargets") or []
            if item.get("targetVaHex")
        ]
        frame_targets = []
        for target in targets:
            frame_row = frame_by_target.get(target)
            if not frame_row:
                continue
            frame_decoded = frame_row.get("decoded") or {}
            frame_targets.append(
                {
                    "targetVaHex": target,
                    "stopReason": frame_row.get("stopReason"),
                    "opcodeCounts": frame_row.get("opcodeCounts") or {},
                    "frameGateSequence": frame_row.get("frameGateSequence") or [],
                    "frameLabels": [item.get("label") for item in frame_row.get("frameGateSequence") or []],
                    "frameSequence": frame_row.get("frameSequence") or [],
                    "gateSequence": frame_row.get("gateSequence") or [],
                    "spriteHexSequence": frame_row.get("spriteHexSequence") or [],
                    "positionWrites": summaries(rows_for(frame_decoded, "write")),
                    "placementExprs": summaries(rows_for(frame_decoded, "placement-expr")),
                    "parentActorLinks": summaries(rows_for(frame_decoded, "parent-actor")),
                    "actorFlags": summaries(rows_for(frame_decoded, "actor-flags")),
                    "actorFlagMasks": actor_flag_masks(frame_decoded),
                    "loops": summaries(rows_for(frame_decoded, "jump", "repeat-loop")),
                    "destroyEnds": summaries(rows_for(frame_decoded, "destroy/end")),
                }
            )

        skills = child.get("skillRows") or []
        for skill in skills:
            skill_index[skill_key(skill)].add(helper_id)

        child_position_writes = rows_for(decoded, "write")
        child_child_writes = rows_for(decoded, "child-write")
        child_placement_exprs = rows_for(decoded, "placement-expr")
        child_motion_steps = rows_for(decoded, "motion-step")
        child_parent_links = rows_for(decoded, "parent-actor", "global-parent-anchor")
        child_pair_links = rows_for(decoded, "link-child-parent")
        child_clear_lists = rows_for(decoded, "clear-display-list")
        child_loops = rows_for(decoded, "jump", "repeat-loop", "call-subscript", "indexed-jump-table", "switch/control", "spawn-child-vm")

        confirmed = {
            "randomRanges": random_ranges(decoded),
            "randomRangeRows": decoded.get("randomRanges") or [],
            "hasFrameScript": bool(frame_targets),
            "frameScriptTargets": frame_targets,
            "initFrameSequence": decoded.get("initFrameSequence") or [],
            "directFrameSequence": decoded.get("frameSequence") or [],
            "waits": decoded.get("waits") or [],
            "parentActorLinks": summaries(child_parent_links),
            "childParentLinks": summaries(child_pair_links),
            "positionWrites": summaries(child_position_writes, limit=18),
            "childWrites": summaries(child_child_writes, limit=18),
            "placementExprs": summaries(child_placement_exprs),
            "motionSteps": summaries(child_motion_steps),
            "clearDisplayLists": summaries(child_clear_lists),
            "actorFlags": summaries(rows_for(decoded, "actor-flags")),
            "actorFlagMasks": actor_flag_masks(decoded),
            "loopsAndControls": summaries(child_loops, limit=18),
            "unknowns": decoded.get("unknowns") or [],
        }

        signals = []
        if confirmed["randomRanges"]:
            signals.append("random-range")
        if frame_targets:
            signals.append("frame-script")
        if confirmed["initFrameSequence"] or confirmed["directFrameSequence"]:
            signals.append("direct-child-frame")
        if confirmed["parentActorLinks"] or any(target["parentActorLinks"] for target in frame_targets):
            signals.append("parent-actor-link")
        if confirmed["childParentLinks"]:
            signals.append("child-parent-link")
        if confirmed["positionWrites"] or any(target["positionWrites"] for target in frame_targets):
            signals.append("position-write")
        if confirmed["placementExprs"] or any(target["placementExprs"] for target in frame_targets):
            signals.append("placement-expr")
        if confirmed["motionSteps"]:
            signals.append("motion-step")
        if confirmed["clearDisplayLists"]:
            signals.append("clear-display-list")
        if confirmed["actorFlags"] or any(target["actorFlags"] for target in frame_targets):
            signals.append("actor-flag")
        if confirmed["loopsAndControls"] or any(target["loops"] for target in frame_targets):
            signals.append("loop/control")
        if confirmed["waits"]:
            signals.append("wait")
        if confirmed["unknowns"]:
            signals.append("unknown")
        signal_counts.update(signals)

        runtime_rows.append(
            {
                "helperId": helper_id,
                "functionVaHex": body.get("functionVaHex") or child.get("functionVaHex"),
                "bodyClass": body.get("bodyClass") or child.get("bodyClass"),
                "childScriptVaHex": child.get("childScriptVaHex"),
                "childStopReason": decoded.get("stopReason"),
                "skillRows": skills,
                "openingObservations": child.get("openingObservations") or [],
                "signals": signals,
                "confirmed": confirmed,
                "childOpcodeCounts": decoded.get("opcodeCounts") or {},
            }
        )

    return {
        "version": 1,
        "kind": "hwanse-battle-helper-effect-runtime-review",
        "source": [
            "out/battle_helper_body_review.json",
            "out/battle_helper_child_script_review.json",
            "out/battle_helper_frame_script_review.json",
        ],
        "status": "confirmed-helper-static-facts",
        "runtimeUsed": False,
        "summary": {
            "helpers": len(runtime_rows),
            "skillsIndexed": len(skill_index),
            "helpersWithRandomRange": sum(1 for row in runtime_rows if row["confirmed"]["randomRanges"]),
            "helpersWithFrameScript": sum(1 for row in runtime_rows if row["confirmed"]["hasFrameScript"]),
            "helpersWithDirectChildFrame": sum(1 for row in runtime_rows if row["confirmed"]["initFrameSequence"] or row["confirmed"]["directFrameSequence"]),
            "helpersWithParentActorLink": sum(1 for row in runtime_rows if "parent-actor-link" in row["signals"]),
            "helpersWithPositionWrite": sum(1 for row in runtime_rows if "position-write" in row["signals"]),
            "helpersWithPlacementExpr": sum(1 for row in runtime_rows if "placement-expr" in row["signals"]),
            "helpersWithActorFlag": sum(1 for row in runtime_rows if "actor-flag" in row["signals"]),
            "helpersWithLoopOrControl": sum(1 for row in runtime_rows if "loop/control" in row["signals"]),
            "helpersWithUnknown": sum(1 for row in runtime_rows if "unknown" in row["signals"]),
            "signalCounts": dict(sorted(signal_counts.items())),
        },
        "interpretationNotes": [
            "This report is deliberately conservative: rows are grouped by confirmed static VM primitives, not by guessed visual names.",
            "random-range is opcode 0x2b. It calls RNG helper 0x427730 with a range/modulo and stores the result into display +0x58.",
            "frame-script is opcode 0x20 and points to a decoded display.frameScript(+0x64) stream.",
            "frame/gate values inside frameScript are opcode 0x21 writes. Gate values are retained as EXE timing fields.",
            "parent-actor-link is opcode 0x42 or 0x40 and proves that the helper object is bound to an actor/global anchor pointer.",
            "position-write and placement-expr are raw 0x12/0x10 primitives. Their exact game-space meaning still needs runner-side validation, so this report keeps the original summaries.",
            "actor-flag is opcode 0xad. It is a confirmed synchronization flag, but exact damage/hit semantics remain a separate battle-engine step.",
            "loop/control lists static loops, calls, switches, and jump-table controls. Loop expansion is intentionally not emulated here.",
        ],
        "runtimeRows": runtime_rows,
        "skillIndex": [
            {"skill": skill, "helperIds": sorted(helpers)}
            for skill, helpers in sorted(skill_index.items())
        ],
    }


def markdown(report: dict[str, Any]) -> str:
    lines = [
        "# Battle Helper Effect Runtime Review",
        "",
        f"- status: `{report['status']}`",
        f"- helpers: `{report['summary']['helpers']}`",
        f"- signal counts: `{report['summary']['signalCounts']}`",
        "",
        "## Interpretation",
        "",
    ]
    lines.extend(f"- {note}" for note in report["interpretationNotes"])
    lines.extend(
        [
            "",
            "## Helper Runtime Facts",
            "",
            "| helper | signals | skills | RNG ranges | direct frames | frameScript frames | parent | writes | flags | controls |",
            "| ---: | --- | --- | --- | --- | --- | --- | --- | --- | --- |",
        ]
    )
    for row in report["runtimeRows"]:
        confirmed = row["confirmed"]
        frame_labels = []
        for target in confirmed["frameScriptTargets"]:
            frame_labels.append(f"{target['targetVaHex']} [{vec(target['frameLabels'])}]")
        parent_count = len(confirmed["parentActorLinks"]) + sum(len(target["parentActorLinks"]) for target in confirmed["frameScriptTargets"])
        write_count = len(confirmed["positionWrites"]) + sum(len(target["positionWrites"]) for target in confirmed["frameScriptTargets"])
        flags = confirmed["actorFlagMasks"][:]
        for target in confirmed["frameScriptTargets"]:
            flags.extend(target["actorFlagMasks"])
        controls = len(confirmed["loopsAndControls"]) + sum(len(target["loops"]) for target in confirmed["frameScriptTargets"])
        lines.append(
            f"| {row['helperId']} | {vec(row['signals'])} | {compact_skills(row['skillRows'])} | "
            f"{vec(confirmed['randomRanges'])} | {vec(confirmed['initFrameSequence'] + confirmed['directFrameSequence'])} | "
            f"{'; '.join(frame_labels) or '-'} | {parent_count} | {write_count} | {vec(flags)} | {controls} |"
        )
    return "\n".join(lines) + "\n"


def html_list(values: list[str], limit: int = 12) -> str:
    if not values:
        return "-"
    clipped = values[:limit]
    suffix = f"<li>... +{len(values) - limit}</li>" if len(values) > limit else ""
    return "<ul>" + "".join(f"<li>{esc(value)}</li>" for value in clipped) + suffix + "</ul>"


def html_page(report: dict[str, Any]) -> str:
    summary_rows = "".join(f"<tr><td>{esc(k)}</td><td><code>{esc(v)}</code></td></tr>" for k, v in report["summary"].items())
    notes = "".join(f"<li>{esc(note)}</li>" for note in report["interpretationNotes"])
    rows = []
    for row in report["runtimeRows"]:
        confirmed = row["confirmed"]
        frame_target_html = []
        for target in confirmed["frameScriptTargets"]:
            frame_rows = "<br>".join(
                f"<code>{esc(item.get('vaHex'))}</code> sprite {esc(item.get('spriteHex'))} frame {esc(item.get('frame'))} gate {esc(item.get('gate'))}"
                for item in target["frameGateSequence"]
            ) or "-"
            details = "".join(
                [
                    f"<details><summary>frames {esc(target['targetVaHex'])}</summary>{frame_rows}</details>",
                    f"<details><summary>frameScript writes</summary>{html_list(target['positionWrites'])}</details>",
                    f"<details><summary>frameScript placement</summary>{html_list(target['placementExprs'])}</details>",
                    f"<details><summary>frameScript parent/flags/control</summary>{html_list(target['parentActorLinks'] + target['actorFlags'] + target['loops'] + target['destroyEnds'])}</details>",
                ]
            )
            frame_target_html.append(details)
        flags = confirmed["actorFlagMasks"][:]
        for target in confirmed["frameScriptTargets"]:
            flags.extend(target["actorFlagMasks"])
        rows.append(
            "<tr>"
            f"<td><code>{esc(row['helperId'])}</code><br><code>{esc(row.get('functionVaHex'))}</code><br>{esc(row.get('bodyClass'))}</td>"
            f"<td>{esc(vec(row['signals']))}</td>"
            f"<td>{esc(compact_skills(row['skillRows']))}</td>"
            f"<td>{esc(vec(confirmed['randomRanges']))}</td>"
            f"<td>{esc(vec(confirmed['initFrameSequence'] + confirmed['directFrameSequence']))}</td>"
            f"<td>{''.join(frame_target_html) or '-'}</td>"
            f"<td>{html_list(confirmed['parentActorLinks'] + confirmed['childParentLinks'])}</td>"
            f"<td>{html_list(confirmed['positionWrites'])}</td>"
            f"<td>{html_list(confirmed['childWrites'])}</td>"
            f"<td>{html_list(confirmed['placementExprs'] + confirmed['motionSteps'])}</td>"
            f"<td>{esc(vec(flags))}</td>"
            f"<td>{html_list(confirmed['loopsAndControls'] + confirmed['clearDisplayLists'])}</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 Helper Effect Runtime 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; }}
    details {{ margin: 4px 0; }}
    summary {{ cursor: pointer; color: #ffd37a; }}
    ul {{ margin: 0; padding-left: 18px; }}
    .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 Helper Effect Runtime Review</h1>
  <p><a href="../web/index.html">홈</a> · <a href="../web/battle_simulator.html">전투 기술 실행</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_sync_timing_review.html">helper sync timing</a> · <a href="battle_helper_effect_runtime_review.json">JSON</a> · <a href="battle_helper_effect_runtime_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>Interpretation</h2><ul>{notes}</ul></section>
  </div>
  <h2>Confirmed Runtime Facts</h2>
  <div class="wide"><table><thead><tr><th>helper</th><th>signals</th><th>skills</th><th>RNG ranges</th><th>direct frames</th><th>frameScript</th><th>parent/link</th><th>position writes</th><th>child writes</th><th>placement/motion</th><th>flags</th><th>control</th></tr></thead><tbody>{''.join(rows)}</tbody></table></div>
</body>
</html>
"""


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


if __name__ == "__main__":
    main()
