use std::{collections::BTreeSet, env::consts::OS, fs};

use camino::{Utf8Path, Utf8PathBuf};
use serde::Serialize;
use thiserror::Error as ThisError;

#[derive(Debug, ThisError)]
pub(crate) enum Error {
    #[error(
        "`--tags <TAGS>` should not contain any values that are also in `--skip-tags <SKIP_TAGS>`: {0}"
    )]
    SkipTagsConflictsWithTags(String),
}

#[allow(clippy::struct_excessive_bools)]
#[derive(Serialize)]
pub(crate) struct Facts {
    pub cache_dir: Utf8PathBuf,
    pub config_dir: Utf8PathBuf,
    pub home_dir: Utf8PathBuf,
    pub is_distro_archlinux: bool,
    pub is_distro_armbian: bool,
    pub is_distro_raspbian: bool,
    pub is_os_linux: bool,
    pub is_os_macos: bool,
    pub is_os_windows: bool,
    pub main_file: Utf8PathBuf,
    pub skip_tags: BTreeSet<String>,
    pub tags: BTreeSet<String>,
}
impl Facts {
    pub fn gather() -> Self {
        let is_os_linux = OS == "linux";
        let os_release =
            fs::read_to_string(Utf8Path::new(OS_RELEASE_PATH)).unwrap_or_else(|_| String::new());
        Self {
            cache_dir: Utf8PathBuf::try_from(dirs::cache_dir().expect("should detect cache_dir"))
                .expect("cache directory path should not contain invalid characters"),
            config_dir: config_dir(),
            home_dir: home_dir(),
            is_distro_archlinux: is_os_linux && is_distro_archlinux(&os_release),
            is_distro_armbian: is_os_linux && is_distro_armbian(&os_release),
            is_distro_raspbian: is_os_linux && is_distro_raspbian(&os_release),
            is_os_linux,
            is_os_macos: OS == "macos",
            is_os_windows: OS == "windows",
            ..Default::default()
        }
    }

    pub fn validate(&self) -> Result<()> {
        if let Some(tag) = self.skip_tags.intersection(&self.tags).next() {
            return Err(Error::SkipTagsConflictsWithTags(tag.clone()));
        }

        Ok(())
    }
}
impl Default for Facts {
    fn default() -> Self {
        Self {
            cache_dir: Utf8PathBuf::new(),
            config_dir: Utf8PathBuf::new(),
            home_dir: Utf8PathBuf::new(),
            is_distro_archlinux: false,
            is_distro_armbian: false,
            is_distro_raspbian: false,
            is_os_linux: false,
            is_os_macos: false,
            is_os_windows: false,
            #[cfg(test)]
            main_file: Utf8PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("main.toml"),
            #[cfg(not(test))]
            main_file: Utf8PathBuf::new(),
            skip_tags: BTreeSet::new(),
            tags: default_tags().into_iter().collect(),
        }
    }
}

pub(crate) type Result<T> = std::result::Result<T, Error>;

pub(crate) fn config_dir() -> Utf8PathBuf {
    Utf8PathBuf::try_from(dirs::config_dir().expect("should detect config_dir"))
        .expect("config directory path should not contain invalid characters")
}

pub(crate) fn default_tags() -> Vec<String> {
    vec![String::from("all")]
}

pub(crate) fn home_dir() -> Utf8PathBuf {
    Utf8PathBuf::try_from(dirs::home_dir().expect("should detect home_dir"))
        .expect("home directory path should not contain invalid characters")
}

const OS_RELEASE_ARCHLINUX: &str = "ID=arch";
const OS_RELEASE_ARMBIAN: &str = r#"NAME="Armbian"#;
const OS_RELEASE_DEBIAN: &str = "ID=debian";
const OS_RELEASE_DEBIAN_LIKE: &str = "ID_LIKE=debian";
const OS_RELEASE_PATH: &str = "/etc/os-release";
const OS_RELEASE_RASPBIAN: &str = "ID=raspbian";

fn is_distro_archlinux(os_release: &str) -> bool {
    os_release.contains(OS_RELEASE_ARCHLINUX)
}

fn is_distro_armbian(os_release: &str) -> bool {
    os_release.contains(OS_RELEASE_DEBIAN) && os_release.contains(OS_RELEASE_ARMBIAN)
}

fn is_distro_raspbian(os_release: &str) -> bool {
    os_release.contains(OS_RELEASE_DEBIAN_LIKE) && os_release.contains(OS_RELEASE_RASPBIAN)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn gather_does_not_panic() {
        Facts::gather();
    }
}
