use crate::dql::QueryResult;

use_prelude!();

impl Store {
    /// Executes multiple DQL queries within a single atomic transaction.
    ///
    /// ```rust,no_run
    /// # use dittolive_ditto::prelude::*;
    /// # use serde_json::json;
    /// # #[tokio::main]
    /// # async fn main() {
    /// # let ditto: Ditto = todo!();
    /// ditto.store().transaction(async |txn| {
    ///   txn.execute((
    ///     "INSERT INTO users DOCUMENTS (:doc)",
    ///     json!({"name": "alice"}),
    ///   )).await?;
    ///
    ///   txn.execute((
    ///     "INSERT INTO users DOCUMENTS (:doc)",
    ///     json!({"name": "bob"}),
    ///   )).await?;
    ///
    ///   Ok::<_, DittoError>(TransactionCompletionAction::Commit)
    /// }).await.unwrap();
    /// # }
    /// ```
    /// The closure returns a [`Result<T, E>`][core::result::Result], and in the success case, this
    /// value will be returned from the call to
    /// [`ditto.store().transaction()`][Store::transaction]. However, if the value is a
    /// [`TransactionCompletionAction`] (i.e. if `T = TransactionCompletionAction`), then this
    /// value is used to determine the behaviour of the transaction:
    /// - if the value is [`TransactionCompletionAction::Commit`], the transaction is committed
    /// - if the value is [`TransactionCompletionAction::Rollback`], the transaction is rolled back
    /// - if the value is any other type, the transaction is implicitly committed
    ///
    /// See the "errors" section below for more information on the error case.
    ///
    /// This ensures that either all statements are executed successfully, or none are executed at
    /// all, providing strong consistency guarantees. Certain mesh configurations may impose
    /// limitations on these guarantees. For more details, refer to the [Ditto
    /// documentation](https://ditto.com/link/sdk-latest-crud-transactions). Transactions are
    /// initiated as read-write transactions by default, and only a single read-write transaction
    /// is being executed at any given time. Any other read-write transaction started concurrently
    /// will wait until the current transaction has been committed or rolled back. Therefore, it is
    /// crucial to make sure a transaction finishes as early as possible so other read-write
    /// transactions aren't blocked for a long time.
    ///
    /// A transaction can also be configured to be read-only, or given an option debugging `hint`
    /// via [`ditto.store.transaction_with_options`][Store::transaction_with_options]. See the docs
    /// for more information.
    ///
    /// # Errors
    ///
    /// See the [module-level docs][module] for more details on error types in transactions.
    ///
    /// If errors occur in an [`execute()`][Transaction::execute] call within a transaction block,
    /// this error can be handled like a normal [`Result`], and the transaction will continue
    /// without being rolled back. If the transaction callback itself returns an [`Err`], the
    /// transaction is implicitly rolled back and the error is propagated to the called.
    ///
    /// For a complete guide on transactions, please refer to the [Ditto
    /// documentation](https://ditto.com/link/sdk-latest-crud-transactions).
    ///
    /// See also - [`Transaction`]
    ///
    /// [module]: crate::store::transactions
    pub async fn transaction<T, E>(
        &self,
        scope: impl AsyncFnOnce(&Transaction) -> Result<T, E>,
    ) -> Result<T, E>
    where
        T: core::any::Any,
        E: From<DittoError>,
    {
        let options = CreateTransactionOptions::default();
        self.transaction_with_options(options, scope).await
    }

    /// Create a transaction with the provided options.
    ///
    /// ```rust,no_run
    /// # use dittolive_ditto::prelude::*;
    /// # use serde_json::json;
    /// # #[tokio::main]
    /// # async fn main() {
    /// # let ditto: Ditto = todo!();
    /// let mut opts = CreateTransactionOptions::new();
    /// opts.hint = Some("debug transaction name");
    /// opts.is_read_only = false;
    ///
    /// ditto.store().transaction_with_options(opts, async |txn| {
    ///   txn.execute((
    ///     "INSERT INTO users DOCUMENTS (:doc)",
    ///     json!({"name": "alice"}),
    ///   )).await?;
    ///
    ///   txn.execute((
    ///     "INSERT INTO users DOCUMENTS (:doc)",
    ///     json!({"name": "bob"}),
    ///   )).await?;
    ///
    ///   Ok::<_, DittoError>(TransactionCompletionAction::Commit)
    /// }).await.unwrap();
    /// # }
    /// ```
    /// If [`is_read_only`][CreateTransactionOptions::is_read_only] is `true`, mutating DQL queries
    /// will error, even if no actual mutation would have occurred.
    ///
    /// See [`ditto.store().transaction()`][Store::transaction] for more documentation on the
    /// behaviour of transactions in general.
    pub async fn transaction_with_options<T, E>(
        &self,
        options: CreateTransactionOptions<'_>,
        scope: impl AsyncFnOnce(&Transaction) -> Result<T, E>,
    ) -> Result<T, E>
    where
        T: core::any::Any,
        E: From<DittoError>,
    {
        let hint = options.hint.map(char_p::new);

        let options = ffi_sdk::BeginTransactionOptions {
            hint: hint.as_ref().map(|s| s.as_ref()),
            is_read_only: options.is_read_only,
        };

        self._transaction_with_options(options, scope).await
    }
}

/// Options for customizing a transaction. Used with [`Store::transaction_with_options`].
///
/// ```rust,no_run
/// # use dittolive_ditto::prelude::*;
/// # #[tokio::main]
/// # async fn main() {
/// # let ditto: Ditto = todo!();
/// let mut options = CreateTransactionOptions::new();
/// options.hint = Some("my transaction name");
/// options.is_read_only = true;
///
/// ditto
///     .store()
///     .transaction_with_options(options, async |txn| {
///         // do transaction stuff
///         Ok::<_, DittoError>(TransactionCompletionAction::Commit)
///     })
///     .await
///     .unwrap();
/// # }
/// ```
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct CreateTransactionOptions<'hint> {
    /// A hint used for debugging and logging
    pub hint: Option<&'hint str>,

    /// Whether the transaction should be created read-only
    pub is_read_only: bool,
}

#[allow(
    clippy::derivable_impls,
    reason = "writing it out makes it clearer what the defaults are"
)]
impl Default for CreateTransactionOptions<'_> {
    fn default() -> Self {
        Self {
            hint: None,
            is_read_only: false,
        }
    }
}

impl CreateTransactionOptions<'_> {
    /// Create a new, default [`CreateTransactionOptions`].
    pub fn new() -> Self {
        Self::default()
    }
}

/// Encapsulates information about a transaction.
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq)]
pub struct TransactionInfo {
    /// A globally unique ID of the transaction.
    pub id: String,

    /// The user hint passed when creating the transaction, useful for debugging
    /// and testing.
    pub hint: Option<String>,

    /// Indicates whether mutating DQL statements can be executed in the transaction. Defaults to
    /// `false`. See [`ditto.store().transaction()`][Store::transaction] for more information.
    pub is_read_only: bool,
}

/// Represents an action that completes a transaction, by either committing it or
/// rolling it back.
#[derive(Debug, Clone, PartialEq)]
#[must_use = "Code should handle whether a transaction succeeded"]
pub enum TransactionCompletionAction {
    /// Represents the action of committing a transaction.
    Commit,

    /// Represents the action of rolling back a transaction.
    Rollback,
}

/// Represents a transaction in the Ditto store.
///
/// A [`Transaction`] is used to group multiple operations into a single atomic unit. This ensures
/// that either all operations within the transaction are applied, or none of them are, maintaining
/// the integrity of the data.
///
/// Please consult the documentation of [`ditto.store().transaction()`][Store::transaction] or the
/// [module-level docs][module] for more information on how to create and use transactions. For a
/// complete guide on transactions, please refer to the [Ditto documentation][docs]
///
/// [module]: crate::store::transactions
/// [docs]: https://ditto.com/link/sdk-latest-crud-transactions
pub struct Transaction {
    pub(crate) ptr: repr_c::Box<ffi_sdk::FfiTransaction>,
}

impl Transaction {
    /// Information about the current transaction.
    pub fn info(&self) -> TransactionInfo {
        self._info()
    }

    /// Executes a DQL query and returns matching items as a [`QueryResult`].
    ///
    /// Note that this method only returns results from the local store without waiting for any
    /// [`SyncSubscription`][crate::sync::SyncSubscription]s to have caught up with the latest
    /// changes. Only use this method if your program must proceed with immediate results. Use a
    /// [`StoreObserver`][crate::store::StoreObserver] (obtained from
    /// [`ditto.store().register_observer()`][crate::store::Store::register_observer]) to receive
    /// updates to query results as soon as they have been synced to this peer.
    pub async fn execute<Q>(&self, query: Q) -> Result<QueryResult, DittoError>
    where
        Q: IntoQuery,
        Q::Args: serde::Serialize,
    {
        self._execute(query).await
    }
}
