// -- START OF SNIPPET.

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

use dittolive_ditto::prelude::*;
use serde::Serialize;
use serde_json::json;

async fn attachment_doc_snippet(
    peer_a: &Ditto,
    peer_b: &Ditto,
    filepath: &Path,
) -> Result<(), Error> {
    // Creating an attachment in the local ditto store from a file:
    let new_attachment = peer_a
        .store()
        .new_attachment(filepath, <_>::default())
        .await?;

    // Creating an attachment in the local ditto store from data
    // is not available in rust, Js-only API.

    // Using the optional `metadata` parameter to store arbitrary metadata with the attachment
    let user_data = HashMap::from_iter([
        ("name".into(), "image.png".into()),
        // Idea: attach a small "summary" of the file contents to preview them in
        // the UI so that the user has some idea of what to "download"/_fetch_.
        //
        // For a text file, it could be some title or the first words of it.
        // For an image, it could be a base64-encoded thumbnail.
        (
            "thumbnail".into(),
            "/9j/4atg9y_zw0ga_x_bzd_w0g_zg9sb...".into(),
        ),
    ]);

    let _new_attachment_with_metadata = peer_a.store().new_attachment(filepath, user_data).await?;

    // creating a ditto document with an attachment
    let new_document /*: impl Serialize */ = json!({
        "_id": "123",
        "my_attachment": new_attachment,
    });

    // Insert the new document into the collection
    // Note that the `my_attachment` field needs to be declared as an `attachment` data type
    peer_a
        .store()
        .execute_v2((
            r#"
                INSERT INTO COLLECTION cars (my_attachment ATTACHMENT)
                DOCUMENTS (:new_document)
            "#,
            json!({
                "new_document": new_document,
            }),
        ))
        .await?;

    // Updating a ditto document with an attachment
    // (update the document "123" with a new attachment)
    peer_a
        .store()
        .execute_v2((
            r#"
                UPDATE COLLECTION cars (my_attachment ATTACHMENT)
                SET my_attachment = :new_attachment
                WHERE _id = '123'
            "#,
            json!({
                "new_attachment": new_attachment,
            }),
        ))
        .await?;

    wait_for_id_to_be_present_in(peer_b, "123").await?;

    // Fetching the attachment token from a document.
    // (Note: this assumes the `peer_b` has received the document; using
    // `.execute` is thus prone to race conditions lest we busy poll. It is thus
    // advisable to `.register_observer()` instead.
    // See `wait_for_id_to_be_present_in()` for an example).
    let query_result = peer_b
        .store()
        .execute_v2(
            r#"
                SELECT *
                FROM COLLECTION cars (my_attachment ATTACHMENT)
                WHERE _id = '123'
            "#,
        )
        .await?;

    let unreachable = "unreachable given that peer_a inserted the right thing";

    let first_item = query_result
        .get_item(0)
        .expect("unreachable thanks to our wait")
        .value();

    let my_attachment = first_item.get("my_attachment").expect(unreachable);
    let attachment_token = my_attachment.as_object().expect(unreachable);

    let attachment_token_id = my_attachment.get("id").expect(unreachable).clone();

    // Fetching the attachment using the token and async/await
    // is not available in rust, Js-only API.

    // Fetching the attachment and using event callbacks to monitor the progress
    peer_b
        .store()
        .fetch_attachment(attachment_token, move |event| {
            use DittoAttachmentFetchEvent::*;
            match event {
                Progress {
                    downloaded_bytes,
                    total_bytes,
                } => {
                    println!(
                        r#"\
                    download of attachment {attachment_token_id:?} in progress: \
                    downloaded {downloaded_bytes} bytes out of {total_bytes}\
                "#
                    );
                }
                Completed { attachment } => {
                    println!(
                        r#"\
                        successfully fetched attachment {attachment_token_id:?}, \
                        located at {}\
                    "#,
                        attachment.path().display(),
                    );
                }
                Deleted => unreachable!("attachment cannot have been deleted while fetching"),
            }
        })?;

    // All currently active attachment fetchers can also be accessed via the `attachment_fetchers`
    // property on the ditto store. For instance, to stop all active attachment fetchers.
    for fetcher in peer_b.store().attachment_fetchers() {
        fetcher.cancel();
    }

    Ok(())
}

async fn wait_for_id_to_be_present_in(
    ditto: &Ditto,
    id: &(impl ?Sized + Serialize),
) -> Result<(), Error> {
    struct DocFound;

    let (sender, receiver) = ::tokio::sync::oneshot::channel();
    let mut sender = Some(sender);
    // Remember to have an overarching *subscription* encompassing this query.
    let observer = ditto.store().register_observer_v2(
        (
            r#"SELECT * FROM COLLECTION cars WHERE _id = :id"#,
            json!({ "id": id }),
        ),
        move |query_result| {
            if query_result.item_count() > 0 {
                if let Some(sender) = sender.take() {
                    sender.send(DocFound).ok();
                }
            }
        },
    )?;

    let DocFound = receiver.await?;
    observer.cancel();

    Ok(())
}

// -- END OF SNIPPET.

use common::*;
mod common;

enum Error {}
impl<E: ::std::error::Error> From<E> for Error {
    fn from(e: E) -> Self {
        panic!("{e}");
    }
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_attachment_doc_snippet() {
    let ditto = get_builder_ditto(None).unwrap();
    test_attachment_doc_snippet_impl(ditto).await;

    let ditto = get_revamp_ditto(None).unwrap();
    test_attachment_doc_snippet_impl(ditto).await;

    let ditto = common::get_revamp_ditto_async(None).await.unwrap();
    test_attachment_doc_snippet_impl(ditto).await;
}
async fn test_attachment_doc_snippet_impl(ditto: Ditto) {
    ditto.disable_sync_with_v3().unwrap();

    attachment_doc_snippet(
        &ditto,
        &ditto,
        concat!(env!("CARGO_MANIFEST_DIR"), "/tests/resources/image.png").as_ref(),
    )
    .await
    .unwrap_or_else(|unreachable| match unreachable {})
}
