"""Relative-baseline perf harness + fixtures (PF-01/03/04/05/07/08).

The federation forbids absolute perf budgets (PF-01, PAPER-50): a budget is a
**regression threshold against a recorded baseline**, tier-tagged, with a
numeric failure message (PF-07). This conftest is mgf-common's own
implementation, exposed to ``tests/perf/test_*.py`` as two fixtures:

  - ``bench``      — ``bench.measure(fn, iterations=, warmup=)`` → median
    per-call **seconds** (median drops GC/scheduling outliers).
  - ``baselines``  — ``baselines.assert_no_regression(metric, measured, …)``.

Normal run (``pytest -m perf``) compares measurements to
``tests/perf/baselines.json`` and fails with the PF-07 numeric message on a
regression (or a missing baseline — PF-03). A re-baseline run
(``MGF_PERF_REBASELINE=1 pytest -m perf`` or ``mgf-common bench rebaseline``)
RECORDS the measured values instead and passes — a deliberate, human-run,
separate commit (PF-04). The baseline file is generated this way, never
hand-edited.

`tests/perf/` is excluded from the default gate (`-m 'not perf'`) and runs on a
tagged hardware tier — see ``hw_tiers.md``.
"""

from __future__ import annotations

import contextlib
import json
import os
import tempfile
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING

import pytest

if TYPE_CHECKING:
    from collections.abc import Callable, Iterator

BASELINES_PATH = Path(__file__).resolve().parent / "baselines.json"
SCHEMA_VERSION = 1
# Reference hardware tier for these baselines (operator discipline, PF-08 —
# not auto-detected). See hw_tiers.md.
HW_TIER = "dev-local-2026"
_DEFAULT_THRESHOLD_PCT = 150


def _rebaselining() -> bool:
    return os.environ.get("MGF_PERF_REBASELINE", "") not in ("", "0", "false")


def _today_iso() -> str:
    import datetime

    return datetime.datetime.now(tz=datetime.UTC).date().isoformat()


def _git_sha() -> str:
    with contextlib.suppress(Exception):
        import subprocess

        root = Path(__file__).resolve().parents[2]
        out = subprocess.run(
            ["git", "-C", str(root), "rev-parse", "--short", "HEAD"],
            capture_output=True,
            text=True,
            timeout=2,
            check=False,
        )
        if out.returncode == 0 and out.stdout.strip():
            return out.stdout.strip()
    return "unknown"


@dataclass
class Baselines:
    """Loaded ``baselines.json`` + the assert/record operations (PF-01/07)."""

    path: Path
    data: dict
    _dirty: bool = field(default=False)

    @classmethod
    def load(cls, path: Path = BASELINES_PATH) -> Baselines:
        if path.is_file():
            data = json.loads(path.read_text(encoding="utf-8"))
        else:
            data = {
                "$schema_version": SCHEMA_VERSION,
                "$updated_at": "",
                "$hw_tier": HW_TIER,
                "metrics": {},
            }
        return cls(path=path, data=data)

    @property
    def hw_tier(self) -> str:
        return str(self.data.get("$hw_tier", HW_TIER))

    def assert_no_regression(
        self,
        metric: str,
        measured: float,
        *,
        units: str,
        description: str,
        threshold_pct: int = _DEFAULT_THRESHOLD_PCT,
    ) -> None:
        metrics = self.data.setdefault("metrics", {})

        if _rebaselining():
            existing = metrics.get(metric, {})
            metrics[metric] = {
                "value": round(measured, 3),
                "units": units,
                "recorded_at": _today_iso(),
                "recorded_commit": _git_sha(),
                "regression_threshold_pct": existing.get("regression_threshold_pct", threshold_pct),
                "description": description,
            }
            self._dirty = True
            return

        entry = metrics.get(metric)
        if entry is None:
            raise AssertionError(
                f"{metric}: no baseline recorded (PF-03). Record one with\n"
                f"  mgf-common bench rebaseline {metric}\n"
                f"(or `MGF_PERF_REBASELINE=1 pytest -m perf`), then commit "
                f"{self.path.name} — a PF-04 re-baseline PR."
            )
        baseline = float(entry["value"])
        thr = float(entry.get("regression_threshold_pct", threshold_pct))
        limit = baseline * (1.0 + thr / 100.0)
        if measured > limit:
            pct = (measured / baseline - 1.0) * 100.0 if baseline else float("inf")
            raise AssertionError(
                f"{metric} regressed by +{pct:.1f}%\n"
                f"  baseline:  {baseline:g} {entry['units']}   "
                f"(recorded {entry['recorded_at']}, commit "
                f"{entry['recorded_commit']}, hw_tier={self.hw_tier})\n"
                f"  measured:  {measured:g} {units}\n"
                f"  threshold: +{thr:g}%\n"
                f"  description: {entry['description']}\n\n"
                f"If the regression is real, find and fix it.\n"
                f"If the baseline is genuinely outdated, re-baseline with:\n"
                f"  mgf-common bench rebaseline {metric}\n"
                f"A re-baselining PR MUST explain WHY the baseline moved (PF-04)."
            )

    def save(self) -> None:
        if not self._dirty:
            return
        self.data["$schema_version"] = SCHEMA_VERSION
        self.data["$updated_at"] = _today_iso() + "T00:00:00+00:00"
        self.data.setdefault("$hw_tier", HW_TIER)
        self.data["metrics"] = dict(sorted(self.data["metrics"].items()))
        fd, tmp = tempfile.mkstemp(dir=str(self.path.parent), suffix=".json.tmp")
        with os.fdopen(fd, "w", encoding="utf-8") as f:
            json.dump(self.data, f, indent=2)
            f.write("\n")
        Path(tmp).replace(self.path)
        self._dirty = False


class _Bench:
    """The `bench` fixture: a measurement helper enforcing warm-up + median."""

    @staticmethod
    def measure(fn: Callable[[], object], *, iterations: int, warmup: int = 100) -> float:
        """Median per-call seconds over ``iterations`` reps after ``warmup``."""
        for _ in range(warmup):
            fn()
        samples = []
        for _ in range(iterations):
            t0 = time.perf_counter()
            fn()
            samples.append(time.perf_counter() - t0)
        samples.sort()
        return samples[len(samples) // 2]


@pytest.fixture(scope="session")
def baselines() -> Iterator[Baselines]:
    b = Baselines.load()
    yield b
    b.save()  # no-op unless a re-baseline run recorded new values


@pytest.fixture(scope="session")
def bench() -> _Bench:
    return _Bench()
