use std::collections::{HashMap, HashSet};
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
use std::path::Path;
#[cfg(windows)]
use std::time::UNIX_EPOCH;

use anyhow::{Context, Result};
use log::{debug, info, warn};
use tokio::fs;
use tokio::io::AsyncReadExt;
use tokio::time::{interval, Duration};

use crate::constants::*;
use crate::download_core::check_metadata_size;

/// Helper function to get the file's last access time as a Unix timestamp.
/// On Unix, it uses `metadata.atime()`. On Windows, it uses `metadata.accessed()`.
fn get_atime(metadata: &std::fs::Metadata) -> std::io::Result<i64> {
    #[cfg(unix)]
    {
        Ok(metadata.atime())
    }
    #[cfg(windows)]
    {
        let accessed = metadata.accessed()?;
        let duration = accessed.duration_since(UNIX_EPOCH).unwrap_or_default();
        Ok(duration.as_secs() as i64)
    }
}

/// Cleans up cache files and calculates total cache utilization.
/// Returns a tuple: (available_volume_bytes, manifest_files_in_cache_bytes).
/// Files are cleaned up if they:
/// - Have not been accessed within the threshold AND are not in `current_hashes`
/// - Are not in `current_hashes` and cache size exceeds TRUSS_TRANSFER_B10FS_MAX_STALE_CACHE_SIZE_GB
pub async fn cleanup_b10cache_and_get_space_stats(
    current_hashes: &HashSet<String>,
    manifest_hash_to_size_map: &HashMap<String, u64>,
) -> Result<(u64, u64)> {
    // Returns (available_volume_bytes, manifest_files_in_cache_bytes)
    let cleanup_threshold_hours = *TRUSS_TRANSFER_B10FS_CLEANUP_HOURS;
    let cache_dir_path = Path::new(&*CACHE_DIR);
    let now = chrono::Utc::now().timestamp();
    let threshold_seconds = cleanup_threshold_hours * 3600;
    let max_stale_cache_size_gb = *crate::constants::TRUSS_TRANSFER_B10FS_MAX_STALE_CACHE_SIZE_GB;

    let mut dir = fs::read_dir(cache_dir_path)
        .await
        .with_context(|| format!("Failed to read cache directory: {cache_dir_path:?}"))?;

    let mut total_bytes_managed_in_cache = 0u64; // All files kept in cache (manifest or not old enough)
    let mut total_files_managed_in_cache = 0usize;
    let mut manifest_files_in_cache_bytes = 0u64; // Size of files from current manifest, correctly cached

    // For size-based cleanup: list of (access_time, file_path, size) for non-current files
    let mut stale_files: Vec<(i64, std::path::PathBuf, u64)> = Vec::new();

    info!(
        "Analyzing b10cache at {} with a cleanup threshold of {} hours ({} days)",
        &*CACHE_DIR,
        cleanup_threshold_hours,
        cleanup_threshold_hours as f64 / 24.0
    );

    if let Some(max_size_gb) = max_stale_cache_size_gb {
        info!("Max stale cache size limit: {} GB", max_size_gb);
    }

    while let Some(entry) = dir.next_entry().await? {
        let path = entry.path();
        // Only process files
        if path.is_file() {
            let metadata = fs::metadata(&path).await?;
            let actual_file_size = metadata.len();
            let atime = get_atime(&metadata)?;

            if let Some(file_name_hash) = path.file_name().and_then(|n| n.to_str()) {
                if let Some(expected_size) = manifest_hash_to_size_map.get(file_name_hash) {
                    // File is part of the current manifest
                    total_bytes_managed_in_cache += actual_file_size;
                    total_files_managed_in_cache += 1;
                    if actual_file_size == *expected_size {
                        manifest_files_in_cache_bytes += actual_file_size;
                    } else {
                        warn!(
                            "Cached file {} (hash: {}) has incorrect size. Expected {}, got {}. Not counting towards current manifest cache size.",
                            path.display(), file_name_hash, expected_size, actual_file_size
                        );
                        // This file, though in manifest, is corrupted in cache.
                        // It won't be deleted by age here, but download logic might replace it if it tries to use it.
                    }
                } else if !current_hashes.contains(file_name_hash) {
                    // File is NOT in current_hashes - candidate for deletion
                    if now - atime > threshold_seconds as i64 {
                        // File is older than threshold - delete immediately
                        info!(
                            "Deleting old cached file {} ({} bytes): not in current manifest and not accessed for over {} hours",
                            file_name_hash, actual_file_size, cleanup_threshold_hours
                        );
                        if let Err(e) = fs::remove_file(&path).await {
                            warn!("Failed to delete cached file {:?}: {}", path, e);
                        }
                    } else {
                        // File is not old enough for time-based cleanup, but might be cleaned up for size limit
                        stale_files.push((atime, path.clone(), actual_file_size));
                        total_bytes_managed_in_cache += actual_file_size;
                        total_files_managed_in_cache += 1;
                        debug!(
                            "Keeping non-manifest file {} ({} bytes): last accessed {} minutes ago.",
                            file_name_hash,
                            actual_file_size,
                            (now - atime) / 60
                        );
                    }
                } else {
                    // File is in current_hashes but not in manifest_hash_to_size_map
                    // (should not happen if maps are consistent)
                    total_bytes_managed_in_cache += actual_file_size;
                    total_files_managed_in_cache += 1;
                }
            }
        }
    }

    // Size-based cleanup if max_stale_cache_size_gb is set
    if let Some(max_size_gb) = max_stale_cache_size_gb {
        let max_size_bytes = max_size_gb * 1024 * 1024 * 1024; // Convert GB to bytes
        let mut current_stale_size: u64 = stale_files.iter().map(|(_, _, size)| size).sum();

        if current_stale_size > max_size_bytes {
            info!(
                "Stale cache size ({:.2} GB) exceeds limit ({} GB). Starting size-based cleanup.",
                current_stale_size as f64 / (1024.0 * 1024.0 * 1024.0),
                max_size_gb
            );

            // Sort by access time (oldest first)
            stale_files.sort_by_key(|(atime, _, _)| *atime);

            // Delete files starting from oldest access time until under the limit
            for (atime, file_path, file_size) in stale_files.iter() {
                if current_stale_size <= max_size_bytes {
                    break;
                }

                if let Some(file_name) = file_path.file_name().and_then(|n| n.to_str()) {
                    info!(
                        "Deleting stale cached file {} ({} bytes) for size limit: last accessed {} hours ago",
                        file_name,
                        file_size,
                        (now - atime) / 3600
                    );

                    if let Err(e) = fs::remove_file(file_path).await {
                        warn!("Failed to delete cached file {:?}: {}", file_path, e);
                    } else {
                        current_stale_size -= file_size;
                        total_bytes_managed_in_cache -= file_size;
                        total_files_managed_in_cache -= 1;
                    }
                }
            }

            info!(
                "Size-based cleanup complete. Stale cache size now: {:.2} GB",
                current_stale_size as f64 / (1024.0 * 1024.0 * 1024.0)
            );
        }
    }

    info!(
        "Cache analysis complete: {} files managed in cache totaling {:.2} GB. Correctly cached files from current manifest: {:.2} GB.",
        total_files_managed_in_cache,
        total_bytes_managed_in_cache as f64 / (1024.0 * 1024.0 * 1024.0),
        manifest_files_in_cache_bytes as f64 / (1024.0 * 1024.0 * 1024.0)
    );

    // Get available disk space for CACHE_DIR's volume
    let stats = fs2::statvfs(cache_dir_path)
        .with_context(|| format!("Failed to get volume stats for {cache_dir_path:?}"))?;
    let available_bytes = stats.available_space(); // f_bavail * f_frsize (available to non-root)

    info!(
        "Total available space on volume for {}: {:.2} GB ({} bytes)",
        &*CACHE_DIR,
        available_bytes as f64 / (1024.0 * 1024.0 * 1024.0),
        available_bytes
    );

    Ok((available_bytes, manifest_files_in_cache_bytes))
}

/// Handling new b10cache:
/// 1. Copy the local file (download_path) to a temporary cache file with a ".incomplete" suffix.
/// 2. Verify that the copied file's size matches the expected size.
/// - (1.), (2.), (3.) with error handling for concurrency.
/// 3. If the sizes match:
///    - Atomically rename the temporary file to the final cache path.
///    - Delete the local file (deduplicate).
///    - Create a symlink from the cache file to the original local path.
/// 4. If the sizes do not match:
///    - Delete the .incomplete file and keep the local file.
pub async fn handle_write_b10cache(download_path: &Path, cache_path: &Path) -> Result<()> {
    info!(
        "b10cache enabled: copying file from {:?} to cache and creating symlink back to {:?}",
        download_path, cache_path
    );
    let size = fs::metadata(download_path).await?.len();
    // check if cache_path exists and has the same size, concurrency with other replica.
    if cache_path.exists() {
        let cache_metadata = fs::metadata(cache_path).await?;
        if cache_metadata.len() as u64 == size {
            info!(
                "Cache file {:?} already exists with the same size. Skipping copy to b10fs.",
                cache_path
            );
            update_atime_by_reading(cache_path)
                .await
                .context("Failed to update atime for cache file")?;
            return Ok(());
        }
    }

    // Build the temporary incomplete file path.
    let mut should_copy = true;
    let incomplete_cache_path = cache_path.with_extension("incomplete");
    if incomplete_cache_path.exists() {
        // Check if the incomplete file has the expected size.
        if check_metadata_size(&incomplete_cache_path, size).await {
            should_copy = false;
        } else {
            warn!(
                "Incomplete cache file {:?} already exists. Deleting it.",
                incomplete_cache_path
            );
            match fs::remove_file(&incomplete_cache_path).await {
                Ok(_) => info!("Deleted incomplete cache file."),
                Err(e) => warn!("Failed to delete incomplete cache file: {}", e),
            }
        }
    }

    if should_copy {
        // Copy the local file to the incomplete cache file with progress monitoring
        info!(
            "Copying local file {:?} to cache file {:?}",
            download_path, incomplete_cache_path
        );

        // Setup progress monitoring with atomics
        let monitor_path = incomplete_cache_path.to_path_buf();
        let monitor_handle = tokio::spawn({
            async move {
                let mut ticker = interval(Duration::from_secs(30));
                let mut last_size = 0;
                loop {
                    ticker.tick().await;
                    let current_size = match fs::metadata(&monitor_path).await {
                        Ok(metadata) => metadata.len(),
                        Err(e) => {
                            warn!(
                                "Issue monitoring the copy: read metadata for {:?} failed: {}",
                                monitor_path, e
                            );
                            continue;
                        }
                    };

                    if current_size == last_size {
                        warn!(
                            "No progress for {:?}: {} bytes written so far.",
                            monitor_path, current_size
                        );
                    } else {
                        let progress_mb = current_size as f64 / (1024.0 * 1024.0);
                        info!(
                            "Copy progress for {:?}: {:.2} MB written so far.",
                            monitor_path, progress_mb
                        );
                    }
                    last_size = current_size;
                }
            }
        });
        let copy_result = match fs::rename(download_path, &incomplete_cache_path).await {
            Ok(_) => {
                info!("Successfully moved file to cache.");
                Ok(0) // Using 0 as a placeholder for success, similar to how copy returns bytes.
            }
            Err(_) => {
                //  Failed to move file, falling back to copy (likely cross-device)
                let copy_result = fs::copy(download_path, &incomplete_cache_path).await;
                info!("Successfully copied local file to incomplete cache.");
                copy_result
            }
        };

        // Stop monitoring regardless of copy result
        monitor_handle.abort();

        match copy_result {
            Ok(_) => debug!("Successfully copied to incomplete cache file."),
            Err(e) => {
                warn!(
                    "Failed to copy local file to incomplete cache file: {}. Maybe b10cache has no storage or permission issues.",
                    e
                );
                return Ok(());
            }
        }
    }

    let incomplete_metadata = match fs::metadata(&incomplete_cache_path).await {
        Ok(metadata) => metadata,
        Err(e) => {
            warn!(
                "Failed to get metadata for incomplete cache file: {}. Maybe b10cache has no storage or concurrency issue.",
                e
            );
            return Ok(());
        }
    };

    if incomplete_metadata.len() as u64 != size {
        warn!(
            "Size mismatch in incomplete cache file: expected {} bytes, got {} bytes. Keeping local file and cleaning up b10cache - perhaps related to high concurrency.",
            size,
            incomplete_metadata.len()
        );
        match fs::remove_file(&incomplete_cache_path).await {
            Ok(_) => info!("Deleted incomplete cache file."),
            Err(e) => warn!("Failed to delete incomplete cache file: {}", e),
        };
        return Ok(());
    }

    // Atomically rename the incomplete file to the final cache file.
    debug!(
        "Atomic rename: renaming incomplete cache file {:?} to final cache file {:?}",
        incomplete_cache_path, cache_path
    );
    // Try atomic rename first, fall back to copy+delete if rename fails
    match fs::rename(&incomplete_cache_path, cache_path).await {
        Ok(()) => {
            // Atomic rename succeeded
        }
        Err(rename_err) => {
            // Rename failed, try copy + delete as fallback
            if let Err(copy_err) = fs::copy(&incomplete_cache_path, cache_path).await {
                return Err(anyhow::Error::new(copy_err).context(format!(
                    "Failed to rename {incomplete_cache_path:?} to {cache_path:?} (rename error: {rename_err}) and copy fallback also failed"
                )));
            }

            // Copy succeeded, now remove the incomplete file
            if let Err(remove_err) = fs::remove_file(&incomplete_cache_path).await {
                // Log warning but don't fail - the cache file was successfully created
                warn!(
                    "Failed to remove incomplete cache file {incomplete_cache_path:?} after copy: {remove_err}"
                );
            }
        }
    }

    // Delete the local file as its copy is now in the cache.
    info!("Deleting local file at {:?}", download_path);
    fs::remove_file(download_path)
        .await
        .with_context(|| format!("Failed to delete local file {download_path:?}"))?;

    // Create a symlink from the cache file to the original download location.
    info!(
        "Creating symlink from cache file {:?} to local file path {:?}",
        cache_path, download_path
    );
    create_symlink_or_skip(cache_path, download_path, size)
        .await
        .with_context(|| {
            format!(
                "Failed to create symlink from cache file {cache_path:?} to local file path {download_path:?}"
            )
        })?;

    info!(
        "Successfully handled b10cache for file: {:?}",
        download_path
    );
    Ok(())
}

/// Verifies that the file exists and updates its atime by reading it
pub async fn update_atime_by_reading(path: &Path) -> Result<()> {
    // Open the file in read-only mode.
    let mut file = fs::File::open(path)
        .await
        .with_context(|| format!("Failed to open file {path:?} for updating atime"))?;
    let mut buffer = [0u8; 1];
    let _ = file.read(&mut buffer).await?;
    Ok(())
}

/// Create a symlink from `src` to `dst` if `dst` does not exist.
/// Returns Ok(()) if `dst` already exists.
pub async fn create_symlink_or_skip(src: &Path, dst: &Path, size: u64) -> Result<()> {
    let src_metadata = fs::metadata(src).await?;
    if src_metadata.len() as u64 != size {
        warn!(
            "File size mismatch before symlink to {:?}. Expected {}, got {}",
            dst,
            size,
            src_metadata.len()
        );
    }
    if dst.exists() {
        return Ok(());
    }
    if let Some(parent) = dst.parent() {
        std::fs::create_dir_all(parent)
            .context("Failed to create parent directory for symlink destination")?;
    }
    #[cfg(unix)]
    {
        std::os::unix::fs::symlink(src, dst).context("Failed to create Unix symlink")?;
    }
    #[cfg(windows)]
    {
        std::os::windows::fs::symlink_file(src, dst).context("Failed to create Windows symlink")?;
    }
    update_atime_by_reading(src)
        .await
        .context("Failed to update atime after symlink")?;
    Ok(())
}
