//! Ensemble decision types for production trading
//!
//! This module defines the core types for ensemble model decisions,
//! including aggregated predictions, confidence scores, and metadata
//! for tracking model contributions.

use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// Action to take based on ensemble decision
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum TradingAction {
    /// Buy signal (long position)
    Buy,
    /// Sell signal (short position)
    Sell,
    /// Hold current position (no action)
    Hold,
}

impl TradingAction {
    /// Convert from signal value (-1.0 to 1.0)
    pub fn from_signal(signal: f64, threshold: f64) -> Self {
        if signal > threshold {
            Self::Buy
        } else if signal < -threshold {
            Self::Sell
        } else {
            Self::Hold
        }
    }

    /// Convert to signal value
    pub fn to_signal(&self) -> f64 {
        match self {
            Self::Buy => 1.0,
            Self::Sell => -1.0,
            Self::Hold => 0.0,
        }
    }
}

/// Ensemble decision with aggregated model predictions
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnsembleDecision {
    /// Trading action (Buy/Sell/Hold)
    pub action: TradingAction,

    /// Ensemble confidence (0.0-1.0)
    pub confidence: f64,

    /// Raw signal value (-1.0 to 1.0, bearish to bullish)
    pub signal: f64,

    /// Disagreement rate (0.0-1.0, % models disagree)
    pub disagreement_rate: f64,

    /// Per-model contributions
    pub model_votes: HashMap<String, ModelVote>,

    /// Timestamp of decision (microseconds since epoch)
    pub timestamp: u64,

    /// Symbol for this decision
    pub symbol: Option<String>,

    /// Additional metadata
    pub metadata: HashMap<String, serde_json::Value>,
}

impl EnsembleDecision {
    /// Create new ensemble decision
    pub fn new(
        action: TradingAction,
        confidence: f64,
        signal: f64,
        disagreement_rate: f64,
        model_votes: HashMap<String, ModelVote>,
    ) -> Self {
        Self {
            action,
            confidence,
            signal,
            disagreement_rate,
            model_votes,
            timestamp: std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap_or_default()
                .as_micros() as u64,
            symbol: None,
            metadata: HashMap::new(),
        }
    }

    /// Set symbol for this decision
    pub fn with_symbol(mut self, symbol: String) -> Self {
        self.symbol = Some(symbol);
        self
    }

    /// Add metadata
    pub fn with_metadata(mut self, key: String, value: serde_json::Value) -> Self {
        self.metadata.insert(key, value);
        self
    }

    /// Check if high disagreement (> 50% models disagree)
    pub fn is_high_disagreement(&self) -> bool {
        self.disagreement_rate > 0.5
    }

    /// Get number of models that voted
    pub fn model_count(&self) -> usize {
        self.model_votes.len()
    }
}

/// Individual model vote in ensemble
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelVote {
    /// Model identifier
    pub model_id: String,

    /// Model's signal (-1.0 to 1.0)
    pub signal: f64,

    /// Model's confidence (0.0 to 1.0)
    pub confidence: f64,

    /// Weight applied to this model's vote
    pub weight: f64,

    /// Model type
    pub model_type: String,
}

impl ModelVote {
    /// Create new model vote
    pub fn new(model_id: String, signal: f64, confidence: f64, weight: f64) -> Self {
        Self {
            model_id,
            signal,
            confidence,
            weight,
            model_type: "unknown".to_owned(),
        }
    }

    /// Set model type
    pub fn with_model_type(mut self, model_type: String) -> Self {
        self.model_type = model_type;
        self
    }

    /// Get trading action from signal
    pub fn action(&self, threshold: f64) -> TradingAction {
        TradingAction::from_signal(self.signal, threshold)
    }
}

/// Model weight configuration for ensemble
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelWeight {
    /// Model identifier
    pub model_id: String,

    /// Static weight (base contribution)
    pub static_weight: f64,

    /// Dynamic weight (performance-adjusted)
    pub dynamic_weight: f64,

    /// Recent performance metrics
    pub performance_metrics: PerformanceMetrics,
}

impl ModelWeight {
    /// Create new model weight
    pub fn new(model_id: String, static_weight: f64) -> Self {
        Self {
            model_id,
            static_weight,
            dynamic_weight: static_weight,
            performance_metrics: PerformanceMetrics::default(),
        }
    }

    /// Get effective weight (static * dynamic adjustment)
    pub fn effective_weight(&self) -> f64 {
        self.static_weight * self.dynamic_weight
    }

    /// Update dynamic weight based on recent performance
    pub fn update_dynamic_weight(&mut self) {
        // Simple performance-based adjustment
        // Sharpe factor: target Sharpe ratio of 1.0 as baseline
        let sharpe_factor = (self.performance_metrics.sharpe_ratio / 1.0)
            .min(1.5)
            .max(0.5);
        // Accuracy factor: target accuracy of 0.55 as baseline
        let accuracy_factor = (self.performance_metrics.accuracy / 0.55).min(1.5).max(0.5);

        // Average the two factors (no division by 2 since we want higher weights)
        self.dynamic_weight = (sharpe_factor + accuracy_factor) / 2.0;
    }
}

/// Performance metrics for a model
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerformanceMetrics {
    /// Recent accuracy (0.0 to 1.0)
    pub accuracy: f64,

    /// Recent Sharpe ratio
    pub sharpe_ratio: f64,

    /// Win rate (0.0 to 1.0)
    pub win_rate: f64,

    /// Number of predictions
    pub prediction_count: u64,

    /// Average latency in microseconds
    pub avg_latency_us: u64,
}

impl Default for PerformanceMetrics {
    fn default() -> Self {
        Self {
            accuracy: 0.5,
            sharpe_ratio: 1.0,
            win_rate: 0.5,
            prediction_count: 0,
            avg_latency_us: 0,
        }
    }
}

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

    #[test]
    fn test_trading_action_from_signal() {
        assert_eq!(TradingAction::from_signal(0.6, 0.5), TradingAction::Buy);
        assert_eq!(TradingAction::from_signal(-0.6, 0.5), TradingAction::Sell);
        assert_eq!(TradingAction::from_signal(0.3, 0.5), TradingAction::Hold);
    }

    #[test]
    fn test_ensemble_decision_creation() {
        let mut votes = HashMap::new();
        votes.insert(
            "DQN".to_owned(),
            ModelVote::new("DQN".to_owned(), 0.8, 0.9, 0.33),
        );
        votes.insert(
            "PPO".to_owned(),
            ModelVote::new("PPO".to_owned(), 0.7, 0.85, 0.33),
        );
        votes.insert(
            "TFT".to_owned(),
            ModelVote::new("TFT".to_owned(), 0.6, 0.8, 0.34),
        );

        let decision = EnsembleDecision::new(TradingAction::Buy, 0.85, 0.7, 0.1, votes);

        assert_eq!(decision.action, TradingAction::Buy);
        assert_eq!(decision.confidence, 0.85);
        assert_eq!(decision.signal, 0.7);
        assert_eq!(decision.disagreement_rate, 0.1);
        assert_eq!(decision.model_count(), 3);
        assert!(!decision.is_high_disagreement());
    }

    #[test]
    fn test_high_disagreement_detection() {
        let votes = HashMap::new();
        let decision = EnsembleDecision::new(TradingAction::Hold, 0.3, 0.0, 0.6, votes);

        assert!(decision.is_high_disagreement());
    }

    #[test]
    fn test_model_weight_adjustment() {
        let mut weight = ModelWeight::new("DQN".to_owned(), 0.33);

        // High performance scenario
        weight.performance_metrics.sharpe_ratio = 2.0;
        weight.performance_metrics.accuracy = 0.6;
        weight.update_dynamic_weight();

        assert!(weight.dynamic_weight > 0.8); // Should increase weight

        // Low performance scenario
        weight.performance_metrics.sharpe_ratio = 0.5;
        weight.performance_metrics.accuracy = 0.4;
        weight.update_dynamic_weight();

        assert!(weight.dynamic_weight < 0.7); // Should decrease weight
    }
}
