//! Password database.
//!
//! This module represents the password database and allows managing it.

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

use log::info;
use serde::{Deserialize, Serialize};

use crate::{
    sop::{Certificate, Sop, SopError},
    DEFAULT_CERT_FILENAME, DEFAULT_KEY_FILENAME, DEFAULT_VALUES_FILENAME,
};

/// The password database.
///
/// This is the in-memory representation of the cleartext password
/// database. It can be updated via methods, and if modified, needs to be
/// explicitly written back to disk. Note that this module does not
/// decrypt or encrypt or do file I/O, and that those are left to the
/// caller.
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Database {
    #[serde(default)]
    certs: HashMap<String, Vec<u8>>,
    kv: HashMap<String, Vec<u8>>,
}

impl Database {
    /// Add a new certificate to the database and give the certificate a
    /// name. The name can be used to access or delete the
    /// certificate. If the name of an existing certificate is used,
    /// the certificate is overwritten with a new one. When the database
    /// is encrypted, it should be encrypted with all the certificates
    /// in the database. encrypted to
    pub fn push_cert(&mut self, name: &str, cert: &Certificate) {
        self.certs.insert(name.into(), cert.as_bytes().to_vec());
    }

    /// Return iterator over all the names of certificates in the
    /// database.
    pub fn cert_names(&self) -> impl Iterator<Item = &str> {
        self.certs.keys().map(|k| k.as_str())
    }

    /// Get a named certificate if one exists.
    pub fn get_cert(&self, name: &str) -> Option<Certificate> {
        self.certs.get(name).map(|c| Certificate::new(c.to_vec()))
    }

    /// Remove a named certificate. It's OK if one doesn't exist.
    pub fn remove_cert(&mut self, name: &str) -> Result<(), DatabaseError> {
        if !self.certs.contains_key(name) {
            Err(DatabaseError::NoSuchCertificate(name.into()))
        } else if self.certs.len() == 1 {
            Err(DatabaseError::MustKeepOnlyCertificate)
        } else {
            self.certs.remove(name);
            Ok(())
        }
    }

    fn certs(&self) -> Vec<Certificate> {
        self.certs
            .values()
            .map(|c| Certificate::new(c.to_vec()))
            .collect()
    }

    /// Construct a [`Database`] from cleartext bytes that have been read
    /// from a file.
    pub fn from_bytes(filename: &Path, data: &[u8]) -> Result<Self, DatabaseError> {
        serde_json::from_slice(data).map_err(|err| DatabaseError::Parse(filename.into(), err))
    }

    /// Serialize a [`Database`] as bytes, to be encrypted and written to
    /// disk.
    pub fn to_bytes(&self) -> Result<Vec<u8>, DatabaseError> {
        serde_json::to_vec(&self).map_err(DatabaseError::ToJson)
    }

    /// Insert a value (secret) into the database under a given name.
    pub fn insert(&mut self, name: &str, value: &[u8]) {
        self.kv.insert(name.into(), value.into());
    }

    /// Iterate over pairs of value names and values.
    pub fn iter(&self) -> impl Iterator<Item = (&str, &[u8])> {
        self.kv.iter().map(|(k, v)| (k.as_str(), v.as_slice()))
    }

    /// Get value with a given name, if any.
    pub fn get(&self, name: &str) -> Option<&[u8]> {
        self.kv.get(name).map(|v| v.as_slice())
    }

    /// Remove a value with a given name. It's OK if no such value exists.
    pub fn remove(&mut self, name: &str) {
        self.kv.remove(name);
    }

    /// Rename a value. The value must exist, and no value with the
    /// new name may exist.
    pub fn rename(&mut self, old: &str, new: &str) -> Result<(), DatabaseError> {
        if let Some(value) = self.kv.remove(old) {
            if self.kv.contains_key(new) {
                Err(DatabaseError::AlreadyExists(old.into(), new.into()))
            } else {
                self.kv.insert(new.into(), value);
                Ok(())
            }
        } else {
            Err(DatabaseError::DoesNotExist(old.into()))
        }
    }

    /// Import certificates and values from another database.
    pub fn import(&mut self, other: &Self) {
        for (name, cert) in other.certs.iter() {
            self.certs.insert(name.to_string(), cert.to_vec());
        }
        for (key, value) in other.kv.iter() {
            self.kv.insert(key.to_string(), value.to_vec());
        }
    }
}

/// Possible errors from using a [`Database`].
#[derive(Debug, thiserror::Error)]
pub enum DatabaseError {
    /// Database is not JSON.
    #[error("failed to parse database file {0} as JSON")]
    Parse(PathBuf, #[source] serde_json::Error),

    /// Can't make JSON from [`Database`].
    #[error("failed to serialize file as JSON")]
    ToJson(#[source] serde_json::Error),

    /// No value with a given name.
    #[error("can't rename value {0}: it does not exist")]
    DoesNotExist(String),

    /// Value with a given name already exists.
    #[error("can't rename value {0} to {1}: it does not exist")]
    AlreadyExists(String, String),

    /// Certificate doesn't exist.
    #[error("database does not have a certificate named {0}")]
    NoSuchCertificate(String),

    /// Can't remove only certificate.
    #[error("can't remove only certificate in database")]
    MustKeepOnlyCertificate,
}

/// An encrypted [`Database`].
///
/// This is the encrypted version of a [`Database`]. It is meant for
/// loading a database from disk and decrypting it, or encrypting a database
/// and writing it to disk.
pub struct EncryptedDatabase {
    values: PathBuf,
    key_file: PathBuf,
    cert_file: PathBuf,
    sop: Sop,
}

impl EncryptedDatabase {
    /// Create a new encrypted database.
    pub fn new(sop: Sop, dirname: &Path) -> Self {
        Self {
            values: dirname.join(DEFAULT_VALUES_FILENAME),
            key_file: dirname.join(DEFAULT_KEY_FILENAME),
            cert_file: dirname.join(DEFAULT_CERT_FILENAME),
            sop,
        }
    }

    /// Install a new private key on this device.
    pub fn install_key(&self, key_filename: &Path) -> Result<(), EncryptedDatabaseError> {
        copy_file(key_filename, &self.key_file)?;
        Ok(())
    }

    /// Install a new certificate identifying a private key on this
    /// device. The private key is assumed to be on an OpenPGP card.
    pub fn install_cert(&self, cert_filename: &Path) -> Result<(), EncryptedDatabaseError> {
        copy_file(cert_filename, &self.cert_file)?;
        Ok(())
    }

    /// Read a database from a file and decrypt it.
    pub fn read_database(&self) -> Result<Database, EncryptedDatabaseError> {
        info!("read and decrypt {}", self.values.display());
        let data = std::fs::read(&self.values)
            .map_err(|err| EncryptedDatabaseError::Load(self.values.clone(), err))?;
        let cleartext = self
            .sop
            .decrypt(data)
            .map_err(EncryptedDatabaseError::Sop)?;
        Database::from_bytes(&self.values, &cleartext).map_err(EncryptedDatabaseError::Database)
    }

    /// Encrypt a database and write it to a file.
    pub fn write_database(&self, db: &Database) -> Result<(), EncryptedDatabaseError> {
        let data = db.to_bytes()?;
        self.sop
            .encrypt(data, &db.certs(), &self.values)
            .map_err(EncryptedDatabaseError::Sop)?;
        Ok(())
    }

    /// Extract the certificate from the private key on this device.
    pub fn extract_cert(&self) -> Result<Certificate, EncryptedDatabaseError> {
        self.sop.extract_cert().map_err(EncryptedDatabaseError::Sop)
    }
}

fn copy_file(input: &Path, db_key: &Path) -> Result<(), EncryptedDatabaseError> {
    info!("copy file {} to {}", input.display(), db_key.display());
    std::fs::copy(input, db_key)
        .map_err(|err| EncryptedDatabaseError::CopyFile(input.into(), err))?;
    Ok(())
}

/// Possible errors from using [`EncryptedDatabase`].
#[derive(Debug, thiserror::Error)]
pub enum EncryptedDatabaseError {
    /// Can't copy private key file into database.
    #[error("failed to copy file {0} to database")]
    CopyFile(PathBuf, #[source] std::io::Error),

    /// Can't run SOP.
    #[error("SOP implementation failed")]
    Sop(#[source] SopError),

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

    /// An error from [`Database`].
    #[error(transparent)]
    Database(#[from] DatabaseError),
}
