//! `sopass` configuration handling.
//!
//! [`Config`] holds the run-time configuration, computed from
//! defaults, plus a configuration file, plus command line options.

use std::path::{Path, PathBuf};

use clingwrap::config::{ConfigFile, ConfigLoader, ConfigValidator};
use log::debug;
use serde::{Deserialize, Serialize};

use crate::{sop::Sop, DEFAULT_CERT_FILENAME, DEFAULT_KEY_FILENAME};

const DEFAULT_SOP: &str = "rsop";

/// Build a run time configuration from defaults, configuration file,
/// and command line options.
#[derive(Default, Debug)]
pub struct ConfigBuilder {
    defaults: File,
    overrides: File,
    filename: Option<PathBuf>,
}

impl ConfigBuilder {
    /// Create a new builder. The `app` is used to compute the default
    /// location of the configuration file. `default_database` is the
    /// default location of the database, unless overridden by
    /// configuration file or command line option.
    pub fn new(_app: &'static str, default_database: &Path) -> Self {
        Self {
            defaults: File::new(default_database, Path::new(DEFAULT_SOP)),
            ..Default::default()
        }
    }

    /// Build a new run time configuration. This can fail, because it
    /// may read the configuration file.
    pub fn build(self) -> Result<Config, ConfigError> {
        debug!("building configuration from {self:#?}");

        let mut loader = ConfigLoader::default();

        if let Some(filename) = &self.filename {
            loader.require_yaml(filename);
        }

        loader
            .load(Some(self.defaults), Some(self.overrides), &File::default())
            .map_err(ConfigError::Load)
    }

    /// Read configuration from this file.
    pub fn filename(&mut self, filename: &Path) {
        self.filename = Some(filename.into());
    }

    /// Use this value for the database location. This overrides the
    /// value in the configuration file.
    pub fn database(&mut self, database: &Path) {
        self.overrides.database = Some(database.into());
    }

    /// Use this value for the SOP implementation. This overrides the
    /// value in the configuration file.
    pub fn sop(&mut self, sop: &Path) {
        self.overrides.sop = Some(sop.into());
    }

    /// Use this value for the SOP implementation for decryption. This
    /// overrides the value in the configuration file.
    pub fn sop_decrypt(&mut self, sop: &Path) {
        self.overrides.sop_decrypt = Some(sop.into());
    }
}

/// The valid, merged run time configuration.
#[derive(Debug, Deserialize, Serialize)]
pub struct Config {
    database: PathBuf,
    key_file: PathBuf,
    cert_file: PathBuf,
    sop: PathBuf,
    sop_decrypt: PathBuf,
}

impl Config {
    fn new(database: &Path, sop: &Path, sop_decrypt: &Path) -> Self {
        Self {
            database: database.into(),
            key_file: database.join(DEFAULT_KEY_FILENAME),
            cert_file: database.join(DEFAULT_CERT_FILENAME),
            sop: sop.into(),
            sop_decrypt: sop_decrypt.into(),
        }
    }

    /// The location of the database.
    pub fn database(&self) -> &Path {
        &self.database
    }

    /// The a way to use configured SOP implementation.
    pub fn sop(&self) -> Sop {
        if self.sop != self.sop_decrypt {
            Sop::hardware_key(&self.sop, &self.sop_decrypt, &self.cert_file)
        } else {
            Sop::software_key(&self.sop, &self.key_file)
        }
    }

    /// Format the configuration into pretty JSON.
    pub fn pretty_json(&self) -> Result<String, ConfigError> {
        serde_json::to_string_pretty(self).map_err(ConfigError::Json)
    }
}

/// A representation of the configuration file.
///
/// We don't set the default values for database and sop here. The database
/// default needs to be computed at run time, and that can fail. The
/// sop default is static, but it seems easier to deal with both values
/// in the same way, at least for now.
#[derive(Default, Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct File {
    database: Option<PathBuf>,
    sop: Option<PathBuf>,
    sop_decrypt: Option<PathBuf>,
}

impl File {
    fn new(database: &Path, sop: &Path) -> Self {
        Self {
            database: Some(database.to_path_buf()),
            sop: Some(sop.to_path_buf()),
            ..Default::default()
        }
    }
}

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

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

// Validate a merged set of config files.
impl ConfigValidator for File {
    type File = File;
    type Valid = Config;
    type Error = ConfigError;

    fn validate(&self, config: &Self::File) -> Result<Self::Valid, Self::Error> {
        let database = config
            .database
            .as_deref()
            .ok_or(ConfigError::Missing("database"))?;
        let sop = config.sop.as_deref().ok_or(ConfigError::Missing("sop"))?;

        let sop_decrypt = config
            .sop_decrypt
            .as_deref()
            .unwrap_or(config.sop_decrypt.as_deref().unwrap_or(sop));

        Ok(Config::new(database, sop, sop_decrypt))
    }
}

/// Everything that can go wrong in this module.
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
    /// Can't load configuration file.
    #[error("failed to load configuration file {0}")]
    Load(#[source] clingwrap::config::ConfigError),

    /// Configuration field is missing, even the default. This is a
    /// programming error.
    #[error("configuration does not specify field {0}")]
    Missing(&'static str),

    /// Can't turn configuration into JSON.
    #[error("failed to serialize configuration as JSON")]
    Json(#[source] serde_json::Error),

    /// Can't validate config.
    #[error("run time configuration is not valid")]
    Validate(#[source] clingwrap::config::ConfigError),
}
