//! Primary Directional Model for Meta-Labeling
//!
//! The primary model is the first stage of meta-labeling, responsible for predicting
//! the direction of the market (BUY/SELL/HOLD). This prediction is then evaluated by
//! the secondary model to determine whether to place a bet and what size.
//!
//! ## Architecture
//!
//! ```text
//! Features (256-dim) -> Primary Model -> (Label, Confidence)
//!                                         |
//!                            BUY/SELL/HOLD + Confidence Score
//! ```
//!
//! ## Performance
//! - Target latency: <50us per prediction
//! - Confidence scores: 0.0 to 1.0
//! - Labels: {-1: SELL, 0: HOLD, 1: BUY}
//!
//! ## Integration
//! - Uses existing ML models (DQN/PPO/MAMBA) from Wave A
//! - Integrates with 256-dim feature extraction
//! - Aligns with triple barrier labels for training

use crate::gpu_acceleration::LabelingError;
use crate::MLError;
use serde::{Deserialize, Serialize};
use std::time::Instant;

/// Direction labels for primary model predictions
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Label {
    /// Buy signal (upward movement expected)
    Buy,
    /// Sell signal (downward movement expected)
    Sell,
    /// Hold signal (no clear direction or low confidence)
    Hold,
}

impl Label {
    /// Convert label to integer representation
    /// - Buy = 1
    /// - Hold = 0
    /// - Sell = -1
    pub const fn to_i8(&self) -> i8 {
        match self {
            Label::Buy => 1,
            Label::Sell => -1,
            Label::Hold => 0,
        }
    }

    /// Create label from integer representation
    pub const fn from_i8(value: i8) -> Self {
        match value {
            1 => Label::Buy,
            -1 => Label::Sell,
            _ => Label::Hold,
        }
    }

    /// Create label from continuous prediction value and threshold
    pub fn from_prediction(prediction: f64, threshold: f64) -> Self {
        if prediction > threshold {
            Label::Buy
        } else if prediction < -threshold {
            Label::Sell
        } else {
            Label::Hold
        }
    }
}

/// Configuration for primary directional model
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PrimaryModelConfig {
    /// Confidence threshold for BUY/SELL decisions (0.0 to 1.0)
    /// Predictions with confidence below this threshold result in HOLD
    pub threshold: f64,

    /// Whether to use ensemble of models (future enhancement)
    pub use_ensemble: bool,
}

impl Default for PrimaryModelConfig {
    fn default() -> Self {
        Self {
            threshold: 0.5,
            use_ensemble: false,
        }
    }
}

impl PrimaryModelConfig {
    /// Validate configuration
    pub fn validate(&self) -> Result<(), MLError> {
        if self.threshold < 0.0 || self.threshold > 1.0 {
            return Err(MLError::ConfigError(format!(
                "Threshold must be in range [0.0, 1.0], got {}",
                self.threshold
            )));
        }
        Ok(())
    }
}

/// Primary directional model for meta-labeling
///
/// Predicts market direction (BUY/SELL/HOLD) with confidence scores.
/// The secondary model then decides whether to place a bet based on this prediction.
#[derive(Debug)]
pub struct PrimaryDirectionalModel {
    config: PrimaryModelConfig,
    // Note: In production, this would hold a reference to the actual ML model
    // For now, we use a simple linear model for demonstration
}

impl PrimaryDirectionalModel {
    /// Create new primary directional model
    pub fn new(config: PrimaryModelConfig) -> Result<Self, MLError> {
        config.validate()?;

        Ok(Self { config })
    }

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

    /// Make direction prediction from features
    ///
    /// ## Arguments
    /// - `features`: 256-dimensional feature vector
    ///
    /// ## Returns
    /// - `(Label, f64)`: Direction label and confidence score
    ///
    /// ## Performance
    /// - Target: <50us latency
    ///
    /// ## Errors
    /// - Returns `DimensionMismatch` if features length != 256
    /// - Returns `InvalidInput` if features contain NaN or infinity
    pub fn predict(&self, features: &[f64]) -> Result<(Label, f64), MLError> {
        let _start = Instant::now();

        // Validate input
        self.validate_features(features)?;

        // Simple linear model for demonstration
        // In production, this would use DQN/PPO/MAMBA from Wave A
        let raw_prediction = self.compute_raw_prediction(features);

        // Calculate confidence as absolute value (capped at 1.0)
        let confidence = raw_prediction.abs().min(1.0);

        // Determine label based on prediction and threshold
        let label = Label::from_prediction(raw_prediction, self.config.threshold);

        Ok((label, confidence))
    }

    /// Predict with timing information
    pub fn predict_timed(&self, features: &[f64]) -> Result<(Label, f64, u64), MLError> {
        let start = Instant::now();
        let (label, confidence) = self.predict(features)?;
        let latency_us = start.elapsed().as_micros() as u64;
        Ok((label, confidence, latency_us))
    }

    /// Validate feature vector
    fn validate_features(&self, features: &[f64]) -> Result<(), MLError> {
        // Check dimension
        if features.len() != 256 {
            return Err(MLError::DimensionMismatch {
                expected: 256,
                actual: features.len(),
            });
        }

        // Check for NaN or infinity
        for (i, &value) in features.iter().enumerate() {
            if value.is_nan() {
                return Err(MLError::InvalidInput(format!(
                    "Feature {} contains NaN value",
                    i
                )));
            }
            if value.is_infinite() {
                return Err(MLError::InvalidInput(format!(
                    "Feature {} contains infinite value",
                    i
                )));
            }
        }

        Ok(())
    }

    /// Compute raw prediction from features
    ///
    /// This is a simplified implementation using a linear model.
    /// In production, this would call into DQN/PPO/MAMBA models.
    fn compute_raw_prediction(&self, features: &[f64]) -> f64 {
        // Simple weighted average of features
        // Positive features -> BUY signal
        // Negative features -> SELL signal

        // Weight recent price action more heavily (features 0-4 are OHLCV)
        let price_features_weight = 0.4;
        let technical_indicators_weight = 0.3; // Features 5-14
        let other_features_weight = 0.3; // Features 15+

        let price_signal = features[0..5].iter().sum::<f64>() / 5.0;
        let technical_signal = if features.len() > 14 {
            features[5..15].iter().sum::<f64>() / 10.0
        } else {
            0.0
        };
        let other_signal = if features.len() > 15 {
            features[15..].iter().sum::<f64>() / (features.len() - 15) as f64
        } else {
            0.0
        };

        let raw_prediction = price_signal * price_features_weight
            + technical_signal * technical_indicators_weight
            + other_signal * other_features_weight;

        // Normalize to reasonable range [-1, 1]
        raw_prediction.tanh()
    }

    /// Get configuration
    pub const fn config(&self) -> &PrimaryModelConfig {
        &self.config
    }
}

// Convert to LabelingError for compatibility with existing meta-labeling code
impl From<MLError> for LabelingError {
    fn from(err: MLError) -> Self {
        match err {
            MLError::DimensionMismatch { expected, actual } => LabelingError::ConfigError(format!(
                "Dimension mismatch: expected {}, got {}",
                expected, actual
            )),
            MLError::InvalidInput(msg) => LabelingError::InvalidInput(msg),
            MLError::ConfigError(reason) => LabelingError::ConfigError(reason),
            MLError::GraphError { .. }
            | MLError::ResourceLimit { .. }
            | MLError::SerializationError { .. }
            | MLError::ValidationError { .. }
            | MLError::ConcurrencyError { .. }
            | MLError::InitializationError { .. }
            | MLError::TrainingError(_)
            | MLError::InferenceError(_)
            | MLError::ModelError(_)
            | MLError::NotTrained(_)
            | MLError::AnyhowError(_)
            | MLError::TensorCreationError { .. }
            | MLError::TensorOperationError(_)
            | MLError::LockError(_)
            | MLError::ModelNotFound(_)
            | MLError::InsufficientData(_)
            | MLError::CheckpointError(_)
            | MLError::DeviceError(_) => LabelingError::ComputationError(err.to_string()),
        }
    }
}

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

    #[test]
    fn test_label_conversions() {
        assert_eq!(Label::Buy.to_i8(), 1);
        assert_eq!(Label::Hold.to_i8(), 0);
        assert_eq!(Label::Sell.to_i8(), -1);

        assert_eq!(Label::from_i8(1), Label::Buy);
        assert_eq!(Label::from_i8(0), Label::Hold);
        assert_eq!(Label::from_i8(-1), Label::Sell);
    }

    #[test]
    fn test_label_from_prediction() {
        assert_eq!(Label::from_prediction(0.8, 0.5), Label::Buy);
        assert_eq!(Label::from_prediction(-0.8, 0.5), Label::Sell);
        assert_eq!(Label::from_prediction(0.3, 0.5), Label::Hold);
        assert_eq!(Label::from_prediction(-0.3, 0.5), Label::Hold);
    }

    #[test]
    fn test_config_validation() {
        let valid_config = PrimaryModelConfig {
            threshold: 0.5,
            use_ensemble: false,
        };
        assert!(valid_config.validate().is_ok());

        let invalid_config = PrimaryModelConfig {
            threshold: 1.5,
            use_ensemble: false,
        };
        assert!(invalid_config.validate().is_err());
    }

    #[test]
    fn test_model_creation() {
        let config = PrimaryModelConfig::default();
        let result = PrimaryDirectionalModel::new(config);
        assert!(result.is_ok());
    }

    #[test]
    fn test_basic_prediction() {
        let config = PrimaryModelConfig::default();
        let model = PrimaryDirectionalModel::new(config).unwrap();

        // Positive features -> BUY
        let features = vec![1.0; 256];
        let (label, confidence) = model.predict(&features).unwrap();
        assert_eq!(label, Label::Buy);
        assert!(confidence > 0.0);

        // Negative features -> SELL
        let features = vec![-1.0; 256];
        let (label, confidence) = model.predict(&features).unwrap();
        assert_eq!(label, Label::Sell);
        assert!(confidence > 0.0);
    }

    #[test]
    fn test_dimension_validation() {
        let config = PrimaryModelConfig::default();
        let model = PrimaryDirectionalModel::new(config).unwrap();

        // Wrong dimension
        let features = vec![1.0; 128];
        let result = model.predict(&features);
        assert!(result.is_err());

        match result {
            Err(MLError::DimensionMismatch { expected, actual }) => {
                assert_eq!(expected, 256);
                assert_eq!(actual, 128);
            },
            _ => panic!("Expected DimensionMismatch error"),
        }
    }

    #[test]
    fn test_nan_detection() {
        let config = PrimaryModelConfig::default();
        let model = PrimaryDirectionalModel::new(config).unwrap();

        let mut features = vec![1.0; 256];
        features[100] = f64::NAN;

        let result = model.predict(&features);
        assert!(result.is_err());
    }

    #[test]
    fn test_infinity_detection() {
        let config = PrimaryModelConfig::default();
        let model = PrimaryDirectionalModel::new(config).unwrap();

        let mut features = vec![1.0; 256];
        features[100] = f64::INFINITY;

        let result = model.predict(&features);
        assert!(result.is_err());
    }
}
