use std::{collections::HashMap, path::PathBuf};

use ffi_sdk::BoxedAttachmentHandle;
use serde::ser::SerializeMap;

use super::ditto_attachment_token::DittoAttachmentToken;
use crate::ditto::{TryUpgrade, WeakDittoHandleWrapper};

#[derive(Debug)]
/// Represents an attachment and can be used to insert the associated attachment into a document at
/// a specific key.
pub struct DittoAttachment {
    id: Box<[u8]>,
    len: u64,
    metadata: HashMap<String, String>,
    ditto: WeakDittoHandleWrapper,
    attachment_handle: BoxedAttachmentHandle,
}

impl serde::Serialize for DittoAttachment {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        let mut map = serializer.serialize_map(Some(4))?;
        map.serialize_entry(
            // FIXME(Daniel): ideally this would be using `_ditto_internal_type_jkb12973t4b`, but
            // that would break compat with older Rust SDK versions: such a Rust SDK (or any
            // *current* non-Rust SDK) inserting an attachment renders any "old" (e.g., 3.0.4) Rust
            // SDK unable to decode its token.
            "_type",
            &(::ffi_sdk::DittoCrdtType::Attachment as u64),
        )?;
        map.serialize_entry("_id", ::serde_bytes::Bytes::new(&self.id[..]))?;
        map.serialize_entry("_len", &self.len)?;
        map.serialize_entry("_meta", &self.metadata)?;
        map.end()
    }
}

impl DittoAttachment {
    /// Create a new DittoAttachment
    pub(crate) fn new(
        id: Box<[u8]>,
        len: u64,
        metadata: HashMap<String, String>,
        ditto: WeakDittoHandleWrapper,
        attachment_handle: BoxedAttachmentHandle,
    ) -> Self {
        Self {
            id,
            len,
            metadata,
            ditto,
            attachment_handle,
        }
    }

    /// Create a new DittoAttachment from a Token
    pub(crate) fn new_with_token(
        token: DittoAttachmentToken,
        ditto: WeakDittoHandleWrapper,
        attachment_handle: BoxedAttachmentHandle,
    ) -> Self {
        Self {
            id: token.id,
            len: token.len,
            metadata: token.metadata,
            ditto,
            attachment_handle,
        }
    }

    /// Return path to an attachment.
    /// # Panics
    /// Panics if Ditto has been released
    pub fn path(&self) -> PathBuf {
        // FIXME(Ronan) Ideally wrap this function in a Result
        let ditto = self.ditto.try_upgrade().unwrap();
        let p =
            unsafe { ffi_sdk::ditto_get_complete_attachment_path(&ditto, &self.attachment_handle) };
        let p_string = p.to_string();
        p_string.into()
    }
}

#[cfg(test)]
mod tests {
    use std::{
        collections::HashMap,
        sync::{
            atomic::{AtomicBool, Ordering},
            Arc, Mutex,
        },
    };

    use serde_json::json;

    use crate::{
        prelude::*,
        store::{
            ditto_attachment_fetch_event::DittoAttachmentFetchEvent,
            ditto_attachment_token::DittoAttachmentToken,
        },
        test_helpers::setup_ditto,
    };

    #[test]
    fn attachment_serialize() {
        let ditto = setup_ditto().unwrap();
        let store = ditto.store();
        let collection = store.collection("test").unwrap();

        let original_test_file_path = "tests/data/attachment_file_1.txt";

        let metadata = {
            let mut m = HashMap::new();
            m.insert("key_1".to_string(), "value_1".to_string());
            m.insert("key_2".to_string(), "value_2".to_string());
            m
        };

        let attachment = collection
            .new_attachment(original_test_file_path, metadata.clone())
            .expect("new_attachment");
        let attachment_id = attachment.id.clone();
        let attachment_len = attachment.len;
        let attachment_file_path = attachment.path();
        assert_ne!(
            original_test_file_path,
            attachment_file_path
                .clone()
                .into_os_string()
                .into_string()
                .unwrap()
        );

        let collection = store.collection("test").unwrap();
        let id = collection.upsert(json!({"hello": "again"})).unwrap();
        let mut doc = collection.find_by_id(id).exec().unwrap();

        let set = doc.set("att", attachment);
        assert!(set.is_ok());

        let attachment_token = doc.get::<DittoAttachmentToken>("att").unwrap();
        assert_eq!(attachment_token.id, attachment_id);
        assert_eq!(attachment_token.len, attachment_len);
        assert_eq!(attachment_token.metadata, metadata);

        let test_file = std::fs::read(original_test_file_path).unwrap();
        let attachment_file = std::fs::read(attachment_file_path).unwrap();

        assert_eq!(test_file, attachment_file);

        assert_eq!(test_file.len() as u64, attachment_len);
    }

    #[test]
    fn attachment_fetch() {
        let ditto = setup_ditto().unwrap();
        let store = ditto.store();
        let collection = store.collection("test").unwrap();

        let original_test_file_path = "tests/data/attachment_file_1.txt";

        let attachment = collection
            .new_attachment(original_test_file_path, HashMap::new())
            .expect("new_attachment");

        let collection = store.collection("test").unwrap();
        let id = collection.upsert(json!({"hello": "again"})).unwrap();
        let mut doc = collection.find_by_id(id).exec().unwrap();

        let set = doc.set("att", attachment);
        assert!(set.is_ok());

        let attachment_token = doc.get::<DittoAttachmentToken>("att").unwrap();

        let finished = Arc::new(AtomicBool::new(false));
        let finished_clone = Arc::clone(&finished);
        let fetched_attachment_data: Arc<Mutex<Vec<u8>>> = Arc::new(Mutex::new(vec![]));

        let _fetcher = collection
            .fetch_attachment(attachment_token, |event| {
                if let DittoAttachmentFetchEvent::Completed { attachment } = event {
                    let att_data_mtx = &*fetched_attachment_data; // move (copy) and reborrow
                    if let Ok(mut fetched_attachment_data) = att_data_mtx.lock() {
                        *fetched_attachment_data = std::fs::read(attachment.path()).unwrap();
                        finished_clone.store(true, Ordering::SeqCst);
                    }
                }
            })
            .unwrap();

        while !finished.load(Ordering::SeqCst) {
            std::thread::yield_now();
        }

        let test_file_data = std::fs::read(original_test_file_path).unwrap();
        let fetched_att_data = fetched_attachment_data.lock().unwrap();
        assert_eq!(test_file_data, *fetched_att_data);
    }
}
