//! Store chunks in the local file system.

use crate::{
    blobs::{BlobRepository, BlobsError},
    chunk::{Chunk, ChunkError, Label},
    id::Id,
    repoxface::{BackupRepository, RepositoryError},
};

#[cfg(test)]
use crate::chunk::Metadata;

/// A backup repository in a local directory.
#[derive(Debug)]
pub struct LocalRepository {
    blobs: BlobRepository,
}

impl LocalRepository {
    /// Create a new [`LocalRepository`] using an initialized and opened
    /// [`BlobRepository`].
    pub fn new(blobs: BlobRepository) -> Self {
        Self { blobs }
    }

    /// Return metadata for all chunks in the backup repository.
    ///
    /// Note that this is for tests only.
    #[cfg(test)]
    fn all_chunks(&self) -> Result<Vec<Metadata>, LocalRepositoryError> {
        Ok(self.blobs.all_blobs()?)
    }

    /// Store a chunk in the backup repository.
    pub fn put(&self, chunk: &Chunk) -> Result<(), LocalRepositoryError> {
        let blob = chunk
            .serialize()
            .map_err(LocalRepositoryError::SerializeChunk)?;
        self.blobs.add_blob(&blob, chunk.metadata())?;
        Ok(())
    }

    /// Get a kind of chunk from the backup repository.
    pub fn get(&self, id: &Id) -> Result<Chunk, LocalRepositoryError> {
        let (blob, _) = self.blobs.get_blob(id)?;
        Chunk::parse(&blob).map_err(|err| LocalRepositoryError::ParseChunk(id.clone(), err))
    }

    /// Remove a chunk from the backup repository.
    pub fn delete(&self, id: &Id) -> Result<(), LocalRepositoryError> {
        self.blobs.remove_blob(id)?;
        Ok(())
    }

    /// Find chunks that have a given label.
    pub fn find(&self, label: &Label) -> Result<Vec<Id>, LocalRepositoryError> {
        Ok(self
            .blobs
            .find_blobs(label)?
            .iter()
            .map(|meta| meta.id().clone())
            .collect())
    }
}

impl BackupRepository for LocalRepository {
    type Error = LocalRepositoryError;

    fn get(&self, id: &Id) -> Result<Chunk, Self::Error> {
        self.get(id)
    }

    fn put(&self, chunk: &Chunk) -> Result<(), Self::Error> {
        self.put(chunk)
    }

    fn delete(&self, id: &Id) -> Result<(), Self::Error> {
        self.delete(id)
    }

    fn find(&self, label: &Label) -> Result<Vec<Id>, Self::Error> {
        self.find(label)
    }
}

/// All potential errors from backup repository.
#[derive(Debug, thiserror::Error)]
pub enum LocalRepositoryError {
    /// Blob repository error.
    #[error(transparent)]
    Blobs(#[from] BlobsError),

    /// Can't serialize a chunk.
    #[error("can't serialize chunk for storing in disk")]
    SerializeChunk(#[source] ChunkError),

    /// Can't parse chunk that has been read from disk.
    #[error("can't de-serialize chunk {0} from {1}")]
    ParseChunk(Id, #[source] ChunkError),
}

impl From<LocalRepositoryError> for RepositoryError {
    fn from(value: LocalRepositoryError) -> Self {
        RepositoryError::Underlying(Box::new(value))
    }
}

#[cfg(test)]
mod test {
    use tempfile::TempDir;

    use crate::{
        chunk::{DataChunk, MetadataFactory},
        cipher::{Engine, EngineKind, Key},
        plaintext::Plaintext,
    };

    use super::*;

    type AnyError<T> = Result<T, Box<dyn std::error::Error>>;

    fn setup() -> AnyError<(TempDir, LocalRepository)> {
        let dir = tempfile::tempdir()?;
        let blobs = BlobRepository::init(dir.path())?;
        Ok((dir, LocalRepository::new(blobs)))
    }

    fn create_chunk(data: &'static str, label: &'static str) -> Chunk {
        let engine = Engine::new(EngineKind::Aead, Key::default());
        let plaintext = Plaintext::uncompressed(data.as_bytes());
        let mut factory = MetadataFactory::default();
        let meta = factory.from_label(label).unwrap();
        Chunk::data(DataChunk::encrypt(&engine, &plaintext, meta.clone()).unwrap())
    }

    #[test]
    fn has_no_chunks_initially() -> AnyError<()> {
        let (_dir, local) = setup()?;
        assert_eq!(local.all_chunks()?, vec![]);
        Ok(())
    }

    #[test]
    fn adds_and_gets_chunk() -> AnyError<()> {
        let chunk = create_chunk("hello, world", "data-chunk");

        let (_dir, local) = setup()?;

        local.put(&chunk)?;
        assert_eq!(local.all_chunks()?, vec![chunk.metadata().clone()]);

        let gotten = local.get(chunk.metadata().id())?;
        assert_eq!(
            serde_json::to_vec(&chunk).unwrap(),
            serde_json::to_vec(&gotten).unwrap()
        );

        Ok(())
    }

    #[test]
    fn removes_chunk() -> AnyError<()> {
        let chunk = create_chunk("hello, world", "data-chunk");

        let (_dir, local) = setup()?;

        local.put(&chunk)?;
        local.delete(chunk.metadata().id())?;
        assert_eq!(local.all_chunks()?, vec![]);

        Ok(())
    }

    #[test]
    fn finds_chunk() -> AnyError<()> {
        let chunk = create_chunk("hello, world", "data-chunk");

        let (_dir, local) = setup()?;

        local.put(&chunk)?;
        let ids = local.find(chunk.metadata().label())?;
        assert_eq!(ids, vec![chunk.metadata().id().clone()]);

        Ok(())
    }
}
