"""Exp (a): three-tier hit-latency microbench.

Measures TTFT of serving a prefix of length L from each tier:
  - miss : fresh unique prompt -> full prefill (recompute)
  - gpu  : re-request same prompt -> HBM prefix-cache hit
  - cpu  : warm -> evict to CPU offload tier -> re-request -> DRAM hit

Each measured request is bracketed by /metrics scrapes so the tier is *verified*
(gpu_hits delta vs external_prefix_cache_hits delta), not assumed.
"""
from __future__ import annotations

import argparse
import json
import statistics
import sys
import time
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from common.util import make_token_prompt, scrape_prefix_cache, measure_ttft  # noqa: E402

LENGTHS = [1024, 2048, 4096, 8192, 16384, 32768, 65536]


def delta(a: dict, b: dict) -> dict:
    return {k: b[k] - a[k] for k in a}


def one_measurement(ep, model, prompt, expect):
    m0 = scrape_prefix_cache(ep)
    res = measure_ttft(ep, model, prompt)
    m1 = scrape_prefix_cache(ep)
    d = delta(m0, m1)
    cached = (res.get("usage") or {}).get("prompt_tokens", None)
    # classify
    if d["ext_hits"] > 0.5:
        tier = "cpu"
    elif d["gpu_hits"] > 0.5:
        tier = "gpu"
    else:
        tier = "miss"
    return {"ttft_s": res["ttft_s"], "e2e_s": res["e2e_s"],
            "tier_observed": tier, "expect": expect,
            "d_gpu_hits": d["gpu_hits"], "d_ext_hits": d["ext_hits"]}


def run_miss(ep, model, L, reps, base):
    rows = []
    for i in range(reps):
        p = make_token_prompt(L, seed=base + i)  # fresh each time
        rows.append(one_measurement(ep, model, p, "miss"))
    return rows


def run_gpu(ep, model, L, reps, base):
    rows = []
    for i in range(reps):
        p = make_token_prompt(L, seed=base + i)
        measure_ttft(ep, model, p)          # warm
        rows.append(one_measurement(ep, model, p, "gpu"))  # hit
    return rows


def run_cpu(ep, model, L, reps, base, flood_tokens, flood_chunk):
    rows = []
    for i in range(reps):
        p = make_token_prompt(L, seed=base + i)
        measure_ttft(ep, model, p)          # warm -> GPU (+offload)
        # flood with distinct content to evict p from the GPU pool to CPU tier
        sent = 0
        fseed = 10_000_000 + (base + i) * 1000
        while sent < flood_tokens:
            fp = make_token_prompt(flood_chunk, seed=fseed)
            measure_ttft(ep, model, fp)
            fseed += 1
            sent += flood_chunk
        rows.append(one_measurement(ep, model, p, "cpu"))  # should hit CPU tier
    return rows


def summarize(rows):
    t = sorted(r["ttft_s"] for r in rows)
    return {
        "n": len(rows),
        "ttft_p50": statistics.median(t) if t else None,
        "ttft_mean": statistics.fmean(t) if t else None,
        "ttft_min": t[0] if t else None,
        "ttft_max": t[-1] if t else None,
        "tier_observed": _modal([r["tier_observed"] for r in rows]),
        "verified_frac": sum(r["tier_observed"] == r["expect"] for r in rows) / len(rows) if rows else 0,
    }


def _modal(xs):
    from collections import Counter
    return Counter(xs).most_common(1)[0][0] if xs else None


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--endpoint", required=True)
    ap.add_argument("--model", required=True)
    ap.add_argument("--mode", required=True, choices=["miss", "gpu", "cpu"])
    ap.add_argument("--reps", type=int, default=8)
    ap.add_argument("--out", required=True)
    ap.add_argument("--lengths", type=str, default=None,
                    help="comma list override, e.g. 1024,4096")
    ap.add_argument("--flood-tokens", type=int, default=120000,
                    help="cpu mode: distinct tokens to flush GPU pool")
    ap.add_argument("--flood-chunk", type=int, default=8192)
    args = ap.parse_args()

    lengths = ([int(x) for x in args.lengths.split(",")] if args.lengths else LENGTHS)
    out = {"mode": args.mode, "reps": args.reps, "by_length": {}, "raw": {}}
    base = {"miss": 1_000, "gpu": 2_000, "cpu": 3_000}[args.mode]

    for L in lengths:
        t0 = time.time()
        if args.mode == "miss":
            rows = run_miss(args.endpoint, args.model, L, args.reps, base)
        elif args.mode == "gpu":
            rows = run_gpu(args.endpoint, args.model, L, args.reps, base)
        else:
            rows = run_cpu(args.endpoint, args.model, L, args.reps, base,
                           args.flood_tokens, args.flood_chunk)
        base += 100_000
        s = summarize(rows)
        out["by_length"][str(L)] = s
        out["raw"][str(L)] = rows
        print(f"[{args.mode}] L={L:>6}  ttft_p50={s['ttft_p50']:.4f}s  "
              f"tier={s['tier_observed']}  verified={s['verified_frac']:.0%}  "
              f"({time.time()-t0:.0f}s)", flush=True)
        Path(args.out).write_text(json.dumps(out, indent=2))

    print(f"wrote {args.out}")


if __name__ == "__main__":
    main()
