use_prelude!();

use crate::error::{DittoError, ErrorKind};

#[derive(Debug, Clone, Default, PartialOrd, Ord, PartialEq, Eq, Hash)]
/// An identifier for a [`DittoDocument`].
///
/// Each `DocumentId` represents a unique identifier for a document.
pub struct DocumentId {
    pub(crate) bytes: Vec<u8>,
}

impl DocumentId {
    /// Create a new DocumentId from a serializable value.
    /// # panic
    /// This function will panic if the value can't be serialized into cbor.
    pub fn new<V: ::serde::Serialize>(value: &V) -> Result<Self, DittoError> {
        let cbor_bytes = ::serde_cbor::to_vec(value).unwrap();
        let bytes = validate_doc_id_cbor_bytes(cbor_bytes)?;
        Ok(Self { bytes })
    }

    /// Return a String representation of the DocumentId that can be used in a query string.
    pub fn to_query_compatible(
        &self,
        string_primitive_fmt: ffi_sdk::StringPrimitiveFormat,
    ) -> String {
        let str_boxed = ffi_sdk::ditto_document_id_query_compatible(
            self.bytes.as_slice().into(),
            string_primitive_fmt,
        );
        str_boxed.into_string()
    }

    /// Return the inner bytes value in a cbor form
    pub fn value(&self) -> ::serde_cbor::Value {
        self.to_cbor()
    }

    /// Return the inner bytes value in a cbor form
    pub fn to_cbor(&self) -> ::serde_cbor::Value {
        ::serde_cbor::from_slice(&self.bytes[..]).expect("DocumentId can be represented as CBOR")
    }
}

impl From<Vec<u8>> for DocumentId {
    fn from(bytes: Vec<u8>) -> Self {
        Self { bytes }
    }
}

impl From<Box<[u8]>> for DocumentId {
    fn from(bytes: Box<[u8]>) -> Self {
        Self {
            bytes: bytes.into(),
        }
    }
}

impl From<&[u8]> for DocumentId {
    fn from(slice: &[u8]) -> Self {
        Self {
            bytes: slice.to_owned().to_vec(),
        }
    }
}

impl AsRef<[u8]> for DocumentId {
    fn as_ref(&self) -> &[u8] {
        &self.bytes[..]
    }
}

impl std::fmt::Display for DocumentId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{}",
            self.to_query_compatible(ffi_sdk::StringPrimitiveFormat::WithoutQuotes)
        )
    }
}

impl serde::Serialize for DocumentId {
    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        // Note (Ham & Daniel): This serialization was previously done by first
        // deserializing the document ID's CBOR bytes into a `serde_cbor::Value`
        // before then serializing that into the serialized form. This was
        // usually fine because the serializer being used to serialize the
        // `serde_cbor::Value` was almost always `serde_cbor`'s, and so while a
        // little bit of a roundabout method of getting back to some CBOR bytes,
        // it always worked fine.
        //
        // However, if you wanted to `upsert` a document into Ditto by
        // specifying the document's content using `serde_json`'s `json!` macro
        // and you wanted to specify the document's ID by providing a
        // `DocumentId` value under the `_id` at the root of the document then
        // this codepath would get hit and would lead to an error if you were
        // using a document ID that contained an integer value. This is because
        // `serde_cbor::Value` has an `Integer` case that stores all integers as
        // `i128`s. `serde_json` does not support serializing `i128`s and would
        // return an error saying as much. It looks like you'd then be able to
        // get this to work by enabling the `"arbitrary_precision"` feature of
        // `serde_json`. This does mean that you don't get an error returned
        // anymore but integers then end up getting serialized as an object
        // like:
        //
        //   { "$serde_json::private::Number": 42 }
        //
        // This does not work well with in a cross-platform world unfortunately.
        //
        // As such, we're instead using `serde_transcode` for now, to avoid all
        // integers getting temporarily represented as `i128`s. This may or may
        // not end up being a suitable long term solution so is something that
        // may need to be re-evaluated in the future.

        let mut deserializer = serde_cbor::Deserializer::from_slice(&self.bytes[..]);
        serde_transcode::transcode(&mut deserializer, serializer)
    }
}

impl<'de> serde::de::Deserialize<'de> for DocumentId {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::de::Deserializer<'de>,
    {
        let cbor_bytes =
            serde_cbor::to_vec(&serde_cbor::Value::deserialize(deserializer)?).unwrap();
        // We have to go to the trouble of crossing the FFI to ensure that the
        // bytes we've got here are in Ditto document ID canonical CBOR form
        let bytes = validate_doc_id_cbor_bytes(cbor_bytes)
            .expect("document id bytes are valid when deserializing");
        Ok(Self { bytes })
    }
}

fn validate_doc_id_cbor_bytes(bytes: Vec<u8>) -> Result<Vec<u8>, DittoError> {
    use safer_ffi::prelude::{AsOut, ManuallyDropMut};

    let mut out_cbor_slot = None;
    let out_cbor = out_cbor_slot.manually_drop_mut().as_out();
    {
        let res = ffi_sdk::ditto_validate_document_id(bytes[..].into(), out_cbor);
        if res != 0 {
            return Err(DittoError::from_ffi(ErrorKind::Internal));
        }
    }

    Ok(match out_cbor_slot {
        None => bytes,
        Some(cbor_boxed_slice) => cbor_boxed_slice.to::<Box<[u8]>>().into(),
    })
}
