//! Rolling correlation monitor for cross-asset exposure limits.
//!
//! Maintains a rolling correlation matrix across traded symbols.
//! Pre-trade check computes effective exposure. Alerts on correlation breakdown.

use std::collections::HashMap;
use std::collections::VecDeque;

/// Rolling correlation monitor for cross-asset exposure limits.
pub struct CorrelationMonitor {
    returns: HashMap<String, VecDeque<f64>>,
    lookback: usize,
    max_effective_exposure: f64,
    correlation_breakdown_threshold: f64,
}

/// Correlation check result.
#[derive(Debug, Clone)]
pub struct CorrelationCheck {
    /// Pairwise correlations between all tracked symbols.
    pub pairwise: Vec<PairCorrelation>,
    /// Effective exposure (sum of absolute position * correlation weights).
    pub effective_exposure: f64,
    /// Whether the effective exposure exceeds the limit.
    pub exposure_exceeded: bool,
    /// Pairs where correlation changed significantly (breakdown).
    pub breakdowns: Vec<CorrelationBreakdown>,
}

/// Correlation between two symbols.
#[derive(Debug, Clone)]
pub struct PairCorrelation {
    pub symbol_a: String,
    pub symbol_b: String,
    pub correlation: f64,
}

/// Significant change in pairwise correlation.
#[derive(Debug, Clone)]
pub struct CorrelationBreakdown {
    pub symbol_a: String,
    pub symbol_b: String,
    pub old_correlation: f64,
    pub new_correlation: f64,
}

impl CorrelationMonitor {
    pub fn new(
        lookback: usize,
        max_effective_exposure: f64,
        correlation_breakdown_threshold: f64,
    ) -> Self {
        Self {
            returns: HashMap::new(),
            lookback,
            max_effective_exposure,
            correlation_breakdown_threshold,
        }
    }

    /// Add a return observation for a symbol.
    pub fn add_return(&mut self, symbol: &str, ret: f64) {
        let entry = self
            .returns
            .entry(symbol.to_owned())
            .or_insert_with(|| VecDeque::with_capacity(self.lookback + 1));
        entry.push_back(ret);
        if entry.len() > self.lookback {
            entry.pop_front();
        }
    }

    /// Compute Pearson correlation between two return series.
    fn pearson_correlation(a: &VecDeque<f64>, b: &VecDeque<f64>) -> f64 {
        let n = a.len().min(b.len());
        if n < 3 {
            return 0.0;
        }

        let mean_a: f64 = a.iter().take(n).sum::<f64>() / n as f64;
        let mean_b: f64 = b.iter().take(n).sum::<f64>() / n as f64;

        let mut cov = 0.0_f64;
        let mut var_a = 0.0_f64;
        let mut var_b = 0.0_f64;

        for i in 0..n {
            let da = a.get(i).copied().unwrap_or(0.0) - mean_a;
            let db = b.get(i).copied().unwrap_or(0.0) - mean_b;
            cov += da * db;
            var_a += da * da;
            var_b += db * db;
        }

        let denom = (var_a * var_b).sqrt();
        if denom < 1e-15 {
            0.0
        } else {
            (cov / denom).clamp(-1.0, 1.0)
        }
    }

    /// Get the correlation breakdown threshold.
    #[must_use]
    pub fn correlation_breakdown_threshold(&self) -> f64 {
        self.correlation_breakdown_threshold
    }

    /// Compute pairwise correlations and effective exposure.
    /// `positions` maps symbol to absolute position size.
    pub fn check(&self, positions: &HashMap<String, f64>) -> CorrelationCheck {
        let symbols: Vec<&String> = self.returns.keys().collect();
        let mut pairwise = Vec::new();

        for i in 0..symbols.len() {
            for j in (i + 1)..symbols.len() {
                let sym_a = symbols.get(i).copied();
                let sym_b = symbols.get(j).copied();
                if let (Some(a), Some(b)) = (sym_a, sym_b) {
                    if let (Some(ra), Some(rb)) = (self.returns.get(a), self.returns.get(b)) {
                        let corr = Self::pearson_correlation(ra, rb);
                        pairwise.push(PairCorrelation {
                            symbol_a: a.clone(),
                            symbol_b: b.clone(),
                            correlation: corr,
                        });
                    }
                }
            }
        }

        // Effective exposure: sum of |pos_i * pos_j * correlation_ij| / total_pos^2 * num_positions
        let total_pos: f64 = positions.values().map(|p| p.abs()).sum();
        let effective_exposure = if total_pos > 0.0 {
            let mut weighted_corr = 0.0_f64;
            for pair in &pairwise {
                let pos_a = positions.get(&pair.symbol_a).copied().unwrap_or(0.0).abs();
                let pos_b = positions.get(&pair.symbol_b).copied().unwrap_or(0.0).abs();
                weighted_corr += pos_a * pos_b * pair.correlation.abs();
            }
            weighted_corr / (total_pos * total_pos).max(1e-15) * positions.len() as f64
        } else {
            0.0
        };

        CorrelationCheck {
            exposure_exceeded: effective_exposure > self.max_effective_exposure,
            effective_exposure,
            breakdowns: Vec::new(), // Breakdowns require historical state; simplified for initial impl
            pairwise,
        }
    }
}

impl Default for CorrelationMonitor {
    fn default() -> Self {
        Self::new(60, 3.0, 0.5)
    }
}

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

    #[test]
    fn test_identical_series_correlation_near_one() {
        let mut monitor = CorrelationMonitor::new(60, 3.0, 0.5);
        for i in 0..30 {
            let ret = 0.01 * (i as f64).sin();
            monitor.add_return("EURUSD", ret);
            monitor.add_return("EURUSD_copy", ret);
        }
        let positions = [
            ("EURUSD".to_string(), 1000.0),
            ("EURUSD_copy".to_string(), 1000.0),
        ]
        .into_iter()
        .collect();
        let check = monitor.check(&positions);
        let pair = check.pairwise.first();
        assert!(pair.is_some());
        if let Some(p) = pair {
            assert!(
                (p.correlation - 1.0).abs() < 0.01,
                "Identical series should have correlation ~1.0, got {}",
                p.correlation
            );
        }
    }

    #[test]
    fn test_uncorrelated_series_near_zero() {
        let mut monitor = CorrelationMonitor::new(60, 3.0, 0.5);
        // Use deterministic but uncorrelated sequences
        for i in 0..100 {
            monitor.add_return("A", (i as f64 * 0.73).sin());
            monitor.add_return("B", (i as f64 * 2.41 + 5.0).cos());
        }
        let positions = [("A".to_string(), 1000.0), ("B".to_string(), 1000.0)]
            .into_iter()
            .collect();
        let check = monitor.check(&positions);
        let pair = check.pairwise.first();
        assert!(pair.is_some());
        if let Some(p) = pair {
            assert!(
                p.correlation.abs() < 0.5,
                "Uncorrelated series should have low correlation, got {}",
                p.correlation
            );
        }
    }

    #[test]
    fn test_lookback_window_enforced() {
        let mut monitor = CorrelationMonitor::new(10, 3.0, 0.5);
        for i in 0..20 {
            monitor.add_return("X", i as f64 * 0.01);
        }
        let returns = monitor.returns.get("X");
        assert!(returns.is_some());
        if let Some(r) = returns {
            assert_eq!(
                r.len(),
                10,
                "Should only keep lookback window entries"
            );
        }
    }

    #[test]
    fn test_effective_exposure_check() {
        let mut monitor = CorrelationMonitor::new(60, 1.5, 0.5);
        // Create highly correlated positions
        for i in 0..30 {
            let ret = 0.01 * (i as f64).sin();
            monitor.add_return("A", ret);
            monitor.add_return("B", ret);
        }
        let positions = [("A".to_string(), 10000.0), ("B".to_string(), 10000.0)]
            .into_iter()
            .collect();
        let check = monitor.check(&positions);
        // With high correlation and large positions, exposure should be notable
        assert!(
            check.effective_exposure > 0.0,
            "Effective exposure should be > 0 for correlated positions"
        );
    }

    #[test]
    fn test_default_monitor() {
        let monitor = CorrelationMonitor::default();
        assert_eq!(monitor.lookback, 60);
        assert!((monitor.max_effective_exposure - 3.0).abs() < f64::EPSILON);
        assert!((monitor.correlation_breakdown_threshold() - 0.5).abs() < f64::EPSILON);
    }

    #[test]
    fn test_empty_positions() {
        let monitor = CorrelationMonitor::new(60, 3.0, 0.5);
        let positions: HashMap<String, f64> = HashMap::new();
        let check = monitor.check(&positions);
        assert!(!check.exposure_exceeded);
        assert!((check.effective_exposure).abs() < f64::EPSILON);
    }

    #[test]
    fn test_insufficient_data_returns_zero_correlation() {
        let mut monitor = CorrelationMonitor::new(60, 3.0, 0.5);
        // Only 2 data points -- below minimum of 3
        monitor.add_return("A", 0.01);
        monitor.add_return("A", 0.02);
        monitor.add_return("B", 0.03);
        monitor.add_return("B", 0.04);
        let positions = [("A".to_string(), 1000.0), ("B".to_string(), 1000.0)]
            .into_iter()
            .collect();
        let check = monitor.check(&positions);
        if let Some(p) = check.pairwise.first() {
            assert!(
                p.correlation.abs() < f64::EPSILON,
                "With < 3 data points, correlation should be 0.0"
            );
        }
    }
}
