#!/usr/bin/env -S uv run --script --quiet
# /// script
# requires-python = ">=3.12"
# dependencies = ["httpx", "rich"]
# ///
"""
benchmark search API permutations to find performance issues.

Usage:
    ./scripts/bench-search              # run with defaults
    ./scripts/bench-search --runs 5     # more runs per permutation
    ./scripts/bench-search --local      # test local server
"""

import asyncio
import statistics
import sys
import time
from dataclasses import dataclass

import httpx
from rich.console import Console

BASE_URL = "https://leaflet-search-backend.fly.dev"

QUERIES = ["python", "atproto", "rust", "blog", ""]
TAGS = ["atproto", "bluesky", "rust", "Webworld", ""]
PLATFORMS = ["leaflet", "pckt", ""]
LIMITS = [10, 40, ""]


@dataclass
class Result:
    name: str
    params: dict
    times: list[float]
    count: int
    status: int

    @property
    def avg(self) -> float:
        return statistics.mean(self.times) * 1000

    @property
    def min(self) -> float:
        return min(self.times) * 1000

    @property
    def max(self) -> float:
        return max(self.times) * 1000

    @property
    def p50(self) -> float:
        return statistics.median(self.times) * 1000

    @property
    def stdev(self) -> float:
        return statistics.stdev(self.times) * 1000 if len(self.times) > 1 else 0


async def bench_search(
    client: httpx.AsyncClient, params: dict, runs: int
) -> Result:
    """benchmark a single search permutation."""
    times = []
    count = 0
    status = 0

    # filter empty params
    clean_params = {k: v for k, v in params.items() if v}

    name = " + ".join(f"{k}={v}" for k, v in clean_params.items()) or "(empty)"

    for _ in range(runs):
        start = time.perf_counter()
        try:
            resp = await client.get("/search", params=clean_params)
            elapsed = time.perf_counter() - start
            times.append(elapsed)
            status = resp.status_code
            if resp.status_code == 200:
                count = len(resp.json())
        except Exception as e:
            times.append(30.0)  # timeout
            status = 0
        # small delay between runs to avoid overwhelming server
        await asyncio.sleep(0.1)

    return Result(name=name, params=clean_params, times=times, count=count, status=status)


async def run_benchmarks(runs: int, console: Console) -> list[Result]:
    """run all search permutations."""
    results = []

    # build permutations - focus on meaningful combinations
    permutations = []

    # query only
    for q in QUERIES:
        if q:
            permutations.append({"q": q})

    # tag only
    for tag in TAGS:
        if tag:
            permutations.append({"tag": tag})

    # query + tag
    for q in ["python", "blog"]:
        for tag in ["atproto", "rust"]:
            permutations.append({"q": q, "tag": tag})

    # platform filter
    for platform in PLATFORMS:
        if platform:
            permutations.append({"q": "blog", "platform": platform})

    # limit variations
    for limit in LIMITS:
        if limit:
            permutations.append({"q": "python", "limit": limit})

    # tag + platform
    for tag in ["atproto", "bluesky"]:
        permutations.append({"tag": tag, "platform": "leaflet"})

    # empty (should return recent)
    permutations.append({})

    console.print(f"[dim]running {len(permutations)} permutations × {runs} runs each...[/dim]\n")

    async with httpx.AsyncClient(base_url=BASE_URL, timeout=30) as client:
        # warmup
        await client.get("/health")

        for i, params in enumerate(permutations):
            result = await bench_search(client, params, runs)
            results.append(result)
            # progress dot
            console.print(".", end="", style="dim")
            if (i + 1) % 20 == 0:
                console.print()

    console.print("\n")
    return results


def print_results(results: list[Result], console: Console):
    """print results as plain text."""
    # sort by p50 descending to show slowest first
    results.sort(key=lambda r: r.p50, reverse=True)

    console.print("results (sorted by p50, slowest first):\n")
    console.print(f"{'permutation':<40} {'p50':>8} {'avg':>8} {'min':>8} {'max':>8} {'count':>6}")
    console.print("-" * 80)

    for r in results:
        p50_str = f"{r.p50:.0f}ms"
        if r.p50 > 1000:
            p50_str = f"[red bold]{p50_str}[/red bold]"
        elif r.p50 > 500:
            p50_str = f"[yellow]{p50_str}[/yellow]"
        elif r.p50 < 200:
            p50_str = f"[green]{p50_str}[/green]"

        console.print(f"{r.name:<40} {p50_str:>8} {r.avg:>7.0f}ms {r.min:>7.0f}ms {r.max:>7.0f}ms {r.count:>6}")

    # summary
    console.print()
    slow = [r for r in results if r.p50 > 500]
    if slow:
        console.print(f"[yellow]⚠ {len(slow)} slow (p50 > 500ms)[/yellow]")
    else:
        console.print("[green]✓ all under 500ms p50[/green]")


async def main():
    global BASE_URL

    runs = 3
    if "--runs" in sys.argv:
        idx = sys.argv.index("--runs")
        if idx + 1 < len(sys.argv):
            runs = int(sys.argv[idx + 1])

    if "--local" in sys.argv:
        BASE_URL = "http://localhost:3000"

    console = Console()
    console.print(f"[bold]benchmarking {BASE_URL}[/bold]\n")

    results = await run_benchmarks(runs, console)
    print_results(results, console)


if __name__ == "__main__":
    asyncio.run(main())
