use std::{
    os::raw::{c_uint, c_void},
    sync::Arc,
    time::Duration,
};

use extern_c::extern_c;
use ffi_sdk::{ffi_utils::Blocking, BoxedLoginProvider};

use crate::{identity::DittoAuthenticator, utils::prelude::*};

/// Implement this trait for a type in order to construct an
/// [`OnlineWithAuthentication`] identity.
// You can find an example in tests::common::mod
pub trait DittoAuthenticationEventHandler: Send + Sync {
    /// This will be called when you need to authenticate.
    /// Usually it will involve a call to `auth.login_with_token`
    fn authentication_required(&self, auth: DittoAuthenticator);

    /// Allows for custom behavior hooks when authentication is expiring
    fn authentication_expiring_soon(&self, auth: DittoAuthenticator, seconds_remaining: Duration);
}

pub(crate) struct LoginProvider(pub(crate) BoxedLoginProvider);

// this inner type allows us to get a reference/ptr before the outer type is
// constructed
pub(crate) struct LoginProviderCtx {
    // The user-provided auth logic (the `.authentication_…()` callbacks above).
    // Also called an `auth_delegate` in other SDKs.
    auth_event_handler: Arc<dyn DittoAuthenticationEventHandler + 'static>,
    // A fancy wrapper around a back-reference to Ditto.
    authenticator: DittoAuthenticator,
}

impl LoginProvider {
    pub fn new(
        auth_event_handler: Arc<dyn 'static + DittoAuthenticationEventHandler>,
        authenticator: DittoAuthenticator,
    ) -> Self {
        let login_provider_ctx = Arc::new(LoginProviderCtx {
            auth_event_handler,
            authenticator,
        });

        let c_provider = unsafe {
            ffi_sdk::ditto_auth_client_make_login_provider(
                // legacy non-preincremented convention.
                Arc::as_ptr(&login_provider_ctx) as *mut c_void,
                Some(extern_c(|ctx: *mut c_void, _: Blocking| {
                    Arc::<LoginProviderCtx>::increment_strong_count(ctx.cast())
                })),
                Some(extern_c(|ctx: *mut c_void, _: Blocking| {
                    Arc::<LoginProviderCtx>::decrement_strong_count(ctx.cast())
                })),
                extern_c(|ctx: *mut c_void, secs_remaining: c_uint, _: Blocking| {
                    let LoginProviderCtx {
                        auth_event_handler,
                        authenticator,
                    }: &LoginProviderCtx = &*ctx.cast();
                    let authenticator = authenticator.retain();
                    if let Some(duration) =
                        (secs_remaining != 0).then(|| Duration::from_secs(secs_remaining.into()))
                    {
                        auth_event_handler.authentication_expiring_soon(authenticator, duration);
                    } else {
                        auth_event_handler.authentication_required(authenticator);
                    }
                }),
            )
        };
        LoginProvider(c_provider)
    }
}
