use core::fmt;
use env_logger;
use semver::Version;
use std::{
    collections::HashMap,
    env,
    fs::{self},
    path::PathBuf,
    sync::Once,
};
use tempfile::TempDir;

use crate::cache::{cache_write, hash_kubeconfig_to_filename};

static INIT: Once = Once::new();

pub fn init_logger() {
    INIT.call_once(|| {
        env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("debug"))
            // TODO: come up with a more useful logging output
            // .format(|buf, record| {
            //     writeln!(
            //         buf,
            //         "[{} {}:{}] {}",
            //         record.level(),
            //         record.module_path().unwrap_or("<unknown>"),
            //         record.line().unwrap_or(0),
            //         record.args()
            //     )
            // })
            .is_test(true)
            .init();
    });
}

#[derive(Clone)]
pub struct TestEnv {
    pub home: PathBuf,
    pub config: PathBuf,
    pub cache: PathBuf,
    pub env_vars: HashMap<String, String>,
}

impl TestEnv {
    fn new() -> Self {
        let tmphome = TempDir::new().expect("failed to create temp config dir");
        let home = tmphome.path().to_path_buf();
        let _ = tmphome.keep();

        if is_keep_tmpdir_truthy() {
            // let _keep = tmphome.keep();
        }

        // TODO: We need to make this code platform specific because the dirs crate doesn't respect XDG_* paths on macos.
        // @See: https://codeberg.org/dirs/dirs-rs/issues/46#issuecomment-5023797
        #[cfg(target_os = "linux")]
        let (config, cache) = (home.join(".config"), home.join(".cache"));

        #[cfg(target_os = "macos")]
        let (config, cache) = (
            home.join("Library").join("Application Support"),
            home.join("Library").join("Caches"),
        );

        Self {
            config,
            cache,
            home: home.to_path_buf(),
            env_vars: HashMap::new(),
        }
    }

    pub fn korrect_config_dir(&self) -> PathBuf {
        self.config.join("korrect")
    }

    pub fn korrect_cache_dir(&self) -> PathBuf {
        self.cache.join("korrect")
    }

    pub fn korrect_bin_dir(&self) -> PathBuf {
        self.home.join(".korrect").join("bin")
    }

    pub fn with_env_var(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.env_vars.insert(key.into(), value.into());
        self
    }

    //FIXME: This can't write to any korrect directory until the environment has been set up.
    // It might make more sense to put this into the setup_korrect method
    pub fn with_hot_cache(
        mut self,
        kubeconfig_content: impl Into<String>,
        cluster_version: &Version,
    ) -> Self {
        self.env_vars.insert(
            "KUBECONFIG".to_string(),
            self.home
                .join("kubeconfig")
                .to_path_buf()
                .to_string_lossy()
                .to_string(),
        );
        self.with_file("kubeconfig", kubeconfig_content);

        let kubeconfig = self.home.join("kubeconfig").to_path_buf();
        let cache_filename = hash_kubeconfig_to_filename(&kubeconfig)
            .unwrap_or_else(|_| panic!("Could not hash kubeconfig [{}].", kubeconfig.display()));
        let path = self.cache.join("korrect").join(cache_filename);
        let _ = cache_write(&path, &cluster_version);
        self
    }

    pub fn with_cache_file(
        &self,
        filename: impl Into<String>,
        contents: impl Into<String>,
    ) -> Self {
        let path = self.korrect_cache_dir().join(filename.into());
        log::debug!("Creating file {}", path.display());
        self.create_file(&path, contents)
    }

    pub fn with_config_file(&self, contents: impl Into<String>) -> Self {
        let path = self.korrect_config_dir().join("config.toml");
        log::debug!("Creating file {}", path.display());
        self.create_file(&path, contents)
    }

    /// Create a file in the home directory of the temp_env.home
    pub fn with_file(&self, filename: impl Into<String>, contents: impl Into<String>) -> Self {
        let path = self.home.join(filename.into());
        log::debug!("Creating file {}", path.display());
        self.create_file(&path, contents)
    }

    /// Create a directory in the home directory of the temp_env.home
    pub fn with_directory(&self, path: impl Into<String>) -> Self {
        let existing_path = &self.home.join(path.into());
        log::debug!("Creating path: {}", existing_path.display());
        fs::create_dir_all(existing_path).unwrap();
        self.clone()
    }

    fn create_file(&self, path: &PathBuf, contents: impl Into<String>) -> Self {
        fs::write(path, contents.into())
            .unwrap_or_else(|_| panic!("Could not write to {}", path.display()));
        self.clone()
    }

    pub fn paths(&self) -> Vec<PathBuf> {
        vec![
            self.home.join(".korrect"),
            self.home.join(".korrect").join("bin"),
            self.config.join("korrect"),
            self.cache.join("korrect"),
        ]
    }
}

impl fmt::Display for TestEnv {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.home.display())
    }
}

fn is_keep_tmpdir_truthy() -> bool {
    match env::var_os("KORRECT_KEEP_TMPDIR") {
        Some(val) => {
            // Try to convert to a string
            if let Some(s) = val.to_str() {
                matches!(s.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on")
            } else {
                false
            }
        }
        None => false,
    }
}

pub fn setup_testenv() -> TestEnv {
    let test_env = TestEnv::new();
    log::debug!("Created temp env: {test_env}");
    test_env
}

pub fn path_containing_korrect(korrect_bin_path: &PathBuf) -> String {
    format!(
        "{}:{}",
        korrect_bin_path.to_str().unwrap(),
        env::var_os("PATH").unwrap_or_default().to_str().unwrap()
    )
}

pub fn assert_dirs_exist(dirs: &[PathBuf]) {
    dirs.iter()
        .for_each(|d| assert!(d.is_dir(), "Missing directory: {}", d.display()));
}

pub fn assert_dirs_not_exist(dirs: &Vec<PathBuf>) {
    dirs.iter()
        .for_each(|d| assert!(!d.exists(), "Directory should not exist: {}", d.display()));
}
