//! Comprehensive caching system for Rustelo
//!
//! This module provides multi-level caching for different content types
//! including:
//! - Memory cache with LRU eviction
//! - File system cache for persistent storage
//! - Content-aware caching strategies
//! - Cache statistics and monitoring

use std::collections::HashMap;
use std::hash::Hash;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};

use serde::{Deserialize, Serialize};

pub mod content;
pub mod lru;
pub mod memory;
pub mod persistent;

/// Cache entry metadata
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheMetadata {
    /// When the entry was created (Unix timestamp)
    pub created_at: u64,
    /// When the entry was last accessed (Unix timestamp)
    pub last_accessed: u64,
    /// How many times this entry has been accessed
    pub access_count: u32,
    /// Content version/etag for validation
    pub version: String,
    /// Content language if applicable
    pub language: Option<String>,
    /// Content type identifier
    pub content_type: String,
    /// Size in bytes
    pub size_bytes: usize,
}

impl CacheMetadata {
    pub fn new(content_type: String, version: String, size_bytes: usize) -> Self {
        let now = current_timestamp();
        Self {
            created_at: now,
            last_accessed: now,
            access_count: 1,
            version,
            language: None,
            content_type,
            size_bytes,
        }
    }

    pub fn with_language(mut self, language: String) -> Self {
        self.language = Some(language);
        self
    }

    pub fn touch(&mut self) {
        self.last_accessed = current_timestamp();
        self.access_count += 1;
    }

    pub fn is_expired(&self, max_age_seconds: u64) -> bool {
        current_timestamp() - self.created_at >= max_age_seconds
    }

    pub fn age_seconds(&self) -> u64 {
        current_timestamp() - self.created_at
    }

    pub fn time_since_last_access(&self) -> u64 {
        current_timestamp() - self.last_accessed
    }
}

/// Generic cache entry with metadata
#[derive(Debug, Clone)]
pub struct CacheEntry<T> {
    pub data: Arc<T>,
    pub metadata: CacheMetadata,
}

impl<T> CacheEntry<T> {
    pub fn new(data: T, content_type: String, version: String, size_bytes: usize) -> Self {
        Self {
            data: Arc::new(data),
            metadata: CacheMetadata::new(content_type, version, size_bytes),
        }
    }

    pub fn with_language(mut self, language: String) -> Self {
        self.metadata = self.metadata.with_language(language);
        self
    }

    pub fn touch(&mut self) {
        self.metadata.touch();
    }

    pub fn is_expired(&self, max_age_seconds: u64) -> bool {
        self.metadata.is_expired(max_age_seconds)
    }
}

/// Cache statistics for monitoring and debugging
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheStats {
    /// Total number of entries
    pub total_entries: usize,
    /// Total memory usage in bytes
    pub total_size_bytes: usize,
    /// Cache hit ratio (0.0 to 1.0)
    pub hit_ratio: f64,
    /// Total hits
    pub total_hits: u64,
    /// Total misses
    pub total_misses: u64,
    /// Average age of entries in seconds
    pub avg_age_seconds: f64,
    /// Entries by content type
    pub entries_by_type: HashMap<String, usize>,
    /// Entries by language
    pub entries_by_language: HashMap<String, usize>,
    /// Cache utilization percentage
    pub utilization_percent: f64,
    /// Most accessed entries
    pub most_accessed: Vec<(String, u32)>,
}

impl Default for CacheStats {
    fn default() -> Self {
        Self {
            total_entries: 0,
            total_size_bytes: 0,
            hit_ratio: 0.0,
            total_hits: 0,
            total_misses: 0,
            avg_age_seconds: 0.0,
            entries_by_type: HashMap::new(),
            entries_by_language: HashMap::new(),
            utilization_percent: 0.0,
            most_accessed: Vec::new(),
        }
    }
}

/// Cache configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheConfig {
    /// Maximum number of entries in memory cache
    pub max_entries: usize,
    /// Maximum memory usage in bytes
    pub max_memory_bytes: usize,
    /// Default TTL in seconds
    pub default_ttl_seconds: u64,
    /// Content-specific TTL settings
    pub content_type_ttl: HashMap<String, u64>,
    /// Whether to enable persistent caching
    pub enable_persistent_cache: bool,
    /// Persistent cache directory
    pub persistent_cache_dir: Option<std::path::PathBuf>,
    /// Cleanup interval in seconds
    pub cleanup_interval_seconds: u64,
    /// Whether to enable cache prewarming
    pub enable_prewarming: bool,
}

impl Default for CacheConfig {
    fn default() -> Self {
        let mut content_type_ttl = HashMap::new();
        content_type_ttl.insert("routes".to_string(), 3600); // 1 hour
        content_type_ttl.insert("i18n".to_string(), 1800); // 30 minutes
        content_type_ttl.insert("manifest".to_string(), 300); // 5 minutes
        content_type_ttl.insert("static".to_string(), 86400); // 24 hours

        // Add all available content types with default TTL
        let available_types = crate::get_all_content_types();
        for content_type in available_types {
            content_type_ttl.insert(content_type.to_string(), 900); // 15 minutes
        }

        Self {
            max_entries: 1000,
            max_memory_bytes: 50 * 1024 * 1024, // 50MB
            default_ttl_seconds: 1800,          // 30 minutes
            content_type_ttl,
            enable_persistent_cache: true,
            persistent_cache_dir: Some(std::path::PathBuf::from("cache")),
            cleanup_interval_seconds: 300, // 5 minutes
            enable_prewarming: true,
        }
    }
}

impl CacheConfig {
    pub fn get_ttl_for_content_type(&self, content_type: &str) -> u64 {
        self.content_type_ttl
            .get(content_type)
            .copied()
            .unwrap_or(self.default_ttl_seconds)
    }
}

/// Trait for cache implementations
pub trait Cache<K, V>
where
    K: Clone + Hash + Eq + Send + Sync,
    V: Clone + Send + Sync,
{
    fn get(&self, key: &K) -> Option<CacheEntry<V>>;
    fn put(&self, key: K, value: V, content_type: String, version: String)
        -> Option<CacheEntry<V>>;
    fn remove(&self, key: &K) -> Option<CacheEntry<V>>;
    fn clear(&self);
    fn size(&self) -> usize;
    fn stats(&self) -> CacheStats;
    fn cleanup_expired(&self);
    fn get_config(&self) -> &CacheConfig;
}

/// Cache key for content items with category support
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct ContentCacheKey {
    pub content_type: String,
    pub identifier: String,
    pub language: Option<String>,
    pub category: Option<String>,
    pub version: Option<String>,
}

impl ContentCacheKey {
    pub fn new(content_type: impl Into<String>, identifier: impl Into<String>) -> Self {
        Self {
            content_type: content_type.into(),
            identifier: identifier.into(),
            language: None,
            category: None,
            version: None,
        }
    }

    pub fn with_language(mut self, language: impl Into<String>) -> Self {
        self.language = Some(language.into());
        self
    }

    pub fn with_category(mut self, category: impl Into<String>) -> Self {
        self.category = Some(category.into());
        self
    }

    pub fn with_version(mut self, version: impl Into<String>) -> Self {
        self.version = Some(version.into());
        self
    }

    /// Create a cache key for category-based content (e.g., posts)
    pub fn for_post(
        content_type: impl Into<String>,
        language: impl Into<String>,
        category: impl Into<String>,
        slug: impl Into<String>,
    ) -> Self {
        Self::new(content_type, slug)
            .with_language(language)
            .with_category(category)
    }

    /// Create a cache key for category index
    pub fn for_category_index(
        content_type: impl Into<String>,
        language: impl Into<String>,
        category: impl Into<String>,
    ) -> Self {
        let category_str = category.into();
        Self::new(content_type, format!("{}/index", &category_str))
            .with_language(language)
            .with_category(category_str)
    }

    /// Create a cache key for global metadata
    pub fn for_global_meta(content_type: impl Into<String>) -> Self {
        Self::new(content_type, "global_meta")
    }

    /// Create a cache key for translations
    pub fn for_translations(content_type: impl Into<String>, post_id: impl Into<String>) -> Self {
        Self::new(content_type, format!("translations:{}", post_id.into()))
    }

    pub fn cache_key(&self) -> String {
        let mut parts = vec![self.content_type.clone()];

        if let Some(ref lang) = self.language {
            parts.push(lang.clone());
        }

        if let Some(ref category) = self.category {
            parts.push(category.clone());
        }

        parts.push(self.identifier.clone());

        if let Some(ref version) = self.version {
            parts.push(version.clone());
        }

        parts.join("/")
    }
}

impl std::fmt::Display for ContentCacheKey {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{}({}):{:?}:{:?}:{:?}",
            self.content_type, self.identifier, self.language, self.category, self.version
        )
    }
}

/// Get current Unix timestamp
pub fn current_timestamp() -> u64 {
    #[cfg(target_arch = "wasm32")]
    {
        // For WASM, use JavaScript Date.now() divided by 1000 to get seconds
        use web_sys::js_sys::Date;
        (Date::now() / 1000.0) as u64
    }
    #[cfg(not(target_arch = "wasm32"))]
    {
        // For native, use SystemTime
        SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs()
    }
}

/// Estimate size of serializable data
pub fn estimate_size<T: serde::Serialize>(data: &T) -> usize {
    serde_json::to_string(data).unwrap_or_default().len()
}

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

    #[test]
    fn test_cache_metadata() {
        let mut metadata = CacheMetadata::new("test".to_string(), "v1".to_string(), 100);

        assert_eq!(metadata.access_count, 1);
        assert!(metadata.age_seconds() < 1); // Should be very recent

        metadata.touch();
        assert_eq!(metadata.access_count, 2);
    }

    #[test]
    fn test_cache_key() {
        let key = ContentCacheKey::new("content", "post-1")
            .with_language("en")
            .with_version("v1");

        assert_eq!(key.content_type, "content");
        assert_eq!(key.identifier, "post-1");
        assert_eq!(key.language, Some("en".to_string()));
        assert_eq!(key.version, Some("v1".to_string()));

        let key_str = key.cache_key();
        assert!(key_str.contains("content"));
        assert!(key_str.contains("post-1"));
        assert!(key_str.contains("en"));
        assert!(key_str.contains("v1"));
    }

    #[test]
    fn test_cache_config() {
        let config = CacheConfig::default();

        assert_eq!(config.get_ttl_for_content_type("routes"), 3600);
        assert_eq!(
            config.get_ttl_for_content_type("unknown"),
            config.default_ttl_seconds
        );
    }

    #[test]
    fn test_cache_entry_expiration() {
        let entry = CacheEntry::new(
            "test data".to_string(),
            "test".to_string(),
            "v1".to_string(),
            100,
        );

        assert!(!entry.is_expired(3600)); // Should not be expired
        assert!(entry.is_expired(0)); // Should be expired with 0 TTL
    }
}
