use lru::LruCache;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
// Note: Use tracing::info! macro directly instead of importing
use uuid::Uuid;

/// Représente un chunk audio décodé en mémoire
#[derive(Debug, Clone)]
pub struct AudioChunk {
    pub track_id: Uuid,
    pub chunk_index: usize,
    pub data: Vec<u8>, // Données audio décodées (PCM)
    pub sample_rate: u32,
    pub channels: u16,
    pub duration_ms: u32,
    pub cached_at: Instant,
    pub access_count: u64,
    pub last_accessed: Instant,
}

/// Clé pour le cache audio
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CacheKey {
    pub track_id: Uuid,
    pub chunk_index: usize,
    pub quality: AudioQuality,
}

/// Qualités audio supportées
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum AudioQuality {
    Low,    // 128kbps
    Medium, // 256kbps
    High,   // 320kbps
}

/// Statistiques du cache
#[derive(Debug, Clone)]
pub struct CacheStats {
    pub hits: u64,
    pub misses: u64,
    pub evictions: u64,
    pub total_size_bytes: usize,
    pub hit_rate: f64,
    pub entries_count: usize,
}

/// Gestionnaire de cache audio avec LRU et métriques
pub struct AudioCache {
    cache: Arc<RwLock<LruCache<CacheKey, AudioChunk>>>,
    max_size_bytes: usize,
    stats: Arc<RwLock<CacheStats>>,
    cleanup_interval: Duration,
    last_cleanup: Arc<RwLock<Instant>>,
}

impl AudioCache {
    pub fn new(max_size_mb: usize) -> Self {
        let max_size_bytes = max_size_mb * 1024 * 1024; // Convertir en bytes
        let cache_capacity = max_size_bytes / (1024 * 1024); // Estimation: ~1MB par chunk

        Self {
            cache: Arc::new(RwLock::new(LruCache::new(
                std::num::NonZeroUsize::new(cache_capacity).unwrap(),
            ))),
            max_size_bytes,
            stats: Arc::new(RwLock::new(CacheStats {
                hits: 0,
                misses: 0,
                evictions: 0,
                total_size_bytes: 0,
                hit_rate: 0.0,
                entries_count: 0,
            })),
            cleanup_interval: Duration::from_secs(300), // Nettoyage toutes les 5 minutes
            last_cleanup: Arc::new(RwLock::new(Instant::now())),
        }
    }

    /// Met un chunk audio en cache
    pub async fn put(&self, key: CacheKey, chunk: AudioChunk) {
        let mut cache = self.cache.write().await;
        let mut stats = self.stats.write().await;

        // Calculer la taille du chunk
        let chunk_size = chunk.data.len();

        // Vérifier si on dépasse la limite de taille
        if stats.total_size_bytes + chunk_size > self.max_size_bytes {
            self.evict_oldest_entries(&mut cache, &mut stats, chunk_size)
                .await;
        }

        // Ajouter au cache
        cache.put(key, chunk);
        stats.total_size_bytes += chunk_size;
        stats.entries_count = cache.len();

        tracing::debug!("Cached audio chunk: {} bytes", chunk_size);
    }

    /// Récupère un chunk audio du cache
    pub async fn get(&self, key: &CacheKey) -> Option<AudioChunk> {
        let mut cache = self.cache.write().await;
        let mut stats = self.stats.write().await;

        match cache.get_mut(key) {
            Some(chunk) => {
                // Mettre à jour les statistiques d'accès
                chunk.access_count += 1;
                chunk.last_accessed = Instant::now();

                stats.hits += 1;
                self.update_hit_rate(&mut stats);

                tracing::debug!(
                    "Cache hit for track {} chunk {}",
                    key.track_id,
                    key.chunk_index
                );
                Some(chunk.clone())
            }
            None => {
                stats.misses += 1;
                self.update_hit_rate(&mut stats);

                tracing::debug!(
                    "Cache miss for track {} chunk {}",
                    key.track_id,
                    key.chunk_index
                );
                None
            }
        }
    }

    /// Vérifie si un chunk est en cache
    pub async fn contains(&self, key: &CacheKey) -> bool {
        let cache = self.cache.read().await;
        cache.contains(key)
    }

    /// Supprime un chunk spécifique du cache
    pub async fn remove(&self, key: &CacheKey) -> Option<AudioChunk> {
        let mut cache = self.cache.write().await;
        let mut stats = self.stats.write().await;

        if let Some(chunk) = cache.pop(key) {
            stats.total_size_bytes -= chunk.data.len();
            stats.entries_count = cache.len();
            Some(chunk)
        } else {
            None
        }
    }

    /// Supprime tous les chunks d'une track
    pub async fn remove_track(&self, track_id: Uuid) -> usize {
        let mut cache = self.cache.write().await;
        let mut stats = self.stats.write().await;
        let mut removed_count = 0;
        let mut removed_size = 0;

        // Collecter les clés à supprimer
        let keys_to_remove: Vec<CacheKey> = cache
            .iter()
            .filter(|(key, _)| key.track_id == track_id)
            .map(|(key, _)| key.clone())
            .collect();

        // Supprimer les chunks
        for key in keys_to_remove {
            if let Some(chunk) = cache.pop(&key) {
                removed_size += chunk.data.len();
                removed_count += 1;
            }
        }

        stats.total_size_bytes -= removed_size;
        stats.entries_count = cache.len();

        tracing::info!(
            "Removed {} chunks for track {} ({} bytes)",
            removed_count,
            track_id,
            removed_size
        );
        removed_count
    }

    /// Nettoie les chunks expirés ou peu utilisés
    pub async fn cleanup(&self) {
        let mut cache = self.cache.write().await;
        let mut stats = self.stats.write().await;

        let now = Instant::now();
        let mut removed_count = 0;
        let mut removed_size = 0;

        // Collecter les clés à supprimer (chunks anciens ou peu utilisés)
        let keys_to_remove: Vec<CacheKey> = cache
            .iter()
            .filter(|(_, chunk)| {
                // Supprimer les chunks non accédés depuis plus de 1 heure
                // ou avec très peu d'accès (< 2) et anciens (> 30 min)
                let time_since_access = now.duration_since(chunk.last_accessed);
                let time_since_cache = now.duration_since(chunk.cached_at);

                time_since_access > Duration::from_secs(3600) || // 1 heure
                (chunk.access_count < 2 && time_since_cache > Duration::from_secs(1800))
                // 30 min
            })
            .map(|(key, _)| key.clone())
            .collect();

        // Supprimer les chunks sélectionnés
        for key in keys_to_remove {
            if let Some(chunk) = cache.pop(&key) {
                removed_size += chunk.data.len();
                removed_count += 1;
            }
        }

        stats.total_size_bytes -= removed_size;
        stats.entries_count = cache.len();
        stats.evictions += removed_count as u64;

        if removed_count > 0 {
            tracing::info!(
                "Cache cleanup: removed {} chunks ({} bytes)",
                removed_count,
                removed_size
            );
        }
    }

    /// Éviction des entrées les plus anciennes
    async fn evict_oldest_entries(
        &self,
        cache: &mut LruCache<CacheKey, AudioChunk>,
        stats: &mut CacheStats,
        needed_space: usize,
    ) {
        let mut freed_space = 0;
        let mut evicted_count = 0;

        // Évincer les entrées les moins récemment utilisées
        while freed_space < needed_space && !cache.is_empty() {
            if let Some((_, chunk)) = cache.pop_lru() {
                freed_space += chunk.data.len();
                evicted_count += 1;
            }
        }

        stats.total_size_bytes -= freed_space;
        stats.evictions += evicted_count as u64;

        tracing::debug!(
            "Evicted {} chunks, freed {} bytes",
            evicted_count,
            freed_space
        );
    }

    /// Met à jour le taux de hit du cache
    fn update_hit_rate(&self, stats: &mut CacheStats) {
        let total = stats.hits + stats.misses;
        if total > 0 {
            stats.hit_rate = stats.hits as f64 / total as f64;
        }
    }

    /// Obtient les statistiques du cache
    pub async fn get_stats(&self) -> CacheStats {
        let stats = self.stats.read().await;
        stats.clone()
    }

    /// Obtient la taille actuelle du cache en MB
    pub async fn get_size_mb(&self) -> f64 {
        let stats = self.stats.read().await;
        stats.total_size_bytes as f64 / (1024.0 * 1024.0)
    }

    /// Vérifie si le cache a besoin d'un nettoyage
    pub async fn needs_cleanup(&self) -> bool {
        let last_cleanup = self.last_cleanup.read().await;
        last_cleanup.elapsed() >= self.cleanup_interval
    }

    /// Force un nettoyage du cache
    pub async fn force_cleanup(&self) {
        self.cleanup().await;
        *self.last_cleanup.write().await = Instant::now();
    }

    /// Vide complètement le cache
    pub async fn clear(&self) {
        let mut cache = self.cache.write().await;
        let mut stats = self.stats.write().await;

        cache.clear();
        stats.total_size_bytes = 0;
        stats.entries_count = 0;

        tracing::info!("Audio cache cleared");
    }

    /// Obtient les chunks les plus populaires (pour analyse)
    pub async fn get_popular_chunks(&self, limit: usize) -> Vec<(CacheKey, u64)> {
        let cache = self.cache.read().await;

        let mut chunks: Vec<(CacheKey, u64)> = cache
            .iter()
            .map(|(key, chunk)| (key.clone(), chunk.access_count))
            .collect();

        chunks.sort_by(|a, b| b.1.cmp(&a.1));
        chunks.truncate(limit);
        chunks
    }
}

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

    #[tokio::test]
    async fn test_audio_cache_basic_operations() {
        let cache = AudioCache::new(10); // 10MB cache

        let track_id = Uuid::new_v4();
        let key = CacheKey {
            track_id,
            chunk_index: 0,
            quality: AudioQuality::High,
        };

        let chunk = AudioChunk {
            track_id,
            chunk_index: 0,
            data: vec![0u8; 1024], // 1KB de données
            sample_rate: 44100,
            channels: 2,
            duration_ms: 100,
            cached_at: Instant::now(),
            access_count: 0,
            last_accessed: Instant::now(),
        };

        // Test put/get
        cache.put(key.clone(), chunk.clone()).await;
        assert!(cache.contains(&key).await);

        let retrieved = cache.get(&key).await;
        assert!(retrieved.is_some());
        assert_eq!(retrieved.unwrap().data.len(), 1024);

        // Test stats
        let stats = cache.get_stats().await;
        assert_eq!(stats.hits, 1);
        assert_eq!(stats.misses, 0);
        assert!(stats.hit_rate > 0.0);
    }

    #[tokio::test]
    async fn test_audio_cache_eviction() {
        let cache = AudioCache::new(1); // Petit cache de 1MB

        // Ajouter plusieurs chunks pour dépasser la limite
        for i in 0..10 {
            let key = CacheKey {
                track_id: Uuid::new_v4(),
                chunk_index: i,
                quality: AudioQuality::High,
            };

            let chunk = AudioChunk {
                track_id: key.track_id,
                chunk_index: i,
                data: vec![0u8; 200 * 1024], // 200KB par chunk
                sample_rate: 44100,
                channels: 2,
                duration_ms: 100,
                cached_at: Instant::now(),
                access_count: 0,
                last_accessed: Instant::now(),
            };

            cache.put(key, chunk).await;
        }

        // Vérifier que certains chunks ont été évincés
        let stats = cache.get_stats().await;
        assert!(stats.evictions > 0);
        assert!(cache.get_size_mb().await <= 1.0);
    }
}
