use crate::{Validator, ProofOfContribution};
use rand::Rng;
use serde::{Deserialize, Serialize};
use blake3::Hasher;

/// Algorithm for selecting validators to produce blocks
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SelectionAlgorithm {
    /// Pure weighted random based on contribution score
    WeightedRandom,

    /// Combines score with randomness from previous block
    VerifiableRandom,

    /// Round-robin among top N validators
    RoundRobin { top_n: usize },
}

/// Validator selector for Proof-of-Contribution
pub struct ValidatorSelector {
    algorithm: SelectionAlgorithm,
}

impl ValidatorSelector {
    pub fn new(algorithm: SelectionAlgorithm) -> Self {
        Self { algorithm }
    }

    /// Create with default algorithm
    pub fn default() -> Self {
        Self::new(SelectionAlgorithm::VerifiableRandom)
    }

    /// Select a validator to produce the next block
    pub fn select_validator(
        &self,
        candidates: &[(&Validator, ProofOfContribution)],
        previous_block_hash: &[u8; 32],
        block_height: u64,
    ) -> Option<[u8; 32]> {
        if candidates.is_empty() {
            return None;
        }

        match &self.algorithm {
            SelectionAlgorithm::WeightedRandom => {
                self.select_weighted_random(candidates)
            }

            SelectionAlgorithm::VerifiableRandom => {
                self.select_verifiable_random(candidates, previous_block_hash, block_height)
            }

            SelectionAlgorithm::RoundRobin { top_n } => {
                self.select_round_robin(candidates, *top_n, block_height)
            }
        }
    }

    /// Weighted random selection based on contribution score
    fn select_weighted_random(
        &self,
        candidates: &[(&Validator, ProofOfContribution)],
    ) -> Option<[u8; 32]> {
        let total_score: f64 = candidates
            .iter()
            .map(|(_, poc)| poc.total_score())
            .sum();

        if total_score == 0.0 {
            // Fallback to uniform random
            let mut rng = rand::thread_rng();
            let idx = rng.gen_range(0..candidates.len());
            return Some(candidates[idx].0.pubkey);
        }

        let mut rng = rand::thread_rng();
        let mut random_point = rng.gen::<f64>() * total_score;

        for (validator, poc) in candidates {
            random_point -= poc.total_score();
            if random_point <= 0.0 {
                return Some(validator.pubkey);
            }
        }

        // Fallback (shouldn't happen)
        Some(candidates[0].0.pubkey)
    }

    /// Verifiable random selection using previous block hash
    /// This makes selection deterministic but unpredictable
    fn select_verifiable_random(
        &self,
        candidates: &[(&Validator, ProofOfContribution)],
        previous_block_hash: &[u8; 32],
        block_height: u64,
    ) -> Option<[u8; 32]> {
        // Generate deterministic but unpredictable random seed
        let mut hasher = Hasher::new();
        hasher.update(previous_block_hash);
        hasher.update(&block_height.to_le_bytes());
        let seed_bytes = hasher.finalize();

        // Convert to f64 in range [0, 1)
        let seed = u64::from_le_bytes(seed_bytes.as_bytes()[0..8].try_into().unwrap());
        let random_value = (seed as f64) / (u64::MAX as f64);

        // Weighted selection using this deterministic random value
        let total_score: f64 = candidates
            .iter()
            .map(|(_, poc)| poc.total_score())
            .sum();

        if total_score == 0.0 {
            let idx = (seed as usize) % candidates.len();
            return Some(candidates[idx].0.pubkey);
        }

        let mut random_point = random_value * total_score;

        for (validator, poc) in candidates {
            random_point -= poc.total_score();
            if random_point <= 0.0 {
                return Some(validator.pubkey);
            }
        }

        Some(candidates[0].0.pubkey)
    }

    /// Round-robin among top N validators
    fn select_round_robin(
        &self,
        candidates: &[(&Validator, ProofOfContribution)],
        top_n: usize,
        block_height: u64,
    ) -> Option<[u8; 32]> {
        // Sort by score (descending)
        let mut sorted: Vec<_> = candidates.to_vec();
        sorted.sort_by(|a, b| {
            b.1.total_score()
                .partial_cmp(&a.1.total_score())
                .unwrap_or(std::cmp::Ordering::Equal)
        });

        // Take top N
        let top_validators: Vec<_> = sorted.into_iter().take(top_n).collect();

        if top_validators.is_empty() {
            return None;
        }

        // Round-robin based on block height
        let idx = (block_height as usize) % top_validators.len();
        Some(top_validators[idx].0.pubkey)
    }

    /// Calculate selection probability for a validator
    pub fn selection_probability(
        &self,
        validator_score: f64,
        total_score: f64,
    ) -> f64 {
        if total_score == 0.0 {
            return 0.0;
        }

        validator_score / total_score
    }
}

/// Selection statistics for transparency
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SelectionStats {
    /// Number of times selected
    pub times_selected: u64,

    /// Number of selection rounds participated in
    pub rounds_participated: u64,

    /// Average contribution score
    pub avg_score: f64,

    /// Selection probability (%)
    pub probability: f64,
}

impl SelectionStats {
    pub fn new() -> Self {
        Self {
            times_selected: 0,
            rounds_participated: 0,
            avg_score: 0.0,
            probability: 0.0,
        }
    }

    /// Update stats after selection round
    pub fn update(&mut self, was_selected: bool, score: f64, probability: f64) {
        self.rounds_participated += 1;

        if was_selected {
            self.times_selected += 1;
        }

        // Update rolling average score
        let total_rounds = self.rounds_participated as f64;
        self.avg_score = (self.avg_score * (total_rounds - 1.0) + score) / total_rounds;

        self.probability = probability;
    }

    /// Actual selection rate
    pub fn selection_rate(&self) -> f64 {
        if self.rounds_participated == 0 {
            return 0.0;
        }

        (self.times_selected as f64 / self.rounds_participated as f64) * 100.0
    }
}

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

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{ContributionScore, ScoreWeights};

    #[test]
    fn test_weighted_random_selection() {
        let selector = ValidatorSelector::new(SelectionAlgorithm::WeightedRandom);

        let candidates = create_test_candidates();
        let candidates_refs: Vec<(&Validator, ProofOfContribution)> = candidates
            .iter()
            .map(|(v, poc)| (v, poc.clone()))
            .collect();
        let selected = selector.select_weighted_random(&candidates_refs);

        assert!(selected.is_some());

        // Verify selected validator is in candidates
        let selected_pubkey = selected.unwrap();
        assert!(candidates.iter().any(|(v, _)| v.pubkey == selected_pubkey));
    }

    #[test]
    fn test_verifiable_random_deterministic() {
        let selector = ValidatorSelector::new(SelectionAlgorithm::VerifiableRandom);
        let candidates = create_test_candidates();
        let candidates_refs: Vec<(&Validator, ProofOfContribution)> = candidates
            .iter()
            .map(|(v, poc)| (v, poc.clone()))
            .collect();
        let prev_hash = [1u8; 32];
        let height = 100;

        let selected1 = selector.select_verifiable_random(&candidates_refs, &prev_hash, height);
        let selected2 = selector.select_verifiable_random(&candidates_refs, &prev_hash, height);

        // Same inputs = same output (deterministic)
        assert_eq!(selected1, selected2);

        // Different block = different selection
        let selected3 = selector.select_verifiable_random(&candidates_refs, &prev_hash, height + 1);
        // Might be same or different, but test passes either way
        assert!(selected3.is_some());
    }

    #[test]
    fn test_round_robin() {
        let selector = ValidatorSelector::new(SelectionAlgorithm::RoundRobin { top_n: 3 });
        let candidates = create_test_candidates();
        let candidates_refs: Vec<(&Validator, ProofOfContribution)> = candidates
            .iter()
            .map(|(v, poc)| (v, poc.clone()))
            .collect();

        let selected1 = selector.select_round_robin(&candidates_refs, 3, 0);
        let selected2 = selector.select_round_robin(&candidates_refs, 3, 1);
        let selected3 = selector.select_round_robin(&candidates_refs, 3, 2);

        assert!(selected1.is_some());
        assert!(selected2.is_some());
        assert!(selected3.is_some());

        // Should cycle through top validators
        assert_ne!(selected1, selected2);
    }

    #[test]
    fn test_selection_probability() {
        let selector = ValidatorSelector::default();

        let prob = selector.selection_probability(0.5, 2.0);
        assert_eq!(prob, 0.25);

        let prob_zero = selector.selection_probability(0.5, 0.0);
        assert_eq!(prob_zero, 0.0);
    }

    #[test]
    fn test_selection_stats() {
        let mut stats = SelectionStats::new();

        stats.update(true, 0.8, 0.25);
        stats.update(false, 0.7, 0.20);
        stats.update(true, 0.9, 0.30);

        assert_eq!(stats.times_selected, 2);
        assert_eq!(stats.rounds_participated, 3);
        assert!((stats.avg_score - 0.8).abs() < 0.1);
        assert!((stats.selection_rate() - 66.67).abs() < 1.0);
    }

    #[test]
    fn test_high_score_validator_selected_more() {
        let selector = ValidatorSelector::new(SelectionAlgorithm::WeightedRandom);

        let high_score_validator = create_test_validator([1u8; 32], 0.9);
        let low_score_validator = create_test_validator([2u8; 32], 0.1);

        let candidates = vec![
            (&high_score_validator, create_poc([1u8; 32], 0.9)),
            (&low_score_validator, create_poc([2u8; 32], 0.1)),
        ];

        // Run selection many times
        let mut high_score_count = 0;
        for _ in 0..1000 {
            if let Some(selected) = selector.select_weighted_random(&candidates) {
                if selected == [1u8; 32] {
                    high_score_count += 1;
                }
            }
        }

        // High score validator should be selected ~90% of time
        let high_score_rate = high_score_count as f64 / 1000.0;
        assert!(high_score_rate > 0.85 && high_score_rate < 0.95);
    }

    fn create_test_candidates() -> Vec<(Validator, ProofOfContribution)> {
        vec![
            (
                create_test_validator([1u8; 32], 0.8),
                create_poc([1u8; 32], 0.8),
            ),
            (
                create_test_validator([2u8; 32], 0.6),
                create_poc([2u8; 32], 0.6),
            ),
            (
                create_test_validator([3u8; 32], 0.4),
                create_poc([3u8; 32], 0.4),
            ),
        ]
    }

    fn create_test_validator(pubkey: [u8; 32], _score: f64) -> Validator {
        Validator::new(pubkey, 1000, "test.onion".to_string(), 0).unwrap()
    }

    fn create_poc(pubkey: [u8; 32], total_score: f64) -> ProofOfContribution {
        let score = ContributionScore {
            stake_score: total_score * 0.4,
            relay_score: total_score * 0.3,
            bandwidth_score: total_score * 0.15,
            uptime_score: total_score * 0.1,
            storage_score: total_score * 0.05,
            weights: ScoreWeights::default(),
        };

        ProofOfContribution {
            validator_pubkey: pubkey,
            score,
            timestamp: 0,
        }
    }
}
