//! Multi-CUSUM Detector
//!
//! Implements parallel CUSUM monitoring across multiple features simultaneously:
//! - Independent CUSUM for each feature dimension (returns, volatility, volume)
//! - Combined detection with configurable modes (ANY, ALL, WEIGHTED_VOTE)
//! - Feature weighting based on importance
//!
//! Use case: Detect structural breaks by monitoring multiple market characteristics
//! simultaneously (e.g., sudden changes in returns + volatility + volume).

use super::cusum::{CUSUMDetector, StructuralBreak};
use serde::{Deserialize, Serialize};
use tracing::warn;

/// Configuration for a single CUSUM detector
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CUSUMConfig {
    /// Detection threshold (h parameter, typically 4-5σ)
    pub threshold: f64,
    /// Baseline mean (μ parameter)
    pub baseline_mean: f64,
    /// Baseline standard deviation (σ parameter)
    pub baseline_std: f64,
    /// Minimum bars between detections (debounce)
    pub min_bars_between: usize,
}

impl Default for CUSUMConfig {
    fn default() -> Self {
        Self {
            threshold: 4.0,
            baseline_mean: 0.0,
            baseline_std: 1.0,
            min_bars_between: 20,
        }
    }
}

/// Detection mode for multi-feature monitoring
/// Note: Cannot derive Eq because f64 doesn't implement Eq (floating point equality is non-transitive)
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[derive(Default)]
pub enum DetectionMode {
    /// Any feature triggers detection
    #[default]
    Any,
    /// All features must trigger detection
    All,
    /// Weighted vote exceeds threshold
    WeightedVote { threshold: f64 },
}


/// Multi-feature breakpoint result
#[derive(Debug, Clone, PartialEq)]
pub struct MultiBreak {
    /// Bar index where break was detected
    pub bar_index: usize,
    /// Features that triggered detection (feature index)
    pub triggered_features: Vec<usize>,
    /// Individual breakpoints for each triggered feature
    pub breakpoints: Vec<StructuralBreak>,
    /// Combined detection score (for weighted mode)
    pub detection_score: f64,
}

/// Multi-CUSUM Detector for parallel feature monitoring
#[derive(Debug, Clone)]
pub struct MultiCUSUM {
    /// Independent CUSUM detectors for each feature
    cusum_detectors: Vec<CUSUMDetector>,
    /// Feature importance weights (must sum to 1.0)
    feature_weights: Vec<f64>,
    /// Detection mode
    detection_mode: DetectionMode,
    /// Total bars processed
    total_bars: usize,
    /// Last detected multi-break (if any)
    last_break: Option<MultiBreak>,
    /// Configuration for each feature (for baseline updates)
    configs: Vec<CUSUMConfig>,
}

impl MultiCUSUM {
    /// Create a new Multi-CUSUM detector
    ///
    /// # Arguments
    /// * `feature_configs` - CUSUM configuration for each feature
    /// * `feature_weights` - Importance weights for each feature (must sum to ~1.0)
    /// * `detection_mode` - How to combine feature detections
    ///
    /// # Returns
    /// * `Ok(MultiCUSUM)` if configuration is valid
    /// * `Err(String)` if validation fails
    pub fn new(
        feature_configs: Vec<CUSUMConfig>,
        feature_weights: Vec<f64>,
        detection_mode: DetectionMode,
    ) -> Result<Self, String> {
        if feature_configs.is_empty() {
            return Err("At least one feature required".to_owned());
        }

        if feature_configs.len() != feature_weights.len() {
            return Err(format!(
                "Feature count mismatch: {} configs vs {} weights",
                feature_configs.len(),
                feature_weights.len()
            ));
        }

        // Validate weights sum to ~1.0 (within tolerance)
        let weight_sum: f64 = feature_weights.iter().sum();
        if (weight_sum - 1.0).abs() > 0.01 {
            return Err(format!(
                "Feature weights must sum to 1.0 (got {})",
                weight_sum
            ));
        }

        // Validate all weights are non-negative
        if feature_weights.iter().any(|&w| w < 0.0) {
            return Err("Feature weights must be non-negative".to_owned());
        }

        // Create independent CUSUM detectors
        // drift_allowance = 0.5 (standard value)
        let cusum_detectors = feature_configs
            .iter()
            .map(|config| {
                CUSUMDetector::new(
                    config.baseline_mean,
                    config.baseline_std,
                    0.5, // drift_allowance (k parameter)
                    config.threshold,
                )
            })
            .collect();

        Ok(Self {
            cusum_detectors,
            feature_weights,
            detection_mode,
            total_bars: 0,
            last_break: None,
            configs: feature_configs,
        })
    }

    /// Update all CUSUM detectors with new feature values
    ///
    /// # Arguments
    /// * `features` - Feature values for current bar (must match detector count)
    /// * `bar_index` - Current bar index for tracking
    ///
    /// # Returns
    /// * `Some(MultiBreak)` if structural break detected (per detection mode)
    /// * `None` otherwise
    pub fn update(&mut self, features: &[f64], bar_index: usize) -> Option<MultiBreak> {
        if features.len() != self.cusum_detectors.len() {
            warn!(
                expected = self.cusum_detectors.len(),
                got = features.len(),
                "Feature count mismatch"
            );
            return None;
        }

        self.total_bars += 1;

        // Update all detectors and collect triggered features
        let mut triggered_features = Vec::new();
        let mut breakpoints = Vec::new();

        for (i, (detector, &value)) in self
            .cusum_detectors
            .iter_mut()
            .zip(features.iter())
            .enumerate()
        {
            if let Some(break_point) = detector.update(value) {
                // Respect min_bars_between debounce
                if detector.observations_since_reset() >= self.configs[i].min_bars_between {
                    triggered_features.push(i);
                    breakpoints.push(break_point);
                    detector.reset(); // Reset after detection
                }
            }
        }

        // Apply detection logic based on mode
        let detection_result = match self.detection_mode {
            DetectionMode::Any => {
                (!triggered_features.is_empty()).then_some(1.0) // Score = 1.0 for ANY mode
            },
            DetectionMode::All => {
                (triggered_features.len() == self.cusum_detectors.len()).then_some(1.0) // Score = 1.0 for ALL mode
            },
            DetectionMode::WeightedVote { threshold } => {
                // Calculate weighted score
                let score: f64 = triggered_features
                    .iter()
                    .map(|&i| self.feature_weights[i])
                    .sum();

                (score >= threshold).then_some(score)
            },
        };

        if let Some(score) = detection_result {
            let multi_break = MultiBreak {
                bar_index,
                triggered_features: triggered_features.clone(),
                breakpoints,
                detection_score: score,
            };
            self.last_break = Some(multi_break.clone());
            Some(multi_break)
        } else {
            None
        }
    }

    /// Get status of all CUSUM detectors
    pub fn get_feature_statuses(&self) -> Vec<CUSUMStatus> {
        self.cusum_detectors
            .iter()
            .enumerate()
            .map(|(i, detector)| {
                let (cumsum_pos, cumsum_neg) = detector.get_current_sums();
                CUSUMStatus {
                    cumsum_pos,
                    cumsum_neg,
                    bars_since_reset: detector.observations_since_reset(),
                    total_bars: self.total_bars,
                    last_break: None, // Not tracking individual breaks
                    feature_index: i,
                }
            })
            .collect()
    }

    /// Get last detected multi-break (if any)
    pub fn last_break(&self) -> Option<&MultiBreak> {
        self.last_break.as_ref()
    }

    /// Get number of features being monitored
    pub fn feature_count(&self) -> usize {
        self.cusum_detectors.len()
    }

    /// Get feature weights
    pub fn feature_weights(&self) -> &[f64] {
        &self.feature_weights
    }

    /// Get detection mode
    pub fn detection_mode(&self) -> DetectionMode {
        self.detection_mode
    }

    /// Get total bars processed
    pub fn total_bars(&self) -> usize {
        self.total_bars
    }

    /// Update baseline parameters for a specific feature
    pub fn update_feature_baseline(&mut self, feature_index: usize, mean: f64, std: f64) {
        if feature_index < self.configs.len() {
            self.configs[feature_index].baseline_mean = mean;
            self.configs[feature_index].baseline_std = std;

            // Recreate the detector with new baseline
            self.cusum_detectors[feature_index] = CUSUMDetector::new(
                mean,
                std,
                0.5, // drift_allowance
                self.configs[feature_index].threshold,
            );
        }
    }
}

/// CUSUM detector status for multi-feature monitoring
#[derive(Debug, Clone)]
pub struct CUSUMStatus {
    /// Current positive cumulative sum
    pub cumsum_pos: f64,
    /// Current negative cumulative sum
    pub cumsum_neg: f64,
    /// Bars since last reset
    pub bars_since_reset: usize,
    /// Total bars processed
    pub total_bars: usize,
    /// Last detected breakpoint (if any)
    pub last_break: Option<StructuralBreak>,
    /// Feature index
    pub feature_index: usize,
}

#[cfg(test)]
#[allow(clippy::assertions_on_result_states, clippy::redundant_clone)]
mod tests {
    use super::*;

    fn create_test_configs(n_features: usize) -> Vec<CUSUMConfig> {
        (0..n_features)
            .map(|i| CUSUMConfig {
                threshold: 3.0 + i as f64 * 0.5,
                baseline_mean: 0.0,
                baseline_std: 0.01,
                min_bars_between: 10,
            })
            .collect()
    }

    #[test]
    fn test_multi_cusum_creation() {
        let configs = create_test_configs(3);
        let weights = vec![0.5, 0.3, 0.2];
        let detector = MultiCUSUM::new(configs, weights, DetectionMode::Any);

        assert!(detector.is_ok());
        let detector = detector.unwrap();
        assert_eq!(detector.feature_count(), 3);
    }

    #[test]
    fn test_multi_cusum_weight_validation() {
        let configs = create_test_configs(3);

        // Weights don't sum to 1.0
        let bad_weights = vec![0.3, 0.3, 0.3];
        let result = MultiCUSUM::new(configs.clone(), bad_weights, DetectionMode::Any);
        assert!(result.is_err());

        // Negative weights
        let bad_weights = vec![0.5, 0.6, -0.1];
        let result = MultiCUSUM::new(configs.clone(), bad_weights, DetectionMode::Any);
        assert!(result.is_err());
    }

    #[test]
    fn test_detection_mode_any() {
        let configs = create_test_configs(3);
        let weights = vec![0.5, 0.3, 0.2];
        let mut detector = MultiCUSUM::new(configs, weights, DetectionMode::Any).unwrap();

        // Stable data
        for i in 0..50 {
            let features = vec![0.0, 0.0, 0.0];
            assert!(detector.update(&features, i).is_none());
        }

        // Trigger first feature only
        let mut detected = false;
        for i in 50..100 {
            let features = vec![0.05, 0.0, 0.0]; // First feature breaks
            if let Some(multi_break) = detector.update(&features, i) {
                assert_eq!(multi_break.triggered_features.len(), 1);
                assert_eq!(multi_break.triggered_features[0], 0);
                detected = true;
                break;
            }
        }

        assert!(detected, "ANY mode should detect when one feature triggers");
    }

    #[test]
    fn test_detection_mode_weighted_vote() {
        let configs = create_test_configs(3);
        let weights = vec![0.5, 0.3, 0.2];
        let mode = DetectionMode::WeightedVote { threshold: 0.6 };
        let mut detector = MultiCUSUM::new(configs, weights, mode).unwrap();

        // Stable data
        for i in 0..50 {
            let features = vec![0.0, 0.0, 0.0];
            assert!(detector.update(&features, i).is_none());
        }

        // Trigger only volume (weight=0.2, below 0.6 threshold)
        for i in 50..80 {
            let features = vec![0.0, 0.0, 0.05];
            assert!(
                detector.update(&features, i).is_none(),
                "Should not detect when score < threshold"
            );
        }

        // Trigger returns + volatility (0.5 + 0.3 = 0.8 > 0.6)
        let mut detected = false;
        for i in 80..150 {
            let features = vec![0.05, 0.05, 0.0];
            if let Some(multi_break) = detector.update(&features, i) {
                assert!(multi_break.detection_score >= 0.6);
                detected = true;
                break;
            }
        }

        assert!(
            detected,
            "Weighted vote should detect when score >= threshold"
        );
    }

    #[test]
    fn test_empty_features() {
        let configs = Vec::new();
        let weights = Vec::new();
        let result = MultiCUSUM::new(configs, weights, DetectionMode::Any);
        assert!(result.is_err(), "Should reject empty feature list");
    }
}
