use super::Options;
use crate::storage::{Error, object_store::ObjectStore};
use async_trait::async_trait;
use futures::AsyncWriteExt;
use futures_util::AsyncReadExt;
use mongodb::bson::{Document, doc};
use mongodb::options::{ClientOptions, IndexOptions};
use mongodb::results::DeleteResult;
use mongodb::{Client, Collection, Database, IndexModel};
use std::fs::File;
use std::io::{Read, Write};
use std::path::Path;
use strum_macros::AsRefStr;
use tempfile::NamedTempFile;
use url::Url;

#[derive(AsRefStr)]
enum Collections {
    #[strum(serialize = "fs.files")]
    FileInfos,
}

#[allow(unused)]
pub struct MongoDbObjectStore {
    pub(crate) db: Database,
}

impl MongoDbObjectStore {
    #[allow(unused)]
    pub async fn new(url: &Url, options: Options) -> Result<Self, Error> {
        let mut opts = ClientOptions::parse(url.to_string()).await?;
        opts.app_name = options.app_name;
        log::debug!(opts:?; "Using Mongo client options");
        let client = Client::with_options(opts)?;

        let database_name = {
            let mut s = url.path().to_string();
            if let Some(stripped) = s.strip_prefix("/") {
                s = stripped.to_string();
            }
            if let Some(found_at) = s.find("?") {
                s.truncate(found_at);
            }
            s.to_string()
        };
        log::debug!(database_name; "Using Mongo database");
        let db: Database = client.database(&database_name);

        // Validate the connection
        let _ = db.list_collection_names().await?;

        // Construct
        let this = Self { db };

        // Ensure indexes
        let _index_models = index_models();
        // TODO ensure_all_indexes().await?;

        Ok(this)
    }
}

#[async_trait]
impl ObjectStore for MongoDbObjectStore {
    async fn del(&self, key: &str) -> Result<(), Error> {
        let coll: Collection<Document> = self.db.collection(Collections::FileInfos.as_ref());
        let filter = doc! { "filename": key };
        let res: DeleteResult = coll.delete_one(filter).await?;
        if res.deleted_count == 0 {
            return Err(Error::not_found("No such file"));
        }
        Ok(())
    }

    async fn get(&self, key: &str) -> Result<NamedTempFile, Error> {
        let bucket = self.db.gridfs_bucket(None);
        let coll: Collection<Document> = self.db.collection(Collections::FileInfos.as_ref());
        let filter = doc! { "filename": key };
        let maybe_document = coll.find_one(filter).await?;
        let doc = match maybe_document {
            None => return Err(Error::not_found("No such record")),
            Some(document) => document,
        };
        let id = match doc.get("_id") {
            None => return Err(Error::not_found("No such record")),
            Some(id) => id,
        };
        let mut download_stream = bucket.open_download_stream(id.clone()).await?;
        let mut buf = Vec::new();
        download_stream
            .read_to_end(&mut buf)
            .await
            .map_err(|e| Error::internal(e.to_string()))?;
        let mut file = NamedTempFile::new().map_err(|e| Error::internal(e.to_string()))?;
        file.write_all(&buf).map_err(|e| Error::internal(e.to_string()))?;
        file.flush().map_err(|e| Error::internal(e.to_string()))?;
        Ok(file)
    }

    async fn put(&self, key: &str, path: &Path) -> Result<(), Error> {
        // Store file in the default GridFS bucket
        let mut file = File::open(path).map_err(|e| Error::internal(e.to_string()))?;
        let mut buf = vec![];
        file.read_to_end(&mut buf).map_err(|e| Error::internal(e.to_string()))?;
        let bucket = self.db.gridfs_bucket(None);
        let filename = key;
        let mut upload_stream = bucket.open_upload_stream(filename).await?;
        upload_stream
            .write_all(&buf[..])
            .await
            .map_err(|e| Error::internal(e.to_string()))?;
        upload_stream
            .close()
            .await
            .map_err(|e| Error::internal(e.to_string()))?;
        Ok(())
    }
}

fn index_models() -> Vec<IndexModel> {
    vec![
        {
            IndexModel::builder()
                .keys({
                    doc! {"ownerAccountId": 1}
                })
                .options(Some(
                    IndexOptions::builder()
                        .name(Some("ownerAccountId_1".to_string()))
                        .build(),
                ))
                .build()
        },
        {
            IndexModel::builder()
                .keys({
                    doc! {"filename": 1, "length": 1}
                })
                .options(Some(
                    IndexOptions::builder()
                        .name(Some("filename_1_length_1".to_string()))
                        .build(),
                ))
                .build()
        },
    ]
}
