use crate::*;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::io::Write;
use std::path::PathBuf;
use std::process::{Command, ExitStatus, Stdio};
use std::{fmt, fs, io};

#[derive(Debug, Serialize, Deserialize, PartialEq, Copy, Clone)]
pub enum UpdateSteps {
    PreInstall,
    Install,
    PostInstall,
}

/// Root of the machine’s dependency graph
#[derive(Debug, Serialize, Deserialize)]
pub struct Updater {
    pub packagers: BTreeMap<String, Packager>,
}

/// A list of equivalent executors that will update a given component
///
/// Example: the `system` one will try to do update the system for as if it is Debian with `apt update`, if it fails it will try for openSUSE with `zypper refresh`, …
/// The step will be considered a succes if **any** executor succeed and will skip all the other ones.
#[derive(Debug, Serialize, Deserialize)]
pub struct Packager {
    executors: Vec<Executor>,
    // TODO: => make a system dependend on another? This will allow to give a "Rust" config which update "rustup", and a custom "git helix" could then be executed after (with the updated toolchain, and NOT concurrently)
}

/// All the infos for an executor to proceed until completion
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Executor {
    pub name: String,
    pre_install: Option<Vec<Cmd>>,
    install: Cmd,
    post_install: Option<Vec<Cmd>>,
    binaries: Option<Vec<String>>, // TODO: find a more explicit name for binaries that need to be present to enable the executor
}

/// A command to execute on the system as part of an executor
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Cmd {
    exe: String,
    params: Option<Vec<String>>,
    current_dir: Option<PathBuf>,
    env: Option<BTreeMap<String, String>>,
}

/// The actual (cleaned) command that will be executed on the system as part of an executor
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActualCmd {
    exe: String,
    params: Vec<String>,
    current_dir: Option<PathBuf>,
    env: BTreeMap<String, String>,
}

impl From<String> for UpdateSteps {
    fn from(value: String) -> Self {
        match value.to_lowercase().as_str() {
            "pre_install" => UpdateSteps::PreInstall,
            "install" => UpdateSteps::Install,
            "post_install" => UpdateSteps::PostInstall,

            _ => panic!("Step {} not recognized", value),
        }
    }
}

impl Display for UpdateSteps {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            UpdateSteps::PreInstall => write!(f, "pre_install"),
            UpdateSteps::Install => write!(f, "install"),
            UpdateSteps::PostInstall => write!(f, "post_install"),
        }
    }
}

pub fn get_packages_folder(opt: &Opt) -> io::Result<PathBuf> {
    if let Some(p) = opt.config_folder.clone() {
        return Ok(p);
    }

    let config_folder = directories::ProjectDirs::from("net", "ZykiCorp", "System Updater")
        .ok_or(io::Error::new(
            io::ErrorKind::NotFound,
            "System’s configuration folder: for its standard location see https://docs.rs/directories/latest/directories/struct.ProjectDirs.html#method.config_dir",
        ))?
        .config_dir()
        .join("packagers");

    Ok(config_folder)
}

impl Updater {
    fn new() -> Updater {
        Updater {
            packagers: BTreeMap::default(),
        }
    }

    /// To create a sample config from code
    #[doc(hidden)]
    fn write_config(&self, opt: &Opt) {
        use std::fs::OpenOptions;

        let config_folder = get_packages_folder(opt).unwrap();

        let mut f = OpenOptions::new()
            .write(true)
            .create(true)
            .truncate(true)
            .open(config_folder.join("default.yaml"))
            .unwrap();

        fs::create_dir_all(&config_folder).unwrap();

        f.write_all(serde_yaml::to_string(&self).unwrap().as_bytes())
            .unwrap();
    }

    // TODO: add option to use &opt.config_file (or folder?) instead
    pub fn from_config(opt: &Opt) -> io::Result<Updater> {
        let mut updater = Updater::new();

        // Example to generate a config file
        if false {
            updater
                .packagers
                .insert("Test".to_owned(), Packager { executors: vec![] });
            let sys = updater
                .packagers
                .get_mut("Test")
                .expect("We just created the key");

            sys.executors.push(Executor {
                name: "Rustup".to_owned(),
                pre_install: None,
                install: Cmd {
                    exe: "rustup".to_owned(),
                    params: Some(vec!["self".to_owned(), "update".to_owned()]),
                    current_dir: None,
                    env: None,
                },
                post_install: None,
                binaries: None,
            });

            sys.executors.push(Executor {
                name: "Cargo".to_owned(),
                pre_install: None,
                install: Cmd {
                    exe: "cargo".to_owned(),
                    params: Some(vec!["install-update".to_owned(), "-a".to_owned()]),
                    current_dir: None,
                    env: None,
                },
                post_install: None,
                binaries: Some(vec!["rustc".to_owned()]),
            });

            updater.write_config(opt);

            panic!("Wrote a config sample.");
        }

        let packages_folder = get_packages_folder(opt)?;
        // TODO: Useless match? Still a risck for "time-of-check to time-of-use" bug
        match packages_folder.try_exists() {
            Ok(true) => {} // Ok: Exist and should be readable
            Ok(false) => {
                return Err(io::Error::new(
                    io::ErrorKind::Other,
                    format!(
                        "Configuration folder not accessible at: {}. (broken symlink?)",
                        packages_folder.display()
                    ),
                ))
            }
            Err(e) => return Err(e),
        }

        for file in packages_folder.read_dir()?.filter(|name| match name {
            Ok(n) => n.file_name().into_string().unwrap().ends_with(".yaml"),
            Err(..) => false,
        }) {
            let file = file?.path();
            let sys = std::fs::read_to_string(&file).unwrap();
            let sys = serde_yaml::from_str(&sys).map_err(|err| {
                io::Error::new(
                    io::ErrorKind::Other,
                    format!(
                        "Encontered an error while parsing config file {}: {}",
                        file.display(),
                        err
                    ),
                )
            })?;
            updater
                .packagers
                .insert(file.file_stem().unwrap().to_str().unwrap().to_owned(), sys);
        }

        // eprintln!("{:#?}", updater);

        Ok(updater)
    }

    pub fn update_all(&self, opt: &Opt) -> Summary {
        let mut status: Vec<_> = vec![];

        // XXX: We may parallelise (iter_par from rayon?) this loop. But the UI will be problematic to handle
        for (packager_name, packager) in &self.packagers {
            // TODO: default status should be an error, but… in the same time we should not have en empty list
            let mut record = Record {
                packager: String::from("No packager"),
                executor: String::from("No executor"),
                status: Err(Error::Config {
                    source: which::Error::CannotFindBinaryPath,
                    filename: String::new(),
                })
                .into(),
            };

            assert!(!packager.executors.is_empty());

            for executor in &packager.executors {
                let mut u = executor.is_usable();
                let mut is_ok = executor.is_usable().is_ok();
                if is_ok {
                    u = self.update(executor, opt);
                    is_ok = u.is_ok();
                }

                record = Record {
                    packager: packager_name.to_string(),
                    executor: executor.name.clone(),
                    status: u.into(),
                };

                if is_ok {
                    break;
                }
            }

            status.push(record);
        }

        Summary { status }
    }

    fn update(&self, sys: &Executor, opt: &Opt) -> Result<()> {
        let steps = &opt.steps;
        assert!(!steps.is_empty());

        if steps.contains(&UpdateSteps::PreInstall) {
            sys.pre_install(opt)?;
        }
        if steps.contains(&UpdateSteps::Install) {
            sys.install(opt)?;
        }
        if steps.contains(&UpdateSteps::PostInstall) {
            sys.post_install(opt)?;
        }

        Ok(())
    }
}

fn are_all_binaries_found_string(
    bins: &[String],
) -> std::result::Result<(), (which::Error, String)> {
    let mut outer_b = String::new(); // TODO: This outer_b feels ackish
    bins.iter()
        .try_for_each(|b| {
            outer_b = b.clone();
            which::which(b)?;
            Ok::<(), which::Error>(())
        })
        .map_err(|s| (s, outer_b))?;
    Ok(())
}

fn are_all_binaries_found_cmd(bins: &[Cmd]) -> std::result::Result<(), (which::Error, String)> {
    let mut outer_b = String::new(); // TODO: This outer_b feels ackish
    bins.iter()
        .try_for_each(|b| {
            outer_b = b.exe.clone();
            which::which(b.exe.clone())?;
            Ok::<(), which::Error>(())
        })
        .map_err(|s| (s, outer_b))?;
    Ok(())
}

impl Executor {
    // TODO: Transform from user’s executable to another one?
    pub fn is_usable(&self) -> Result<()> {
        are_all_binaries_found_cmd(&self.pre_install.clone().unwrap_or_default())
            .map_err(|(source, filename)| Error::Config { source, filename })?;
        are_all_binaries_found_cmd(&[self.install.clone()])
            .map_err(|(source, filename)| Error::Config { source, filename })?;

        are_all_binaries_found_cmd(&self.post_install.clone().unwrap_or_default())
            .map_err(|(source, filename)| Error::Config { source, filename })?;

        are_all_binaries_found_string(&self.binaries.clone().unwrap_or_default())
            .map_err(|(source, filename)| Error::Config { source, filename })?;

        Ok(())
    }

    pub fn pre_install(&self, opt: &Opt) -> Result<()> {
        if let Some(pre_install) = &self.pre_install {
            for cmd in pre_install {
                let cmd = cmd.clone().prepare(opt);
                let exit_status = cmd.execute(opt).map_err(|err| Error::Execution {
                    source: err,
                    step: UpdateSteps::PreInstall,
                    cmd: cmd.clone(),
                })?;

                if !exit_status.success() {
                    return Result::Err(Error::Execution {
                        source: io::Error::new(io::ErrorKind::Other, format!("{}", exit_status)),
                        step: UpdateSteps::PreInstall,
                        cmd,
                    });
                }
            }
        }
        Ok(())
    }

    pub fn install(&self, opt: &Opt) -> Result<()> {
        let cmd = self.install.clone().prepare(opt);
        let exit_status = cmd.execute(opt).map_err(|err| Error::Execution {
            source: err,
            step: UpdateSteps::Install,
            cmd: cmd.clone(),
        })?;

        if !exit_status.success() {
            return Err(Error::Execution {
                source: io::Error::new(io::ErrorKind::Other, format!("{}", exit_status)),
                step: UpdateSteps::Install,
                cmd,
            });
        }
        Ok(())
    }

    pub fn post_install(&self, opt: &Opt) -> Result<()> {
        if let Some(post_install) = &self.post_install {
            for cmd in post_install {
                let cmd = cmd.clone().prepare(opt);
                let exit_status = cmd.execute(opt).map_err(|err| Error::Execution {
                    source: err,
                    step: UpdateSteps::PostInstall,
                    cmd: cmd.clone(),
                })?;

                if !exit_status.success() {
                    return Err(Error::Execution {
                        source: io::Error::new(io::ErrorKind::Other, format!("{}", exit_status)),
                        step: UpdateSteps::PostInstall,
                        cmd,
                    });
                }
            }
        }
        Ok(())
    }
}

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

impl Cmd {
    #[allow(dead_code)] // "To easily generate from code"
    fn new() -> Cmd {
        Cmd {
            exe: "".into(),
            params: None,
            current_dir: None,
            env: None,
        }
    }

    fn prepare(self, _opt: &Opt) -> ActualCmd {
        // TODO: I’m not convinced by helping the user and only escaping the PATH. Either all or none
        // This means I need to know how to know which values to pass for (at least) rustup & cargo
        let env = match self.env {
            Some(env) => env,
            None => BTreeMap::default(),
        };

        ActualCmd {
            exe: self.exe,
            params: self.params.unwrap_or_default(),
            current_dir: self.current_dir,
            env,
        }
    }
}

impl ActualCmd {
    fn execute(&self, opt: &Opt) -> io::Result<ExitStatus> {
        let mut cmd = Command::new(&self.exe);

        cmd.args(&self.params).envs(&self.env);

        if let Some(cdir) = &self.current_dir {
            cmd.current_dir(std::fs::canonicalize(cdir)?);
        }

        println!();
        println!("*** Executing: {} ***", self);
        // eprintln!("{:?}", self.params);

        if opt.quiet {
            // FIXME: stdin does not work with sudo?
            cmd.stdin(Stdio::null())
                .stdout(Stdio::null())
                .stderr(Stdio::null());
        }

        cmd.status()
    }
}

impl fmt::Display for ActualCmd {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let command = if !self.params.is_empty() {
            format!("{} {}", &self.exe, &self.params.join(" "))
        } else {
            self.exe.clone()
        };
        write!(f, "{}", command)?;

        if let Some(cdir) = &self.current_dir {
            write!(f, " in {:?}", cdir)?;
        }

        Ok(())

        // // TODO: remove me (too verbose)
        // if !self.env.is_empty() {
        //     writeln!(f, " with the following environment variable:")?;
        //     writeln!(f, "{:#?}", self.env)
        // } else {
        //     write!(f, " without any environment variable. ")
        // }
    }
}
