use std::error::Error;
use std::fmt;
use std::fs;
use std::io;
use std::path::Path;
use std::process::Command;

// Define a custom error type for download operations
#[derive(Debug)]
pub enum DownloadError {
    CreateDirectoryFailed(io::Error),
    CurlExecutionFailed(io::Error),
    DownloadFailed(String),
    FileMetadataError(io::Error),
    FileTooSmall(i64, i64),
    RemoveFileFailed(io::Error),
    ExtractionFailed(String),
    CommandExecutionFailed(io::Error),
    InvalidUrl(String),
    NotAFile(String),
    PlatformNotSupported(String),
    InstallationFailed(String),
}

// Implement Display for DownloadError
impl fmt::Display for DownloadError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            DownloadError::CreateDirectoryFailed(e) => {
                write!(f, "Error creating directories: {}", e)
            }
            DownloadError::CurlExecutionFailed(e) => write!(f, "Error executing curl: {}", e),
            DownloadError::DownloadFailed(url) => write!(f, "Error downloading url: {}", url),
            DownloadError::FileMetadataError(e) => write!(f, "Error getting file metadata: {}", e),
            DownloadError::FileTooSmall(size, min) => write!(
                f,
                "Error: Downloaded file is too small ({}KB < {}KB)",
                size, min
            ),
            DownloadError::RemoveFileFailed(e) => write!(f, "Error removing file: {}", e),
            DownloadError::ExtractionFailed(e) => write!(f, "Error extracting archive: {}", e),
            DownloadError::CommandExecutionFailed(e) => write!(f, "Error executing command: {}", e),
            DownloadError::InvalidUrl(url) => write!(f, "Invalid URL: {}", url),
            DownloadError::NotAFile(path) => write!(f, "Not a file: {}", path),
            DownloadError::PlatformNotSupported(msg) => write!(f, "{}", msg),
            DownloadError::InstallationFailed(msg) => write!(f, "{}", msg),
        }
    }
}

// Implement Error trait for DownloadError
impl Error for DownloadError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            DownloadError::CreateDirectoryFailed(e) => Some(e),
            DownloadError::CurlExecutionFailed(e) => Some(e),
            DownloadError::FileMetadataError(e) => Some(e),
            DownloadError::RemoveFileFailed(e) => Some(e),
            DownloadError::CommandExecutionFailed(e) => Some(e),
            _ => None,
        }
    }
}

/**
 * Download a file from URL to destination using the curl command.
 * This function is primarily intended for downloading archives that will be extracted
 * to a directory.
 *
 * # Arguments
 *
 * * `url` - The URL to download from
 * * `dest` - The destination directory where the file will be saved or extracted
 * * `min_size_kb` - Minimum required file size in KB (0 for no minimum)
 *
 * # Returns
 *
 * * `Ok(String)` - The path where the file was saved or extracted
 * * `Err(DownloadError)` - An error if the download failed
 *
 * # Examples
 *
 * ```no_run
 * use herolib_os::download;
 *
 * fn main() -> Result<(), Box<dyn std::error::Error>> {
 *     // Download a file with no minimum size requirement
 *     let path = download("https://example.com/file.txt", "/tmp/", 0)?;
 *
 *     // Download a file with minimum size requirement of 100KB
 *     let path = download("https://example.com/file.zip", "/tmp/", 100)?;
 *
 *     Ok(())
 * }
 * ```
 *
 * # Notes
 *
 * If the URL ends with .tar.gz, .tgz, .tar, or .zip, the file will be automatically
 * extracted to the destination directory.
 */
pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result<String, DownloadError> {
    // Create parent directories if they don't exist
    let dest_path = Path::new(dest);
    fs::create_dir_all(dest_path).map_err(DownloadError::CreateDirectoryFailed)?;

    // Extract filename from URL
    let filename = match url.split('/').last() {
        Some(name) => name,
        None => {
            return Err(DownloadError::InvalidUrl(
                "cannot extract filename".to_string(),
            ))
        }
    };

    // Create a full path for the downloaded file
    let file_path = format!("{}/{}", dest.trim_end_matches('/'), filename);

    // Create a temporary path for downloading
    let temp_path = format!("{}.download", file_path);

    // Use curl to download the file with progress bar
    println!("Downloading {} to {}", url, file_path);
    let output = Command::new("curl")
        .args(&[
            "--progress-bar",
            "--location",
            "--fail",
            "--output",
            &temp_path,
            url,
        ])
        .status()
        .map_err(DownloadError::CurlExecutionFailed)?;

    if !output.success() {
        return Err(DownloadError::DownloadFailed(url.to_string()));
    }

    // Show file size after download
    match fs::metadata(&temp_path) {
        Ok(metadata) => {
            let size_bytes = metadata.len();
            let size_kb = size_bytes / 1024;
            let size_mb = size_kb / 1024;
            if size_mb > 1 {
                println!(
                    "Download complete! File size: {:.2} MB",
                    size_bytes as f64 / (1024.0 * 1024.0)
                );
            } else {
                println!(
                    "Download complete! File size: {:.2} KB",
                    size_bytes as f64 / 1024.0
                );
            }
        }
        Err(_) => println!("Download complete!"),
    }

    // Check file size if minimum size is specified
    if min_size_kb > 0 {
        let metadata = fs::metadata(&temp_path).map_err(DownloadError::FileMetadataError)?;
        let size_kb = metadata.len() as i64 / 1024;
        if size_kb < min_size_kb {
            fs::remove_file(&temp_path).map_err(DownloadError::RemoveFileFailed)?;
            return Err(DownloadError::FileTooSmall(size_kb, min_size_kb));
        }
    }

    // Check if it's a compressed file that needs extraction
    let lower_url = url.to_lowercase();
    let is_archive = lower_url.ends_with(".tar.gz")
        || lower_url.ends_with(".tgz")
        || lower_url.ends_with(".tar")
        || lower_url.ends_with(".zip");

    if is_archive {
        // Extract the file using the appropriate command with progress indication
        println!("Extracting {} to {}", temp_path, dest);
        let output = if lower_url.ends_with(".zip") {
            Command::new("unzip")
                .args(&["-o", &temp_path, "-d", dest]) // Removed -q for verbosity
                .status()
        } else if lower_url.ends_with(".tar.gz") || lower_url.ends_with(".tgz") {
            Command::new("tar")
                .args(&["-xzvf", &temp_path, "-C", dest]) // Added v for verbosity
                .status()
        } else {
            Command::new("tar")
                .args(&["-xvf", &temp_path, "-C", dest]) // Added v for verbosity
                .status()
        };

        match output {
            Ok(status) => {
                if !status.success() {
                    return Err(DownloadError::ExtractionFailed(
                        "Error extracting archive".to_string(),
                    ));
                }
            }
            Err(e) => return Err(DownloadError::CommandExecutionFailed(e)),
        }

        // Show number of extracted files
        match fs::read_dir(dest) {
            Ok(entries) => {
                let count = entries.count();
                println!("Extraction complete! Extracted {} files/directories", count);
            }
            Err(_) => println!("Extraction complete!"),
        }

        // Remove the temporary file
        fs::remove_file(&temp_path).map_err(DownloadError::RemoveFileFailed)?;

        Ok(dest.to_string())
    } else {
        // Just rename the temporary file to the final destination
        fs::rename(&temp_path, &file_path).map_err(|e| DownloadError::CreateDirectoryFailed(e))?;

        Ok(file_path)
    }
}

/**
 * Download a file from URL to a specific file destination using the curl command.
 *
 * # Arguments
 *
 * * `url` - The URL to download from
 * * `dest` - The destination file path where the file will be saved
 * * `min_size_kb` - Minimum required file size in KB (0 for no minimum)
 *
 * # Returns
 *
 * * `Ok(String)` - The path where the file was saved
 * * `Err(DownloadError)` - An error if the download failed
 *
 * # Examples
 *
 * ```no_run
 * use herolib_os::download_file;
 *
 * fn main() -> Result<(), Box<dyn std::error::Error>> {
 *     // Download a file with no minimum size requirement
 *     let path = download_file("https://example.com/file.txt", "/tmp/file.txt", 0)?;
 *
 *     // Download a file with minimum size requirement of 100KB
 *     let path = download_file("https://example.com/file.zip", "/tmp/file.zip", 100)?;
 *
 *     Ok(())
 * }
 * ```
 */
pub fn download_file(url: &str, dest: &str, min_size_kb: i64) -> Result<String, DownloadError> {
    // Create parent directories if they don't exist
    let dest_path = Path::new(dest);
    if let Some(parent) = dest_path.parent() {
        fs::create_dir_all(parent).map_err(DownloadError::CreateDirectoryFailed)?;
    }

    // Create a temporary path for downloading
    let temp_path = format!("{}.download", dest);

    // Use curl to download the file with progress bar
    println!("Downloading {} to {}", url, dest);
    let output = Command::new("curl")
        .args(&[
            "--progress-bar",
            "--location",
            "--fail",
            "--output",
            &temp_path,
            url,
        ])
        .status()
        .map_err(DownloadError::CurlExecutionFailed)?;

    if !output.success() {
        return Err(DownloadError::DownloadFailed(url.to_string()));
    }

    // Show file size after download
    match fs::metadata(&temp_path) {
        Ok(metadata) => {
            let size_bytes = metadata.len();
            let size_kb = size_bytes / 1024;
            let size_mb = size_kb / 1024;
            if size_mb > 1 {
                println!(
                    "Download complete! File size: {:.2} MB",
                    size_bytes as f64 / (1024.0 * 1024.0)
                );
            } else {
                println!(
                    "Download complete! File size: {:.2} KB",
                    size_bytes as f64 / 1024.0
                );
            }
        }
        Err(_) => println!("Download complete!"),
    }

    // Check file size if minimum size is specified
    if min_size_kb > 0 {
        let metadata = fs::metadata(&temp_path).map_err(DownloadError::FileMetadataError)?;
        let size_kb = metadata.len() as i64 / 1024;
        if size_kb < min_size_kb {
            fs::remove_file(&temp_path).map_err(DownloadError::RemoveFileFailed)?;
            return Err(DownloadError::FileTooSmall(size_kb, min_size_kb));
        }
    }

    // Rename the temporary file to the final destination
    fs::rename(&temp_path, dest).map_err(|e| DownloadError::CreateDirectoryFailed(e))?;

    Ok(dest.to_string())
}

/**
 * Download a file and install it if it's a supported package format.
 *
 * # Arguments
 *
 * * `url` - The URL to download from
 * * `min_size_kb` - Minimum required file size in KB (0 for no minimum)
 *
 * # Returns
 *
 * * `Ok(String)` - The path where the file was saved or extracted
 * * `Err(DownloadError)` - An error if the download or installation failed
 *
 * # Examples
 *
 * ```no_run
 * use herolib_os::download_install;
 *
 * fn main() -> Result<(), Box<dyn std::error::Error>> {
 *     // Download and install a .deb package
 *     let result = download_install("https://example.com/package.deb", 100)?;
 *     Ok(())
 * }
 * ```
 *
 * # Notes
 *
 * Currently only supports .deb packages on Debian-based systems.
 * For other file types, it behaves the same as the download function.
 */
pub fn download_install(url: &str, min_size_kb: i64) -> Result<String, DownloadError> {
    // Extract filename from URL
    let filename = match url.split('/').last() {
        Some(name) => name,
        None => {
            return Err(DownloadError::InvalidUrl(
                "cannot extract filename".to_string(),
            ))
        }
    };

    // Create a proper destination path
    let dest_path = format!("/tmp/{}", filename);

    // Check if it's a compressed file that needs extraction
    let lower_url = url.to_lowercase();
    let is_archive = lower_url.ends_with(".tar.gz")
        || lower_url.ends_with(".tgz")
        || lower_url.ends_with(".tar")
        || lower_url.ends_with(".zip");

    let download_result = if is_archive {
        // For archives, use the directory-based download function
        download(url, "/tmp", min_size_kb)?
    } else {
        // For regular files, use the file-specific download function
        download_file(url, &dest_path, min_size_kb)?
    };

    // Check if the downloaded result is a file
    let path = Path::new(&dest_path);
    if !path.is_file() {
        return Ok(download_result); // Not a file, might be an extracted directory
    }

    // Check if it's a .deb package
    if dest_path.to_lowercase().ends_with(".deb") {
        // Check if we're on a Debian-based platform
        let platform_check = Command::new("sh")
            .arg("-c")
            .arg("command -v dpkg > /dev/null && command -v apt > /dev/null || test -f /etc/debian_version")
            .status();

        match platform_check {
            Ok(status) => {
                if !status.success() {
                    return Err(DownloadError::PlatformNotSupported(
                        "Cannot install .deb package: not on a Debian-based system".to_string(),
                    ));
                }
            }
            Err(_) => {
                return Err(DownloadError::PlatformNotSupported(
                    "Failed to check system compatibility for .deb installation".to_string(),
                ))
            }
        }

        // Install the .deb package non-interactively
        println!("Installing package: {}", dest_path);
        let install_result = Command::new("sudo")
            .args(&["dpkg", "--install", &dest_path])
            .status();

        match install_result {
            Ok(status) => {
                if !status.success() {
                    // If dpkg fails, try to fix dependencies and retry
                    println!("Attempting to resolve dependencies...");
                    let fix_deps = Command::new("sudo")
                        .args(&["apt-get", "install", "-f", "-y"])
                        .status();

                    if let Ok(fix_status) = fix_deps {
                        if !fix_status.success() {
                            return Err(DownloadError::InstallationFailed(
                                "Failed to resolve package dependencies".to_string(),
                            ));
                        }
                    } else {
                        return Err(DownloadError::InstallationFailed(
                            "Failed to resolve package dependencies".to_string(),
                        ));
                    }
                }
                println!("Package installation completed successfully");
            }
            Err(e) => return Err(DownloadError::CommandExecutionFailed(e)),
        }
    }

    Ok(download_result)
}
