//! Background ML Prediction Generation Loop
//!
//! This module implements a continuous background task that generates ML predictions
//! every 60 seconds for all configured trading symbols. Predictions are saved to the
//! ensemble_predictions table for consumption by the paper trading executor.
//!
//! ## Architecture
//!
//! ```text
//! ┌─────────────────────────────────────────────────────────┐
//! │    Prediction Generation Loop (60s interval)            │
//! │                                                           │
//! │  1. Fetch current market data for all symbols           │
//! │  2. Extract 26 features from OHLCV data                 │
//! │  3. Call EnsembleCoordinator.predict()                  │
//! │  4. Save EnsembleDecision to ensemble_predictions       │
//! │  5. Sleep 60 seconds                                    │
//! │  6. Repeat (with graceful shutdown on SIGTERM)          │
//! └─────────────────────────────────────────────────────────┘
//! ```
//!
//! ## Error Handling
//!
//! The loop is designed to be resilient:
//! - Database connection errors: Log and retry next cycle
//! - Model inference errors: Log and continue (ensemble degradation)
//! - Feature extraction errors: Log and skip symbol
//! - NO crashes: Loop always continues unless shutdown signal received
//!
//! ## Graceful Shutdown
//!
//! The task listens for SIGTERM/SIGINT and exits cleanly:
//! - Waits for current prediction cycle to complete
//! - Flushes any pending database writes
//! - Returns immediately after cleanup

use anyhow::{Context, Result};
use ml::Features;
use sqlx::PgPool;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::broadcast;
use tokio::time::interval;
use tracing::{debug, error, info, warn};
use uuid::Uuid;

use crate::ensemble_coordinator::EnsembleCoordinator;

/// Configuration for prediction generation loop
#[derive(Debug, Clone)]
pub struct PredictionLoopConfig {
    /// Interval between prediction cycles (default: 60 seconds)
    pub prediction_interval: Duration,

    /// List of symbols to generate predictions for
    pub symbols: Vec<String>,

    /// Account ID for attribution
    pub account_id: String,

    /// Strategy ID for attribution
    pub strategy_id: String,

    /// Node ID for system context
    pub node_id: String,
}

impl Default for PredictionLoopConfig {
    fn default() -> Self {
        Self {
            prediction_interval: Duration::from_secs(60),
            symbols: vec![
                "ES.FUT".to_string(),
                "NQ.FUT".to_string(),
                "ZN.FUT".to_string(),
                "6E.FUT".to_string(),
            ],
            account_id: "prediction_generator_001".to_string(),
            strategy_id: "ensemble_ml_v1".to_string(),
            node_id: hostname::get()
                .ok()
                .and_then(|h| h.into_string().ok())
                .unwrap_or_else(|| "unknown".to_string()),
        }
    }
}

impl PredictionLoopConfig {
    /// Create configuration from environment variables
    pub fn from_env() -> Self {
        Self {
            prediction_interval: std::env::var("PREDICTION_INTERVAL_SECONDS")
                .ok()
                .and_then(|s| s.parse().ok())
                .map(Duration::from_secs)
                .unwrap_or(Duration::from_secs(60)),
            symbols: std::env::var("PREDICTION_SYMBOLS")
                .ok()
                .map(|s| s.split(',').map(|sym| sym.trim().to_string()).collect())
                .unwrap_or_else(|| {
                    vec![
                        "ES.FUT".to_string(),
                        "NQ.FUT".to_string(),
                        "ZN.FUT".to_string(),
                        "6E.FUT".to_string(),
                    ]
                }),
            account_id: std::env::var("PREDICTION_ACCOUNT_ID")
                .unwrap_or_else(|_| "prediction_generator_001".to_string()),
            strategy_id: std::env::var("PREDICTION_STRATEGY_ID")
                .unwrap_or_else(|_| "ensemble_ml_v1".to_string()),
            node_id: hostname::get()
                .ok()
                .and_then(|h| h.into_string().ok())
                .unwrap_or_else(|| "unknown".to_string()),
        }
    }
}

/// Background task for continuous ML prediction generation
pub struct PredictionGenerationLoop {
    coordinator: Arc<EnsembleCoordinator>,
    db_pool: PgPool,
    config: PredictionLoopConfig,
}

impl PredictionGenerationLoop {
    /// Create new prediction generation loop
    pub fn new(
        coordinator: Arc<EnsembleCoordinator>,
        db_pool: PgPool,
        config: PredictionLoopConfig,
    ) -> Self {
        Self {
            coordinator,
            db_pool,
            config,
        }
    }

    /// Start the prediction generation loop
    ///
    /// This runs forever until a shutdown signal is received via the shutdown_rx channel.
    /// The loop generates predictions for all configured symbols every `prediction_interval`.
    pub async fn run(&self, mut shutdown_rx: broadcast::Receiver<()>) -> Result<()> {
        info!(
            "Starting prediction generation loop for symbols: {:?}, interval: {:?}",
            self.config.symbols, self.config.prediction_interval
        );

        let mut ticker = interval(self.config.prediction_interval);

        loop {
            tokio::select! {
                _ = ticker.tick() => {
                    // Generate predictions for all symbols
                    if let Err(e) = self.generate_predictions_for_all_symbols().await {
                        error!("Prediction generation cycle failed: {}", e);
                        // Don't crash - log and continue to next cycle
                    }
                }
                _ = shutdown_rx.recv() => {
                    info!("Shutdown signal received, stopping prediction generation loop");
                    break;
                }
            }
        }

        info!("Prediction generation loop stopped gracefully");
        Ok(())
    }

    /// Generate predictions for all configured symbols
    async fn generate_predictions_for_all_symbols(&self) -> Result<()> {
        debug!(
            "Starting prediction generation cycle for {} symbols",
            self.config.symbols.len()
        );

        let start_time = std::time::Instant::now();

        for symbol in &self.config.symbols {
            if let Err(e) = self.generate_prediction_for_symbol(symbol).await {
                warn!("Failed to generate prediction for {}: {}", symbol, e);
                // Continue with other symbols even if one fails
            }
        }

        let cycle_duration = start_time.elapsed();
        info!(
            "Prediction generation cycle completed in {:.2}s for {} symbols",
            cycle_duration.as_secs_f64(),
            self.config.symbols.len()
        );

        Ok(())
    }

    /// Generate prediction for a single symbol
    async fn generate_prediction_for_symbol(&self, symbol: &str) -> Result<()> {
        debug!("Generating prediction for {}", symbol);

        // 1. Fetch current market data and extract features
        let features = self.fetch_features_for_symbol(symbol).await?;

        // 2. Generate ensemble prediction
        let start_inference = std::time::Instant::now();
        let decision = self
            .coordinator
            .predict(&features)
            .await
            .context("Ensemble prediction failed")?;
        let inference_latency_us = start_inference.elapsed().as_micros() as i32;

        // 3. Save prediction to database
        self.save_prediction_to_database(symbol, &decision, inference_latency_us)
            .await?;

        debug!(
            "Prediction saved for {}: action={:?}, confidence={:.3}, disagreement={:.3}, latency={}μs",
            symbol, decision.action, decision.confidence, decision.disagreement_rate, inference_latency_us
        );

        Ok(())
    }

    /// Fetch market data and extract features for a symbol
    async fn fetch_features_for_symbol(&self, symbol: &str) -> Result<Features> {
        // Query recent market data from database
        let market_data = self.query_recent_market_data(symbol).await?;

        // Extract 26 features from OHLCV data
        // For production, use real feature engineering from ml/src/features
        // Extract features from OHLCV data (simplified 15-dim vector for DB-backed path;
        // the EnsembleCoordinator.fetch_features_for_symbol path uses the full 51-dim
        // ProductionFeatureExtractorAdapter when market data updates are streamed).
        let features = self.extract_features_from_market_data(symbol, &market_data)?;

        Ok(features)
    }

    /// Query recent market data from database
    async fn query_recent_market_data(&self, symbol: &str) -> Result<MarketDataSnapshot> {
        // Query last 100 bars of OHLCV data for technical indicators
        let bars = sqlx::query_as::<_, OhlcvBar>(
            r#"
            SELECT
                open,
                high,
                low,
                close,
                volume
            FROM ohlcv_bars
            WHERE symbol = $1
            ORDER BY timestamp DESC
            LIMIT 100
            "#,
        )
        .bind(symbol)
        .fetch_all(&self.db_pool)
        .await
        .context("Failed to query market data")?;

        if bars.is_empty() {
            return Err(anyhow::anyhow!("No market data available for {}", symbol));
        }

        Ok(MarketDataSnapshot { bars })
    }

    /// Extract features from market data
    fn extract_features_from_market_data(
        &self,
        symbol: &str,
        market_data: &MarketDataSnapshot,
    ) -> Result<Features> {
        // Use the most recent bar for current price data
        let latest_bar = market_data
            .bars
            .first()
            .ok_or_else(|| anyhow::anyhow!("No bars in market data"))?;

        // Calculate basic technical indicators
        // In production, use ml/src/features/technical_indicators.rs
        let close_prices: Vec<f64> = market_data.bars.iter().map(|b| b.close).collect();
        let volumes: Vec<f64> = market_data.bars.iter().map(|b| b.volume).collect();

        // Basic features (5 OHLCV + 10 technical indicators = 15 total)
        // Simplified for MVP - production should use full 26-feature extraction
        let mut feature_values = vec![
            latest_bar.open,
            latest_bar.high,
            latest_bar.low,
            latest_bar.close,
            latest_bar.volume,
        ];

        // Add 10 technical indicators (simplified calculations)
        feature_values.push(calculate_sma(&close_prices, 20)); // SMA_20
        feature_values.push(calculate_sma(&close_prices, 50)); // SMA_50
        feature_values.push(calculate_rsi(&close_prices, 14)); // RSI_14
        feature_values.push(calculate_volatility(&close_prices, 20)); // Volatility_20
        feature_values.push(calculate_momentum(&close_prices, 10)); // Momentum_10
        feature_values.push(calculate_volume_ratio(&volumes, 20)); // Volume_Ratio
        feature_values.push(latest_bar.high - latest_bar.low); // Range
        feature_values.push(latest_bar.close - latest_bar.open); // Body
        feature_values.push(calculate_returns(&close_prices)); // Returns_1
        feature_values.push(calculate_log_returns(&close_prices)); // Log_Returns_1

        let feature_names = vec![
            "open",
            "high",
            "low",
            "close",
            "volume",
            "sma_20",
            "sma_50",
            "rsi_14",
            "volatility_20",
            "momentum_10",
            "volume_ratio_20",
            "range",
            "body",
            "returns_1",
            "log_returns_1",
        ]
        .into_iter()
        .map(|s| s.to_string())
        .collect();

        let mut features = Features::new(feature_values, feature_names);
        features.symbol = Some(symbol.to_string());

        Ok(features)
    }

    /// Save prediction to ensemble_predictions table
    async fn save_prediction_to_database(
        &self,
        symbol: &str,
        decision: &ml::ensemble::EnsembleDecision,
        inference_latency_us: i32,
    ) -> Result<()> {
        let prediction_id = Uuid::new_v4();
        let prediction_timestamp = chrono::Utc::now();

        // Extract per-model votes from decision
        let (dqn_signal, dqn_confidence, dqn_weight, dqn_vote) =
            extract_model_vote(decision, "DQN");
        let (ppo_signal, ppo_confidence, ppo_weight, ppo_vote) =
            extract_model_vote(decision, "PPO");
        let (mamba2_signal, mamba2_confidence, mamba2_weight, mamba2_vote) =
            extract_model_vote(decision, "MAMBA2");
        let (tft_signal, tft_confidence, tft_weight, tft_vote) =
            extract_model_vote(decision, "TFT");

        // Convert action to string
        let ensemble_action = format!("{:?}", decision.action).to_uppercase();

        // Insert into database
        sqlx::query(
            r#"
            INSERT INTO ensemble_predictions (
                id,
                prediction_timestamp,
                symbol,
                account_id,
                strategy_id,
                ensemble_action,
                ensemble_signal,
                ensemble_confidence,
                disagreement_rate,
                dqn_signal,
                dqn_confidence,
                dqn_weight,
                dqn_vote,
                ppo_signal,
                ppo_confidence,
                ppo_weight,
                ppo_vote,
                mamba2_signal,
                mamba2_confidence,
                mamba2_weight,
                mamba2_vote,
                tft_signal,
                tft_confidence,
                tft_weight,
                tft_vote,
                node_id,
                inference_latency_us,
                metadata
            )
            VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28)
            "#,
        )
        .bind(prediction_id)
        .bind(prediction_timestamp)
        .bind(symbol)
        .bind(&self.config.account_id)
        .bind(&self.config.strategy_id)
        .bind(ensemble_action)
        .bind(decision.signal)
        .bind(decision.confidence)
        .bind(decision.disagreement_rate)
        .bind(dqn_signal)
        .bind(dqn_confidence)
        .bind(dqn_weight)
        .bind(dqn_vote)
        .bind(ppo_signal)
        .bind(ppo_confidence)
        .bind(ppo_weight)
        .bind(ppo_vote)
        .bind(mamba2_signal)
        .bind(mamba2_confidence)
        .bind(mamba2_weight)
        .bind(mamba2_vote)
        .bind(tft_signal)
        .bind(tft_confidence)
        .bind(tft_weight)
        .bind(tft_vote)
        .bind(&self.config.node_id)
        .bind(inference_latency_us)
        .bind(serde_json::json!({"generator": "background_loop"}))
        .execute(&self.db_pool)
        .await
        .context("Failed to insert prediction into database")?;

        Ok(())
    }
}

/// Extract model vote from ensemble decision
fn extract_model_vote(
    decision: &ml::ensemble::EnsembleDecision,
    model_id: &str,
) -> (Option<f64>, Option<f64>, Option<f64>, Option<String>) {
    if let Some(vote) = decision.model_votes.get(model_id) {
        let action_str = format!("{:?}", vote.action(0.0)).to_uppercase(); // threshold=0.0 for basic classification
        (
            Some(vote.signal),
            Some(vote.confidence),
            Some(vote.weight),
            Some(action_str),
        )
    } else {
        (None, None, None, None)
    }
}

/// Market data snapshot for feature extraction
#[derive(Debug, Clone)]
struct MarketDataSnapshot {
    bars: Vec<OhlcvBar>,
}

/// OHLCV bar from database
#[derive(Debug, Clone, sqlx::FromRow)]
struct OhlcvBar {
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    volume: f64,
}

// ================================================================================================
// Technical Indicator Calculations (Simplified)
// For production, use ml/src/features/technical_indicators.rs
// ================================================================================================

fn calculate_sma(prices: &[f64], period: usize) -> f64 {
    if prices.len() < period {
        return prices.iter().sum::<f64>() / prices.len() as f64;
    }
    prices.iter().take(period).sum::<f64>() / period as f64
}

fn calculate_rsi(prices: &[f64], period: usize) -> f64 {
    if prices.len() < period + 1 {
        return 50.0; // Neutral
    }

    let mut gains = 0.0;
    let mut losses = 0.0;

    for i in 0..period {
        let change = prices[i] - prices[i + 1];
        if change > 0.0 {
            gains += change;
        } else {
            losses += change.abs();
        }
    }

    let avg_gain = gains / period as f64;
    let avg_loss = losses / period as f64;

    if avg_loss == 0.0 {
        return 100.0;
    }

    let rs = avg_gain / avg_loss;
    100.0 - (100.0 / (1.0 + rs))
}

fn calculate_volatility(prices: &[f64], period: usize) -> f64 {
    if prices.len() < period {
        return 0.0;
    }

    let mean = calculate_sma(prices, period);
    let variance: f64 = prices
        .iter()
        .take(period)
        .map(|p| (p - mean).powi(2))
        .sum::<f64>()
        / period as f64;

    variance.sqrt()
}

fn calculate_momentum(prices: &[f64], period: usize) -> f64 {
    if prices.len() <= period {
        return 0.0;
    }
    prices.first().copied().unwrap_or(0.0) - prices.get(period).copied().unwrap_or(0.0)
}

fn calculate_volume_ratio(volumes: &[f64], period: usize) -> f64 {
    if volumes.is_empty() {
        return 1.0;
    }
    let avg_volume = calculate_sma(volumes, period);
    if avg_volume == 0.0 {
        return 1.0;
    }
    volumes.first().copied().unwrap_or(0.0) / avg_volume
}

fn calculate_returns(prices: &[f64]) -> f64 {
    if prices.len() < 2 {
        return 0.0;
    }
    let price_0 = prices.first().copied().unwrap_or(0.0);
    let price_1 = prices.get(1).copied().unwrap_or(1.0);
    if price_1 == 0.0 {
        return 0.0;
    }
    (price_0 - price_1) / price_1
}

fn calculate_log_returns(prices: &[f64]) -> f64 {
    if prices.len() < 2 {
        return 0.0;
    }
    let price_0 = prices.first().copied().unwrap_or(0.0);
    let price_1 = prices.get(1).copied().unwrap_or(1.0);
    if price_1 == 0.0 {
        return 0.0;
    }
    (price_0 / price_1).ln()
}

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

    #[test]
    fn test_config_default() {
        let config = PredictionLoopConfig::default();
        assert_eq!(config.prediction_interval, Duration::from_secs(60));
        assert_eq!(config.symbols.len(), 4);
        assert!(config.symbols.contains(&"ES.FUT".to_string()));
    }

    #[test]
    fn test_calculate_sma() {
        let prices = vec![10.0, 11.0, 12.0, 13.0, 14.0];
        let sma = calculate_sma(&prices, 3);
        assert!((sma - 11.0).abs() < 0.01); // (10 + 11 + 12) / 3 = 11
    }

    #[test]
    fn test_calculate_rsi() {
        let prices = vec![
            50.0, 51.0, 52.0, 51.5, 53.0, 52.0, 54.0, 53.5, 55.0, 54.5, 56.0, 55.5, 57.0, 56.5,
            58.0,
        ];
        let rsi = calculate_rsi(&prices, 14);
        assert!((0.0..=100.0).contains(&rsi));
    }

    #[test]
    fn test_calculate_returns() {
        let prices = vec![110.0, 100.0];
        let returns = calculate_returns(&prices);
        assert!((returns - 0.1).abs() < 0.001); // 10% return
    }

    #[test]
    fn test_extract_model_vote() {
        use ml::ensemble::{EnsembleDecision, ModelVote, TradingAction};
        use std::collections::HashMap;

        let mut model_votes = HashMap::new();
        model_votes.insert(
            "DQN".to_string(),
            ModelVote::new("DQN".to_string(), 0.8, 0.9, 0.33),
        );

        let decision = EnsembleDecision::new(TradingAction::Buy, 0.85, 0.75, 0.15, model_votes);

        let (signal, confidence, weight, vote) = extract_model_vote(&decision, "DQN");
        assert_eq!(signal, Some(0.8));
        assert_eq!(confidence, Some(0.9));
        assert_eq!(weight, Some(0.33));
        assert_eq!(vote, Some("BUY".to_string()));

        // Non-existent model
        let (signal, confidence, weight, vote) = extract_model_vote(&decision, "UNKNOWN");
        assert_eq!(signal, None);
        assert_eq!(confidence, None);
        assert_eq!(weight, None);
        assert_eq!(vote, None);
    }
}
