//! Regime Orchestrator
//!
//! Wires CUSUM structural break detection to regime state changes and database persistence.
//! This is the central coordinator that:
//! 1. Detects structural breaks using CUSUM
//! 2. Classifies regimes (Trending, Ranging, Volatile, Transition)
//! 3. Calculates confidence from ADX
//! 4. Persists regime states to database
//! 5. Updates transition matrix
//!
//! ## Architecture
//!
//! ```text
//! CUSUM Breaks → Regime Classifiers → Database Persistence
//!                    ↓                        ↓
//!                ADX Confidence      Transition Matrix
//! ```
//!
//! ## Usage Example
//!
//! ```rust,no_run
//! use ml::regime::orchestrator::RegimeOrchestrator;
//! use sqlx::PgPool;
//!
//! # async fn example(pool: PgPool) -> Result<(), Box<dyn std::error::Error>> {
//! let mut orchestrator = RegimeOrchestrator::new(pool).await?;
//!
//! // Process market data
//! let bars = vec![/* OHLCV bars */];
//! let regime_state = orchestrator.detect_and_persist("ES.FUT", &bars).await?;
//!
//! println!("Regime: {}, Confidence: {:.2}", regime_state.regime, regime_state.confidence);
//! # Ok(())
//! # }
//! ```

use crate::{
    cusum::CUSUMDetector,
    ranging::RangingClassifier,
    trending::{TrendingClassifier, TrendingSignal},
    volatile::{VolatileClassifier, VolatileSignal},
};
use crate::OHLCVBar;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use std::collections::HashMap;
use thiserror::Error;

/// Errors that can occur during regime orchestration
#[derive(Debug, Error)]
pub enum OrchestratorError {
    /// Database operation failed
    #[error("Database error: {0}")]
    Database(#[from] sqlx::Error),

    /// Insufficient data for regime detection
    #[error("Insufficient data: need at least {required} bars, got {actual}")]
    InsufficientData { required: usize, actual: usize },

    /// Configuration error
    #[error("Configuration error: {0}")]
    Configuration(String),

    /// Regime detection failed
    #[error("Regime detection failed: {0}")]
    DetectionFailed(String),
}

/// Regime state output
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegimeState {
    /// Regime classification
    pub regime: String,
    /// Confidence score (0.0-1.0)
    pub confidence: f64,
    /// Timestamp of regime detection
    pub timestamp: DateTime<Utc>,
    /// CUSUM positive sum
    pub cusum_s_plus: Option<f64>,
    /// CUSUM negative sum
    pub cusum_s_minus: Option<f64>,
    /// ADX value
    pub adx: Option<f64>,
    /// Regime stability
    pub stability: Option<f64>,
}

/// Regime Orchestrator - Central coordinator for regime detection
///
/// Coordinates structural break detection, regime classification, and database persistence.
/// Uses CUSUM to detect breaks, then classifies regimes using Trending, Ranging, and
/// Volatile detectors.
#[derive(Debug)]
pub struct RegimeOrchestrator {
    /// CUSUM structural break detector
    cusum: CUSUMDetector,

    /// Trending regime classifier
    trending_classifier: TrendingClassifier,

    /// Ranging regime classifier
    ranging_classifier: RangingClassifier,

    /// Volatile regime classifier
    volatile_classifier: VolatileClassifier,

    /// Database connection pool
    db_pool: PgPool,

    /// Cached regime states per symbol
    cached_regimes: HashMap<String, RegimeState>,

    /// Tracks the bar index at which the current regime started, per symbol.
    /// Used to compute transition duration_bars when a regime change occurs.
    regime_start_bars: HashMap<String, i64>,

    /// Tracks the cumulative bar count processed per symbol.
    /// Incremented each time detect_and_persist is called for a symbol.
    cumulative_bars: HashMap<String, i64>,

    /// Minimum bars required for detection
    min_bars: usize,
}

impl RegimeOrchestrator {
    /// Create a new regime orchestrator
    ///
    /// # Arguments
    ///
    /// * `pool` - PostgreSQL connection pool
    ///
    /// # Returns
    ///
    /// New orchestrator with default detector configurations
    ///
    /// # Errors
    ///
    /// Returns error if database pool creation fails
    pub async fn new(pool: PgPool) -> Result<Self, OrchestratorError> {
        let db_pool = pool;
        // Initialize detectors with default parameters
        let cusum = CUSUMDetector::new(
            0.0, // target_mean
            1.0, // target_std
            0.5, // drift_allowance (k = 0.5σ)
            5.0, // detection_threshold (h = 5σ)
        );

        let trending_classifier = TrendingClassifier::new(
            25.0, // ADX threshold
            0.55, // Hurst threshold
            50,   // lookback period
        );

        let ranging_classifier = RangingClassifier::new(
            20,   // Bollinger period
            2.0,  // Bollinger std
            20.0, // ADX threshold
        );

        let volatile_classifier = VolatileClassifier::new(
            1.5,  // Parkinson threshold multiplier
            0.03, // Garman-Klass threshold
            2.0,  // ATR expansion multiplier
            50,   // lookback period
        );

        Ok(Self {
            cusum,
            trending_classifier,
            ranging_classifier,
            volatile_classifier,
            db_pool,
            cached_regimes: HashMap::new(),
            regime_start_bars: HashMap::new(),
            cumulative_bars: HashMap::new(),
            min_bars: 20, // Minimum bars for statistical significance
        })
    }

    /// Create orchestrator with custom detector configurations
    ///
    /// # Arguments
    ///
    /// * `pool` - PostgreSQL connection pool
    /// * `cusum_threshold` - CUSUM detection threshold (h parameter)
    /// * `adx_threshold` - ADX threshold for trending detection
    /// * `lookback_period` - Lookback period for regime classifiers
    ///
    /// # Errors
    ///
    /// Returns error if database pool creation fails
    pub async fn with_config(
        pool: PgPool,
        cusum_threshold: f64,
        adx_threshold: f64,
        lookback_period: usize,
    ) -> Result<Self, OrchestratorError> {
        let db_pool = pool;
        let cusum = CUSUMDetector::new(0.0, 1.0, 0.5, cusum_threshold);
        let trending_classifier = TrendingClassifier::new(adx_threshold, 0.55, lookback_period);
        let ranging_classifier = RangingClassifier::new(20, 2.0, adx_threshold);
        let volatile_classifier = VolatileClassifier::new(1.5, 0.03, 2.0, lookback_period);

        Ok(Self {
            cusum,
            trending_classifier,
            ranging_classifier,
            volatile_classifier,
            db_pool,
            cached_regimes: HashMap::new(),
            regime_start_bars: HashMap::new(),
            cumulative_bars: HashMap::new(),
            min_bars: 20,
        })
    }

    /// Detect regime and persist to database
    ///
    /// # Algorithm
    ///
    /// 1. Run CUSUM to detect structural breaks
    /// 2. If break detected, query regime classifiers:
    ///    - Trending (ADX + Hurst)
    ///    - Ranging (Bollinger oscillation + variance ratio)
    ///    - Volatile (Parkinson/Garman-Klass volatility)
    /// 3. Determine regime based on classifier signals
    /// 4. Calculate confidence from ADX (normalized to 0-1)
    /// 5. Persist to database (regime_states table)
    /// 6. Record transition (regime_transitions table)
    ///
    /// # Arguments
    ///
    /// * `symbol` - Trading symbol (e.g., "ES.FUT")
    /// * `bars` - OHLCV bars for analysis
    ///
    /// # Returns
    ///
    /// Current regime state with confidence
    ///
    /// # Errors
    ///
    /// Returns error if:
    /// - Insufficient data (< 20 bars)
    /// - Database persistence fails
    /// - Regime detection fails
    pub async fn detect_and_persist(
        &mut self,
        symbol: &str,
        bars: &[OHLCVBar],
    ) -> Result<RegimeState, OrchestratorError> {
        // Validate input
        if bars.len() < self.min_bars {
            return Err(OrchestratorError::InsufficientData {
                required: self.min_bars,
                actual: bars.len(),
            });
        }

        // Get cached regime (previous state)
        let prev_regime = self.cached_regimes.get(symbol).map(|r| r.regime.clone());
        let prev_regime_for_transition = prev_regime.clone();

        // Step 1: Run CUSUM on returns to detect structural breaks
        let mut break_detected = false;

        for i in 1..bars.len() {
            let log_return = (bars[i].close / bars[i - 1].close).ln();
            if let Some(_break) = self.cusum.update(log_return) {
                break_detected = true;
                break; // Break on first detection
            }
        }

        // Always get CUSUM sums (even if no break) - for feature extraction
        let (cusum_s_plus, cusum_s_minus) = self.cusum.get_current_sums();

        // Step 2: If break detected OR forced detection, classify regime
        let regime = if break_detected || prev_regime.is_none() {
            // Query regime classifiers (all use canonical OHLCVBar)
            let trending_signal = if let Some(&last_bar) = bars.last() {
                self.trending_classifier.classify(last_bar)
            } else {
                TrendingSignal::Ranging {
                    adx: 0.0,
                    hurst: 0.5,
                }
            };

            let ranging_signal = if let Some(&last_bar) = bars.last() {
                self.ranging_classifier.classify(last_bar)
            } else {
                crate::ranging::RangingSignal::NotRanging
            };

            let volatile_signal = if let Some(&last_bar) = bars.last() {
                self.volatile_classifier.classify(last_bar)
            } else {
                VolatileSignal::Low
            };

            // Step 3: Determine regime based on priority:
            // 1. Volatile (highest priority during crisis)
            // 2. Trending (strong directional moves)
            // 3. Ranging (mean-reverting, sideways)
            // 4. Transition (default/ambiguous)

            match volatile_signal {
                VolatileSignal::Extreme => "Volatile".to_owned(),
                VolatileSignal::High => "Volatile".to_owned(),
                VolatileSignal::Low | VolatileSignal::Medium => {
                    // Not highly volatile, check trending
                    match trending_signal {
                        TrendingSignal::StrongTrend { .. } => "Trending".to_owned(),
                        TrendingSignal::WeakTrend { .. } => {
                            // Weak trend, check if ranging
                            match ranging_signal {
                                crate::ranging::RangingSignal::StrongRanging
                                | crate::ranging::RangingSignal::ModerateRanging => {
                                    "Ranging".to_owned()
                                },
                                crate::ranging::RangingSignal::WeakRanging
                                | crate::ranging::RangingSignal::NotRanging => {
                                    "Trending".to_owned()
                                }, // Default to weak trend
                            }
                        },
                        TrendingSignal::Ranging { .. } => {
                            // Check ranging classifier
                            match ranging_signal {
                                crate::ranging::RangingSignal::StrongRanging
                                | crate::ranging::RangingSignal::ModerateRanging => {
                                    "Ranging".to_owned()
                                },
                                crate::ranging::RangingSignal::WeakRanging
                                | crate::ranging::RangingSignal::NotRanging => {
                                    "Normal".to_owned()
                                }, // Default to normal
                            }
                        },
                    }
                },
            }
        } else {
            // No break detected, return cached regime
            prev_regime.unwrap_or_else(|| "Normal".to_owned())
        };

        // Step 4: Calculate confidence from ADX (normalize to 0-1)
        let adx = self.trending_classifier.get_trend_strength();
        let confidence = (adx / 100.0).clamp(0.0, 1.0); // ADX is 0-100, normalize to 0-1

        // Step 5: Persist to database
        let timestamp = bars.last().ok_or(OrchestratorError::InsufficientData {
            required: 1,
            actual: 0,
        })?.timestamp;

        sqlx::query!(
            r#"
            INSERT INTO regime_states (symbol, regime, confidence, event_timestamp, cusum_s_plus, cusum_s_minus, adx, stability)
            VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
            ON CONFLICT (symbol, event_timestamp) DO UPDATE
            SET regime = EXCLUDED.regime, confidence = EXCLUDED.confidence
            "#,
            symbol,
            regime,
            confidence,
            timestamp,
            Some(cusum_s_plus), Some(cusum_s_minus), Some(adx), None::<f64>
        ).execute(&self.db_pool).await?;

        // Update cumulative bar counter for this symbol
        let current_bar_count = {
            let counter = self.cumulative_bars.entry(symbol.to_string()).or_insert(0);
            *counter += bars.len() as i64;
            *counter
        };

        // Initialize regime_start_bars on first detection for this symbol
        if !self.regime_start_bars.contains_key(symbol) {
            self.regime_start_bars
                .insert(symbol.to_string(), current_bar_count);
        }

        // Step 6: Record transition if regime changed
        if let Some(prev) = prev_regime_for_transition {
            if prev != regime {
                // Calculate duration: bars since the last regime started
                let regime_start = self
                    .regime_start_bars
                    .get(symbol)
                    .copied()
                    .unwrap_or(0);
                let duration_bars = (current_bar_count - regime_start).max(1) as i32;

                // Record the new regime start bar
                self.regime_start_bars
                    .insert(symbol.to_string(), current_bar_count);

                sqlx::query!(
                    r#"
                    INSERT INTO regime_transitions
                        (symbol, event_timestamp, from_regime, to_regime, duration_bars, transition_probability, adx_at_transition, cusum_alert_triggered)
                    VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
                    "#,
                    symbol,
                    timestamp,
                    prev,
                    regime,
                    duration_bars,
                    None::<f64>, // transition_probability (calculated separately)
                    Some(adx),
                    break_detected
                )
                .execute(&self.db_pool)
                .await?;
            }
        }

        // Update cache
        let regime_state = RegimeState {
            regime: regime.clone(),
            confidence,
            timestamp,
            cusum_s_plus: Some(cusum_s_plus),
            cusum_s_minus: Some(cusum_s_minus),
            adx: Some(adx),
            stability: None,
        };

        self.cached_regimes
            .insert(symbol.to_string(), regime_state.clone());

        Ok(regime_state)
    }

    /// Get cached regime state for a symbol
    ///
    /// # Arguments
    ///
    /// * `symbol` - Trading symbol
    ///
    /// # Returns
    ///
    /// Cached regime state if available
    pub fn get_cached_regime(&self, symbol: &str) -> Option<&RegimeState> {
        self.cached_regimes.get(symbol)
    }

    /// Reset CUSUM detector (call after break detection)
    pub fn reset_cusum(&mut self) {
        self.cusum.reset();
    }

    /// Get current CUSUM sums
    pub fn get_cusum_sums(&self) -> (f64, f64) {
        self.cusum.get_current_sums()
    }

    /// Get current ADX value
    pub fn get_adx(&self) -> f64 {
        self.trending_classifier.get_trend_strength()
    }

    /// Get database pool reference
    pub fn pool(&self) -> &PgPool {
        &self.db_pool
    }
}

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

    fn create_test_bars(count: usize, base_price: f64) -> Vec<OHLCVBar> {
        let base_time = Utc::now();
        (0..count)
            .map(|i| {
                let price = base_price + (i as f64 * 0.1);
                OHLCVBar {
                    timestamp: base_time + chrono::Duration::seconds(i as i64 * 60),
                    open: price,
                    high: price + 0.5,
                    low: price - 0.5,
                    close: price + 0.2,
                    volume: 1000.0,
                }
            })
            .collect()
    }

    #[test]
    fn test_regime_state_serialization() {
        let state = RegimeState {
            regime: "Trending".to_owned(),
            confidence: 0.85,
            timestamp: Utc::now(),
            cusum_s_plus: Some(3.5),
            cusum_s_minus: Some(0.0),
            adx: Some(45.0),
            stability: Some(0.9),
        };

        let json = serde_json::to_string(&state).unwrap();
        let deserialized: RegimeState = serde_json::from_str(&json).unwrap();

        assert_eq!(state.regime, deserialized.regime);
        assert_eq!(state.confidence, deserialized.confidence);
    }

    #[test]
    fn test_bar_conversion() {
        let bars = create_test_bars(5, 100.0);
        assert_eq!(bars.len(), 5);
        assert!(bars[0].close > 100.0);
        assert!(bars[4].close > bars[0].close);
    }

    #[test]
    fn test_insufficient_data_error() {
        let _bars = create_test_bars(10, 100.0);
        let error = OrchestratorError::InsufficientData {
            required: 20,
            actual: 10,
        };

        let error_msg = error.to_string();
        assert!(error_msg.contains("Insufficient data"));
        assert!(error_msg.contains("20"));
        assert!(error_msg.contains("10"));
    }
}
