//! Regime Detection Models for Market State Identification
//!
//! Implements regime detection algorithms to identify different market states
//! and adapt ML models accordingly.
//!
//! ## Sub-modules
//!
//! - [`feature_classifier`] -- Real-time regime classification using ADX, Hurst exponent,
//!   and volatility z-score thresholds.
//! - [`hmm`] -- Offline 3-state Gaussian Hidden Markov Model for regime transition estimation.

#![deny(clippy::unwrap_used, clippy::expect_used)]
#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
#![allow(dead_code)]
#![allow(missing_docs)]
#![allow(missing_debug_implementations)]
#![allow(unused_crate_dependencies)]
#![allow(clippy::float_arithmetic)]
#![allow(clippy::partial_pub_fields)]
#![allow(clippy::shadow_reuse)]
#![allow(clippy::shadow_unrelated)]
#![allow(clippy::shadow_same)]
#![allow(clippy::doc_markdown)]
#![allow(clippy::indexing_slicing)]
#![allow(clippy::missing_const_for_fn)]
#![allow(clippy::module_name_repetitions)]
#![allow(clippy::integer_division)]
#![allow(clippy::similar_names)]
#![allow(clippy::too_many_lines)]
#![allow(clippy::as_conversions)]
#![allow(clippy::cast_precision_loss)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::default_numeric_fallback)]
#![allow(clippy::arithmetic_side_effects)]

pub mod feature_classifier;
pub mod hmm;

use serde::{Deserialize, Serialize};

use ml_core::MLError;
use ml_dqn::RegimeType;

pub use feature_classifier::{ClassifierConfig, FeatureClassifier};
pub use hmm::{HMMConfig, HiddenMarkovModel};

/// Configuration for regime detection
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegimeDetectionConfig {
    pub window_size: usize,
    pub min_regime_duration: usize,
    pub threshold: f64,
}

impl Default for RegimeDetectionConfig {
    fn default() -> Self {
        Self {
            window_size: 100,
            min_regime_duration: 10,
            threshold: 0.05,
        }
    }
}

/// Regime detection engine combining feature-based classification and HMM.
///
/// The engine accumulates feature data via [`update_features`](Self::update_features)
/// and uses a [`FeatureClassifier`] to classify the current regime from the most
/// recent ADX, Hurst exponent, and volatility observations.
///
/// Feature vector layout (expected indices):
/// - Index 0: ADX (Average Directional Index)
/// - Index 1: Hurst exponent
/// - Index 2: Current volatility
///
/// For legacy compatibility, arbitrary feature slices are also accepted and
/// accumulated; the classifier only uses the first 3 elements when present.
#[derive(Debug)]
pub struct RegimeDetectionEngine {
    pub total_updates: u64,
    pub feature_data: Vec<f64>,
    config: RegimeDetectionConfig,
    classifier: FeatureClassifier,
    /// Most recent ADX value (updated via `update_features`)
    last_adx: f64,
    /// Most recent Hurst exponent (updated via `update_features`)
    last_hurst: f64,
}

impl RegimeDetectionEngine {
    /// Create a new regime detection engine.
    pub fn new(config: RegimeDetectionConfig) -> Result<Self, MLError> {
        let classifier_config = ClassifierConfig {
            vol_window: config.window_size,
            ..ClassifierConfig::default()
        };
        let classifier = FeatureClassifier::new(classifier_config);

        Ok(Self {
            total_updates: 0,
            feature_data: Vec::new(),
            config,
            classifier,
            last_adx: 0.0,
            last_hurst: 0.5, // neutral default
        })
    }

    /// Create engine with custom classifier configuration.
    pub fn with_classifier_config(
        config: RegimeDetectionConfig,
        classifier_config: ClassifierConfig,
    ) -> Result<Self, MLError> {
        let classifier = FeatureClassifier::new(classifier_config);

        Ok(Self {
            total_updates: 0,
            feature_data: Vec::new(),
            config,
            classifier,
            last_adx: 0.0,
            last_hurst: 0.5,
        })
    }

    /// Update the engine with new feature observations.
    ///
    /// Expected feature layout:
    /// - `features[0]` = ADX
    /// - `features[1]` = Hurst exponent
    /// - `features[2]` = Current volatility
    ///
    /// If fewer elements are provided, only the available ones are used.
    pub fn update_features(&mut self, features: &[f64]) -> Result<(), MLError> {
        self.feature_data.extend_from_slice(features);
        self.total_updates += 1;

        // Cap feature_data to avoid unbounded growth in long-running sessions.
        // Keep the most recent `window_size * 3` entries (3 features per update).
        let max_len = self.config.window_size * 3;
        if self.feature_data.len() > max_len * 2 {
            let drain_to = self.feature_data.len() - max_len;
            self.feature_data.drain(..drain_to);
        }

        // Extract structured features when available
        if let Some(&adx) = features.first() {
            self.last_adx = adx;
        }
        if let Some(&hurst) = features.get(1) {
            self.last_hurst = hurst;
        }
        if let Some(&vol) = features.get(2) {
            self.classifier.update_volatility(vol);
        }

        Ok(())
    }

    /// Detect the current market regime using the feature classifier.
    ///
    /// Returns a [`RegimeType`] based on accumulated ADX, Hurst exponent,
    /// and volatility data.
    pub fn detect_regime(&self) -> Result<RegimeType, MLError> {
        Ok(self.classifier.classify(self.last_adx, self.last_hurst))
    }

    /// Get a reference to the internal feature classifier.
    pub fn classifier(&self) -> &FeatureClassifier {
        &self.classifier
    }

    /// Get the engine configuration.
    pub fn config(&self) -> &RegimeDetectionConfig {
        &self.config
    }
}

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

    #[tokio::test]
    async fn test_regime_detection_engine_creation() -> Result<(), Box<dyn std::error::Error>> {
        let config = RegimeDetectionConfig::default();
        let engine = RegimeDetectionEngine::new(config)?;

        assert_eq!(engine.total_updates, 0);
        assert!(engine.feature_data.is_empty());

        Ok(())
    }

    #[tokio::test]
    async fn test_feature_data_update() -> Result<(), Box<dyn std::error::Error>> {
        let mut engine = RegimeDetectionEngine::new(RegimeDetectionConfig::default())?;

        let features = vec![0.1, 0.01];
        let result = engine.update_features(&features);

        assert!(result.is_ok());
        assert_eq!(engine.total_updates, 1);
        assert_eq!(engine.feature_data.len(), 2);

        Ok(())
    }

    #[tokio::test]
    async fn test_regime_detection_default_ranging() -> Result<(), Box<dyn std::error::Error>> {
        let engine = RegimeDetectionEngine::new(RegimeDetectionConfig::default())?;

        // No features -> ADX=0, Hurst=0.5, no vol -> Ranging fallback
        let regime = engine.detect_regime()?;
        assert_eq!(regime, RegimeType::Ranging);

        Ok(())
    }

    #[tokio::test]
    async fn test_regime_detection_trending() -> Result<(), Box<dyn std::error::Error>> {
        let mut engine = RegimeDetectionEngine::new(RegimeDetectionConfig::default())?;

        // Feed trending features: ADX=30, Hurst=0.65, vol=0.02
        for _ in 0..20 {
            engine.update_features(&[30.0, 0.65, 0.02])?;
        }

        let regime = engine.detect_regime()?;
        assert_eq!(regime, RegimeType::Trending);

        Ok(())
    }

    #[tokio::test]
    async fn test_regime_detection_volatile() -> Result<(), Box<dyn std::error::Error>> {
        let mut engine = RegimeDetectionEngine::new(RegimeDetectionConfig::default())?;

        // Fill with low vol first
        for _ in 0..50 {
            engine.update_features(&[10.0, 0.50, 0.01])?;
        }
        // Spike volatility
        engine.update_features(&[10.0, 0.50, 0.50])?;

        let regime = engine.detect_regime()?;
        assert_eq!(regime, RegimeType::Volatile);

        Ok(())
    }

    #[test]
    fn test_config_defaults() {
        let config = RegimeDetectionConfig::default();

        assert_eq!(config.window_size, 100);
        assert_eq!(config.min_regime_duration, 10);
        assert_eq!(config.threshold, 0.05);
    }

    #[test]
    fn test_config_serialization() {
        let config = RegimeDetectionConfig::default();

        let serialized =
            serde_json::to_string(&config).unwrap_or_else(|_| "{}".to_owned());
        let deserialized: RegimeDetectionConfig =
            serde_json::from_str(&serialized).unwrap_or_default();

        assert_eq!(config.window_size, deserialized.window_size);
        assert_eq!(config.min_regime_duration, deserialized.min_regime_duration);
        assert!(config.threshold - deserialized.threshold < f64::EPSILON);
    }

    #[test]
    fn test_with_classifier_config() {
        let regime_config = RegimeDetectionConfig::default();
        let classifier_config = ClassifierConfig {
            adx_trending_threshold: 30.0,
            hurst_trending_threshold: 0.60,
            ..ClassifierConfig::default()
        };
        let engine =
            RegimeDetectionEngine::with_classifier_config(regime_config, classifier_config);
        assert!(engine.is_ok());
        let engine = engine.unwrap_or_else(|_| {
            RegimeDetectionEngine::new(RegimeDetectionConfig::default())
                .unwrap_or_else(|_| unreachable!())
        });
        assert_eq!(engine.classifier().config().adx_trending_threshold, 30.0);
    }
}
