//! LRU hot cache for frequently accessed entity data.
//!
//! Provides a thread-safe, TTL-aware LRU cache for entity metadata
//! (function signatures, type definitions, file summaries) that are
//! repeatedly accessed during search and code generation.

use std::collections::HashMap;
use std::sync::RwLock;
use std::time::{Duration, Instant};

use tracing::debug;

/// Default TTL for cached entities.
const DEFAULT_TTL_SECS: u64 = 600; // 10 minutes

/// Default maximum entries.
const DEFAULT_MAX_ENTRIES: usize = 2000;

/// A cached entity with LRU tracking metadata.
#[derive(Debug, Clone)]
struct CacheEntry {
    value: CachedEntity,
    inserted_at: Instant,
    last_accessed: Instant,
    access_count: u64,
    ttl: Duration,
}

impl CacheEntry {
    fn is_valid(&self) -> bool {
        self.inserted_at.elapsed() < self.ttl
    }

    fn touch(&mut self) {
        self.last_accessed = Instant::now();
        self.access_count += 1;
    }
}

/// Types of entities that can be cached.
#[derive(Debug, Clone)]
pub enum CachedEntity {
    /// Function signatures and metadata for a file.
    FileFunctions {
        file_path: String,
        functions: Vec<String>,
        types: Vec<String>,
    },
    /// A code snippet or summary for quick retrieval.
    CodeSnippet {
        file_path: String,
        content: String,
        line_range: Option<(usize, usize)>,
    },
    /// Arbitrary string data (for extensibility).
    Raw(String),
}

/// Thread-safe LRU cache for hot entity data.
///
/// Evicts entries based on least-recently-used policy when at capacity.
/// Also respects TTL for staleness.
pub struct HotCache {
    entries: RwLock<HashMap<String, CacheEntry>>,
    max_entries: usize,
    default_ttl: Duration,
    stats: RwLock<CacheStats>,
}

/// Aggregate cache statistics.
#[derive(Debug, Clone, Default)]
pub struct CacheStats {
    pub hits: u64,
    pub misses: u64,
    pub evictions: u64,
    pub expired: u64,
}

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

impl HotCache {
    /// Create a new HotCache with default settings.
    pub fn new() -> Self {
        Self::with_config(DEFAULT_MAX_ENTRIES, Duration::from_secs(DEFAULT_TTL_SECS))
    }

    /// Create a new HotCache with custom capacity and TTL.
    pub fn with_config(max_entries: usize, default_ttl: Duration) -> Self {
        debug!(
            max_entries,
            ttl_secs = default_ttl.as_secs(),
            "Initializing HotCache"
        );
        Self {
            entries: RwLock::new(HashMap::with_capacity(max_entries.min(1024))),
            max_entries,
            default_ttl,
            stats: RwLock::new(CacheStats::default()),
        }
    }

    /// Get an entity from the cache, promoting it in LRU order.
    /// Returns `None` if not present or expired.
    pub fn get(&self, key: &str) -> Option<CachedEntity> {
        // Try a quick read first.
        {
            let entries = self.entries.read().ok()?;
            if let Some(entry) = entries.get(key) {
                if !entry.is_valid() {
                    // Expired — will remove below.
                    drop(entries);
                    self.remove(key);
                    self.inc_expired();
                    self.inc_miss();
                    return None;
                }
                // Clone the value; we'll update access time with a write lock.
                let value = entry.value.clone();
                drop(entries);

                // Update LRU metadata.
                if let Ok(mut entries) = self.entries.write() {
                    if let Some(entry) = entries.get_mut(key) {
                        entry.touch();
                    }
                }
                self.inc_hit();
                return Some(value);
            }
        }
        self.inc_miss();
        None
    }

    /// Insert or update an entity in the cache.
    pub fn put(&self, key: &str, value: CachedEntity) {
        self.put_with_ttl(key, value, self.default_ttl);
    }

    /// Insert with a custom TTL.
    pub fn put_with_ttl(&self, key: &str, value: CachedEntity, ttl: Duration) {
        let mut entries = match self.entries.write() {
            Ok(e) => e,
            Err(_) => return,
        };

        // Evict if at capacity and this is a new key.
        if entries.len() >= self.max_entries && !entries.contains_key(key) {
            self.evict_lru_inner(&mut entries);
        }

        let now = Instant::now();
        entries.insert(
            key.to_string(),
            CacheEntry {
                value,
                inserted_at: now,
                last_accessed: now,
                access_count: 0,
                ttl,
            },
        );
    }

    /// Remove a specific entry.
    pub fn remove(&self, key: &str) {
        if let Ok(mut entries) = self.entries.write() {
            entries.remove(key);
        }
    }

    /// Clear the entire cache.
    pub fn clear(&self) {
        if let Ok(mut entries) = self.entries.write() {
            let count = entries.len();
            entries.clear();
            debug!(evicted = count, "HotCache cleared");
        }
    }

    /// Number of entries currently stored.
    pub fn len(&self) -> usize {
        self.entries.read().map(|e| e.len()).unwrap_or(0)
    }

    /// Returns true if the cache is empty.
    pub fn is_empty(&self) -> bool {
        self.len() == 0
    }

    /// Get current cache statistics.
    pub fn stats(&self) -> CacheStats {
        self.stats.read().ok().map(|s| s.clone()).unwrap_or_default()
    }

    /// Reset statistics counters.
    pub fn reset_stats(&self) {
        if let Ok(mut stats) = self.stats.write() {
            *stats = CacheStats::default();
        }
    }

    /// Purge all expired entries.
    pub fn purge_expired(&self) {
        if let Ok(mut entries) = self.entries.write() {
            let before = entries.len();
            entries.retain(|_, v| v.is_valid());
            let purged = before - entries.len();
            if purged > 0 {
                debug!(purged, "HotCache purged expired entries");
                if let Ok(mut stats) = self.stats.write() {
                    stats.expired += purged as u64;
                }
            }
        }
    }

    /// Evict the least-recently-used entry. Must be called with write lock held.
    fn evict_lru_inner(&self, entries: &mut HashMap<String, CacheEntry>) {
        let lru_key = entries
            .iter()
            .min_by_key(|(_, v)| v.last_accessed)
            .map(|(k, _)| k.clone());

        if let Some(key) = lru_key {
            entries.remove(&key);
            if let Ok(mut stats) = self.stats.write() {
                stats.evictions += 1;
            }
        }
    }

    fn inc_hit(&self) {
        if let Ok(mut stats) = self.stats.write() {
            stats.hits += 1;
        }
    }

    fn inc_miss(&self) {
        if let Ok(mut stats) = self.stats.write() {
            stats.misses += 1;
        }
    }

    fn inc_expired(&self) {
        if let Ok(mut stats) = self.stats.write() {
            stats.expired += 1;
        }
    }
}

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

    #[test]
    fn test_hot_cache_put_get() {
        let cache = HotCache::new();
        cache.put("key1", CachedEntity::Raw("hello".into()));
        let result = cache.get("key1");
        assert!(result.is_some());
        if let Some(CachedEntity::Raw(s)) = result {
            assert_eq!(s, "hello");
        }
    }

    #[test]
    fn test_hot_cache_miss() {
        let cache = HotCache::new();
        assert!(cache.get("nonexistent").is_none());
        let stats = cache.stats();
        assert_eq!(stats.misses, 1);
        assert_eq!(stats.hits, 0);
    }

    #[test]
    fn test_hot_cache_hit_rate() {
        let cache = HotCache::new();
        cache.put("k", CachedEntity::Raw("v".into()));
        cache.get("k"); // hit
        cache.get("k"); // hit
        cache.get("miss"); // miss
        let stats = cache.stats();
        assert_eq!(stats.hits, 2);
        assert_eq!(stats.misses, 1);
        assert!((stats.hit_rate() - 0.6667).abs() < 0.01);
    }

    #[test]
    fn test_hot_cache_lru_eviction() {
        let cache = HotCache::with_config(3, Duration::from_secs(60));
        cache.put("a", CachedEntity::Raw("1".into()));
        cache.put("b", CachedEntity::Raw("2".into()));
        cache.put("c", CachedEntity::Raw("3".into()));

        // Access "a" so it's most recently used.
        cache.get("a");

        // Insert "d" — should evict "b" (least recently used).
        cache.put("d", CachedEntity::Raw("4".into()));

        assert!(cache.get("a").is_some()); // promoted
        assert!(cache.get("b").is_none()); // evicted
        assert!(cache.get("c").is_some()); // still there
        assert!(cache.get("d").is_some()); // new
    }

    #[test]
    fn test_hot_cache_ttl_expiry() {
        let cache = HotCache::with_config(100, Duration::from_millis(1));
        cache.put("ephemeral", CachedEntity::Raw("gone".into()));
        // Sleep just past TTL.
        std::thread::sleep(Duration::from_millis(5));
        assert!(cache.get("ephemeral").is_none());
        let stats = cache.stats();
        assert_eq!(stats.expired, 1);
    }

    #[test]
    fn test_hot_cache_remove() {
        let cache = HotCache::new();
        cache.put("x", CachedEntity::Raw("val".into()));
        cache.remove("x");
        assert!(cache.get("x").is_none());
    }

    #[test]
    fn test_hot_cache_clear() {
        let cache = HotCache::new();
        for i in 0..10 {
            cache.put(&format!("k{}", i), CachedEntity::Raw(format!("v{}", i)));
        }
        assert_eq!(cache.len(), 10);
        cache.clear();
        assert!(cache.is_empty());
    }

    #[test]
    fn test_hot_cache_file_functions() {
        let cache = HotCache::new();
        cache.put(
            "src/main.rs",
            CachedEntity::FileFunctions {
                file_path: "src/main.rs".into(),
                functions: vec!["main".into(), "setup".into()],
                types: vec!["Config".into()],
            },
        );
        if let Some(CachedEntity::FileFunctions { functions, .. }) = cache.get("src/main.rs") {
            assert_eq!(functions.len(), 2);
        } else {
            panic!("Expected FileFunctions variant");
        }
    }

    #[test]
    fn test_hot_cache_purge_expired() {
        let cache = HotCache::with_config(100, Duration::from_millis(1));
        cache.put("a", CachedEntity::Raw("1".into()));
        cache.put("b", CachedEntity::Raw("2".into()));
        std::thread::sleep(Duration::from_millis(5));
        // Add a fresh one.
        cache.put_with_ttl("c", CachedEntity::Raw("3".into()), Duration::from_secs(60));
        cache.purge_expired();
        assert_eq!(cache.len(), 1); // only "c" survives
    }

    #[test]
    fn test_cache_stats_default() {
        let stats = CacheStats::default();
        assert_eq!(stats.hits, 0);
        assert_eq!(stats.misses, 0);
        assert_eq!(stats.evictions, 0);
        assert_eq!(stats.hit_rate(), 0.0);
    }
}
