use std::{
    collections::{BTreeMap, HashMap},
    convert::TryFrom,
};

use serde::Deserialize;

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

#[derive(Debug, Clone, Deserialize)]
#[serde(try_from = "DittoAttachmentTokenStoredFormat")]
/// Serves as a token for a specific attachment that you can pass to a call to
/// [`fetch_attachment`](crate::store::collection::Collection::fetch_attachment).
pub struct DittoAttachmentToken {
    pub(crate) id: Box<[u8]>,
    pub(crate) len: u64,
    pub(crate) metadata: HashMap<String, String>,
}

impl DittoAttachmentToken {
    /// Returns the `id` of this attachment, encoded so as to be compatible with the
    /// [`crate::store::dql::Query`] API.
    pub fn id(&self) -> String {
        crate::utils::base64_encode_unpadded(&self.id)
    }

    /// Returns the size, in bytes, of this attachment's data. Compatible with the
    /// [`crate::store::dql::Query`] API.
    #[allow(clippy::len_without_is_empty)]
    pub fn len(&self) -> u64 {
        self.len
    }

    /// Returns the metadata that was associated with this attachment file when the source peer
    /// called [`new_attachment`][crate::store::Store::new_attachment].
    pub fn metadata(&self) -> &HashMap<String, String> {
        &self.metadata
    }
}

impl TryFrom<DittoAttachmentTokenStoredFormat> for DittoAttachmentToken {
    type Error = DittoError;

    fn try_from(value: DittoAttachmentTokenStoredFormat) -> Result<Self, Self::Error> {
        if value._type != (::ffi_sdk::DittoCrdtType::Attachment as u64) {
            return Err(ErrorKind::Internal.into());
        }
        let ret = DittoAttachmentToken {
            id: value._id,
            len: value._len,
            metadata: value._meta,
        };
        Ok(ret)
    }
}

// Helper struct to help deserialize. This is to let us use
// #[derive(Deserialize)] instead of manually implementing it.
// See `impl serde::Serialize for DittoAttachment`
#[derive(Deserialize)]
struct DittoAttachmentTokenStoredFormat {
    /// FIXME(Daniel): we have to use `alias` instead of `rename` so as to avoid
    /// breakage with older Rust SDK versions using `_type` as the key name.
    #[serde(alias = "_ditto_internal_type_jkb12973t4b")]
    _type: u64,
    #[serde(with = "serde_bytes")]
    _id: Box<[u8]>,
    _len: u64,
    _meta: HashMap<String, String>,
}

/// Trait abstracting over [`DittoAttachmentToken`] and a
/// [`QueryResultItem::value()`][crate::store::dql::QueryResultItem::value] matching an input
/// [`DittoAttachment`][super::DittoAttachment].
pub trait DittoAttachmentTokenLike {
    fn parse_attachment_token(self) -> Result<DittoAttachmentToken, DittoError>;
}

impl DittoAttachmentTokenLike for DittoAttachmentToken {
    fn parse_attachment_token(self) -> Result<DittoAttachmentToken, DittoError> {
        Ok(self)
    }
}

impl DittoAttachmentTokenLike for &'_ BTreeMap<CborValue, CborValue> {
    fn parse_attachment_token(self) -> Result<DittoAttachmentToken, DittoError> {
        #[derive(Deserialize)]
        struct DittoAttachmentTokenStoredFormatInQueryResultItem {
            id: Box<str>,
            len: u64,
            metadata: HashMap<String, String>,
        }

        let DittoAttachmentTokenStoredFormatInQueryResultItem { id, len, metadata } =
            ::serde_cbor::from_slice(&::serde_cbor::to_vec(self)?)?;
        let id = crate::utils::base64_decode_unpadded(&id).map_err(|err| {
            DittoError::from_str(
                ErrorKind::InvalidInput,
                format!("expected base64 `.id` field, got: {err}"),
            )
        })?;
        Ok(DittoAttachmentToken { id, len, metadata })
    }
}

impl DittoAttachmentTokenLike for BTreeMap<CborValue, CborValue> {
    fn parse_attachment_token(self) -> Result<DittoAttachmentToken, DittoError> {
        (&self).parse_attachment_token()
    }
}
