use std::{
    collections::{hash_map, HashMap, HashSet},
    future::Future,
    ops::Deref,
    pin::{pin, Pin},
};

use crate::prelude::*;
use futures::FutureExt;
use iref::IriBuf;

use rexie::{ObjectStore, Rexie, Store, Transaction, TransactionMode};
use taganak_core::default_graph_view;
use taganak_orm::GraphORM;

use async_stream::stream;

use crate::graphs::{ConfigurableGraph, GraphConfig};

use super::FakeSend;

const DEFAULT_DB: &str = "";
const DB_VERSION: u32 = 1;

#[derive(Debug)]
pub struct IndexedDbSourceGraph {
    config: IndexedDbSourceGraphConfig,
}

#[derive(Debug, Clone, PartialEq, Eq, GraphORM, Hash)]
#[rdf(
    prefix(ns = "sdk", iri = "http://sdk.taganak.net/vocab/taganak#"),
    rdf_type = "sdk:IndexedDbGraphConfig"
)]
pub struct IndexedDbSourceGraphConfig {
    #[rdf(predicate = "sdk:graphName")]
    graph_name: String,
}

impl IndexedDbSourceGraphConfig {
    pub fn new(source_iri: IriBuf) -> Self {
        Self {
            // FIXME clean up all that IRI mess for this store
            graph_name: format!("<{}>", source_iri.to_string()),
        }
    }
}

impl GraphConfig for IndexedDbSourceGraphConfig {
    type Graph = IndexedDbSourceGraph;
}

impl ConfigurableGraph for IndexedDbSourceGraph {
    type Config = IndexedDbSourceGraphConfig;

    fn from_config(config: Self::Config) -> Result<Self, SourceError> {
        tracing::debug!(
            "Instantiating IndexedDbGraph for database {}",
            config.graph_name,
        );
        Ok(Self { config })
    }
}

impl IndexedDbSourceGraph {
    async fn db(&self) -> Result<Rexie, SourceError> {
        let store = ObjectStore::new("triples");
        tracing::trace!("Acquiring database {}", self.config.graph_name);

        Rexie::builder(&self.config.graph_name)
            .version(DB_VERSION)
            .add_object_store(store)
            .build()
            .await
            .map_err(|_| SourceError::UnsupportedSourceConfig)
    }

    async fn store(&self) -> (Transaction, Store) {
        let transaction = self
            .db()
            .await
            .unwrap()
            .transaction(&["triples"], TransactionMode::ReadWrite)
            .unwrap();
        let store = transaction.store("triples").unwrap();
        (transaction, store)
    }
}

impl<'g> GraphView for &'g IndexedDbSourceGraph {
    type Stream =
        std::pin::Pin<Box<dyn Stream<Item = Result<Arc<Triple>, GraphError>> + 'g + Send>>;

    fn stream(self) -> impl core::future::Future<Output = Result<Self::Stream, GraphError>> + Send {
        async move {
            Ok(Box::pin(FakeSend::new(stream! {
                // FIXME error handling
                let transaction = self.db().await.unwrap().transaction(&["triples"], TransactionMode::ReadOnly).unwrap();
                let store = transaction.store("triples").unwrap();

                // FIXME error handling
                let keys = store.get_all_keys(None, None).await.unwrap();
                for key in keys {
                    if let Some(value) = store.get(key.clone()).await.unwrap() {
                        let s: String = serde_wasm_bindgen::from_value(key).unwrap();
                        let s_term: Arc<Term> = Arc::new(s.try_into().unwrap());

                        let pos: HashMap<String, HashSet<String>> =
                            serde_wasm_bindgen::from_value(value).unwrap();

                        for (p, os) in pos.iter() {
                            let p_term: Arc<Term> = Arc::new(p.clone().try_into().unwrap());
                            for o in os.iter() {
                                yield
                                    Ok(Triple::new(
                                        s_term.clone(),
                                        p_term.clone(),
                                        Arc::new(o.clone().try_into().unwrap()),
                                    )
                                    .unwrap());

                            }
                        }
                    }
                }

                transaction.done().await.unwrap();
            })) as std::pin::Pin<Box<_>>)
        }
    }

    default_graph_view!();
}

impl Graph for IndexedDbSourceGraph {
    type View<'g> = &'g Self;

    fn view(&self) -> impl core::future::Future<Output = Self::View<'_>> + Send {
        core::future::ready(self)
    }

    fn add(
        &mut self,
        triple: Arc<Triple>,
    ) -> impl core::future::Future<Output = Result<(), GraphError>> + Send {
        FakeSend::new(async move {
            // FIXME error handling
            let transaction = self
                .db()
                .await
                .unwrap()
                .transaction(&["triples"], TransactionMode::ReadWrite)
                .unwrap();
            let store = transaction.store("triples").unwrap();

            let key: wasm_bindgen::JsValue = triple.subject().to_string().into();
            let mut pos: HashMap<String, HashSet<String>> =
                match store.get(key.clone()).await.unwrap() {
                    Some(v) => serde_wasm_bindgen::from_value(v).unwrap(),
                    None => HashMap::new(),
                };

            tracing::trace!(?key, ?pos, "Current value");
            pos.entry(triple.predicate().to_string())
                .or_default()
                .insert(triple.object().to_string());
            tracing::trace!(?key, ?pos, "New value");

            store
                .put(&serde_wasm_bindgen::to_value(&pos).unwrap(), Some(&key))
                .await
                .unwrap();

            transaction.done().await.unwrap();

            Ok(())
        })
    }

    fn remove(
        &mut self,
        triple: TripleRef<'_>,
    ) -> impl core::future::Future<Output = Result<(), GraphError>> + Send {
        FakeSend::new(async move {
            // FIXME error handling
            let transaction = self
                .db()
                .await
                .unwrap()
                .transaction(&["triples"], TransactionMode::ReadWrite)
                .unwrap();
            let store = transaction.store("triples").unwrap();

            let key: wasm_bindgen::JsValue = triple.subject().to_string().into();
            if !store.key_exists(key.clone()).await.unwrap() {
                return Ok(());
            }
            let mut pos: HashMap<String, HashSet<String>> =
                match store.get(key.clone()).await.unwrap() {
                    Some(v) => serde_wasm_bindgen::from_value(v).unwrap(),
                    None => return Ok(()),
                };

            tracing::trace!(?key, ?pos, "Current value");
            match pos.entry(triple.predicate().to_string()) {
                hash_map::Entry::Occupied(entry) => {
                    entry.into_mut().remove(&triple.object().to_string());
                }
                hash_map::Entry::Vacant(_) => return Ok(()),
            }
            tracing::trace!(?key, ?pos, "New value");

            store
                .put(&serde_wasm_bindgen::to_value(&pos).unwrap(), Some(&key))
                .await
                .unwrap();

            transaction.done().await.unwrap();

            Ok(())
        })
    }

    fn merge<G>(
        &mut self,
        source_graph: G,
    ) -> impl core::future::Future<Output = Result<(), GraphError>> + Send
    where
        G: GraphView,
    {
        FakeSend::new(async move {
            tracing::debug!(?self, "Merging from another graph into IndexedDbGraph");

            // FIXME error handling
            let (mut transaction, mut store) = self.store().await;

            let mut subjects = source_graph
                .clone()
                .subjects(None, None, None)
                .await
                .unwrap();
            while let Some(subject) = subjects.next().await {
                // FIXME error handling
                let subject = subject.unwrap();

                tracing::trace!(?subject, "Handling next subject");

                (transaction, store) = self.store().await;
                let key: wasm_bindgen::JsValue = subject.to_string().into();
                let mut pos: HashMap<String, HashSet<String>> =
                    match store.get(key.clone()).await.unwrap() {
                        Some(v) => serde_wasm_bindgen::from_value(v).unwrap(),
                        None => HashMap::new(),
                    };
                tracing::trace!(?key, ?pos, "Current value");

                let describe = source_graph
                    .clone()
                    .describe(subject.as_ref(), None)
                    .await
                    .unwrap();
                let mut predicates = describe.clone().predicates(None, None, None).await.unwrap();

                while let Some(predicate) = predicates.next().await {
                    // FIXME error handling
                    let predicate = predicate.unwrap();

                    tracing::trace!(?subject, ?predicate, "Handling next predicate");

                    let os = pos.entry(predicate.to_string()).or_default();
                    let mut objects = describe
                        .clone()
                        .objects(None, Some(predicate.as_ref()), None)
                        .await
                        .unwrap();
                    while let Some(object) = objects.next().await {
                        // FIXME error handling
                        let object = object.unwrap();

                        tracing::trace!(?subject, ?predicate, ?object, "Handling next object");

                        os.insert(object.to_string());
                    }
                }
                tracing::trace!(?key, ?pos, "New value");

                (transaction, store) = self.store().await;
                store
                    .put(&serde_wasm_bindgen::to_value(&pos).unwrap(), Some(&key))
                    .await
                    .unwrap();
            }

            tracing::debug!("Merge done; waiting for transaction to finish");
            transaction.done().await.unwrap();

            Ok(())
        })
    }
}
