//! Feature-based regime classifier using ADX, Hurst exponent, and volatility z-score.
//!
//! Classifies market regimes in real-time from observable features:
//! - **Trending**: ADX > threshold AND Hurst > trending threshold (persistent trend)
//! - **Volatile**: Volatility z-score > threshold (extreme volatility spike)
//! - **Ranging**: Hurst < ranging threshold AND vol z-score < 0 (mean-reverting)
//! - Fallback -> Ranging (most conservative default)

use std::collections::VecDeque;

use serde::{Deserialize, Serialize};

use ml_dqn::RegimeType;

/// Configuration for the feature-based regime classifier.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClassifierConfig {
    /// ADX value above which the market is considered trending (default: 25.0)
    pub adx_trending_threshold: f64,
    /// Hurst exponent above which trend persistence is confirmed (default: 0.55)
    pub hurst_trending_threshold: f64,
    /// Hurst exponent below which mean-reversion is confirmed (default: 0.45)
    pub hurst_ranging_threshold: f64,
    /// Volatility z-score above which the market is classified as volatile (default: 2.0)
    pub vol_zscore_threshold: f64,
    /// Size of the rolling volatility window for z-score computation (default: 100)
    pub vol_window: usize,
}

impl Default for ClassifierConfig {
    fn default() -> Self {
        Self {
            adx_trending_threshold: 25.0,
            hurst_trending_threshold: 0.55,
            hurst_ranging_threshold: 0.45,
            vol_zscore_threshold: 2.0,
            vol_window: 100,
        }
    }
}

/// Real-time feature-based regime classifier.
///
/// Maintains a rolling window of volatility observations for z-score computation
/// and classifies the current market regime from ADX, Hurst exponent, and
/// volatility features.
#[derive(Debug, Clone)]
pub struct FeatureClassifier {
    config: ClassifierConfig,
    vol_history: VecDeque<f64>,
}

impl FeatureClassifier {
    /// Create a new classifier with the given configuration.
    pub fn new(config: ClassifierConfig) -> Self {
        let vol_window = config.vol_window;
        Self {
            config,
            vol_history: VecDeque::with_capacity(vol_window),
        }
    }

    /// Push a new volatility observation into the rolling window.
    ///
    /// Older observations are evicted once the window is full.
    pub fn update_volatility(&mut self, vol: f64) {
        if self.vol_history.len() >= self.config.vol_window {
            self.vol_history.pop_front();
        }
        self.vol_history.push_back(vol);
    }

    /// Compute the z-score of the latest volatility observation against the
    /// rolling window. Returns `None` if fewer than 2 observations exist.
    fn vol_zscore(&self) -> Option<f64> {
        if self.vol_history.len() < 2 {
            return None;
        }
        let n = self.vol_history.len() as f64;
        let sum: f64 = self.vol_history.iter().sum();
        let mean = sum / n;
        let var: f64 = self.vol_history.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n;
        let std_dev = var.sqrt();

        if std_dev < f64::EPSILON {
            return Some(0.0);
        }

        let latest = self.vol_history.back()?;
        Some((latest - mean) / std_dev)
    }

    /// Classify the current market regime.
    ///
    /// # Arguments
    ///
    /// * `adx` - Average Directional Index (0-100)
    /// * `hurst` - Hurst exponent (0.0 - 1.0). H > 0.5 = persistent, H < 0.5 = mean-reverting
    ///
    /// # Classification Rules
    ///
    /// 1. **Volatile**: vol z-score > `vol_zscore_threshold` (checked first -- extreme vol overrides)
    /// 2. **Trending**: ADX > `adx_trending_threshold` AND Hurst > `hurst_trending_threshold`
    /// 3. **Ranging**: Hurst < `hurst_ranging_threshold` AND vol z-score < 0
    /// 4. Fallback: **Ranging** (most conservative)
    pub fn classify(&self, adx: f64, hurst: f64) -> RegimeType {
        let vol_z = self.vol_zscore().unwrap_or(0.0);

        // Volatile dominates -- extreme vol spikes override everything
        if vol_z > self.config.vol_zscore_threshold {
            return RegimeType::Volatile;
        }

        // Trending: strong directional movement + persistent autocorrelation
        if adx > self.config.adx_trending_threshold && hurst > self.config.hurst_trending_threshold
        {
            return RegimeType::Trending;
        }

        // Ranging: mean-reverting Hurst + below-average volatility
        if hurst < self.config.hurst_ranging_threshold && vol_z < 0.0 {
            return RegimeType::Ranging;
        }

        // Fallback: Ranging (most conservative default)
        RegimeType::Ranging
    }

    /// Get the current rolling volatility window length.
    pub fn vol_history_len(&self) -> usize {
        self.vol_history.len()
    }

    /// Get a reference to the classifier config.
    pub fn config(&self) -> &ClassifierConfig {
        &self.config
    }
}

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

    fn make_classifier(vol_window: usize) -> FeatureClassifier {
        FeatureClassifier::new(ClassifierConfig {
            vol_window,
            ..ClassifierConfig::default()
        })
    }

    /// Fill the rolling window with a constant value so mean = val, std = 0.
    fn fill_constant(classifier: &mut FeatureClassifier, val: f64, n: usize) {
        for _ in 0..n {
            classifier.update_volatility(val);
        }
    }

    #[test]
    fn test_trending_regime() {
        let mut c = make_classifier(20);
        // Fill with moderate volatility so z-score stays near 0
        fill_constant(&mut c, 0.02, 20);

        let regime = c.classify(30.0, 0.65);
        assert_eq!(regime, RegimeType::Trending, "ADX>25 + Hurst>0.55 = Trending");
    }

    #[test]
    fn test_ranging_regime() {
        let mut c = make_classifier(20);
        // Fill with moderate vol, then push a below-average observation
        for i in 0..20 {
            c.update_volatility(0.02 + (i as f64) * 0.001);
        }
        // Push a low-vol observation to get negative z-score
        c.update_volatility(0.005);

        let regime = c.classify(15.0, 0.40);
        assert_eq!(regime, RegimeType::Ranging, "Hurst<0.45 + vol_z<0 = Ranging");
    }

    #[test]
    fn test_volatile_regime() {
        let mut c = make_classifier(20);
        // Fill with low volatility
        fill_constant(&mut c, 0.01, 19);
        // Spike the last observation
        c.update_volatility(0.10);

        let regime = c.classify(10.0, 0.50);
        assert_eq!(
            regime,
            RegimeType::Volatile,
            "vol z-score > 2.0 = Volatile (overrides everything)"
        );
    }

    #[test]
    fn test_fallback_to_ranging() {
        let mut c = make_classifier(20);
        fill_constant(&mut c, 0.02, 20);

        // ADX below trending but Hurst above ranging threshold: neither trending nor ranging
        let regime = c.classify(20.0, 0.50);
        assert_eq!(regime, RegimeType::Ranging, "No strong signal = fallback Ranging");
    }

    #[test]
    fn test_volatile_overrides_trending() {
        let mut c = make_classifier(20);
        fill_constant(&mut c, 0.01, 19);
        c.update_volatility(0.10);

        // Even though ADX and Hurst say trending, vol spike should override
        let regime = c.classify(35.0, 0.70);
        assert_eq!(
            regime,
            RegimeType::Volatile,
            "Vol spike overrides trending classification"
        );
    }

    #[test]
    fn test_empty_vol_history_fallback() {
        let c = make_classifier(20);
        // No volatility data -- z-score defaults to 0.0
        let regime = c.classify(20.0, 0.50);
        assert_eq!(regime, RegimeType::Ranging, "Empty vol history = fallback Ranging");
    }

    #[test]
    fn test_single_observation_fallback() {
        let mut c = make_classifier(20);
        c.update_volatility(0.02);
        // Only 1 observation -- z-score defaults to 0.0 (need >= 2)
        let regime = c.classify(30.0, 0.65);
        assert_eq!(
            regime,
            RegimeType::Trending,
            "Single vol observation (z-score=0) allows trending classification"
        );
    }

    #[test]
    fn test_vol_window_eviction() {
        let mut c = make_classifier(5);
        for i in 0..10 {
            c.update_volatility(i as f64);
        }
        assert_eq!(c.vol_history_len(), 5, "Window should not grow beyond capacity");
    }

    #[test]
    fn test_zscore_zero_variance() {
        let mut c = make_classifier(10);
        fill_constant(&mut c, 0.05, 10);
        // All identical values -> std_dev ~ 0 -> z-score = 0
        let regime = c.classify(30.0, 0.60);
        assert_eq!(
            regime,
            RegimeType::Trending,
            "Zero-variance vol history: z-score=0, so trending if ADX+Hurst qualify"
        );
    }
}
