//! PAGES Test for Variance Changepoint Detection
//!
//! Implementation of Page's Test (one-sided CUSUM) for detecting changes in variance.
//! This test monitors the cumulative sum of log-likelihood ratios to detect shifts in
//! the variance of a time series. Particularly useful for regime detection in financial markets.
//!
//! ## Algorithm
//! - Page's statistic: Pₜ = max(0, Pₜ₋₁ + log(σ²ₜ/σ²₀) - k)
//! - Detection when: Pₜ > h
//! - k: drift allowance (reduces false positives)
//! - h: detection threshold (higher = fewer false alarms)
//!
//! ## Performance Target
//! - <80μs per update operation
//! - Memory efficient with rolling variance computation
//!
//! ## Usage
//! ```rust
//! use ml::regime::pages_test::PAGESTest;
//!
//! let mut pages = PAGESTest::new(1.0, 0.5, 5.0, 20);
//!
//! // Update with new values
//! if let Some(change) = pages.update(1.5)? {
//!     println!("Variance change detected at index {}", change.detection_index);
//! }
//! ```

use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;

/// Result of PAGES variance changepoint detection
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct VarianceChange {
    /// Index where variance change was detected
    pub detection_index: usize,

    /// Current Page's cumulative sum value (exceeds threshold)
    pub pages_statistic: f64,

    /// Current variance estimate
    pub current_variance: f64,

    /// Target variance being monitored against
    pub target_variance: f64,

    /// Variance ratio (current/target)
    pub variance_ratio: f64,
}

/// PAGES Test for detecting variance changes in time series
///
/// Uses one-sided CUSUM to monitor cumulative deviations from target variance.
/// Efficient online algorithm with O(1) update complexity.
#[derive(Debug)]
pub struct PAGESTest {
    /// Target variance (σ²₀) - baseline to compare against
    target_variance: f64,

    /// Drift allowance (k) - reduces false positives
    drift_allowance: f64,

    /// Detection threshold (h) - triggers alarm when exceeded
    detection_threshold: f64,

    /// Current Page's cumulative sum
    cumulative_sum: f64,

    /// Rolling window size for variance estimation
    window_size: usize,

    /// Recent values for rolling variance computation
    recent_values: VecDeque<f64>,

    /// Running sum for efficient mean computation
    running_sum: f64,

    /// Running sum of squares for efficient variance computation
    running_sum_squares: f64,

    /// Number of updates processed (for indexing)
    update_count: usize,
}

impl PAGESTest {
    /// Create a new PAGES test detector
    ///
    /// # Arguments
    /// - `target_variance`: Expected baseline variance (σ²₀)
    /// - `drift_allowance`: Drift parameter k (typical: 0.25 to 1.0)
    /// - `detection_threshold`: Alarm threshold h (typical: 4.0 to 8.0)
    /// - `window_size`: Rolling window for variance estimation (typical: 20-50)
    ///
    /// # Recommendations
    /// - For high-frequency trading: k=0.5, h=5.0, window=20
    /// - For daily data: k=1.0, h=8.0, window=50
    /// - Smaller k = more sensitive to small changes
    /// - Larger h = fewer false alarms
    pub fn new(
        target_variance: f64,
        drift_allowance: f64,
        detection_threshold: f64,
        window_size: usize,
    ) -> Self {
        assert!(target_variance > 0.0, "Target variance must be positive");
        assert!(
            drift_allowance >= 0.0,
            "Drift allowance must be non-negative"
        );
        assert!(
            detection_threshold > 0.0,
            "Detection threshold must be positive"
        );
        assert!(window_size >= 2, "Window size must be at least 2");

        Self {
            target_variance,
            drift_allowance,
            detection_threshold,
            cumulative_sum: 0.0,
            window_size,
            recent_values: VecDeque::with_capacity(window_size),
            running_sum: 0.0,
            running_sum_squares: 0.0,
            update_count: 0,
        }
    }

    /// Update PAGES test with new observation
    ///
    /// # Arguments
    /// - `value`: New observation
    ///
    /// # Returns
    /// - `Ok(Some(VarianceChange))` if variance change detected
    /// - `Ok(None)` if no change detected
    /// - `Err` if value is non-finite
    ///
    /// # Performance
    /// - Target: <80μs per call
    /// - O(1) time complexity with rolling statistics
    pub fn update(&mut self, value: f64) -> Result<Option<VarianceChange>> {
        if !value.is_finite() {
            anyhow::bail!("PAGES test received non-finite value: {}", value);
        }

        self.update_count += 1;

        // Add new value to rolling window
        self.recent_values.push_back(value);
        self.running_sum += value;
        self.running_sum_squares += value * value;

        // Remove oldest value if window full
        if self.recent_values.len() > self.window_size {
            if let Some(old_value) = self.recent_values.pop_front() {
                self.running_sum -= old_value;
                self.running_sum_squares -= old_value * old_value;
            }
        }

        // Need at least 2 values to compute variance
        if self.recent_values.len() < 2 {
            return Ok(None);
        }

        // Compute current variance using Welford's online algorithm
        let current_variance = self.get_current_variance();

        // Protect against division by zero or negative variance
        if current_variance <= 1e-10 {
            // Reset cumulative sum when variance is negligible
            self.cumulative_sum = 0.0;
            return Ok(None);
        }

        // Page's statistic: Pₜ = max(0, Pₜ₋₁ + log(σ²ₜ/σ²₀) - k)
        let variance_ratio = current_variance / self.target_variance;
        let log_likelihood_ratio = variance_ratio.ln();

        self.cumulative_sum =
            (self.cumulative_sum + log_likelihood_ratio - self.drift_allowance).max(0.0);

        // Check if alarm threshold exceeded
        if self.cumulative_sum > self.detection_threshold {
            let change = VarianceChange {
                detection_index: self.update_count,
                pages_statistic: self.cumulative_sum,
                current_variance,
                target_variance: self.target_variance,
                variance_ratio,
            };

            // Reset cumulative sum after detection
            self.cumulative_sum = 0.0;

            Ok(Some(change))
        } else {
            Ok(None)
        }
    }

    /// Reset PAGES test state
    ///
    /// Clears all accumulated state while preserving configuration parameters.
    /// Useful for starting fresh analysis on a new time series segment.
    pub fn reset(&mut self) {
        self.cumulative_sum = 0.0;
        self.recent_values.clear();
        self.running_sum = 0.0;
        self.running_sum_squares = 0.0;
        self.update_count = 0;
    }

    /// Get current variance estimate from rolling window
    ///
    /// Uses efficient online computation with running statistics.
    /// Formula: σ² = (Σx² - (Σx)²/n) / (n-1)
    pub fn get_current_variance(&self) -> f64 {
        let n = self.recent_values.len();
        if n < 2 {
            return 0.0;
        }

        let n_f64 = n as f64;
        let mean = self.running_sum / n_f64;

        // Variance = E[X²] - (E[X])²
        let variance = (self.running_sum_squares / n_f64) - (mean * mean);

        // Apply Bessel's correction (n-1 instead of n)
        let corrected_variance = variance * n_f64 / (n_f64 - 1.0);

        corrected_variance.max(0.0) // Protect against numerical errors
    }

    /// Get current Page's cumulative sum (for monitoring)
    pub fn get_cumulative_sum(&self) -> f64 {
        self.cumulative_sum
    }

    /// Get number of values in current rolling window
    pub fn get_window_fill(&self) -> usize {
        self.recent_values.len()
    }

    /// Get total number of updates processed
    pub fn get_update_count(&self) -> usize {
        self.update_count
    }

    /// Get target variance
    pub fn get_target_variance(&self) -> f64 {
        self.target_variance
    }

    /// Get detection threshold
    pub fn get_detection_threshold(&self) -> f64 {
        self.detection_threshold
    }

    /// Get drift allowance
    pub fn get_drift_allowance(&self) -> f64 {
        self.drift_allowance
    }
}

impl Default for PAGESTest {
    /// Create PAGES test with default parameters for HFT applications
    ///
    /// - target_variance: 1.0 (normalized)
    /// - drift_allowance: 0.5 (balanced sensitivity)
    /// - detection_threshold: 5.0 (moderate false alarm rate)
    /// - window_size: 20 (suitable for minute bars)
    fn default() -> Self {
        Self::new(1.0, 0.5, 5.0, 20)
    }
}

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

    #[test]
    fn test_pages_new_initialization() {
        let pages = PAGESTest::new(1.0, 0.5, 5.0, 20);
        assert_eq!(pages.get_target_variance(), 1.0);
        assert_eq!(pages.get_drift_allowance(), 0.5);
        assert_eq!(pages.get_detection_threshold(), 5.0);
        assert_eq!(pages.get_cumulative_sum(), 0.0);
        assert_eq!(pages.get_window_fill(), 0);
        assert_eq!(pages.get_update_count(), 0);
    }

    #[test]
    fn test_pages_stable_variance_no_detection() {
        let mut pages = PAGESTest::new(1.0, 0.5, 5.0, 20);

        // Feed values with variance ≈ 1.0 (stable)
        for _ in 0..50 {
            let value = rand::random::<f64>() * 2.0 - 1.0; // uniform [-1, 1], variance ≈ 1/3
            let result = pages.update(value).unwrap();
            assert!(
                result.is_none(),
                "Should not detect change in stable variance"
            );
        }
    }

    #[test]
    fn test_pages_variance_increase_detection() {
        let mut pages = PAGESTest::new(1.0, 0.5, 5.0, 20);

        // Phase 1: Stable variance ≈ 1.0
        for i in 0..30 {
            let value = if i % 2 == 0 { 1.0 } else { -1.0 };
            pages.update(value).unwrap();
        }

        // Phase 2: Increased variance (3x larger swings)
        let mut detected = false;
        for i in 0..50 {
            let value = if i % 2 == 0 { 3.0 } else { -3.0 };
            if let Some(change) = pages.update(value).unwrap() {
                detected = true;
                assert!(
                    change.variance_ratio > 1.0,
                    "Should detect variance increase"
                );
                assert!(change.pages_statistic > pages.get_detection_threshold());
                break;
            }
        }

        assert!(
            detected,
            "Should detect variance increase within 50 samples"
        );
    }

    #[test]
    fn test_pages_reset() {
        let mut pages = PAGESTest::new(1.0, 0.5, 5.0, 20);

        // Add some values
        for i in 0..10 {
            pages.update(i as f64).unwrap();
        }

        assert!(pages.get_window_fill() > 0);
        assert!(pages.get_update_count() > 0);

        // Reset
        pages.reset();

        assert_eq!(pages.get_window_fill(), 0);
        assert_eq!(pages.get_update_count(), 0);
        assert_eq!(pages.get_cumulative_sum(), 0.0);
    }

    #[test]
    fn test_pages_non_finite_value() {
        let mut pages = PAGESTest::new(1.0, 0.5, 5.0, 20);

        assert!(pages.update(f64::NAN).is_err());
        assert!(pages.update(f64::INFINITY).is_err());
        assert!(pages.update(f64::NEG_INFINITY).is_err());
    }
}
