use super::Options;
use crate::storage::Error;
use crate::storage::associations::{BelongsTo, HasMany};
use crate::storage::session_store::SessionStore;
use crate::storage::session_store::session_store::FindOptions;
use async_trait::async_trait;
use pbbson::Model;
use pbbson::bson::Bson;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::Duration;

struct DocHolder {
    docs: Mutex<HashMap<String, Model>>,
}

pub struct MemorySessionStore<B: Clone + Send + Sync + ToString> {
    doc_holders: Mutex<HashMap<String, Arc<DocHolder>>>,

    #[allow(unused)]
    belongs_tos_by_bucket: HashMap<String, Vec<BelongsTo<B>>>,

    #[allow(unused)]
    has_manys_by_bucket: HashMap<String, Vec<HasMany<B>>>,
}

impl<B: Clone + Send + Sync + ToString> MemorySessionStore<B> {
    pub fn new(options: Options<B>) -> Self {
        Self {
            belongs_tos_by_bucket: options.belongs_tos_by_bucket,
            doc_holders: Mutex::new(HashMap::new()),
            has_manys_by_bucket: options.has_manys_by_bucket,
        }
    }
}

#[async_trait]
impl<B: Clone + Send + Sync + ToString> SessionStore<B> for MemorySessionStore<B> {
    async fn create(&self, bucket: B, model: Model, _maybe_expires: Option<Duration>) -> Result<Model, Error> {
        let doc_holder = self.doc_holder(bucket.clone())?;
        let mut model = model.clone();
        let model_id = match model.id() {
            Ok(id) => id,
            _ => {
                let id = xid::new().to_string();
                model.insert("id", id.clone());
                id
            }
        };
        let mut docs = doc_holder
            .docs
            .lock()
            .map_err(|e| Error::failed_precondition(e.to_string()))?;

        // Store
        docs.insert(model_id.clone(), model.clone());
        Ok(model)
    }

    async fn delete(&self, bucket: B, id: &str) -> Result<(), Error> {
        let doc_holder = self.doc_holder(bucket.clone())?;
        let mut docs = doc_holder
            .docs
            .lock()
            .map_err(|e| Error::failed_precondition(e.to_string()))?;
        if docs.remove(id).is_none() {
            return Err(Error::not_found("No such record"));
        };
        Ok(())
    }

    async fn find(&self, bucket: B, id: &str) -> Result<Model, Error> {
        let doc_holder = self.doc_holder(bucket)?;
        let docs = doc_holder
            .docs
            .lock()
            .map_err(|e| Error::failed_precondition(e.to_string()))?;
        let model = match docs.get(id) {
            None => return Err(Error::not_found("No such record")),
            Some(model) => model,
        };
        Ok(model.clone())
    }

    async fn find_many(&self, bucket: B, options: FindOptions) -> Result<Vec<Model>, Error> {
        let doc_holder = self.doc_holder(bucket)?;
        let docs = doc_holder
            .docs
            .lock()
            .map_err(|e| Error::failed_precondition(e.to_string()))?;
        let models = docs
            .iter()
            .filter_map(|(id, model)| match matches_filter(id, model, &options.filter) {
                true => Some(model.clone()),
                _ => None,
            })
            .collect();
        Ok(models)
    }

    async fn update(&self, bucket: B, model: Model, maybe_expires: Option<Duration>) -> Result<Model, Error> {
        let _ = self.find(bucket.clone(), &model.id()?).await?;
        self.create(bucket, model, maybe_expires).await
    }
}

fn matches_filter(id: &String, model: &Model, filter: &Model) -> bool {
    for (filter_key, filter_value) in filter.iter() {
        if filter_key == "id" {
            if &Bson::String(id.to_owned()) != filter_value {
                return false;
            }
        } else if model.get(filter_key) != Some(filter_value) {
            return false;
        }
    }
    true
}

impl<B: Clone + Send + Sync + ToString> MemorySessionStore<B> {
    fn doc_holder(&self, bucket: B) -> Result<Arc<DocHolder>, Error> {
        let mut doc_holders = self
            .doc_holders
            .lock()
            .map_err(|e| Error::failed_precondition(e.to_string()))?;
        match doc_holders.get(&bucket.to_string()) {
            Some(doc_holder) => Ok(doc_holder.clone()),
            None => {
                let doc_holder = Arc::new(DocHolder {
                    docs: Default::default(),
                });
                doc_holders.insert(bucket.to_string(), doc_holder.clone());
                Ok(doc_holder)
            }
        }
    }
}
