use crate::service::Services;
use crate::{error, Result};
use itertools::join;
use snafu::{ensure, ResultExt};
use std::collections::HashSet;
use std::fs::{self, OpenOptions};
use std::io::prelude::*;
use std::os::unix::fs::OpenOptionsExt;
use std::path::{Path, PathBuf};
use std::process::Command;
const SYSTEMCTL_DAEMON_RELOAD: &str = "systemctl daemon-reload";
const DEFAULT_FILE_MODE: u32 = 0o644;
/// Query the API for ConfigurationFile data
#[allow(clippy::implicit_hasher)]
pub async fn get_affected_config_files
(
    socket_path: P,
    files_limit: Option>,
) -> Result
where
    P: AsRef,
{
    // Only want a query parameter if we had specific affected files, otherwise we want all
    let query = files_limit.map(|files| ("names", join(&files, ",")));
    debug!("Querying API for configuration file metadata");
    let uri = "/configuration-files";
    let config_files: model::ConfigurationFiles = schnauzer::get_json(socket_path, uri, query)
        .await
        .context(error::GetJsonSnafu { uri })?;
    Ok(config_files)
}
/// Given a map of Service objects, return a HashSet of
/// affected configuration file names
pub fn get_config_file_names(services: &Services) -> HashSet {
    debug!("Building set of affected configuration file names");
    let mut config_file_set = HashSet::new();
    for service in services.0.values() {
        for file in &service.model.configuration_files {
            config_file_set.insert(file.to_string());
        }
    }
    trace!("Config file names: {:?}", config_file_set);
    config_file_set
}
/// Render the configuration files
// If strict is True, return an error if we fail to render any template.
// If strict is False, ignore failures, always returning an Ok value
// containing any successfully rendered templates.
pub fn render_config_files(
    registry: &handlebars::Handlebars<'_>,
    config_files: model::ConfigurationFiles,
    settings: model::Model,
    strict: bool,
) -> Result> {
    // Go write all the configuration files from template
    let mut rendered_configs = Vec::new();
    for (name, metadata) in config_files {
        debug!("Rendering {}", &name);
        let try_rendered = registry.render(&name, &settings);
        if strict {
            let rendered = try_rendered.context(error::TemplateRenderSnafu { template: name })?;
            rendered_configs.push(RenderedConfigFile::new(
                &metadata.path,
                rendered,
                &metadata.mode,
            ));
        } else {
            match try_rendered {
                Ok(rendered) => rendered_configs.push(RenderedConfigFile::new(
                    &metadata.path,
                    rendered,
                    &metadata.mode,
                )),
                Err(err) => warn!("Unable to render template '{}': {}", &name, err),
            }
        }
    }
    trace!("Rendered configs: {:?}", &rendered_configs);
    Ok(rendered_configs)
}
/// Write all the configuration files to disk
pub fn write_config_files(rendered_configs: &[RenderedConfigFile]) -> Result<()> {
    for cfg in rendered_configs {
        debug!("Writing {:?}", &cfg.path);
        cfg.write_to_disk()?;
    }
    Ok(())
}
/// Run `systemd daemon-reload` if any modified config file requires it.
pub fn reload_config_files(rendered_configs: &[RenderedConfigFile]) -> Result<()> {
    if rendered_configs
        .iter()
        .any(RenderedConfigFile::needs_reload)
    {
        let mut args = SYSTEMCTL_DAEMON_RELOAD.split(' ');
        let program = args.next().expect("failed to split on space");
        trace!("Command: {}", &program);
        trace!("Args: {:?}", &args);
        let result = Command::new(program).args(args).output().context(
            error::CommandExecutionFailureSnafu {
                command: SYSTEMCTL_DAEMON_RELOAD,
            },
        )?;
        // If the reload command exited nonzero, call it a failure
        ensure!(
            result.status.success(),
            error::FailedReloadCommandSnafu {
                command: SYSTEMCTL_DAEMON_RELOAD,
                stderr: String::from_utf8_lossy(&result.stderr),
            }
        );
        trace!(
            "Command stdout: {}",
            String::from_utf8_lossy(&result.stdout)
        );
        trace!(
            "Command stderr: {}",
            String::from_utf8_lossy(&result.stderr)
        );
    }
    Ok(())
}
/// RenderedConfigFile contains both the path to the config file
/// and the rendered data to write.
#[derive(Debug)]
pub struct RenderedConfigFile {
    path: PathBuf,
    rendered: String,
    mode: Option,
}
impl RenderedConfigFile {
    fn new(path: &str, rendered: String, mode: &Option) -> RenderedConfigFile {
        RenderedConfigFile {
            path: PathBuf::from(&path),
            rendered,
            mode: mode.to_owned(),
        }
    }
    /// Writes the rendered template at the proper location
    fn write_to_disk(&self) -> Result<()> {
        if let Some(dirname) = self.path.parent() {
            fs::create_dir_all(dirname).context(error::TemplateWriteSnafu {
                path: dirname,
                pathtype: "directory",
            })?;
        };
        let mut binding = OpenOptions::new();
        let options = binding
            .write(true)
            .create(true)
            .truncate(true)
            .mode(DEFAULT_FILE_MODE);
        // See if this file has a config setting for a specific mode
        if let Some(mode) = &self.mode {
            let mode_int =
                u32::from_str_radix(mode.as_str(), 8).context(error::TemplateModeSnafu {
                    path: &self.path,
                    mode,
                })?;
            options.mode(mode_int);
        }
        let mut file = options
            .open(&self.path)
            .context(error::TemplateWriteSnafu {
                path: &self.path,
                pathtype: "file",
            })?;
        file.write_all(self.rendered.as_bytes())
            .context(error::TemplateWriteSnafu {
                path: &self.path,
                pathtype: "file",
            })
    }
    /// Checks whether the config file needs `systemd` to reload.
    fn needs_reload(&self) -> bool {
        self.path.to_string_lossy().starts_with("/etc/systemd/")
    }
}
#[cfg(test)]
mod test {
    use super::*;
    use crate::service::Services;
    use maplit::{hashmap, hashset};
    use std::convert::TryInto;
    #[test]
    fn test_get_config_file_names() {
        let input_map = hashmap!(
            "foo".to_string() => model::Service {
                configuration_files: vec!["file1".try_into().unwrap()],
                restart_commands: vec!["echo hi".to_string()]
            },
            "bar".to_string() => model::Service {
                configuration_files: vec!["file1".try_into().unwrap(), "file2".try_into().unwrap()],
                restart_commands: vec!["echo hi".to_string()]
            },
        );
        let services = Services::from_model_services(input_map, None);
        let expected_output = hashset! {"file1".to_string(), "file2".to_string() };
        assert_eq!(get_config_file_names(&services), expected_output)
    }
}