use std::sync::Arc;

use psysonic_analysis::analysis_runtime::{enqueue_track_analysis, AnalysisBackfillPriority};
use psysonic_audio as audio;
use psysonic_core::user_agent::subsonic_wire_user_agent;
use tauri::Manager;

use crate::file_transfer::{apply_server_http_get, stream_to_file};
use super::downloads::{resolve_hot_cache_root, HotCacheDownloadResult};
use super::offline::enqueue_analysis_seed_from_file;

#[tauri::command]
#[specta::specta]
pub async fn download_track_hot_cache(
    track_id: String,
    server_id: String,
    url: String,
    suffix: String,
    custom_dir: Option<String>,
    app: tauri::AppHandle,
) -> Result<HotCacheDownloadResult, String> {
    let root = resolve_hot_cache_root(custom_dir, &app)?;
    let cache_dir = root.join(&server_id);

    tokio::fs::create_dir_all(&cache_dir)
        .await
        .map_err(|e| e.to_string())?;

    let file_path = cache_dir.join(format!("{}.{}", track_id, suffix));
    let path_str = file_path.to_string_lossy().to_string();

    if file_path.exists() {
        let size = tokio::fs::metadata(&file_path)
            .await
            .map(|m| m.len())
            .unwrap_or(0);
        crate::app_deprintln!(
            "[hot-cache] download disk_hit track_id={} server_id={} bytes={}",
            track_id,
            server_id,
            size
        );
        // Disk hit: still seed analysis, but do not block the command (full-file read); the
        // prefetch worker runs invokes sequentially.
        let app_seed = app.clone();
        let tid = track_id.clone();
        let sid = server_id.clone();
        let fp = file_path.clone();
        tokio::spawn(async move {
            enqueue_analysis_seed_from_file(
                &app_seed,
                &sid,
                &tid,
                &fp,
                Some(AnalysisBackfillPriority::Middle),
            )
            .await;
        });
        return Ok(HotCacheDownloadResult {
            path: path_str,
            size,
        });
    }

    crate::app_deprintln!(
        "[hot-cache] download http_start track_id={} server_id={}",
        track_id,
        server_id
    );

    let client = reqwest::Client::builder()
        .user_agent(subsonic_wire_user_agent())
        .timeout(std::time::Duration::from_secs(120))
        .build()
        .map_err(|e| e.to_string())?;

    let http_registry = app
        .try_state::<Arc<psysonic_core::server_http::ServerHttpRegistry>>()
        .map(|s| Arc::clone(&*s));

    let response = apply_server_http_get(&client, http_registry.as_deref(), Some(&server_id), &url)
        .send()
        .await
        .map_err(|e| e.to_string())?;
    if !response.status().is_success() {
        return Err(format!("HTTP {}", response.status().as_u16()));
    }

    // Stream directly to a .part file; rename on success to avoid partial files.
    let part_path = file_path.with_extension(format!("{suffix}.part"));
    if let Err(e) = stream_to_file(response, &part_path, None).await {
        let _ = tokio::fs::remove_file(&part_path).await;
        return Err(e);
    }
    tokio::fs::rename(&part_path, &file_path)
        .await
        .map_err(|e| e.to_string())?;

    let app_seed = app.clone();
    let tid = track_id.clone();
    let sid = server_id.clone();
    let fp = file_path.clone();
    tokio::spawn(async move {
        enqueue_analysis_seed_from_file(
            &app_seed,
            &sid,
            &tid,
            &fp,
            Some(AnalysisBackfillPriority::Middle),
        )
        .await;
    });

    let size = tokio::fs::metadata(&file_path)
        .await
        .map(|m| m.len())
        .unwrap_or(0);
    crate::app_deprintln!(
        "[hot-cache] download http_done track_id={} server_id={} bytes={}",
        track_id,
        server_id,
        size
    );
    Ok(HotCacheDownloadResult {
        path: path_str,
        size,
    })
}

/// Promotes bytes captured by the manual streaming path into hot cache on disk.
/// Returns `Ok(None)` when no completed stream cache is available for this URL.
#[tauri::command]
#[specta::specta]
pub async fn promote_stream_cache_to_hot_cache(
    track_id: String,
    server_id: String,
    url: String,
    suffix: String,
    custom_dir: Option<String>,
    app: tauri::AppHandle,
    state: tauri::State<'_, audio::AudioEngine>,
) -> Result<Option<HotCacheDownloadResult>, String> {
    let root = resolve_hot_cache_root(custom_dir, &app)?;
    let cache_dir = root.join(&server_id);
    tokio::fs::create_dir_all(&cache_dir)
        .await
        .map_err(|e| e.to_string())?;

    let file_path = cache_dir.join(format!("{}.{}", track_id, suffix));
    let path_str = file_path.to_string_lossy().to_string();

    if file_path.exists() {
        let size = tokio::fs::metadata(&file_path)
            .await
            .map(|m| m.len())
            .unwrap_or(0);
        crate::app_deprintln!(
            "[hot-cache] promote disk_hit track_id={} server_id={} bytes={}",
            track_id,
            server_id,
            size
        );
        let app_seed = app.clone();
        let tid = track_id.clone();
        let sid = server_id.clone();
        let fp = file_path.clone();
        tokio::spawn(async move {
            enqueue_analysis_seed_from_file(&app_seed, &sid, &tid, &fp, None).await;
        });
        return Ok(Some(HotCacheDownloadResult { path: path_str, size }));
    }

    if let Some(bytes) = audio::take_stream_completed_for_url(&state, &url) {
        let part_path = file_path.with_extension(format!("{suffix}.part"));
        if let Err(e) = tokio::fs::write(&part_path, &bytes).await {
            let _ = tokio::fs::remove_file(&part_path).await;
            return Err(e.to_string());
        }
        tokio::fs::rename(&part_path, &file_path)
            .await
            .map_err(|e| e.to_string())?;

        let priority = psysonic_analysis::analysis_runtime::analysis_backfill_resolve_priority(
            &app,
            &server_id,
            &track_id,
            None,
        );
        let format_hint = Some(suffix.to_ascii_lowercase());
        let _ = enqueue_track_analysis(
            &app,
            &server_id,
            &track_id,
            &bytes,
            format_hint.as_deref(),
            priority,
        )
        .await;

        let size = tokio::fs::metadata(&file_path)
            .await
            .map(|m| m.len())
            .unwrap_or(0);
        crate::app_deprintln!(
            "[hot-cache] promote from_stream_ram track_id={} server_id={} bytes={}",
            track_id,
            server_id,
            size
        );
        return Ok(Some(HotCacheDownloadResult { path: path_str, size }));
    }

    if let Some(spill_path) = audio::take_stream_completed_spill_for_url(&state, &url) {
        if let Err(e) = tokio::fs::rename(&spill_path, &file_path).await {
            if let Err(copy_err) = tokio::fs::copy(&spill_path, &file_path).await {
                let _ = tokio::fs::remove_file(&spill_path).await;
                return Err(format!("promote spill rename: {e}; copy: {copy_err}"));
            }
            let _ = tokio::fs::remove_file(&spill_path).await;
        }
        let app_seed = app.clone();
        let tid = track_id.clone();
        let sid = server_id.clone();
        let fp = file_path.clone();
        tokio::spawn(async move {
            enqueue_analysis_seed_from_file(&app_seed, &sid, &tid, &fp, None).await;
        });
        let size = tokio::fs::metadata(&file_path)
            .await
            .map(|m| m.len())
            .unwrap_or(0);
        crate::app_deprintln!(
            "[hot-cache] promote from_stream_spill track_id={} server_id={} bytes={}",
            track_id,
            server_id,
            size
        );
        return Ok(Some(HotCacheDownloadResult { path: path_str, size }));
    }

    crate::app_deprintln!(
        "[hot-cache] promote skip track_id={} reason=no_completed_stream_for_url",
        track_id
    );
    Ok(None)
}

#[tauri::command]
#[specta::specta]
pub async fn get_hot_cache_size(custom_dir: Option<String>, app: tauri::AppHandle) -> u64 {
    resolve_hot_cache_root(custom_dir, &app)
        .map(|root| super::fs_utils::dir_size_recursive(&root))
        .unwrap_or(0)
}

#[tauri::command]
#[specta::specta]
pub async fn delete_hot_cache_track(
    local_path: String,
    custom_dir: Option<String>,
    app: tauri::AppHandle,
) -> Result<(), String> {
    let file_path = std::path::PathBuf::from(&local_path);
    let existed = file_path.exists();
    if existed {
        tokio::fs::remove_file(&file_path)
            .await
            .map_err(|e| e.to_string())?;
    }
    crate::app_deprintln!(
        "[hot-cache] delete file existed={} path_suffix={}",
        existed,
        file_path
            .file_name()
            .and_then(|s| s.to_str())
            .unwrap_or("?")
    );

    let boundary = resolve_hot_cache_root(custom_dir, &app)?;
    if let Some(parent) = file_path.parent() {
        super::fs_utils::prune_empty_dirs_up_to(parent, &boundary);
    }

    Ok(())
}

/// Removes the entire hot cache root (`psysonic-hot-cache` for the active location).
#[tauri::command]
#[specta::specta]
pub async fn purge_hot_cache(custom_dir: Option<String>, app: tauri::AppHandle) -> Result<(), String> {
    let root = resolve_hot_cache_root(custom_dir, &app)?;
    if !root.exists() {
        return Ok(());
    }
    tokio::fs::remove_dir_all(&root)
        .await
        .map_err(|e| e.to_string())?;
    crate::app_deprintln!(
        "[hot-cache] purge root={} status=ok",
        root.to_string_lossy()
    );
    Ok(())
}
