use super::{Options, create, delete, find, find_many, update};
use crate::storage::{
    Error,
    associations::{BelongsTo, HasMany},
    config_store::ConfigStore,
};
use async_trait::async_trait;
use mongodb::{
    bson::Document,
    {Client, ClientSession, Collection, Database, IndexModel, options::ClientOptions},
};
use pbbson::Model;
use pbbson::bson::{self, doc};
use std::collections::HashMap;
use url::Url;

#[allow(unused)]
pub struct MongodbConfigStore<B: Clone + Send + Sync + ToString> {
    pub belongs_tos_by_bucket: HashMap<String, Vec<BelongsTo<B>>>,
    pub db: Database,
    pub has_manys_by_bucket: HashMap<String, Vec<HasMany<B>>>,
    pub index_models_by_bucket: HashMap<String, Vec<IndexModel>>,
}

impl<B: Clone + Send + Sync + ToString> MongodbConfigStore<B> {
    #[allow(unused)]
    pub async fn new(url: &Url, options: Options<B>) -> Result<Self, Error> {
        let mut opts = ClientOptions::parse(url.to_string())
            .await
            .map_err(|e| Error::internal(e.to_string()))?;
        opts.app_name = options.app_name;
        log::debug!(opts:?; "Using Mongo client options");
        let client = Client::with_options(opts).map_err(|e| Error::internal(e.to_string()))?;

        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
            .map_err(|e| Error::internal(e.to_string()))?;

        // Construct
        let this = Self {
            belongs_tos_by_bucket: options.belongs_tos_by_bucket,
            db,
            has_manys_by_bucket: options.has_manys_by_bucket,
            index_models_by_bucket: options.index_models_by_bucket,
        };

        // Ensure indexes
        this.ensure_all_indexes().await?;

        Ok(this)
    }
}

#[async_trait]
impl<B: Clone + Send + Sync + ToString> ConfigStore<B> for MongodbConfigStore<B> {
    async fn create(&self, bucket: B, model: Model) -> Result<Model, Error> {
        create::create(self, bucket, model).await
    }

    async fn delete(&self, bucket: B, id: &str, by_account_id: Option<String>) -> Result<(), Error> {
        delete::delete(self, bucket, id, by_account_id).await
    }

    async fn find(&self, bucket: B, id: &str) -> Result<Model, Error> {
        find::find(self, bucket, id).await
    }

    async fn find_many(&self, bucket: B, filter: Model, ordering: Option<Model>) -> Result<Vec<Model>, Error> {
        find_many::find_many(self, bucket, filter, ordering).await
    }

    async fn update(&self, bucket: B, model: Model) -> Result<Model, Error> {
        update::update(self, bucket, model).await
    }
}

impl<B: Clone + Send + Sync + ToString> MongodbConfigStore<B> {
    async fn ensure_all_indexes(&self) -> Result<(), Error> {
        for (bucket, index_models) in self.index_models_by_bucket.iter() {
            let coll: Collection<Document> = self.db.collection(bucket.as_ref());
            mongodb_ensure_index::ensure_indexes(&coll, index_models.clone())
                .await
                .map_err(|e| Error::internal(e.to_string()))?;
        }
        Ok(())
    }

    pub async fn find_in_session(
        &self,
        session: &mut ClientSession,
        bucket: B,
        id: bson::oid::ObjectId,
    ) -> Result<Model, Error> {
        let coll: Collection<Document> = self.db.collection(&bucket.to_string());
        let filter = doc! {"_id": id};
        let maybe_document = coll
            .find_one(filter)
            .session(session)
            .await
            .map_err(|e| Error::internal(e.to_string()))?;
        match maybe_document {
            None => Err(Error::not_found("No such record")),
            Some(document) => Ok(document.into()),
        }
    }
}
