#!/usr/bin/env python3
"""Summarize prompt-free vLLM scheduler/operator-profiler step records."""

from __future__ import annotations

import argparse
import json
import math
import statistics
from collections import Counter, defaultdict
from pathlib import Path
from typing import Any


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser()
    parser.add_argument("--cell-root", type=Path, required=True)
    parser.add_argument("--output", type=Path, required=True)
    return parser.parse_args()


def percentile(values: list[float], fraction: float) -> float:
    ordered = sorted(values)
    index = min(len(ordered) - 1, max(0, math.ceil(fraction * len(ordered)) - 1))
    return ordered[index]


def stats(values: list[float]) -> dict[str, float]:
    return {
        "min": min(values),
        "mean": statistics.fmean(values),
        "p50": percentile(values, 0.50),
        "p95": percentile(values, 0.95),
        "p99": percentile(values, 0.99),
        "max": max(values),
    }


def summarize_cell(path: Path) -> dict[str, Any]:
    streams = sorted(path.glob("opprof/*.jsonl"))
    if len(streams) != 1:
        raise ValueError(f"expected one opprof stream in {path}, got {len(streams)}")
    groups: dict[tuple[str, str], dict[str, list[float]]] = defaultdict(
        lambda: defaultdict(list)
    )
    phase_counts: Counter[str] = Counter()
    graph_counts: Counter[str] = Counter()
    prefix_queries = 0
    prefix_hits = 0
    total_records = 0
    dropped_records_max = 0
    with streams[0].open() as handle:
        for line in handle:
            record = json.loads(line)
            if not record.get("model_executed", False):
                continue
            prefill_tokens = int(record.get("prefill_tokens", 0))
            decode_tokens = int(record.get("decode_tokens", 0))
            if prefill_tokens and decode_tokens:
                phase = "true_mixed"
            elif prefill_tokens:
                phase = "pure_prefill"
            elif decode_tokens:
                phase = "pure_decode"
            else:
                phase = "empty"
            graph = str((record.get("cudagraph") or {}).get("runtime_mode", "UNKNOWN"))
            duration_ms = (
                int(record["complete_mono_ns"]) - int(record["submit_mono_ns"])
            ) / 1e6
            if duration_ms < 0:
                raise ValueError(f"negative duration in {streams[0]}")
            group = groups[(phase, graph)]
            group["submit_to_complete_ms"].append(duration_ms)
            group["scheduled_requests"].append(int(record["scheduled_requests"]))
            group["prefill_tokens"].append(prefill_tokens)
            group["decode_tokens"].append(decode_tokens)
            group["total_tokens"].append(prefill_tokens + decode_tokens)
            group["queue_waiting"].append(int(record["queues"]["waiting"]))
            group["kv_usage"].append(float(record["kv"]["usage"]))
            local_prefix = (record.get("prefix") or {}).get("local") or {}
            prefix_queries += int(local_prefix.get("queries", 0))
            prefix_hits += int(local_prefix.get("hits", 0))
            dropped_records_max = max(
                dropped_records_max, int(record.get("dropped_records_before", 0))
            )
            phase_counts[phase] += 1
            graph_counts[graph] += 1
            total_records += 1
    if total_records == 0:
        raise ValueError(f"empty opprof stream: {streams[0]}")
    summarized_groups = []
    for (phase, graph), metrics in sorted(groups.items()):
        summarized_groups.append(
            {
                "phase": phase,
                "cudagraph_runtime_mode": graph,
                "steps": len(metrics["submit_to_complete_ms"]),
                **{name: stats(values) for name, values in metrics.items()},
            }
        )
    cell = path.name
    tp_text, mns_text = cell.split("_")
    return {
        "cell": cell,
        "tensor_parallel_size": int(tp_text.removeprefix("tp")),
        "max_num_seqs": int(mns_text.removeprefix("mns")),
        "stream": str(streams[0]),
        "records": total_records,
        "phase_counts": dict(phase_counts),
        "cudagraph_runtime_mode_counts": dict(graph_counts),
        "prefix": {
            "queries": prefix_queries,
            "hits": prefix_hits,
            "hit_rate": prefix_hits / prefix_queries if prefix_queries else 0.0,
        },
        "dropped_records_before_max": dropped_records_max,
        "groups": summarized_groups,
    }


def main() -> None:
    args = parse_args()
    cells = [summarize_cell(path) for path in sorted(args.cell_root.glob("tp*_mns*"))]
    if not cells:
        raise SystemExit(f"no cells found in {args.cell_root}")
    payload = {
        "schema_version": "qwen30_vllm020_opprof_summary.v1",
        "measurement_semantics": (
            "complete_mono_ns - submit_mono_ns from the existing vLLM opprof record; "
            "this includes the instrumented submit-to-completion interval and is not "
            "claimed to be a single-kernel CUDA-event duration"
        ),
        "contains_prompt_text": False,
        "cell_root": str(args.cell_root),
        "cells": cells,
    }
    args.output.parent.mkdir(parents=True, exist_ok=True)
    args.output.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
    print(json.dumps({"cells": len(cells), "records": sum(c["records"] for c in cells)}))


if __name__ == "__main__":
    main()
