//! Gateway Cache - LRU cache for frequently accessed content
//!
//! Caches resolved content to reduce load on storage nodes
//! and improve response times for popular content.

use crate::cid::ContentId;
use super::GatewayResponse;
use std::collections::HashMap;

/// LRU cache entry
#[derive(Debug, Clone)]
struct CacheEntry {
    /// Cached response
    response: GatewayResponse,
    /// Access count
    access_count: u64,
    /// Last access timestamp
    last_access: u64,
    /// Size in bytes
    size: u64,
}

/// LRU cache for gateway responses
pub struct GatewayCache {
    /// Maximum cache size in bytes
    max_size: u64,
    /// Current size in bytes
    current_size: u64,
    /// Cached entries by CID
    entries: HashMap<ContentId, CacheEntry>,
    /// Access order for LRU eviction (CID, last_access)
    access_order: Vec<(ContentId, u64)>,
    /// Cache statistics
    stats: CacheStats,
}

/// Cache statistics
#[derive(Debug, Clone, Default)]
pub struct CacheStats {
    /// Total hits
    pub hits: u64,
    /// Total misses
    pub misses: u64,
    /// Total evictions
    pub evictions: u64,
    /// Total bytes cached
    pub bytes_cached: u64,
    /// Total bytes evicted
    pub bytes_evicted: u64,
}

impl GatewayCache {
    /// Create a new cache with maximum size in bytes
    pub fn new(max_size: u64) -> Self {
        Self {
            max_size,
            current_size: 0,
            entries: HashMap::new(),
            access_order: Vec::new(),
            stats: CacheStats::default(),
        }
    }

    /// Get an entry from the cache
    pub fn get(&self, cid: &ContentId) -> Option<GatewayResponse> {
        self.entries.get(cid).map(|entry| entry.response.clone())
    }

    /// Get an entry and update access stats
    pub fn get_mut(&mut self, cid: &ContentId) -> Option<GatewayResponse> {
        let now = current_timestamp();

        if let Some(entry) = self.entries.get_mut(cid) {
            entry.access_count += 1;
            entry.last_access = now;
            self.stats.hits += 1;

            // Update access order
            if let Some(pos) = self.access_order.iter().position(|(c, _)| c == cid) {
                self.access_order.remove(pos);
            }
            self.access_order.push((cid.clone(), now));

            Some(entry.response.clone())
        } else {
            self.stats.misses += 1;
            None
        }
    }

    /// Put an entry in the cache
    pub fn put(&mut self, cid: ContentId, response: GatewayResponse) {
        let size = response.content.len() as u64;

        // Don't cache if larger than max size
        if size > self.max_size {
            return;
        }

        // Remove existing entry if present
        if self.entries.contains_key(&cid) {
            self.remove(&cid);
        }

        // Evict entries until we have space
        while self.current_size + size > self.max_size && !self.entries.is_empty() {
            self.evict_lru();
        }

        let now = current_timestamp();
        let entry = CacheEntry {
            response,
            access_count: 1,
            last_access: now,
            size,
        };

        self.entries.insert(cid.clone(), entry);
        self.access_order.push((cid, now));
        self.current_size += size;
        self.stats.bytes_cached += size;
    }

    /// Remove an entry from the cache
    pub fn remove(&mut self, cid: &ContentId) -> Option<GatewayResponse> {
        if let Some(entry) = self.entries.remove(cid) {
            self.current_size -= entry.size;
            self.access_order.retain(|(c, _)| c != cid);
            Some(entry.response)
        } else {
            None
        }
    }

    /// Evict the least recently used entry
    fn evict_lru(&mut self) {
        if let Some((cid, _)) = self.access_order.first().cloned() {
            if let Some(entry) = self.entries.remove(&cid) {
                self.current_size -= entry.size;
                self.stats.evictions += 1;
                self.stats.bytes_evicted += entry.size;
            }
            self.access_order.remove(0);
        }
    }

    /// Clear the entire cache
    pub fn clear(&mut self) {
        self.entries.clear();
        self.access_order.clear();
        self.current_size = 0;
    }

    /// Get cache statistics
    pub fn stats(&self) -> &CacheStats {
        &self.stats
    }

    /// Get current cache size in bytes
    pub fn size(&self) -> u64 {
        self.current_size
    }

    /// Get maximum cache size in bytes
    pub fn max_size(&self) -> u64 {
        self.max_size
    }

    /// Get number of entries in cache
    pub fn len(&self) -> usize {
        self.entries.len()
    }

    /// Check if cache is empty
    pub fn is_empty(&self) -> bool {
        self.entries.is_empty()
    }

    /// Check if CID is in cache
    pub fn contains(&self, cid: &ContentId) -> bool {
        self.entries.contains_key(cid)
    }

    /// Get hit rate
    pub fn hit_rate(&self) -> f64 {
        let total = self.stats.hits + self.stats.misses;
        if total == 0 {
            0.0
        } else {
            self.stats.hits as f64 / total as f64
        }
    }

    /// Prune entries not accessed since cutoff timestamp
    pub fn prune_stale(&mut self, cutoff_timestamp: u64) {
        let stale: Vec<ContentId> = self
            .entries
            .iter()
            .filter(|(_, entry)| entry.last_access < cutoff_timestamp)
            .map(|(cid, _)| cid.clone())
            .collect();

        for cid in stale {
            self.remove(&cid);
        }
    }
}

/// Get current timestamp in seconds
fn current_timestamp() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_secs()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::cid::ContentId;

    fn make_response(data: &[u8]) -> GatewayResponse {
        let cid = ContentId::from_content(data);
        GatewayResponse {
            cid: cid.clone(),
            content: data.to_vec(),
            mime_type: "application/octet-stream".to_string(),
            size: data.len() as u64,
        }
    }

    #[test]
    fn test_cache_put_get() {
        let mut cache = GatewayCache::new(1024);

        let data = b"hello world";
        let cid = ContentId::from_content(data);
        let response = make_response(data);

        cache.put(cid.clone(), response);

        let retrieved = cache.get(&cid).unwrap();
        assert_eq!(retrieved.content, data);
    }

    #[test]
    fn test_cache_eviction() {
        let mut cache = GatewayCache::new(100);

        // Add entries until we exceed limit
        for i in 0..10 {
            let data = vec![i; 20];
            let cid = ContentId::from_content(&data);
            let response = make_response(&data);
            cache.put(cid, response);
        }

        // Should have evicted some entries
        assert!(cache.size() <= 100);
        assert!(cache.len() < 10);
    }

    #[test]
    fn test_cache_lru_order() {
        let mut cache = GatewayCache::new(100);

        // Add 3 entries (each ~10 bytes)
        let entries: Vec<_> = (0..3)
            .map(|i| {
                let data = vec![i; 10];
                let cid = ContentId::from_content(&data);
                let response = make_response(&data);
                (cid, response)
            })
            .collect();

        for (cid, response) in &entries {
            cache.put(cid.clone(), response.clone());
        }

        // Access first entry to make it recently used
        cache.get_mut(&entries[0].0);

        // Add more entries to trigger eviction
        for i in 3..10 {
            let data = vec![i; 10];
            let cid = ContentId::from_content(&data);
            let response = make_response(&data);
            cache.put(cid, response);
        }

        // First entry should still be present (was accessed recently)
        assert!(cache.contains(&entries[0].0));
    }
}
