//! Store chunks in the local file system.

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

use rusqlite::Connection;

use crate::{
    chunk::{Chunk, ChunkError, Label, Metadata},
    cipher::Engine,
    client::ClientChunk,
    credential::Credential,
    id::Id,
    util::{UtilError, read_file, write_file},
};

/// A backup repository, also known as a chunk store, in a local directory.
pub struct ChunkStore {
    dir: PathBuf,
    conn: Connection,
}

impl ChunkStore {
    /// Has a directory been initialized as a chunk store?
    pub fn is_init(dir: &Path) -> bool {
        Self::db(dir).exists()
    }

    /// Initialize a directory for storing chunks. The directory must exist.
    pub fn init(dir: &Path) -> Result<Self, StoreError> {
        if !dir.exists() {
            return Err(StoreError::NoSuchDir(dir.into()));
        }

        if Self::is_init(dir) {
            return Err(StoreError::AlreadyInit(dir.into()));
        }

        Self::init_or_open(dir)
    }

    /// Open a chunk store directory. This fails if the chunk store
    /// hasn't been initialized.
    pub fn open(dir: &Path) -> Result<Self, StoreError> {
        if !dir.exists() {
            return Err(StoreError::NoSuchDir(dir.into()));
        }

        if !Self::is_init(dir) {
            return Err(StoreError::NotInit(dir.into()));
        }

        Self::init_or_open(dir)
    }

    fn init_or_open(dir: &Path) -> Result<Self, StoreError> {
        let db = Self::db(dir);
        let conn = Connection::open(&db).map_err(|err| StoreError::CreateDb(db.clone(), err))?;
        db::create_tables(&conn).map_err(|err| StoreError::CreateTables(db.clone(), err))?;

        Ok(Self {
            dir: dir.into(),
            conn,
        })
    }

    /// Return metadata for all chunks in the chunk store.
    pub fn all_chunks(&self) -> Result<Vec<Metadata>, StoreError> {
        db::all_chunks(&self.conn)
    }

    /// Store a chunk in the store.
    pub fn add_chunk(&self, chunk: &Chunk) -> Result<(), StoreError> {
        let filename = self.chunk_filename(chunk.metadata().id());
        let chunk_blob = chunk.serialize().map_err(StoreError::SerializeChunk)?;
        write_file(&filename, &chunk_blob).map_err(StoreError::WriteChunk)?;
        db::insert(&self.conn, chunk)?;
        Ok(())
    }

    // Get any kind of chunk from the store.
    fn get_chunk(&self, id: &Id) -> Result<Chunk, StoreError> {
        let filename = self.chunk_filename(id);
        let data = read_file(&filename).map_err(StoreError::ReadChunk)?;
        Chunk::parse(&data).map_err(|err| StoreError::ParseChunk(id.clone(), filename, err))
    }

    /// Get a data chunk from the store.
    pub fn get_data_chunk(&self, id: &Id) -> Result<Chunk, StoreError> {
        let chunk = self.get_chunk(id)?;
        if let Chunk::Data(_) = &chunk {
            Ok(chunk)
        } else {
            Err(StoreError::NotData(id.clone()))
        }
    }

    /// Get a client chunk from the store.
    pub fn get_client_chunk(&self, id: &Id) -> Result<Chunk, StoreError> {
        let chunk = self.get_chunk(id)?;
        if let Chunk::Client(_) = &chunk {
            Ok(chunk)
        } else {
            Err(StoreError::NotClient(id.clone()))
        }
    }

    /// Find a client chunk by name and open it.
    pub fn open_client(
        &self,
        engine: &Engine,
        wanted: &str,
    ) -> Result<(Id, ClientChunk), StoreError> {
        fn is_old(id: &Id, map: &HashMap<Id, ClientChunk>) -> bool {
            for client in map.values() {
                if client.old_versions().contains(id) {
                    return true;
                }
            }
            false
        }

        let metas = self.find_client_chunks()?;

        let mut map: HashMap<Id, ClientChunk> = HashMap::new();
        for x in metas {
            let chunk = self.get_client_chunk(x.id()).unwrap();
            let plaintext = match chunk {
                Chunk::Client(data) => data.decrypt(engine).unwrap(),
                _ => return Err(StoreError::NotClient(x.id().clone())),
            };
            let client = ClientChunk::try_from(&plaintext).unwrap();
            map.insert(x.id().clone(), client);
        }

        let mut roots: Vec<&Id> = map.keys().collect();
        loop {
            if roots.is_empty() {
                return Err(StoreError::NoSuchClient(wanted.to_string()));
            }

            let mut found = None;
            for (i, id) in roots.iter().enumerate() {
                if is_old(id, &map) {
                    found = Some(i);
                    break;
                }
            }

            if let Some(i) = found {
                roots.remove(i);
                if roots.len() == 1 {
                    break;
                }
            } else {
                break;
            }
        }

        match roots.len() {
            0 => Err(StoreError::NoSuchClient(wanted.to_string())),
            1 => {
                let id = roots
                    .first()
                    .ok_or(StoreError::NoSuchClient(wanted.to_string()))?;
                let c = map
                    .get(id)
                    .ok_or(StoreError::NoSuchClient(wanted.to_string()))?;
                Ok(((*id).clone(), c.clone()))
            }
            _ => Err(StoreError::TooManyClients(wanted.to_string())),
        }
    }

    /// Get a credential chunk from the store.
    pub fn get_credential_chunk(&self, id: &Id) -> Result<Credential, StoreError> {
        let chunk = self.get_chunk(id)?;
        if let Chunk::Credential { credential, .. } = chunk {
            Ok(credential)
        } else {
            Err(StoreError::NotCredential(id.clone()))
        }
    }

    /// Remove a chunk from the store.
    pub fn remove_chunk(&self, id: &Id) -> Result<(), StoreError> {
        let filename = self.chunk_filename(id);
        std::fs::remove_file(&filename)
            .map_err(|err| StoreError::RemoveChunkFile(filename, err))?;
        db::delete(&self.conn, id)
    }

    /// Find chunks that have a given label.
    pub fn find_chunks(&self, label: &Label) -> Result<Vec<Metadata>, StoreError> {
        db::find(&self.conn, label)
    }

    /// Find client chunks.
    pub fn find_client_chunks(&self) -> Result<Vec<Metadata>, StoreError> {
        let label = Label::from("client");
        self.find_chunks(&label)
    }

    /// Find client chunks that we can open.
    pub fn find_our_client_chunks(&self, engine: &Engine) -> Result<Vec<ClientChunk>, StoreError> {
        let clients = self.find_client_chunks()?;

        let our_clients: Vec<ClientChunk> = clients
            .iter()
            .filter_map(|meta| self.get_client_chunk(meta.id()).ok())
            .filter_map(|chunk| match chunk {
                Chunk::Client(data) => Some(data),
                _ => None,
            })
            .filter_map(|chunk| chunk.decrypt(engine).ok())
            .filter_map(|plaintext| ClientChunk::try_from(&plaintext).ok())
            .collect();

        Ok(our_clients)
    }

    /// Find credential chunks.
    pub fn find_credential_chunks(&self) -> Result<Vec<Metadata>, StoreError> {
        let label = Label::from("credential");
        self.find_chunks(&label)
    }

    /// Get all credential chunks.
    pub fn get_credential_chunks(&self) -> Result<Vec<Credential>, StoreError> {
        let credentials = self.find_credential_chunks()?;

        let mut creds = vec![];
        for meta in credentials.iter() {
            creds.push(self.get_credential_chunk(meta.id())?);
        }

        Ok(creds)
    }

    fn db(dir: &Path) -> PathBuf {
        dir.join("index.sqlite")
    }

    /// Return filename where chunk is stored, assuming it exists in the store.
    pub fn chunk_filename(&self, id: &Id) -> PathBuf {
        self.dir.join(format!("{id}.chunk"))
    }
}

mod db {
    use super::{Chunk, Connection, Id, Label, Metadata, StoreError};

    pub fn create_tables(conn: &Connection) -> Result<(), rusqlite::Error> {
        conn.execute(
            "CREATE TABLE IF NOT EXISTS chunks (id BLOB, label BLOB, metadata BLOB)",
            [],
        )?;
        Ok(())
    }

    pub fn insert(conn: &Connection, chunk: &Chunk) -> Result<(), StoreError> {
        let meta = chunk.metadata();
        let meta_blob = meta.to_bytes().map_err(StoreError::MetadataToBlob)?;
        conn.execute(
            "INSERT INTO chunks (id, label, metadata) VALUES (?1, ?2, ?3)",
            (meta.id().to_string(), meta.label().as_bytes(), &meta_blob),
        )
        .map_err(StoreError::Insert)?;
        Ok(())
    }

    pub fn delete(conn: &Connection, id: &Id) -> Result<(), StoreError> {
        conn.execute("DELETE FROM chunks WHERE id = ?1", (id.to_string(),))
            .map_err(StoreError::RemoveFromIndex)?;
        Ok(())
    }

    pub fn all_chunks(conn: &Connection) -> Result<Vec<Metadata>, StoreError> {
        let mut stmt = conn
            .prepare("SELECT metadata FROM chunks")
            .map_err(StoreError::Prepare)?;
        let iter = stmt
            .query_map([], |row| {
                let data: Result<Vec<u8>, _> = row.get(0);
                Ok(data)
            })
            .map_err(StoreError::Query)?;
        let mut metas = vec![];
        for meta in iter {
            let meta: Result<Vec<u8>, _> = meta.map_err(StoreError::Row)?;
            let meta: Vec<u8> = meta.map_err(StoreError::Row)?;
            let meta = Metadata::from_bytes(&meta).map_err(StoreError::MetadataFromBlob)?;
            metas.push(meta);
        }
        Ok(metas)
    }

    pub fn find(conn: &Connection, label: &Label) -> Result<Vec<Metadata>, StoreError> {
        let mut stmt = conn
            .prepare("SELECT metadata FROM chunks WHERE label = ?1")
            .map_err(StoreError::Prepare)?;
        let iter = stmt
            .query_map(&[(1, label.as_bytes())], |row| {
                let data: Result<Vec<u8>, _> = row.get(0);
                Ok(data)
            })
            .map_err(StoreError::Query)?;
        let mut metas = vec![];
        for meta in iter {
            let meta: Result<Vec<u8>, _> = meta.map_err(StoreError::Row)?;
            let meta: Vec<u8> = meta.map_err(StoreError::Row)?;
            let meta = Metadata::from_bytes(&meta).map_err(StoreError::MetadataFromBlob)?;
            metas.push(meta);
        }

        Ok(metas)
    }
}

/// All potential errors from chunk store.
#[derive(Debug, thiserror::Error)]
pub enum StoreError {
    /// Backup repository directory doesn't exist.
    #[error("backup repository directory does not exist: {0}")]
    NoSuchDir(PathBuf),

    /// Can't re-initialize a directory.
    #[error("directory is already initialized as a backup repository: {0}")]
    AlreadyInit(PathBuf),

    /// Backup repository directory has not been initialized.
    #[error("directory is not initialized as a backup repository: {0}")]
    NotInit(PathBuf),

    /// Can't create chunk index.
    #[error("can't create chunk index database {0}")]
    CreateDb(PathBuf, #[source] rusqlite::Error),

    /// Can't create SQL tables in chunk index.
    #[error("can't create tables in chunk index database {0}")]
    CreateTables(PathBuf, #[source] rusqlite::Error),

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

    /// Can't write chunk.
    #[error("can't write chunk to disk")]
    WriteChunk(#[source] UtilError),

    /// Can't insert chunk into index.
    #[error("can't store chunk information in chunk index database")]
    Insert(#[source] rusqlite::Error),

    /// Can't serialize chunk metadata for index.
    #[error("can't serialize chunk metadata for storing in chunk index database")]
    MetadataToBlob(#[source] ChunkError),

    /// Can't parse chunk metadata from index.
    #[error("can't deserialize chunk metadata from in chunk index database")]
    MetadataFromBlob(#[source] ChunkError),

    /// Can=t prepare SQL statement.
    #[error("can't prepare SQL statement")]
    Prepare(#[source] rusqlite::Error),

    /// Can't run SQL query.
    #[error("can't query chunk index database")]
    Query(#[source] rusqlite::Error),

    /// Can't understand SQL query result row.
    #[error("can't query parse query result row from chunk index database")]
    Row(#[source] rusqlite::Error),

    /// Can't read chunk file from disk.
    #[error("can't read chunk file")]
    ReadChunk(#[source] UtilError),

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

    /// Can't remove chunk file.
    #[error("can't remove chunk file {0}")]
    RemoveChunkFile(PathBuf, #[source] std::io::Error),

    /// Can't remove chunk from index.
    #[error("can't remove chunk from chunk index database")]
    RemoveFromIndex(#[source] rusqlite::Error),

    /// Wanted data chunk, got something else.
    #[error("retrieved chunk, but it's not a data chunk: {0}")]
    NotData(Id),

    /// Wanted client chunk, got something else.
    #[error("retrieved chunk, but it's not a client chunk: {0}")]
    NotClient(Id),

    /// Wanted credential chunk, got something else.
    #[error("retrieved chunk, but it's not a credential chunk: {0}")]
    NotCredential(Id),

    /// FIXME
    #[error("no client found named {0}")]
    NoSuchClient(String),

    /// FIXME
    #[error("more than one client found named {0}")]
    TooManyClients(String),
}

#[cfg(test)]
mod test {
    use crate::{
        chunk::{DataChunk, Label, MetadataFactory},
        cipher::{Engine, EngineKind, Key},
        client::ClientChunk,
        credential::CredentialMethod,
        plaintext::Plaintext,
        sop::Sop,
    };

    use super::*;

    #[test]
    fn nonexistent_dir_is_not_init() {
        assert!(!ChunkStore::is_init(Path::new("/this/does/not/exist")));
    }

    #[test]
    fn root_dir_is_not_init() {
        assert!(!ChunkStore::is_init(Path::new("/")));
    }

    #[test]
    fn empty_dir_is_not_init() {
        let dir = tempfile::tempdir().unwrap();
        assert!(!ChunkStore::is_init(dir.path()));
    }

    #[test]
    fn cant_init_nonexistent_dir() {
        let dir = Path::new("/this/does/not/exist");
        assert!(matches!(
            ChunkStore::init(dir),
            Err(StoreError::NoSuchDir(_))
        ));
    }

    #[test]
    fn inits_empty_dir() {
        let dir = tempfile::tempdir().unwrap();
        ChunkStore::init(dir.path()).unwrap();
        assert!(ChunkStore::is_init(dir.path()));
    }

    #[test]
    fn has_no_chunks_initially() {
        let dir = tempfile::tempdir().unwrap();
        ChunkStore::init(dir.path()).unwrap();
        let store = ChunkStore::open(dir.path()).unwrap();
        assert_eq!(store.all_chunks().unwrap(), vec![]);
    }

    #[test]
    fn adds_chunk() {
        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 dir = tempfile::tempdir().unwrap();
        ChunkStore::init(dir.path()).unwrap();
        let store = ChunkStore::open(dir.path()).unwrap();
        store.add_chunk(&chunk).unwrap();
        assert_eq!(store.all_chunks().unwrap(), vec![chunk.metadata().clone()]);

        let gotten = store.get_chunk(meta.id()).unwrap();
        assert_eq!(
            serde_json::to_vec(&chunk).unwrap(),
            serde_json::to_vec(&gotten).unwrap()
        );
    }

    #[test]
    fn removes_chunk() {
        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 dir = tempfile::tempdir().unwrap();
        ChunkStore::init(dir.path()).unwrap();
        let store = ChunkStore::open(dir.path()).unwrap();
        store.add_chunk(&chunk).unwrap();
        store.remove_chunk(chunk.metadata().id()).unwrap();
        assert_eq!(store.all_chunks().unwrap(), vec![]);
    }

    #[test]
    fn finds_no_chunk_in_empty_store() {
        let label = Label::from("data-chunk");
        let dir = tempfile::tempdir().unwrap();
        ChunkStore::init(dir.path()).unwrap();
        let store = ChunkStore::open(dir.path()).unwrap();
        assert_eq!(store.find_chunks(&label).unwrap(), vec![]);
    }

    #[test]
    fn finds_no_chunk_when_none_match_label() {
        let label = Label::from("data-chunk");
        let other_label = Label::from("other-chunk");

        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(other_label).unwrap();
        let chunk = Chunk::data(DataChunk::encrypt(&engine, &plaintext, meta.clone()).unwrap());

        let dir = tempfile::tempdir().unwrap();
        ChunkStore::init(dir.path()).unwrap();
        let store = ChunkStore::open(dir.path()).unwrap();
        store.add_chunk(&chunk).unwrap();

        assert_eq!(store.find_chunks(&label).unwrap(), vec![]);
    }

    #[test]
    fn finds_chunk_when_labels_match() {
        let label = Label::from("data-chunk");
        let other_label = Label::from("other-chunk");

        let engine = Engine::new(EngineKind::Aead, Key::default());
        let plaintext = Plaintext::uncompressed("hello, world".as_bytes());
        let mut factory = MetadataFactory::default();

        let other_meta = factory.from_label(other_label).unwrap();
        let other = Chunk::data(DataChunk::encrypt(&engine, &plaintext, other_meta).unwrap());

        let meta1 = factory.from_label(label.clone()).unwrap();
        let chunk1 = Chunk::data(DataChunk::encrypt(&engine, &plaintext, meta1.clone()).unwrap());

        let meta2 = factory.from_label(label.clone()).unwrap();
        let chunk2 = Chunk::data(DataChunk::encrypt(&engine, &plaintext, meta2.clone()).unwrap());

        let dir = tempfile::tempdir().unwrap();
        ChunkStore::init(dir.path()).unwrap();
        let store = ChunkStore::open(dir.path()).unwrap();
        store.add_chunk(&other).unwrap();
        store.add_chunk(&chunk1).unwrap();
        store.add_chunk(&chunk2).unwrap();

        assert_eq!(store.find_chunks(&label).unwrap(), vec![meta1, meta2]);
    }

    #[test]
    fn finds_client_chunk() {
        let client = ClientChunk::new("xyzzy");
        let plaintext = client.to_plaintext().unwrap();
        let mut factory = MetadataFactory::default();
        let metadata = factory.from_label("client").unwrap();
        let client_key = Key::default();
        let engine = Engine::new(EngineKind::Aead, client_key);
        let data = DataChunk::encrypt(&engine, &plaintext, metadata.clone()).unwrap();
        let chunk = Chunk::Client(data);
        let dir = tempfile::tempdir().unwrap();
        ChunkStore::init(dir.path()).unwrap();
        let store = ChunkStore::open(dir.path()).unwrap();
        store.add_chunk(&chunk).unwrap();
        assert_eq!(store.find_client_chunks().unwrap(), vec![metadata]);
    }

    #[test]
    fn finds_credential_chunk() {
        let client_key = Key::default();
        let sop = Sop::default();
        let openpgp_key = sop.generate_key("alice@example.com").unwrap();
        let method = CredentialMethod::openpgp_soft(sop, openpgp_key);

        let credential = Credential::new(&method, &client_key).unwrap();
        let mut factory = MetadataFactory::default();
        let metadata = factory.from_label("credential").unwrap();
        let chunk = Chunk::credential(metadata.clone(), credential.clone());

        let dir = tempfile::tempdir().unwrap();
        ChunkStore::init(dir.path()).unwrap();
        let store = ChunkStore::open(dir.path()).unwrap();
        store.add_chunk(&chunk).unwrap();

        assert_eq!(store.find_credential_chunks().unwrap(), vec![metadata]);

        assert_eq!(store.get_credential_chunks().unwrap(), vec![credential]);
    }

    #[test]
    fn finds_no_clients_when_there_are_none() {
        let client_key = Key::default();
        let engine = Engine::new(EngineKind::Aead, client_key);

        let dir = tempfile::tempdir().unwrap();
        ChunkStore::init(dir.path()).unwrap();
        let store = ChunkStore::open(dir.path()).unwrap();
        assert_eq!(store.find_our_client_chunks(&engine).unwrap(), vec![]);
    }

    #[test]
    fn opens_client_chunk() {
        let client = ClientChunk::new("xyzzy");
        let plaintext = client.to_plaintext().unwrap();
        let mut factory = MetadataFactory::default();
        let metadata = factory.from_label("client").unwrap();
        let client_key = Key::default();
        let engine = Engine::new(EngineKind::Aead, client_key);
        let data = DataChunk::encrypt(&engine, &plaintext, metadata.clone()).unwrap();
        let chunk = Chunk::Client(data);
        let dir = tempfile::tempdir().unwrap();
        ChunkStore::init(dir.path()).unwrap();
        let store = ChunkStore::open(dir.path()).unwrap();
        store.add_chunk(&chunk).unwrap();
        assert_eq!(store.find_client_chunks().unwrap(), vec![metadata]);

        let (_, opened) = store.open_client(&engine, "xyzzy").unwrap();
        assert_eq!(opened.name(), "xyzzy");
    }
}
