//! Validation Metrics for DQN Training
//!
//! Comprehensive validation system to prevent training failures like:
//! - Loss explosion (Trial #35: 1.207 → 2,612 at epoch 500)
//! - Overfitting (train/val divergence)
//! - Action collapse (HOLD > 90%)
//! - Exploration collapse (entropy < 0.1)
//! - Q-value instability (divergence, explosion)

use serde::{Deserialize, Serialize};

/// Comprehensive validation metrics tracked per epoch
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationMetrics {
    /// Current epoch number
    pub epoch: usize,
    
    /// Training loss (on training set)
    pub train_loss: f32,
    
    /// Validation loss (on holdout set)
    pub val_loss: f32,
    
    /// Mean Q-value across all actions
    pub q_value_mean: f32,
    
    /// Standard deviation of Q-values
    pub q_value_std: f32,
    
    /// Action distribution [BUY%, SELL%, HOLD%]
    pub action_distribution: [f32; 3],
    
    /// Policy entropy H = -Σ p_i log(p_i)
    /// Range: 0.0 (deterministic) to 1.099 (uniform over 3 actions)
    pub policy_entropy: f32,
    
    /// Win rate (% profitable actions on validation set)
    pub win_rate: f32,
    
    /// Sharpe ratio (reward_mean / reward_std on validation set)
    pub sharpe_ratio: f32,
    
    /// Gradient norm (for explosion detection)
    pub gradient_norm: f32,
}

impl ValidationMetrics {
    /// Create new validation metrics
    pub fn new(
        epoch: usize,
        train_loss: f32,
        val_loss: f32,
        q_value_mean: f32,
        q_value_std: f32,
        action_distribution: [f32; 3],
        policy_entropy: f32,
        win_rate: f32,
        sharpe_ratio: f32,
        gradient_norm: f32,
    ) -> Self {
        Self {
            epoch,
            train_loss,
            val_loss,
            q_value_mean,
            q_value_std,
            action_distribution,
            policy_entropy,
            win_rate,
            sharpe_ratio,
            gradient_norm,
        }
    }

    /// Check if model is overfitting based on train/val divergence
    ///
    /// Overfitting signals:
    /// 1. Train loss decreasing while validation loss increasing (5 epoch trend)
    /// 2. Train/val loss ratio > 2.0 (memorization)
    /// 3. Action distribution mismatch > 30% between train/val
    ///
    /// # Arguments
    /// * `history` - Previous validation metrics (last 5+ epochs)
    ///
    /// # Returns
    /// `true` if overfitting detected
    pub fn is_overfitting(&self, history: &[Self]) -> bool {
        if history.len() < 5 {
            return false;
        }
        
        let recent = &history[history.len()-5..];
        
        // Signal 1: Train loss decreasing, validation loss increasing
        let train_decreasing = recent.windows(2)
            .all(|w| w[1].train_loss < w[0].train_loss);
        let val_increasing = recent.windows(2)
            .all(|w| w[1].val_loss > w[0].val_loss);
        
        if train_decreasing && val_increasing {
            return true;
        }
        
        // Signal 2: Train/val ratio > 2.0 (severe overfitting)
        if self.val_loss > 0.0 && self.train_loss / self.val_loss > 2.0 {
            return true;
        }
        
        false
    }
    
    /// Check if model is production ready
    ///
    /// Production criteria:
    /// 1. Validation loss < 5.0 (reasonable accuracy)
    /// 2. Q-values finite and bounded (|mean| < 1000)
    /// 3. Action diversity (HOLD < 70%)
    /// 4. Sufficient exploration (entropy > 0.1)
    /// 5. Profitability (win_rate > 50%, Sharpe > 1.5)
    ///
    /// # Returns
    /// `true` if all production criteria met
    pub fn is_production_ready(&self) -> bool {
        // Criterion 1: Reasonable validation loss
        if self.val_loss >= 5.0 {
            return false;
        }
        
        // Criterion 2: Q-values stable (finite and bounded)
        if !self.q_value_mean.is_finite() || self.q_value_mean.abs() >= 1000.0 {
            return false;
        }
        
        // Criterion 3: Action diversity (not just HOLD)
        if self.action_distribution[2] >= 0.7 {
            return false;
        }
        
        // Criterion 4: Sufficient exploration
        if self.policy_entropy <= 0.1 {
            return false;
        }
        
        // Criterion 5: Profitability
        if self.win_rate <= 0.5 || self.sharpe_ratio <= 1.5 {
            return false;
        }
        
        true
    }
    
    /// Check if Q-values have exploded (numerical instability)
    ///
    /// # Returns
    /// `true` if Q-values > 10,000 (explosion threshold)
    pub fn has_q_value_explosion(&self) -> bool {
        self.q_value_mean.abs() > 10_000.0
    }
    
    /// Check if gradients have exploded (training divergence)
    ///
    /// # Returns
    /// `true` if gradient norm > 100
    pub fn has_gradient_explosion(&self) -> bool {
        self.gradient_norm > 100.0
    }
    
    /// Check if action distribution has collapsed (HOLD > 90%)
    ///
    /// # Arguments
    /// * `threshold` - HOLD percentage threshold (default: 0.9)
    ///
    /// # Returns
    /// `true` if HOLD action exceeds threshold
    pub fn has_action_collapse(&self, threshold: f32) -> bool {
        self.action_distribution[2] > threshold
    }
    
    /// Check if policy entropy has collapsed (exploration failure)
    ///
    /// # Arguments
    /// * `threshold` - Minimum acceptable entropy (default: 0.1)
    ///
    /// # Returns
    /// `true` if entropy below threshold
    pub fn has_entropy_collapse(&self, threshold: f32) -> bool {
        self.policy_entropy < threshold
    }
    
    /// Get train/val loss ratio (overfitting indicator)
    ///
    /// # Returns
    /// Ratio of train_loss to val_loss, or 0.0 if val_loss is zero
    pub fn train_val_ratio(&self) -> f32 {
        if self.val_loss > 0.0 {
            self.train_loss / self.val_loss
        } else {
            0.0
        }
    }
}

/// Early stopping criteria enumeration
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum EarlyStopCriteria {
    /// Stop when validation loss increases for N epochs
    ValidationLossIncrease { patience: usize },
    
    /// Stop when overfitting detected (train/val divergence)
    Overfitting,
    
    /// Stop when HOLD action > threshold for N epochs
    ActionCollapse { hold_threshold: f32, patience: usize },
    
    /// Stop when entropy < threshold for N epochs
    EntropyCollapse { threshold: f32, patience: usize },
    
    /// Stop when Q-values > threshold (explosion)
    QValueExplosion { threshold: f32 },
    
    /// Stop when gradient norm > threshold (explosion)
    GradientExplosion { threshold: f32 },
    
    /// Stop if ANY of the above criteria trigger
    All,
}

impl Default for EarlyStopCriteria {
    fn default() -> Self {
        Self::All
    }
}

impl EarlyStopCriteria {
    /// Check if early stopping should trigger
    ///
    /// # Arguments
    /// * `current` - Current epoch metrics
    /// * `history` - Historical metrics
    ///
    /// # Returns
    /// `Some(reason)` if stopping should occur, `None` otherwise
    pub fn should_stop(&self, current: &ValidationMetrics, history: &[ValidationMetrics]) -> Option<String> {
        match self {
            Self::ValidationLossIncrease { patience } => {
                if history.len() < *patience {
                    return None;
                }
                
                let recent = &history[history.len() - patience..];
                let increasing = recent.windows(2)
                    .all(|w| w[1].val_loss > w[0].val_loss);
                
                if increasing {
                    Some(format!("Validation loss increased for {} epochs", patience))
                } else {
                    None
                }
            }
            
            Self::Overfitting => {
                if current.is_overfitting(history) {
                    Some(format!("Overfitting detected (train/val ratio: {:.2})", current.train_val_ratio()))
                } else {
                    None
                }
            }
            
            Self::ActionCollapse { hold_threshold, patience } => {
                if history.len() < *patience {
                    return None;
                }
                
                let recent = &history[history.len() - patience..];
                let collapsed = recent.iter()
                    .all(|m| m.action_distribution[2] > *hold_threshold);
                
                if collapsed {
                    Some(format!("Action collapse: HOLD > {:.0}% for {} epochs", hold_threshold * 100.0, patience))
                } else {
                    None
                }
            }
            
            Self::EntropyCollapse { threshold, patience } => {
                if history.len() < *patience {
                    return None;
                }
                
                let recent = &history[history.len() - patience..];
                let collapsed = recent.iter()
                    .all(|m| m.policy_entropy < *threshold);
                
                if collapsed {
                    Some(format!("Entropy collapse: entropy < {:.2} for {} epochs", threshold, patience))
                } else {
                    None
                }
            }
            
            Self::QValueExplosion { threshold } => {
                if current.q_value_mean.abs() > *threshold {
                    Some(format!("Q-value explosion: |Q| = {:.2} > {}", current.q_value_mean, threshold))
                } else {
                    None
                }
            }
            
            Self::GradientExplosion { threshold } => {
                if current.gradient_norm > *threshold {
                    Some(format!("Gradient explosion: norm = {:.2} > {}", current.gradient_norm, threshold))
                } else {
                    None
                }
            }
            
            Self::All => {
                // Check all criteria
                let criteria = [
                    Self::ValidationLossIncrease { patience: 5 },
                    Self::Overfitting,
                    Self::ActionCollapse { hold_threshold: 0.9, patience: 10 },
                    Self::EntropyCollapse { threshold: 0.1, patience: 10 },
                    Self::QValueExplosion { threshold: 10_000.0 },
                    Self::GradientExplosion { threshold: 100.0 },
                ];
                
                for criterion in &criteria {
                    if let Some(reason) = criterion.should_stop(current, history) {
                        return Some(reason);
                    }
                }
                
                None
            }
        }
    }
}

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

    #[test]
    fn test_overfitting_detection_train_val_divergence() {
        // Create history where train↓ but val↑
        let history = vec![
            ValidationMetrics::new(1, 2.0, 2.0, 1.0, 0.1, [0.3, 0.3, 0.4], 0.5, 0.6, 1.8, 0.5),
            ValidationMetrics::new(2, 1.8, 2.1, 1.0, 0.1, [0.3, 0.3, 0.4], 0.5, 0.6, 1.8, 0.5),
            ValidationMetrics::new(3, 1.6, 2.2, 1.0, 0.1, [0.3, 0.3, 0.4], 0.5, 0.6, 1.8, 0.5),
            ValidationMetrics::new(4, 1.4, 2.3, 1.0, 0.1, [0.3, 0.3, 0.4], 0.5, 0.6, 1.8, 0.5),
            ValidationMetrics::new(5, 1.2, 2.4, 1.0, 0.1, [0.3, 0.3, 0.4], 0.5, 0.6, 1.8, 0.5),
        ];
        
        let current = ValidationMetrics::new(6, 1.0, 2.5, 1.0, 0.1, [0.3, 0.3, 0.4], 0.5, 0.6, 1.8, 0.5);
        
        assert!(current.is_overfitting(&history), "Should detect overfitting from train/val divergence");
    }

    #[test]
    fn test_overfitting_detection_high_ratio() {
        let history = vec![];
        let current = ValidationMetrics::new(1, 1.0, 3.0, 1.0, 0.1, [0.3, 0.3, 0.4], 0.5, 0.6, 1.8, 0.5);
        
        assert!(current.is_overfitting(&history), "Should detect overfitting from train/val ratio > 2.0");
    }

    #[test]
    fn test_production_ready_all_criteria_met() {
        let metrics = ValidationMetrics::new(
            10,
            1.0,     // train_loss
            2.0,     // val_loss < 5.0 ✓
            5.0,     // q_value_mean (finite, |x| < 1000) ✓
            1.0,     // q_value_std
            [0.3, 0.4, 0.3],  // HOLD < 70% ✓
            0.8,     // policy_entropy > 0.1 ✓
            0.55,    // win_rate > 0.5 ✓
            2.0,     // sharpe_ratio > 1.5 ✓
            0.5,     // gradient_norm
        );
        
        assert!(metrics.is_production_ready(), "Should pass all production criteria");
    }

    #[test]
    fn test_production_not_ready_high_hold() {
        let metrics = ValidationMetrics::new(
            10,
            1.0,
            2.0,
            5.0,
            1.0,
            [0.1, 0.1, 0.8],  // HOLD > 70% ✗
            0.8,
            0.55,
            2.0,
            0.5,
        );
        
        assert!(!metrics.is_production_ready(), "Should fail due to high HOLD %");
    }

    #[test]
    fn test_q_value_explosion() {
        let metrics = ValidationMetrics::new(10, 1.0, 2.0, 15_000.0, 1.0, [0.3, 0.3, 0.4], 0.5, 0.6, 1.8, 0.5);
        assert!(metrics.has_q_value_explosion(), "Should detect Q-value explosion");
    }

    #[test]
    fn test_gradient_explosion() {
        let metrics = ValidationMetrics::new(10, 1.0, 2.0, 5.0, 1.0, [0.3, 0.3, 0.4], 0.5, 0.6, 1.8, 150.0);
        assert!(metrics.has_gradient_explosion(), "Should detect gradient explosion");
    }

    #[test]
    fn test_action_collapse() {
        let metrics = ValidationMetrics::new(10, 1.0, 2.0, 5.0, 1.0, [0.05, 0.05, 0.95], 0.5, 0.6, 1.8, 0.5);
        assert!(metrics.has_action_collapse(0.9), "Should detect action collapse");
    }

    #[test]
    fn test_entropy_collapse() {
        let metrics = ValidationMetrics::new(10, 1.0, 2.0, 5.0, 1.0, [0.3, 0.3, 0.4], 0.05, 0.6, 1.8, 0.5);
        assert!(metrics.has_entropy_collapse(0.1), "Should detect entropy collapse");
    }

    #[test]
    fn test_early_stop_validation_loss_increase() {
        let history = vec![
            ValidationMetrics::new(1, 1.0, 2.0, 1.0, 0.1, [0.3, 0.3, 0.4], 0.5, 0.6, 1.8, 0.5),
            ValidationMetrics::new(2, 1.0, 2.1, 1.0, 0.1, [0.3, 0.3, 0.4], 0.5, 0.6, 1.8, 0.5),
            ValidationMetrics::new(3, 1.0, 2.2, 1.0, 0.1, [0.3, 0.3, 0.4], 0.5, 0.6, 1.8, 0.5),
            ValidationMetrics::new(4, 1.0, 2.3, 1.0, 0.1, [0.3, 0.3, 0.4], 0.5, 0.6, 1.8, 0.5),
            ValidationMetrics::new(5, 1.0, 2.4, 1.0, 0.1, [0.3, 0.3, 0.4], 0.5, 0.6, 1.8, 0.5),
        ];
        
        let current = ValidationMetrics::new(6, 1.0, 2.5, 1.0, 0.1, [0.3, 0.3, 0.4], 0.5, 0.6, 1.8, 0.5);
        let criteria = EarlyStopCriteria::ValidationLossIncrease { patience: 5 };
        
        assert!(criteria.should_stop(&current, &history).is_some(), "Should trigger early stopping");
    }

    #[test]
    fn test_early_stop_all_criteria() {
        let history = vec![];
        let current = ValidationMetrics::new(10, 1.0, 2.0, 15_000.0, 1.0, [0.3, 0.3, 0.4], 0.5, 0.6, 1.8, 0.5);
        let criteria = EarlyStopCriteria::All;
        
        let result = criteria.should_stop(&current, &history);
        assert!(result.is_some(), "Should trigger early stopping due to Q-value explosion");
        assert!(result.unwrap().contains("Q-value explosion"), "Should identify Q-value explosion as reason");
    }
}
