"""In-memory TSDB ring buffer for otelctx metrics."""

from __future__ import annotations

import json
import logging
import math
import threading
import time
from collections import deque
from dataclasses import dataclass, field
from datetime import UTC, datetime, timedelta
from typing import Sequence

logger = logging.getLogger(__name__)


@dataclass
class DataPoint:
    timestamp: float  # Unix epoch seconds
    value: float
    labels: dict[str, str] = field(default_factory=dict)


class RingBuffer:
    """Fixed-capacity ring buffer for metric samples."""

    def __init__(self, capacity: int = 10_000) -> None:
        self._buf: deque[DataPoint] = deque(maxlen=capacity)
        self._lock = threading.RLock()
        self.capacity = capacity

    def push(self, point: DataPoint) -> None:
        with self._lock:
            self._buf.append(point)

    def query(
        self,
        start: float | None = None,
        end: float | None = None,
        labels: dict[str, str] | None = None,
    ) -> list[DataPoint]:
        now = time.time()
        _start = start if start is not None else 0.0
        _end = end if end is not None else now
        with self._lock:
            result = []
            for p in self._buf:
                if p.timestamp < _start or p.timestamp > _end:
                    continue
                if labels:
                    if not all(p.labels.get(k) == v for k, v in labels.items()):
                        continue
                result.append(p)
        return result

    def recent(self, window_seconds: float = 300.0) -> list[DataPoint]:
        return self.query(start=time.time() - window_seconds)

    def __len__(self) -> int:
        with self._lock:
            return len(self._buf)


def _percentile(values: Sequence[float], p: float) -> float:
    if not values:
        return 0.0
    sv = sorted(values)
    idx = int(math.ceil(p / 100.0 * len(sv))) - 1
    return sv[max(0, idx)]


class TsdbRingBuffer:
    """Multi-series TSDB backed by per-series RingBuffers."""

    def __init__(self, per_series_capacity: int = 10_000) -> None:
        self._cap = per_series_capacity
        self._lock = threading.RLock()
        self._series: dict[str, RingBuffer] = {}

    def _get_or_create(self, series_key: str) -> RingBuffer:
        if series_key not in self._series:
            self._series[series_key] = RingBuffer(capacity=self._cap)
        return self._series[series_key]

    def record(self, metric_name: str, value: float, labels: dict[str, str] | None = None, timestamp: float | None = None) -> None:
        ts = timestamp or time.time()
        point = DataPoint(timestamp=ts, value=value, labels=labels or {})
        with self._lock:
            buf = self._get_or_create(metric_name)
        buf.push(point)

    def query_series(
        self,
        metric_name: str,
        window_seconds: float = 300.0,
        labels: dict[str, str] | None = None,
    ) -> list[dict]:
        with self._lock:
            buf = self._series.get(metric_name)
        if not buf:
            return []
        points = buf.recent(window_seconds)
        if labels:
            points = [p for p in points if all(p.labels.get(k) == v for k, v in labels.items())]
        return [{"timestamp": p.timestamp, "value": p.value, "labels": p.labels} for p in points]

    def get_stats(self, metric_name: str, window_seconds: float = 300.0) -> dict:
        """Return count, min, max, mean, p50, p95, p99 for a series."""
        points = self.query_series(metric_name, window_seconds=window_seconds)
        values = [p["value"] for p in points]
        if not values:
            return {"metric_name": metric_name, "count": 0, "window_seconds": window_seconds}
        return {
            "metric_name": metric_name,
            "count": len(values),
            "window_seconds": window_seconds,
            "min": min(values),
            "max": max(values),
            "mean": sum(values) / len(values),
            "p50": _percentile(values, 50),
            "p95": _percentile(values, 95),
            "p99": _percentile(values, 99),
        }

    def list_series(self) -> list[str]:
        with self._lock:
            return list(self._series.keys())

    def series_count(self) -> int:
        with self._lock:
            return len(self._series)

    def ingest_from_metric_payload(self, payload: dict) -> int:
        """Ingest a normalized metric payload dict (from otelctx.ingest).
        Returns number of data points stored."""
        count = 0
        for metric in payload.get("metrics", []):
            metric_name = metric.get("metric_name") or metric.get("name", "unknown")
            service_name = metric.get("service_name", "unknown")
            labels = {
                "service": service_name,
                "metric_type": metric.get("metric_type", "unknown"),
            }
            for dp in metric.get("data_points", []):
                value = dp.get("value")
                if value is None:
                    continue
                ts_ns = dp.get("time_unix_nano")
                ts = (ts_ns / 1e9) if ts_ns else time.time()
                lbl = dict(labels)
                lbl.update({str(k): str(v) for k, v in dp.get("attributes", {}).items()})
                self.record(metric_name, float(value), lbl, ts)
                count += 1
        return count

    def aggregate_window(
        self,
        metric_name: str,
        window_seconds: float = 300.0,
        step_seconds: float = 60.0,
        labels: dict[str, str] | None = None,
    ) -> list[dict]:
        """Bucket metric points into tumbling windows of *step_seconds*.

        Returns one summary dict per bucket with keys:
        ``bucket_start``, ``bucket_end``, ``count``, ``min``, ``max``,
        ``mean``, ``p50``, ``p95``, ``p99``.
        """
        now = time.time()
        start = now - window_seconds
        points = self.query_series(metric_name, window_seconds=window_seconds, labels=labels)

        if not points:
            return []

        # Build bucket index → list[float]
        buckets: dict[int, list[float]] = {}
        for p in points:
            bucket_idx = int((p["timestamp"] - start) / step_seconds)
            buckets.setdefault(bucket_idx, []).append(p["value"])

        result = []
        total_steps = int(window_seconds / step_seconds) + 1
        for idx in range(total_steps):
            values = buckets.get(idx, [])
            bucket_start = start + idx * step_seconds
            bucket_end = bucket_start + step_seconds
            if not values:
                result.append({
                    "bucket_start": bucket_start,
                    "bucket_end": bucket_end,
                    "count": 0,
                })
            else:
                result.append({
                    "bucket_start": bucket_start,
                    "bucket_end": bucket_end,
                    "count": len(values),
                    "min": min(values),
                    "max": max(values),
                    "mean": sum(values) / len(values),
                    "p50": _percentile(values, 50),
                    "p95": _percentile(values, 95),
                    "p99": _percentile(values, 99),
                })
        return result

    # ------------------------------------------------------------------
    # TSDB Persistence
    # ------------------------------------------------------------------

    def flush_to_db(self, session_factory) -> int:
        """Write all current series data points to SQLite MetricPoint table.

        Returns the number of rows inserted.
        """
        from otelctx.db.models import MetricPoint

        count = 0
        with self._lock:
            series_snapshot = {k: list(v._buf) for k, v in self._series.items()}

        session = session_factory()
        try:
            batch: list[MetricPoint] = []
            for metric_name, points in series_snapshot.items():
                for dp in points:
                    batch.append(
                        MetricPoint(
                            metric_name=metric_name,
                            value=dp.value,
                            time=datetime.fromtimestamp(dp.timestamp, tz=UTC),
                            service_name=dp.labels.get("service", "unknown"),
                            metric_type=dp.labels.get("metric_type", "gauge"),
                            attributes_json=json.dumps(dp.labels) if dp.labels else None,
                        )
                    )
            session.bulk_save_objects(batch)
            session.commit()
            count = len(batch)
        except Exception:
            session.rollback()
            logger.exception("flush_to_db failed")
        finally:
            session.close()
        return count

    def warm_from_db(self, session_factory, since_hours: float = 24) -> int:
        """Load recent MetricPoint rows back into ring buffers on startup.

        Returns the number of data points loaded.
        """
        from otelctx.db.models import MetricPoint

        cutoff = datetime.now(UTC) - timedelta(hours=since_hours)
        count = 0
        session = session_factory()
        try:
            rows = (
                session.query(MetricPoint)
                .filter(MetricPoint.time >= cutoff)
                .order_by(MetricPoint.time.asc())
                .all()
            )
            for row in rows:
                labels: dict[str, str] = {}
                if row.attributes_json:
                    try:
                        labels = json.loads(row.attributes_json)
                    except (json.JSONDecodeError, TypeError):
                        pass
                ts = row.time.timestamp() if row.time else time.time()
                self.record(row.metric_name, row.value, labels=labels, timestamp=ts)
                count += 1
        except Exception:
            logger.exception("warm_from_db failed")
        finally:
            session.close()
        return count

    def start_periodic_flush(self, session_factory, interval_seconds: float = 60) -> threading.Event:
        """Run flush_to_db in a background daemon thread at *interval_seconds*.

        Returns a :class:`threading.Event` that can be set to stop the loop.
        """
        stop_event = threading.Event()

        def _loop() -> None:
            while not stop_event.wait(timeout=interval_seconds):
                try:
                    flushed = self.flush_to_db(session_factory)
                    logger.debug("periodic flush: %d points written", flushed)
                except Exception:
                    logger.exception("periodic flush error")

        t = threading.Thread(target=_loop, daemon=True, name="tsdb-periodic-flush")
        t.start()
        return stop_event
