//! Secondary Model for Meta-Labeling
//!
//! This module implements a secondary betting model that predicts whether to trade
//! given a primary signal. The secondary model helps reduce false positives and
//! optimizes position sizing based on confidence and market conditions.
//!
//! ## Algorithm
//!
//! The secondary model combines:
//! 1. Primary prediction (direction, confidence, expected return)
//! 2. Market features (volatility, liquidity, momentum)
//! 3. Risk assessment (position sizing based on confidence)
//!
//! ## Performance Targets
//!
//! - Latency: <50us per prediction
//! - Throughput: >10K predictions/second
//! - False positive reduction: 30-50%

use serde::{Deserialize, Serialize};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;

use crate::MLError;

/// Configuration for secondary betting model
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecondaryModelConfig {
    /// Minimum confidence threshold to consider trading (0.0 to 1.0)
    pub min_confidence: f64,
    /// Maximum confidence cap (0.0 to 1.0)
    pub max_confidence: f64,
    /// Minimum bet size (fraction of portfolio)
    pub min_bet_size: f64,
    /// Maximum bet size (fraction of portfolio)
    pub max_bet_size: f64,
    /// Whether to use ML model (vs rule-based)
    pub use_ml_model: bool,
}

impl Default for SecondaryModelConfig {
    fn default() -> Self {
        Self {
            min_confidence: 0.60, // Only trade when reasonably confident
            max_confidence: 0.95, // Cap at 95% to avoid overconfidence
            min_bet_size: 0.01,   // 1% minimum
            max_bet_size: 0.20,   // 20% maximum
            use_ml_model: false,  // Start with rule-based approach
        }
    }
}

impl SecondaryModelConfig {
    /// Validate configuration parameters
    pub fn validate(&self) -> Result<(), MLError> {
        if self.min_confidence >= self.max_confidence {
            return Err(MLError::ConfigError(
                "min_confidence must be less than max_confidence".to_owned(),
            ));
        }

        if self.min_confidence < 0.0 || self.max_confidence > 1.0 {
            return Err(MLError::ConfigError(
                "Confidence thresholds must be in [0.0, 1.0]".to_owned(),
            ));
        }

        if self.min_bet_size < 0.0 || self.max_bet_size > 1.0 {
            return Err(MLError::ConfigError(
                "Bet sizes must be in [0.0, 1.0]".to_owned(),
            ));
        }

        if self.min_bet_size >= self.max_bet_size {
            return Err(MLError::ConfigError(
                "min_bet_size must be less than max_bet_size".to_owned(),
            ));
        }

        Ok(())
    }
}

/// Primary prediction from the main model
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PrimaryPrediction {
    /// Predicted direction: 1 (buy), -1 (sell), 0 (hold)
    pub direction: i8,
    /// Confidence in the prediction (0.0 to 1.0)
    pub confidence: f64,
    /// Expected return from the trade
    pub expected_return: f64,
    /// Feature vector used for prediction
    pub features: Vec<f64>,
}

/// Trade decision from secondary model
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TradeDecision {
    /// Whether to execute the trade
    pub should_trade: bool,
    /// Position size (fraction of portfolio, 0.0 to `max_bet_size`)
    pub bet_size: f64,
    /// Adjusted confidence after secondary analysis
    pub confidence: f64,
    /// Expected return adjusted for risk
    pub risk_adjusted_return: f64,
}

/// Statistics for secondary model performance
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecondaryModelStatistics {
    /// Total predictions made
    pub total_predictions: u64,
    /// Number of trades recommended
    pub total_trades: u64,
    /// Number of trades rejected
    pub total_rejections: u64,
    /// Average bet size when trading
    pub average_bet_size: f64,
    /// Average confidence when trading
    pub average_confidence: f64,
}

impl Default for SecondaryModelStatistics {
    fn default() -> Self {
        Self {
            total_predictions: 0,
            total_trades: 0,
            total_rejections: 0,
            average_bet_size: 0.0,
            average_confidence: 0.0,
        }
    }
}

/// Secondary betting model for meta-labeling
///
/// This model decides whether to trade given a primary signal, and
/// determines the optimal position size based on confidence and risk.
#[derive(Debug)]
pub struct SecondaryBettingModel {
    config: SecondaryModelConfig,
    // Statistics (atomic for thread-safe updates)
    total_predictions: Arc<AtomicU64>,
    total_trades: Arc<AtomicU64>,
    total_bet_size: Arc<AtomicU64>, // Stored as fixed-point (multiply by 1e6)
    /// Exponential moving average of prediction confidence, fixed-point (x `1_000_000`)
    confidence_ema: Arc<AtomicU64>,
    /// EMA smoothing factor (higher = more weight on recent observations)
    confidence_ema_alpha: f64,
}

impl SecondaryBettingModel {
    /// Create new secondary betting model
    pub fn new(config: SecondaryModelConfig) -> Result<Self, MLError> {
        config.validate()?;

        Ok(Self {
            config,
            total_predictions: Arc::new(AtomicU64::new(0)),
            total_trades: Arc::new(AtomicU64::new(0)),
            total_bet_size: Arc::new(AtomicU64::new(0)),
            confidence_ema: Arc::new(AtomicU64::new(0)),
            confidence_ema_alpha: 0.99,
        })
    }

    /// Get model name
    pub const fn name(&self) -> &str {
        "secondary_betting_model"
    }

    /// Check if model is ready for predictions
    pub const fn is_ready(&self) -> bool {
        true // Rule-based model is always ready
    }

    /// Decide whether to trade given primary signal and market features
    ///
    /// # Arguments
    ///
    /// * `primary` - Primary prediction from main model
    /// * `features` - Market features (volatility, liquidity, momentum, etc.)
    ///
    /// # Returns
    ///
    /// Trade decision with bet sizing recommendation
    pub fn should_trade(
        &self,
        primary: &PrimaryPrediction,
        features: &[f64],
    ) -> Result<TradeDecision, MLError> {
        // Update statistics
        self.total_predictions.fetch_add(1, Ordering::Relaxed);

        // Update confidence EMA (fixed-point encoding: value * 1_000_000)
        {
            let current_ema_fixed = self.confidence_ema.load(Ordering::Relaxed);
            let current_ema = current_ema_fixed as f64 / 1_000_000.0;
            let alpha = self.confidence_ema_alpha;
            let new_ema =
                if current_ema_fixed == 0 && self.total_predictions.load(Ordering::Relaxed) == 1 {
                    // First prediction: initialize EMA to this confidence value
                    primary.confidence
                } else {
                    alpha * current_ema + (1.0 - alpha) * primary.confidence
                };
            self.confidence_ema
                .store((new_ema * 1_000_000.0) as u64, Ordering::Relaxed);
        }

        // Validate inputs
        if primary.features.is_empty() || features.is_empty() {
            return Err(MLError::ValidationError {
                message: "Features cannot be empty".to_owned(),
            });
        }

        // Step 1: Check primary confidence threshold
        if primary.confidence < self.config.min_confidence {
            return Ok(TradeDecision {
                should_trade: false,
                bet_size: 0.0,
                confidence: primary.confidence,
                risk_adjusted_return: 0.0,
            });
        }

        // Step 2: Check expected return (must be positive for long, negative for short)
        let direction_factor = primary.direction as f64;
        let directional_return = primary.expected_return * direction_factor;

        if directional_return <= 0.0 {
            return Ok(TradeDecision {
                should_trade: false,
                bet_size: 0.0,
                confidence: primary.confidence,
                risk_adjusted_return: 0.0,
            });
        }

        // Step 3: Assess market conditions from features
        let market_score = self.assess_market_conditions(features)?;

        // Step 4: Combine primary confidence with market assessment
        let combined_confidence = self.combine_confidence(primary.confidence, market_score);

        // Step 5: Re-check combined confidence
        if combined_confidence < self.config.min_confidence {
            return Ok(TradeDecision {
                should_trade: false,
                bet_size: 0.0,
                confidence: combined_confidence,
                risk_adjusted_return: 0.0,
            });
        }

        // Step 6: Calculate position size based on confidence and risk
        let bet_size = self.calculate_bet_size(combined_confidence, features)?;

        // Step 7: Calculate risk-adjusted return
        let risk_adjusted_return = directional_return * combined_confidence;

        // Update trade statistics
        if bet_size > 0.0 {
            self.total_trades.fetch_add(1, Ordering::Relaxed);
            let bet_size_fixed = (bet_size * 1_000_000.0) as u64;
            self.total_bet_size
                .fetch_add(bet_size_fixed, Ordering::Relaxed);
        }

        Ok(TradeDecision {
            should_trade: bet_size > 0.0,
            bet_size,
            confidence: combined_confidence,
            risk_adjusted_return,
        })
    }

    /// Assess market conditions from feature vector
    ///
    /// Returns score in [0.0, 1.0] where:
    /// - 0.0-0.3: Poor conditions (high risk)
    /// - 0.3-0.7: Neutral conditions
    /// - 0.7-1.0: Favorable conditions (low risk)
    fn assess_market_conditions(&self, features: &[f64]) -> Result<f64, MLError> {
        if features.len() < 3 {
            return Err(MLError::ValidationError {
                message: "Need at least 3 market features (volatility, liquidity, momentum)"
                    .to_string(),
            });
        }

        // Extract key market features (assuming standard ordering)
        let volatility = features.first().copied().unwrap_or(0.5);
        let liquidity = features.get(1).copied().unwrap_or(0.5);
        let momentum = features.get(2).copied().unwrap_or(0.5);

        // Calculate market score
        // Lower volatility is better (more stable)
        let volatility_score = 1.0 - volatility;
        // Higher liquidity is better (easier execution)
        let liquidity_score = liquidity;
        // Strong momentum is favorable
        let momentum_score = momentum;

        // Weighted combination (40% volatility, 40% liquidity, 20% momentum)
        let market_score = 0.4 * volatility_score + 0.4 * liquidity_score + 0.2 * momentum_score;

        // Clamp to [0.0, 1.0]
        Ok(market_score.clamp(0.0, 1.0))
    }

    /// Combine primary confidence with market assessment
    fn combine_confidence(&self, primary_confidence: f64, market_score: f64) -> f64 {
        // Geometric mean gives conservative estimate
        // If either confidence or market score is low, combined is low
        let combined = (primary_confidence * market_score).sqrt();

        // Clamp to configured range
        combined.clamp(0.0, self.config.max_confidence)
    }

    /// Calculate bet size based on confidence and market conditions
    #[allow(clippy::unnecessary_wraps)]
    fn calculate_bet_size(&self, confidence: f64, features: &[f64]) -> Result<f64, MLError> {
        // Base bet size scales linearly with confidence
        let base_bet = self.config.min_bet_size
            + (confidence - self.config.min_confidence)
                / (self.config.max_confidence - self.config.min_confidence)
                * (self.config.max_bet_size - self.config.min_bet_size);

        // Risk adjustment based on volatility
        let volatility = features.first().copied().unwrap_or(0.5);
        let risk_factor = 1.0 - volatility * 0.5; // Reduce bet size in high volatility

        let adjusted_bet = base_bet * risk_factor;

        // Clamp to configured limits
        Ok(adjusted_bet.clamp(self.config.min_bet_size, self.config.max_bet_size))
    }

    /// Get model statistics
    pub fn get_statistics(&self) -> SecondaryModelStatistics {
        let total_predictions = self.total_predictions.load(Ordering::Relaxed);
        let total_trades = self.total_trades.load(Ordering::Relaxed);
        let total_bet_size_fixed = self.total_bet_size.load(Ordering::Relaxed);

        let average_bet_size = if total_trades > 0 {
            (total_bet_size_fixed as f64) / (total_trades as f64 * 1_000_000.0)
        } else {
            0.0
        };

        SecondaryModelStatistics {
            total_predictions,
            total_trades,
            total_rejections: total_predictions.saturating_sub(total_trades),
            average_bet_size,
            average_confidence: self.confidence_ema.load(Ordering::Relaxed) as f64 / 1_000_000.0,
        }
    }

    /// Reset statistics
    pub fn reset_statistics(&mut self) {
        self.total_predictions.store(0, Ordering::Relaxed);
        self.total_trades.store(0, Ordering::Relaxed);
        self.total_bet_size.store(0, Ordering::Relaxed);
        self.confidence_ema.store(0, Ordering::Relaxed);
    }
}

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

    #[test]
    fn test_config_validation() {
        let valid = SecondaryModelConfig::default();
        assert!(valid.validate().is_ok());

        let invalid = SecondaryModelConfig {
            min_confidence: 0.9,
            max_confidence: 0.6,
            ..Default::default()
        };
        assert!(invalid.validate().is_err());
    }

    #[test]
    fn test_market_assessment() {
        let config = SecondaryModelConfig::default();
        let model = SecondaryBettingModel::new(config).unwrap();

        // Low volatility, high liquidity, good momentum
        let good_features = vec![0.2, 0.8, 0.7];
        let score = model.assess_market_conditions(&good_features).unwrap();
        assert!(score > 0.6);

        // High volatility, low liquidity, weak momentum
        let bad_features = vec![0.9, 0.2, 0.3];
        let score = model.assess_market_conditions(&bad_features).unwrap();
        assert!(score < 0.5);
    }

    #[test]
    fn test_confidence_combination() {
        let config = SecondaryModelConfig::default();
        let model = SecondaryBettingModel::new(config).unwrap();

        // Both high
        let combined = model.combine_confidence(0.8, 0.9);
        assert!(combined > 0.8);

        // One low
        let combined = model.combine_confidence(0.8, 0.3);
        assert!(combined < 0.6);
    }

    #[test]
    fn test_secondary_model_confidence_ema() {
        let config = SecondaryModelConfig::default();
        let mut model = SecondaryBettingModel::new(config).unwrap();

        let primary = PrimaryPrediction {
            direction: 1,
            confidence: 0.8,
            expected_return: 0.05,
            features: vec![0.5, 0.6, 0.7],
        };
        let features = vec![0.2, 0.8, 0.7];

        // First prediction initializes EMA to 0.8
        let _ = model.should_trade(&primary, &features).unwrap();
        let stats = model.get_statistics();
        assert!((stats.average_confidence - 0.8).abs() < 0.01);

        // Second prediction with confidence 0.6
        // EMA = 0.99 * 0.8 + 0.01 * 0.6 = 0.798
        let primary2 = PrimaryPrediction {
            direction: 1,
            confidence: 0.6,
            expected_return: 0.05,
            features: vec![0.5, 0.6, 0.7],
        };
        let _ = model.should_trade(&primary2, &features).unwrap();
        let stats2 = model.get_statistics();
        assert!((stats2.average_confidence - 0.798).abs() < 0.01);

        // Reset clears EMA
        model.reset_statistics();
        let stats3 = model.get_statistics();
        assert!((stats3.average_confidence - 0.0).abs() < 1e-6);
    }

    #[test]
    fn test_bet_size_calculation() {
        let config = SecondaryModelConfig::default();
        let max_bet_size = config.max_bet_size;
        let min_bet_size = config.min_bet_size;
        let model = SecondaryBettingModel::new(config.clone()).unwrap();

        // High confidence, low volatility
        let features = vec![0.2, 0.8, 0.7];
        let bet = model.calculate_bet_size(0.9, &features).unwrap();
        assert!(bet > min_bet_size);
        assert!(bet <= max_bet_size);

        // Medium confidence, high volatility
        let features = vec![0.8, 0.5, 0.5];
        let bet_vol = model.calculate_bet_size(0.7, &features).unwrap();
        assert!(bet_vol < bet); // Should be smaller due to volatility
    }
}
