use core::fmt;
use std::{
    env,
    fs::{File, copy, read_dir},
    path::{Path, PathBuf},
};

use anyhow::{Context, Result};
use flate2::bufread::GzDecoder;
use semver::Version;
use tar::Archive;
use tempfile::TempDir;

use crate::network::{get_helm_release_versions, get_stable_version};

#[derive(Debug, Clone)]
pub enum ManagedBinary {
    Helm,
    Kubectl,
    Korrect,
}

impl fmt::Display for ManagedBinary {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let s = match self {
            ManagedBinary::Helm => "helm",
            ManagedBinary::Kubectl => "kubectl",
            ManagedBinary::Korrect => "korrect-shim",
        };
        write!(f, "{}", s)
    }
}

impl ManagedBinary {
    pub fn bin_url(&self, version: &str, os: &str, arch: &str) -> Result<String> {
        // Informed by https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
        match self {
            ManagedBinary::Helm => {
                let base_url =
                    env::var("KORRECT_BASE_URL").unwrap_or("https://get.helm.sh".to_owned());
                let url = format!("{base_url}/helm-{version}-{os}-{arch}.tar.gz");
                Ok(url)
            }
            ManagedBinary::Kubectl => {
                let base_url =
                    env::var("KORRECT_BASE_URL").unwrap_or("https://dl.k8s.io".to_owned());
                let url = format!("{base_url}/release/{version}/bin/{os}/{arch}/kubectl");
                Ok(url)
            }
            ManagedBinary::Korrect => {
                anyhow::bail!("This method [bin_url] is not implemented for this managed binary.");
            }
        }
    }
    pub fn latest_version_url(&self, minor: Option<u64>) -> Result<String> {
        match self {
            ManagedBinary::Helm => match minor {
                Some(_) => {
                    let base_url =
                        env::var("KORRECT_BASE_URL").unwrap_or("https://api.github.com".to_owned());

                    let url = format!("{}/repos/helm/helm/tags", base_url);
                    Ok(url)
                }
                None => {
                    let base_url =
                        env::var("KORRECT_BASE_URL").unwrap_or("https://get.helm.sh".to_owned());
                    let url = format!("{}/helm-latest-version", base_url);
                    Ok(url)
                }
            },
            ManagedBinary::Kubectl => {
                let base_url =
                    env::var("KORRECT_BASE_URL").unwrap_or("https://dl.k8s.io".to_owned());
                let stable_file = match minor {
                    Some(minor) => {
                        format!("stable-1.{minor}.txt")
                    }
                    None => "stable.txt".to_string(),
                };
                let url = format!("{}/release/{stable_file}", base_url);
                Ok(url)
            }
            ManagedBinary::Korrect => {
                anyhow::bail!(
                    "The method [latest_version_url] is not implemented for this managed binary."
                );
            }
        }
    }

    pub fn exact_bin_version_from_cluster_version(
        &self,
        cluster_version: &Version,
        max_bin_version: &Version,
    ) -> Option<Version> {
        match self {
            ManagedBinary::Helm => {
                self.latest_bin_version_from_cluster_version(cluster_version, max_bin_version)
            }
            ManagedBinary::Kubectl => Some(cluster_version.clone()),
            ManagedBinary::Korrect => None,
        }
    }

    /// # Arguments
    /// * `cluster_version` - The version of the k8s cluster
    /// * `max_bin_version` - The latest version of managed binary in existence. Used to ensure that we don't suggest a version that's so "latest" it doesn't yet exist.
    ///
    /// # Returns
    /// The latest managed binary version compatible with a given cluster version
    pub fn latest_bin_version_from_cluster_version(
        &self,
        cluster_version: &Version,
        max_bin_version: &Version,
    ) -> Option<Version> {
        if cluster_version.major != 1 {
            return None;
        }
        match self {
            ManagedBinary::Helm => {
                let minor = std::cmp::min(cluster_version.minor - 12, max_bin_version.minor);
                self.get_latest_version_for_minor(Some(minor)).ok()
            }
            ManagedBinary::Kubectl => {
                let minor = std::cmp::min(cluster_version.minor + 1, max_bin_version.minor);
                self.get_latest_version_for_minor(Some(minor)).ok()
            }
            ManagedBinary::Korrect => None,
        }
    }

    pub fn from_str(name: &str) -> Result<Self> {
        match name.to_lowercase().as_str() {
            "helm" => Ok(ManagedBinary::Helm),
            "kubectl" => Ok(ManagedBinary::Kubectl),
            "k" => Ok(ManagedBinary::Kubectl),
            "korrect" => Ok(ManagedBinary::Korrect),
            _ => Ok(ManagedBinary::Korrect),
            // At the moment, there's no clear benefit to throwing an error.
            // _ => anyhow::bail!(format!("Invalid binary type: {}", name)),
        }
    }

    fn find_helm_binary_in_unpacked_archive(base_dir: &Path) -> Option<PathBuf> {
        for entry in read_dir(base_dir).ok()? {
            let entry = entry.ok()?;
            let path = entry.path();
            if path.is_dir() {
                let candidate = path.join("helm");
                if candidate.exists() && candidate.is_file() {
                    return Some(candidate);
                }
            }
        }
        None
    }

    pub fn extract_helm_from_tar_gz(source: &PathBuf, destination: &PathBuf) -> Result<()> {
        let file = File::open(source)
            .with_context(|| format!("Failed to open archive file: {}", source.display()))?;
        let reader = std::io::BufReader::new(file);
        let decompressor = GzDecoder::new(reader);
        let mut archive = Archive::new(decompressor);

        let temp_dir = TempDir::new()?;
        log::debug!(
            "Extracting [{}] to [{}].",
            source.display(),
            temp_dir.path().display()
        );
        archive
            .unpack(&temp_dir)
            .context("Failed to unpack tar.gz archive")?;

        let helm_path = Self::find_helm_binary_in_unpacked_archive(temp_dir.path())
            .context("Could not find 'helm' binary in extracted archive")?;

        log::debug!(
            "Moving [{}] to [{}].",
            &helm_path.display(),
            destination.display()
        );
        // note that we're not using rename because that will fail across mount points. the original file will be cleaned up as the temp dir will be removed
        copy(&helm_path, destination)
            .with_context(|| format!("Failed to move helm binary to {}", destination.display()))?;
        Ok(())
    }

    pub fn get_latest_version_for_minor(&self, minor: Option<u64>) -> Result<Version> {
        match self {
            ManagedBinary::Helm => match minor {
                Some(minor) => self.get_helm_latest_version(Some(minor)),
                //TODO: move this logic into a self.get_latest_version method
                None => {
                    let url = self.latest_version_url(minor)?;
                    get_stable_version(&url)
                }
            },
            ManagedBinary::Kubectl => {
                //TODO: move this logic into a self.get_latest_version method
                let url = self.latest_version_url(minor)?;
                get_stable_version(&url)
            }
            ManagedBinary::Korrect => {
                anyhow::bail!(
                    "This method [get_latest_version] is not implemented for this managed binary."
                );
            }
        }
    }

    pub fn get_helm_latest_version(&self, helm_minor: Option<u64>) -> Result<Version> {
        let url = self.latest_version_url(helm_minor)?;
        // TODO: versions needs to be cached
        let versions = get_helm_release_versions(&url)?;
        let helm_minor = helm_minor.unwrap_or({
            versions
                .iter()
                .max_by(|a, b| a.minor.cmp(&b.minor))
                .unwrap()
                .minor
        });
        let max_patch = versions
            .iter()
            .filter_map(|version| {
                if version.minor == helm_minor {
                    Some(version)
                } else {
                    None
                }
            })
            .max_by(|a, b| a.patch.cmp(&b.patch))
            .unwrap();
        log::debug!("Max Patch: {}", max_patch);
        Ok(max_patch.clone())
    }
}
