use crate::api::client::ApiClient;
use crate::errors::LithicError;
use crate::errors::LithicError::UrlParseError;
use crate::installer::manager::{Install, Installed};
use indicatif::ProgressBar;
use std::path::{Path, PathBuf};
use tokio::io::AsyncWriteExt;
use tracing::{debug, info, warn};
use url::Url;
use yansi::Paint;

pub async fn download_requested_mods(
   mod_dir: &Path,
   mods_requested: &mut Vec<Install>,
   api_client: &ApiClient,
   pb: Option<&ProgressBar>,
) -> Result<Vec<Installed>, LithicError> {
   let mut tasks = Vec::with_capacity(mods_requested.len());

   // create the outgoing vec with a capacity of what's being requested as we know it ahead of time

   while let Some(mod_request) = mods_requested.pop() {
      info!(
         "{} {}",
         "Attempting to download mod".bright_green(),
         mod_request.mod_id.to_string().bright_yellow()
      );

      let client = api_client.clone();
      let dir = mod_dir.to_path_buf();

      let pb_clone = pb.cloned();

      let task = tokio::spawn(async move {
         let mut installed = Installed {
            mod_id: mod_request.mod_id.clone(),
            mod_name: mod_request.mod_name.clone(),
            installed_file_path: None,
            old_file_path: mod_request.current_file_path.clone(),
            install_version: mod_request.version_to_install.clone(),
            success: false,
         };

         match download_mod(&dir, mod_request.download_url.to_string(), &client).await {
            Ok(installed_path) => {
               info!(
                  "{} {}: {}",
                  "Successfully downloaded mod".bright_green(),
                  mod_request.mod_id.magenta(),
                  installed_path.display().to_string().bright_yellow()
               );
               if let Some(p) = &pb_clone {
                  p.set_message(format!("Downloaded {}", mod_request.mod_id.bright_green()));
                  p.inc(1);
               }

               installed.installed_file_path = Some(installed_path);
               installed.success = true;
               installed.clone()
            }
            Err(e) => {
               warn!("Failed to download mod: {}, {}", mod_request.download_url, e);
               if let Some(p) = &pb_clone {
                  p.set_message(format!("Failed download: {}", mod_request.mod_id.bright_red()));
                  p.inc(1);
               }

               installed.clone()
            }
         }
      });

      tasks.push(task);
   }

   let mut result: Vec<Installed> = Vec::with_capacity(tasks.len());
   for task in tasks {
      match task.await {
         Ok(installed) => result.push(installed),
         Err(e) => warn!("Task join error in download_requested_mods: {}", e),
      }
   }

   Ok(result)
}

pub async fn download_mod(
   mod_dir: &Path,
   download_url: String,
   api_client: &ApiClient,
) -> Result<PathBuf, LithicError> {
   let url = Url::parse(download_url.as_str()).map_err(UrlParseError)?;
   debug!("Trying to download url: {}", url.to_string());

   let filename_from_api = url
      .path_segments()
      .and_then(|mut s| s.next_back())
      .unwrap_or("download.zip")
      .replace(' ', "_");

   // Replace any spaces in the downloaded file with _ . This makes it easier to process later
   let filename_fix = mod_dir
      .to_path_buf()
      .join(&filename_from_api)
      .to_string_lossy()
      .to_string();
   let requested_file_path = PathBuf::from(filename_fix);

   // Retry logic - attempt download up to 3 times
   let max_retries = 3;
   let mut attempt = 0;

   while attempt < max_retries {
      attempt += 1;
      info!(
         "{}: [{}] {}",
         "Download attempt".bright_blue(),
         attempt.to_string().magenta(),
         url.to_string().bright_yellow()
      );

      match download_and_verify(&url, &requested_file_path, api_client).await {
         // file_path here is the verified path after the file has been downloaded
         Ok(file_path) => {
            info!(
               "{} {} {} {}",
               "Successfully downloaded".bright_green(),
               file_path.display().to_string().bright_yellow(),
               "on attempt".bright_green(),
               attempt.to_string().magenta()
            );
            return Ok(file_path);
         }
         Err(e) => {
            warn!("Download attempt {} failed for {}: {}", attempt, url, e);

            // Clean up any partial downloads
            if requested_file_path.exists()
               && let Err(clean_err) = tokio::fs::remove_file(&requested_file_path).await
            {
               warn!(
                  "Failed to clean up partial download {}: {}",
                  requested_file_path.display(),
                  clean_err
               );
            }

            info!(
               "{} {} {}",
               "Download failed on attempt ".yellow(),
               attempt.to_string().magenta(),
               e.to_string().red()
            );

            // Add a small delay between retries
            if attempt < max_retries {
               let delay = std::time::Duration::from_secs(2u64.pow((attempt - 1) as u32));
               tokio::time::sleep(delay).await;
            }
         }
      }
   }

   Err(LithicError::SimpleError("Maximum retries exceeded".to_string()))
}

pub async fn download_and_verify(
   url: &Url,
   file_path: impl AsRef<Path>,
   api_client: &ApiClient,
) -> Result<PathBuf, LithicError> {
   let file_path = file_path.as_ref();
   let response = api_client
      .get_request(url.as_ref())
      .await
      .map_err(|e| LithicError::SimpleError(e.to_string()))?;

   // Check if we got a successful response
   if !response.status().is_success() {
      return Err(LithicError::SimpleError(format!(
         "Server returned error status: {}",
         response.status()
      )));
   }

   // Get the full response body
   let bytes = response.bytes().await.map_err(|e| LithicError::IoError {
      context: format!("Failure reading response from API {url}"),
      source: std::io::Error::other(e),
   })?;

   // Verify we have actual content
   if bytes.is_empty() {
      return Err(LithicError::SimpleError("Downloaded file is empty".to_string()));
   }

   // Create and write to temp file first
   let temp_file_path = file_path.with_extension("tmp");

   let mut file = tokio::fs::File::create(&temp_file_path)
      .await
      .map_err(|e| LithicError::IoError {
         context: format!("Unable to create temp file {}", temp_file_path.to_string_lossy()),
         source: e,
      })?;

   file.write_all(&bytes).await.map_err(|e| LithicError::IoError {
      context: format!(
         "Failure while writing to file {}",
         temp_file_path.to_string_lossy()
      ),
      source: e,
   })?;

   // Ensure all data is written to disk
   file.sync_all().await.map_err(|e| LithicError::IoError {
      context: format!(
         "Failed to flush file data for {}",
         temp_file_path.to_string_lossy()
      ),
      source: e,
   })?;

   // Close the file
   drop(file);

   // Pre-verify the zip file
   // verify_zip_file(&temp_file_path).await?;

   // Rename temp file to final file
   tokio::fs::rename(&temp_file_path, file_path)
      .await
      .map_err(|e| LithicError::IoError {
         context: format!("Failed to rename temp file to {}", file_path.to_string_lossy()),
         source: e,
      })?;

   debug!("File downloaded to {}", file_path.display());

   Ok(file_path.into())
}
