//! In-memory cache implementation with LRU eviction
//!
//! Provides thread-safe in-memory caching with LRU (Least Recently Used)
//! eviction, size limits, TTL support, and comprehensive statistics.

use std::collections::HashMap;
use std::hash::Hash;
use std::sync::{Arc, RwLock};
use std::time::Duration;
#[cfg(not(target_arch = "wasm32"))]
use std::time::Instant;

use super::{current_timestamp, Cache, CacheConfig, CacheEntry, CacheStats};

#[cfg(target_arch = "wasm32")]
#[derive(Debug, Clone, Copy)]
struct Instant {
    timestamp: f64,
}

#[cfg(target_arch = "wasm32")]
impl Instant {
    fn now() -> Self {
        use web_sys::js_sys::Date;
        Self {
            timestamp: Date::now(),
        }
    }

    fn elapsed(&self) -> Duration {
        use web_sys::js_sys::Date;
        let now = Date::now();
        let elapsed_ms = now - self.timestamp;
        Duration::from_millis(elapsed_ms.max(0.0) as u64)
    }
}

/// Thread-safe in-memory cache with LRU eviction
#[derive(Debug)]
pub struct MemoryCache<K, V>
where
    K: Clone + Hash + Eq + Send + Sync + std::fmt::Debug,
    V: Clone + Send + Sync + std::fmt::Debug,
{
    /// Internal storage
    storage: Arc<RwLock<MemoryCacheStorage<K, V>>>,
    /// Cache configuration
    config: CacheConfig,
}

/// Internal storage structure
#[derive(Debug)]
struct MemoryCacheStorage<K, V>
where
    K: Clone + Hash + Eq + Send + Sync + std::fmt::Debug,
    V: Clone + Send + Sync + std::fmt::Debug,
{
    /// Main data storage
    data: HashMap<K, CacheEntry<V>>,
    /// Access order for LRU eviction
    access_order: Vec<K>,
    /// Current total size in bytes
    current_size_bytes: usize,
    /// Statistics
    total_hits: u64,
    total_misses: u64,
    /// Last cleanup timestamp
    last_cleanup: Instant,
}

impl<K, V> MemoryCache<K, V>
where
    K: Clone + Hash + Eq + Send + Sync + std::fmt::Debug,
    V: Clone + Send + Sync + std::fmt::Debug,
{
    /// Create a new memory cache with default configuration
    pub fn new() -> Self {
        Self::with_config(CacheConfig::default())
    }

    /// Create a new memory cache with custom configuration
    pub fn with_config(config: CacheConfig) -> Self {
        Self {
            storage: Arc::new(RwLock::new(MemoryCacheStorage {
                data: HashMap::new(),
                access_order: Vec::new(),
                current_size_bytes: 0,
                total_hits: 0,
                total_misses: 0,
                last_cleanup: Instant::now(),
            })),
            config,
        }
    }

    /// Get cache utilization as a percentage (0.0 to 1.0)
    pub fn utilization(&self) -> f64 {
        let storage = self.storage.read().unwrap();
        if self.config.max_memory_bytes == 0 {
            0.0
        } else {
            storage.current_size_bytes as f64 / self.config.max_memory_bytes as f64
        }
    }

    /// Get number of entries
    pub fn len(&self) -> usize {
        self.storage.read().unwrap().data.len()
    }

    /// Check if cache is empty
    pub fn is_empty(&self) -> bool {
        self.storage.read().unwrap().data.is_empty()
    }

    /// Force cleanup of expired entries
    pub fn cleanup_expired(&self) {
        let mut storage = self.storage.write().unwrap();

        let mut keys_to_remove = Vec::new();

        for (key, entry) in &storage.data {
            let ttl = self
                .config
                .get_ttl_for_content_type(&entry.metadata.content_type);
            if entry.is_expired(ttl) {
                keys_to_remove.push(key.clone());
            }
        }

        for key in keys_to_remove {
            if let Some(entry) = storage.data.remove(&key) {
                storage.current_size_bytes = storage
                    .current_size_bytes
                    .saturating_sub(entry.metadata.size_bytes);
                storage.access_order.retain(|k| k != &key);
            }
        }

        storage.last_cleanup = Instant::now();

        if !storage.data.is_empty() {
            tracing::debug!(
                "Cache cleanup completed: {} entries, {} bytes used",
                storage.data.len(),
                storage.current_size_bytes
            );
        }
    }

    /// Evict least recently used entries until size constraints are met
    fn evict_lru(&self, storage: &mut MemoryCacheStorage<K, V>) {
        while (storage.data.len() >= self.config.max_entries
            || storage.current_size_bytes > self.config.max_memory_bytes)
            && !storage.access_order.is_empty()
        {
            if let Some(lru_key) = storage.access_order.first().cloned() {
                if let Some(entry) = storage.data.remove(&lru_key) {
                    storage.current_size_bytes = storage
                        .current_size_bytes
                        .saturating_sub(entry.metadata.size_bytes);
                    storage.access_order.retain(|k| k != &lru_key);

                    tracing::trace!(
                        "Evicted LRU entry: {} bytes freed",
                        entry.metadata.size_bytes
                    );
                }
            } else {
                break;
            }
        }
    }

    /// Update access order for LRU tracking
    fn update_access_order(&self, storage: &mut MemoryCacheStorage<K, V>, key: &K) {
        // Remove key from current position
        storage.access_order.retain(|k| k != key);
        // Add to end (most recently used)
        storage.access_order.push(key.clone());
    }

    /// Check if automatic cleanup should run
    fn should_cleanup(&self, storage: &MemoryCacheStorage<K, V>) -> bool {
        storage.last_cleanup.elapsed() > Duration::from_secs(self.config.cleanup_interval_seconds)
    }

    /// Get cache statistics
    pub fn get_detailed_stats(&self) -> CacheStats {
        let storage = self.storage.read().unwrap();

        let total_requests = storage.total_hits + storage.total_misses;
        let hit_ratio = if total_requests > 0 {
            storage.total_hits as f64 / total_requests as f64
        } else {
            0.0
        };

        let mut entries_by_type = HashMap::new();
        let mut entries_by_language = HashMap::new();
        let mut most_accessed = Vec::new();
        let mut total_age_seconds = 0u64;

        for entry in storage.data.values() {
            // Count by content type
            *entries_by_type
                .entry(entry.metadata.content_type.clone())
                .or_insert(0) += 1;

            // Count by language
            if let Some(ref lang) = entry.metadata.language {
                *entries_by_language.entry(lang.clone()).or_insert(0) += 1;
            }

            // Track most accessed
            most_accessed.push((
                format!("{}:{}", entry.metadata.content_type, "item"),
                entry.metadata.access_count,
            ));

            total_age_seconds += entry.metadata.age_seconds();
        }

        // Sort most accessed by access count
        most_accessed.sort_by(|a, b| b.1.cmp(&a.1));
        most_accessed.truncate(10); // Keep top 10

        let avg_age_seconds = if storage.data.is_empty() {
            0.0
        } else {
            total_age_seconds as f64 / storage.data.len() as f64
        };

        let utilization_percent = if self.config.max_memory_bytes > 0 {
            (storage.current_size_bytes as f64 / self.config.max_memory_bytes as f64) * 100.0
        } else {
            0.0
        };

        CacheStats {
            total_entries: storage.data.len(),
            total_size_bytes: storage.current_size_bytes,
            hit_ratio,
            total_hits: storage.total_hits,
            total_misses: storage.total_misses,
            avg_age_seconds,
            entries_by_type,
            entries_by_language,
            utilization_percent,
            most_accessed,
        }
    }
}

impl<K, V> Default for MemoryCache<K, V>
where
    K: Clone + Hash + Eq + Send + Sync + std::fmt::Debug,
    V: Clone + Send + Sync + std::fmt::Debug,
{
    fn default() -> Self {
        Self::new()
    }
}

impl<K, V> Cache<K, V> for MemoryCache<K, V>
where
    K: Clone + Hash + Eq + Send + Sync + std::fmt::Debug,
    V: Clone + Send + Sync + std::fmt::Debug,
{
    fn get(&self, key: &K) -> Option<CacheEntry<V>> {
        let mut storage = self.storage.write().unwrap();

        // Check if automatic cleanup should run
        if self.should_cleanup(&storage) {
            drop(storage);
            self.cleanup_expired();
            storage = self.storage.write().unwrap();
        }

        // Check if entry exists and is not expired
        let should_remove = {
            if let Some(entry) = storage.data.get(key) {
                let ttl = self
                    .config
                    .get_ttl_for_content_type(&entry.metadata.content_type);
                entry.is_expired(ttl)
            } else {
                false
            }
        };

        if should_remove {
            // Remove expired entry
            if let Some(entry) = storage.data.remove(key) {
                storage.current_size_bytes = storage
                    .current_size_bytes
                    .saturating_sub(entry.metadata.size_bytes);
                storage.access_order.retain(|k| k != key);
            }
            storage.total_misses += 1;
            None
        } else if storage.data.contains_key(key) {
            // Update entry and access order separately
            storage.total_hits += 1;

            // Clone the entry first
            let mut result = None;
            if let Some(entry) = storage.data.get_mut(key) {
                entry.touch();
                result = Some(entry.clone());
            }

            // Update access order
            self.update_access_order(&mut storage, key);
            result
        } else {
            storage.total_misses += 1;
            None
        }
    }

    fn put(
        &self,
        key: K,
        value: V,
        content_type: String,
        version: String,
    ) -> Option<CacheEntry<V>> {
        let mut storage = self.storage.write().unwrap();

        // Estimate size of the new entry
        let estimated_size = std::mem::size_of::<V>() + content_type.len() + version.len();

        // Check if we need to evict entries first
        if storage.current_size_bytes + estimated_size > self.config.max_memory_bytes
            || storage.data.len() >= self.config.max_entries
        {
            self.evict_lru(&mut storage);
        }

        let entry = CacheEntry::new(value, content_type, version, estimated_size);
        storage.current_size_bytes += estimated_size;

        let old_entry = storage.data.insert(key.clone(), entry.clone());

        // Update size if replacing an existing entry
        if let Some(ref old) = old_entry {
            storage.current_size_bytes = storage
                .current_size_bytes
                .saturating_sub(old.metadata.size_bytes);
        }

        self.update_access_order(&mut storage, &key);

        tracing::trace!(
            "Cache entry added: {} bytes, total: {} bytes, {} entries",
            estimated_size,
            storage.current_size_bytes,
            storage.data.len()
        );

        old_entry
    }

    fn remove(&self, key: &K) -> Option<CacheEntry<V>> {
        let mut storage = self.storage.write().unwrap();

        if let Some(entry) = storage.data.remove(key) {
            storage.current_size_bytes = storage
                .current_size_bytes
                .saturating_sub(entry.metadata.size_bytes);
            storage.access_order.retain(|k| k != key);
            Some(entry)
        } else {
            None
        }
    }

    fn clear(&self) {
        let mut storage = self.storage.write().unwrap();
        storage.data.clear();
        storage.access_order.clear();
        storage.current_size_bytes = 0;
        tracing::info!("Memory cache cleared");
    }

    fn size(&self) -> usize {
        self.storage.read().unwrap().data.len()
    }

    fn stats(&self) -> CacheStats {
        self.get_detailed_stats()
    }

    fn cleanup_expired(&self) {
        self.cleanup_expired()
    }

    fn get_config(&self) -> &CacheConfig {
        &self.config
    }
}

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

    use super::*;

    #[test]
    fn test_memory_cache_basic_operations() {
        let cache = MemoryCache::new();

        // Test put and get
        let old = cache.put(
            "key1".to_string(),
            "value1".to_string(),
            "test".to_string(),
            "v1".to_string(),
        );
        assert!(old.is_none());

        let entry = cache.get(&"key1".to_string());
        assert!(entry.is_some());
        assert_eq!(*entry.unwrap().data, "value1");

        // Test cache miss
        let missing = cache.get(&"nonexistent".to_string());
        assert!(missing.is_none());
    }

    #[test]
    fn test_memory_cache_lru_eviction() {
        let config = CacheConfig {
            max_entries: 2,
            ..Default::default()
        };
        let cache = MemoryCache::with_config(config);

        // Add entries up to limit
        cache.put(
            "key1".to_string(),
            "value1".to_string(),
            "test".to_string(),
            "v1".to_string(),
        );
        cache.put(
            "key2".to_string(),
            "value2".to_string(),
            "test".to_string(),
            "v1".to_string(),
        );

        // Access key1 to make it more recently used
        cache.get(&"key1".to_string());

        // Add one more entry (should evict key2)
        cache.put(
            "key3".to_string(),
            "value3".to_string(),
            "test".to_string(),
            "v1".to_string(),
        );

        // key1 and key3 should exist, key2 should be evicted
        assert!(cache.get(&"key1".to_string()).is_some());
        assert!(cache.get(&"key2".to_string()).is_none());
        assert!(cache.get(&"key3".to_string()).is_some());
    }

    #[test]
    fn test_memory_cache_expiration() {
        let mut config = CacheConfig::default();
        config.content_type_ttl.insert("test".to_string(), 0); // Immediate expiration
        let cache = MemoryCache::with_config(config);

        cache.put(
            "key1".to_string(),
            "value1".to_string(),
            "test".to_string(),
            "v1".to_string(),
        );

        // Entry should be expired and return None
        thread::sleep(Duration::from_millis(1));
        let entry = cache.get(&"key1".to_string());
        assert!(entry.is_none());
    }

    #[test]
    fn test_memory_cache_stats() {
        let cache = MemoryCache::new();

        // Add some entries
        cache.put(
            "key1".to_string(),
            "value1".to_string(),
            "blog".to_string(),
            "v1".to_string(),
        );
        cache.put(
            "key2".to_string(),
            "value2".to_string(),
            "routes".to_string(),
            "v1".to_string(),
        );

        // Generate some hits and misses
        cache.get(&"key1".to_string());
        cache.get(&"key1".to_string());
        cache.get(&"nonexistent".to_string());

        let stats = cache.stats();
        assert_eq!(stats.total_entries, 2);
        assert_eq!(stats.total_hits, 2);
        assert_eq!(stats.total_misses, 1);
        assert!(stats.hit_ratio > 0.0);
        assert!(stats.entries_by_type.contains_key("blog"));
        assert!(stats.entries_by_type.contains_key("routes"));
    }

    #[test]
    fn test_memory_cache_clear() {
        let cache = MemoryCache::new();

        cache.put(
            "key1".to_string(),
            "value1".to_string(),
            "test".to_string(),
            "v1".to_string(),
        );
        cache.put(
            "key2".to_string(),
            "value2".to_string(),
            "test".to_string(),
            "v1".to_string(),
        );

        assert_eq!(cache.size(), 2);

        cache.clear();
        assert_eq!(cache.size(), 0);
        assert!(cache.get(&"key1".to_string()).is_none());
    }

    #[test]
    fn test_memory_cache_utilization() {
        let config = CacheConfig {
            max_memory_bytes: 1000,
            ..Default::default()
        };
        let cache = MemoryCache::with_config(config);

        let initial_utilization = cache.utilization();
        assert_eq!(initial_utilization, 0.0);

        cache.put(
            "key1".to_string(),
            "value1".to_string(),
            "test".to_string(),
            "v1".to_string(),
        );

        let utilization_after_put = cache.utilization();
        assert!(utilization_after_put > 0.0);
    }
}
