use crate::aliases::{DownloadURL, ModID, ModVersion, PinnedVersionInfo};
use crate::api::structs::Release;
use crate::config::manager::Package;
use crate::errors::LithicError;
use semver::Version;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use tracing::{debug, error, info};
use yansi::Paint;

#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct LithicPkgs {
   pub mods: HashMap<ModID, LithicPkgData>,
   pub game_versions: HashSet<String>,
   pub mod_tags: HashMap<String, String>,
}

#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct LithicPkgData {
   pub pin_version: ModVersion,
}

#[derive(PartialEq, Eq, Debug, Clone)]
pub struct LatestVersionFound {
   pub latest_version: Version,
   pub download_url: Option<String>,
   pub game_versions: Vec<String>,
   pub changelog: Option<String>,
}

pub fn parse_latest_version(releases: &[Release]) -> PinnedVersionInfo {
   let mut errors: Vec<LithicError> = Vec::new();

   // TODO: Review for version pinning, the error needs to be handled better for that
   let result = releases
      .iter()
      .filter_map(|release| {
         let Some(version_str) = &release.mod_version else {
            errors.push(LithicError::SimpleError(format!(
               "Unable to parse version NULL for {:?}",
               release.filename
            )));
            return None;
         };

         // Check if this mod has a pinned version and return the max by that version
         // only clone when passing to parse_version if required
         match parse_version(version_str) {
            Ok(version) => Some((
               version,
               release.main_file.clone(),
               release.tags.clone(),
               release.changelog.clone(),
            )),
            Err(e) => {
               errors.push(e);
               None
            }
         }
      })
      .max_by(|(v1, _, _, _), (v2, _, _, _)| v1.cmp(v2))
      .map(
         |(latest_version, download_url, game_versions, changelog)| LatestVersionFound {
            latest_version,
            download_url,
            game_versions,
            changelog,
         },
      );

   if !errors.is_empty() {
      for error in &errors {
         info!("parse_latest_version: {}", error.to_string());
      }
   }

   return_version_results(result)
}

pub fn parse_download_url_from_version<V: AsRef<[Release]>>(
   releases: V,
   version: &str,
) -> Result<DownloadURL, LithicError> {
   releases
      .as_ref()
      .iter()
      .find_map(|release| {
         release
            .mod_version
            .as_ref()
            .filter(|mv| *mv == version)
            .and_then(|_| release.main_file.clone())
      })
      .ok_or_else(|| {
         LithicError::SimpleError(format!(
            "Version {version} not found. Use [Lithic info -m modid] for valid versions"
         ))
      })
      .map(Into::into)
}

pub fn parse_version(mod_version: &str) -> Result<Version, LithicError> {
   lenient_semver::parse(mod_version).map_err(|e| LithicError::SimpleError(e.to_string()))
}

/// retrieve a version based on version pinning information.
pub fn parse_pinned_version(
   releases: &[Release],
   mod_pkg: &Package,
   pinned_game_version: impl AsRef<str>,
) -> PinnedVersionInfo {
   // user should be using Lithic itself to set pinned_game_version so we trust that its valid, otherwise this function
   // will not return the correct version

   let pinned_game_version = pinned_game_version.as_ref();

   // filter out versions that are not declared as compatible with the pinned game version
   let gres = if pinned_game_version.is_empty() {
      info!("pinned_game_version was empty");
      releases.to_owned()
   } else {
      info!("found pinned_game_version: {pinned_game_version}");
      releases
         .iter()
         .filter(|r| {
            let mut found = false;

            for tag in &r.tags {
               match compare_versions(tag.as_str(), pinned_game_version) {
                  Ok(c) => match c {
                     std::cmp::Ordering::Less | std::cmp::Ordering::Equal => {
                        debug!("Version {tag} is <= {pinned_game_version} --- returning true");
                        found = true;
                     }
                     std::cmp::Ordering::Greater => {
                        debug!("Version {tag} is > {pinned_game_version} --- returning false");
                     }
                  },
                  Err(e) => {
                     error!("{e}");
                  }
               }
            }

            found
         })
         .cloned()
         .collect()
   };

   debug!("releases found for game version {:?}", gres);

   // filter out any version that doesn't match the pinned mod version
   let mres = if mod_pkg.pinned_version.is_some() {
      gres
         .iter()
         .filter(|r| {
            let ver = if let Some(v) = &mod_pkg.pinned_version {
               v.to_string()
            } else {
               return true;
            };

            match compare_versions(r.mod_version.clone().unwrap_or_default().as_str(), &ver) {
               Ok(c) => match c {
                  std::cmp::Ordering::Less | std::cmp::Ordering::Equal => true,
                  std::cmp::Ordering::Greater => false,
               },
               Err(e) => {
                  info!(
                     "{} {}",
                     "parse_pinned_version-mres:".bright_yellow(),
                     e.red().bold()
                  );
                  false
               }
            }
         })
         .cloned()
         .collect()
   } else {
      gres
   };

   let final_res = mres
      .iter()
      .filter_map(|r| {
         let version_str = r.mod_version.as_ref()?;
         match parse_version(version_str) {
            Ok(v) => Some((v, r.main_file.clone(), r.tags.clone(), r.changelog.clone())),
            Err(e) => {
               info!(
                  "{} {}",
                  "parse_pinned_version-final_res:".bright_yellow(),
                  e.red().bold()
               );
               None
            }
         }
      })
      .max_by(|(v1, _, _, _), (v2, _, _, _)| v1.cmp(v2))
      .map(
         |(latest_version, download_url, game_versions, changelog)| LatestVersionFound {
            latest_version,
            download_url: download_url.clone(),
            game_versions,
            changelog,
         },
      );

   return_version_results(final_res)
}

fn return_version_results(
   result: Option<LatestVersionFound>,
) -> (ModVersion, DownloadURL, Vec<String>, String) {
   match result {
      Some(latest_versions_found) => (
         latest_versions_found.latest_version.to_string().into(),
         latest_versions_found
            .download_url
            .clone()
            .unwrap_or_default()
            .into(),
         latest_versions_found.game_versions,
         latest_versions_found.changelog.unwrap_or(String::new()),
      ),
      None => ("".into(), "".into(), Vec::new(), String::new()),
   }
}

pub fn compare_versions(mod_version: &str, other_version: &str) -> Result<std::cmp::Ordering, LithicError> {
   let mv = parse_version(mod_version)?;
   let ov = parse_version(other_version)?;
   Ok(mv.cmp(&ov))
}
