//! Use [`ditto.store().collection("...")`] to access the [`Collection`] API.
//!
//! [`ditto.store().collection("...")`]: crate::store::Store::collection

#![allow(deprecated)]

use_prelude!();

// TODO(v5): Remove pub from mod
/// ```
/// use dittolive_ditto::store::collection::document::{DittoDocument, DittoMutDocument};
/// ```
#[doc(hidden)]
pub mod document;
use std::sync::Weak;

pub use self::document::{DittoDocument, DittoMutDocument};

// TODO(v5): remove
/// ```
/// use dittolive_ditto::store::collection::document_id::DocumentId;
/// ```
#[doc(hidden)]
pub mod document_id {
    // Re-exporting new location from old location to prevent hard breakage
    pub use crate::store::DocumentId;
}
pub use self::document_id::DocumentId;

// TODO(v5): Remove pub from mod
/// ```
/// use dittolive_ditto::store::collection::pending_cursor_operation::PendingCursorOperation;
/// ```
#[doc(hidden)]
pub mod pending_cursor_operation;
pub use self::pending_cursor_operation::PendingCursorOperation;

// TODO(v5): Remove pub from mod
/// ```
/// use dittolive_ditto::store::collection::pending_id_specific_operation::PendingIdSpecificOperation;
/// ```
#[doc(hidden)]
pub mod pending_id_specific_operation;
pub use self::pending_id_specific_operation::PendingIdSpecificOperation;

// TODO(v5): Remove pub from mod
/// ```
/// use dittolive_ditto::store::collection::type_traits::MutableValue;
/// ```
#[doc(hidden)]
pub mod type_traits;
pub use self::type_traits::MutableValue;
use crate::{
    error::{DittoError, ErrorKind},
    store::attachment::{
        DittoAttachment, DittoAttachmentFetchEvent, DittoAttachmentFetcher, DittoAttachmentToken,
        FetcherVersion,
    },
};

/// Use [`ditto.store().collection("...")?`] to query or mutate documents in a [`Collection`].
///
/// [`ditto.store().collection("...")?`]: crate::store::Store::collection
#[deprecated(
    since = "4.14.4",
    note = "Will be removed in SDK 5.0. Use DQL queries instead"
)]
#[derive(Clone, Debug)]
pub struct Collection {
    pub(crate) ditto: Weak<BoxedDitto>,
    pub(crate) collection_name: char_p::Box,
}

impl Collection {
    pub(crate) fn new(ditto: Weak<BoxedDitto>, collection_name: String) -> Self {
        let collection_name = char_p::new(collection_name.as_str());
        Self {
            ditto,
            collection_name,
        }
    }

    /// Returns the name of this `Collection`.
    ///
    /// # Example
    ///
    /// ```
    /// # use dittolive_ditto::prelude::*;
    /// # fn example(ditto: &Ditto) -> anyhow::Result<()> {
    /// let cars = ditto.store().collection("cars")?;
    /// assert_eq!(cars.name(), "cars");
    /// # Ok(())
    /// # }
    /// ```
    pub fn name(&self) -> &str {
        self.collection_name.as_ref().to_str()
    }

    /// Query all documents in the named collection.
    ///
    /// This produces a [`PendingCursorOperation`] which may be used to perform
    /// subsequent operations on the queried documents, in this case all documents
    /// in the named collection.
    ///
    /// # Example
    ///
    /// ```
    /// use dittolive_ditto::prelude::*;
    /// # fn example(ditto: &Ditto) -> anyhow::Result<()> {
    /// let collection = ditto.store().collection("cars")?;
    /// let pending_op = collection.find_all();
    /// let documents: Vec<_> = pending_op.exec()?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn find_all(&self) -> PendingCursorOperation<'_> {
        self.find("true")
    }

    /// Queries documents in the named collection according to the given QueryBuilder query string.
    ///
    /// **Note: The query strings accepted here are _NOT_ Ditto-Query-Language (DQL) queries.
    /// If you can achieve your use-case via a DQL query, we recommend you do that instead.
    /// See [the DQL module docs] for more info about DQL**.
    ///
    /// This produces a [`PendingCursorOperation`] which may be used to perform
    /// subsequent operations on the queried documents, in this case all documents
    /// in the named collection.
    ///
    /// For more details about the query syntax accepted here, [see the `query_builder`
    /// module documentation]
    ///
    /// [the DQL module docs]: crate::dql
    /// [see the `query_builder` module documentation]: crate::store::query_builder#legacy-query-syntax
    pub fn find(&self, query: &'_ str) -> PendingCursorOperation<'_> {
        #[allow(deprecated)]
        PendingCursorOperation::<'_>::new(
            self.ditto.clone(),
            self.collection_name.to_owned(),
            query,
            None,
        )
    }

    /// Generates a [`PendingCursorOperation`] with the provided query and query arguments that can
    /// be used to find the documents matching the query at a point in time or you can chain a call
    /// to [`PendingCursorOperation::observe_local`] or [`PendingCursorOperation::subscribe`] if
    /// you want to get updates about documents matching the query as they occur. It can also be
    /// used to update, remove, or evict documents.
    ///
    /// This is the recommended function to use when performing queries on a collection if you have
    /// any dynamic data included in the query string. It allows you to provide a query string with
    /// placeholders, in the form of `$args.my_arg_name`, along with an accompanying dictionary of
    /// arguments, in the form of `{ "my_arg_name": "some value" }`, and the placeholders will be
    /// appropriately replaced by the matching provided arguments from the dictionary. This includes
    /// handling things like wrapping strings in quotation marks and arrays in square brackets, for
    /// example.
    pub fn find_with_args<V: ::serde::Serialize, C: Borrow<V>>(
        &self,
        query: &'_ str,
        query_args: C,
    ) -> PendingCursorOperation<'_> {
        #[allow(deprecated)]
        PendingCursorOperation::<'_>::new(
            self.ditto.clone(),
            self.collection_name.to_owned(),
            query,
            Some(serde_cbor::to_vec(query_args.borrow()).unwrap()),
        )
    }

    /// Generates a [`PendingIdSpecificOperation`] with the provided document Id that can be used
    /// to find the document at a point in time or you can chain a call to
    /// [`PendingIdSpecificOperation::observe_local`] or [`PendingIdSpecificOperation::subscribe`]
    /// if you want to get updates about the document over time. It can also be used to update,
    /// remove, or evict the document.
    pub fn find_by_id(&self, doc_id: impl Into<DocumentId>) -> PendingIdSpecificOperation {
        PendingIdSpecificOperation {
            ditto: self.ditto.clone(),
            collection_name: self.collection_name.to_owned(),
            doc_id: doc_id.into(),
        }
    }
}

/// Convenience impl to avoid having to type `.clone()` everywhere.
impl From<&DocumentId> for DocumentId {
    fn from(doc: &DocumentId) -> Self {
        doc.clone()
    }
}

impl Collection {
    /// Inserts a new document into the collection and returns its Id. If the
    /// document already exists, the provided document content will be merged
    /// with the existing document's content.
    pub fn upsert<V: ::serde::Serialize, C: Borrow<V>>(
        &self,
        content: C,
    ) -> Result<DocumentId, DittoError> {
        self.insert_cbor(
            ::serde_cbor::to_vec(content.borrow()).unwrap().as_slice(),
            WriteStrategy::Merge,
            None,
        )
    }

    /// Inserts a new document into the collection and returns its Id. If the
    /// document already exists, the behavior is determined by the provided
    /// `write_strategy`.
    pub fn upsert_with_strategy<V: ::serde::Serialize, C: Borrow<V>>(
        &self,
        content: C,
        write_strategy: WriteStrategy,
    ) -> Result<DocumentId, DittoError> {
        self.insert_cbor(
            ::serde_cbor::to_vec(content.borrow()).unwrap().as_slice(),
            write_strategy,
            None,
        )
    }

    /// Inserts a new document into the collection and returns its assigned Id.
    /// Use this method when the content has already been serialized externally
    /// into a CBOR-formatted byte array
    pub(crate) fn insert_cbor(
        &self,
        cbor: &'_ [u8],
        write_strategy: WriteStrategy,
        write_txn: Option<&'_ mut ffi_sdk::CWriteTransaction>,
    ) -> Result<DocumentId, DittoError> {
        let ditto = self
            .ditto
            .upgrade()
            .ok_or(crate::error::ErrorKind::ReleasedDittoInstance)?;
        let write_strategy_rs = write_strategy.as_write_strategy_rs();

        let hint: Option<char_p::Ref<'_>> = None;
        let id = {
            ffi_sdk::ditto_collection_insert_value(
                &*ditto,
                self.collection_name.as_ref(),
                cbor.into(),
                write_strategy_rs,
                hint,
                write_txn,
            )
        }
        .ok_or(ErrorKind::InvalidInput)?;

        Ok(id.to::<Box<[u8]>>().into())
    }

    /// Creates a new attachment, which can then be inserted into a document.
    ///
    /// This API is deprecated. Use [`ditto.store().new_attachment()`][Store::new_attachment]
    /// instead.
    ///
    /// The file residing at the provided path will be copied into the 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 metadata about the attachment, which will be replicated to other peers
    /// alongside the file attachment.
    #[doc(hidden)]
    #[deprecated = "Use `ditto.store().new_attachment()` instead"]
    pub fn new_attachment<P: AsRef<Path>>(
        &self,
        filepath: P,
        metadata: HashMap<String, String>,
    ) -> Result<DittoAttachment, DittoError> {
        let ditto = &self
            .ditto
            .upgrade()
            .ok_or(crate::error::ErrorKind::ReleasedDittoInstance)?;
        DittoAttachment::from_file_and_metadata(&filepath, metadata, ditto)
    }

    /// Fetch the attachment corresponding to the provided attachment token.
    ///
    /// This API is deprecated. Use [`ditto.store().fetch_attachment()`][Store::fetch_attachment]
    /// instead.
    ///
    /// - `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.
    #[doc(hidden)]
    #[deprecated = "Use `ditto.store().fetch_attachment()` instead"]
    pub fn fetch_attachment<'a>(
        &self,
        attachment_token: DittoAttachmentToken,
        on_fetch_event: impl 'a + Send + Sync + Fn(DittoAttachmentFetchEvent),
    ) -> Result<DittoAttachmentFetcher<'a, FetcherVersion::V1>, DittoError> {
        let ditto = self
            .ditto
            .upgrade()
            .ok_or(crate::error::ErrorKind::ReleasedDittoInstance)?;
        DittoAttachmentFetcher::new(attachment_token, None, &ditto, move |ev, _| {
            on_fetch_event(ev)
        })
    }
}
