use super::{
    MongodbConfigStore,
    serde::{now_as_datetime, transform_from_mongo, transform_to_mongo, try_to_object_id},
};
use crate::storage::Error;
use mongodb::{
    ClientSession, Collection,
    bson::Document,
    results::{InsertOneResult, UpdateResult},
};
use pbbson::Model;
use pbbson::bson::{self, Bson, doc};

pub async fn create<B: Clone + Send + Sync + ToString>(
    this: &MongodbConfigStore<B>,
    mut session: Option<&mut ClientSession>,
    bucket: B,
    model: Model,
) -> Result<Model, Error> {
    let coll: Collection<Document> = this.db.collection(&bucket.to_string());
    let now = now_as_datetime();

    // Set some defaults
    let mut model = model.clone();
    model.set_datetime("createdAt", now);
    if model.get("id").is_none() {
        model.insert("id", bson::oid::ObjectId::new());
    }

    // Work with a local copy from here on, where we replace id with _id
    let mut local_model = transform_to_mongo(model.clone())?;
    let local_model_id: Bson = match model.get("id") {
        Some(Bson::Binary(binary)) => binary.into(),
        Some(Bson::ObjectId(object_id)) => object_id.into(),
        Some(Bson::String(id)) => try_to_object_id(id)?.into(),
        _ => return Err(Error::internal("Unsupported id type")),
    };
    local_model.insert("_id", local_model_id.clone());
    let maybe_created_by_account_id = local_model.get("createdByAccountId");

    // Store
    let _res: InsertOneResult = {
        let op = coll.insert_one(local_model.clone());
        if let Some(ref mut session) = session {
            op.session(&mut **session).await?
        } else {
            op.await?
        }
    };

    // Maintain belongs-to fk's
    for belongs_to in this
        .belongs_tos_by_bucket
        .get(&bucket.to_string())
        .cloned()
        .unwrap_or_default()
    {
        let local_value = local_model.get(&belongs_to.local);
        match local_value {
            None => {}
            Some(Bson::ObjectId(id)) => {
                match belongs_to.inverse {
                    None => {
                        // TODO validate fk only
                    }
                    Some(inverse) => {
                        // Update foreign bucket to reference this
                        let add_to_set = doc! {&inverse: &local_model_id};
                        let mut set = doc! {"updatedAt": now};
                        if let Some(created_by_account_id) = maybe_created_by_account_id {
                            set.insert("updatedByAccountId", created_by_account_id);
                        }
                        let update = doc! {"$addToSet": add_to_set, "$set": set};
                        let remote_coll: Collection<Document> = this.db.collection(&belongs_to.remote.to_string());
                        let filter = doc! { "_id": id };
                        let res: UpdateResult = {
                            let op = remote_coll.update_one(filter, update);
                            if let Some(ref mut session) = session {
                                op.session(&mut **session).await?
                            } else {
                                op.await?
                            }
                        };
                        if res.matched_count == 0 {
                            //error!("Could not find _ to add _ fk")
                        }
                    }
                }
            }
            _ => {
                log::error!("Unknown fk type: {local_value:?}");
            }
        }
    }

    // Maintain has-many fk's
    for has_many in this
        .has_manys_by_bucket
        .get(&bucket.to_string())
        .cloned()
        .unwrap_or_default()
    {
        let local_value = model.get(&has_many.local);
        match local_value {
            Some(Bson::Array(array)) => {
                match has_many.inverse {
                    None => {
                        // TODO validate fk only
                    }
                    Some(inverse) => {
                        // Update foreign bucket to reference this
                        let remote_coll: Collection<Document> = this.db.collection(&has_many.remote.to_string());
                        for id in array.iter() {
                            let id = match id {
                                Bson::ObjectId(object_id) => *object_id,
                                Bson::String(id) => try_to_object_id(id)?,
                                _ => return Err(Error::invalid_argument("Unsupport fk datatype")),
                            };
                            let set = doc! {&inverse: &local_model_id, "updatedAt": now/* TODO , "updatedByAccountId": &account_id*/};
                            let update = doc! {"$set": set};
                            let filter = doc! { "_id": id };
                            let res: UpdateResult = {
                                let op = remote_coll.update_one(filter, update);
                                if let Some(ref mut session) = session {
                                    op.session(&mut **session).await?
                                } else {
                                    op.await?
                                }
                            };
                            if res.matched_count == 0 {
                                //error!("Could not find _ to add _ fk")
                            }
                        }
                    }
                }
            }
            _ => {
                log::error!("Unknown fk type: {local_value:?}");
            }
        }
    }

    transform_from_mongo(model)
}
