//! Parametric `VaR` calculation using variance-covariance method
//! Production implementation for risk management

use anyhow::Result;
use common::types::Price;
use nalgebra::{DMatrix, DVector};
use num::FromPrimitive;
use rust_decimal::Decimal;
use std::collections::HashMap;
/// Parametric `VaR` calculator using variance-covariance method
#[derive(Debug)]
pub struct ParametricVaR {
    /// Confidence level (e.g., 0.95 for 95% `VaR`)
    confidence_level: f64,
    /// Covariance matrix of asset returns
    covariance_matrix: Option<DMatrix<f64>>,
    /// Mean returns vector
    mean_returns: Option<DVector<f64>>,
    /// Asset symbols
    symbols: Vec<String>,
}

impl ParametricVaR {
    /// Create new parametric `VaR` calculator
    #[must_use]
    pub const fn new(confidence_level: f64) -> Self {
        Self {
            confidence_level,
            covariance_matrix: None,
            mean_returns: None,
            symbols: Vec::new(),
        }
    }

    /// Update covariance matrix with new market data
    pub fn update_covariance_matrix(
        &mut self,
        returns_data: &HashMap<String, Vec<f64>>,
    ) -> Result<()> {
        let symbols: Vec<String> = returns_data.keys().cloned().collect();
        let n_assets = symbols.len();

        if n_assets == 0 {
            return Err(anyhow::anyhow!(
                "No assets provided for covariance calculation"
            ));
        }

        // Build returns matrix
        let mut returns_matrix = Vec::new();
        let mut min_length = usize::MAX;

        // Find minimum length across all return series
        for returns in returns_data.values() {
            min_length = min_length.min(returns.len());
        }

        // Build matrix with consistent length
        for symbol in &symbols {
            if let Some(returns) = returns_data.get(symbol) {
                if let Some(slice) = returns.get(returns.len().saturating_sub(min_length)..) {
                    returns_matrix.push(slice.to_vec());
                }
            }
        }

        // Calculate covariance matrix
        let mut covariance = DMatrix::zeros(n_assets, n_assets);
        let mut means = DVector::zeros(n_assets);

        // Calculate means
        for (i, returns) in returns_matrix.iter().enumerate() {
            if let Some(mean_val) = means.get_mut(i) {
                *mean_val = returns.iter().sum::<f64>() / returns.len() as f64;
            }
        }

        // Calculate covariances
        for i in 0..n_assets {
            for j in 0..n_assets {
                let mut covar = 0.0;
                for k in 0..min_length {
                    let first_asset_return = returns_matrix
                        .get(i)
                        .and_then(|row| row.get(k))
                        .copied()
                        .unwrap_or(0.0);
                    let second_asset_return = returns_matrix
                        .get(j)
                        .and_then(|row| row.get(k))
                        .copied()
                        .unwrap_or(0.0);
                    let mean_i = means.get(i).copied().unwrap_or(0.0);
                    let mean_j = means.get(j).copied().unwrap_or(0.0);
                    covar += (first_asset_return - mean_i) * (second_asset_return - mean_j);
                }
                if let Some(cell) = covariance.get_mut((i, j)) {
                    *cell = covar / (min_length - 1) as f64;
                }
            }
        }

        self.covariance_matrix = Some(covariance);
        self.mean_returns = Some(means);
        self.symbols = symbols;

        Ok(())
    }

    /// Calculate `VaR` for given portfolio weights
    pub fn calculate_var(
        &self,
        portfolio_weights: &DVector<f64>,
        portfolio_value: Price,
    ) -> Result<Decimal> {
        let covar_matrix = self
            .covariance_matrix
            .as_ref()
            .ok_or_else(|| anyhow::anyhow!("Covariance matrix not initialized"))?;

        // Calculate portfolio variance: w^T * Σ * w
        let portfolio_variance = portfolio_weights.transpose() * covar_matrix * portfolio_weights;
        let portfolio_vol = portfolio_variance
            .get(0)
            .copied()
            .ok_or_else(|| {
                anyhow::anyhow!(
                    "Failed to calculate portfolio variance - invalid matrix dimensions"
                )
            })?
            .sqrt();

        // Get z-score for confidence level
        let z_score = Self::get_z_score(self.confidence_level);

        // VaR = z * σ * V (where V is portfolio value)
        let var_percentage = z_score * portfolio_vol;
        let portfolio_value_f64 = portfolio_value
            .to_string()
            .parse::<f64>()
            .map_err(|e| anyhow::anyhow!("Failed to parse portfolio value: {e}"))?;

        let var_amount = var_percentage * portfolio_value_f64;

        FromPrimitive::from_f64(var_amount.abs())
            .ok_or_else(|| anyhow::anyhow!("Failed to convert VaR amount to Decimal: {var_amount}"))
    }

    /// Get z-score for given confidence level using Abramowitz-Stegun
    /// rational approximation for the inverse normal CDF.
    /// Accurate to ~4.5e-4 across the full range.
    fn get_z_score(confidence_level: f64) -> f64 {
        let p = 1.0 - confidence_level;
        if p <= 0.0 || p >= 1.0 {
            return 2.326; // fallback to 99% z-score for invalid inputs
        }

        let t = (-2.0_f64 * p.ln()).sqrt();

        // Coefficients from Abramowitz-Stegun formula 26.2.23
        let c0 = 2.515517;
        let c1 = 0.802853;
        let c2 = 0.010328;
        let d1 = 1.432788;
        let d2 = 0.189269;
        let d3 = 0.001308;

        t - (c0 + c1 * t + c2 * t * t) / (1.0 + d1 * t + d2 * t * t + d3 * t * t * t)
    }

    /// Calculate component `VaR` (marginal contribution to `VaR`)
    pub fn calculate_component_var(
        &self,
        portfolio_weights: &DVector<f64>,
        portfolio_value: Price,
    ) -> Result<Vec<Price>> {
        let covar_matrix = self
            .covariance_matrix
            .as_ref()
            .ok_or_else(|| anyhow::anyhow!("Covariance matrix not initialized"))?;

        let portfolio_variance = portfolio_weights.transpose() * covar_matrix * portfolio_weights;
        let portfolio_vol = portfolio_variance.get(0).copied().ok_or_else(|| {
            anyhow::anyhow!("Failed to calculate portfolio variance in component VaR - invalid matrix dimensions")
        })?.sqrt();

        let z_score = Self::get_z_score(self.confidence_level);
        let portfolio_value_f64 = portfolio_value
            .to_string()
            .parse::<f64>()
            .map_err(|e| anyhow::anyhow!("Failed to parse portfolio value: {e}"))?;

        let mut component_vars = Vec::new();

        for i in 0..portfolio_weights.len() {
            // Marginal VaR = (Σ * w) / σ_p
            let mut marginal_var = 0.0;
            for j in 0..portfolio_weights.len() {
                let covar_val = covar_matrix.get((i, j)).copied().unwrap_or(0.0);
                let weight_j = portfolio_weights.get(j).copied().unwrap_or(0.0);
                marginal_var += covar_val * weight_j;
            }
            marginal_var /= portfolio_vol;

            // Component VaR = weight * marginal VaR * z-score * portfolio value
            let weight_i = portfolio_weights.get(i).copied().unwrap_or(0.0);
            let component_var = weight_i * marginal_var * z_score * portfolio_value_f64;

            component_vars.push(Price::from_f64(component_var.abs()).unwrap_or(Price::ZERO));
        }

        Ok(component_vars.into_iter().collect())
    }
}

#[cfg(test)]
#[allow(clippy::assertions_on_result_states, clippy::str_to_string)]
mod tests {
    use super::*;
    use common::types::Price;
    use nalgebra::DVector;
    use std::collections::HashMap;

    #[test]
    fn test_parametric_var_new() {
        let var_calc = ParametricVaR::new(0.95);
        assert!((var_calc.confidence_level - 0.95).abs() < 1e-6);
        assert!(var_calc.covariance_matrix.is_none());
        assert!(var_calc.mean_returns.is_none());
        assert_eq!(var_calc.symbols.len(), 0);
    }

    #[test]
    fn test_z_score_accuracy() {
        assert!(
            (ParametricVaR::get_z_score(0.90) - 1.282).abs() < 0.01,
            "90% z-score: got {}, expected ~1.282",
            ParametricVaR::get_z_score(0.90)
        );
        assert!(
            (ParametricVaR::get_z_score(0.95) - 1.645).abs() < 0.01,
            "95% z-score: got {}, expected ~1.645",
            ParametricVaR::get_z_score(0.95)
        );
        assert!(
            (ParametricVaR::get_z_score(0.99) - 2.326).abs() < 0.01,
            "99% z-score: got {}, expected ~2.326",
            ParametricVaR::get_z_score(0.99)
        );
        assert!(
            (ParametricVaR::get_z_score(0.999) - 3.090).abs() < 0.05,
            "99.9% z-score should be ~3.09, not ~2.48; got {}",
            ParametricVaR::get_z_score(0.999)
        );
    }

    #[test]
    fn test_z_score_edge_cases() {
        // 50% confidence should give z near 0
        let z = ParametricVaR::get_z_score(0.5);
        assert!(
            z.abs() < 0.1,
            "50% z-score should be ~0.0, got {}",
            z
        );
        // Very high confidence should give large z
        let z_high = ParametricVaR::get_z_score(0.9999);
        assert!(
            z_high > 3.0,
            "99.99% z-score should be >3.0, got {}",
            z_high
        );
        // Boundary: p=0 or p=1 should return fallback
        assert!((ParametricVaR::get_z_score(1.0) - 2.326).abs() < 1e-6);
        assert!((ParametricVaR::get_z_score(0.0) - 2.326).abs() < 1e-6);
    }

    #[test]
    fn test_update_covariance_matrix_empty_data() {
        let mut var_calc = ParametricVaR::new(0.95);
        let empty_data: HashMap<String, Vec<f64>> = HashMap::new();

        let result = var_calc.update_covariance_matrix(&empty_data);
        assert!(result.is_err());
        assert!(result
            .unwrap_err()
            .to_string()
            .contains("No assets provided"));
    }

    #[test]
    fn test_update_covariance_matrix_single_asset() -> Result<()> {
        let mut var_calc = ParametricVaR::new(0.95);
        let mut returns_data = HashMap::new();
        returns_data.insert("AAPL".to_string(), vec![0.01, -0.02, 0.03, -0.01, 0.02]);

        var_calc.update_covariance_matrix(&returns_data)?;

        assert!(var_calc.covariance_matrix.is_some());
        assert!(var_calc.mean_returns.is_some());
        assert_eq!(var_calc.symbols.len(), 1);
        assert_eq!(var_calc.symbols[0], "AAPL");

        Ok(())
    }

    #[test]
    fn test_update_covariance_matrix_multiple_assets() -> Result<()> {
        let mut var_calc = ParametricVaR::new(0.95);
        let mut returns_data = HashMap::new();
        returns_data.insert("AAPL".to_string(), vec![0.01, -0.02, 0.03, -0.01]);
        returns_data.insert("MSFT".to_string(), vec![0.02, -0.01, 0.01, 0.00]);
        returns_data.insert("GOOGL".to_string(), vec![-0.01, 0.03, -0.02, 0.01]);

        var_calc.update_covariance_matrix(&returns_data)?;

        assert!(var_calc.covariance_matrix.is_some());
        assert!(var_calc.mean_returns.is_some());
        assert_eq!(var_calc.symbols.len(), 3);

        let covar = var_calc.covariance_matrix.as_ref().unwrap();
        assert_eq!(covar.nrows(), 3);
        assert_eq!(covar.ncols(), 3);

        Ok(())
    }

    #[test]
    fn test_covariance_matrix_symmetry() -> Result<()> {
        let mut var_calc = ParametricVaR::new(0.95);
        let mut returns_data = HashMap::new();
        returns_data.insert("AAPL".to_string(), vec![0.01, -0.02, 0.03, -0.01, 0.02]);
        returns_data.insert("MSFT".to_string(), vec![0.02, -0.01, 0.01, 0.00, -0.01]);

        var_calc.update_covariance_matrix(&returns_data)?;

        let covar = var_calc.covariance_matrix.as_ref().unwrap();
        // Covariance matrix should be symmetric
        for i in 0..covar.nrows() {
            for j in 0..covar.ncols() {
                let val_ij = covar.get((i, j)).copied().unwrap_or(0.0);
                let val_ji = covar.get((j, i)).copied().unwrap_or(0.0);
                assert!(
                    (val_ij - val_ji).abs() < 1e-10,
                    "Covariance matrix not symmetric"
                );
            }
        }

        Ok(())
    }

    #[test]
    fn test_calculate_var_without_covariance() {
        let var_calc = ParametricVaR::new(0.95);
        let weights = DVector::from_vec(vec![1.0]);
        let portfolio_value = Price::from_f64(1000000.0).unwrap();

        let result = var_calc.calculate_var(&weights, portfolio_value);
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("not initialized"));
    }

    #[test]
    fn test_calculate_var_single_asset() -> Result<()> {
        let mut var_calc = ParametricVaR::new(0.95);
        let mut returns_data = HashMap::new();
        // Returns with known standard deviation
        returns_data.insert("AAPL".to_string(), vec![0.02, -0.02, 0.03, -0.01, 0.01]);

        var_calc.update_covariance_matrix(&returns_data)?;

        let weights = DVector::from_vec(vec![1.0]);
        let portfolio_value = Price::from_f64(1000000.0)?;

        let var = var_calc.calculate_var(&weights, portfolio_value)?;

        // VaR should be positive
        assert!(var > Decimal::ZERO);
        // VaR should be reasonable (less than portfolio value)
        assert!(var < Decimal::from(1000000));

        Ok(())
    }

    #[test]
    fn test_calculate_var_portfolio() -> Result<()> {
        let mut var_calc = ParametricVaR::new(0.95);
        let mut returns_data = HashMap::new();
        returns_data.insert("AAPL".to_string(), vec![0.01, -0.02, 0.03, -0.01, 0.02]);
        returns_data.insert("MSFT".to_string(), vec![0.02, -0.01, 0.01, 0.00, -0.01]);

        var_calc.update_covariance_matrix(&returns_data)?;

        // Equal weight portfolio
        let weights = DVector::from_vec(vec![0.5, 0.5]);
        let portfolio_value = Price::from_f64(1000000.0)?;

        let var = var_calc.calculate_var(&weights, portfolio_value)?;

        assert!(var > Decimal::ZERO);
        assert!(var < Decimal::from(1000000));

        Ok(())
    }

    #[test]
    fn test_calculate_var_different_confidence_levels() -> Result<()> {
        let mut returns_data = HashMap::new();
        returns_data.insert("AAPL".to_string(), vec![0.01, -0.02, 0.03, -0.01, 0.02]);

        let portfolio_value = Price::from_f64(1000000.0)?;
        let weights = DVector::from_vec(vec![1.0]);

        // 90% confidence
        let mut var_90 = ParametricVaR::new(0.90);
        var_90.update_covariance_matrix(&returns_data)?;
        let var_90_result = var_90.calculate_var(&weights, portfolio_value)?;

        // 95% confidence
        let mut var_95 = ParametricVaR::new(0.95);
        var_95.update_covariance_matrix(&returns_data)?;
        let var_95_result = var_95.calculate_var(&weights, portfolio_value)?;

        // 99% confidence
        let mut var_99 = ParametricVaR::new(0.99);
        var_99.update_covariance_matrix(&returns_data)?;
        let var_99_result = var_99.calculate_var(&weights, portfolio_value)?;

        // Higher confidence should produce higher VaR
        assert!(var_99_result > var_95_result);
        assert!(var_95_result > var_90_result);

        Ok(())
    }

    #[test]
    fn test_calculate_component_var_without_covariance() {
        let var_calc = ParametricVaR::new(0.95);
        let weights = DVector::from_vec(vec![1.0]);
        let portfolio_value = Price::from_f64(1000000.0).unwrap();

        let result = var_calc.calculate_component_var(&weights, portfolio_value);
        assert!(result.is_err());
    }

    #[test]
    fn test_calculate_component_var() -> Result<()> {
        let mut var_calc = ParametricVaR::new(0.95);
        let mut returns_data = HashMap::new();
        returns_data.insert("AAPL".to_string(), vec![0.01, -0.02, 0.03, -0.01, 0.02]);
        returns_data.insert("MSFT".to_string(), vec![0.02, -0.01, 0.01, 0.00, -0.01]);
        returns_data.insert("GOOGL".to_string(), vec![-0.01, 0.03, -0.02, 0.01, 0.02]);

        var_calc.update_covariance_matrix(&returns_data)?;

        let weights = DVector::from_vec(vec![0.4, 0.3, 0.3]);
        let portfolio_value = Price::from_f64(1000000.0)?;

        let component_vars = var_calc.calculate_component_var(&weights, portfolio_value)?;

        // Should have one component VaR for each asset
        assert_eq!(component_vars.len(), 3);

        // All component VaRs should be non-negative
        for comp_var in &component_vars {
            assert!(*comp_var >= Price::ZERO);
        }

        Ok(())
    }

    #[test]
    fn test_component_var_sum_equals_total_var() -> Result<()> {
        let mut var_calc = ParametricVaR::new(0.95);
        let mut returns_data = HashMap::new();
        returns_data.insert("AAPL".to_string(), vec![0.01, -0.02, 0.03, -0.01, 0.02]);
        returns_data.insert("MSFT".to_string(), vec![0.02, -0.01, 0.01, 0.00, -0.01]);

        var_calc.update_covariance_matrix(&returns_data)?;

        let weights = DVector::from_vec(vec![0.6, 0.4]);
        let portfolio_value = Price::from_f64(1000000.0)?;

        let total_var = var_calc.calculate_var(&weights, portfolio_value)?;
        let component_vars = var_calc.calculate_component_var(&weights, portfolio_value)?;

        let sum_component_vars: Decimal = component_vars
            .iter()
            .map(|p| p.to_string().parse::<Decimal>().unwrap_or(Decimal::ZERO))
            .sum();

        // Component VaRs should sum to total VaR (within tolerance)
        let diff = (total_var - sum_component_vars).abs();
        let tolerance = total_var * Decimal::from_f64_retain(0.01).unwrap(); // 1% tolerance
        assert!(
            diff < tolerance,
            "Component VaRs don't sum to total: total={}, sum={}",
            total_var,
            sum_component_vars
        );

        Ok(())
    }

    #[test]
    fn test_mean_returns_calculation() -> Result<()> {
        let mut var_calc = ParametricVaR::new(0.95);
        let mut returns_data = HashMap::new();
        let returns = vec![0.02, -0.02, 0.04, -0.04]; // Mean = 0.0
        returns_data.insert("TEST".to_string(), returns.clone());

        var_calc.update_covariance_matrix(&returns_data)?;

        let means = var_calc.mean_returns.as_ref().unwrap();
        let calculated_mean = means.get(0).copied().unwrap_or(f64::NAN);

        let expected_mean: f64 = returns.iter().sum::<f64>() / returns.len() as f64;
        assert!((calculated_mean - expected_mean).abs() < 1e-10);

        Ok(())
    }

    #[test]
    fn test_unequal_length_returns() -> Result<()> {
        let mut var_calc = ParametricVaR::new(0.95);
        let mut returns_data = HashMap::new();
        returns_data.insert("AAPL".to_string(), vec![0.01, -0.02, 0.03, -0.01, 0.02]);
        returns_data.insert("MSFT".to_string(), vec![0.02, -0.01, 0.01]); // Shorter

        // Should handle unequal lengths by truncating to minimum
        let result = var_calc.update_covariance_matrix(&returns_data);
        assert!(result.is_ok());

        Ok(())
    }

    #[test]
    fn test_portfolio_with_zero_weights() -> Result<()> {
        let mut var_calc = ParametricVaR::new(0.95);
        let mut returns_data = HashMap::new();
        returns_data.insert("AAPL".to_string(), vec![0.01, -0.02, 0.03, -0.01, 0.02]);
        returns_data.insert("MSFT".to_string(), vec![0.02, -0.01, 0.01, 0.00, -0.01]);

        var_calc.update_covariance_matrix(&returns_data)?;

        // One asset with zero weight
        let weights = DVector::from_vec(vec![1.0, 0.0]);
        let portfolio_value = Price::from_f64(1000000.0)?;

        let var = var_calc.calculate_var(&weights, portfolio_value)?;
        assert!(var > Decimal::ZERO);

        Ok(())
    }
}
