use safer_ffi::char_p;

use crate::{error::DittoError, utils::zstr::ZString};

/// A DQL query string with its arguments
///
/// Most APIs in the Ditto SDK don't take a [`QueryV2`] directly, but instead take a generic
/// parameter that implements [`IntoQuery`], a trait implemented by types that can be turned into a
/// [`QueryV2`].
///
/// Common examples are:
/// - `String` (and string-like types)
/// - `(String, Args)` where `Args` is anything that implements `Serialize`
///
/// ```rust
/// # use dittolive_ditto::prelude::*;
/// # use serde_json::json;
/// let select_query = "SELECT * FROM cars".into_query();
///
/// let insert_query = (
///     "INSERT INTO cars DOCUMENTS (:doc)",
///     json!({"doc": {"foo": "bar"}}),
/// ).into_query();
/// ```
///
/// ## The `Args` type
///
/// This type is generic over its arguments, with the requirement that the arguments must:
/// - implement `Serialize`
/// - serialize to a map-like type (e.g. [`serde_json::json!({"some":
/// "object"})`][serde_json::json] or [`HashMap`][std::collections::HashMap], not a `String`)
///
/// If the arguments are mutated using interior mutability, the result is not specified and may
/// cause logic errors. Note that this is **not** [undefined behaviour][ub], since any errors will
/// be confined to this instance of [`QueryV2`].
///
/// When no arguments are provided, the `Args` type defaults to `()`
///
/// [ub]: https://doc.rust-lang.org/reference/behavior-considered-undefined.html
#[derive(Debug, Clone, PartialEq)]
pub struct QueryV2<Args> {
    pub(crate) string: ZString,
    pub(crate) args: Args,
    pub(crate) args_cbor: Option<Vec<u8>>,
}

/// Types which can be used to construct a [`QueryV2`].
///
/// The main implementors are:
/// - String types (e.g. `String`, `&str`, etc.)
/// - Tuples `(S, A)`, where `S` is a string type, and `A` implements [`Serialize`]
///
/// This conversion may be fallible.
///
/// Note that, due to historical naming reasons, this trait is not used to create a [`Query`].
///
/// [`Query`]: super::query::Query
/// [`Serialize`]: serde::Serialize
pub trait IntoQuery {
    /// The type of the arguments provided with this query
    type Args;

    /// Convert this object into a [`QueryV2`]
    fn into_query(self) -> Result<QueryV2<Self::Args>, DittoError>;
}

/// A query implements [`IntoQuery`] :)
impl<A> IntoQuery for QueryV2<A> {
    type Args = A;
    fn into_query(self) -> Result<QueryV2<Self::Args>, DittoError> {
        Ok(self)
    }
}

impl<Q: ?Sized> IntoQuery for &Q
where
    Q: ToOwned,
    Q::Owned: IntoQuery,
{
    type Args = <Q::Owned as IntoQuery>::Args;
    fn into_query(self) -> Result<QueryV2<Self::Args>, DittoError> {
        let owned = self.to_owned();
        owned.into_query()
    }
}

impl<Q, A> IntoQuery for (Q, A)
where
    Q: IntoQuery<Args = ()>,
    A: serde::Serialize,
{
    type Args = A;
    fn into_query(self) -> Result<QueryV2<Self::Args>, DittoError> {
        let (string, args) = self;
        let string = string.into_query()?.string;
        let args_cbor = serde_cbor::to_vec(&args).unwrap();

        Ok(QueryV2 {
            string,
            args,
            args_cbor: Some(args_cbor),
        })
    }
}

impl IntoQuery for String {
    type Args = ();
    fn into_query(self) -> Result<QueryV2<Self::Args>, DittoError> {
        Ok(QueryV2 {
            string: char_p::new(self).into(),
            args: (),
            args_cbor: None,
        })
    }
}
