//! # Ditto needs an `Identity` to start syncing with other peers.
//!
//! The various identity configurations that you can use when initializing a `Ditto` instance.
//!
//! - [`OfflinePlayground`](crate::prelude::OfflinePlayground): Develop peer-to-peer apps with no
//! cloud connection. This mode offers no security and must only be used for development.
//! - [`OnlineWithAuthentication`](crate::prelude::OnlineWithAuthentication): Run Ditto in secure
//! production mode, logging on to Ditto Cloud or on on-premises authentication server. User
//! permissions are centrally managed.
//! - [`OnlinePlayground`](crate::prelude::OnlinePlayground): Test a Ditto Cloud app with weak
//! shared token authentication ("Playground mode"). This mode is not secure and must only be used
//! for development.
//! - [`SharedKey`](crate::prelude::SharedKey): A mode where any device is trusted provided they
//! know the secret key. This is a simplistic authentication model normally only suitable for
//! private apps where users and devices are both trusted.
//! - [`Manual`](crate::prelude::Manual): A manually-provided certificate identity. This accepts a
//!   base64-encoded bundle.

use std::sync::{Arc, RwLock, Weak};

use ffi_sdk::BoxedAuthClient;
use tokio::runtime::Handle;

use crate::{
    auth::{DittoAuthenticationEventHandler, DittoAuthenticator, LoginProvider, ValidityListener},
    ditto::AppId,
    error::{DittoError, ErrorKind},
    transport::TransportSync,
};
use_prelude!();

pub(crate) type SharedIdentity = Arc<dyn Identity>;

// Identity is pub because it is part of the Builder's public interface
// It is therefore Sealed to prevent downstream implementations
#[doc(hidden)]
pub trait Identity: private::Sealed + Send + Sync {
    /// Returns the current SiteId identifying the Ditto peer
    fn site_id(&self) -> SiteId {
        unsafe { ffi_sdk::ditto_auth_client_get_site_id(&self.auth_client()) }
    }

    /// Returns a shared reference to the underlying AuthClient
    fn auth_client(&self) -> Arc<BoxedAuthClient>;

    /// Returns the underlying `DittoAuthenticator` if specified
    fn authenticator(&self) -> Option<DittoAuthenticator>;

    /// Constructs a `ValidityListener` given a shared reference to the Ditto
    /// `Transports`
    fn make_listener(
        &self,
        transports: Weak<RwLock<TransportSync>>,
    ) -> Option<Arc<ValidityListener>>;

    /// Returns the current AppId
    fn app_id(&self) -> AppId {
        AppId(unsafe { ffi_sdk::ditto_auth_client_get_app_id(&self.auth_client()) }.into_string())
    }

    /// Returns if the current web auth token is valid
    fn is_web_valid(&self) -> bool;

    /// Returns if the configured x509 certificate is valid
    fn is_x509_valid(&self) -> bool;

    /// Indicates if cloud sync should be enabled by default
    fn is_cloud_sync_enabled(&self) -> bool;

    /// Returns the configured URL for Auth
    fn auth_url(&self) -> Result<String, DittoError>;

    /// Returns the configured URL for websocket sync
    fn sync_url(&self) -> Result<String, DittoError>;

    /// Indicates whether the specific Identity type requires an offline only license token to be
    /// set.
    fn requires_offline_only_license_token(&self) -> bool;
}

/// Run Ditto in secure production mode, logging on to Ditto Cloud or an
/// on-premises authentication server. User permissions are centrally managed.
/// Sync will not work until a successful login has occurred.
///
/// The only required configuration is the application's UUID, which can be
/// found on the Ditto portal where the app is registered.
///
/// By default cloud sync is enabled. This means the SDK will sync to a Big Peer
/// in Ditto's cloud when an internet connection is available. This is
/// controlled by the `enable_cloud_sync` parameter. If `true` (default), a
/// suitable wss:// URL will be added to the
/// [`TransportConfig`](crate::prelude::TransportConfig).
///
/// To prevent cloud sync, or to specify your own URL later, pass `false`.
///
/// Authentication requests are handled by Ditto's cloud by default, configured
/// in the portal at `<https://portal.ditto.live>`.
///
/// To use a different or on-premises authentication service, pass a custom
/// HTTPS base URL as the * `auth_url` parameter.
pub struct OnlineWithAuthentication {
    auth: DittoAuthenticator,
    app_id: AppId,
    enable_cloud_sync: bool,
    auth_url: String,
}

#[deprecated(note = "use OnlinePlayground instead")]
pub type OnlinePlaygroundV2 = OnlinePlayground;

/// Test a Ditto Cloud app with a simple shared token ("Playground mode"). This
/// mode offers no security and must only be used for development. Other
/// behavior mirrors the
/// [`OnlineWithAuthentication`](crate::prelude::OnlineWithAuthentication)
/// identity.
pub struct OnlinePlayground {
    auth: DittoAuthenticator,
    app_id: AppId,
    enable_cloud_sync: bool,
    auth_url: String,
}

/// An identity where any device is trusted provided they know the secret key.
/// This is a simplistic authentication model normally only suitable for private
/// apps where users and devices are both trusted. In this mode, any string may
/// be used as the app id.
pub struct SharedKey {
    auth: DittoAuthenticator,
}

/// An identity where devices are manually configured with a x509 certificate
/// bundle
pub struct Manual {
    auth: DittoAuthenticator,
}

/// Develop peer-to-peer apps with no cloud connection. This mode offers no
/// security and must only be used for development. In this mode, any string can
/// be used as the name of the app.
pub struct OfflinePlayground {
    auth: DittoAuthenticator,
}

impl OnlineWithAuthentication {
    /// Construct an OnlineWithAuthentication Identity.
    ///
    /// - `ditto_root`: The directory containing ditto local storage
    /// - `app_id`: The unique identifier of the application, usually a UUIDv4 string
    /// - `auth_event_handler`: A user created type which implements the
    ///   `DittoAuthenticationEventHandler` trait
    /// - `enable_cloud_sync`: whether cloud sync should be started by default
    /// - `custom_auth_url`: an optional custom authentication URL; default is is `cloud.ditto.live`
    pub fn new(
        ditto_root: Arc<dyn DittoRoot>,
        app_id: AppId,
        auth_event_handler: impl DittoAuthenticationEventHandler + 'static,
        enable_cloud_sync: bool,
        custom_auth_url: Option<&str>,
    ) -> Result<Self, DittoError> {
        let auth_url = match custom_auth_url {
            Some(url) => url.to_owned(),
            None => app_id.default_auth_url(),
        };
        let c_working_dir = ditto_root.root_dir_to_c_str()?;
        let c_app_id = char_p::new(app_id.to_string());
        let c_auth_url = char_p::new(auth_url.as_str());

        let login_provider = LoginProvider::new(auth_event_handler);

        let LoginProvider {
            _provider: c_provider,
            ctx,
        } = login_provider;

        let auth_client = unsafe {
            let executor = make_executor();

            ffi_sdk::ditto_auth_client_make_with_web_with_executor(
                c_working_dir.as_ref(),
                c_app_id.as_ref(),
                c_auth_url.as_ref(),
                Some(c_provider),
                executor,
            )
            .ok_or(ErrorKind::Config)?
        };

        let authenticator = DittoAuthenticator::new(Arc::new(auth_client));
        ctx.lock()
            .unwrap()
            .set_authenticator(authenticator.retain());

        Ok(OnlineWithAuthentication {
            app_id,
            auth: authenticator,
            enable_cloud_sync,
            auth_url,
        })
    }
}

impl Identity for OnlineWithAuthentication {
    fn auth_client(&self) -> Arc<BoxedAuthClient> {
        self.auth.auth_client()
    }

    fn is_web_valid(&self) -> bool {
        unsafe { ffi_sdk::ditto_auth_client_is_web_valid(&self.auth_client()) != 0 }
    }

    fn is_x509_valid(&self) -> bool {
        unsafe { ffi_sdk::ditto_auth_client_is_x509_valid(&self.auth_client()) != 0 }
    }

    fn is_cloud_sync_enabled(&self) -> bool {
        self.enable_cloud_sync
    }

    fn auth_url(&self) -> Result<String, DittoError> {
        Ok(self.auth_url.to_owned())
    }

    fn sync_url(&self) -> Result<String, DittoError> {
        Ok(self.app_id.default_sync_url())
    }

    fn authenticator(&self) -> Option<DittoAuthenticator> {
        Some(self.auth.retain())
    }

    fn make_listener(
        &self,
        transports: Weak<RwLock<TransportSync>>,
    ) -> Option<Arc<ValidityListener>> {
        Some(ValidityListener::new(transports, self.auth.auth_client()))
    }

    fn requires_offline_only_license_token(&self) -> bool {
        false
    }
}

impl OfflinePlayground {
    /// Construct an `OfflinePlayground` identity.
    ///
    /// - `ditto_root`: Location for the local database
    /// - `app_id`: A unique string identifying the App. For an `OfflinePlayground` identity this
    ///   does not need to be a UUIDv4.
    pub fn new(ditto_root: Arc<dyn DittoRoot>, app_id: AppId) -> Result<Self, DittoError> {
        let c_data_dir = ditto_root.root_dir_to_c_str()?;
        let c_app_id = app_id.to_c_string();
        let auth_client = unsafe {
            let executor = make_executor();

            ffi_sdk::ditto_auth_client_make_for_development_with_executor(
                Some(c_data_dir.as_ref()),
                c_app_id.as_ref(),
                0,
                executor,
            )
            .ok_or(ErrorKind::Config)?
        };
        let authenticator = DittoAuthenticator::new(Arc::new(auth_client));
        Ok(Self {
            auth: authenticator,
        })
    }

    /// Generate an `OfflinePlayground` identity with a random AppId for testing
    pub fn random(ditto_root: Arc<dyn DittoRoot>) -> Result<Self, DittoError> {
        let app_id = AppId::generate();
        Self::new(ditto_root, app_id)
    }
}

impl Identity for OfflinePlayground {
    fn site_id(&self) -> SiteId {
        unsafe { ffi_sdk::ditto_auth_client_get_site_id(&self.auth_client()) }
    }

    fn auth_client(&self) -> Arc<BoxedAuthClient> {
        self.auth.auth_client()
    }

    fn is_web_valid(&self) -> bool {
        unsafe { ffi_sdk::ditto_auth_client_is_web_valid(&self.auth_client()) != 0 }
    }

    fn is_x509_valid(&self) -> bool {
        unsafe { ffi_sdk::ditto_auth_client_is_x509_valid(&self.auth_client()) != 0 }
    }

    fn is_cloud_sync_enabled(&self) -> bool {
        false
    }

    fn auth_url(&self) -> Result<String, DittoError> {
        Err(DittoError::new(
            ErrorKind::InvalidInput,
            "Cloud Auth is not enabled for OfflinePlayground Identities".to_string(),
        ))
    }
    fn sync_url(&self) -> Result<String, DittoError> {
        Err(DittoError::new(
            ErrorKind::InvalidInput,
            "Cloud sync is not enabled for OfflinePlayground Identities".to_string(),
        ))
    }

    fn authenticator(&self) -> Option<DittoAuthenticator> {
        Some(self.auth.retain())
    }

    fn make_listener(
        &self,
        transports: Weak<RwLock<TransportSync>>,
    ) -> Option<Arc<ValidityListener>> {
        Some(ValidityListener::new(transports, self.auth.auth_client()))
    }

    fn requires_offline_only_license_token(&self) -> bool {
        true
    }
}

impl SharedKey {
    /// Construct a `SharedKey` identity.
    ///
    /// - `ditto_root`: An instance of `DittoRoot` for local storage
    /// - `app_id`: The unique UUIDv4 identifying this app; must be shared across all instances
    /// - `key_der_b64`: A pre-shared key for securing peer-to-peer communication encoded as a
    ///   Base64 string
    pub fn new(
        ditto_root: Arc<dyn DittoRoot>,
        app_id: AppId,
        key_der_b64: &str,
    ) -> Result<Self, DittoError> {
        let c_data_dir = ditto_root.root_dir_to_c_str()?;
        let c_app_id = app_id.to_c_string();
        let c_key_der = char_p::new(key_der_b64);
        let auth_client = unsafe {
            let executor = make_executor();
            ffi_sdk::ditto_auth_client_make_with_shared_key_with_executor(
                Some(c_data_dir.as_ref()),
                c_app_id.as_ref(),
                c_key_der.as_ref(),
                0,
                executor,
            )
            .ok_or(ErrorKind::Config)?
        };
        let authenticator = DittoAuthenticator::new(Arc::new(auth_client));
        Ok(Self {
            auth: authenticator,
        })
    }
}

impl Identity for SharedKey {
    fn site_id(&self) -> SiteId {
        unsafe { ffi_sdk::ditto_auth_client_get_site_id(&self.auth_client()) }
    }

    fn auth_client(&self) -> Arc<BoxedAuthClient> {
        self.auth.auth_client()
    }

    fn is_web_valid(&self) -> bool {
        unsafe { ffi_sdk::ditto_auth_client_is_web_valid(&self.auth_client()) != 0 }
    }

    fn is_x509_valid(&self) -> bool {
        unsafe { ffi_sdk::ditto_auth_client_is_x509_valid(&self.auth_client()) != 0 }
    }

    fn auth_url(&self) -> Result<String, DittoError> {
        let msg = "SharedKey Identities don't support auth urls".to_owned();
        Err(DittoError::new(ErrorKind::Config, msg))
    }

    fn sync_url(&self) -> Result<String, DittoError> {
        let msg = "SharedKey Identities to support cloud sync".to_owned();
        Err(DittoError::new(ErrorKind::Config, msg))
    }

    fn authenticator(&self) -> Option<DittoAuthenticator> {
        None
    }

    fn make_listener(
        &self,
        _transports: Weak<RwLock<TransportSync>>,
    ) -> Option<Arc<ValidityListener>> {
        None
    }

    fn is_cloud_sync_enabled(&self) -> bool {
        false
    }

    fn requires_offline_only_license_token(&self) -> bool {
        true
    }
}

impl Manual {
    /// Construct a `Manual` identity.
    ///
    /// - `ditto_root`: DittoRoot instance indicating local storage directory
    /// - `certificate_config_b64`: A valid configuration of a x509 PKI Client Certificate Chain,
    ///   Private Key, and other Identity data which identifies this instance of this app to other
    ///   peers and allows for a TLS session to be established. This bundle should be provided as a
    ///   single Base64 encoded string
    pub fn new(certificate_config_b64: &str) -> Result<Self, DittoError> {
        let certificate_config = char_p::new(certificate_config_b64);
        let auth_client = unsafe {
            let executor = make_executor();
            ffi_sdk::ditto_auth_client_make_with_static_x509_with_executor(
                certificate_config.as_ref(),
                executor,
            )
            .ok_or(ErrorKind::Config)?
        };
        let authenticator = DittoAuthenticator::new(Arc::new(auth_client));
        Ok(Self {
            auth: authenticator,
        })
    }
}

impl Identity for Manual {
    fn site_id(&self) -> SiteId {
        unsafe { ffi_sdk::ditto_auth_client_get_site_id(&self.auth_client()) }
    }

    fn auth_client(&self) -> Arc<BoxedAuthClient> {
        self.auth.auth_client()
    }

    fn is_web_valid(&self) -> bool {
        unsafe { ffi_sdk::ditto_auth_client_is_web_valid(&self.auth_client()) != 0 }
    }

    fn is_x509_valid(&self) -> bool {
        unsafe { ffi_sdk::ditto_auth_client_is_x509_valid(&self.auth_client()) != 0 }
    }

    fn auth_url(&self) -> Result<String, DittoError> {
        let msg = "Manual Identities don't support auth urls".to_owned();
        Err(DittoError::new(ErrorKind::Config, msg))
    }

    fn sync_url(&self) -> Result<String, DittoError> {
        let msg = "Manual Identities to support cloud sync".to_owned();
        Err(DittoError::new(ErrorKind::Config, msg))
    }

    fn authenticator(&self) -> Option<DittoAuthenticator> {
        None
    }

    fn make_listener(
        &self,
        _transports: Weak<RwLock<TransportSync>>,
    ) -> Option<Arc<ValidityListener>> {
        None
    }

    fn is_cloud_sync_enabled(&self) -> bool {
        false
    }

    fn requires_offline_only_license_token(&self) -> bool {
        true
    }
}

impl OnlinePlayground {
    /// Construct a new `OnlinePlayground` identity.
    ///
    /// - `ditto_root`: Instance of DittoRoot indicating local storage directory
    /// - `app_id`: A unique AppId which must be a valid UUIDv4
    /// - `shared_token`: A shared token used to set up the `OnlinePlayground` session. This token
    ///   is provided by the portal when setting up the application.
    /// - `enable_cloud_sync`: Should WebSocket sync with `wss://<app_id>.cloud.ditto.live` be
    ///   enabled by default. Do not enable this if you want to provide a custom sync URL later
    /// - `custom_auth_url`: An optional alternative URL for authentication requests. Defaults to `https://<app_id>.cloud.ditto.live/`
    pub fn new(
        ditto_root: Arc<dyn DittoRoot>,
        app_id: AppId,
        shared_token: String,
        enable_cloud_sync: bool,
        custom_auth_url: Option<&str>,
    ) -> Result<Self, DittoError> {
        let c_data_dir = ditto_root.root_dir_to_c_str()?;
        let c_app_id = app_id.to_c_string();
        let c_shared_token = char_p::new(shared_token.as_str());
        let auth_url = match custom_auth_url {
            Some(url) => url.to_owned(),
            None => app_id.default_auth_url(),
        };
        let c_auth_url = char_p::new(auth_url.as_str());
        let auth_client = unsafe {
            let executor = make_executor();

            ffi_sdk::ditto_auth_client_make_anonymous_client_with_executor(
                c_data_dir.as_ref(),
                c_app_id.as_ref(),
                c_shared_token.as_ref(),
                c_auth_url.as_ref(),
                executor,
            )
            .ok_or(ErrorKind::Config)?
        };
        let authenticator = DittoAuthenticator::new(Arc::new(auth_client));
        Ok(Self {
            app_id,
            auth: authenticator,
            enable_cloud_sync,
            auth_url,
        })
    }
}

impl Identity for OnlinePlayground {
    fn auth_client(&self) -> Arc<BoxedAuthClient> {
        self.auth.auth_client()
    }

    fn is_web_valid(&self) -> bool {
        unsafe { ffi_sdk::ditto_auth_client_is_web_valid(&self.auth_client()) != 0 }
    }

    fn is_x509_valid(&self) -> bool {
        unsafe { ffi_sdk::ditto_auth_client_is_x509_valid(&self.auth_client()) != 0 }
    }

    fn auth_url(&self) -> Result<String, DittoError> {
        Ok(self.auth_url.to_owned())
    }

    fn sync_url(&self) -> Result<String, DittoError> {
        Ok(self.app_id.default_sync_url())
    }

    fn authenticator(&self) -> Option<DittoAuthenticator> {
        Some(self.auth.retain())
    }

    fn make_listener(
        &self,
        transports: Weak<RwLock<TransportSync>>,
    ) -> Option<Arc<ValidityListener>> {
        Some(ValidityListener::new(transports, self.auth.auth_client()))
    }

    fn is_cloud_sync_enabled(&self) -> bool {
        self.enable_cloud_sync
    }

    fn requires_offline_only_license_token(&self) -> bool {
        false
    }
}

mod private {
    use super::*;
    pub trait Sealed {}
    impl Sealed for OnlinePlayground {}
    impl Sealed for Manual {}
    impl Sealed for OnlineWithAuthentication {}
    impl Sealed for OfflinePlayground {}
    impl Sealed for SharedKey {}
}

pub(crate) fn make_executor() -> repr_c::Box<ffi_sdk::Executor> {
    unsafe {
        match Handle::try_current() {
            Ok(handle) if cfg!(feature = "shared-runtime-hack") => {
                let handle = Box::new(handle);
                ffi_sdk::ditto_make_executor_from_handle(handle)
            }
            _ => ffi_sdk::ditto_make_executor_new_runtime(),
        }
    }
}
