//! Cache layer for agent definitions and transpiled code
//!
//! Two-level cache:
//! - Memory: LRU cache for fast access
//! - Disk: Persistent cache in ~/.typeagent/cache/

use crate::error::{Error, Result};
use lru::LruCache;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::fs;
use std::num::NonZeroUsize;
use std::path::{Path, PathBuf};

/// Cache strategy configuration
#[derive(Debug, Clone)]
pub enum CacheStrategy {
    /// Memory-only cache (LRU)
    Memory {
        /// Max entries in LRU cache
        max_entries: usize,
    },
    /// Disk-only cache
    Disk {
        /// Cache directory
        cache_dir: PathBuf,
    },
    /// Both memory and disk cache
    Both {
        /// Max entries in memory
        max_entries: usize,
        /// Disk cache directory
        cache_dir: PathBuf,
    },
    /// No caching
    None,
}

impl Default for CacheStrategy {
    fn default() -> Self {
        Self::Both {
            max_entries: 1000,
            cache_dir: dirs::home_dir()
                .unwrap_or_else(|| PathBuf::from("."))
                .join(".typeagent")
                .join("cache"),
        }
    }
}

/// Cached entry metadata
#[derive(Debug, Clone, Serialize, Deserialize)]
struct CacheEntry {
    /// File path (for debugging)
    file_path: String,
    /// File modification time (Unix timestamp)
    mtime: u64,
    /// Cached Nickel code
    nickel_code: String,
}

/// Cache manager
pub struct CacheManager {
    strategy: CacheStrategy,
    /// In-memory LRU cache
    memory_cache: Option<LruCache<String, CacheEntry>>,
}

impl CacheManager {
    /// Create a new cache manager with strategy
    pub fn new(strategy: CacheStrategy) -> Self {
        let memory_cache = match &strategy {
            CacheStrategy::Memory { max_entries } | CacheStrategy::Both { max_entries, .. } => {
                NonZeroUsize::new(*max_entries).map(LruCache::new)
            }
            _ => None,
        };

        Self {
            strategy,
            memory_cache,
        }
    }

    /// Get cached transpiled Nickel code
    ///
    /// Returns Some(nickel_code) if cache hit and mtime matches, None otherwise
    pub fn get_transpiled(&mut self, file_path: &str, mtime: u64) -> Option<String> {
        let cache_key = Self::compute_cache_key(file_path);

        match &self.strategy {
            CacheStrategy::None => None,

            CacheStrategy::Memory { .. } => {
                // Check memory cache only
                self.memory_cache
                    .as_mut()?
                    .get(&cache_key)
                    .and_then(|entry| {
                        if entry.mtime == mtime {
                            Some(entry.nickel_code.clone())
                        } else {
                            None
                        }
                    })
            }

            CacheStrategy::Disk { cache_dir } => {
                // Check disk cache only
                self.get_from_disk(cache_dir, &cache_key, mtime)
            }

            CacheStrategy::Both { cache_dir, .. } => {
                // Check memory first
                if let Some(entry) = self.memory_cache.as_mut()?.get(&cache_key) {
                    if entry.mtime == mtime {
                        return Some(entry.nickel_code.clone());
                    }
                }

                // Fall back to disk
                let nickel_code = self.get_from_disk(cache_dir, &cache_key, mtime)?;

                // Populate memory cache for next access
                if let Some(ref mut mem_cache) = self.memory_cache {
                    mem_cache.put(
                        cache_key,
                        CacheEntry {
                            file_path: file_path.to_string(),
                            mtime,
                            nickel_code: nickel_code.clone(),
                        },
                    );
                }

                Some(nickel_code)
            }
        }
    }

    /// Cache transpiled Nickel code
    pub fn put_transpiled(&mut self, file_path: &str, mtime: u64, nickel_code: &str) -> Result<()> {
        let cache_key = Self::compute_cache_key(file_path);
        let entry = CacheEntry {
            file_path: file_path.to_string(),
            mtime,
            nickel_code: nickel_code.to_string(),
        };

        match &self.strategy {
            CacheStrategy::None => Ok(()),

            CacheStrategy::Memory { .. } => {
                // Store in memory only
                if let Some(ref mut mem_cache) = self.memory_cache {
                    mem_cache.put(cache_key, entry);
                }
                Ok(())
            }

            CacheStrategy::Disk { cache_dir } => {
                // Store on disk only
                self.put_to_disk(cache_dir, &cache_key, &entry)
            }

            CacheStrategy::Both { cache_dir, .. } => {
                // Store in both
                if let Some(ref mut mem_cache) = self.memory_cache {
                    mem_cache.put(cache_key.clone(), entry.clone());
                }
                self.put_to_disk(cache_dir, &cache_key, &entry)
            }
        }
    }

    /// Clear all caches
    pub fn clear(&mut self) -> Result<()> {
        // Clear memory
        if let Some(ref mut mem_cache) = self.memory_cache {
            mem_cache.clear();
        }

        // Clear disk
        match &self.strategy {
            CacheStrategy::Disk { cache_dir } | CacheStrategy::Both { cache_dir, .. } => {
                if cache_dir.exists() {
                    fs::remove_dir_all(cache_dir).map_err(|e| {
                        Error::cache(
                            format!("Failed to remove cache directory: {:?}", cache_dir),
                            e.to_string(),
                        )
                    })?;
                }
            }
            _ => {}
        }

        Ok(())
    }

    /// Get cache statistics
    pub fn stats(&self) -> CacheStats {
        let memory_entries = self.memory_cache.as_ref().map(|c| c.len()).unwrap_or(0);

        let disk_entries = match &self.strategy {
            CacheStrategy::Disk { cache_dir } | CacheStrategy::Both { cache_dir, .. } => {
                if cache_dir.exists() {
                    fs::read_dir(cache_dir)
                        .ok()
                        .map(|entries| entries.filter_map(|e| e.ok()).count())
                        .unwrap_or(0)
                } else {
                    0
                }
            }
            _ => 0,
        };

        CacheStats {
            memory_entries,
            disk_entries,
            strategy: format!("{:?}", self.strategy),
        }
    }

    /// Compute SHA256 hash of file path for cache key
    fn compute_cache_key(file_path: &str) -> String {
        let mut hasher = Sha256::new();
        hasher.update(file_path.as_bytes());
        hex::encode(hasher.finalize())
    }

    /// Get entry from disk cache
    fn get_from_disk(&self, cache_dir: &Path, cache_key: &str, mtime: u64) -> Option<String> {
        let cache_file = cache_dir.join(cache_key);

        if !cache_file.exists() {
            return None;
        }

        // Read and deserialize
        let content = fs::read_to_string(&cache_file).ok()?;
        let entry: CacheEntry = serde_json::from_str(&content).ok()?;

        // Validate mtime
        if entry.mtime != mtime {
            // Stale cache - remove it (ignore errors)
            fs::remove_file(&cache_file).ok();
            return None;
        }

        Some(entry.nickel_code)
    }

    /// Write entry to disk cache
    fn put_to_disk(&self, cache_dir: &Path, cache_key: &str, entry: &CacheEntry) -> Result<()> {
        // Ensure cache directory exists
        if !cache_dir.exists() {
            fs::create_dir_all(cache_dir).map_err(|e| {
                Error::cache(
                    format!("Failed to create cache directory: {:?}", cache_dir),
                    e.to_string(),
                )
            })?;
        }

        let cache_file = cache_dir.join(cache_key);

        // Serialize and write
        let content = serde_json::to_string_pretty(entry).map_err(|e| {
            Error::cache(
                format!("Failed to serialize cache entry for: {}", entry.file_path),
                e.to_string(),
            )
        })?;

        fs::write(&cache_file, content).map_err(|e| {
            Error::cache(
                format!("Failed to write cache file: {:?}", cache_file),
                e.to_string(),
            )
        })?;

        Ok(())
    }
}

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

/// Cache statistics
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheStats {
    /// Number of entries in memory cache
    pub memory_entries: usize,
    /// Number of entries in disk cache
    pub disk_entries: usize,
    /// Cache strategy description
    pub strategy: String,
}

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

    #[test]
    fn test_compute_cache_key() {
        let key1 = CacheManager::compute_cache_key("/path/to/agent.mdx");
        let key2 = CacheManager::compute_cache_key("/path/to/agent.mdx");
        let key3 = CacheManager::compute_cache_key("/different/path.mdx");

        // Same path produces same key
        assert_eq!(key1, key2);
        // Different path produces different key
        assert_ne!(key1, key3);
        // Keys are hex-encoded SHA256 (64 chars)
        assert_eq!(key1.len(), 64);
    }

    #[test]
    fn test_memory_cache_hit() {
        let mut cache = CacheManager::new(CacheStrategy::Memory { max_entries: 10 });

        let path = "/test/agent.mdx";
        let mtime = 12345u64;
        let nickel = "{ config = { role = \"test\" } }";

        // Put to cache
        cache.put_transpiled(path, mtime, nickel).unwrap();

        // Get from cache - should hit
        let result = cache.get_transpiled(path, mtime);
        assert_eq!(result, Some(nickel.to_string()));
    }

    #[test]
    fn test_memory_cache_miss_stale_mtime() {
        let mut cache = CacheManager::new(CacheStrategy::Memory { max_entries: 10 });

        let path = "/test/agent.mdx";
        let mtime = 12345u64;
        let nickel = "{ config = { role = \"test\" } }";

        // Put to cache
        cache.put_transpiled(path, mtime, nickel).unwrap();

        // Get with different mtime - should miss
        let result = cache.get_transpiled(path, mtime + 1);
        assert_eq!(result, None);
    }

    #[test]
    fn test_disk_cache_roundtrip() {
        let temp_dir = std::env::temp_dir().join("typeagent-test-cache");
        let mut cache = CacheManager::new(CacheStrategy::Disk {
            cache_dir: temp_dir.clone(),
        });

        let path = "/test/agent.mdx";
        let mtime = 12345u64;
        let nickel = "{ config = { role = \"test\" } }";

        // Put to cache
        cache.put_transpiled(path, mtime, nickel).unwrap();

        // Get from cache - should hit
        let result = cache.get_transpiled(path, mtime);
        assert_eq!(result, Some(nickel.to_string()));

        // Cleanup
        cache.clear().unwrap();
    }

    #[test]
    fn test_both_cache_strategy() {
        let temp_dir = std::env::temp_dir().join("typeagent-test-cache-both");
        let mut cache = CacheManager::new(CacheStrategy::Both {
            max_entries: 10,
            cache_dir: temp_dir.clone(),
        });

        let path = "/test/agent.mdx";
        let mtime = 12345u64;
        let nickel = "{ config = { role = \"test\" } }";

        // Put to cache (should go to both memory and disk)
        cache.put_transpiled(path, mtime, nickel).unwrap();

        // Clear memory cache
        if let Some(ref mut mem_cache) = cache.memory_cache {
            mem_cache.clear();
        }

        // Get from cache - should hit from disk and repopulate memory
        let result = cache.get_transpiled(path, mtime);
        assert_eq!(result, Some(nickel.to_string()));

        // Memory should now have the entry
        assert_eq!(cache.memory_cache.as_ref().unwrap().len(), 1);

        // Cleanup
        cache.clear().unwrap();
    }

    #[test]
    fn test_cache_stats() {
        let temp_dir = std::env::temp_dir().join("typeagent-test-cache-stats");
        let mut cache = CacheManager::new(CacheStrategy::Both {
            max_entries: 10,
            cache_dir: temp_dir.clone(),
        });

        // Initially empty
        let stats = cache.stats();
        assert_eq!(stats.memory_entries, 0);
        assert_eq!(stats.disk_entries, 0);

        // Add entries
        cache.put_transpiled("/test/1.mdx", 111, "code1").unwrap();
        cache.put_transpiled("/test/2.mdx", 222, "code2").unwrap();

        let stats = cache.stats();
        assert_eq!(stats.memory_entries, 2);
        assert_eq!(stats.disk_entries, 2);

        // Cleanup
        cache.clear().unwrap();
    }

    #[test]
    fn test_clear_cache() {
        let temp_dir = std::env::temp_dir().join("typeagent-test-cache-clear");
        let mut cache = CacheManager::new(CacheStrategy::Both {
            max_entries: 10,
            cache_dir: temp_dir.clone(),
        });

        // Add entries
        cache.put_transpiled("/test/1.mdx", 111, "code1").unwrap();
        cache.put_transpiled("/test/2.mdx", 222, "code2").unwrap();

        // Verify they exist
        let stats = cache.stats();
        assert_eq!(stats.memory_entries, 2);
        assert_eq!(stats.disk_entries, 2);

        // Clear
        cache.clear().unwrap();

        // Verify empty
        let stats = cache.stats();
        assert_eq!(stats.memory_entries, 0);
        assert_eq!(stats.disk_entries, 0);
    }

    #[test]
    fn test_none_strategy() {
        let mut cache = CacheManager::new(CacheStrategy::None);

        let path = "/test/agent.mdx";
        let mtime = 12345u64;
        let nickel = "{ config = { role = \"test\" } }";

        // Put should succeed but do nothing
        cache.put_transpiled(path, mtime, nickel).unwrap();

        // Get should always return None
        let result = cache.get_transpiled(path, mtime);
        assert_eq!(result, None);
    }
}
