"""Base scenario class for benchmarks."""

from abc import ABC, abstractmethod
from dataclasses import dataclass, field, asdict
from typing import Any
import time


@dataclass
class BenchmarkResult:
    """Result of a single benchmark run."""
    scenario: str
    tool: str
    runs: int = 10
    mean_ms: float = 0.0
    p50_ms: float = 0.0
    p95_ms: float = 0.0
    p99_ms: float = 0.0
    min_ms: float = 0.0
    max_ms: float = 0.0
    success_rate: float = 1.0
    errors: list[str] = field(default_factory=list)
    metadata: dict[str, Any] = field(default_factory=dict)

    def to_dict(self) -> dict:
        return asdict(self)


class BaseScenario(ABC):
    """Abstract base class for benchmark scenarios."""

    name: str = "base"
    description: str = ""
    runs: int = 10

    @abstractmethod
    def setup(self) -> None:
        """Prepare the browser/environment. Called once before warmup."""
        pass

    @abstractmethod
    def run_browser_hybrid(self) -> tuple[bool, float]:
        """Run benchmark with browser-hybrid. Returns (success, elapsed_ms)."""
        pass

    @abstractmethod
    def run_playwright(self) -> tuple[bool, float]:
        """Run benchmark with Playwright. Returns (success, elapsed_ms)."""
        pass

    @abstractmethod
    def run_selenium(self) -> tuple[bool, float]:
        """Run benchmark with Selenium. Returns (success, elapsed_ms)."""
        pass

    @abstractmethod
    def run_agent_browser(self) -> tuple[bool, float]:
        """Run benchmark with agent-browser. Returns (success, elapsed_ms)."""
        pass

    def warmup(self) -> None:
        """Optional warmup run. Default no-op."""
        pass

    def teardown(self) -> None:
        """Optional cleanup. Default no-op."""
        pass

    def _collect_timings(self, timings: list[float], errors: list[str]) -> dict:
        """Compute statistics from a list of timings."""
        if not timings:
            return {"mean_ms": 0, "p50_ms": 0, "p95_ms": 0, "p99_ms": 0, "min_ms": 0, "max_ms": 0}
        sorted_timings = sorted(timings)
        n = len(sorted_timings)
        return {
            "mean_ms": round(sum(timings) / n, 2),
            "p50_ms": round(sorted_timings[n // 2], 2),
            "p95_ms": round(sorted_timings[int(n * 0.95)], 2),
            "p99_ms": round(sorted_timings[min(int(n * 0.99), n - 1)], 2),
            "min_ms": round(min(timings), 2),
            "max_ms": round(max(timings), 2),
        }

    def _timeit(self, fn) -> tuple[bool, float]:
        """Time a function. Returns (success, elapsed_ms)."""
        start = time.perf_counter()
        try:
            result = fn()
            elapsed = (time.perf_counter() - start) * 1000
            # Some operations return False on failure
            if result is False:
                return False, elapsed
            return True, elapsed
        except Exception as e:
            elapsed = (time.perf_counter() - start) * 1000
            return False, elapsed

    def run_all(self) -> dict[str, BenchmarkResult]:
        """Run the scenario for all tools. Returns dict of tool -> result."""
        results = {}

        for tool, run_fn in [
            ("browser-hybrid", self.run_browser_hybrid),
            ("playwright", self.run_playwright),
            ("selenium", self.run_selenium),
            ("agent-browser", self.run_agent_browser),
        ]:
            timings = []
            errors = []
            for i in range(self.runs):
                success, elapsed = self._timeit(run_fn)
                timings.append(elapsed)
                if not success:
                    errors.append(f"run {i} failed")

            stats = self._collect_timings(timings, errors)
            results[tool] = BenchmarkResult(
                scenario=self.name,
                tool=tool,
                runs=self.runs,
                success_rate=round((self.runs - len(errors)) / self.runs, 2),
                errors=errors,
                **stats,
            )

        return results
