use super::{Options, create, delete, find, find_many, update};
use crate::storage::config_store::ConfigStore;
use crate::storage::config_store::config_store::FindOptions;
use crate::storage::{
    Error,
    associations::{BelongsTo, HasMany},
};
use async_trait::async_trait;
use mongodb::{
    bson::Document,
    options::{ReadConcern, WriteConcern},
    {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>>,
    pub can_transaact: bool,
}

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?;
        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 can_transaact = {
            let admin_db = client.database("admin");
            admin_db.run_command(doc! {"replSetGetStatus": 1}).await.is_ok()
        };
        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,
            can_transaact,
        };

        // 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> {
        match self.can_transaact {
            false => create::create(self, None, bucket, model).await,
            true => {
                let mut session = self.db.client().start_session().await?;
                session
                    .start_transaction()
                    .read_concern(ReadConcern::majority())
                    .write_concern(WriteConcern::majority())
                    .await?;
                let model = create::create(self, Some(&mut session), bucket, model).await?;
                session.commit_transaction().await?;
                Ok(model)
            }
        }
    }

    async fn delete(&self, bucket: B, id: &str, by_account_id: Option<String>) -> Result<(), Error> {
        match self.can_transaact {
            false => delete::delete(self, None, bucket, id, by_account_id).await,
            true => {
                let mut session = self.db.client().start_session().await?;
                session
                    .start_transaction()
                    .read_concern(ReadConcern::majority())
                    .write_concern(WriteConcern::majority())
                    .await?;
                delete::delete(self, Some(&mut session), bucket, id, by_account_id).await?;
                session.commit_transaction().await?;
                Ok(())
            }
        }
    }

    async fn find(&self, bucket: B, id: &str) -> Result<Model, Error> {
        match self.can_transaact {
            false => find::find(self, None, bucket, id).await,
            true => {
                let mut session = self.db.client().start_session().await?;
                session
                    .start_transaction()
                    .read_concern(ReadConcern::majority())
                    .write_concern(WriteConcern::majority())
                    .await?;
                let model = find::find(self, Some(&mut session), bucket, id).await?;
                session.commit_transaction().await?;
                Ok(model)
            }
        }
    }

    async fn find_many(&self, bucket: B, options: FindOptions) -> Result<Vec<Model>, Error> {
        match self.can_transaact {
            false => find_many::find_many(self, None, bucket, options).await,
            true => {
                let mut session = self.db.client().start_session().await?;
                session
                    .start_transaction()
                    .read_concern(ReadConcern::majority())
                    .write_concern(WriteConcern::majority())
                    .await?;
                let models = find_many::find_many(self, Some(&mut session), bucket, options).await?;
                session.commit_transaction().await?;
                Ok(models)
            }
        }
    }

    async fn update(&self, bucket: B, model: Model) -> Result<Model, Error> {
        match self.can_transaact {
            false => update::update(self, None, bucket, model).await,
            true => {
                let mut session = self.db.client().start_session().await?;
                session
                    .start_transaction()
                    .read_concern(ReadConcern::majority())
                    .write_concern(WriteConcern::majority())
                    .await?;
                let model = update::update(self, Some(&mut session), bucket, model).await?;
                session.commit_transaction().await?;
                Ok(model)
            }
        }
    }
}

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?;
        }
        Ok(())
    }

    pub async fn find_in_session(
        &self,
        session: &mut Option<&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 = {
            let op = coll.find_one(filter);
            if let Some(session) = session {
                op.session(&mut **session).await?
            } else {
                op.await?
            }
        };
        match maybe_document {
            None => Err(Error::not_found("No such record")),
            Some(document) => Ok(document.into()),
        }
    }
}
