"""
Anomaly Detector Service

Detects anomalies in time-series metrics using multiple methods:
- Z-Score: Statistical deviation from mean
- IQR: Interquartile range outliers
- MAD: Median absolute deviation
- EWMA: Exponentially weighted moving average
- Threshold: Simple threshold-based detection

Supports adaptive sensitivity and multi-metric correlation analysis.
"""

import json
import math
import uuid
from collections import deque
from datetime import datetime, timedelta, timezone
from typing import Any

import redis.asyncio as redis

from ..self_healing_types import (
    Anomaly,
    AnomalyDetectionConfig,
    AnomalyDetectionResult,
    AnomalySeverity,
    AnomalyType,
    DetectionMethod,
    MetricPoint,
)


class AnomalyDetector:
    """
    Multi-method anomaly detector for time-series metrics.

    Usage:
        detector = AnomalyDetector(config, redis_client)
        await detector.initialize()

        # Add data points
        await detector.add_point(MetricPoint(...))

        # Detect anomalies
        result = await detector.detect()
    """

    def __init__(
        self,
        config: AnomalyDetectionConfig,
        redis_client: redis.Redis | None = None,
    ):
        """Initialize anomaly detector"""
        self.config = config
        self._redis = redis_client

        # Data storage
        self._data_points: deque[MetricPoint] = deque(maxlen=config.window_size)
        self._baseline_mean: float = 0.0
        self._baseline_std: float = 0.0
        self._baseline_median: float = 0.0
        self._baseline_mad: float = 0.0
        self._ewma_value: float | None = None

        # Detection state
        self._last_detection_at: datetime | None = None
        self._anomalies_detected: list[Anomaly] = []

    async def initialize(self) -> None:
        """Load historical data from Redis"""
        if self._redis:
            await self._load_data()

    async def shutdown(self) -> None:
        """Save state before shutdown"""
        if self._redis:
            await self._save_data()

    # =========================================================================
    # Public API
    # =========================================================================

    async def add_point(self, point: MetricPoint) -> Anomaly | None:
        """
        Add a data point and check for anomalies.

        Args:
            point: Metric data point

        Returns:
            Detected anomaly if any, None otherwise
        """
        self._data_points.append(point)

        # Update baseline statistics
        self._update_baseline()

        # Check for anomaly
        anomaly = await self._detect_point(point)

        if anomaly:
            self._anomalies_detected.append(anomaly)

        # Persist state
        await self._save_data()

        return anomaly

    async def add_points(self, points: list[MetricPoint]) -> list[Anomaly]:
        """Add multiple points and detect anomalies"""
        anomalies = []
        for point in points:
            anomaly = await self.add_point(point)
            if anomaly:
                anomalies.append(anomaly)
        return anomalies

    async def detect(self) -> AnomalyDetectionResult:
        """
        Run full anomaly detection on current data window.

        Returns:
            Detection result with all anomalies found
        """
        start_time = datetime.now(timezone.utc)
        anomalies: list[Anomaly] = []

        if len(self._data_points) < self.config.min_data_points:
            return AnomalyDetectionResult(
                metric_name=self.config.metric_name,
                data_points_analyzed=len(self._data_points),
                is_healthy=True,
            )

        # Detect anomalies in all points
        for point in self._data_points:
            anomaly = await self._detect_point(point)
            if anomaly:
                anomalies.append(anomaly)

        detection_time_ms = (datetime.now(timezone.utc) - start_time).total_seconds() * 1000
        self._last_detection_at = datetime.now(timezone.utc)

        return AnomalyDetectionResult(
            metric_name=self.config.metric_name,
            detection_time_ms=detection_time_ms,
            data_points_analyzed=len(self._data_points),
            anomalies=anomalies,
            baseline_mean=self._baseline_mean,
            baseline_std=self._baseline_std,
            is_healthy=len(anomalies) == 0,
            next_check_at=datetime.now(timezone.utc) + timedelta(minutes=5),
        )

    def get_statistics(self) -> dict[str, Any]:
        """Get current baseline statistics"""
        return {
            "metric_name": self.config.metric_name,
            "data_points": len(self._data_points),
            "baseline_mean": self._baseline_mean,
            "baseline_std": self._baseline_std,
            "baseline_median": self._baseline_median,
            "baseline_mad": self._baseline_mad,
            "ewma_value": self._ewma_value,
            "last_detection_at": self._last_detection_at,
            "total_anomalies": len(self._anomalies_detected),
        }

    def clear_history(self) -> None:
        """Clear detection history"""
        self._anomalies_detected.clear()

    # =========================================================================
    # Detection Methods
    # =========================================================================

    async def _detect_point(self, point: MetricPoint) -> Anomaly | None:
        """Detect if a single point is anomalous"""
        if len(self._data_points) < self.config.min_data_points:
            return None

        method = self.config.detection_method

        if method == DetectionMethod.ZSCORE:
            return self._detect_zscore(point)
        elif method == DetectionMethod.IQR:
            return self._detect_iqr(point)
        elif method == DetectionMethod.MAD:
            return self._detect_mad(point)
        elif method == DetectionMethod.EWMA:
            return self._detect_ewma(point)
        elif method == DetectionMethod.THRESHOLD:
            return self._detect_threshold(point)
        else:
            # Default to z-score
            return self._detect_zscore(point)

    def _detect_zscore(self, point: MetricPoint) -> Anomaly | None:
        """Z-score based anomaly detection"""
        if self._baseline_std == 0:
            return None

        z_score = abs(point.value - self._baseline_mean) / self._baseline_std

        if z_score >= self.config.sensitivity:
            return self._create_anomaly(
                point=point,
                expected=self._baseline_mean,
                deviation=z_score,
                method=DetectionMethod.ZSCORE,
            )

        return None

    def _detect_iqr(self, point: MetricPoint) -> Anomaly | None:
        """Interquartile range anomaly detection"""
        values = sorted([p.value for p in self._data_points])
        n = len(values)

        if n < 4:
            return None

        q1_idx = n // 4
        q3_idx = 3 * n // 4
        q1 = values[q1_idx]
        q3 = values[q3_idx]
        iqr = q3 - q1

        if iqr == 0:
            return None

        lower_bound = q1 - self.config.sensitivity * iqr
        upper_bound = q3 + self.config.sensitivity * iqr

        if point.value < lower_bound or point.value > upper_bound:
            deviation = (
                (point.value - upper_bound) / iqr
                if point.value > upper_bound
                else (lower_bound - point.value) / iqr
            )
            return self._create_anomaly(
                point=point,
                expected=(q1 + q3) / 2,
                deviation=abs(deviation),
                method=DetectionMethod.IQR,
            )

        return None

    def _detect_mad(self, point: MetricPoint) -> Anomaly | None:
        """Median absolute deviation anomaly detection"""
        if self._baseline_mad == 0:
            return None

        # Modified z-score using MAD
        modified_z = 0.6745 * abs(point.value - self._baseline_median) / self._baseline_mad

        if modified_z >= self.config.sensitivity:
            return self._create_anomaly(
                point=point,
                expected=self._baseline_median,
                deviation=modified_z,
                method=DetectionMethod.MAD,
            )

        return None

    def _detect_ewma(self, point: MetricPoint) -> Anomaly | None:
        """Exponentially weighted moving average anomaly detection"""
        alpha = 0.3  # Smoothing factor

        if self._ewma_value is None:
            self._ewma_value = point.value
            return None

        # Update EWMA
        old_ewma = self._ewma_value
        self._ewma_value = alpha * point.value + (1 - alpha) * self._ewma_value

        # Calculate deviation from expected EWMA
        if self._baseline_std > 0:
            deviation = abs(point.value - old_ewma) / self._baseline_std

            if deviation >= self.config.sensitivity:
                return self._create_anomaly(
                    point=point,
                    expected=old_ewma,
                    deviation=deviation,
                    method=DetectionMethod.EWMA,
                )

        return None

    def _detect_threshold(self, point: MetricPoint) -> Anomaly | None:
        """Simple threshold-based anomaly detection"""
        # Use sensitivity as multiplier of std for threshold
        upper_threshold = self._baseline_mean + self.config.sensitivity * self._baseline_std
        lower_threshold = self._baseline_mean - self.config.sensitivity * self._baseline_std

        if point.value > upper_threshold or point.value < lower_threshold:
            deviation = abs(point.value - self._baseline_mean) / (self._baseline_std or 1)
            return self._create_anomaly(
                point=point,
                expected=self._baseline_mean,
                deviation=deviation,
                method=DetectionMethod.THRESHOLD,
            )

        return None

    # =========================================================================
    # Helper Methods
    # =========================================================================

    def _create_anomaly(
        self,
        point: MetricPoint,
        expected: float,
        deviation: float,
        method: DetectionMethod,
    ) -> Anomaly:
        """Create an anomaly record"""
        # Determine anomaly type
        if point.value > expected:
            anomaly_type = AnomalyType.SPIKE
        else:
            anomaly_type = AnomalyType.DROP

        # Determine severity based on deviation
        severity = self._calculate_severity(deviation)

        # Calculate confidence (based on data quality)
        confidence = min(1.0, len(self._data_points) / self.config.window_size)

        return Anomaly(
            anomaly_id=str(uuid.uuid4()),
            anomaly_type=anomaly_type,
            severity=severity,
            metric_name=self.config.metric_name,
            detected_at=datetime.now(timezone.utc),
            detection_method=method,
            expected_value=expected,
            actual_value=point.value,
            deviation=deviation,
            confidence=confidence,
            context={
                "baseline_mean": self._baseline_mean,
                "baseline_std": self._baseline_std,
                "data_points": len(self._data_points),
            },
            labels=point.labels,
        )

    def _calculate_severity(self, deviation: float) -> AnomalySeverity:
        """Calculate severity based on deviation"""
        sensitivity = self.config.sensitivity

        if deviation >= sensitivity * 3:
            return AnomalySeverity.CRITICAL
        elif deviation >= sensitivity * 2:
            return AnomalySeverity.HIGH
        elif deviation >= sensitivity * 1.5:
            return AnomalySeverity.MEDIUM
        else:
            return AnomalySeverity.LOW

    def _update_baseline(self) -> None:
        """Update baseline statistics"""
        if not self._data_points:
            return

        values = [p.value for p in self._data_points]

        # Mean and std
        n = len(values)
        self._baseline_mean = sum(values) / n

        if n > 1:
            variance = sum((v - self._baseline_mean) ** 2 for v in values) / (n - 1)
            self._baseline_std = math.sqrt(variance)
        else:
            self._baseline_std = 0.0

        # Median
        sorted_values = sorted(values)
        mid = n // 2
        if n % 2 == 0:
            self._baseline_median = (sorted_values[mid - 1] + sorted_values[mid]) / 2
        else:
            self._baseline_median = sorted_values[mid]

        # MAD (Median Absolute Deviation)
        deviations = sorted(abs(v - self._baseline_median) for v in values)
        mad_mid = n // 2
        if n % 2 == 0:
            self._baseline_mad = (deviations[mad_mid - 1] + deviations[mad_mid]) / 2
        else:
            self._baseline_mad = deviations[mad_mid]

    async def _load_data(self) -> None:
        """Load state from Redis"""
        if not self._redis:
            return

        key = f"snbg:anomaly:data:{self.config.metric_name}"
        data = await self._redis.get(key)

        if data:
            state = json.loads(data)
            # Restore data points
            for point_data in state.get("data_points", []):
                point = MetricPoint(
                    timestamp=datetime.fromisoformat(point_data["timestamp"]),
                    value=point_data["value"],
                    metric_name=point_data["metric_name"],
                    labels=point_data.get("labels", {}),
                )
                self._data_points.append(point)

            # Restore baseline
            self._baseline_mean = state.get("baseline_mean", 0.0)
            self._baseline_std = state.get("baseline_std", 0.0)
            self._baseline_median = state.get("baseline_median", 0.0)
            self._baseline_mad = state.get("baseline_mad", 0.0)
            self._ewma_value = state.get("ewma_value")

    async def _save_data(self) -> None:
        """Save state to Redis"""
        if not self._redis:
            return

        key = f"snbg:anomaly:data:{self.config.metric_name}"
        state = {
            "data_points": [
                {
                    "timestamp": p.timestamp.isoformat(),
                    "value": p.value,
                    "metric_name": p.metric_name,
                    "labels": p.labels,
                }
                for p in self._data_points
            ],
            "baseline_mean": self._baseline_mean,
            "baseline_std": self._baseline_std,
            "baseline_median": self._baseline_median,
            "baseline_mad": self._baseline_mad,
            "ewma_value": self._ewma_value,
        }
        await self._redis.set(key, json.dumps(state), ex=86400 * 7)


class MultiMetricAnomalyDetector:
    """
    Detects anomalies across multiple correlated metrics.

    Usage:
        detector = MultiMetricAnomalyDetector(redis_client)
        await detector.add_detector("cpu_usage", cpu_config)
        await detector.add_detector("memory_usage", mem_config)

        # Add correlated metrics
        await detector.add_point("cpu_usage", cpu_point)
        await detector.add_point("memory_usage", mem_point)

        # Detect across all metrics
        results = await detector.detect_all()
    """

    def __init__(self, redis_client: redis.Redis | None = None):
        """Initialize multi-metric detector"""
        self._redis = redis_client
        self._detectors: dict[str, AnomalyDetector] = {}
        self._correlations: dict[tuple[str, str], float] = {}

    async def add_detector(
        self,
        metric_name: str,
        config: AnomalyDetectionConfig | None = None,
    ) -> AnomalyDetector:
        """Add a detector for a metric"""
        if config is None:
            config = AnomalyDetectionConfig(metric_name=metric_name)

        detector = AnomalyDetector(config, self._redis)
        await detector.initialize()
        self._detectors[metric_name] = detector
        return detector

    def get_detector(self, metric_name: str) -> AnomalyDetector | None:
        """Get detector by metric name"""
        return self._detectors.get(metric_name)

    async def add_point(
        self,
        metric_name: str,
        point: MetricPoint,
    ) -> Anomaly | None:
        """Add a point to a specific metric detector"""
        detector = self._detectors.get(metric_name)
        if not detector:
            return None
        return await detector.add_point(point)

    async def detect_all(self) -> dict[str, AnomalyDetectionResult]:
        """Run detection on all metrics"""
        results = {}
        for name, detector in self._detectors.items():
            results[name] = await detector.detect()
        return results

    async def detect_correlated_anomalies(self) -> list[dict[str, Any]]:
        """
        Detect anomalies that occur across correlated metrics.

        Returns list of correlated anomaly events.
        """
        correlated_events: list[dict[str, Any]] = []

        # Get all recent anomalies
        all_anomalies: dict[str, list[Anomaly]] = {}
        for name, detector in self._detectors.items():
            result = await detector.detect()
            if result.anomalies:
                all_anomalies[name] = result.anomalies

        # Find time-correlated anomalies (within 5 minutes)
        time_window = timedelta(minutes=5)
        metric_names = list(all_anomalies.keys())

        for i, name1 in enumerate(metric_names):
            for name2 in metric_names[i + 1:]:
                for a1 in all_anomalies.get(name1, []):
                    for a2 in all_anomalies.get(name2, []):
                        time_diff = abs((a1.detected_at - a2.detected_at).total_seconds())
                        if time_diff <= time_window.total_seconds():
                            correlated_events.append({
                                "metrics": [name1, name2],
                                "anomalies": [a1.anomaly_id, a2.anomaly_id],
                                "time_diff_seconds": time_diff,
                                "severity": max(
                                    a1.severity.value, a2.severity.value
                                ),
                            })

        return correlated_events

    def set_correlation(self, metric1: str, metric2: str, correlation: float) -> None:
        """Set expected correlation between two metrics"""
        key = (min(metric1, metric2), max(metric1, metric2))
        self._correlations[key] = correlation

    def list_metrics(self) -> list[str]:
        """List all monitored metrics"""
        return list(self._detectors.keys())

    def get_all_statistics(self) -> dict[str, dict[str, Any]]:
        """Get statistics for all metrics"""
        return {
            name: detector.get_statistics()
            for name, detector in self._detectors.items()
        }
