//! Persistent cache implementation for disk-based caching
//!
//! Provides file system-based caching for long-term storage of content
//! with compression, serialization, and cache validation.

use std::collections::HashMap;
use std::fs;
use std::hash::Hash;
use std::path::{Path, PathBuf};

use serde::{Deserialize, Serialize};
use tracing::{debug, error, warn};

use super::{current_timestamp, estimate_size, CacheConfig, CacheEntry, CacheMetadata};

/// Persistent cache that stores entries on disk
#[derive(Debug)]
pub struct PersistentCache {
    /// Base directory for cache files
    cache_dir: PathBuf,
    /// Cache configuration
    config: CacheConfig,
    /// In-memory index of cached items
    index: HashMap<String, CacheFileInfo>,
}

/// Information about a cached file
#[derive(Debug, Clone, Serialize, Deserialize)]
struct CacheFileInfo {
    /// File path relative to cache directory
    file_path: String,
    /// Cache metadata
    metadata: CacheMetadata,
    /// File hash for integrity checking
    hash: String,
}

/// Cache file header with metadata
#[derive(Debug, Clone, Serialize, Deserialize)]
struct CacheFileHeader {
    /// Version of the cache format
    version: u32,
    /// Content metadata
    metadata: CacheMetadata,
    /// Compression used (none, gzip, etc.)
    compression: String,
    /// Data hash for integrity
    hash: String,
}

impl PersistentCache {
    /// Create a new persistent cache
    pub fn new(cache_dir: PathBuf) -> std::io::Result<Self> {
        Self::with_config(cache_dir, CacheConfig::default())
    }

    /// Create a new persistent cache with configuration
    pub fn with_config(cache_dir: PathBuf, config: CacheConfig) -> std::io::Result<Self> {
        // Create cache directory if it doesn't exist
        fs::create_dir_all(&cache_dir)?;

        let mut cache = Self {
            cache_dir,
            config,
            index: HashMap::new(),
        };

        // Load existing cache index
        cache.load_index()?;

        Ok(cache)
    }

    /// Store an item in the persistent cache
    pub fn put<T>(
        &mut self,
        key: &str,
        data: &T,
        content_type: String,
        version: String,
    ) -> std::io::Result<()>
    where
        T: Serialize,
    {
        let serialized = serde_json::to_vec(data)
            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;

        let size_bytes = serialized.len();
        let hash = self.calculate_hash(&serialized);

        // Create metadata
        let metadata = CacheMetadata::new(content_type, version, size_bytes);

        // Create file header
        let header = CacheFileHeader {
            version: 1,
            metadata: metadata.clone(),
            compression: "none".to_string(),
            hash: hash.clone(),
        };

        // Generate file path
        let file_path = self.get_file_path(key);
        let relative_path = file_path
            .strip_prefix(&self.cache_dir)
            .unwrap_or(&file_path)
            .to_string_lossy()
            .to_string();

        // Create parent directory if needed
        if let Some(parent) = file_path.parent() {
            fs::create_dir_all(parent)?;
        }

        // Write cache file
        let header_bytes = serde_json::to_vec(&header)
            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;

        let header_len = header_bytes.len() as u32;
        let mut file_data = Vec::new();
        file_data.extend_from_slice(&header_len.to_le_bytes());
        file_data.extend_from_slice(&header_bytes);
        file_data.extend_from_slice(&serialized);

        fs::write(&file_path, file_data)?;

        // Update index
        let file_info = CacheFileInfo {
            file_path: relative_path,
            metadata,
            hash,
        };

        self.index.insert(key.to_string(), file_info);
        self.save_index()?;

        debug!("Cached item '{}' to disk ({} bytes)", key, size_bytes);
        Ok(())
    }

    /// Get an item from the persistent cache
    pub fn get<T>(&mut self, key: &str) -> std::io::Result<Option<T>>
    where
        T: for<'de> Deserialize<'de>,
    {
        let file_info = match self.index.get(key) {
            Some(info) => info,
            None => return Ok(None),
        };

        // Check if entry is expired
        let ttl = self
            .config
            .get_ttl_for_content_type(&file_info.metadata.content_type);
        if file_info.metadata.is_expired(ttl) {
            warn!("Cache entry '{}' is expired, removing", key);
            self.remove(key)?;
            return Ok(None);
        }

        let file_path = self.cache_dir.join(&file_info.file_path);
        if !file_path.exists() {
            warn!("Cache file missing for key '{}', removing from index", key);
            self.index.remove(key);
            self.save_index()?;
            return Ok(None);
        }

        // Read cache file
        let file_data = fs::read(&file_path)?;
        if file_data.len() < 4 {
            error!("Invalid cache file format for key '{}'", key);
            self.remove(key)?;
            return Ok(None);
        }

        // Parse header length
        let header_len =
            u32::from_le_bytes([file_data[0], file_data[1], file_data[2], file_data[3]]) as usize;

        if file_data.len() < 4 + header_len {
            error!("Invalid cache file format for key '{}'", key);
            self.remove(key)?;
            return Ok(None);
        }

        // Parse header
        let header_bytes = &file_data[4..4 + header_len];
        let header: CacheFileHeader = serde_json::from_slice(header_bytes)
            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;

        // Extract data
        let data_bytes = &file_data[4 + header_len..];

        // Verify integrity
        let data_hash = self.calculate_hash(data_bytes);
        if data_hash != header.hash {
            error!("Cache integrity check failed for key '{}'", key);
            self.remove(key)?;
            return Ok(None);
        }

        // Deserialize data
        let data: T = serde_json::from_slice(data_bytes)
            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;

        // Update access time in index
        if let Some(file_info) = self.index.get_mut(key) {
            file_info.metadata.touch();
            self.save_index()?;
        }

        debug!("Retrieved cached item '{}' from disk", key);
        Ok(Some(data))
    }

    /// Remove an item from the cache
    pub fn remove(&mut self, key: &str) -> std::io::Result<bool> {
        let file_info = match self.index.remove(key) {
            Some(info) => info,
            None => return Ok(false),
        };

        let file_path = self.cache_dir.join(&file_info.file_path);
        if file_path.exists() {
            fs::remove_file(&file_path)?;
        }

        self.save_index()?;
        debug!("Removed cached item '{}' from disk", key);
        Ok(true)
    }

    /// Clear all cached items
    pub fn clear(&mut self) -> std::io::Result<()> {
        // Remove all files
        for file_info in self.index.values() {
            let file_path = self.cache_dir.join(&file_info.file_path);
            if file_path.exists() {
                if let Err(e) = fs::remove_file(&file_path) {
                    warn!("Failed to remove cache file {:?}: {}", file_path, e);
                }
            }
        }

        self.index.clear();
        self.save_index()?;
        debug!("Cleared all cached items from disk");
        Ok(())
    }

    /// Get the number of cached items
    pub fn len(&self) -> usize {
        self.index.len()
    }

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

    /// Cleanup expired entries
    pub fn cleanup_expired(&mut self) -> std::io::Result<()> {
        let mut expired_keys = Vec::new();

        for (key, file_info) in &self.index {
            let ttl = self
                .config
                .get_ttl_for_content_type(&file_info.metadata.content_type);
            if file_info.metadata.is_expired(ttl) {
                expired_keys.push(key.clone());
            }
        }

        for key in expired_keys {
            self.remove(&key)?;
        }

        debug!("Cleaned up expired cache entries from disk");
        Ok(())
    }

    /// Get total size of cached data in bytes
    pub fn total_size_bytes(&self) -> usize {
        self.index
            .values()
            .map(|info| info.metadata.size_bytes)
            .sum()
    }

    /// Generate file path for a cache key
    fn get_file_path(&self, key: &str) -> PathBuf {
        // Create a safe filename from the key
        let safe_key = key.replace(['/', '\\', ':', '*', '?', '"', '<', '>', '|'], "_");
        let hash = self.calculate_hash(key.as_bytes());
        let filename = format!("{}_{}.cache", safe_key, &hash[..8]);

        // Organize into subdirectories based on first two chars of hash
        let subdir = if hash.len() >= 2 { &hash[..2] } else { "00" };

        self.cache_dir.join(subdir).join(filename)
    }

    /// Calculate hash of data for integrity checking
    fn calculate_hash(&self, data: &[u8]) -> String {
        // Simple hash implementation (in production, use a proper hash function)
        use std::collections::hash_map::DefaultHasher;
        use std::hash::Hasher;

        let mut hasher = DefaultHasher::new();
        hasher.write(data);
        format!("{:x}", hasher.finish())
    }

    /// Load cache index from disk
    fn load_index(&mut self) -> std::io::Result<()> {
        let index_path = self.cache_dir.join("index.json");

        if index_path.exists() {
            let index_data = fs::read_to_string(&index_path)?;
            match serde_json::from_str::<HashMap<String, CacheFileInfo>>(&index_data) {
                Ok(index) => {
                    self.index = index;
                    debug!("Loaded cache index with {} entries", self.index.len());
                }
                Err(e) => {
                    warn!("Failed to parse cache index, starting fresh: {}", e);
                    self.index.clear();
                }
            }
        }

        Ok(())
    }

    /// Save cache index to disk
    fn save_index(&self) -> std::io::Result<()> {
        let index_path = self.cache_dir.join("index.json");
        let index_data = serde_json::to_string_pretty(&self.index)
            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;

        fs::write(&index_path, index_data)?;
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use std::env;

    use tempfile::TempDir;

    use super::*;

    #[test]
    fn test_persistent_cache_basic_operations() -> std::io::Result<()> {
        let temp_dir = env::temp_dir().join("test_cache");
        let mut cache = PersistentCache::new(temp_dir)?;

        // Test put and get
        let data = "test data".to_string();
        cache.put("key1", &data, "test".to_string(), "v1".to_string())?;

        let retrieved: Option<String> = cache.get("key1")?;
        assert_eq!(retrieved, Some(data));

        // Test cache miss
        let missing: Option<String> = cache.get("nonexistent")?;
        assert!(missing.is_none());

        Ok(())
    }

    #[test]
    fn test_persistent_cache_removal() -> std::io::Result<()> {
        let temp_dir = TempDir::new()?;
        let mut cache = PersistentCache::new(temp_dir.path().to_path_buf())?;

        let data = "test data".to_string();
        cache.put("key1", &data, "test".to_string(), "v1".to_string())?;

        assert_eq!(cache.len(), 1);

        let removed = cache.remove("key1")?;
        assert!(removed);
        assert_eq!(cache.len(), 0);

        let retrieved: Option<String> = cache.get("key1")?;
        assert!(retrieved.is_none());

        Ok(())
    }

    #[test]
    fn test_persistent_cache_persistence() -> std::io::Result<()> {
        let temp_dir = TempDir::new()?;
        let cache_dir = temp_dir.path().to_path_buf();

        // Create cache and add data
        {
            let mut cache = PersistentCache::new(cache_dir.clone())?;
            let data = "persistent data".to_string();
            cache.put(
                "persistent_key",
                &data,
                "test".to_string(),
                "v1".to_string(),
            )?;
        }

        // Create new cache instance and verify data persisted
        {
            let mut cache = PersistentCache::new(cache_dir)?;
            let retrieved: Option<String> = cache.get("persistent_key")?;
            assert_eq!(retrieved, Some("persistent data".to_string()));
        }

        Ok(())
    }

    #[test]
    fn test_persistent_cache_clear() -> std::io::Result<()> {
        let temp_dir = TempDir::new()?;
        let mut cache = PersistentCache::new(temp_dir.path().to_path_buf())?;

        let data = "test data".to_string();
        cache.put("key1", &data, "test".to_string(), "v1".to_string())?;
        cache.put("key2", &data, "test".to_string(), "v1".to_string())?;

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

        cache.clear()?;
        assert_eq!(cache.len(), 0);

        let retrieved: Option<String> = cache.get("key1")?;
        assert!(retrieved.is_none());

        Ok(())
    }
}
