"""
Prometheus metrics export for CAAE monitoring.

Exports key metrics including:
- fallbacks_triggered: Number of times CAAE queries fell back to baseline
- circuit_breaker_active: Current state of circuit breaker
- queue_depth_* : Queue depth statistics
- decision_*: Decision metrics (swap, recompute, hold)

Integration:
    from caae.metrics import MetricsCollector

    collector = MetricsCollector()

    # After each decision
    collector.record_decision(
        action="swap",
        fallback=False,
        queue_depth_ms=2.5
    )

    # Get current metrics
    metrics_text = collector.generate_prometheus_metrics()

    # Or use FastAPI endpoint:
    # GET /metrics -> application/openmetrics-text

Author: CAAE Research
Date: January 23, 2026
"""

import time
from dataclasses import dataclass, field
from typing import Dict, List, Optional


@dataclass
class MetricValue:
    """A single metric value with timestamp."""

    value: float
    timestamp: float = field(default_factory=time.time)


class Counter:
    """Prometheus Counter metric."""

    def __init__(self, name: str, description: str, labels: List[str] = None):
        self.name = name
        self.description = description
        self.labels = labels or []
        self.values: Dict[str, float] = {}

    def inc(self, amount: float = 1.0, labels: Dict[str, str] = None):
        """Increment counter."""
        key = self._make_key(labels)
        self.values[key] = self.values.get(key, 0) + amount

    def _make_key(self, labels: Optional[Dict[str, str]]) -> str:
        """Create label key."""
        if not labels:
            return ""
        return ",".join(f"{k}={v}" for k, v in sorted(labels.items()))

    def to_prometheus(self) -> str:
        """Convert to Prometheus text format."""
        lines = [
            f"# HELP {self.name} {self.description}",
            f"# TYPE {self.name} counter",
        ]

        if not self.values:
            lines.append(f"{self.name} 0")
        else:
            for key, value in self.values.items():
                if key:
                    lines.append(f"{self.name}{{{key}}} {value}")
                else:
                    lines.append(f"{self.name} {value}")

        return "\n".join(lines)


class Gauge:
    """Prometheus Gauge metric."""

    def __init__(self, name: str, description: str, labels: List[str] = None):
        self.name = name
        self.description = description
        self.labels = labels or []
        self.values: Dict[str, float] = {}

    def set(self, value: float, labels: Dict[str, str] = None):
        """Set gauge value."""
        key = self._make_key(labels)
        self.values[key] = value

    def inc(self, amount: float = 1.0, labels: Dict[str, str] = None):
        """Increment gauge."""
        key = self._make_key(labels)
        self.values[key] = self.values.get(key, 0) + amount

    def dec(self, amount: float = 1.0, labels: Dict[str, str] = None):
        """Decrement gauge."""
        key = self._make_key(labels)
        self.values[key] = self.values.get(key, 0) - amount

    def _make_key(self, labels: Optional[Dict[str, str]]) -> str:
        """Create label key."""
        if not labels:
            return ""
        return ",".join(f"{k}={v}" for k, v in sorted(labels.items()))

    def to_prometheus(self) -> str:
        """Convert to Prometheus text format."""
        lines = [
            f"# HELP {self.name} {self.description}",
            f"# TYPE {self.name} gauge",
        ]

        if not self.values:
            lines.append(f"{self.name} 0")
        else:
            for key, value in self.values.items():
                if key:
                    lines.append(f"{self.name}{{{key}}} {value}")
                else:
                    lines.append(f"{self.name} {value}")

        return "\n".join(lines)


class Histogram:
    """Prometheus Histogram metric."""

    def __init__(
        self,
        name: str,
        description: str,
        buckets: List[float] = None,
        labels: List[str] = None,
    ):
        self.name = name
        self.description = description
        self.labels = labels or []
        self.buckets = buckets or [0.1, 0.5, 1.0, 5.0, 10.0, 50.0, 100.0]
        self.values: Dict[str, List[float]] = {}

    def observe(self, value: float, labels: Dict[str, str] = None):
        """Record a value."""
        key = self._make_key(labels)
        if key not in self.values:
            self.values[key] = []
        self.values[key].append(value)

    def _make_key(self, labels: Optional[Dict[str, str]]) -> str:
        """Create label key."""
        if not labels:
            return ""
        return ",".join(f"{k}={v}" for k, v in sorted(labels.items()))

    def to_prometheus(self) -> str:
        """Convert to Prometheus text format."""
        lines = [
            f"# HELP {self.name} {self.description}",
            f"# TYPE {self.name} histogram",
        ]

        for key, values in self.values.items():
            if not values:
                continue

            values_sorted = sorted(values)
            count = len(values_sorted)
            total = sum(values_sorted)

            label_str = f"{{{key}}}" if key else ""

            # Bucket lines
            for bucket in self.buckets:
                bucket_count = sum(1 for v in values_sorted if v <= bucket)
                lines.append(f'{self.name}_bucket{{le="{bucket}"{key}}} {bucket_count}')

            lines.append(f'{self.name}_bucket{{le="+Inf"{key}}} {count}')
            lines.append(f"{self.name}_sum{label_str} {total}")
            lines.append(f"{self.name}_count{label_str} {count}")

        return "\n".join(lines)


class MetricsCollector:
    """
    Prometheus metrics collector for CAAE.

    Collects and exports metrics for:
    - Decision actions (swap, recompute, hold)
    - Fallback counts and ratios
    - Circuit breaker state
    - Queue depth measurements
    - Decision latency
    """

    def __init__(self):
        """Initialize metrics collector."""
        # Counter metrics
        self.total_decisions = Counter(
            "caae_total_decisions", "Total number of CAAE decisions made"
        )

        self.swaps_executed = Counter(
            "caae_swaps_executed_total", "Total number of swap decisions executed"
        )

        self.recomputes_triggered = Counter(
            "caae_recomputes_triggered_total",
            "Total number of recompute decisions triggered",
        )

        self.holds_issued = Counter(
            "caae_holds_issued_total", "Total number of hold decisions issued"
        )

        self.fallbacks_triggered = Counter(
            "caae_fallbacks_triggered_total",
            "Total number of times fallback to baseline occurred",
        )

        self.caae_errors = Counter(
            "caae_service_errors_total", "Total number of CAAE service errors"
        )

        # Gauge metrics
        self.circuit_breaker_state = Gauge(
            "caae_circuit_breaker_state",
            "Circuit breaker state (0=closed, 1=half_open, 2=open)",
        )

        self.circuit_breaker_active = Gauge(
            "caae_circuit_breaker_active",
            "Is circuit breaker currently active (tripped)",
        )

        self.queue_depth_current = Gauge(
            "caae_queue_depth_current_ms", "Current PCIe queue depth in milliseconds"
        )

        self.queue_depth_max = Gauge(
            "caae_queue_depth_max_ms", "Maximum PCIe queue depth observed"
        )

        self.queue_depth_avg = Gauge(
            "caae_queue_depth_avg_ms", "Average PCIe queue depth"
        )

        self.queue_depth_p99 = Gauge("caae_queue_depth_p99_ms", "P99 PCIe queue depth")

        self.queue_depth_threshold_exceeded = Counter(
            "caae_queue_depth_threshold_exceeded_total",
            "Number of times queue depth exceeded circuit breaker threshold",
        )

        # Histogram metrics
        self.decision_latency_ms = Histogram(
            "caae_decision_latency_ms",
            "Latency of CAAE decisions in milliseconds",
            buckets=[0.1, 0.5, 1.0, 5.0, 10.0, 50.0, 100.0],
        )

        # State tracking
        self.last_update = time.time()
        self.window_decisions = 0
        self.window_fallbacks = 0

    def record_decision(
        self,
        action: str,
        fallback: bool = False,
        queue_depth_ms: float = 0.0,
        decision_latency_ms: float = 0.0,
    ):
        """
        Record a single decision.

        Args:
            action: "swap", "recompute", or "hold"
            fallback: Whether this was a fallback to baseline
            queue_depth_ms: Queue depth at time of decision
            decision_latency_ms: Latency of decision in ms
        """
        self.total_decisions.inc()

        if action == "swap":
            self.swaps_executed.inc()
        elif action == "recompute":
            self.recomputes_triggered.inc()
        elif action == "hold":
            self.holds_issued.inc()

        if fallback:
            self.fallbacks_triggered.inc()

        self.queue_depth_current.set(queue_depth_ms)

        if decision_latency_ms > 0:
            self.decision_latency_ms.observe(decision_latency_ms)

        self.window_decisions += 1
        if fallback:
            self.window_fallbacks += 1

    def record_caae_error(self):
        """Record a CAAE service error."""
        self.caae_errors.inc()

    def record_queue_depth_threshold_exceeded(self):
        """Record an event where queue depth exceeded threshold."""
        self.queue_depth_threshold_exceeded.inc()

    def update_queue_depth_stats(
        self,
        current: float,
        min_val: float,
        max_val: float,
        avg: float,
        p99: float,
    ):
        """
        Update queue depth statistics.

        Args:
            current: Current queue depth
            min_val: Minimum observed
            max_val: Maximum observed
            avg: Average
            p99: 99th percentile
        """
        self.queue_depth_current.set(current)
        self.queue_depth_max.set(max_val)
        self.queue_depth_avg.set(avg)
        self.queue_depth_p99.set(p99)

    def update_circuit_breaker_state(self, state: int, is_active: bool):
        """
        Update circuit breaker state.

        Args:
            state: 0=closed, 1=half_open, 2=open
            is_active: Whether breaker is currently active
        """
        self.circuit_breaker_state.set(float(state))
        self.circuit_breaker_active.set(1.0 if is_active else 0.0)

    def generate_prometheus_metrics(self) -> str:
        """
        Generate Prometheus OpenMetrics format output.

        Returns:
            String with all metrics in OpenMetrics format
        """
        parts = [
            self.total_decisions.to_prometheus(),
            self.swaps_executed.to_prometheus(),
            self.recomputes_triggered.to_prometheus(),
            self.holds_issued.to_prometheus(),
            self.fallbacks_triggered.to_prometheus(),
            self.caae_errors.to_prometheus(),
            self.circuit_breaker_state.to_prometheus(),
            self.circuit_breaker_active.to_prometheus(),
            self.queue_depth_current.to_prometheus(),
            self.queue_depth_max.to_prometheus(),
            self.queue_depth_avg.to_prometheus(),
            self.queue_depth_p99.to_prometheus(),
            self.queue_depth_threshold_exceeded.to_prometheus(),
            self.decision_latency_ms.to_prometheus(),
        ]

        # Add OpenMetrics EOF marker
        output = "\n\n".join(parts)
        output += "\n\n# EOF\n"

        return output

    def get_fallback_ratio(self) -> float:
        """
        Get current fallback ratio.

        Returns:
            Fraction of decisions that were fallbacks (0.0 - 1.0)
        """
        if self.window_decisions == 0:
            return 0.0
        return self.window_fallbacks / self.window_decisions

    def reset_window_stats(self):
        """Reset window statistics (call periodically)."""
        self.window_decisions = 0
        self.window_fallbacks = 0
        self.last_update = time.time()


# Global collector instance
_collector: Optional[MetricsCollector] = None


def get_collector() -> MetricsCollector:
    """Get or create the global metrics collector."""
    global _collector
    if _collector is None:
        _collector = MetricsCollector()
    return _collector


def reset_collector():
    """Reset the global metrics collector (for testing)."""
    global _collector
    _collector = None
