//! Ensemble anomaly detection with temporal pattern analysis
//!
//! Detects coordinated attacks, sudden signal shifts, and model drift
//! in ensemble predictions to identify compromised or poisoned models.

use std::collections::{HashMap, VecDeque};

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
use tracing::{debug, error, warn};

use ml_ensemble::EnsembleDecision;

/// Ensemble anomaly detector with temporal pattern analysis
///
/// Detects three types of anomalies:
/// 1. Sudden signal shifts (>50% change from previous prediction)
/// 2. Coordinated attacks (>80% of models predict extreme values)
/// 3. Model behavioral drift (individual model deviates from historical mean)
///
/// # Performance
/// - Detection: ~15us per ensemble decision
/// - History update: ~5us
pub struct EnsembleAnomalyDetector {
    /// Historical ensemble signal distribution
    signal_history: RwLock<VecDeque<f64>>,

    /// Per-model signal history
    model_signal_history: RwLock<HashMap<String, VecDeque<f64>>>,

    /// Configuration
    config: AnomalyDetectorConfig,
}

impl std::fmt::Debug for EnsembleAnomalyDetector {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("EnsembleAnomalyDetector")
            .field("config", &self.config)
            .field("signal_history", &"RwLock<VecDeque<f64>>")
            .field("model_signal_history", &"RwLock<HashMap<..>>")
            .finish()
    }
}

/// Configuration for anomaly detection
#[derive(Debug, Clone)]
pub struct AnomalyDetectorConfig {
    /// Size of ensemble signal history window (default: 100)
    pub signal_window_size: usize,

    /// Size of per-model history window (default: 50)
    pub model_window_size: usize,

    /// Threshold for sudden signal shift (default: 0.5 = 50%)
    pub sudden_shift_threshold: f64,

    /// Threshold for coordinated attack detection (default: 0.8 = 80%)
    pub coordinated_attack_threshold: f64,

    /// Threshold for model drift detection (default: 0.7)
    pub drift_threshold: f64,

    /// Extreme value threshold (default: 0.9)
    pub extreme_value_threshold: f64,
}

impl Default for AnomalyDetectorConfig {
    fn default() -> Self {
        Self {
            signal_window_size: 100,
            model_window_size: 50,
            sudden_shift_threshold: 0.5,
            coordinated_attack_threshold: 0.8,
            drift_threshold: 0.7,
            extreme_value_threshold: 0.9,
        }
    }
}

/// Anomaly report with detected issues
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnomalyReport {
    /// Whether any anomalies were detected
    pub has_anomalies: bool,

    /// List of detected anomalies
    pub anomalies: Vec<Anomaly>,

    /// Report timestamp
    pub timestamp: DateTime<Utc>,

    /// Overall severity
    pub severity: AnomalySeverity,
}

/// Types of anomalies detected
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Anomaly {
    /// Sudden large change in ensemble signal
    SuddenShift {
        previous: f64,
        current: f64,
        magnitude: f64,
    },

    /// Multiple models producing extreme predictions simultaneously
    CoordinatedAttack {
        extreme_ratio: f64,
        affected_models: Vec<String>,
    },

    /// Individual model drifting from historical behavior
    ModelDrift {
        model_id: String,
        historical_mean: f64,
        current_signal: f64,
        drift_magnitude: f64,
    },
}

/// Severity levels for anomalies
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum AnomalySeverity {
    /// Single minor outlier
    Low = 1,

    /// Multiple outliers or moderate drift
    Medium = 2,

    /// Coordinated attack or extreme shift
    High = 3,

    /// System-wide compromise suspected
    Critical = 4,
}

impl EnsembleAnomalyDetector {
    /// Create a new anomaly detector with default configuration
    pub fn new() -> Self {
        Self::with_config(AnomalyDetectorConfig::default())
    }

    /// Create a new anomaly detector with custom configuration
    pub fn with_config(config: AnomalyDetectorConfig) -> Self {
        Self {
            signal_history: RwLock::new(VecDeque::new()),
            model_signal_history: RwLock::new(HashMap::new()),
            config,
        }
    }

    /// Detect anomalies in ensemble decision
    ///
    /// # Arguments
    /// * `decision` - Ensemble decision to analyze
    ///
    /// # Returns
    /// AnomalyReport with detected issues and severity
    ///
    /// # Performance
    /// ~15us per call (includes history access and calculations)
    pub async fn detect_anomaly(&self, decision: &EnsembleDecision) -> AnomalyReport {
        let mut anomalies = Vec::new();

        // Detection 1: Sudden signal shift
        if let Some(shift_anomaly) = self.detect_sudden_shift(decision).await {
            anomalies.push(shift_anomaly);
        }

        // Detection 2: Coordinated attack
        if let Some(attack_anomaly) = self.detect_coordinated_attack(decision).await {
            anomalies.push(attack_anomaly);
        }

        // Detection 3: Model drift
        let drift_anomalies = self.detect_model_drift(decision).await;
        anomalies.extend(drift_anomalies);

        // Calculate overall severity
        let severity = self.calculate_severity(&anomalies);

        // Log based on severity
        match severity {
            AnomalySeverity::Low => {
                debug!("Low severity anomaly detected: {} issues", anomalies.len());
            },
            AnomalySeverity::Medium => {
                warn!(
                    "Medium severity anomaly detected: {} issues",
                    anomalies.len()
                );
            },
            AnomalySeverity::High => {
                warn!(
                    "High severity anomaly detected: {} issues - possible attack",
                    anomalies.len()
                );
            },
            AnomalySeverity::Critical => {
                error!(
                    "CRITICAL anomaly detected: {} issues - system-wide compromise suspected",
                    anomalies.len()
                );
            },
        }

        AnomalyReport {
            has_anomalies: !anomalies.is_empty(),
            anomalies,
            timestamp: Utc::now(),
            severity,
        }
    }

    /// Detect sudden signal shift
    async fn detect_sudden_shift(&self, decision: &EnsembleDecision) -> Option<Anomaly> {
        let signal_history = self.signal_history.read().await;

        if let Some(&previous_signal) = signal_history.back() {
            let current_signal = decision.signal;
            let magnitude = (current_signal - previous_signal).abs();

            if magnitude > self.config.sudden_shift_threshold {
                warn!(
                    "Sudden signal shift detected: {:.3} -> {:.3} (magnitude: {:.3})",
                    previous_signal, current_signal, magnitude
                );

                return Some(Anomaly::SuddenShift {
                    previous: previous_signal,
                    current: current_signal,
                    magnitude,
                });
            }
        }

        None
    }

    /// Detect coordinated attack (all models predict extreme)
    async fn detect_coordinated_attack(&self, decision: &EnsembleDecision) -> Option<Anomaly> {
        if decision.model_votes.is_empty() {
            return None;
        }

        let extreme_models: Vec<String> = decision
            .model_votes
            .iter()
            .filter(|(_, vote)| vote.signal.abs() > self.config.extreme_value_threshold)
            .map(|(model_id, _)| model_id.clone())
            .collect();

        let extreme_ratio = extreme_models.len() as f64 / decision.model_votes.len() as f64;

        if extreme_ratio > self.config.coordinated_attack_threshold {
            warn!(
                "Coordinated attack detected: {:.1}% of models ({}/{}) producing extreme predictions",
                extreme_ratio * 100.0,
                extreme_models.len(),
                decision.model_votes.len()
            );

            return Some(Anomaly::CoordinatedAttack {
                extreme_ratio,
                affected_models: extreme_models,
            });
        }

        None
    }

    /// Detect model drift (per-model behavioral changes)
    async fn detect_model_drift(&self, decision: &EnsembleDecision) -> Vec<Anomaly> {
        let model_history = self.model_signal_history.read().await;
        let mut drift_anomalies = Vec::new();

        for (model_id, vote) in &decision.model_votes {
            if let Some(history) = model_history.get(model_id) {
                if history.len() < 10 {
                    // Need at least 10 samples for reliable statistics
                    continue;
                }

                let historical_mean: f64 = history.iter().sum::<f64>() / history.len() as f64;
                let current_signal = vote.signal;
                let drift_magnitude = (current_signal - historical_mean).abs();

                if drift_magnitude > self.config.drift_threshold {
                    warn!(
                        "Model drift detected for {}: mean={:.3}, current={:.3}, drift={:.3}",
                        model_id, historical_mean, current_signal, drift_magnitude
                    );

                    drift_anomalies.push(Anomaly::ModelDrift {
                        model_id: model_id.clone(),
                        historical_mean,
                        current_signal,
                        drift_magnitude,
                    });
                }
            }
        }

        drift_anomalies
    }

    /// Calculate overall severity based on anomaly types
    fn calculate_severity(&self, anomalies: &[Anomaly]) -> AnomalySeverity {
        if anomalies.is_empty() {
            return AnomalySeverity::Low;
        }

        let has_coordinated_attack = anomalies
            .iter()
            .any(|a| matches!(a, Anomaly::CoordinatedAttack { .. }));

        let has_sudden_shift = anomalies
            .iter()
            .any(|a| matches!(a, Anomaly::SuddenShift { .. }));

        let drift_count = anomalies
            .iter()
            .filter(|a| matches!(a, Anomaly::ModelDrift { .. }))
            .count();

        // Critical: Coordinated attack detected
        if has_coordinated_attack {
            return AnomalySeverity::Critical;
        }

        // High: Sudden shift + multiple drifts
        if has_sudden_shift && drift_count >= 2 {
            return AnomalySeverity::High;
        }

        // Medium: Multiple drifts or single sudden shift
        if drift_count >= 3 || has_sudden_shift {
            return AnomalySeverity::Medium;
        }

        // Low: Single drift
        AnomalySeverity::Low
    }

    /// Update history with new decision
    ///
    /// Should be called after anomaly detection to maintain rolling window.
    pub async fn update_history(&self, decision: &EnsembleDecision) {
        // Update ensemble signal history
        {
            let mut signal_history = self.signal_history.write().await;
            signal_history.push_back(decision.signal);

            if signal_history.len() > self.config.signal_window_size {
                signal_history.pop_front();
            }
        }

        // Update per-model history
        {
            let mut model_history = self.model_signal_history.write().await;

            for (model_id, vote) in &decision.model_votes {
                let history = model_history
                    .entry(model_id.clone())
                    .or_insert_with(VecDeque::new);

                history.push_back(vote.signal);

                if history.len() > self.config.model_window_size {
                    history.pop_front();
                }
            }
        }

        debug!(
            "Updated anomaly detector history (ensemble signal: {:.3})",
            decision.signal
        );
    }

    /// Get current statistics for monitoring
    pub async fn get_statistics(&self) -> DetectorStatistics {
        let signal_history = self.signal_history.read().await;
        let model_history = self.model_signal_history.read().await;

        DetectorStatistics {
            signal_history_size: signal_history.len(),
            model_count: model_history.len(),
            total_samples: model_history.values().map(|h| h.len()).sum(),
        }
    }

    /// Reset all history (useful for testing or system reset)
    pub async fn reset_history(&self) {
        let mut signal_history = self.signal_history.write().await;
        signal_history.clear();

        let mut model_history = self.model_signal_history.write().await;
        model_history.clear();
    }
}

impl Default for EnsembleAnomalyDetector {
    fn default() -> Self {
        Self::new()
    }
}

/// Statistics about detector state
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DetectorStatistics {
    /// Number of ensemble signals in history
    pub signal_history_size: usize,

    /// Number of models being tracked
    pub model_count: usize,

    /// Total samples across all models
    pub total_samples: usize,
}

#[cfg(test)]
#[allow(clippy::field_reassign_with_default)]
mod tests {
    use super::*;
    use ml_ensemble::{ModelVote, TradingAction};

    fn create_test_decision(
        signal: f64,
        model_votes: HashMap<String, ModelVote>,
    ) -> EnsembleDecision {
        EnsembleDecision {
            signal,
            confidence: 0.8,
            action: TradingAction::Hold,
            disagreement_rate: 0.0,
            model_votes,
            timestamp: std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap_or_default()
                .as_micros() as u64,
            symbol: None,
            metadata: HashMap::new(),
        }
    }

    #[tokio::test]
    async fn test_sudden_shift_detection() {
        let detector = EnsembleAnomalyDetector::new();

        // Build history with stable predictions
        for _ in 0..10 {
            let decision = create_test_decision(0.1, HashMap::new());
            detector.update_history(&decision).await;
        }

        // Inject sudden shift
        let decision = create_test_decision(0.8, HashMap::new());
        let report = detector.detect_anomaly(&decision).await;

        assert!(report.has_anomalies);
        assert!(matches!(report.anomalies[0], Anomaly::SuddenShift { .. }));
    }

    #[tokio::test]
    async fn test_coordinated_attack_detection() {
        let detector = EnsembleAnomalyDetector::new();

        // Create decision with all models predicting extreme values
        let mut model_votes = HashMap::new();
        model_votes.insert(
            "model1".to_owned(),
            ModelVote {
                model_id: "model1".to_owned(),
                signal: 0.95,
                confidence: 0.9,
                weight: 0.33,
                model_type: "DQN".to_owned(),
            },
        );
        model_votes.insert(
            "model2".to_owned(),
            ModelVote {
                model_id: "model2".to_owned(),
                signal: 0.96,
                confidence: 0.9,
                weight: 0.33,
                model_type: "PPO".to_owned(),
            },
        );
        model_votes.insert(
            "model3".to_owned(),
            ModelVote {
                model_id: "model3".to_owned(),
                signal: 0.97,
                confidence: 0.9,
                weight: 0.34,
                model_type: "TFT".to_owned(),
            },
        );

        let decision = create_test_decision(0.96, model_votes);
        let report = detector.detect_anomaly(&decision).await;

        assert!(report.has_anomalies);
        assert!(matches!(
            report.anomalies[0],
            Anomaly::CoordinatedAttack { .. }
        ));
        assert_eq!(report.severity, AnomalySeverity::Critical);
    }

    #[tokio::test]
    async fn test_model_drift_detection() {
        let detector = EnsembleAnomalyDetector::new();

        // Build history with stable model behavior
        for _ in 0..20 {
            let mut model_votes = HashMap::new();
            model_votes.insert(
                "model1".to_owned(),
                ModelVote {
                    model_id: "model1".to_owned(),
                    signal: 0.1,
                    confidence: 0.8,
                    weight: 1.0,
                    model_type: "DQN".to_owned(),
                },
            );

            let decision = create_test_decision(0.1, model_votes);
            detector.update_history(&decision).await;
        }

        // Inject drift
        let mut model_votes = HashMap::new();
        model_votes.insert(
            "model1".to_owned(),
            ModelVote {
                model_id: "model1".to_owned(),
                signal: 0.9,
                confidence: 0.8,
                weight: 1.0,
                model_type: "DQN".to_owned(),
            },
        );

        let decision = create_test_decision(0.9, model_votes);
        let report = detector.detect_anomaly(&decision).await;

        assert!(report.has_anomalies);
        // Should detect both SuddenShift (0.1 -> 0.9) and ModelDrift
        // Check that at least one anomaly is ModelDrift
        assert!(
            report
                .anomalies
                .iter()
                .any(|a| matches!(a, Anomaly::ModelDrift { .. })),
            "Expected to find ModelDrift anomaly, but got: {:?}",
            report.anomalies
        );
    }

    #[tokio::test]
    async fn test_no_anomaly() {
        let detector = EnsembleAnomalyDetector::new();

        // Build history
        for _ in 0..10 {
            let decision = create_test_decision(0.5, HashMap::new());
            detector.update_history(&decision).await;
        }

        // Normal prediction
        let decision = create_test_decision(0.52, HashMap::new());
        let report = detector.detect_anomaly(&decision).await;

        assert!(!report.has_anomalies);
    }

    #[tokio::test]
    async fn test_severity_calculation() {
        let detector = EnsembleAnomalyDetector::new();

        // Test Critical (coordinated attack)
        let anomalies = vec![Anomaly::CoordinatedAttack {
            extreme_ratio: 0.9,
            affected_models: vec!["m1".to_owned(), "m2".to_owned()],
        }];
        assert_eq!(
            detector.calculate_severity(&anomalies),
            AnomalySeverity::Critical
        );

        // Test High (shift + multiple drifts)
        let anomalies = vec![
            Anomaly::SuddenShift {
                previous: 0.0,
                current: 0.8,
                magnitude: 0.8,
            },
            Anomaly::ModelDrift {
                model_id: "m1".to_owned(),
                historical_mean: 0.0,
                current_signal: 0.8,
                drift_magnitude: 0.8,
            },
            Anomaly::ModelDrift {
                model_id: "m2".to_owned(),
                historical_mean: 0.0,
                current_signal: 0.8,
                drift_magnitude: 0.8,
            },
        ];
        assert_eq!(
            detector.calculate_severity(&anomalies),
            AnomalySeverity::High
        );
    }

    #[tokio::test]
    async fn test_history_management() {
        let mut config = AnomalyDetectorConfig::default();
        config.signal_window_size = 5;
        config.model_window_size = 3;

        let detector = EnsembleAnomalyDetector::with_config(config);

        // Add more than window size
        for i in 0..10 {
            let mut model_votes = HashMap::new();
            model_votes.insert(
                "model1".to_owned(),
                ModelVote {
                    model_id: "model1".to_owned(),
                    signal: i as f64 / 10.0,
                    confidence: 0.8,
                    weight: 1.0,
                    model_type: "DQN".to_owned(),
                },
            );

            let decision = create_test_decision(i as f64 / 10.0, model_votes);
            detector.update_history(&decision).await;
        }

        let stats = detector.get_statistics().await;

        // Should maintain window size
        assert_eq!(stats.signal_history_size, 5);
        assert_eq!(stats.model_count, 1);
    }

    #[tokio::test]
    async fn test_reset_history() {
        let detector = EnsembleAnomalyDetector::new();

        // Add some data
        for i in 0..10 {
            let decision = create_test_decision(i as f64 / 10.0, HashMap::new());
            detector.update_history(&decision).await;
        }

        let stats_before = detector.get_statistics().await;
        assert!(stats_before.signal_history_size > 0);

        // Reset
        detector.reset_history().await;

        let stats_after = detector.get_statistics().await;
        assert_eq!(stats_after.signal_history_size, 0);
        assert_eq!(stats_after.model_count, 0);
    }
}
