//! Virtual machine specification.

use crate::config::Configuration;
use crate::util::{check_network_names, expand_tilde};

use log::debug;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};

#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct OneVmInputSpecification {
    #[serde(default)]
    pub ssh_key_files: Option<Vec<PathBuf>>,

    pub rsa_host_key: Option<String>,
    pub rsa_host_cert: Option<String>,
    pub dsa_host_key: Option<String>,
    pub dsa_host_cert: Option<String>,
    pub ecdsa_host_key: Option<String>,
    pub ecdsa_host_cert: Option<String>,
    pub ed25519_host_key: Option<String>,
    pub ed25519_host_cert: Option<String>,

    pub base: Option<PathBuf>,
    pub image: Option<PathBuf>,
    pub image_size_gib: Option<u64>,
    pub memory_mib: Option<u64>,
    pub cpus: Option<u64>,
    pub generate_host_certificate: Option<bool>,
    pub autostart: Option<bool>,
    pub networks: Option<Vec<String>>,
    pub ca_key: Option<PathBuf>,
    pub user_ca_pubkey: Option<PathBuf>,
    pub allow_authorized_keys: Option<bool>,
}

impl OneVmInputSpecification {
    fn ssh_key_files(&self, config: &Configuration, name: &str) -> Option<Vec<PathBuf>> {
        get(
            &self.ssh_key_files,
            &config.authorized_keys,
            SpecificationError::NoAuthorizedKeys(name.to_string()),
        )
        .ok()
    }

    fn user_ca_pubkey(&self, config: &Configuration, name: &str) -> Option<PathBuf> {
        get(
            &self.user_ca_pubkey,
            &config.user_ca_pubkey,
            SpecificationError::NoAuthorizedKeys(name.to_string()),
        )
        .ok()
    }

    fn allow_authorized_keys(&self, config: &Configuration) -> bool {
        get(
            &self.allow_authorized_keys,
            &config.default_allow_authorized_keys,
            SpecificationError::NoAuthorizedKeys("".to_string()),
        )
        .unwrap_or(true)
    }

    fn base_image(
        &self,
        config: &Configuration,
        name: &str,
    ) -> Result<PathBuf, SpecificationError> {
        get(
            &self.base,
            &config.default_base_image,
            SpecificationError::NoBaseImage(name.to_string()),
        )
    }

    fn image(&self, config: &Configuration, name: &str) -> Result<PathBuf, SpecificationError> {
        let default_image = &config
            .image_directory
            .as_ref()
            .map(|dirname| dirname.join(format!("{name}.qcow2")));

        get(
            &self.image,
            default_image,
            SpecificationError::NoImage(name.to_string()),
        )
    }

    fn image_size_gib(
        &self,
        config: &Configuration,
        name: &str,
    ) -> Result<u64, SpecificationError> {
        get(
            &self.image_size_gib,
            &config.default_image_gib,
            SpecificationError::NoBaseImage(name.to_string()),
        )
    }

    fn memory_mib(&self, config: &Configuration, name: &str) -> Result<u64, SpecificationError> {
        get(
            &self.memory_mib,
            &config.default_memory_mib,
            SpecificationError::NoBaseImage(name.to_string()),
        )
    }

    fn cpus(&self, config: &Configuration, name: &str) -> Result<u64, SpecificationError> {
        get(
            &self.cpus,
            &config.default_cpus,
            SpecificationError::NoBaseImage(name.to_string()),
        )
    }

    fn autostart(&self, config: &Configuration) -> bool {
        if let Some(x) = self.autostart {
            x
        } else {
            config.default_autostart.unwrap_or_default()
        }
    }

    fn networks(&self, config: &Configuration) -> Vec<String> {
        if let Some(ref x) = self.networks {
            x.clone()
        } else if let Some(ref x) = config.default_networks {
            x.clone()
        } else {
            vec!["default".to_string()]
        }
    }
}

fn get<'a, T>(
    input: &'a Option<T>,
    default: &'a Option<T>,
    error: SpecificationError,
) -> Result<T, SpecificationError>
where
    T: Clone,
{
    if let Some(input) = input {
        Ok((*input).clone())
    } else if let Some(default) = default {
        Ok((*default).clone())
    } else {
        Err(error)
    }
}

/// Effective virtual machine specification.
///
/// This is the specification as read from the input file, with the
/// defaults from the configuration file already applied.
#[derive(Debug, Serialize)]
pub struct Specification {
    /// Name of new virtual machine to create.
    pub name: String,

    /// SSH public keys to install in the default user's `authorized_keys` file.
    pub ssh_keys: Vec<String>,

    /// RSA host key to install in new VM.
    pub rsa_host_key: Option<String>,

    /// RSA host certificate.
    pub rsa_host_cert: Option<String>,

    /// DSA host key to install in new VM.
    pub dsa_host_key: Option<String>,

    /// DSA host certificate.
    pub dsa_host_cert: Option<String>,

    /// ECDSA host key to install in new VM.
    pub ecdsa_host_key: Option<String>,

    /// ECDSA host certificate.
    pub ecdsa_host_cert: Option<String>,

    /// Ed25519 host key to install in new VM.
    pub ed25519_host_key: Option<String>,

    /// Ed25519 host certificate.
    pub ed25519_host_cert: Option<String>,

    /// Path to base image.
    pub base: PathBuf,

    /// Path to new VM image, to be created.
    pub image: PathBuf,

    /// Size of new image, in GiB.
    pub image_size_gib: u64,

    /// Size of memory for new VM, in MiB.
    pub memory_mib: u64,

    /// CPUs new VM should have.
    pub cpus: u64,

    /// Should a new host key and certificate be created for new VM?
    pub generate_host_certificate: bool,

    /// Should the VM be started automatically when host starts?
    pub autostart: bool,

    /// Path to CA key for creating host certificate.
    pub ca_key: Option<PathBuf>,

    /// Path to CA publicv key for verifying user certificates.
    pub user_ca_pubkey: Option<PathBuf>,

    /// Allow SSH server to use per-user authorized keys files?
    pub allow_authorized_keys: bool,

    /// List of networks to which host should be added.
    pub networks: Vec<String>,
}

/// Errors from this module.
#[derive(Debug, thiserror::Error)]
pub enum SpecificationError {
    /// No base image file specified.
    #[error("No base image or default base image specified for {0}")]
    NoBaseImage(String),

    /// No image file specified.
    #[error("No image filename specified for {0} and no image_directory in configuration")]
    NoImage(String),

    /// No image size specified.
    #[error("No image size specified for {0} and no default configured")]
    NoImageSize(String),

    /// No memory size specified.
    #[error("No memory size specified for {0} and no default configured")]
    NoMemorySize(String),

    /// No CPU count specified.
    #[error("No CPU count specified for {0} and no default configured")]
    NoCpuCount(String),

    /// No SSH authorized keys or user CA specified.
    #[error("No SSH authorized keys nor user CA specified for {0} and no default configured")]
    NoAuthorizedKeys(String),

    /// Error reading specification file.
    #[error("Couldn't read specification file {0}")]
    Read(PathBuf, #[source] std::io::Error),

    /// Error reading SSH public key.
    #[error("Failed to read SSH public key file {0}")]
    SshKeyRead(PathBuf, #[source] std::io::Error),

    /// Network name error.
    #[error(transparent)]
    NetworkNameError(#[from] crate::util::NetworkNameError),

    /// Error parsing string as UTF8.
    #[error(transparent)]
    FromUtf8Error(#[from] std::string::FromUtf8Error),

    /// Error parsing YAML.
    #[error(transparent)]
    YamlError(#[from] serde_yaml::Error),

    /// Error expanding a ~user in a path name.
    #[error(transparent)]
    HomeDirError(#[from] home_dir::Error),
}

impl Specification {
    /// Read all specifications from a file.
    ///
    /// Apply values from the provided configuration so that the
    /// returned specifications are *effective* and the caller doesn't
    /// need to worry about the configuration anymore.
    ///
    /// Also, SSH public keys are read from the files named in the
    /// input specification.
    pub fn from_file(
        config: &Configuration,
        filename: &Path,
    ) -> Result<Vec<Specification>, SpecificationError> {
        debug!("reading specification from {}", filename.display());
        let spec = fs::read(filename)
            .map_err(|err| SpecificationError::Read(filename.to_path_buf(), err))?;
        let input: HashMap<String, OneVmInputSpecification> = serde_yaml::from_slice(&spec)?;
        debug!("specification as read from file: {:#?}", input);

        let mut machines = vec![];
        for (name, machine) in input.iter() {
            let spec = Specification::one_machine(config, name, machine)?;
            debug!("machine with defaults applied: {:#?}", spec);
            machines.push(spec);
        }

        Ok(machines)
    }

    fn one_machine(
        config: &Configuration,
        name: &str,
        input: &OneVmInputSpecification,
    ) -> Result<Specification, SpecificationError> {
        let ssh_keys = if let Some(key_filenames) = input.ssh_key_files(config, name) {
            ssh_keys(&key_filenames)?
        } else {
            vec![]
        };
        let user_ca_pubkey = input.user_ca_pubkey(config, name);
        if ssh_keys.is_empty() && user_ca_pubkey.is_none() {
            return Err(SpecificationError::NoAuthorizedKeys(name.to_string()));
        }
        let user_ca_pubkey = if let Some(filename) = user_ca_pubkey {
            Some(expand_tilde(&filename)?)
        } else {
            None
        };
        let ca_key = if let Some(filename) = &input.ca_key {
            Some(expand_tilde(filename)?)
        } else {
            config.ca_key.clone()
        };
        let gen_cert = if let Some(v) = &input.generate_host_certificate {
            *v
        } else if let Some(v) = &config.default_generate_host_certificate {
            *v
        } else {
            false
        };

        let networks = input.networks(config);
        check_network_names(&networks)?;

        let spec = Specification {
            name: name.to_string(),
            ssh_keys,
            rsa_host_key: input.rsa_host_key.clone(),
            rsa_host_cert: input.rsa_host_cert.clone(),
            dsa_host_key: input.dsa_host_key.clone(),
            dsa_host_cert: input.dsa_host_cert.clone(),
            ecdsa_host_key: input.ecdsa_host_key.clone(),
            ecdsa_host_cert: input.ecdsa_host_cert.clone(),
            ed25519_host_key: input.ed25519_host_key.clone(),
            ed25519_host_cert: input.ed25519_host_cert.clone(),
            base: expand_tilde(&input.base_image(config, name)?)?,
            image: expand_tilde(&input.image(config, name)?)?,
            image_size_gib: input.image_size_gib(config, name)?,
            memory_mib: input.memory_mib(config, name)?,
            cpus: input.cpus(config, name)?,
            generate_host_certificate: gen_cert,
            autostart: input.autostart(config),
            ca_key,
            user_ca_pubkey,
            allow_authorized_keys: input.allow_authorized_keys(config),
            networks,
        };

        debug!("specification as with defaults applied: {:#?}", spec);
        Ok(spec)
    }
}

fn ssh_keys(filenames: &[PathBuf]) -> Result<Vec<String>, SpecificationError> {
    let mut keys = vec![];
    for filename in filenames {
        let filename = expand_tilde(filename)?;
        let key =
            std::fs::read(&filename).map_err(|e| SpecificationError::SshKeyRead(filename, e))?;
        let key = String::from_utf8(key)?;
        let key = key.strip_suffix('\n').unwrap_or(&key);
        keys.push(key.to_string());
    }
    Ok(keys)
}
