"""Concurrency benchmark + old-vs-new comparison for the OCR service.

Two subcommands:

  sweep    Drive one running service across a list of concurrency levels and
           record latency/throughput/success per level. Reuses the request
           harness in concurrency_test.py.

  compare  Load two sweep result files (old + new) and emit a markdown table,
           a CSV, and comparison plots (latency & throughput vs concurrency,
           plus a per-level speedup bar).

Typical flow (see scripts/run_concurrency_comparison.sh):

  # against the new vLLM service (its optimized endpoint)
  python concurrency_compare.py sweep --label new \
      --url http://127.0.0.1:5002/v1/api/ai/suya_ocr_vllm/ \
      --image temp_image_c3c56948-282e-453b-8fac-6c482243d1e5.jpg \
      --levels 1,2,4,8,16 --out results/compare/new.json

  # ... swap containers, then against the old service (its OCR endpoint)
  python concurrency_compare.py sweep --label old \
      --url http://127.0.0.1:5002/v1/api/ai/suya_ocr/ \
      --image temp_image_c3c56948-282e-453b-8fac-6c482243d1e5.jpg \
      --levels 1,2,4,8,16 --out results/compare/old.json

  python concurrency_compare.py compare \
      --old results/compare/old.json --new results/compare/new.json \
      --out-dir results/compare
"""
from __future__ import annotations

import argparse
import csv
import json
import time
from pathlib import Path
from typing import Any, Dict, List

from concurrency_test import _encode_image, run_endpoint


def _parse_levels(value: str) -> List[int]:
    return [int(x.strip()) for x in value.split(",") if x.strip()]


# ---------------------------------------------------------------- sweep -----

def run_sweep(args: argparse.Namespace) -> None:
    image_path = Path(args.image)
    image_type = image_path.suffix.lstrip(".").lower() or "png"
    payload = {
        "file": _encode_image(image_path),
        "type": "jpg" if image_type == "jpeg" else image_type,
        "skip_text_detection": False,
        "skip_table_detection": False,
        "recognize_math": False,
        "ocr_with_boxes": True,
    }

    levels = _parse_levels(args.levels)
    rows: List[Dict[str, Any]] = []
    for concurrency in levels:
        requests_count = max(concurrency, concurrency * args.reqs_per_level)
        print(f"[{args.label}] concurrency={concurrency} requests={requests_count}", flush=True)
        row = run_endpoint(
            args.label, args.url, payload,
            requests_count=requests_count, concurrency=concurrency, timeout=args.timeout,
        )
        rows.append(row)
        lat = row["latency_seconds"]
        print(f"  -> success={row['success']}/{row['requests']} "
              f"rps={row['throughput_rps']:.3f} mean={lat['mean']:.2f}s p95={lat['p95']:.2f}s",
              flush=True)

    out = {
        "label": args.label,
        "url": args.url,
        "image": str(image_path),
        "levels": levels,
        "reqs_per_level": args.reqs_per_level,
        "started_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
        "results": rows,
    }
    out_path = Path(args.out)
    out_path.parent.mkdir(parents=True, exist_ok=True)
    out_path.write_text(json.dumps(out, indent=2), encoding="utf-8")
    print(f"wrote {out_path}")


# -------------------------------------------------------------- compare -----

def _index_by_concurrency(sweep: Dict[str, Any]) -> Dict[int, Dict[str, Any]]:
    return {row["concurrency"]: row for row in sweep["results"]}


def _comparison_rows(old: Dict[str, Any], new: Dict[str, Any]) -> List[Dict[str, Any]]:
    old_by_c = _index_by_concurrency(old)
    new_by_c = _index_by_concurrency(new)
    concurrencies = sorted(set(old_by_c) | set(new_by_c))
    rows = []
    for c in concurrencies:
        o = old_by_c.get(c)
        n = new_by_c.get(c)
        row: Dict[str, Any] = {"concurrency": c}
        row["old_mean_s"] = round(o["latency_seconds"]["mean"], 2) if o else None
        row["new_mean_s"] = round(n["latency_seconds"]["mean"], 2) if n else None
        row["old_p95_s"] = round(o["latency_seconds"]["p95"], 2) if o else None
        row["new_p95_s"] = round(n["latency_seconds"]["p95"], 2) if n else None
        row["old_rps"] = round(o["throughput_rps"], 3) if o else None
        row["new_rps"] = round(n["throughput_rps"], 3) if n else None
        row["old_fail"] = o["failed"] if o else None
        row["new_fail"] = n["failed"] if n else None
        if o and n and o["latency_seconds"]["mean"] and n["latency_seconds"]["mean"]:
            row["latency_speedup"] = round(o["latency_seconds"]["mean"] / n["latency_seconds"]["mean"], 2)
        else:
            row["latency_speedup"] = None
        if o and n and o["throughput_rps"]:
            row["throughput_gain"] = round(n["throughput_rps"] / o["throughput_rps"], 2)
        else:
            row["throughput_gain"] = None
        rows.append(row)
    return rows


def _write_table(rows: List[Dict[str, Any]], old_label: str, new_label: str, path: Path) -> str:
    cols = [
        ("concurrency", "conc"),
        ("old_mean_s", f"{old_label} mean(s)"),
        ("new_mean_s", f"{new_label} mean(s)"),
        ("latency_speedup", "latency ×"),
        ("old_p95_s", f"{old_label} p95(s)"),
        ("new_p95_s", f"{new_label} p95(s)"),
        ("old_rps", f"{old_label} rps"),
        ("new_rps", f"{new_label} rps"),
        ("throughput_gain", "rps ×"),
        ("old_fail", f"{old_label} fail"),
        ("new_fail", f"{new_label} fail"),
    ]
    header = "| " + " | ".join(label for _, label in cols) + " |"
    sep = "| " + " | ".join("---" for _ in cols) + " |"
    lines = [header, sep]
    for r in rows:
        cells = []
        for key, _ in cols:
            v = r.get(key)
            cells.append("" if v is None else str(v))
        lines.append("| " + " | ".join(cells) + " |")
    md = "\n".join(lines) + "\n"
    path.write_text(md, encoding="utf-8")
    return md


def _write_csv(rows: List[Dict[str, Any]], path: Path) -> None:
    if not rows:
        return
    with path.open("w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
        writer.writeheader()
        writer.writerows(rows)


def _write_plots(rows, old_label, new_label, out_dir: Path) -> List[Path]:
    import matplotlib
    matplotlib.use("Agg")
    import matplotlib.pyplot as plt

    x = [r["concurrency"] for r in rows]
    paths = []

    # 1. latency (mean + p95) vs concurrency
    fig, ax = plt.subplots(figsize=(9, 6))
    ax.plot(x, [r["old_mean_s"] for r in rows], marker="o", color="#c0392b", label=f"{old_label} mean")
    ax.plot(x, [r["old_p95_s"] for r in rows], marker="^", color="#c0392b", linestyle="--", label=f"{old_label} p95")
    ax.plot(x, [r["new_mean_s"] for r in rows], marker="o", color="#27ae60", label=f"{new_label} mean")
    ax.plot(x, [r["new_p95_s"] for r in rows], marker="^", color="#27ae60", linestyle="--", label=f"{new_label} p95")
    ax.set_xlabel("Concurrency (simultaneous requests)")
    ax.set_ylabel("Latency per request (s)")
    ax.set_title("OCR latency vs concurrency — old vs new")
    ax.grid(True, alpha=0.3)
    ax.legend()
    fig.tight_layout()
    p = out_dir / "latency_vs_concurrency.png"
    fig.savefig(p, dpi=160)
    plt.close(fig)
    paths.append(p)

    # 2. throughput vs concurrency
    fig, ax = plt.subplots(figsize=(9, 6))
    ax.plot(x, [r["old_rps"] for r in rows], marker="o", color="#c0392b", label=old_label)
    ax.plot(x, [r["new_rps"] for r in rows], marker="o", color="#27ae60", label=new_label)
    ax.set_xlabel("Concurrency (simultaneous requests)")
    ax.set_ylabel("Throughput (requests/s)")
    ax.set_title("OCR throughput vs concurrency — old vs new")
    ax.grid(True, alpha=0.3)
    ax.legend()
    fig.tight_layout()
    p = out_dir / "throughput_vs_concurrency.png"
    fig.savefig(p, dpi=160)
    plt.close(fig)
    paths.append(p)

    # 3. per-level latency speedup bar
    fig, ax = plt.subplots(figsize=(9, 6))
    speedups = [r["latency_speedup"] or 0 for r in rows]
    ax.bar([str(c) for c in x], speedups, color="#2980b9")
    ax.axhline(1.0, color="gray", linestyle="--", linewidth=1)
    for i, v in enumerate(speedups):
        ax.text(i, v, f"{v:.2f}×", ha="center", va="bottom")
    ax.set_xlabel("Concurrency")
    ax.set_ylabel(f"Latency speedup ({old_label} mean / {new_label} mean)")
    ax.set_title("Per-level latency speedup (>1 = new is faster)")
    ax.grid(True, axis="y", alpha=0.3)
    fig.tight_layout()
    p = out_dir / "latency_speedup.png"
    fig.savefig(p, dpi=160)
    plt.close(fig)
    paths.append(p)

    return paths


def run_compare(args: argparse.Namespace) -> None:
    old = json.loads(Path(args.old).read_text(encoding="utf-8"))
    new = json.loads(Path(args.new).read_text(encoding="utf-8"))
    old_label = old.get("label", "old")
    new_label = new.get("label", "new")
    out_dir = Path(args.out_dir)
    out_dir.mkdir(parents=True, exist_ok=True)

    rows = _comparison_rows(old, new)
    md = _write_table(rows, old_label, new_label, out_dir / "comparison_table.md")
    _write_csv(rows, out_dir / "comparison_table.csv")
    plots = _write_plots(rows, old_label, new_label, out_dir)

    print(md)
    print("plots:")
    for p in plots:
        print(f"  {p}")


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    sub = parser.add_subparsers(dest="cmd", required=True)

    sp = sub.add_parser("sweep", help="benchmark one running service across concurrency levels")
    sp.add_argument("--label", required=True, help="short name for this service, e.g. old / new")
    sp.add_argument("--url", required=True, help="full endpoint URL to POST to")
    sp.add_argument("--image", required=True, help="path to a png/jpg test image")
    sp.add_argument("--levels", default="1,2,4,8,16", help="comma-separated concurrency levels")
    sp.add_argument("--reqs-per-level", type=int, default=2, help="requests = level * this (>=level)")
    sp.add_argument("--timeout", type=float, default=900)
    sp.add_argument("--out", required=True, help="output JSON path")
    sp.set_defaults(func=run_sweep)

    cp = sub.add_parser("compare", help="compare two sweep result files")
    cp.add_argument("--old", required=True, help="old-service sweep JSON")
    cp.add_argument("--new", required=True, help="new-service sweep JSON")
    cp.add_argument("--out-dir", default="results/compare")
    cp.set_defaults(func=run_compare)

    args = parser.parse_args()
    args.func(args)


if __name__ == "__main__":
    main()
