//! Thumbnail cache manager with LRU eviction

use log::error;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::Mutex;

use crate::storage::db_service::{DatabaseService, Query, QueryParam, RusqliteService};

/// Configuration for the thumbnail cache
#[derive(Debug, Clone)]
pub struct CacheConfig {
    /// Maximum cache size in bytes (0 = unlimited)
    pub max_size_bytes: u64,
    /// Subdirectory name for cached thumbnails
    pub cache_subdir: String,
    /// Whether caching is enabled
    pub enabled: bool,
}

impl Default for CacheConfig {
    fn default() -> Self {
        Self {
            max_size_bytes: 1024 * 1024 * 1024, // 1GB
            cache_subdir: "thumbnails".to_string(),
            enabled: true,
        }
    }
}

/// Thumbnail cache with LRU eviction
pub struct ThumbnailCache {
    config: Arc<Mutex<CacheConfig>>,
    cache_dir: PathBuf,
}

impl ThumbnailCache {
    /// Create a new thumbnail cache
    pub fn new(app_data_dir: PathBuf, config: CacheConfig) -> Self {
        let cache_dir = app_data_dir.join(&config.cache_subdir);

        // Create cache directory if it doesn't exist
        if let Err(e) = std::fs::create_dir_all(&cache_dir) {
            error!("Failed to create thumbnail cache directory: {}", e);
        }

        Self {
            config: Arc::new(Mutex::new(config)),
            cache_dir,
        }
    }

    /// Check if caching is enabled
    pub fn is_enabled(&self) -> bool {
        self.config.lock().map(|c| c.enabled).unwrap_or(true)
    }

    /// Get cached thumbnail path, or None if not cached
    /// Updates last_accessed timestamp for LRU tracking
    pub async fn get_cached_path(
        &self,
        db: Arc<RusqliteService>,
        item_id: &str,
        image_type: &str,
        tag: &str,
    ) -> Option<PathBuf> {
        // Primary lookup: exact (item_id, image_type, tag) match.
        let exact = Query::with_params(
            "SELECT file_path FROM thumbnails
             WHERE item_id = ? AND image_type = ? AND image_tag = ?",
            vec![
                QueryParam::String(item_id.to_string()),
                QueryParam::String(image_type.to_string()),
                QueryParam::String(tag.to_string()),
            ],
        );

        if let Ok(Some(path_str)) = db
            .query_optional(exact, |row| row.get::<_, String>(0))
            .await
        {
            let path = PathBuf::from(&path_str);
            if path.exists() {
                self.touch(&db, item_id, image_type, Some(tag)).await;
                return Some(path);
            }
            // File gone — drop the stale row and fall through to the tag-agnostic
            // lookup below (another cached image for this item may still exist).
            let _ = db
                .execute(Query::with_params(
                    "DELETE FROM thumbnails WHERE item_id = ? AND image_type = ? AND image_tag = ?",
                    vec![
                        QueryParam::String(item_id.to_string()),
                        QueryParam::String(image_type.to_string()),
                        QueryParam::String(tag.to_string()),
                    ],
                ))
                .await;
        }

        // Fallback: any cached image for this item + type, newest first. The
        // `image_tag` is a cache-busting version, and callers don't always pass
        // the same tag the image was cached under — e.g. the mini player asks for
        // the album image using the *track's* primary_image_tag. Ignoring the tag
        // here lets those still resolve offline instead of hitting the server.
        let any_tag = Query::with_params(
            "SELECT file_path FROM thumbnails
             WHERE item_id = ? AND image_type = ?
             ORDER BY cached_at DESC LIMIT 1",
            vec![
                QueryParam::String(item_id.to_string()),
                QueryParam::String(image_type.to_string()),
            ],
        );

        let path_str: String = db.query_optional(any_tag, |row| row.get(0)).await.ok()??;
        let path = PathBuf::from(&path_str);
        if path.exists() {
            self.touch(&db, item_id, image_type, None).await;
            Some(path)
        } else {
            None
        }
    }

    /// Update `last_accessed` for LRU tracking. When `tag` is `Some`, scope to
    /// that exact row; when `None`, touch every row for the item + type.
    async fn touch(
        &self,
        db: &Arc<RusqliteService>,
        item_id: &str,
        image_type: &str,
        tag: Option<&str>,
    ) {
        let query = match tag {
            Some(tag) => Query::with_params(
                "UPDATE thumbnails SET last_accessed = CURRENT_TIMESTAMP
                 WHERE item_id = ? AND image_type = ? AND image_tag = ?",
                vec![
                    QueryParam::String(item_id.to_string()),
                    QueryParam::String(image_type.to_string()),
                    QueryParam::String(tag.to_string()),
                ],
            ),
            None => Query::with_params(
                "UPDATE thumbnails SET last_accessed = CURRENT_TIMESTAMP
                 WHERE item_id = ? AND image_type = ?",
                vec![
                    QueryParam::String(item_id.to_string()),
                    QueryParam::String(image_type.to_string()),
                ],
            ),
        };
        let _ = db.execute(query).await;
    }

    /// Save thumbnail to cache
    // The arguments are the cache key (item/type/tag) plus the payload and its
    // dimensions — all independent scalars borrowed from the caller. A parameter
    // struct would only move the same list one level down.
    #[allow(clippy::too_many_arguments)]
    pub async fn save_thumbnail(
        &self,
        db: Arc<RusqliteService>,
        item_id: &str,
        image_type: &str,
        tag: &str,
        data: &[u8],
        width: Option<i32>,
        height: Option<i32>,
    ) -> Result<PathBuf, String> {
        if !self.is_enabled() {
            return Err("Thumbnail caching is disabled".to_string());
        }

        // Generate safe filename
        let safe_tag = tag.replace(|c: char| !c.is_alphanumeric(), "_");
        let filename = format!("{}_{}_{}.jpg", item_id, image_type, safe_tag);
        let file_path = self.cache_dir.join(&filename);

        // Ensure we have space (evict LRU items if needed)
        self.ensure_space(db.clone(), data.len() as u64).await?;

        // Write file to disk
        std::fs::write(&file_path, data).map_err(|e| format!("Failed to write file: {}", e))?;

        // Insert/update database entry
        let query = Query::with_params(
            "INSERT INTO thumbnails (item_id, image_type, image_tag, file_path, width, height, file_size, last_accessed, cached_at)
             VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
             ON CONFLICT(item_id, image_type, image_tag) DO UPDATE SET
                file_path = excluded.file_path,
                width = excluded.width,
                height = excluded.height,
                file_size = excluded.file_size,
                last_accessed = CURRENT_TIMESTAMP",
            vec![
                QueryParam::String(item_id.to_string()),
                QueryParam::String(image_type.to_string()),
                QueryParam::String(tag.to_string()),
                QueryParam::String(file_path.to_string_lossy().to_string()),
                width.map(QueryParam::Int).unwrap_or(QueryParam::Null),
                height.map(QueryParam::Int).unwrap_or(QueryParam::Null),
                QueryParam::Int64(data.len() as i64),
            ],
        );

        db.execute(query)
            .await
            .map_err(|e| format!("Failed to save to database: {}", e))?;

        Ok(file_path)
    }

    /// Ensure there's enough space by evicting LRU items if needed
    async fn ensure_space(
        &self,
        db: Arc<RusqliteService>,
        needed_bytes: u64,
    ) -> Result<(), String> {
        let max_size = {
            let config = self.config.lock().map_err(|e| e.to_string())?;
            config.max_size_bytes
        };

        if max_size == 0 {
            return Ok(()); // Unlimited
        }

        let current_size = self.get_cache_size(db.clone()).await;

        if current_size + needed_bytes <= max_size {
            return Ok(()); // Enough space
        }

        // Need to evict LRU items
        let to_free = (current_size + needed_bytes).saturating_sub(max_size);
        self.evict_lru(db, to_free).await
    }

    /// Evict least recently used items to free up space
    async fn evict_lru(&self, db: Arc<RusqliteService>, to_free: u64) -> Result<(), String> {
        let mut freed: u64 = 0;

        // Get items ordered by last_accessed (oldest first)
        let query = Query::new(
            "SELECT id, file_path, file_size FROM thumbnails
             ORDER BY last_accessed ASC",
        );

        let items: Vec<(i64, String, i64)> = db
            .query_many(query, |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))
            .await
            .map_err(|e| e.to_string())?;

        for (id, path, size) in items {
            if freed >= to_free {
                break;
            }

            // Delete file from disk
            let _ = std::fs::remove_file(&path);

            // Delete from database
            let delete_query = Query::with_params(
                "DELETE FROM thumbnails WHERE id = ?",
                vec![QueryParam::Int64(id)],
            );
            let _ = db.execute(delete_query).await;

            freed += size as u64;
        }

        Ok(())
    }

    /// Get current total cache size in bytes
    pub async fn get_cache_size(&self, db: Arc<RusqliteService>) -> u64 {
        let query = Query::new("SELECT COALESCE(SUM(file_size), 0) FROM thumbnails");

        db.query_one(query, |row| row.get::<_, i64>(0))
            .await
            .unwrap_or(0) as u64
    }

    /// Get count of cached items
    pub async fn get_item_count(&self, db: Arc<RusqliteService>) -> i64 {
        let query = Query::new("SELECT COUNT(*) FROM thumbnails");

        db.query_one(query, |row| row.get(0)).await.unwrap_or(0)
    }

    /// Get the current cache limit in bytes
    pub async fn get_limit(&self, db: Arc<RusqliteService>) -> u64 {
        let query = Query::with_params(
            "SELECT value FROM cache_settings WHERE key = ?",
            vec![QueryParam::String("image_cache_limit_bytes".to_string())],
        );

        db.query_optional(query, |row| row.get::<_, String>(0))
            .await
            .ok()
            .flatten()
            .and_then(|s| s.parse().ok())
            .unwrap_or(1024 * 1024 * 1024) // 1GB default
    }

    /// Set the cache limit in bytes
    pub async fn set_limit(
        &self,
        db: Arc<RusqliteService>,
        limit_bytes: u64,
    ) -> Result<(), String> {
        // Update database setting
        let query = Query::with_params(
            "INSERT OR REPLACE INTO cache_settings (key, value, updated_at)
             VALUES (?, ?, CURRENT_TIMESTAMP)",
            vec![
                QueryParam::String("image_cache_limit_bytes".to_string()),
                QueryParam::String(limit_bytes.to_string()),
            ],
        );

        db.execute(query)
            .await
            .map_err(|e| format!("Failed to update setting: {}", e))?;

        // Update in-memory config
        if let Ok(mut config) = self.config.lock() {
            config.max_size_bytes = limit_bytes;
        }

        // If new limit is lower, evict to comply
        let current_size = self.get_cache_size(db.clone()).await;
        if limit_bytes > 0 && current_size > limit_bytes {
            let to_free = current_size - limit_bytes;
            self.evict_lru(db, to_free).await?;
        }

        Ok(())
    }

    /// Clear all cached thumbnails
    pub async fn clear_cache(&self, db: Arc<RusqliteService>) -> Result<(), String> {
        // Get all file paths
        let query = Query::new("SELECT file_path FROM thumbnails");

        let paths: Vec<String> = db
            .query_many(query, |row| row.get(0))
            .await
            .map_err(|e| e.to_string())?;

        // Delete files from disk
        for path in paths {
            let _ = std::fs::remove_file(&path);
        }

        // Clear database
        let delete_query = Query::new("DELETE FROM thumbnails");
        db.execute(delete_query)
            .await
            .map_err(|e| format!("Failed to clear database: {}", e))?;

        Ok(())
    }

    /// Delete cached thumbnail for a specific item
    pub async fn delete_item(&self, db: Arc<RusqliteService>, item_id: &str) -> Result<(), String> {
        // Get file paths for this item
        let query = Query::with_params(
            "SELECT file_path FROM thumbnails WHERE item_id = ?",
            vec![QueryParam::String(item_id.to_string())],
        );

        let paths: Vec<String> = db
            .query_many(query, |row| row.get(0))
            .await
            .map_err(|e| e.to_string())?;

        // Delete files
        for path in paths {
            let _ = std::fs::remove_file(&path);
        }

        // Delete from database
        let delete_query = Query::with_params(
            "DELETE FROM thumbnails WHERE item_id = ?",
            vec![QueryParam::String(item_id.to_string())],
        );
        db.execute(delete_query)
            .await
            .map_err(|e| format!("Failed to delete from database: {}", e))?;

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::storage::db_service::RusqliteService;
    use rusqlite::Connection;
    use std::sync::{Arc, Mutex};
    use tempfile::TempDir;

    fn setup_test_db() -> (Arc<RusqliteService>, TempDir) {
        let temp_dir = TempDir::new().unwrap();
        let conn = Connection::open_in_memory().unwrap();

        // Create tables
        conn.execute(
            "CREATE TABLE thumbnails (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                item_id TEXT NOT NULL,
                image_type TEXT NOT NULL,
                image_tag TEXT NOT NULL,
                file_path TEXT NOT NULL,
                width INTEGER,
                height INTEGER,
                file_size INTEGER DEFAULT 0,
                cached_at TEXT DEFAULT CURRENT_TIMESTAMP,
                last_accessed TEXT DEFAULT CURRENT_TIMESTAMP,
                UNIQUE(item_id, image_type, image_tag)
            )",
            [],
        )
        .unwrap();

        conn.execute(
            "CREATE TABLE cache_settings (
                key TEXT PRIMARY KEY,
                value TEXT NOT NULL,
                updated_at TEXT DEFAULT CURRENT_TIMESTAMP
            )",
            [],
        )
        .unwrap();

        let db_service = Arc::new(RusqliteService::new(Arc::new(Mutex::new(conn))));
        (db_service, temp_dir)
    }

    #[test]
    fn test_cache_creation() {
        let temp_dir = TempDir::new().unwrap();
        let cache = ThumbnailCache::new(temp_dir.path().to_path_buf(), CacheConfig::default());

        assert!(cache.cache_dir.exists());
        assert!(cache.is_enabled());
    }

    #[tokio::test]
    async fn test_save_and_get_thumbnail() {
        let (conn, temp_dir) = setup_test_db();
        let cache = ThumbnailCache::new(temp_dir.path().to_path_buf(), CacheConfig::default());

        // Save a thumbnail
        let data = b"fake image data";
        let path = cache
            .save_thumbnail(
                conn.clone(),
                "item1",
                "Primary",
                "tag1",
                data,
                Some(100),
                Some(100),
            )
            .await
            .unwrap();

        assert!(path.exists());

        // Get cached path
        let cached = cache
            .get_cached_path(conn.clone(), "item1", "Primary", "tag1")
            .await;
        assert!(cached.is_some());
        assert_eq!(cached.unwrap(), path);
    }

    #[tokio::test]
    async fn test_cache_miss() {
        let (conn, temp_dir) = setup_test_db();
        let cache = ThumbnailCache::new(temp_dir.path().to_path_buf(), CacheConfig::default());

        let cached = cache
            .get_cached_path(conn.clone(), "nonexistent", "Primary", "tag1")
            .await;
        assert!(cached.is_none());
    }

    #[tokio::test]
    async fn test_lru_eviction() {
        let (conn, temp_dir) = setup_test_db();
        let config = CacheConfig {
            max_size_bytes: 100, // Very small limit
            ..Default::default()
        };
        let cache = ThumbnailCache::new(temp_dir.path().to_path_buf(), config);

        // Add items that exceed limit
        let data = vec![0u8; 60];
        cache
            .save_thumbnail(conn.clone(), "item1", "Primary", "tag1", &data, None, None)
            .await
            .unwrap();

        // Second item should trigger eviction
        cache
            .save_thumbnail(conn.clone(), "item2", "Primary", "tag2", &data, None, None)
            .await
            .unwrap();

        // First item should be evicted
        let cached = cache
            .get_cached_path(conn.clone(), "item1", "Primary", "tag1")
            .await;
        assert!(cached.is_none());

        // Second item should exist
        let cached = cache
            .get_cached_path(conn.clone(), "item2", "Primary", "tag2")
            .await;
        assert!(cached.is_some());
    }

    #[tokio::test]
    async fn test_clear_cache() {
        let (conn, temp_dir) = setup_test_db();
        let cache = ThumbnailCache::new(temp_dir.path().to_path_buf(), CacheConfig::default());

        let data = b"fake image data";
        cache
            .save_thumbnail(conn.clone(), "item1", "Primary", "tag1", data, None, None)
            .await
            .unwrap();
        cache
            .save_thumbnail(conn.clone(), "item2", "Primary", "tag2", data, None, None)
            .await
            .unwrap();

        assert_eq!(cache.get_item_count(conn.clone()).await, 2);

        cache.clear_cache(conn.clone()).await.unwrap();

        assert_eq!(cache.get_item_count(conn.clone()).await, 0);
        assert_eq!(cache.get_cache_size(conn.clone()).await, 0);
    }

    #[tokio::test]
    async fn test_set_limit() {
        let (conn, temp_dir) = setup_test_db();
        let cache = ThumbnailCache::new(temp_dir.path().to_path_buf(), CacheConfig::default());

        // Save some thumbnails
        let data = vec![0u8; 50];
        cache
            .save_thumbnail(conn.clone(), "item1", "Primary", "tag1", &data, None, None)
            .await
            .unwrap();
        cache
            .save_thumbnail(conn.clone(), "item2", "Primary", "tag2", &data, None, None)
            .await
            .unwrap();

        // Set a limit smaller than current size
        cache.set_limit(conn.clone(), 60).await.unwrap();

        // Some items should be evicted
        let size = cache.get_cache_size(conn.clone()).await;
        assert!(size <= 60);
    }
}
