//! # Entry point for the DittoSDK
//!
//! `Ditto` is a cross-platform peer-to-peer database that allows apps to sync with and even without
//! internet connectivity.
//!
//! To manage your local data and connections to other peers in the mesh, `Ditto` gives you access
//! to:
//! * [`Store`], the entry point to the database.
//! * [`TransportConfig`] to change the transport layers in use.
//! * [`Presence`] to monitor peers in the mesh.
//! * [`DiskUsage`] to monitor local Ditto disk usage.

use_prelude!();

pub mod builder;
pub mod init;

use std::{env, sync::Weak};

/// The log levels that the Ditto SDK supports.
pub use ffi_sdk::CLogLevel as LogLevel;
use ffi_sdk::{BoxedDitto, FsComponent};
use uuid::Uuid;

#[allow(deprecated)]
use self::builder::DittoBuilder;
#[allow(deprecated)]
use crate::presence::observer_legacy::PeersObserver;
use crate::{
    disk_usage::DiskUsage,
    ditto::init::ConfigOrIdentity,
    error::{DittoError, ErrorKind, LicenseTokenError},
    identity::DittoAuthenticator,
    presence::Presence,
    small_peer_info::SmallPeerInfo,
    transport::TransportConfig,
    utils::{extension_traits::FfiResultIntoRustResult, prelude::*},
};

#[extension(pub(crate) trait TryUpgrade)]
impl std::sync::Weak<BoxedDitto> {
    fn try_upgrade(&self) -> Result<Arc<BoxedDitto>, ErrorKind> {
        self.upgrade().ok_or(ErrorKind::ReleasedDittoInstance)
    }
}

/// The entrypoint for accessing all Ditto functionality.
///
/// Use the `Ditto` object to access all other Ditto APIs, such as:
///
/// - [`ditto.store()`] to access the [`Store`] API and read and write data on this peer
/// - [`ditto.sync()`] to access the [`Sync`] API and sync data with other peers
/// - [`ditto.presence()`] to access the [`Presence`] API and inspect connected peers
/// - [`ditto.small_peer_info()`] to access the [`SmallPeerInfo`] API and manage peer metadata
/// - [`ditto.disk_usage()`] to access the [`DiskUsage`] API and inspect disk usage
///
/// [`ditto.store()`]: crate::Ditto::store
/// [`ditto.sync()`]: crate::Ditto::sync
/// [`ditto.presence()`]: crate::Ditto::presence
/// [`ditto.small_peer_info()`]: crate::Ditto::small_peer_info
/// [`ditto.disk_usage()`]: crate::Ditto::disk_usage
pub struct Ditto {
    pub(crate) fields: Arc<DittoFields>,
    /// Always `true` except in `Auth::logout()`.
    is_shut_down_able: bool,
}

impl std::ops::Deref for Ditto {
    type Target = DittoFields;

    #[inline]
    fn deref(&'_ self) -> &'_ DittoFields {
        &self.fields
    }
}

impl Ditto {
    pub(crate) fn upgrade(weak: &Weak<DittoFields>) -> Result<Ditto> {
        let fields = weak.upgrade().ok_or(ErrorKind::ReleasedDittoInstance)?;
        Ok(Ditto::new_temp(fields))
    }
}

/// Inner fields for Ditto
#[doc(hidden)]
// TODO(pub_check)
pub struct DittoFields {
    // FIXME: (Ham & Daniel) - ideally we'd use this in the same way as we do
    // with the `fields` on `Ditto` (only ever extracting weak references)
    pub(crate) ditto: Arc<ffi_sdk::BoxedDitto>,
    has_auth: bool,
    state: ConfigOrIdentity,
    pub(crate) store: Store,
    pub(crate) sync: crate::sync::Sync,
    #[allow(deprecated)]
    site_id: SiteId,
    presence: Arc<Presence>,
    disk_usage: DiskUsage,
    small_peer_info: SmallPeerInfo,
    // This field must go last since after it is dropped the backing fs will dangle.
    ditto_root: Arc<dyn DittoRoot>, // Arc since Identity will need a copy
}

// We use this pattern to ensure `self` is not used after `ManuallyDrop::take()`-ing its fields.
impl Drop for Ditto {
    fn drop(&mut self) {
        if self.is_shut_down_able {
            // stop all transports
            self.sync().stop();
            // Here, ditto is implicitly dropped using ditto_free if there is no strong reference to
            // it anymore. We need to make sure that `ditto_shutdown` is called before
            // `ditto_free` gets called though, as this will perform all the necessary
            // pre-drop actions such as stopping TCP servers, etc.
            ffi_sdk::ditto_shutdown(&self.ditto);
        }
    }
}

// Public interface for modifying Transport configuration.
impl Ditto {
    /// Clean shutdown of the Ditto instance
    pub fn close(self) {
        // take ownership of Ditto in order to drop it
    }

    /// Starts the network transports. Ditto will connect to other devices.
    ///
    /// By default, Ditto will enable all peer-to-peer transport types.
    /// The network configuration can be customized using [`set_transport_config`].
    ///
    /// Performance of initial sync when bootstrapping a new peer can be improved
    /// by calling [`disable_sync_with_v3`] before calling `sync().start()`. Only
    /// do this when all peers in the mesh are known to be running Ditto v4 or higher.
    ///
    /// [`set_transport_config`]: Self::set_transport_config
    /// [`disable_sync_with_v3`]: Self::disable_sync_with_v3
    #[deprecated(since = "4.14.4", note = "Use `ditto.sync().start()` instead")]
    pub fn start_sync(&self) -> Result<(), DittoError> {
        let result = ffi_sdk::dittoffi_ditto_try_start_sync(&self.ditto);
        if let Some(error) = result.error {
            if ffi_sdk::dittoffi_error_code(&*error)
                == ffi_sdk::FfiErrorCode::ActivationNotActivated
            {
                return Err(ErrorKind::NotActivated.into());
            }
            return Err(DittoError::from(error));
        }
        Ok(())
    }

    /// Stop syncing on all transports.
    ///
    /// You may continue to use the Ditto store locally but no data
    /// will sync to or from other devices.
    #[deprecated(since = "4.14.4", note = "Use `ditto.sync().stop()` instead")]
    pub fn stop_sync(&self) {
        ffi_sdk::dittoffi_ditto_stop_sync(&self.ditto);
    }

    /// Returns `true` if sync is currently active, otherwise returns `false`.
    ///
    /// Use `ditto.sync().start()` to activate and `ditto.sync().stop()` to deactivate sync.
    #[deprecated(since = "4.14.4", note = "Use `ditto.sync().is_active()` instead")]
    pub fn is_sync_active(&self) -> bool {
        ffi_sdk::dittoffi_ditto_is_sync_active(&self.ditto)
    }

    /// Set a new [`TransportConfig`] and begin syncing over these
    /// transports. Any change to start or stop a specific transport should proceed via providing a
    /// modified configuration to this method.
    pub fn set_transport_config(&self, config: TransportConfig) {
        let cbor = serde_cbor::to_vec(&config).expect("bug: failed to serialize TransportConfig");
        ffi_sdk::dittoffi_ditto_try_set_transport_config(&self.ditto, (&*cbor).into(), false)
            .into_rust_result()
            .expect("bug: core failed to set transport config");
    }

    /// Convenience method to update the current transport config of the receiver.
    ///
    /// Invokes the block with a copy of the current transport config which
    /// you can alter to your liking. The updated transport config is then set
    /// on the receiver.
    ///
    /// You may use this method to alter the configuration at any time.
    /// Sync will not begin until [`ditto.sync().start()`] is invoked.
    ///
    /// # Example
    ///
    /// Edit the config by simply mutating the `&mut TransportConfig` passed
    /// to your callback:
    ///
    /// ```
    /// use dittolive_ditto::prelude::*;
    /// # fn example(ditto: &Ditto) -> anyhow::Result<()> {
    ///
    /// // Enable the TCP listener on port 4000
    /// ditto.update_transport_config(|config| {
    ///     config.listen.tcp.enabled = true;
    ///     config.listen.tcp.interface_ip = "0.0.0.0".to_string();
    ///     config.listen.tcp.port = 4000;
    /// });
    /// # Ok(())
    /// # }
    /// ```
    ///
    /// [`ditto.sync().start()`]: crate::sync::Sync::start
    pub fn update_transport_config(&self, update: impl FnOnce(&mut TransportConfig)) {
        let mut transport_config = self.transport_config();
        update(&mut transport_config);
        self.set_transport_config(transport_config);
    }

    /// Returns a snapshot of the currently configured transports.
    #[doc(hidden)]
    #[deprecated(note = "Use `.transport_config()` instead")]
    pub fn current_transport_config(&self) -> Result<TransportConfig, DittoError> {
        let transport_config_cbor = ffi_sdk::dittoffi_ditto_transport_config(&self.ditto);
        let transport_config =
            serde_cbor::from_slice::<TransportConfig>(transport_config_cbor.as_slice())?;
        Ok(transport_config)
    }

    /// Returns a snapshot of the currently configured transports.
    ///
    /// # Example
    ///
    /// ```
    /// # use dittolive_ditto::prelude::*;
    /// # fn example(ditto: &Ditto) {
    /// let transport_config = ditto.transport_config();
    /// println!("Current transport config: {transport_config:#?}");
    /// # }
    /// ```
    pub fn transport_config(&self) -> TransportConfig {
        let transport_config_cbor = ffi_sdk::dittoffi_ditto_transport_config(&self.ditto);
        serde_cbor::from_slice::<TransportConfig>(transport_config_cbor.as_slice())
            .expect("bug: failed to deserialize TransportConfig from core")
    }
}

impl Ditto {
    /// Return the version of the SDK.
    #[deprecated(
        since = "4.14.4",
        note = "Method `Ditto::with_sdk_version()` will be removed in SDK 5.0. Use \
                `Ditto::version()` instead"
    )]
    pub fn with_sdk_version<R>(ret: impl FnOnce(&'_ str) -> R) -> R {
        ret(ffi_sdk::ditto_get_sdk_version().to_str())
    }

    /// Return the version of the SDK.
    pub fn version() -> String {
        Self::init_sdk_version();
        ffi_sdk::dittoffi_get_sdk_semver().to_string()
    }

    #[allow(deprecated)]
    fn init_sdk_version() {
        let platform = DittoBuilder::platform();
        let sdk_semver = DittoBuilder::sdk_version();
        let c_version = char_p::new(sdk_semver);
        ffi_sdk::ditto_init_sdk_version(platform, ffi_sdk::Language::Rust, c_version.as_ref());
    }
}

#[doc(hidden)]
impl Ditto {
    #[deprecated(note = "Use `DittoLogger::set_logging_enabled()` instead")]
    /// Enable or disable logging.
    pub fn set_logging_enabled(enabled: bool) {
        ffi_sdk::ditto_logger_enabled(enabled)
    }

    #[deprecated(note = "Use `DittoLogger::get_logging_enabled()` instead")]
    /// Return true if logging is enabled.
    pub fn get_logging_enabled() -> bool {
        ffi_sdk::ditto_logger_enabled_get()
    }

    #[deprecated(note = "Use `DittoLogger::get_emoji_log_level_headings_enabled()` instead")]
    /// Represent whether or not emojis should be used as the log level indicator in the logs.
    pub fn get_emoji_log_level_headings_enabled() -> bool {
        ffi_sdk::ditto_logger_emoji_headings_enabled_get()
    }

    #[deprecated(note = "Use `DittoLogger::set_emoji_log_level_headings_enabled()` instead")]
    /// Set whether or not emojis should be used as the log level indicator in the logs.
    pub fn set_emoji_log_level_headings_enabled(enabled: bool) {
        ffi_sdk::ditto_logger_emoji_headings_enabled(enabled);
    }

    #[deprecated(note = "Use `DittoLogger::get_minimum_log_level()` instead")]
    /// Get the current minimum log level.
    pub fn get_minimum_log_level() -> LogLevel {
        ffi_sdk::ditto_logger_minimum_log_level_get()
    }

    #[deprecated(note = "Use `DittoLogger::set_minimum_log_level()` instead")]
    /// Set the current minimum log level.
    pub fn set_minimum_log_level(log_level: LogLevel) {
        ffi_sdk::ditto_logger_minimum_log_level(log_level);
    }
}

impl Ditto {
    /// Activate an offline [`Ditto`] instance by setting a license token.
    ///
    /// You cannot initiate sync on an offline
    /// ([`OfflinePlayground`], [`Manual`], or [`SharedKey`])
    /// [`Ditto`] instance before you have activated it.
    pub fn set_offline_only_license_token(&self, license_token: &str) -> Result<(), DittoError> {
        if self.state.requires_offline_only_license_token() {
            use ffi_sdk::LicenseVerificationResult;
            use safer_ffi::prelude::{AsOut, ManuallyDropMut};
            let c_license: char_p::Box = char_p::new(license_token);

            let mut err_msg = None;
            let out_err_msg = err_msg.manually_drop_mut().as_out();
            let res =
                ffi_sdk::ditto_verify_license(&self.ditto, c_license.as_ref(), Some(out_err_msg));

            if res == LicenseVerificationResult::LicenseOk {
                return Ok(());
            }

            let err_msg = err_msg.unwrap();
            error!("{err_msg}");

            match res {
                LicenseVerificationResult::LicenseExpired => {
                    Err(DittoError::license(LicenseTokenError::Expired {
                        message: err_msg.as_ref().to_string(),
                    }))
                }
                LicenseVerificationResult::VerificationFailed => {
                    Err(DittoError::license(LicenseTokenError::VerificationFailed {
                        message: err_msg.as_ref().to_string(),
                    }))
                }
                LicenseVerificationResult::UnsupportedFutureVersion => Err(DittoError::license(
                    LicenseTokenError::UnsupportedFutureVersion {
                        message: err_msg.as_ref().to_string(),
                    },
                )),
                _ => panic!("Unexpected license verification result {:?}", res),
            }
        } else {
            Err(DittoError::new(
                ErrorKind::Internal,
                "Offline license tokens should only be used for Manual, SharedKey or \
                 OfflinePlayground identities",
            ))
        }
    }

    /// Look for a license token from a given environment variable.
    pub fn set_license_from_env(&self, var_name: &str) -> Result<(), DittoError> {
        match env::var(var_name) {
            Ok(token) => self.set_offline_only_license_token(&token),
            Err(env::VarError::NotPresent) => {
                let msg = format!("No license token found for env var {}", &var_name);
                Err(DittoError::from_str(ErrorKind::Config, msg))
            }
            Err(e) => Err(DittoError::new(ErrorKind::Config, e)),
        }
    }
}

impl Ditto {
    /// Returns a reference to the underlying local data store.
    pub fn store(&self) -> &Store {
        &self.store
    }

    /// Entrypoint to Ditto's [`Sync`] API for syncing documents between peers.
    ///
    /// [`Sync`]: crate::sync::Sync
    pub fn sync(&self) -> &crate::sync::Sync {
        &self.sync
    }

    #[cfg(feature = "experimental-bus")]
    #[deprecated(
        since = "4.14.4",
        note = "Experimental bus API will be removed in SDK 5.0"
    )]
    /// Returns a handle to the bus controller. This can be used to send either individual messages
    /// or open streams to remote peers.
    /// NOTE: This API is experimental and may change in future releases.
    pub fn bus(&self) -> crate::experimental::bus::Bus {
        ffi_sdk::dittoffi_get_experimental_bus(&self.ditto)
    }

    /// Return a reference to the [`SmallPeerInfo`] object.
    pub fn small_peer_info(&self) -> &SmallPeerInfo {
        &self.small_peer_info
    }

    /// Returns the site ID that the instance of Ditto is using as part of its identity.
    #[doc(hidden)]
    #[deprecated(note = "Use `ditto.presence().graph().local_peer.peer_key_string` instead")]
    pub fn site_id(&self) -> u64 {
        self.site_id
    }

    /// Returns the Ditto persistence directory path.
    #[doc(hidden)]
    #[deprecated(note = "Use `ditto.absolute_persistence_directory()` instead")]
    pub fn persistence_directory(&self) -> &Path {
        self.ditto_root.root_path()
    }

    /// The absolute path to the persistence directory used by Ditto to persist data.
    ///
    /// This returns the final, resolved absolute file path where Ditto stores its data.
    /// The value depends on what was provided in [`DittoConfig::persistence_directory`].
    ///
    /// - If an **absolute path** was provided, it returns that path unchanged.
    /// - If a **relative path** was provided, it returns the path resolved relative to
    ///   [`ditto.default_root_directory()`].
    /// - If no path was provided, it returns the default path using the pattern
    ///   `{default_root}/ditto-{database-id}` where `{default_root}` corresponds to
    ///   [`ditto.default_root_directory()`] and `{database-id}` is the Ditto database ID in
    ///   lowercase.
    ///
    /// This property always returns a consistent value throughout the lifetime of the Ditto
    /// instance and represents the actual directory being used for persistence.
    ///
    /// - Note: "Database ID" was previously referred to as "App ID" in older versions of the SDK.
    ///
    /// - Note: It is not recommended to directly read or write to this directory as its structure
    ///   and contents are managed by Ditto and may change in future versions.
    ///
    /// - Note: When [`DittoLogger`] is enabled, logs may be written to this directory even after a
    ///   Ditto instance has been deallocated. Please refer to the documentation of [`DittoLogger`]
    ///   for more information.
    ///
    /// - See also: [`DittoConfig::persistence_directory`]
    ///
    /// [`DittoConfig::persistence_directory`]: DittoConfig::persistence_directory
    pub fn absolute_persistence_directory(&self) -> PathBuf {
        let path = ffi_sdk::dittoffi_ditto_absolute_persistence_directory(&self.ditto);
        PathBuf::from(path.to_str())
    }

    /// Returns an owned snapshot of the _effective_ `DittoConfig` as used by the core library.
    ///
    /// - Modifying this `DittoConfig` have no effect on the active Ditto configuration.
    /// - The returned `DittoConfig` may be different than the one passed to [`Ditto::open{,_sync}`]
    ///   because it will have resolved details such as the absolute path to the persistence
    ///   directory.
    pub fn config(&self) -> DittoConfig {
        let config_cbor = ffi_sdk::dittoffi_ditto_config(&self.ditto);
        serde_cbor::from_slice(&config_cbor).expect("bug: should deserialize DittoConfig")
    }

    /// Returns the application Id being used by this [`Ditto`] instance.
    #[doc(hidden)]
    #[deprecated(note = "Use `.app_id()` instead")]
    pub fn application_id(&self) -> AppId {
        AppId(ffi_sdk::ditto_auth_client_get_app_id(&self.ditto).into_string())
    }

    /// Returns the [`AppId`] being used by this [`Ditto`] instance.
    #[deprecated(since = "4.14.4", note = "Use ditto.config() for configuration access")]
    pub fn app_id(&self) -> AppId {
        AppId(ffi_sdk::ditto_auth_client_get_app_id(&self.ditto).into_string())
    }

    /// Set a custom identifier for the current device.
    ///
    /// When using [`observe_peers`](Ditto::observe_peers), each remote peer is represented by a
    /// short UTF-8 “device name”. By default this will be a truncated version of the device’s
    /// hostname. It does not need to be unique among peers. Configure the device name before
    /// calling [`sync().start()`](crate::sync::Sync::start). If it is too long it will be
    /// truncated.
    pub fn set_device_name(&self, name: &str) {
        let c_device_name: char_p::Box = char_p::new(name.to_owned());
        // We don't currently expose the device name to the user so we don't
        // need to worry about the returned (potentially truncated) value here
        let _ = ffi_sdk::ditto_set_device_name(&self.ditto, c_device_name.as_ref());
    }

    /// Request information about Ditto peers in range of this device.
    ///
    /// This method returns an observer which should be held as long as updates are required. A
    /// newly registered observer will have a peers update delivered to it immediately. Then it will
    /// be invoked repeatedly when Ditto devices come and go, or the active connections to them
    /// change.
    #[doc(hidden)]
    #[deprecated(note = "Use `presence().observe()` instead")]
    #[allow(deprecated)]
    pub fn observe_peers<H>(&self, handler: H) -> PeersObserver
    where
        H: Fn(crate::transport::v2::V2Presence) + Send + Sync + 'static,
    {
        self.presence.add_observer(handler)
    }

    /// Return a handle to the [`Presence`] API to monitor peers' activity in the Ditto mesh.
    ///
    /// # Example
    ///
    /// Use [`ditto.presence().graph()`] to request a current [`PresenceGraph`] of connected peers:
    ///
    /// ```
    /// use dittolive_ditto::prelude::*;
    ///
    /// # fn example(ditto: &Ditto) {
    /// let presence_graph: PresenceGraph = ditto.presence().graph();
    /// println!("Ditto mesh right now: {presence_graph:#?}");
    /// # }
    /// ```
    ///
    /// # Example
    ///
    /// Use [`ditto.presence().observe(...)`] to subscribe to changes in mesh presence:
    ///
    /// ```
    /// use dittolive_ditto::prelude::*;
    ///
    /// # fn example(ditto: &Ditto) {
    /// let _observer = ditto.presence().observe(|graph| {
    ///     println!("Ditto mesh update! {graph:#?}");
    /// });
    ///
    /// // The observer is cancelled when dropped.
    /// // In a real application, hold onto it for as long as you need it alive.
    /// drop(_observer);
    /// # }
    /// ```
    ///
    /// [`ditto.presence().graph()`]: crate::presence::Presence::graph
    /// [`PresenceGraph`]: crate::presence::PresenceGraph
    /// [`ditto.presence().observe(...)`]: crate::presence::Presence::observe
    pub fn presence(&self) -> &Arc<Presence> {
        &self.presence
    }

    /// Return a [`DiskUsage`] to monitor the disk usage of the Ditto
    /// instance. It can be used to retrieve an immediate representation of the Ditto file system:
    ///
    /// ```
    /// # use dittolive_ditto::prelude::Ditto;
    /// # fn call_diskusage(ditto: Ditto) {
    /// let fs_tree = ditto.disk_usage().exec();
    /// # }
    /// ```
    /// Or to bind a callback to the changes :
    /// ```
    /// # use dittolive_ditto::prelude::Ditto;
    /// # fn call_diskusage(ditto:Ditto) {
    /// let handle = ditto.disk_usage().observe(|fs_tree| {
    ///     // do something with the graph
    /// });
    /// // The handle must be kept to keep receiving updates on the file system.
    /// // To stop receiving update, drop the handle.
    /// # }
    /// ```
    pub fn disk_usage(&self) -> &DiskUsage {
        &self.disk_usage
    }

    #[doc(hidden)]
    #[deprecated(note = "Use `ditto.absolute_persistence_directory()` instead")]
    /// Returns the [`DittoRoot`] path.
    pub fn root_dir(&self) -> &Path {
        #[allow(deprecated)]
        self.persistence_directory()
    }

    #[doc(hidden)]
    #[deprecated(note = "Use `ditto.absolute_persistence_directory()` instead")]
    /// Returns the ditto persistence data directory path.
    pub fn data_dir(&self) -> &Path {
        #[allow(deprecated)]
        self.persistence_directory()
    }

    #[cfg(test)]
    #[deprecated(
        since = "4.14.4",
        note = "Use `ditto.absolute_persistence_directory()` instead"
    )]
    /// Returns the [`DittoRoot`].
    pub fn root(&self) -> Arc<dyn DittoRoot> {
        self.ditto_root.retain()
    }

    /// Returns the current [`DittoAuthenticator`], if it exists.
    #[doc(hidden)]
    #[deprecated(note = "Use `.auth()` instead")]
    pub fn authenticator(&self) -> Option<DittoAuthenticator> {
        self.auth()
    }

    /// Returns the current [`DittoAuthenticator`], if it exists.
    ///
    /// The [`DittoAuthenticator`] is available when using the [`OnlinePlayground`]
    /// and [`OnlineWithAuthentication`] identities.
    pub fn auth(&self) -> Option<DittoAuthenticator> {
        self.fields.has_auth.then(|| DittoAuthenticator {
            ditto_fields: Arc::downgrade(&self.fields),
        })
    }

    /// Returns `true` if this `Ditto` instance has been activated with a valid
    /// license token.
    pub fn is_activated(&self) -> bool {
        ffi_sdk::dittoffi_ditto_is_activated(&self.ditto)
    }
}

// Constructors
impl Ditto {
    /// Entrypoint to constructing `Ditto` with the builder pattern.
    ///
    /// # Example
    ///
    /// ```no_run
    /// # use std::sync::Arc;
    /// use dittolive_ditto::prelude::*;
    ///
    /// fn main() -> anyhow::Result<()> {
    ///     let app_id = AppId::from_env("DITTO_APP_ID")?;
    ///     let playground_token = std::env::var("DITTO_PLAYGROUND_TOKEN")?;
    ///     let cloud_sync = true;
    ///     let custom_auth_url = None;
    ///
    ///     // Initialize Ditto
    ///     let ditto = Ditto::builder()
    ///         .with_root(Arc::new(PersistentRoot::from_current_exe()?))
    ///         .with_identity(|ditto_root| {
    ///             identity::OnlinePlayground::new(
    ///                 ditto_root,
    ///                 app_id,
    ///                 playground_token,
    ///                 cloud_sync,
    ///                 custom_auth_url,
    ///             )
    ///         })?
    ///         .build()?;
    ///
    ///     Ok(())
    /// }
    /// ```
    ///
    /// [See the crate docs for examples of what to do next using `Ditto`][0].
    ///
    /// [0]: crate#playground-quickstart
    #[deprecated(
        note = "Use `Ditto::open(config)` or `Ditto::open_sync(config)` instead. The builder \
                pattern will be removed in SDK 5.0."
    )]
    #[allow(deprecated)]
    pub fn builder() -> DittoBuilder {
        DittoBuilder::new()
    }

    /// Construct a [`Ditto`] instance with sensible defaults.
    ///
    /// This instance will still need to have a license set (if necessary) and sync functionality
    /// manually started.
    #[doc(hidden)]
    #[deprecated(note = "Use `Ditto::open(...)` or `Ditto::open_sync(...)` instead")]
    #[allow(deprecated)]
    pub fn new(app_id: AppId) -> Ditto {
        Ditto::builder()
            .with_root(Arc::new(
                PersistentRoot::from_current_exe().expect("Invalid Ditto Root"),
            ))
            .with_identity(|ditto_root| identity::OfflinePlayground::new(ditto_root, app_id))
            .expect("Invalid Ditto Identity")
            .with_minimum_log_level(LogLevel::Info)
            .build()
            .expect("Failed to build Ditto Instance")
    }

    // This isn't public to customers and is only used internally. It's only currently used when we
    // have access to the `DittoFields` and want to create a `Ditto` instance for whatever reason,
    // but we don't want `Ditto` to be shut down when this `Ditto` instance is dropped.
    //
    // This should definitely be considered a hack for now.
    pub(crate) fn new_temp(fields: Arc<DittoFields>) -> Ditto {
        Ditto {
            fields,
            is_shut_down_able: false,
        }
    }
}

impl Ditto {
    /// Removes all sync metadata for any remote peers which aren't currently connected. This method
    /// shouldn't usually be called. Manually running garbage collection often will result in slower
    /// sync times. Ditto automatically runs a garbage a collection process in the background at
    /// optimal times.
    ///
    /// Manually running garbage collection is typically only useful during testing if large amounts
    /// of data are being generated. Alternatively, if an entire data set is to be evicted and it's
    /// clear that maintaining this metadata isn't necessary, then garbage collection could be run
    /// after evicting the old data.
    pub fn run_garbage_collection(&self) {
        ffi_sdk::ditto_run_garbage_collection(&self.ditto);
    }

    /// Disables sync compatibility with V3 peers, enabling full DQL support
    /// including mutating statements (INSERT, UPDATE, DELETE).
    ///
    /// This is a one-way operation that cannot be reversed. After calling this,
    /// the Ditto instance will no longer be able to sync with V3 SDK peers.
    pub fn disable_sync_with_v3(&self) -> Result<()> {
        let result = ffi_sdk::ditto_disable_sync_with_v3(&self.ditto);
        if result == 0 {
            Ok(())
        } else {
            Err(DittoError::from_ffi(ErrorKind::Internal))
        }
    }
}

/// `SiteId`s are used to identity Ditto peers
#[deprecated(note = "Use `ditto.presence().graph().local_peer.peer_key_string` instead")]
pub type SiteId = u64;

#[derive(Clone, Debug)]
// pub struct AppId(uuid::Uuid); // Demo apps still use arbitrary strings
/// The ID of this Ditto App, used to determine which peers to sync with
pub struct AppId(pub(crate) String); // neither String nor Vec<u8> are Copy

impl AppId {
    /// Generate a random AppId from a UUIDv4
    pub fn generate() -> Self {
        let uuid = uuid::Uuid::new_v4();
        AppId::from_uuid(uuid)
    }

    /// Generate an AppId from a given UUIDv4
    pub fn from_uuid(uuid: Uuid) -> Self {
        let id_str = format!("{:x}", &uuid); // lower-case with hypens
        AppId(id_str)
    }

    /// Attempt to grab a specific AppId from some environment variable
    pub fn from_env(var: &str) -> Result<Self, DittoError> {
        let id_str = env::var(var).map_err(|err| DittoError::new(ErrorKind::Config, err))?;
        Ok(AppId(id_str))
    }

    /// Return the corresponding string
    pub fn as_str(&self) -> &str {
        &self.0
    }

    /// Return the corresponding c string
    pub fn to_c_string(&self) -> char_p::Box {
        char_p::new(self.0.as_str())
    }

    /// Return the default auth URL associated with the app Id. This is of the form
    /// `https://{app_id}.cloud.ditto.live/` by default.
    pub fn default_auth_url(&self) -> String {
        format!("https://{}.cloud.ditto.live", self.0)
    }

    /// Return the default WebSocket sync URL which is of the form
    /// `wss://{app_id}.cloud.ditto.live/` by default.
    pub fn default_sync_url(&self) -> String {
        format!("wss://{}.cloud.ditto.live", self.0)
    }
}

use std::{fmt, fmt::Display, str::FromStr};

impl Display for AppId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl FromStr for AppId {
    type Err = DittoError;
    fn from_str(s: &str) -> Result<AppId, DittoError> {
        // later s will need to be a valid UUIDv4
        Ok(AppId(s.to_string()))
    }
}

#[cfg(test)]
#[path = "tests.rs"]
mod tests;
