//! SSH CA tool configuration handling.

use clingwrap::config::{ConfigFile, ConfigLoader, ConfigValidator};
use directories_next::ProjectDirs;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

const APPLICATION: &str = "sshca";
const DEFAULT_PASS_KEY: &str = "sshca.store";

/// Representation of SSH CA tool configuration.
#[derive(Debug, Serialize)]
pub struct Config {
    /// Path to file in which keys and other things are store.
    pub store: PathBuf,

    /// How is the store stored persistently?
    pub how: StoreHow,

    /// Name (key) for the sopass command for persisting the key store.
    pub pass_key: String,
}

/// Supported ways how to persist the store.
#[derive(Debug, Default, Copy, Clone, Serialize, Deserialize)]
pub enum StoreHow {
    /// Store in a cleartext file.
    #[default]
    #[serde(rename = "file")]
    File,

    /// Store using the [`sopass`](https://sopass.liw.fi/) password
    /// manager, instead of an un-encrypted, cleartext file.
    #[serde(rename = "sopass")]
    Pass,
}

#[derive(Default, Clone, Deserialize, Serialize)]
struct File {
    store: Option<PathBuf>,
    how: Option<StoreHow>,
    pass: Option<String>,
}

impl<'a> ConfigFile<'a> for File {
    type Error = ConfigError;

    fn merge(&mut self, file: File) -> Result<(), Self::Error> {
        if let Some(v) = &file.store {
            self.store = Some(v.into());
        }
        if let Some(v) = &file.how {
            self.how = Some(*v);
        }
        if let Some(v) = &file.pass {
            self.pass = Some(v.into());
        }
        Ok(())
    }
}

impl ConfigValidator for File {
    type File = File;
    type Valid = Config;
    type Error = ConfigError;

    fn validate(&self, runtime: &Self::File) -> Result<Self::Valid, Self::Error> {
        Ok(Config {
            store: runtime
                .store
                .as_deref()
                .ok_or(ConfigError::Missing("store"))?
                .to_path_buf(),
            how: runtime.how.ok_or(ConfigError::Missing("how"))?,
            pass_key: runtime
                .pass
                .as_deref()
                .ok_or(ConfigError::Missing("pass"))?
                .to_string(),
        })
    }
}

/// Build a [`Config`].
pub struct ConfigBuilder {
    defaults: File,
    overrides: File,
    loader: ConfigLoader,
}

impl ConfigBuilder {
    /// Create a new configuration builder.
    pub fn new(filename: &Option<PathBuf>) -> Result<Self, ConfigError> {
        let mut loader = ConfigLoader::default();
        loader.xdg("", "", APPLICATION);
        if let Some(filename) = filename {
            loader.allow_yaml(filename);
        }
        Ok(Self {
            loader,
            defaults: File {
                store: Some(dirs().data_dir().join("store.yaml")),
                how: Some(StoreHow::default()),
                pass: Some(DEFAULT_PASS_KEY.to_string()),
            },
            overrides: File::default(),
        })
    }

    /// Create a new [`Config`] by loading files and validating them.
    pub fn build(&mut self) -> Result<Config, ConfigError> {
        let valid = self
            .loader
            .load(
                Some(self.defaults.clone()),
                Some(self.overrides.clone()),
                &File::default(),
            )
            .map_err(ConfigError::Load)?;
        Ok(valid)
    }

    /// Set store file.
    pub fn store(&mut self, store: &Option<PathBuf>) -> &Self {
        if let Some(v) = store {
            self.overrides.store = Some(v.to_path_buf());
        }
        self
    }
}

fn dirs() -> ProjectDirs {
    if let Some(dirs) = ProjectDirs::from("", "", APPLICATION) {
        dirs
    } else {
        panic!("can't figure out the configuration directory");
    }
}

/// Errors from the `config` module.
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
    /// Can't load configuration file.
    #[error("failed to load configuration from file")]
    Load(#[source] clingwrap::config::ConfigError),

    /// Config is invalid.
    #[error("failed to validate configuration")]
    Validate(#[source] clingwrap::config::ConfigError),

    /// Field is not set.
    #[error("configuration value {0} is not set")]
    Missing(&'static str),
}
