//! A client chunk.
//!
//! The client chunk represents a backup client: a specific
//! configuration for a backup of a specific computer. Two different
//! computers will always be different clients, but two different
//! backups of the same computer to different backup repositories are
//! also different clients.

use std::collections::HashMap;

use serde::{Deserialize, Serialize};

use crate::{
    cipher::Key,
    id::Id,
    plaintext::{Plaintext, PlaintextError},
};

/// A client chunk.
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
pub struct ClientChunk {
    name: String,
    keys: HashMap<String, Key>,
    old_versions: Vec<Id>,
}

impl ClientChunk {
    /// Create a new client chunk.
    pub fn new(name: &str) -> Self {
        Self {
            name: name.to_string(),
            keys: HashMap::new(),
            old_versions: vec![],
        }
    }

    /// Name of this client.
    pub fn name(&self) -> &str {
        &self.name
    }

    /// Names of keys stored for this client.
    pub fn key_names(&self) -> impl Iterator<Item = &str> {
        self.keys.keys().map(|k| k.as_str())
    }

    /// Get key given its name, if it exists.
    pub fn key(&self, name: &str) -> Option<&Key> {
        self.keys.get(name)
    }

    /// IDs of old versions of this client.
    pub fn old_versions(&self) -> &[Id] {
        &self.old_versions
    }

    /// Add IDs for old versions of the client to this version of the chunk.
    pub fn add_old_versions(&mut self, old: &[Id]) {
        self.old_versions.extend_from_slice(old);
    }

    /// Generate a new key and store it in the client chunk under a given name.
    pub fn generate_key(&mut self, name: &str) -> Result<Key, ClientChunkError> {
        if self.keys.contains_key(name) {
            Err(ClientChunkError::KeyExists(name.to_string()))
        } else {
            let key = Key::default();
            self.keys.insert(name.to_string(), key.clone());
            Ok(key)
        }
    }

    /// Serialize client chunk into a [`Plaintext`] so it can be
    /// encrypted as a chunk.
    pub fn to_plaintext(&self) -> Result<Plaintext, ClientChunkError> {
        let data = postcard::to_allocvec(&self).map_err(ClientChunkError::Ser)?;
        Ok(Plaintext::uncompressed(&data))
    }
}

impl TryFrom<&Plaintext> for ClientChunk {
    type Error = ClientChunkError;

    fn try_from(plaintext: &Plaintext) -> Result<Self, Self::Error> {
        let data = plaintext
            .as_bytes()
            .map_err(ClientChunkError::PlaintextDe)?;
        postcard::from_bytes(&data).map_err(ClientChunkError::De)
    }
}

/// Errors from handling client chunks.
#[derive(Debug, thiserror::Error)]
pub enum ClientChunkError {
    /// Client already has a key with the name.
    #[error("client already has a key named {0}")]
    KeyExists(String),

    /// Can't serialize with postcard.
    #[error("failed to serialize client chunk with postcard")]
    Ser(#[source] postcard::Error),

    /// Can't de-serialize with postcard.
    #[error("failed to de-serialize client chunk with postcard")]
    De(#[source] postcard::Error),

    /// Can't de-serialize [`Plaintext`].
    #[error("failed to de-serialize client chunk plaintext")]
    PlaintextDe(#[source] PlaintextError),
}

#[cfg(test)]
mod test {
    use crate::id::IdFactory;

    use super::*;

    #[test]
    fn has_name() {
        let client = ClientChunk::new("laptop");
        assert_eq!(client.name(), "laptop");
    }

    #[test]
    fn has_no_keys_initially() {
        let client = ClientChunk::new("laptop");
        assert_eq!(client.key_names().count(), 0);
    }

    #[test]
    fn gets_correct_key() {
        let key = Key::from("secret");
        let client = ClientChunk {
            name: "laptop".to_string(),
            keys: HashMap::from([("foo".to_string(), key.clone())]),
            old_versions: vec![],
        };
        assert_eq!(client.key("bar"), None);
        assert_eq!(client.key("foo"), Some(&key));
    }

    #[test]
    fn generates_key() {
        let mut client = ClientChunk::new("laptop");
        client.generate_key("primary").unwrap();
        assert_eq!(client.key_names().count(), 1);
        assert!(client.key("primary").is_some());
    }

    #[test]
    fn serialiazion_round_trip() {
        let mut client = ClientChunk::new("laptop");
        client.generate_key("primary").unwrap();
        let plaintext = client.to_plaintext().unwrap();
        let de = ClientChunk::try_from(&plaintext).unwrap();
        assert_eq!(client, de);
    }

    #[test]
    fn sets_old_versions() {
        let mut client = ClientChunk::new("laptop");
        let mut factory = IdFactory::default();
        let id = factory.id();
        client.add_old_versions(std::slice::from_ref(&id));
        assert_eq!(client.old_versions(), &[id]);
    }
}
