use std::{
    collections::HashMap,
    fs::File,
    io::{BufWriter, Write},
};

use super::IntoFileSource;
use crate::{
    config::{
        errors::ConfigError,
        vars::{CS_CLIENT_ACCESS_KEY, CS_CLIENT_KEY},
    },
    zerokms::ClientKey,
};
use serde::{Deserialize, Serialize};

/// Represents the `cipherstash.secret.toml` configuration file.
///
/// To create a new instance of this struct, use the [CipherStashSecretConfigFileBuilder].
#[derive(Debug, Deserialize, Serialize)]
pub struct CipherStashSecretConfigFile {
    encrypt: SensitiveEncryptSection,
    auth: Option<SensitiveAuthSection>,
    #[serde(skip)]
    meta: Option<String>,
}

impl CipherStashSecretConfigFile {
    pub const DEFAULT_FILE_NAME: &'static str = "cipherstash.secret.toml";

    /// Save the configuration to the given path.
    pub fn persist(self, file_path: impl AsRef<std::path::Path>) -> Result<(), ConfigError> {
        let mut writer =
            write_header(&file_path, self.meta.as_deref()).map_err(ConfigError::invalid)?;
        let toml = toml::to_string(&self).map_err(ConfigError::invalid)?;
        writer
            .write_all(toml.as_bytes())
            .and_then(|_| writer.flush())
            .map_err(|e| ConfigError::io(&e, file_path))
    }

    /// Return the configuration values for use in env file for docker-compose.
    /// Returns a HashMap with the keys `CS_CLIENT_KEY` and `CS_CLIENT_ACCESS_KEY`.
    /// The response is wrapped in a Result to match the behavior of the `env_data` method on `CipherStashConfigFile`.
    pub fn env_data(&self) -> Result<HashMap<&'static str, String>, ConfigError> {
        let mut env_data = HashMap::new();
        env_data.insert(CS_CLIENT_KEY, self.encrypt.client_key.clone());
        if let Some(auth) = &self.auth {
            env_data.insert(CS_CLIENT_ACCESS_KEY, auth.access_key.clone());
        }
        Ok(env_data)
    }

    /// Returns the configuration as an set of environment variables formatted as a single string.
    pub fn display_env(self) -> String {
        let mut result = String::from("\n");

        if let Some(auth) = self.auth {
            result = format!(
                "{}export {}={}\n",
                result, CS_CLIENT_ACCESS_KEY, auth.access_key
            );
        }
        result = format!(
            "{}export {}={}\n",
            result, CS_CLIENT_KEY, self.encrypt.client_key
        );

        result
    }
}

fn write_header(
    path: impl AsRef<std::path::Path>,
    meta: Option<&str>,
) -> Result<BufWriter<File>, std::io::Error> {
    let mut writer = BufWriter::new(std::fs::File::create(&path)?);
    writer.write_all(b"# CipherStash secrets file\n")?;
    if let Some(meta) = meta {
        writer.write_all(meta.as_bytes())?;
    }
    Ok(writer)
}

/// Builder (with a terribly long, Java like name) for the `cipherstash.secret.toml` configuration file (used by stash CLI).
#[derive(Default)]
pub struct CipherStashSecretConfigFileBuilder {
    client_key: Option<ClientKey>,
    access_key: Option<String>,
    meta: Option<String>,
}

impl CipherStashSecretConfigFileBuilder {
    /// Create a new builder.
    pub fn new() -> Self {
        Self::default()
    }

    /// Set the client key.
    pub fn with_client_key(mut self, client_key: ClientKey) -> Self {
        self.client_key = Some(client_key);
        self
    }

    /// Set the access key.
    pub fn with_access_key(mut self, access_key: String) -> Self {
        self.access_key = Some(access_key);
        self
    }

    /// Adds a comment to the top of the configuration file.
    /// Like [Self::with_meta], but accepts an `Option`.
    ///
    /// If `None` is passed, no comment will be added.
    pub fn with_meta_opt<S: Into<String>>(mut self, meta: Option<S>) -> Self {
        self.meta = meta.map(Into::into);
        self
    }

    /// Adds a comment to the top of the configuration file.
    pub fn with_meta(mut self, meta: impl Into<String>) -> Self {
        self.meta = Some(meta.into());
        self
    }

    /// Attempt to build the `CipherStashSecretConfigFile`.
    /// It will error if the client key is missing or invalid.
    ///
    /// NOTE: This method does not currently validate the access key.
    pub fn try_build(self) -> Result<CipherStashSecretConfigFile, ConfigError> {
        self.client_key
            .ok_or(ConfigError::invalid("Missing client key"))?
            .to_hex_v1()
            .map_err(ConfigError::invalid)
            .map(|client_key| CipherStashSecretConfigFile {
                encrypt: SensitiveEncryptSection { client_key },
                auth: self
                    .access_key
                    .map(|access_key| SensitiveAuthSection { access_key }),
                meta: self.meta,
            })
    }
}

impl IntoFileSource for CipherStashSecretConfigFile {
    fn default_path() -> std::path::PathBuf {
        std::path::PathBuf::from(".")
    }

    fn filename() -> String {
        Self::DEFAULT_FILE_NAME.to_string()
    }
}

/// Represents the encrypt section of the "secret" config file, usually called `cipherstash.secret.toml`.
#[derive(Debug, Deserialize, Serialize)]
struct SensitiveEncryptSection {
    client_key: String,
}

/// Represents the auth section of the "secret" config file, usually called `cipherstash.secret.toml`.
#[derive(Debug, Deserialize, Serialize)]
struct SensitiveAuthSection {
    access_key: String,
}

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

    static CLIENT_KEY_HEX: &str = "a4627031a16b7065726d75746174696f6e900e080b0f0a0d060c04000301020905076770325f66726f6da16b7065726d75746174696f6e900902000e0103050807060c0a0f0b0d046570325f746fa16b7065726d75746174696f6e90080403020500070f0b0e060d0a01090c627033a16b7065726d75746174696f6e982116100e12181f181b040a070b181e18191501050c111400181a0d181809030f1702181d0813182006181c";

    #[test]
    fn serialize_cipherstash_secret_config_file() {
        let config = CipherStashSecretConfigFileBuilder::new()
            .with_client_key(ClientKey::from_hex_v1(Uuid::new_v4(), CLIENT_KEY_HEX).unwrap())
            .try_build()
            .unwrap();

        let toml = toml::to_string(&config).unwrap();
        assert_eq!(
            toml,
            format!(
                r#"[encrypt]
client_key = "{CLIENT_KEY_HEX}"
"#,
            )
        );
    }
}
