use super::{Disallowed, IntoFileSource};
use crate::config::{
    errors::ConfigError,
    vars::{CS_CLIENT_ID, CS_DEFAULT_KEYSET_ID, CS_REGION, CS_WORKSPACE_CRN, CS_WORKSPACE_ID},
};
use cts_common::{Crn, Region, WorkspaceId};
use serde::{Deserialize, Serialize};
use std::{
    collections::HashMap,
    fs::File,
    io::{BufWriter, Write},
};

/// Represents the standard configuration file usually named `cipherstash.toml`.
///
/// The file contains 2 sections: `encrypt` and `auth`.
///
/// ## `encrypt` section
///
/// It contains an `encrypt` which specifies the client ID and an optional default keyset ID.
/// If not provided and the client is only bound to a single keyset, the client will use that keyset.
/// Otherwise it will throw an error.
///
/// ## `auth` section
///
/// The `auth` section specifies what workspace to use.
/// How to authenticate to that workspace is specified in the `cipherstash.secret.toml` file.
///
/// The workspace can be specified by either a [Crn] or a [WorkspaceId] and [Region].
/// The [Crn] is the preferred method.
/// Note that regions must include the cloud provider (e.g. `us-west-1.aws`).
///
/// # Example
///
/// ```toml
/// [encrypt]
/// client_id = "1792d1b2-ae14-4d49-ba26-5dcc5a36a716"
/// default_keyset_id = "1792d1b2-ae14-4d49-ba26-5dcc5a36a716"
///
/// [auth]
/// workspace_crn = "crn:us-west-1.aws:E4UMRN47WJNSMAKR"
/// ```
///
/// Or, using the workspace ID and region:
///
/// ```toml
/// [encrypt]
/// client_id = "1792d1b2-ae14-4d49-ba26-5dcc5a36a716"
/// default_keyset_id = "1792d1b2-ae14-4d49-ba26-5dcc5a36a716"
///
/// [auth]
/// workspace_id = "R7MMSPNCFRY7ZBNH"
/// region = "us-west-1.aws"
/// ```
///
/// Note that `client_key` is *not* allowed in this file.
/// Instead, the client key should be stored in the `cipherstash.secret.toml` file or set as an environment variable, `CS_CLIENT_KEY`.
///
/// To create a new configuration file, use the [CipherStashConfigFileBuilder].
#[derive(Debug, Deserialize, Serialize)]
pub struct CipherStashConfigFile {
    encrypt: NonSensitiveEncryptSection,
    auth: NonSensitiveAuthSection,
    #[serde(skip)]
    meta: Option<String>,
}

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

    // FIXME: This should be a method on the FileSource
    // Ditto for the CipherStashSecretConfigFile
    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 to use in env file for docker-compose.
    /// Returns a map of environment variables to set.
    ///
    /// Includes:
    /// - `CS_CLIENT_ID`
    /// - `CS_DEFAULT_KEYSET_ID` (if set)
    /// - `CS_WORKSPACE_CRN` (if set)
    /// - `CS_WORKSPACE_ID` (if set)
    /// - `CS_REGION` (if set)
    ///
    /// Note that this doesn't perform any consistency validation (e.g. if both `workspace_crn` and `workspace_id` are set)
    /// as this will be done by the config builder when it is created.
    pub fn env_data(&self) -> Result<HashMap<&'static str, String>, ConfigError> {
        let mut env_data = HashMap::new();
        env_data.insert(CS_CLIENT_ID, self.encrypt.client_id.clone());
        if let Some(keyset_id) = &self.encrypt.default_keyset_id {
            env_data.insert(CS_DEFAULT_KEYSET_ID, keyset_id.clone());
        }
        self.auth
            .workspace_crn
            .as_ref()
            .map(|crn| env_data.insert(CS_WORKSPACE_CRN, crn.to_string()));

        self.auth
            .region
            .as_ref()
            .map(|region| env_data.insert(CS_REGION, region.to_string()));

        self.auth
            .workspace_id
            .as_ref()
            .map(|workspace_id| env_data.insert(CS_WORKSPACE_ID, workspace_id.to_string()));

        Ok(env_data)
    }

    /// Writes the configuration to the given writer as environment variables.
    /// See [Self::env_data] for the list of environment variables.
    pub fn display_env(self, writer: &mut impl Write) -> Result<(), std::io::Error> {
        for (key, value) in self.env_data().unwrap() {
            writeln!(writer, "export {key}={value}")?;
        }

        Ok(())
    }
}

fn write_header(
    path: impl AsRef<std::path::Path>,
    meta: Option<&str>,
) -> Result<BufWriter<File>, std::io::Error> {
    let mut writer = std::fs::File::create(&path).map(BufWriter::new)?;
    writer.write_all(b"# CipherStash configuration 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.toml` configuration file (used by stash CLI).
///
/// This is a builder for the `cipherstash.toml` configuration file.
///
/// # Config settings
///
/// The following settings are available:
/// - `client_id`: The client ID to use.
/// - `default_keyset_id`: The default keyset ID to use.
/// - `workspace_crn`: The workspace CRN to use.
/// - `workspace_id`: The workspace ID to use.
/// - `region`: The region to use.
/// - `meta`: A comment to add to the top of the configuration file.
///
/// ## Specifying the workspace
///
/// The workspace can be specified by either a [Crn] or a [WorkspaceId] and [Region].
/// The [Crn] is the preferred method.
/// Note that regions must include the cloud provider (e.g. `us-west-1.aws`).
///
#[derive(Default)]
pub struct CipherStashConfigFileBuilder {
    client_id: Option<String>,
    default_keyset_id: Option<String>,
    workspace_crn: Option<Crn>,
    workspace_id: Option<WorkspaceId>,
    region: Option<Region>,
    meta: Option<String>,
}

impl CipherStashConfigFileBuilder {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn with_client_id(mut self, client_id: impl Into<String>) -> Self {
        self.client_id = Some(client_id.into());
        self
    }

    pub fn with_default_keyset_id(mut self, default_keyset_id: impl Into<String>) -> Self {
        self.default_keyset_id = Some(default_keyset_id.into());
        self
    }

    pub fn with_workspace_crn(mut self, crn: Crn) -> Self {
        self.workspace_crn.replace(crn);
        self
    }

    pub fn with_workspace_id(mut self, workspace_id: WorkspaceId) -> Self {
        self.workspace_id.replace(workspace_id);
        self
    }

    pub fn with_region(mut self, region: Region) -> Self {
        self.region.replace(region);
        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 a [CipherStashConfigFile] from the current state of the builder.
    /// It will error if any required fields are missing (specifically `client_id` and `workspace_crn`).
    pub fn try_build(self) -> Result<CipherStashConfigFile, ConfigError> {
        let auth = NonSensitiveAuthSection {
            workspace_id: self.workspace_id,
            region: self.region,
            workspace_crn: self.workspace_crn,
            access_key: Disallowed(None),
        };

        self.client_id
            .ok_or(ConfigError::invalid("Missing workspace_id"))
            .map(|client_id| CipherStashConfigFile {
                encrypt: NonSensitiveEncryptSection {
                    client_id,
                    default_keyset_id: self.default_keyset_id,
                    client_key: Disallowed(None),
                },
                auth,
                meta: self.meta,
            })
    }
}

impl IntoFileSource for CipherStashConfigFile {
    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 `cipherstash.toml` configuration file.
#[derive(Debug, Deserialize, Serialize)]
struct NonSensitiveEncryptSection {
    client_id: String,
    default_keyset_id: Option<String>,

    /// Client key must not be stored in this file.
    client_key: Disallowed<String>,
}

/// Represents the auth section of the `cipherstash.toml` configuration file.
#[derive(Debug, Deserialize, Serialize)]
pub(super) struct NonSensitiveAuthSection {
    /// The workspace can be specified by either a [Crn] or a [WorkspaceId] and [Region].
    workspace_id: Option<WorkspaceId>,
    region: Option<Region>,
    workspace_crn: Option<Crn>,

    /// Access key must not be stored in this file.
    access_key: Disallowed<String>,
}

impl NonSensitiveAuthSection {
    /// Constructor for other types within the module.
    /// We only want other types to be able to specify the workspace CRN.
    pub(super) fn new(workspace_crn: Crn) -> Self {
        Self {
            workspace_id: None,
            region: None,
            workspace_crn: Some(workspace_crn),
            access_key: Disallowed(None),
        }
    }

    pub(super) fn set_workspace_crn(&mut self, workspace_crn: Crn) {
        self.workspace_crn = Some(workspace_crn);
    }

    pub(super) fn workspace_crn(&self) -> Option<Crn> {
        self.workspace_crn.clone()
    }
}

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

    // Fixture for the encrypt section of the cipherstash.toml file
    static ENCRYPT_TOML: &str = r#"
        client_id = "1792d1b2-ae14-4d49-ba26-5dcc5a36a716"
        default_keyset_id = "1792d1b2-ae14-4d49-ba26-5dcc5a36a716"
    "#;

    // Fixture for the auth section of the cipherstash.toml file using a separate workspace ID and region
    static AUTH_TOML_EXPLICIT: &str = r#"
        workspace_id = "R7MMSPNCFRY7ZBNH"
        region = "us-west-1.aws"
    "#;

    // Fixture for the auth section of the cipherstash.toml file using a CRN
    static AUTH_TOML_CRN: &str = r#"
        workspace_crn = "crn:us-west-1.aws:R7MMSPNCFRY7ZBNH"
    "#;

    fn cipherstash_toml(encrypt: &str, auth: &str) -> String {
        format!(
            r#"
            [encrypt]
            {encrypt}

            [auth]
            {auth}
        "#,
        )
    }

    fn assert_encrypt_section_valid(input: &str) -> CipherStashConfigFile {
        let config: CipherStashConfigFile = toml::from_str(input).unwrap();
        assert_eq!(
            config.encrypt.client_id,
            "1792d1b2-ae14-4d49-ba26-5dcc5a36a716"
        );
        assert_eq!(
            config.encrypt.default_keyset_id,
            Some("1792d1b2-ae14-4d49-ba26-5dcc5a36a716".to_string())
        );
        config
    }

    #[test]
    fn deserialize_cipherstash_config_file_explicit() {
        let toml = cipherstash_toml(ENCRYPT_TOML, AUTH_TOML_EXPLICIT);
        let config = assert_encrypt_section_valid(&toml);

        assert_eq!(
            config.auth.workspace_id,
            WorkspaceId::try_from("R7MMSPNCFRY7ZBNH").ok()
        );
        assert_eq!(config.auth.region, Region::new("us-west-1.aws").ok());
        assert!(
            config.auth.workspace_crn.is_none(),
            "Workspace CRN should be None when using workspace_id and region"
        );
    }

    #[test]
    fn deserialize_cipherstash_config_file_crn() {
        let toml = cipherstash_toml(ENCRYPT_TOML, AUTH_TOML_CRN);
        let config = assert_encrypt_section_valid(&toml);

        assert_eq!(
            config.auth.workspace_crn,
            Crn::try_from("crn:us-west-1.aws:R7MMSPNCFRY7ZBNH").ok()
        );

        assert!(
            config.auth.workspace_id.is_none(),
            "Workspace ID should be None when using workspace_crn"
        );
        assert!(
            config.auth.region.is_none(),
            "Region should be None when using workspace_crn"
        );
    }

    #[test]
    fn deserialize_cipherstash_config_file_disallowed_client_key() {
        let toml = r#"
            [encrypt]
            client_id = "1792d1b2-ae14-4d49-ba26-5dcc5a36a716"
            default_keyset_id = "1792d1b2-ae14-4d49-ba26-5dcc5a36a716"
            client_key = "a4627031a16b7065721d75746174611f6e900e080b0f0a0d060c04112301020905076770325f66726f6da16b7065726d75746174696f6e900902000e0103050807060c0a0f0b0d046570325f746fa16b7065726d75746174696f6e90080403020500070f0b0e060d0a01090c627033a16b7065726d75746174696f6e982116100e12181f181b040a070b181e18191501050c111400181a0d181809030f1702181d0813182006181c"
        "#;
        let result = toml::from_str::<CipherStashConfigFile>(toml);
        assert!(result.is_err());
    }

    #[test]
    fn serialize_cipherstash_config_file() {
        let config = CipherStashConfigFileBuilder::new()
            .with_client_id("1792d1b2-ae14-4d49-ba26-5dcc5a36a716")
            .with_default_keyset_id("1792d1b2-ae14-4d49-ba26-5dcc5a36a716")
            .with_workspace_crn(Crn::try_from("crn:us-west-1.aws:E4UMRN47WJNSMAKR").unwrap())
            .try_build()
            .unwrap();

        let toml = toml::to_string(&config).unwrap();
        assert_eq!(
            toml,
            r#"[encrypt]
client_id = "1792d1b2-ae14-4d49-ba26-5dcc5a36a716"
default_keyset_id = "1792d1b2-ae14-4d49-ba26-5dcc5a36a716"

[auth]
workspace_crn = "crn:us-west-1.aws:E4UMRN47WJNSMAKR"
"#
        );
    }
}
