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

use clingwrap::config::{ConfigError, ConfigFile, ConfigLoader, ConfigValidator};
use log::trace;
use pasetors::{
    keys::{AsymmetricKeyPair, AsymmetricPublicKey, AsymmetricSecretKey},
    version4::V4,
};
use serde::{Deserialize, Serialize};

#[derive(Debug)]
pub struct ServerConfig {
    key_pair: Option<AsymmetricKeyPair<V4>>,
    key_pair_file: PathBuf,
    listen: String,
    repository: PathBuf,
}

impl ServerConfig {
    pub fn load(filenames: &[PathBuf]) -> Result<Self, ServerConfigError> {
        trace!("load configs: {filenames:?}");
        let mut loader = ConfigLoader::default();
        if filenames.is_empty() {
            loader.allow_yaml("server.yaml");
        } else {
            for filename in filenames.iter() {
                loader.allow_yaml(filename);
            }
        }

        trace!("loader: {loader:?}");
        let mut config = loader
            .load(None, None, &File::default())
            .map_err(ServerConfigError::LoadConfig)?;

        if config.key_pair_file.exists() {
            config.key_pair = Some(KeyPairFile::load(&config.key_pair_file)?);
        }

        Ok(config)
    }

    pub fn key_pair_file(&self) -> &Path {
        &self.key_pair_file
    }

    pub fn listen(&self) -> &str {
        &self.listen
    }

    pub fn repository(&self) -> &Path {
        &self.repository
    }

    pub fn key_pair(&self) -> Result<&AsymmetricKeyPair<V4>, ServerConfigError> {
        self.key_pair
            .as_ref()
            .ok_or(ServerConfigError::KeyPairMissing)
    }
}

/// A serialized form of a key pair.
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct KeyPairFile {
    public: Vec<u8>,
    secret: Vec<u8>,
}

impl KeyPairFile {
    fn load(filename: &Path) -> Result<AsymmetricKeyPair<V4>, ServerConfigError> {
        let data = std::fs::read(filename)
            .map_err(|err| ServerConfigError::KeyPairFileRead(filename.to_path_buf(), err))?;
        let pair: Self = serde_json::from_slice(&data)
            .map_err(|err| ServerConfigError::KeyPairFileLoad(filename.to_path_buf(), err))?;

        let public = AsymmetricPublicKey::<V4>::from(&pair.public)
            .map_err(ServerConfigError::DeserPublic)?;
        let secret = AsymmetricSecretKey::<V4>::from(&pair.secret)
            .map_err(ServerConfigError::DeserSecret)?;
        Ok(AsymmetricKeyPair::<V4> { public, secret })
    }

    /// Write key pair to a named file.
    pub fn write(&self, filename: &Path) -> Result<(), ServerConfigError> {
        let data = serde_json::to_vec(self).map_err(ServerConfigError::KeyPairFileToJson)?;
        std::fs::write(filename, &data)
            .map_err(|err| ServerConfigError::KeyPairFileWrite(filename.to_path_buf(), err))?;
        Ok(())
    }
}

impl From<&AsymmetricKeyPair<V4>> for KeyPairFile {
    fn from(pair: &AsymmetricKeyPair<V4>) -> Self {
        Self {
            public: pair.public.as_bytes().to_vec(),
            secret: pair.secret.as_bytes().to_vec(),
        }
    }
}

#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq)]
struct File {
    key_pair_file: Option<PathBuf>,
    listen: Option<String>,
    repository: Option<PathBuf>,
}

impl<'a> ConfigFile<'a> for File {
    type Error = ServerConfigError;
    fn merge(&mut self, other: Self) -> Result<(), Self::Error> {
        trace!("merge {other:?}");
        if let Some(v) = other.key_pair_file {
            self.key_pair_file = Some(v);
        }
        if let Some(v) = other.listen {
            self.listen = Some(v);
        }
        if let Some(v) = other.repository {
            self.repository = Some(v);
        }
        Ok(())
    }
}

impl ConfigValidator for File {
    type File = File;
    type Valid = ServerConfig;
    type Error = ServerConfigError;
    fn validate(&self, runtime: &Self::File) -> Result<Self::Valid, Self::Error> {
        trace!("{runtime:#?}");
        Ok(ServerConfig {
            key_pair: None,
            key_pair_file: runtime
                .key_pair_file
                .clone()
                .ok_or(ServerConfigError::Missing("key_pair_file"))?,
            listen: runtime
                .listen
                .clone()
                .ok_or(ServerConfigError::Missing("server_url"))?,
            repository: runtime
                .repository
                .clone()
                .ok_or(ServerConfigError::Missing("repository"))?,
        })
    }
}

#[derive(Debug, thiserror::Error)]
pub enum ServerConfigError {
    /// Can't load configuration files.
    #[error("failed to load configuration files")]
    LoadConfig(#[source] ConfigError),

    /// Configuration value is misssing.
    #[error("configuration is missing value {0:?}")]
    Missing(&'static str),

    /// Can't deserialize public key.
    #[error("failed to deserialize PASETO public key")]
    DeserPublic(#[source] pasetors::errors::Error),

    /// Can't deserialize secret key.
    #[error("failed to deserialize PASETO secret key")]
    DeserSecret(#[source] pasetors::errors::Error),

    /// Caller needs key pair, but it's not in config.
    #[error("configuration does not have a token signing key pair")]
    KeyPairMissing,

    /// Can't read key pair file.
    #[error("failed to load key pair file {0}")]
    KeyPairFileRead(PathBuf, #[source] std::io::Error),

    /// Can't parse key pair file.
    #[error("failed to understand key pair file {0}")]
    KeyPairFileLoad(PathBuf, #[source] serde_json::Error),

    /// Can't serialize key pair file to JSON.
    #[error("failed to serialize key pair to JSON")]
    KeyPairFileToJson(#[source] serde_json::Error),

    /// Can't write key pair file.
    #[error("can't init: failed to write key pair file {0}")]
    KeyPairFileWrite(PathBuf, #[source] std::io::Error),
}

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

    #[test]
    fn merges() {
        let mut file = File::default();
        let other = File {
            key_pair_file: Some(PathBuf::from("keypair.json")),
            listen: Some("http://localhost".to_string()),
            repository: Some(PathBuf::from("/srv/backups")),
        };

        file.merge(other.clone()).unwrap();
        assert_eq!(file, other);
    }
}
