//! Use [`ditto.store()`] to access the [`Store`] API to read, write, and remove documents.
//!
//! The `Store` provides two interfaces for interacting with data: Ditto Query Language
//! (DQL), and the legacy "Query Builder" API. Where possible, we recommend developing new
//! functionality using DQL, as we will eventually deprecate Query Builder.
//!
//! - [See the `dql` module docs for examples of DQL queries in action][0]
//!
//! [`ditto.store()`]: crate::prelude::Ditto::store
//! [0]: crate::dql

use_prelude!();

use std::{
    ops::Deref,
    sync::{
        atomic::{self, AtomicU64},
        RwLock, Weak,
    },
};

use ffi_sdk::FfiStoreObserver;

use crate::{debug, error};

pub mod attachment;
mod document_id;
mod observer;
pub mod transactions;

use self::attachment::{DittoAttachmentFetcher, FetcherVersion};
pub use self::{
    document_id::DocumentId,
    observer::{ChangeHandler, ChangeHandlerWithSignalNext, SignalNext, StoreObserver},
};
use crate::{
    ditto::DittoFields,
    dql::{query::IntoQuery, *},
    error::{DittoError, ErrorKind},
    utils::{extension_traits::FfiResultIntoRustResult, SetArc},
};

type CancelToken = u64;

/// Use [`ditto.store()`] to access the [`Store`] API to read, write, and remove documents.
///
/// [See the `store` module for guide-level docs and examples][0].
///
/// [`ditto.store()`]: crate::prelude::Ditto::store
/// [0]: crate::store
#[derive(Clone)]
pub struct Store {
    ditto: Arc<ffi_sdk::BoxedDitto>,
    // FIXME(Daniel): unify this field with `.ditto`
    weak_ditto_fields: Weak<DittoFields>,
    #[allow(clippy::type_complexity)]
    attachment_fetchers: Arc<
        RwLock<HashMap<CancelToken, (bool, DittoAttachmentFetcher<'static, FetcherVersion::V2>)>>,
    >,
}

impl Store {
    pub(crate) fn new(
        ditto: Arc<ffi_sdk::BoxedDitto>,
        weak_ditto_fields: Weak<DittoFields>,
    ) -> Self {
        Self {
            ditto,
            weak_ditto_fields,
            attachment_fetchers: <_>::default(),
        }
    }

    /// Installs and returns a store observer for a query, configuring Ditto to
    /// trigger the passed in change handler whenever documents in the local
    /// store change such that the result of the matching query changes. The
    /// passed in query must be a `SELECT` query, otherwise an error will be
    /// returned.
    ///
    /// # Example
    ///
    /// ```
    /// use dittolive_ditto::prelude::*;
    /// # fn main() -> anyhow::Result<()> {
    /// # let (_root, ditto) = dittolive_ditto::doctest_helpers::doctest_ditto();
    ///
    /// let store = ditto.store();
    /// let _observer = store.register_observer(
    ///     "SELECT * FROM cars WHERE color = 'blue'",
    ///     move |query_result| {
    ///         for item in query_result.iter() {
    ///             // ... handle each item
    ///         }
    ///     },
    /// )?;
    /// # Ok(())
    /// # }
    /// ```
    ///
    /// The first invocation of the change handler will always happen after
    /// this method has returned.
    ///
    /// The observer will remain active until:
    ///
    /// - the [`StoreObserver`] handle gets dropped,
    /// - the [`observer.cancel()`] method is called, or
    /// - the owning [`Ditto`] instance has shut down
    ///
    /// Observer callbacks will never be called concurrently. That is, one callback
    /// must return before the observer will call the handler again. See
    /// [`ditto.store().register_observer_with_signal_next(...)`] if you want
    /// to manually signal readiness for the next callback.
    ///
    /// [`ditto.store().register_observer_with_signal_next(...)`]: crate::store::Store::register_observer_with_signal_next
    /// [`observer.cancel()`]: crate::store::StoreObserver::cancel
    /// [`Ditto`]: crate::prelude::Ditto
    pub fn register_observer<Q, F>(
        &self,
        query: Q,
        on_change: F,
    ) -> Result<Arc<StoreObserver>, DittoError>
    where
        Q: IntoQuery,
        Q::Args: Serialize,
        F: ChangeHandler,
    {
        let ditto = Ditto::upgrade(&self.weak_ditto_fields)?;
        let query = query.into_query()?;

        let observer = Arc::new(StoreObserver::new(
            &ditto,
            &query.string,
            query.args_cbor.as_deref(),
            on_change,
        )?);
        Ok(observer)
    }

    /// Installs and returns a store observer for a query, configuring Ditto to
    /// trigger the passed in change handler whenever documents in the local
    /// store change such that the result of the matching query changes. The
    /// passed in query must be a `SELECT` query, otherwise an error will be
    /// returned.
    ///
    /// Here, a function is passed as an additional argument to the change
    /// handler. This allows the change handler to control how frequently
    /// it is called. See [`register_observer`] for a convenience method that
    /// automatically signals the next invocation.
    ///
    /// # Example
    ///
    /// ```
    /// use dittolive_ditto::prelude::*;
    /// # fn main() -> anyhow::Result<()> {
    /// # let (_root, ditto) = dittolive_ditto::doctest_helpers::doctest_ditto();
    ///
    /// let store = ditto.store();
    /// let _observer = store.register_observer_with_signal_next(
    ///     "SELECT * FROM cars WHERE color = 'blue'",
    ///     move |query_result, signal_next| {
    ///         for item in query_result.iter() {
    ///             // ... handle each item
    ///         }
    ///
    ///         // Call `signal_next` when you're ready for the next callback
    ///         signal_next();
    ///     },
    /// )?;
    /// # Ok(())
    /// # }
    /// ```
    ///
    /// The first invocation of the change handler will always happen after
    /// this method has returned.
    ///
    /// The observer will remain active until:
    ///
    /// - the [`StoreObserver`] handle gets dropped,
    /// - the [`StoreObserver::cancel`] method is called, or
    /// - the owning [`Ditto`] instance has shut down
    ///
    /// After invoking the callback once, the observer will wait to deliver
    /// another callback until after you've called [`signal_next`].
    ///
    /// [`register_observer`]: Store::register_observer
    /// [`signal_next`]: crate::store::SignalNext
    pub fn register_observer_with_signal_next<Q, F>(
        &self,
        query: Q,
        on_change: F,
    ) -> Result<Arc<StoreObserver>, DittoError>
    where
        Q: IntoQuery,
        Q::Args: Serialize,
        F: ChangeHandlerWithSignalNext,
    {
        let ditto = Ditto::upgrade(&self.weak_ditto_fields)?;
        let query = query.into_query()?;

        let new_obs = Arc::new(StoreObserver::with_signal_next(
            &ditto,
            &query.string,
            query.args_cbor.as_deref(),
            on_change,
        )?);
        Ok(new_obs)
    }

    /// Gets temporary access to the set of currently registered observers.
    ///
    /// A (read) lock is held until the return value is dropped: this means
    /// that neither [`Self::register_observer()`] nor
    /// [`StoreObserver::cancel()`] can make progress until this read
    /// lock is released.
    pub fn observers(&self) -> impl '_ + Deref<Target = SetArc<StoreObserver>> {
        let observers: repr_c::Vec<repr_c::Box<FfiStoreObserver>> =
            ffi_sdk::dittoffi_store_observers(&self.ditto);
        let observers: Vec<_> = observers.into();
        let observers = observers
            .into_iter()
            .map(|handle: repr_c::Box<FfiStoreObserver>| Arc::new(StoreObserver { handle }))
            .collect::<SetArc<_>>();

        Box::new(observers)
    }

    /// Executes the given query in the local store and returns the result.
    ///
    /// # Example
    ///
    /// ```
    /// use dittolive_ditto::prelude::*;
    /// use dittolive_ditto::dql::QueryResult;
    /// # #[tokio::main]
    /// # async fn main() -> anyhow::Result<()> {
    /// # let (_root, ditto) = dittolive_ditto::doctest_helpers::doctest_ditto();
    ///
    /// // Query a collection
    /// let result = ditto.store().execute("SELECT * FROM cars").await?;
    ///
    /// // Insert a document into a collection
    /// let insert_result: QueryResult = ditto
    ///     .store()
    ///     .execute((
    ///          "INSERT INTO cars DOCUMENTS (:newCar)",
    ///          serde_json::json!({
    ///              "newCar": {
    ///                  "make": "ford",
    ///                  "color": "blue"
    ///              }
    ///          })
    ///     ))
    ///     .await?;
    /// # Ok(())
    /// # }
    /// ```
    ///
    /// Use placeholders to incorporate values from the optional `query_args`
    /// parameter into the query. The keys of the query arguments object must
    /// match the placeholders used within the query. You can not use placeholders
    /// in the `FROM` clause.
    ///
    /// This method only returns results from the local store without waiting for any
    /// [`SyncSubscription`]s to have caught up with the
    /// latest changes. Only use this method if your program must proceed with immediate results.
    ///
    /// Use [`ditto.store().register_observer(...)`] to receive updates to query results
    /// as soon as they have been synced to this peer.
    ///
    /// ## Query parameter
    ///
    /// The `query` parameter must implement [`IntoQuery`], which is a trait that is implemented by
    /// objects that can be turned into a query string along with the relevant query arguments.
    ///
    /// For queries with no arguments, a [`String`] is sufficient.
    ///
    /// [`SyncSubscription`]: crate::sync::SyncSubscription
    /// [`ditto.store().register_observer(...)`]: crate::store::Store::register_observer
    pub async fn execute<Q>(&self, query: Q) -> Result<QueryResult, DittoError>
    where
        Q: IntoQuery,
        Q::Args: serde::Serialize,
    {
        let query = query.into_query()?;
        let query_string = (&*query.string).into();
        let query_args = query.args_cbor.as_deref().map(Into::into);

        let ffi_query_result =
            ffi_sdk::dittoffi_try_exec_statement(&self.ditto, query_string, query_args)
                .into_rust_result()?;

        Ok(QueryResult::from(ffi_query_result))
    }

    /// Creates a new attachment, which can then be inserted into a document.
    ///
    /// The file residing at the provided path will be copied into Ditto’s store. The
    /// [`DittoAttachment`] object that is returned is what you can
    /// then use to insert an attachment into a document.
    ///
    /// You can provide custom user data about the attachment, which will be replicated to other
    /// peers alongside the file attachment.
    pub async fn new_attachment(
        &self,
        filepath: &(impl ?Sized + AsRef<Path>),
        user_data: HashMap<String, String>,
    ) -> Result<DittoAttachment, DittoError> {
        DittoAttachment::from_file_and_metadata(filepath, user_data, &self.ditto)
    }

    /// Creates a new attachment from in-memory data
    ///
    /// Refer to [`new_attachment`](Self::new_attachment) for additional information.
    pub async fn new_attachment_from_bytes(
        &self,
        bytes: &(impl ?Sized + AsRef<[u8]>),
        user_data: HashMap<String, String>,
    ) -> Result<DittoAttachment, DittoError> {
        DittoAttachment::from_bytes_and_metadata(bytes, user_data, &self.ditto)
    }

    /// Fetches the attachment corresponding to the provided attachment token.
    /// - `attachment_token`: can be either a [`DittoAttachmentToken`], or a `&BTreeMap<CborValue,
    ///   CborValue>`, that is, the output of a [`QueryResultItem::value()`] once casted
    ///   [`.as_object()`][crate::prelude::CborValueGetters::as_object()].
    ///
    /// - `on_fetch_event`: A closure that will be called when the status of the request to fetch
    ///   the attachment has changed. If the attachment is already available then this will be
    ///   called almost immediately with a completed status value.
    ///
    /// The returned [`DittoAttachmentFetcher`] is a handle which is safe to discard, unless you
    /// wish to be able to [`.cancel()`][DittoAttachmentFetcher::cancel] the fetching operation.
    /// When not explicitly cancelled, the fetching operation will remain active until it either
    /// completes, the attachment is deleted, or the owning [`Ditto`] object is dropped.
    pub fn fetch_attachment(
        &self,
        attachment_token: impl attachment::DittoAttachmentTokenLike,
        on_fetch_event: impl 'static + Send + Sync + Fn(DittoAttachmentFetchEvent),
    ) -> Result<DittoAttachmentFetcher<'static, FetcherVersion::V2>, DittoError> {
        let attachment_token = attachment_token.parse_attachment_token()?;

        let weak_ditto = self.weak_ditto_fields.clone();
        let ditto = weak_ditto
            .upgrade()
            .ok_or(ErrorKind::ReleasedDittoInstance)?;

        let mut attachment_fetchers_lockguard = self.attachment_fetchers.write().unwrap();
        let fetcher = DittoAttachmentFetcher::new(
            attachment_token,
            Some(&ditto),
            &self.ditto,
            // Shim around `on_fetch_event` to `cancel` on completion.
            move |event, cancel_token: &AtomicU64| {
                let has_finished = matches! {
                    event,
                    | DittoAttachmentFetchEvent::Completed { .. }
                    | DittoAttachmentFetchEvent::Deleted
                };
                on_fetch_event(event);
                if has_finished {
                    if let Some(ditto) = weak_ditto.upgrade() {
                        let mut attachment_fetchers_inner_lockguard =
                            ditto.store.attachment_fetchers.write().unwrap();
                        // Relaxed is fine thanks to the lock.
                        let cancel_token = cancel_token.load(atomic::Ordering::Relaxed);
                        ditto.store.unregister_fetcher(
                            cancel_token,
                            Some(&mut *attachment_fetchers_inner_lockguard),
                        );
                    }
                }
            },
        )?;
        let (cancel_token, was_zero) = fetcher.cancel_token_ensure_unique();
        attachment_fetchers_lockguard.insert(cancel_token, (was_zero, fetcher.clone()));
        Ok(fetcher)
    }

    fn unregister_fetcher(
        &self,
        mut fetcher_cancel_token: CancelToken,
        fetchers: Option<
            &mut HashMap<CancelToken, (bool, DittoAttachmentFetcher<'static, FetcherVersion::V2>)>,
        >,
    ) -> bool {
        let mut lock_guard = None;
        let fetchers = fetchers.unwrap_or_else(|| {
            &mut **lock_guard.get_or_insert(self.attachment_fetchers.write().unwrap())
        });

        let Some((was_zero, removed_fetcher)) = fetchers.remove(&fetcher_cancel_token) else {
            return false;
        };
        drop(lock_guard);

        if was_zero {
            fetcher_cancel_token = 0;
        }

        let att_token = &removed_fetcher.context.token;

        #[allow(deprecated)] // Workaround for patched tracing
        {
            debug!(
                token_id = %att_token.id(),
                %fetcher_cancel_token,
                "unregistering ditto attachment fetcher"
            );
        }

        let status = ffi_sdk::ditto_cancel_resolve_attachment(
            &self.ditto,
            att_token.id.as_ref().into(),
            fetcher_cancel_token,
        );

        if status != 0 {
            #[allow(deprecated)] // Workaround for patched tracing
            {
                error!(
                    token_id = %att_token.id(),
                    %fetcher_cancel_token,
                    "failed to clean up attachment fetcher"
                );
            }
        }
        status == 0
    }

    /// Gets a copy of the set of currently registered attachment fetchers.
    ///
    /// A (read) lock is held during the copy: this contends with [`Self::fetch_attachment()`] and
    /// with [`DittoAttachmentFetcher::cancel()`].
    pub fn attachment_fetchers(&self) -> Vec<DittoAttachmentFetcher<'static, FetcherVersion::V2>> {
        self.attachment_fetchers
            .read()
            .unwrap()
            .iter()
            .map(|(_, (_, fetcher))| fetcher.clone())
            .collect()
    }
}

/// Specify the order of returned Documents in a query.
#[deprecated(since = "5.1.0", note = "Use DQL `ORDER BY` clauses instead.")]
#[non_exhaustive]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum SortDirection {
    /// First result is "smallest", last result is "largest"
    Ascending,

    /// First result is "largest", last result is "smallest"
    Descending,
}
