//! Thumbnail cache and image-URL commands.
//!
//! TRACES: UR-007 | JA-028 | DR-016

use serde::Deserialize;
use std::sync::{Arc, OnceLock};
use tauri::State;
use tokio::sync::Semaphore;

use super::{DatabaseWrapper, ThumbnailCacheWrapper};
use crate::commands::repository::RepositoryManagerWrapper;
use crate::repository::types::{ImageOptions, ImageType};
use crate::repository::MediaRepository;
use crate::thumbnail::{ThumbnailCacheStats, ThumbnailWorker};

/// Get cached thumbnail path, returns None if not cached
/// Also updates last_accessed timestamp for LRU tracking
#[tauri::command]
#[specta::specta]
pub async fn thumbnail_get_cached(
    db: State<'_, DatabaseWrapper>,
    thumbnail_cache: State<'_, ThumbnailCacheWrapper>,
    item_id: String,
    image_type: String,
    tag: String,
) -> Result<Option<String>, String> {
    let db_service = {
        let database = db.0.lock().map_err(|e| e.to_string())?;
        Arc::new(database.service())
    };

    let result = thumbnail_cache
        .0
        .get_cached_path(db_service, &item_id, &image_type, &tag)
        .await
        .map(|p| p.to_string_lossy().to_string());

    Ok(result)
}

/// Download and save a thumbnail to cache
/// Returns the local file path on success
#[tauri::command]
#[specta::specta]
pub async fn thumbnail_save(
    db: State<'_, DatabaseWrapper>,
    thumbnail_cache: State<'_, ThumbnailCacheWrapper>,
    item_id: String,
    image_type: String,
    tag: String,
    url: String,
) -> Result<String, String> {
    // Download the image
    let worker = ThumbnailWorker::new();
    let data = worker
        .download_with_retry(&url, 2)
        .await
        .map_err(|e| e.to_string())?;

    // Save to cache
    let db_service = {
        let database = db.0.lock().map_err(|e| e.to_string())?;
        Arc::new(database.service())
    };

    let path = thumbnail_cache
        .0
        .save_thumbnail(db_service, &item_id, &image_type, &tag, &data, None, None)
        .await?;
    Ok(path.to_string_lossy().to_string())
}

/// Get thumbnail cache statistics
#[tauri::command]
#[specta::specta]
pub async fn thumbnail_get_stats(
    db: State<'_, DatabaseWrapper>,
    thumbnail_cache: State<'_, ThumbnailCacheWrapper>,
) -> Result<ThumbnailCacheStats, String> {
    let db_service = {
        let database = db.0.lock().map_err(|e| e.to_string())?;
        Arc::new(database.service())
    };

    let total_size_bytes = thumbnail_cache.0.get_cache_size(db_service.clone()).await;
    let item_count = thumbnail_cache.0.get_item_count(db_service.clone()).await;
    let limit_bytes = thumbnail_cache.0.get_limit(db_service).await;

    Ok(ThumbnailCacheStats {
        total_size_bytes,
        item_count,
        limit_bytes,
    })
}

/// Set thumbnail cache storage limit in bytes
#[tauri::command]
#[specta::specta]
pub async fn thumbnail_set_limit(
    db: State<'_, DatabaseWrapper>,
    thumbnail_cache: State<'_, ThumbnailCacheWrapper>,
    limit_bytes: u64,
) -> Result<(), String> {
    let db_service = {
        let database = db.0.lock().map_err(|e| e.to_string())?;
        Arc::new(database.service())
    };

    thumbnail_cache.0.set_limit(db_service, limit_bytes).await
}

/// Clear all cached thumbnails
#[tauri::command]
#[specta::specta]
pub async fn thumbnail_clear_cache(
    db: State<'_, DatabaseWrapper>,
    thumbnail_cache: State<'_, ThumbnailCacheWrapper>,
) -> Result<(), String> {
    let db_service = {
        let database = db.0.lock().map_err(|e| e.to_string())?;
        Arc::new(database.service())
    };

    thumbnail_cache.0.clear_cache(db_service).await
}

/// Delete cached thumbnails for a specific item
#[tauri::command]
#[specta::specta]
pub async fn thumbnail_delete_item(
    db: State<'_, DatabaseWrapper>,
    thumbnail_cache: State<'_, ThumbnailCacheWrapper>,
    item_id: String,
) -> Result<(), String> {
    let db_service = {
        let database = db.0.lock().map_err(|e| e.to_string())?;
        Arc::new(database.service())
    };

    thumbnail_cache.0.delete_item(db_service, &item_id).await
}

fn mime_from_ext(ext: Option<&str>) -> &'static str {
    match ext {
        Some("jpg") | Some("jpeg") => "image/jpeg",
        Some("png") => "image/png",
        Some("gif") => "image/gif",
        Some("webp") => "image/webp",
        _ => "image/jpeg",
    }
}

/// Limit concurrent image downloads to avoid saturating the connection pool.
/// Without this, rendering a page with hundreds of album cards fires hundreds of
/// concurrent HTTP requests, starving API calls and causing timeouts.
static IMAGE_DOWNLOAD_SEMAPHORE: OnceLock<Semaphore> = OnceLock::new();

fn image_semaphore() -> &'static Semaphore {
    IMAGE_DOWNLOAD_SEMAPHORE.get_or_init(|| Semaphore::new(6))
}

/// Request to get an image URL (with caching)
#[derive(specta::Type, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetImageRequest {
    pub item_id: String,
    pub image_type: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub max_width: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub max_height: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tag: Option<String>,
}

/// Get image as base64 data URL, caching if not already cached
/// This extends the thumbnail system to serve all images through Rust with automatic caching
#[tauri::command]
#[specta::specta]
pub async fn image_get_url(
    repository_manager: State<'_, RepositoryManagerWrapper>,
    thumbnail_cache: State<'_, ThumbnailCacheWrapper>,
    db: State<'_, DatabaseWrapper>,
    repository_handle: String,
    request: GetImageRequest,
) -> Result<String, String> {
    use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
    use std::fs;

    let tag = request.tag.as_deref().unwrap_or("default");

    // Get database service
    let db_service = {
        let database = db.0.lock().map_err(|e| e.to_string())?;
        Arc::new(database.service())
    };

    // Check cache first
    if let Some(cached_path) = thumbnail_cache
        .0
        .get_cached_path(
            db_service.clone(),
            &request.item_id,
            &request.image_type,
            tag,
        )
        .await
    {
        let image_data =
            fs::read(&cached_path).map_err(|e| format!("Failed to read cached image: {}", e))?;
        let base64_data = BASE64.encode(&image_data);
        let mime_type = mime_from_ext(cached_path.extension().and_then(|s| s.to_str()));
        return Ok(format!("data:{};base64,{}", mime_type, base64_data));
    }

    // Not cached — fetch from server and cache.
    // Acquire semaphore to limit concurrent downloads (prevents connection pool starvation).
    let _permit = image_semaphore()
        .acquire()
        .await
        .map_err(|_| "Image download semaphore closed".to_string())?;

    let repository = repository_manager
        .0
        .get(&repository_handle)
        .ok_or_else(|| "Repository not found - user may need to log in".to_string())?;

    let image_type_enum = match request.image_type.as_str() {
        "Primary" => ImageType::Primary,
        "Backdrop" => ImageType::Backdrop,
        "Banner" => ImageType::Banner,
        "Thumb" => ImageType::Thumb,
        "Logo" => ImageType::Logo,
        _ => ImageType::Primary,
    };

    let options = ImageOptions {
        max_width: request.max_width,
        max_height: request.max_height,
        quality: Some(90),
        tag: request.tag.clone(),
    };

    let server_url = repository.get_image_url(&request.item_id, image_type_enum, Some(options));
    let image_data = repository
        .download_bytes(&server_url)
        .await
        .map_err(|e| format!("Failed to download image: {}", e))?;

    let cached_path = thumbnail_cache
        .0
        .save_thumbnail(
            db_service,
            &request.item_id,
            &request.image_type,
            tag,
            &image_data,
            request.max_width.map(|w| w as i32),
            request.max_height.map(|h| h as i32),
        )
        .await?;

    let base64_data = BASE64.encode(&image_data);
    let mime_type = mime_from_ext(cached_path.extension().and_then(|s| s.to_str()));
    Ok(format!("data:{};base64,{}", mime_type, base64_data))
}
