//! Represent chunks of file data for backups.
//!
//! A `Chunk` is an in-memory representation of some data that forms
//! part of a backup. The chunk is always encrypted, but it's possible
//! to access the plain text data by decrypting it.
//!
//! A chunk always has some associated, plain text metadata
//! ([`Metadata`]), which is stored as part of the chunk. This can be
//! accessed without decrypting the chunk.
//!
//! A chunk can be serialized for persistent storage, and constructed
//! by de-serializing. The serialized representation starts with a
//! [magic cookie](https://en.wikipedia.org/wiki/Magic_cookie) to
//! allow identifying a binary blob as a chunk, and to make it easier
//! to evolve the serialization format in the future.

use std::fmt;

use serde::{Deserialize, Serialize};

use crate::{
    cipher::Engine,
    credential::Credential,
    id::{Id, IdError, IdFactory},
    plaintext::{Plaintext, PlaintextError},
};

/// This is the first version of the format that's meant for
/// production use. It is followed by data encoded using the
/// [`postcard`](https://crates.io/crates/postcard) crate, and
/// encrypted using the
/// [`EngineKind::Aead`](`crate::cipher::EngineKind::Aead`) cipher engine.
pub const COOKIE_0: &[u8] = b"Obnam3\xFF\0";

/// Every kind of chunk supported by Obnam.
#[derive(Debug, Serialize, Deserialize)]
pub enum Chunk {
    /// A data chunk.
    Data(DataChunk),

    /// A client chunk.
    Client(DataChunk),

    /// A credential chunk.
    Credential {
        /// Metadata for this chunk.
        metadata: Metadata,

        /// The encrypted credential.
        credential: Credential,
    },
}

impl Chunk {
    /// Construct a data chunk.
    pub fn data(data: DataChunk) -> Self {
        Self::Data(data)
    }

    /// Construct a client chunk.
    pub fn client(data: DataChunk) -> Self {
        Self::Client(data)
    }

    /// Construct a credential chunk.
    pub fn credential(metadata: Metadata, credential: Credential) -> Self {
        Self::Credential {
            metadata,
            credential,
        }
    }

    /// Metadata for this chunk.
    pub fn metadata(&self) -> &Metadata {
        match self {
            Self::Client(x) => x.metadata(),
            Self::Credential { metadata, .. } => metadata,
            Self::Data(x) => x.metadata(),
        }
    }

    /// Serialize [`Chunk`] for storage.
    pub fn serialize(&self) -> Result<Vec<u8>, ChunkError> {
        let vec = COOKIE_0.to_vec();
        postcard::to_extend(self, vec).map_err(ChunkError::ChunkSer)
    }

    /// Parse a serialized form of the chunk back into a runtime value.
    pub fn parse(bytes: &[u8]) -> Result<Self, ChunkError> {
        if let Some(rest) = bytes.strip_prefix(COOKIE_0) {
            // This doesn't check for a cookie, but the decryption will.
            postcard::from_bytes(rest).map_err(ChunkError::ParseChunk)
        } else {
            Err(ChunkError::NoCookie)
        }
    }
}

/// An encrypted data chunk.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DataChunk {
    ciphertext: Vec<u8>,
    metadata: Metadata,
}

impl DataChunk {
    /// Create a new chunk by encrypting some plain text data and chunk metadata.
    pub fn encrypt(
        engine: &Engine,
        plaintext: &Plaintext,
        metadata: Metadata,
    ) -> Result<Self, ChunkError> {
        let ad = metadata.to_bytes()?;
        Ok(Self {
            ciphertext: engine
                .encrypt(&plaintext.serialize()?, &ad)
                .map_err(ChunkError::Encrypt)?,
            metadata,
        })
    }

    /// Return chunk metadata. This does not require decryption.
    pub fn metadata(&self) -> &Metadata {
        &self.metadata
    }

    /// Decrypt a chunk from ciphertext.
    pub fn decrypt(&self, engine: &Engine) -> Result<Plaintext, ChunkError> {
        let ad = self.metadata.to_bytes()?;
        let decrypted = engine
            .decrypt(&self.ciphertext, &ad)
            .map_err(ChunkError::Decrypt)?;
        Ok(Plaintext::parse(&decrypted)?)
    }
}

/// Generate metadata for chunks.
#[derive(Debug, Default)]
pub struct MetadataFactory {
    id_factory: IdFactory,
}

impl MetadataFactory {
    /// Create new metadata given a chunk ID and label.
    pub fn metadata<L: Into<Label>>(&mut self, id: Id, label: L) -> Result<Metadata, ChunkError> {
        Ok(Metadata::new(id, label.into()))
    }

    /// Create new metadata given only a chunk label. Invent a new ID.
    pub fn from_label<L: Into<Label>>(&mut self, label: L) -> Result<Metadata, ChunkError> {
        Ok(Metadata::new(self.id_factory.id(), label.into()))
    }
}

/// Metadata for a chunk.
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct Metadata {
    id: Id,
    label: Label,
}

impl Metadata {
    fn new(id: Id, label: Label) -> Self {
        Self { id, label }
    }

    /// Return id.
    pub fn id(&self) -> &Id {
        &self.id
    }

    /// Return label.
    pub fn label(&self) -> &Label {
        &self.label
    }

    /// Serialize as a byte vector.
    pub fn to_bytes(&self) -> Result<Vec<u8>, ChunkError> {
        postcard::to_allocvec(self).map_err(ChunkError::MetadataSer)
    }

    /// De-serialize from a byte vector.
    pub fn from_bytes(bytes: &[u8]) -> Result<Self, ChunkError> {
        postcard::from_bytes(bytes).map_err(ChunkError::MetadataDe)
    }
}

impl From<(&Id, &Label)> for Metadata {
    fn from((id, label): (&Id, &Label)) -> Self {
        Self {
            id: id.clone(),
            label: label.clone(),
        }
    }
}

/// A chunk label.
///
/// The label is chosen by the backup application. It is represented
/// as a binary blob. For performance reasons, it should be fairly
/// small, order of 20 or 30 bytes. It is stored in the backup
/// repository as plain text, without encryption.
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct Label {
    label: Vec<u8>,
}

impl Label {
    /// Serialize as a byte vector.
    pub fn as_bytes(&self) -> &[u8] {
        &self.label
    }
}

impl From<&String> for Label {
    fn from(label: &String) -> Self {
        Self::from(label.as_bytes())
    }
}

impl From<&str> for Label {
    fn from(label: &str) -> Self {
        Self::from(label.as_bytes())
    }
}

impl From<&[u8]> for Label {
    fn from(label: &[u8]) -> Self {
        Self::from(label.to_vec())
    }
}

impl From<Vec<u8>> for Label {
    fn from(label: Vec<u8>) -> Self {
        Self { label }
    }
}

impl fmt::Display for Label {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", String::from_utf8_lossy(&self.label))
    }
}

/// An error from the `obnam::chunk` module.
#[derive(Debug, thiserror::Error)]
pub enum ChunkError {
    /// Can't serialize Metadata.
    #[error("failed to serialize chunk metadata")]
    MetadataSer(#[source] postcard::Error),

    /// Can't de-serialize Metadata.
    #[error("failed to de-serialize chunk metadata")]
    MetadataDe(#[source] postcard::Error),

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

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

    /// Problem with [`Plaintext`].
    #[error(transparent)]
    PlaintextSer(#[from] PlaintextError),

    /// Can't parse a serialized Chunk.
    #[error("failed to de-serialize chunk")]
    ParseChunk(#[source] postcard::Error),

    /// Can't encrypt.
    #[error("failed to encrypt chunk")]
    Encrypt(#[source] crate::cipher::CipherError),

    /// Can't decrypt.
    #[error("failed to decrypt chunk")]
    Decrypt(#[source] crate::cipher::CipherError),

    /// Chunk does not start with the expected magic cookie.
    #[error("chunk does not start with magic cookie")]
    NoCookie,

    /// Can't create Id from string.
    #[error("failed to create chunk ID")]
    IdFromString(#[source] IdError),
}

#[cfg(test)]
mod test {
    use postcard::{from_bytes, to_allocvec};

    use crate::cipher::{EngineKind, Key};

    use super::*;

    #[test]
    fn label_display() {
        let label = Label::from("xyzzy");
        assert_eq!(label.to_string(), "xyzzy");
    }

    #[test]
    fn label_round_trip() {
        let label = Label::from("my-label");
        let data = to_allocvec(&label).unwrap();
        let decoded: Label = from_bytes(&data).unwrap();
        assert_eq!(label, decoded);
    }

    #[test]
    fn metadata() {
        let mut factory = MetadataFactory::default();
        let meta = factory.from_label("data-chunk").unwrap();
        assert_eq!(meta.label().to_string(), "data-chunk");
    }

    #[test]
    fn metadata_round_trip() {
        let mut factory = MetadataFactory::default();
        let meta = factory.from_label("data-chunk").unwrap();
        let ser = to_allocvec(&meta).unwrap();
        let deser = from_bytes(&ser).unwrap();
        assert_eq!(meta, deser);
    }

    #[test]
    fn chunk_round_trip() {
        let engine = Engine::new(EngineKind::Aead, Key::default());
        let plaintext = Plaintext::uncompressed("hello, world".as_bytes());
        let mut factory = MetadataFactory::default();
        let meta = factory.from_label("data-chunk").unwrap();
        let chunk = Chunk::data(DataChunk::encrypt(&engine, &plaintext, meta.clone()).unwrap());

        let ser = chunk.serialize().unwrap();
        let chunk = Chunk::parse(&ser).unwrap();

        if let Chunk::Data(data) = chunk {
            let decrypted = data.decrypt(&engine).unwrap();
            assert_eq!(plaintext, decrypted);
        } else {
            panic!("didn't get a data chunk back");
        }
    }

    #[test]
    fn ciphertext_does_not_contain_plaintext() {
        let engine = Engine::new(EngineKind::Aead, Key::default());
        let plaintext = Plaintext::uncompressed("hello, world".as_bytes());
        let mut factory = MetadataFactory::default();
        let meta = factory.from_label("data-chunk").unwrap();
        let chunk = Chunk::data(DataChunk::encrypt(&engine, &plaintext, meta.clone()).unwrap());

        let ser = chunk.serialize().unwrap();
        let bytes = plaintext.as_bytes().unwrap();
        for i in 0..(ser.len() - bytes.len()) {
            if ser[i..].starts_with(&bytes) {
                panic!("serialized encrypted chunk contains plaintext");
            }
        }

        if let Chunk::Data(data) = chunk {
            assert_eq!(data.decrypt(&engine).unwrap(), plaintext);
        } else {
            panic!("didn't get a data chunk back");
        }
    }
}
