use super::MemoryConfigStore;
use crate::storage::Error;
use pbbson::Model;
use pbbson::bson::Bson;

pub(crate) async fn perform<B: Clone + Send + Sync + ToString>(
    this: &MemoryConfigStore<B>,
    bucket: B,
    model: Model,
) -> Result<Model, Error> {
    let model_id = model.id()?;
    let doc_holder = this.doc_holder(&bucket)?;
    let mut docs = doc_holder
        .docs
        .lock()
        .map_err(|e| Error::failed_precondition(e.to_string()))?;
    let existing = match docs.get(&model_id) {
        None => return Err(Error::not_found("No such record")),
        Some(existing) => existing.clone(),
    };

    // Store the update
    docs.insert(model.id()?, model.clone());

    // Maintain fk's of dereferenced belongs-tos
    let belongs_tos = this
        .belongs_tos_by_bucket
        .get(&bucket.to_string())
        .cloned()
        .unwrap_or_default();
    for belongs_to in belongs_tos.iter() {
        let existing_fk = existing.get(&belongs_to.local);
        let new_fk = model.get(&belongs_to.local).cloned();
        match existing_fk {
            None => continue,
            Some(existing_fk_value) => {
                if existing_fk == new_fk.as_ref() {
                    continue;
                }
                let existing_fk_id = match existing_fk_value.as_str() {
                    Some(existing_fk_id) => existing_fk_id,
                    None => continue,
                };
                let remote_bucket = this.doc_holder(&belongs_to.remote)?;
                let mut remote_docs = remote_bucket
                    .docs
                    .lock()
                    .map_err(|e| Error::failed_precondition(e.to_string()))?;
                let remote_model = match remote_docs.get(existing_fk_id) {
                    None => continue,
                    Some(remote_model) => remote_model,
                };
                if let Some(inverse) = belongs_to.inverse.as_ref() {
                    let remote_fk_value = remote_model.get(inverse.clone());
                    if let Some(Bson::Array(array)) = remote_fk_value {
                        let array: Vec<_> = array
                            .iter()
                            .filter(|remote_fk| *remote_fk != &Bson::String(model_id.clone()))
                            .collect();
                        let mut remote_model = remote_model.clone();
                        remote_model.insert(inverse.clone(), array);
                        remote_docs.insert(existing_fk_id.to_string(), remote_model);
                    }
                }
            }
        }
    }

    // Maintain fk's of newly referenced belongs-tos
    for belongs_to in belongs_tos.iter() {
        let existing_fk = existing.get(&belongs_to.local);
        let new_fk = model.get(&belongs_to.local);
        match new_fk {
            None => continue,
            Some(new_fk_value) => {
                if existing_fk == new_fk {
                    continue;
                }
                let new_fk_object_id = match new_fk_value.as_str() {
                    None => continue,
                    Some(s) => s,
                };
                let remote_bucket = this.doc_holder(&belongs_to.remote)?;
                let mut remote_docs = remote_bucket
                    .docs
                    .lock()
                    .map_err(|e| Error::failed_precondition(e.to_string()))?;
                let remote_model = match remote_docs.get(new_fk_object_id) {
                    None => continue,
                    Some(remote_model) => remote_model,
                };
                if let Some(inverse) = belongs_to.inverse.as_ref() {
                    let remote_fk_value = remote_model.get(inverse.clone());
                    if let Some(Bson::Array(array)) = remote_fk_value {
                        let mut array = array.clone();
                        array.push(Bson::String(model_id.clone()));
                        let mut remote_model = remote_model.clone();
                        remote_model.insert(inverse.clone(), array);
                        remote_docs.insert(new_fk_object_id.to_string(), remote_model);
                    }
                }
            }
        }
    }

    // Maintain has-many fk's
    for has_many in this
        .has_manys_by_bucket
        .get(&bucket.to_string())
        .cloned()
        .unwrap_or_default()
    {
        let existing_local_value = existing.get(&has_many.local);
        let local_value = model.get(&has_many.local);
        match (existing_local_value, local_value) {
            (Some(Bson::Array(existing_array)), Some(Bson::Array(array))) => {
                match has_many.inverse.clone() {
                    None => {
                        // TODO validate fk only
                    }
                    Some(inverse) => {
                        let remote_bucket = this.doc_holder(&has_many.remote)?;
                        let mut remote_docs = remote_bucket
                            .docs
                            .lock()
                            .map_err(|e| Error::failed_precondition(e.to_string()))?;
                        // Maintain dereferenced fks
                        for existing_id in existing_array.iter() {
                            if array.contains(existing_id) {
                                continue;
                            }
                            if let Bson::String(id) = existing_id {
                                let foreign_model = match remote_docs.get_mut(id.as_str()) {
                                    None => {
                                        continue;
                                    }
                                    Some(foreign_model) => foreign_model,
                                };
                                match foreign_model.get_mut(&inverse) {
                                    Some(Bson::Array(_array)) => {
                                        // TODO array.delete(index of model_id))
                                    }
                                    _ => {
                                        foreign_model.remove(&inverse);
                                    }
                                };
                            }
                        }

                        // Maintain newly referenced fks
                        for id in array.iter() {
                            if existing_array.contains(id) {
                                continue;
                            }
                            if let Bson::String(id) = id {
                                let foreign_model = match remote_docs.get_mut(id.as_str()) {
                                    None => {
                                        continue;
                                    }
                                    Some(foreign_model) => foreign_model,
                                };
                                match foreign_model.get_mut(&inverse) {
                                    Some(Bson::Array(_array)) => {
                                        // TODO array.push(index of model_id))
                                    }
                                    _ => {
                                        foreign_model.insert(&inverse, Bson::String(model_id.clone()));
                                    }
                                };
                            }
                        }
                    }
                }
            }
            _ => {
                log::error!("Unknown fk type: {local_value:?}");
            }
        }
    }

    Ok(model)
}
