//! # config-vault-source
//!
//! `config-vault-source` is an extension for the `config` crate that allows loading configurations
//! directly from HashiCorp Vault.
//!
//! Source code of `config-vault-source` is pretty similar as in parent crate `config-vault` but with support
//! of tls, async and nested configuration keys.
//!
//!
//!
//! This library implements a custom `Source` or `AsyncSource` via async feature for the `config` crate that can
//! connect to a HashiCorp Vault server and load secrets from the KV1/KV2 engines as
//! configuration values.
//!
//! ## Example
//!
//! ```
//! use config::{Config, ConfigError};
//! use config_vault::VaultSource;
//!
//! fn load_config() -> Result<Config, ConfigError> {
//!     let vault_source = VaultSource::new(
//!         "http://127.0.0.1:8200".to_string(),  // Vault address
//!         "hvs.EXAMPLE_TOKEN".to_string(),      // Vault token
//!         "secret".to_string(),                 // KV mount name
//!         "dev".to_string(),        // Secret path
//!     );
//!
//!     Config::builder()
//!         .add_source(vault_source)
//!         // You can add other sources
//!         .build()
//! }
//! ```
//!
//! If you want to use the KV1 engine, you can use the `new_v1` method instead of `new`:
//!
//! ```
//! use config_vault::VaultSource;
//!
//! let vault_source = VaultSource::new_v1(
//!         "http://127.0.0.1:8200".to_string(),  // Vault address
//!         "hvs.EXAMPLE_TOKEN".to_string(),      // Vault token
//!         "secret".to_string(),                 // KV mount name
//!         "dev".to_string(),        // Secret path
//! );
//! ```

pub mod builder;
mod utils;

pub use builder::VaultSourceBuilder;

use std::ops::Deref;
use std::str::FromStr;

use config::Source;
use config::{ConfigError, Map, Value};

#[cfg(feature = "async")]
use async_trait::async_trait;
#[cfg(feature = "async")]
use config::AsyncSource;
use url::Url;

use crate::utils::flatten_json;

/// A `Source` for the `config` library that loads configurations from HashiCorp Vault.
///
/// This source connects to a HashiCorp Vault server and loads a secret from
/// the version 2 of the KV (Key-Value) engine. The values from the secret are included
/// in the configuration as flat key-value pairs.
///
/// # Example
///
/// ```
/// use config_vault::VaultSource;
///
/// let vault = VaultSource::new(
///     "http://vault.example.com:8200".to_string(),
///     "my-token".to_string(),
///     "secret".to_string(),
///     "dev".to_string(),
/// );
/// ```
#[derive(Debug, Clone, PartialEq)]
pub struct VaultSource {
    config: VaultConfig,
    kv_version: KvVersion,
}

#[derive(Debug, Clone, PartialEq)]
pub struct VaultAddr(Url);

impl Deref for VaultAddr {
    type Target = Url;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl FromStr for VaultAddr {
    type Err = ConfigError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let url = url::Url::parse(s)
            .map_err(|e| ConfigError::Message(format!("Invalid Vault address: {e}")))?;

        if url.cannot_be_a_base() {
            return Err(ConfigError::Message(
                "Vault address cannot be a base URL".into(),
            ));
        }

        if !url.path().trim_matches('/').is_empty() {
            return Err(ConfigError::Message(
                "Vault address must not contain a path (e.g. use https://host:8200, not https://host:8200/v1)"
                    .into(),
            ));
        }

        Ok(VaultAddr(url))
    }
}

impl TryFrom<&str> for VaultAddr {
    type Error = ConfigError;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        value.parse()
    }
}

impl TryFrom<String> for VaultAddr {
    type Error = ConfigError;

    fn try_from(value: String) -> Result<Self, Self::Error> {
        value.parse()
    }
}

#[derive(Debug, Clone, PartialEq)]
pub struct VaultConfig {
    pub address: VaultAddr,
    pub token: String,
    pub mount: String,
    pub path: String,

    #[cfg(feature = "tls")]
    pub tls: Option<TlsConfig>,
}

#[cfg(feature = "tls")]
#[derive(Debug, Clone, PartialEq)]
pub struct TlsConfig {
    pub ca_cert_bytes: Option<Vec<u8>>,
    pub client_cert: Option<Vec<u8>>,
    pub client_key: Option<Vec<u8>>,
    pub danger_accept_invalid_certs: bool,
}

#[derive(Debug, Clone, PartialEq, Default)]
pub enum KvVersion {
    V1,
    #[default]
    V2,
}

impl KvVersion {
    pub fn api_path(&self, mount: &str, path: &str) -> String {
        match self {
            KvVersion::V1 => format!("v1/{}/{}", mount, path),
            KvVersion::V2 => format!("v1/{}/data/{}", mount, path),
        }
    }
}

impl VaultSource {
    pub fn builder() -> VaultSourceBuilder {
        VaultSourceBuilder::new()
    }

    fn build_blocking_client(&self) -> Result<reqwest::blocking::Client, ConfigError> {
        let mut builder = reqwest::blocking::Client::builder();

        #[cfg(feature = "tls")]
        if let Some(tls) = &self.config.tls {
            if let Some(ca_bytes) = &tls.ca_cert_bytes {
                let cert = reqwest::Certificate::from_pem(&ca_bytes)
                    .map_err(|e| ConfigError::Foreign(Box::new(e)))?;

                builder = builder.add_root_certificate(cert);
            }

            if let (Some(cert), Some(key)) = (&tls.client_cert, &tls.client_key) {
                let mut identity_bytes = cert.clone();
                identity_bytes.extend_from_slice(key);
                let identity = reqwest::Identity::from_pem(&identity_bytes)
                    .map_err(|e| ConfigError::Foreign(Box::new(e)))?;
                builder = builder.identity(identity);
            }

            if tls.danger_accept_invalid_certs {
                builder = builder.danger_accept_invalid_certs(true);
            }
        }

        builder
            .build()
            .map_err(|e| ConfigError::Foreign(Box::new(e)))
    }

    #[cfg(feature = "async")]
    fn build_async_client(&self) -> Result<reqwest::Client, ConfigError> {
        let mut builder = reqwest::Client::builder();

        #[cfg(feature = "tls")]
        if let Some(tls) = &self.config.tls {
            if let Some(ca_bytes) = &tls.ca_cert_bytes {
                let cert = reqwest::Certificate::from_pem(&ca_bytes)
                    .map_err(|e| ConfigError::Foreign(Box::new(e)))?;

                builder = builder.add_root_certificate(cert);
            }

            if let (Some(cert), Some(key)) = (&tls.client_cert, &tls.client_key) {
                let mut identity_bytes = cert.clone();
                identity_bytes.extend_from_slice(key);
                let identity = reqwest::Identity::from_pem(&identity_bytes)
                    .map_err(|e| ConfigError::Foreign(Box::new(e)))?;
                builder = builder.identity(identity);
            }

            if tls.danger_accept_invalid_certs {
                builder = builder.danger_accept_invalid_certs(true);
            }
        }

        builder
            .build()
            .map_err(|e| ConfigError::Foreign(Box::new(e)))
    }
}

impl Source for VaultSource {
    fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> {
        Box::new(self.clone())
    }

    fn collect(&self) -> Result<Map<String, Value>, ConfigError> {
        let client = self.build_blocking_client()?;
        let resp = client
            .get(self.config.address.as_str())
            .header("X-Vault-Token", &self.config.token)
            .send()
            .map_err(|e| ConfigError::Foreign(Box::new(e)))?;

        if !resp.status().is_success() {
            return Err(ConfigError::Message("Vault request failed".into()));
        }

        let raw: serde_json::Value = resp.json().map_err(|e| ConfigError::Foreign(Box::new(e)))?;
        let json_obj = raw
            .get("data")
            .and_then(|x| {
                if self.kv_version == KvVersion::V2 {
                    x.get("data")
                } else {
                    Some(x)
                }
            })
            .and_then(|x| x.as_object())
            .ok_or_else(|| ConfigError::Message("Vault response missing data".into()))?;

        let mut secret = std::collections::HashMap::new();
        flatten_json(
            "",
            &serde_json::Value::Object(json_obj.clone()),
            &mut secret,
        );
        Ok(secret)
    }
}

#[cfg(feature = "async")]
#[async_trait]
impl AsyncSource for VaultSource {
    async fn collect(&self) -> Result<Map<String, Value>, ConfigError> {
        let client = self.build_async_client()?;

        let resp = client
            .get(self.config.address.as_str())
            .header("X-Vault-Token", &self.config.token)
            .send()
            .await
            .map_err(|e| ConfigError::Foreign(Box::new(e)))?;

        if !resp.status().is_success() {
            return Err(ConfigError::Message("Vault request failed".into()));
        }

        let raw: serde_json::Value = resp
            .json()
            .await
            .map_err(|e| ConfigError::Foreign(Box::new(e)))?;
        let json_obj = raw
            .get("data")
            .and_then(|x| {
                if self.kv_version == KvVersion::V2 {
                    x.get("data")
                } else {
                    Some(x)
                }
            })
            .and_then(|x| x.as_object())
            .ok_or_else(|| ConfigError::Message("Vault response missing data".into()))?;

        let mut secret = std::collections::HashMap::new();
        flatten_json(
            "",
            &serde_json::Value::Object(json_obj.clone()),
            &mut secret,
        );
        Ok(secret)
    }
}
