"""
Starvex Alerting - Threshold-based alerting for anomalies.

Provides a lightweight in-process alerting engine with:
- Block rate spike detection
- Latency SLA violations (p99)
- Error rate alerts

Includes optional Supabase Edge Function integration for alert delivery.
"""

from __future__ import annotations

import logging
import math
import platform
import threading
import time
from collections import deque
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Callable, Deque, Dict, Iterable, List, Optional, Protocol

import httpx

logger = logging.getLogger(__name__)

SDK_VERSION = "3.0.0"
DEFAULT_API_HOST = "https://decqadhkqnacujoyirkh.supabase.co/functions/v1"


class AlertSeverity(str, Enum):
    INFO = "info"
    WARNING = "warning"
    CRITICAL = "critical"


class AlertType(str, Enum):
    BLOCK_RATE_SPIKE = "block_rate_spike"
    LATENCY_SLA = "latency_sla"
    ERROR_RATE = "error_rate"


@dataclass
class AlertRule:
    name: str
    alert_type: AlertType
    threshold: float
    window_seconds: int
    min_events: int = 20
    comparator: str = ">"
    severity: AlertSeverity = AlertSeverity.WARNING
    cooldown_seconds: int = 300
    enabled: bool = True
    description: Optional[str] = None


@dataclass
class AlertEvent:
    timestamp: float
    status: str
    latency_ms: float
    blocked: bool
    error: bool


@dataclass
class AlertNotification:
    alert_type: AlertType
    severity: AlertSeverity
    message: str
    metrics: Dict[str, Any]
    window_seconds: int
    rule_name: str
    triggered_at: float
    context: Dict[str, Any] = field(default_factory=dict)

    def to_dict(self) -> Dict[str, Any]:
        return {
            "alert_type": self.alert_type.value,
            "severity": self.severity.value,
            "message": self.message,
            "metrics": self.metrics,
            "window_seconds": self.window_seconds,
            "rule_name": self.rule_name,
            "triggered_at": self.triggered_at,
            "context": self.context,
            "sdk_version": SDK_VERSION,
        }


class AlertSink(Protocol):
    def send(self, alert: AlertNotification) -> None:
        ...


class SupabaseAlertSink:
    """Send alerts to a Supabase Edge Function."""

    def __init__(
        self,
        api_key: str,
        host: Optional[str] = None,
        timeout_seconds: float = 8.0,
        max_retries: int = 2,
        retry_delay: float = 0.75,
    ) -> None:
        self.api_key = api_key
        self.host = (host or DEFAULT_API_HOST).rstrip("/")
        self.timeout_seconds = timeout_seconds
        self.max_retries = max_retries
        self.retry_delay = retry_delay

    def send(self, alert: AlertNotification) -> None:
        payload = alert.to_dict()
        headers = {
            "Content-Type": "application/json",
            "x-api-key": self.api_key,
            "User-Agent": f"starvex-sdk/{SDK_VERSION} python/{platform.python_version()}",
        }

        for attempt in range(self.max_retries + 1):
            try:
                with httpx.Client(timeout=self.timeout_seconds) as client:
                    response = client.post(
                        f"{self.host}/log-alert",
                        json=payload,
                        headers=headers,
                    )
                if response.status_code == 200:
                    return
                if response.status_code < 500:
                    logger.debug("Alert delivery failed: %s", response.text)
                    return
            except Exception as exc:
                logger.debug("Alert delivery error: %s", exc)

            if attempt < self.max_retries:
                time.sleep(self.retry_delay * (2**attempt))


class AlertManager:
    """
    In-process alert manager with sliding-window threshold checks.

    Call :meth:`record_event` for each request to update metrics.
    """

    def __init__(
        self,
        rules: Optional[List[AlertRule]] = None,
        sinks: Optional[List[AlertSink]] = None,
        context: Optional[Dict[str, Any]] = None,
    ) -> None:
        self.rules = rules or self._default_rules()
        self._events: Deque[AlertEvent] = deque()
        self._lock = threading.Lock()
        self._last_alerted: Dict[str, float] = {}
        self._sinks: List[AlertSink] = list(sinks or [])
        self._handlers: List[Callable[[AlertNotification], None]] = []
        self._context = context or {}
        self._max_window_seconds = max((r.window_seconds for r in self.rules), default=300)

    def add_sink(self, sink: AlertSink) -> None:
        self._sinks.append(sink)

    def add_handler(self, handler: Callable[[AlertNotification], None]) -> None:
        self._handlers.append(handler)

    def set_context(self, **kwargs: Any) -> None:
        self._context.update(kwargs)

    def record_event(
        self,
        status: str,
        latency_ms: float = 0.0,
        blocked: Optional[bool] = None,
        error: Optional[bool] = None,
        timestamp: Optional[float] = None,
    ) -> None:
        if blocked is None:
            blocked = status == "blocked"
        if error is None:
            error = status in {"error", "failed", "flagged"}

        event = AlertEvent(
            timestamp=timestamp or time.time(),
            status=status,
            latency_ms=latency_ms,
            blocked=blocked,
            error=error,
        )

        alerts: List[AlertNotification] = []
        with self._lock:
            self._events.append(event)
            self._prune_locked(event.timestamp)
            alerts = self._evaluate_rules_locked(event.timestamp)

        for alert in alerts:
            self._emit_alert(alert)

    def record_trace_event(self, event: Dict[str, Any]) -> None:
        status = event.get("status") or "success"
        latency_ms = float(event.get("latency_ms") or 0.0)
        self.record_event(status=status, latency_ms=latency_ms)

    # -- internals -------------------------------------------------------

    def _prune_locked(self, now: float) -> None:
        cutoff = now - self._max_window_seconds
        while self._events and self._events[0].timestamp < cutoff:
            self._events.popleft()

    def _evaluate_rules_locked(self, now: float) -> List[AlertNotification]:
        notifications: List[AlertNotification] = []
        events_snapshot = list(self._events)

        for rule in self.rules:
            if not rule.enabled:
                continue

            window_start = now - rule.window_seconds
            window_events = [e for e in events_snapshot if e.timestamp >= window_start]
            if len(window_events) < rule.min_events:
                continue

            metrics = self._compute_metrics(window_events)
            value = self._metric_value(rule.alert_type, metrics)
            if value is None:
                continue

            if not _compare(value, rule.threshold, rule.comparator):
                continue

            last = self._last_alerted.get(rule.name)
            if last is not None and (now - last) < rule.cooldown_seconds:
                continue

            self._last_alerted[rule.name] = now
            message = self._format_message(rule, value, metrics)
            notifications.append(
                AlertNotification(
                    alert_type=rule.alert_type,
                    severity=rule.severity,
                    message=message,
                    metrics=metrics,
                    window_seconds=rule.window_seconds,
                    rule_name=rule.name,
                    triggered_at=now,
                    context=self._context.copy(),
                )
            )

        return notifications

    def _emit_alert(self, alert: AlertNotification) -> None:
        for sink in self._sinks:
            try:
                sink.send(alert)
            except Exception as exc:
                logger.debug("Alert sink failure: %s", exc)

        for handler in self._handlers:
            try:
                handler(alert)
            except Exception as exc:
                logger.debug("Alert handler failure: %s", exc)

    @staticmethod
    def _compute_metrics(events: Iterable[AlertEvent]) -> Dict[str, Any]:
        events_list = list(events)
        total = len(events_list)
        blocked = sum(1 for e in events_list if e.blocked)
        errors = sum(1 for e in events_list if e.error)
        latencies = [e.latency_ms for e in events_list if e.latency_ms is not None]

        block_rate = blocked / total if total else 0.0
        error_rate = errors / total if total else 0.0
        p99_latency = _percentile(latencies, 99)
        p95_latency = _percentile(latencies, 95)
        avg_latency = sum(latencies) / len(latencies) if latencies else 0.0

        return {
            "total_events": total,
            "blocked_events": blocked,
            "error_events": errors,
            "block_rate": block_rate,
            "error_rate": error_rate,
            "p99_latency_ms": p99_latency,
            "p95_latency_ms": p95_latency,
            "avg_latency_ms": avg_latency,
        }

    @staticmethod
    def _metric_value(alert_type: AlertType, metrics: Dict[str, Any]) -> Optional[float]:
        if alert_type == AlertType.BLOCK_RATE_SPIKE:
            return float(metrics.get("block_rate", 0.0))
        if alert_type == AlertType.ERROR_RATE:
            return float(metrics.get("error_rate", 0.0))
        if alert_type == AlertType.LATENCY_SLA:
            return float(metrics.get("p99_latency_ms", 0.0))
        return None

    @staticmethod
    def _format_message(rule: AlertRule, value: float, metrics: Dict[str, Any]) -> str:
        total = metrics.get("total_events", 0)
        window_min = max(1, int(rule.window_seconds / 60))

        if rule.alert_type == AlertType.BLOCK_RATE_SPIKE:
            return (
                f"Block rate {value:.1%} exceeded {rule.threshold:.1%} "
                f"over last {window_min}m (n={total})."
            )
        if rule.alert_type == AlertType.ERROR_RATE:
            return (
                f"Error rate {value:.1%} exceeded {rule.threshold:.1%} "
                f"over last {window_min}m (n={total})."
            )
        if rule.alert_type == AlertType.LATENCY_SLA:
            return (
                f"p99 latency {value:.0f}ms exceeded {rule.threshold:.0f}ms "
                f"over last {window_min}m (n={total})."
            )
        return f"Alert triggered for {rule.name}"

    @staticmethod
    def _default_rules() -> List[AlertRule]:
        return [
            AlertRule(
                name="block_rate_spike",
                alert_type=AlertType.BLOCK_RATE_SPIKE,
                threshold=0.20,
                window_seconds=300,
                min_events=20,
                severity=AlertSeverity.WARNING,
                description="Block rate spike >20% over 5 minutes.",
            ),
            AlertRule(
                name="latency_sla_violation",
                alert_type=AlertType.LATENCY_SLA,
                threshold=500.0,
                window_seconds=300,
                min_events=20,
                severity=AlertSeverity.CRITICAL,
                description="p99 latency exceeded 500ms over 5 minutes.",
            ),
            AlertRule(
                name="error_rate_spike",
                alert_type=AlertType.ERROR_RATE,
                threshold=0.05,
                window_seconds=300,
                min_events=20,
                severity=AlertSeverity.WARNING,
                description="Error rate exceeded 5% over 5 minutes.",
            ),
        ]


def _percentile(values: List[float], percentile: float) -> float:
    if not values:
        return 0.0
    values = sorted(values)
    k = (len(values) - 1) * (percentile / 100.0)
    f = int(k)
    c = min(f + 1, len(values) - 1)
    if f == c:
        return float(values[f])
    d = k - f
    return float(values[f]) + d * (float(values[c]) - float(values[f]))


def _compare(value: float, threshold: float, comparator: str) -> bool:
    if comparator == ">":
        return value > threshold
    if comparator == ">=":
        return value >= threshold
    if comparator == "<":
        return value < threshold
    if comparator == "<=":
        return value <= threshold
    if comparator == "==":
        return math.isclose(value, threshold)
    return value > threshold

