use super::rating::GlickoRating;
use std::f64::consts::PI;

pub struct Glicko2Calculator {
    tau: f64,       // System volatility constraint (0.5)
    epsilon: f64,   // Convergence tolerance (0.000001)
}

impl Glicko2Calculator {
    pub fn new() -> Self {
        Self {
            tau: 0.5,
            epsilon: 0.0001,  // Relaxed for performance
        }
    }
    
    pub fn new_with_tau(tau: f64) -> Self {
        Self { tau, epsilon: 0.0001 }
    }
    
    /// Update a player's rating based on match results
    /// 
    /// Arguments:
    /// - player: Current rating
    /// - results: Vec of (opponent_rating, weighted_outcome)
    ///   where weighted_outcome is from calculate_weighted_score (0.0-1.2)
    pub fn update_rating(
        &self,
        player: &GlickoRating,
        results: &[(GlickoRating, f64)],
    ) -> GlickoRating {
        if results.is_empty() {
            return *player;
        }
        
        let (mu, phi, sigma) = player.to_glicko2_scale();
        
        // Step 1: Calculate g(φⱼ) for each opponent
        let g_values: Vec<f64> = results.iter()
            .map(|(opp, _)| {
                let (_, phi_j, _) = opp.to_glicko2_scale();
                self.g(phi_j)
            })
            .collect();
        
        // Step 2: Calculate E(μ, μⱼ, φⱼ) for each opponent
        let e_values: Vec<f64> = results.iter()
            .zip(g_values.iter())
            .map(|((opp, _), g_j)| {
                let (mu_j, _, _) = opp.to_glicko2_scale();
                self.e(mu, mu_j, *g_j)
            })
            .collect();
        
        // Step 3: Calculate variance (v)
        let v = self.calculate_variance(&g_values, &e_values);
        
        // Step 4: Calculate rating change direction (Δ)
        let delta = self.calculate_delta(&g_values, &e_values, results, v);
        
        // Step 5: Update volatility (σ')
        let sigma_prime = self.update_volatility(phi, sigma, delta, v);
        
        // Step 6: Pre-rating period RD update (φ*)
        let phi_star = (phi.powi(2) + sigma_prime.powi(2)).sqrt();
        
        // Step 7: Update rating and RD
        let phi_prime = 1.0 / (1.0 / phi_star.powi(2) + 1.0 / v).sqrt();
        
        let mu_prime = mu + phi_prime.powi(2) * results.iter()
            .zip(g_values.iter())
            .zip(e_values.iter())
            .map(|(((_, outcome), g_j), e_j)| g_j * (outcome - e_j))
            .sum::<f64>();
        
        // Step 8: Convert back to display scale
        GlickoRating::from_glicko2_scale(mu_prime, phi_prime, sigma_prime)
    }
    
    /// g(φⱼ) = 1 / √(1 + 3φⱼ² / π²)
    fn g(&self, phi_j: f64) -> f64 {
        1.0 / (1.0 + 3.0 * phi_j.powi(2) / PI.powi(2)).sqrt()
    }
    
    /// E(μ, μⱼ, φⱼ) = 1 / (1 + exp(-g(φⱼ) × (μ - μⱼ)))
    fn e(&self, mu: f64, mu_j: f64, g_j: f64) -> f64 {
        1.0 / (1.0 + (-g_j * (mu - mu_j)).exp())
    }
    
    /// v = 1 / Σⱼ [ g(φⱼ)² × E × (1 - E) ]
    fn calculate_variance(&self, g_values: &[f64], e_values: &[f64]) -> f64 {
        let sum: f64 = g_values.iter()
            .zip(e_values.iter())
            .map(|(g_j, e_j)| g_j.powi(2) * e_j * (1.0 - e_j))
            .sum();
        1.0 / sum
    }
    
    /// Δ = v × Σⱼ [ g(φⱼ) × (sⱼ - E) ]
    fn calculate_delta(
        &self,
        g_values: &[f64],
        e_values: &[f64],
        results: &[(GlickoRating, f64)],
        v: f64,
    ) -> f64 {
        v * g_values.iter()
            .zip(e_values.iter())
            .zip(results.iter())
            .map(|((g_j, e_j), (_, outcome))| g_j * (outcome - e_j))
            .sum::<f64>()
    }
    
    /// Update volatility using bisection algorithm (more reliable than Illinois)
    fn update_volatility(&self, phi: f64, sigma: f64, delta: f64, v: f64) -> f64 {
        let ln_sigma_sq = sigma.powi(2).ln();
        let phi_sq = phi.powi(2);
        let delta_sq = delta.powi(2);
        let tau_sq = self.tau.powi(2);
        
        // Helper function for f(x)
        let compute_f = |x: f64| {
            let exp_x = x.exp();
            let denom = 2.0 * (phi_sq + v + exp_x).powi(2);
            let numer = exp_x * (delta_sq - phi_sq - v - exp_x);
            numer / denom - (x - ln_sigma_sq) / tau_sq
        };
        
        // Find initial bracket [a, b] where f(a)*f(b) < 0
        let mut a = ln_sigma_sq;
        let fa_init = compute_f(a);
        
        // Find b such that f(b) has opposite sign
        let mut b = if delta_sq > phi_sq + v {
            (delta_sq - phi_sq - v).ln()
        } else {
            let mut k = 1.0;
            let mut candidate = ln_sigma_sq - k * self.tau;
            while compute_f(candidate) >= 0.0 && k < 10.0 {
                k += 1.0;
                candidate = ln_sigma_sq - k * self.tau;
            }
            candidate
        };
        
        let mut fa = fa_init;
        let mut fb = compute_f(b);
        
        // Ensure proper bracket
        if fa * fb >= 0.0 {
            // If still same sign, just return initial guess
            return sigma;
        }
        
        // Bisection with iteration limit
        let mut iterations = 0;
        const MAX_ITERATIONS: usize = 50;
        
        while (b - a).abs() > self.epsilon && iterations < MAX_ITERATIONS {
            let c = (a + b) / 2.0;
            let fc = compute_f(c);
            
            if fc * fa < 0.0 {
                b = c;
            } else {
                a = c;
                fa = fc;
            }
            
            iterations += 1;
        }
        
        ((a + b) / 2.0 / 2.0).exp()
    }
}

impl Default for Glicko2Calculator {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::glicko::score_weight::calculate_weighted_score;
    
    #[test]
    fn test_rating_unchanged_no_matches() {
        let calc = Glicko2Calculator::new();
        let player = GlickoRating::new_player();
        let results = vec![];
        
        let new_rating = calc.update_rating(&player, &results);
        assert_eq!(new_rating, player);
    }
    
    #[test]
    fn test_score_margin_impact() {
        let calc = Glicko2Calculator::new();
        let player = GlickoRating::new_player();
        let opponent = GlickoRating::new_player();
        
        // Close win (11-9, equal ratings)
        let close_outcome = calculate_weighted_score(player.rating, opponent.rating, 11, 9);
        let close_results = vec![(opponent, close_outcome)];
        let close_new = calc.update_rating(&player, &close_results);
        
        // Blowout win (11-2, equal ratings)
        let blowout_outcome = calculate_weighted_score(player.rating, opponent.rating, 11, 2);
        let blowout_results = vec![(opponent, blowout_outcome)];
        let blowout_new = calc.update_rating(&player, &blowout_results);
        
        // Blowout should give bigger rating boost
        assert!(blowout_new.rating > close_new.rating);
        println!("Close win: {} -> {}", player.rating, close_new.rating);
        println!("Blowout win: {} -> {}", player.rating, blowout_new.rating);
    }
}
