#!/usr/bin/env python3
"""Build a per-skill frame gate coverage review for battle actions.

This report separates three timing layers that should not be mixed:

* actor frame gates from opcode 0x21, including finite 0x06 repeat-loop expansion
* engine/barrier waits from 0xbf/0xc1, which are not fixed millisecond delays
* helper/effect frameScript gates exposed through the helper sync reports
"""
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"
TIMELINE_JSON = OUT / "battle_action_event_timeline_review.json"
HELPER_SYNC_JSON = OUT / "battle_helper_sync_timing_review.json"
RUNTIME_TIMING_JSON = OUT / "runtime_timing.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 key_for(row: dict[str, Any]) -> str:
    return f"{row.get('ownerKey')}:{str(row.get('skillIdHex')).lower()}"


def short_frames(frames: list[dict[str, Any]], limit: int = 36) -> str:
    labels = []
    for frame in frames[:limit]:
        label = f"{frame.get('frame')}@{frame.get('gate')}"
        if frame.get("repeatIteration"):
            label += f"r{frame.get('repeatIteration')}"
        labels.append(label)
    if len(frames) > limit:
        labels.append(f"... +{len(frames) - limit}")
    return vec(labels)


def build_helper_index(helper_sync: dict[str, Any]) -> dict[int, dict[str, Any]]:
    by_helper: dict[int, dict[str, Any]] = {}
    for helper in helper_sync.get("rows") or []:
        helper_id = helper.get("helperId")
        if helper_id is None:
            continue
        helper_summary = {
            "helperId": helper_id,
            "syncSignals": helper.get("syncSignals") or [],
            "childScriptVaHex": helper.get("childScriptVaHex"),
            "frameScriptCount": len(helper.get("frameScriptTimelines") or []),
            "actorFlagCount": len(helper.get("combinedActorFlagEvents") or []),
            "waitCount": len((helper.get("childTimeline") or {}).get("waitEvents") or []),
            "loopingFrameScriptCount": sum(1 for target in helper.get("frameScriptTimelines") or [] if target.get("loopEvents")),
            "frameScripts": [
                {
                    "targetVaHex": target.get("targetVaHex"),
                    "durationGate": target.get("durationGate"),
                    "frameGateSequence": [
                        {"frame": item.get("frame"), "gate": item.get("gate"), "tick": item.get("tick")}
                        for item in target.get("frameEvents") or []
                    ],
                    "loopEvents": target.get("loopEvents") or [],
                }
                for target in helper.get("frameScriptTimelines") or []
            ],
        }
        by_helper[int(helper_id)] = helper_summary
    return by_helper


def wall_clock_calibration() -> dict[str, Any]:
    if not RUNTIME_TIMING_JSON.exists():
        return {
            "status": "missing-runtime-timing-report",
            "previewPolicy": "durationMs = max(1, gate) * 48; runtime_timing.json was not available when this report was generated.",
        }
    timing = json.loads(RUNTIME_TIMING_JSON.read_text(encoding="utf-8"))
    loop = timing.get("loop") or {}
    api = timing.get("timingApi") or {}
    tick_ms = int(loop.get("frameIntervalMs") or 48)
    fps = loop.get("nominalFps")
    return {
        "status": "exe-main-loop-grounded",
        "tickMs": tick_ms,
        "fpsApprox": fps if fps is not None else round(1000 / tick_ms, 6),
        "evidence": [
            f"{api.get('dll', 'KERNEL32.dll')}!{api.get('name', 'GetTickCount')} refs: {', '.join(api.get('directTextCallRefs') or [])}",
            f"frame interval global {loop.get('frameIntervalGlobalHex')} = {tick_ms}ms",
            f"update target {loop.get('updateTargetVaHex')} is called from {loop.get('updateCallVaHex')}",
            f"catch-up update count is clamped to {loop.get('frameCap')}",
        ],
        "previewPolicy": f"durationMs = max(1, gate) * {tick_ms}; wait barriers such as 0xbf/0xc1 remain barrier events, not fixed frame gates.",
    }


def classify(row: dict[str, Any]) -> str:
    if row["expandedRepeatLoopCount"]:
        return "fixed-gate-repeat-expanded"
    if row["waitBarrierCount"] and row["helperFrameScriptCount"]:
        return "actor-gates-plus-engine-wait-and-helper"
    if row["helperFrameScriptCount"]:
        return "actor-gates-plus-helper-frameScript"
    if row["waitBarrierCount"]:
        return "actor-gates-plus-engine-wait"
    if row["directFrameSelectorCount"]:
        return "actor-gates-with-direct-frame-selector"
    return "actor-frame-gates"


def wait_kind(item: dict[str, Any]) -> str:
    opcode = item.get("opcode")
    if opcode == "0xbf":
        return "result/reaction-barrier"
    if opcode == "0xc1":
        return "actor-flag-barrier"
    return "unknown-wait"


def build() -> dict[str, Any]:
    display = json.loads(DISPLAY_JSON.read_text(encoding="utf-8"))
    timeline = json.loads(TIMELINE_JSON.read_text(encoding="utf-8"))
    helper_sync = json.loads(HELPER_SYNC_JSON.read_text(encoding="utf-8"))
    helper_by_id = build_helper_index(helper_sync)
    timeline_by_key = {key_for(row): row for row in timeline.get("rows") or []}

    rows = []
    class_counts: Counter[str] = Counter()
    wait_opcode_counts: Counter[str] = Counter()
    for decoded in display.get("decodedRows") or []:
        key = key_for(decoded)
        action = timeline_by_key.get(key, {})
        raw_rows = decoded.get("rows") or []
        raw_frames = [row for row in raw_rows if row.get("category") == "frame"]
        expanded_frames = (action.get("timeline") or {}).get("frameEvents") or []
        repeat_loops = [row for row in raw_rows if row.get("category") == "repeat-loop"]
        waits = [row for row in raw_rows if row.get("category") == "wait" or row.get("opcode") in {"0xbf", "0xc1"}]
        wait_opcode_counts.update(str(row.get("opcode") or "unknown") for row in waits)
        direct = [row for row in raw_rows if row.get("directFrameSelector")]
        action_timeline = action.get("timeline") or {}
        action_helper_ids = [
            int(item.get("helperId"))
            for item in action_timeline.get("helperCalls") or []
            if item.get("helperId") is not None
        ]
        helpers = [helper_by_id[helper_id] for helper_id in action_helper_ids if helper_id in helper_by_id]
        helper_frame_scripts = sum(helper.get("frameScriptCount") or 0 for helper in helpers)
        helper_waits = sum(helper.get("waitCount") or 0 for helper in helpers)
        helper_loops = sum(helper.get("loopingFrameScriptCount") or 0 for helper in helpers)
        row = {
            "ownerKey": decoded.get("ownerKey"),
            "ownerName": decoded.get("ownerName"),
            "skillName": decoded.get("skillName"),
            "familyName": decoded.get("familyName"),
            "skillIdHex": decoded.get("skillIdHex"),
            "levelOrFixed": decoded.get("levelOrFixed"),
            "entryStartVaHex": decoded.get("entryStartVaHex"),
            "startSource": decoded.get("startSource"),
            "confidence": decoded.get("confidence"),
            "note": decoded.get("note"),
            "rawFrameCount": len(raw_frames),
            "expandedFrameCount": len(expanded_frames),
            "rawDurationGate": sum(int(frame.get("gate") or 0) for frame in raw_frames),
            "expandedDurationGate": action.get("durationGateFromFrames"),
            "rawFrameGateSequence": [{"frame": item.get("frame"), "gate": item.get("gate")} for item in raw_frames],
            "expandedFrameGateSequence": [
                {
                    "tick": item.get("tick"),
                    "frame": item.get("frame"),
                    "gate": item.get("gate"),
                    "repeatIteration": item.get("repeatIteration"),
                    "repeatSourceVaHex": item.get("repeatSourceVaHex"),
                }
                for item in expanded_frames
            ],
            "expandedRepeatLoopCount": action.get("repeatLoopCount") or 0,
            "repeatLoops": [
                {
                    "vaHex": item.get("vaHex"),
                    "targetVaHex": item.get("targetVaHex"),
                    "repeatCount": item.get("repeatCount"),
                    "summary": item.get("summary"),
                }
                for item in repeat_loops
            ],
            "waitBarrierCount": len(waits),
            "waitBarriers": [
                {
                    "vaHex": item.get("vaHex"),
                    "opcode": item.get("opcode"),
                    "waitKind": wait_kind(item),
                    "summary": item.get("summary"),
                    "bytes": item.get("bytes"),
                }
                for item in waits
            ],
            "directFrameSelectorCount": len(direct),
            "resultHitEventCount": action.get("hitEventCount") or 0,
            "confirmedHitEventCount": (action.get("hitClassCounts") or {}).get("confirmed-result-hit-window", 0),
            "helperIds": sorted(set(action_helper_ids)),
            "helperFrameScriptCount": helper_frame_scripts,
            "helperWaitCount": helper_waits,
            "helperLoopCount": helper_loops,
            "helperTiming": helpers,
        }
        row["timingClass"] = classify(row)
        class_counts[row["timingClass"]] += 1
        rows.append(row)

    return {
        "version": 1,
        "kind": "hwanse-battle-frame-gate-review",
        "source": [
            "out/battle_display_vm_static_decode.json",
            "out/battle_action_event_timeline_review.json",
            "out/battle_helper_sync_timing_review.json",
            "out/runtime_timing.json",
        ],
        "status": "actor-repeat-gates-expanded-helper-gates-indexed",
        "wallClockCalibration": wall_clock_calibration(),
        "summary": {
            "rows": len(rows),
            "rowsWithRepeatLoopExpansion": sum(1 for row in rows if row["expandedRepeatLoopCount"]),
            "rowsWithEngineWaitBarrier": sum(1 for row in rows if row["waitBarrierCount"]),
            "rowsWithHelperFrameScript": sum(1 for row in rows if row["helperFrameScriptCount"]),
            "rowsWithDirectFrameSelector": sum(1 for row in rows if row["directFrameSelectorCount"]),
            "resultHitEventsAfterRepeatExpansion": sum(row["resultHitEventCount"] for row in rows),
            "confirmedHitEventsAfterRepeatExpansion": sum(row["confirmedHitEventCount"] for row in rows),
            "timingClassCounts": dict(sorted(class_counts.items())),
            "waitBarrierOpcodeCounts": dict(sorted(wait_opcode_counts.items())),
        },
        "interpretationNotes": [
            "0x21 frame gate values are fixed EXE gate fields and are accumulated as gate units, not browser milliseconds.",
            "0x06 repeat-loop bodies are expanded for schedule review. The repeat count is total body executions, so only count-1 extra copies are appended after the original body.",
            "0xbf almost always appears after 0xc2 result sound and 0xad hit flag set/clear. It is best treated as a result/reaction barrier, not as a fixed sound-duration delay.",
            "0xc1 carries an actor flag mask and is best treated separately as an actor-flag barrier.",
            "Helper frameScripts have their own 0x21 frame/gate timelines and are indexed per skill when the helper id is known.",
            "This report verifies scheduling coverage only. It still does not prove damage, miss, critical, or status formula timing.",
            "Static EXE main-loop analysis grounds wall-clock preview at 48ms per frame gate tick (about 20.83Hz update).",
        ],
        "rows": rows,
    }


def markdown(report: dict[str, Any]) -> str:
    lines = [
        "# Battle Frame Gate Review",
        "",
        f"- status: `{report['status']}`",
        f"- rows: `{report['summary']['rows']}`",
        f"- repeat-loop expanded rows: `{report['summary']['rowsWithRepeatLoopExpansion']}`",
        f"- result hit events after expansion: `{report['summary']['resultHitEventsAfterRepeatExpansion']}`",
        "",
        "## Interpretation",
        "",
    ]
    lines.extend(f"- {note}" for note in report["interpretationNotes"])
    lines.extend(
        [
            "",
            "## Rows",
            "",
            "| actor | skill | id | class | raw frames | expanded frames | raw gate | expanded gate | repeat | waits | helper scripts | hits | frames@gate |",
            "| --- | --- | ---: | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- |",
        ]
    )
    for row in report["rows"]:
        lines.append(
            f"| {row['ownerName']} | {row['skillName']} | `{row['skillIdHex']}` | `{row['timingClass']}` | "
            f"{row['rawFrameCount']} | {row['expandedFrameCount']} | {row['rawDurationGate']} | {row['expandedDurationGate']} | "
            f"{row['expandedRepeatLoopCount']} | {row['waitBarrierCount']} | {row['helperFrameScriptCount']} | {row['resultHitEventCount']} | "
            f"{short_frames(row['expandedFrameGateSequence'], limit=18)} |"
        )
    return "\n".join(lines) + "\n"


def details_html(row: dict[str, Any]) -> str:
    loops = vec([f"{item.get('vaHex')} -> {item.get('targetVaHex')} x{item.get('repeatCount')}" for item in row.get("repeatLoops") or []])
    waits = vec([f"{item.get('opcode')} {item.get('waitKind')} {item.get('vaHex')} {item.get('summary')}" for item in row.get("waitBarriers") or []], "")
    helpers = []
    for helper in row.get("helperTiming") or []:
        frames = []
        for script in helper.get("frameScripts") or []:
            frame_text = vec([f"{item.get('frame')}@{item.get('gate')}" for item in script.get("frameGateSequence") or []])
            frames.append(f"{script.get('targetVaHex')} gate {script.get('durationGate')}: {frame_text}")
        helpers.append(f"#{helper.get('helperId')} signals {vec(helper.get('syncSignals'))}; {vec(frames)}")
    return (
        f"<details><summary>detail</summary>"
        f"<p><strong>repeat:</strong> {esc(loops)}</p>"
        f"<p><strong>wait:</strong> {esc(waits)}</p>"
        f"<p><strong>helper:</strong> {esc(vec(helpers))}</p>"
        f"<p><strong>expanded frames:</strong> {esc(short_frames(row.get('expandedFrameGateSequence') or [], limit=80))}</p>"
        f"</details>"
    )


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"])
    table_rows = []
    for row in report["rows"]:
        table_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['timingClass'])}</code></td>"
            f"<td>{esc(row['rawFrameCount'])} -> {esc(row['expandedFrameCount'])}</td>"
            f"<td>{esc(row['rawDurationGate'])} -> {esc(row['expandedDurationGate'])}</td>"
            f"<td>{esc(row['expandedRepeatLoopCount'])}</td>"
            f"<td>{esc(row['waitBarrierCount'])}</td>"
            f"<td>{esc(row['helperFrameScriptCount'])}</td>"
            f"<td>{esc(row['resultHitEventCount'])} / {esc(row['confirmedHitEventCount'])}</td>"
            f"<td>{esc(short_frames(row.get('expandedFrameGateSequence') or []))}</td>"
            f"<td>{details_html(row)}</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 Frame Gate 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 Frame Gate Review</h1>
  <p><a href="../web/index.html">홈</a> · <a href="../web/battle_simulator.html">전투 기술 실행</a> · <a href="battle_action_event_timeline_review.html">actor hit timeline</a> · <a href="battle_helper_sync_timing_review.html">helper sync timing</a> · <a href="battle_frame_gate_review.json">JSON</a> · <a href="battle_frame_gate_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>Per Skill Gate Coverage</h2>
  <div class="wide">
    <table>
      <thead><tr><th>actor</th><th>skill</th><th>class</th><th>frames</th><th>gate</th><th>repeat</th><th>wait</th><th>helper FS</th><th>hits</th><th>expanded frames@gate</th><th>detail</th></tr></thead>
      <tbody>{''.join(table_rows)}</tbody>
    </table>
  </div>
</body>
</html>
"""


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


if __name__ == "__main__":
    main()
