"""
RBSP Performance Monitoring.

Provides timing, metrics collection, histogram percentiles, slow-query
logging, and human-readable performance reports.  All classes are
thread-safe and use only standard-library modules.
"""

from __future__ import annotations

import bisect
import threading
import time
from collections import defaultdict
from typing import Any, Dict, List, Optional


# ---------------------------------------------------------------------------
# Timer
# ---------------------------------------------------------------------------

class Timer:
    """Context manager that records wall-clock elapsed time.

    Usage::

        with Timer("index") as t:
            do_work()
        print(t.elapsed_ms)
    """

    def __init__(self, name: str = "") -> None:
        self.name = name
        self._start: float = 0.0
        self._end: float = 0.0

    # -- properties ----------------------------------------------------------

    @property
    def elapsed(self) -> float:
        """Elapsed seconds (updates live while inside the context)."""
        if self._end > 0.0:
            return self._end - self._start
        if self._start > 0.0:
            return time.monotonic() - self._start
        return 0.0

    @property
    def elapsed_ms(self) -> float:
        """Elapsed milliseconds."""
        return self.elapsed * 1000.0

    # -- context manager -----------------------------------------------------

    def __enter__(self) -> "Timer":
        self._start = time.monotonic()
        self._end = 0.0
        return self

    def __exit__(self, *_: Any) -> None:
        self._end = time.monotonic()


# ---------------------------------------------------------------------------
# Histogram helper
# ---------------------------------------------------------------------------

class _Histogram:
    """Bounded sorted list with cheap percentile queries.

    Keeps at most *max_samples* values in sorted order using bisect.
    When full, the oldest value is evicted (FIFO via a ring buffer).
    """

    __slots__ = ("_values", "_ring", "_max", "_pos", "_count",
                 "_min", "_max_val", "_sum")

    def __init__(self, max_samples: int = 10_000) -> None:
        self._values: List[float] = []
        self._ring: List[float] = []
        self._max = max_samples
        self._pos = 0
        self._count = 0
        self._min = float("inf")
        self._max_val = float("-inf")
        self._sum = 0.0

    def record(self, value: float) -> None:
        self._count += 1
        self._sum += value
        if value < self._min:
            self._min = value
        if value > self._max_val:
            self._max_val = value

        if len(self._ring) < self._max:
            # Still filling up
            self._ring.append(value)
            bisect.insort(self._values, value)
        else:
            # Evict oldest
            old = self._ring[self._pos]
            idx = bisect.bisect_left(self._values, old)
            # Find exact match (there could be duplicates with same value)
            while idx < len(self._values) and self._values[idx] != old:
                idx += 1
            if idx < len(self._values):
                self._values.pop(idx)
            self._ring[self._pos] = value
            bisect.insort(self._values, value)
        self._pos = (self._pos + 1) % self._max

    def snapshot(self) -> Dict[str, Any]:
        n = len(self._values)
        if n == 0:
            return {"min": 0.0, "max": 0.0, "avg": 0.0,
                    "p50": 0.0, "p95": 0.0, "p99": 0.0, "count": 0}
        return {
            "min": self._min,
            "max": self._max_val,
            "avg": self._sum / self._count,
            "p50": self._percentile(50),
            "p95": self._percentile(95),
            "p99": self._percentile(99),
            "count": self._count,
        }

    def _percentile(self, p: int) -> float:
        n = len(self._values)
        if n == 0:
            return 0.0
        k = (p / 100.0) * (n - 1)
        lo = int(k)
        hi = min(lo + 1, n - 1)
        frac = k - lo
        return self._values[lo] + frac * (self._values[hi] - self._values[lo])


# ---------------------------------------------------------------------------
# Metrics
# ---------------------------------------------------------------------------

class Metrics:
    """Thread-safe metrics collector.

    Supports counters, gauges, and histograms.  Safe to instantiate
    multiple times, but a convenience :meth:`get_instance` is provided
    for singleton-style usage.
    """

    _instance: Optional["Metrics"] = None
    _instance_lock = threading.Lock()

    def __init__(self) -> None:
        self._lock = threading.Lock()
        self._counters: Dict[str, int] = defaultdict(int)
        self._gauges: Dict[str, float] = {}
        self._histograms: Dict[str, _Histogram] = {}

    @classmethod
    def get_instance(cls) -> "Metrics":
        """Return a global singleton Metrics instance."""
        if cls._instance is None:
            with cls._instance_lock:
                if cls._instance is None:
                    cls._instance = cls()
        return cls._instance

    @classmethod
    def reset_instance(cls) -> None:
        """Discard the singleton (mainly for testing)."""
        with cls._instance_lock:
            cls._instance = None

    # -- counters ------------------------------------------------------------

    def counter(self, name: str, value: int = 1) -> None:
        """Increment a named counter by *value*."""
        with self._lock:
            self._counters[name] += value

    def get_counter(self, name: str) -> int:
        """Return the current value of a counter (0 if never set)."""
        with self._lock:
            return self._counters.get(name, 0)

    # -- gauges --------------------------------------------------------------

    def gauge(self, name: str, value: float) -> None:
        """Set a gauge to an absolute *value*."""
        with self._lock:
            self._gauges[name] = value

    def get_gauge(self, name: str) -> float:
        """Return the current gauge value (0.0 if never set)."""
        with self._lock:
            return self._gauges.get(name, 0.0)

    # -- histograms ----------------------------------------------------------

    def histogram(self, name: str, value: float) -> None:
        """Record a single observation in a named histogram."""
        with self._lock:
            h = self._histograms.get(name)
            if h is None:
                h = _Histogram()
                self._histograms[name] = h
            h.record(value)

    def get_histogram(self, name: str) -> Dict[str, Any]:
        """Return histogram statistics: min, max, avg, p50, p95, p99, count."""
        with self._lock:
            h = self._histograms.get(name)
            if h is None:
                return {"min": 0.0, "max": 0.0, "avg": 0.0,
                        "p50": 0.0, "p95": 0.0, "p99": 0.0, "count": 0}
            return h.snapshot()

    # -- timer shortcut ------------------------------------------------------

    def timer(self, name: str) -> "_MetricsTimer":
        """Return a :class:`Timer`-like context manager that auto-records
        the elapsed milliseconds into the histogram *name*."""
        return _MetricsTimer(name, self)

    # -- aggregation ---------------------------------------------------------

    def snapshot(self) -> Dict[str, Any]:
        """Return all metrics as a plain dictionary."""
        with self._lock:
            return {
                "counters": dict(self._counters),
                "gauges": dict(self._gauges),
                "histograms": {
                    name: h.snapshot() for name, h in self._histograms.items()
                },
            }

    def reset(self) -> None:
        """Clear all collected metrics."""
        with self._lock:
            self._counters.clear()
            self._gauges.clear()
            self._histograms.clear()


class _MetricsTimer(Timer):
    """Timer subclass that records into a :class:`Metrics` histogram on exit."""

    def __init__(self, name: str, metrics: Metrics) -> None:
        super().__init__(name)
        self._metrics = metrics

    def __exit__(self, *exc: Any) -> None:
        super().__exit__(*exc)
        self._metrics.histogram(self.name, self.elapsed_ms)


# ---------------------------------------------------------------------------
# SlowQueryLog
# ---------------------------------------------------------------------------

class SlowQueryLog:
    """Thread-safe log of queries whose duration exceeds a threshold.

    Parameters
    ----------
    threshold_ms:
        Only queries slower than this value (in milliseconds) are recorded.
    max_entries:
        Maximum number of entries kept.  Oldest entries are evicted first.
    """

    def __init__(
        self,
        threshold_ms: float = 100.0,
        max_entries: int = 1000,
    ) -> None:
        self._threshold_ms = threshold_ms
        self._max_entries = max_entries
        self._lock = threading.Lock()
        self._entries: List[Dict[str, Any]] = []

    def log(self, query: str, duration_ms: float, results_count: int) -> None:
        """Record a slow query if *duration_ms* >= threshold."""
        if duration_ms < self._threshold_ms:
            return
        entry = {
            "query": query,
            "duration_ms": duration_ms,
            "results_count": results_count,
            "timestamp": time.time(),
        }
        with self._lock:
            self._entries.append(entry)
            if len(self._entries) > self._max_entries:
                self._entries = self._entries[-self._max_entries:]

    def get_slow_queries(self) -> List[Dict[str, Any]]:
        """Return a copy of all logged slow queries."""
        with self._lock:
            return list(self._entries)

    def clear(self) -> None:
        """Remove all logged entries."""
        with self._lock:
            self._entries.clear()


# ---------------------------------------------------------------------------
# PerformanceReport
# ---------------------------------------------------------------------------

class PerformanceReport:
    """Generates a human-readable performance report from :class:`Metrics`."""

    @staticmethod
    def generate(metrics: Metrics) -> str:
        """Return a multi-line report string summarizing all metrics."""
        snap = metrics.snapshot()
        lines: List[str] = []

        lines.append("=== RBSP Performance Report ===")
        lines.append("")

        # Counters
        counters = snap.get("counters", {})
        if counters:
            lines.append("Counters:")
            for name in sorted(counters):
                lines.append(f"  {name}: {counters[name]}")
            lines.append("")

        # Gauges
        gauges = snap.get("gauges", {})
        if gauges:
            lines.append("Gauges:")
            for name in sorted(gauges):
                lines.append(f"  {name}: {gauges[name]:.4f}")
            lines.append("")

        # Histograms
        histograms = snap.get("histograms", {})
        if histograms:
            lines.append("Histograms:")
            for name in sorted(histograms):
                h = histograms[name]
                lines.append(f"  {name}:")
                lines.append(f"    count: {h['count']}")
                lines.append(f"    min:   {h['min']:.4f}")
                lines.append(f"    max:   {h['max']:.4f}")
                lines.append(f"    avg:   {h['avg']:.4f}")
                lines.append(f"    p50:   {h['p50']:.4f}")
                lines.append(f"    p95:   {h['p95']:.4f}")
                lines.append(f"    p99:   {h['p99']:.4f}")
            lines.append("")

        if not counters and not gauges and not histograms:
            lines.append("No metrics collected.")
            lines.append("")

        lines.append("=== End Report ===")
        return "\n".join(lines)
