use std::{
    os::raw::{c_int, c_uint, c_void},
    pin::Pin,
    sync::{Arc, Mutex, RwLock, Weak},
    time::Duration,
};

use ffi_sdk::{BoxedAuthClient, BoxedLoginProvider};

use crate::{
    ditto::{Ditto, DittoFields},
    error::{DittoError, ErrorKind},
    transport::TransportSync,
    utils::prelude::*,
};

/// Implement this trait for a type in order to construct an
/// [`OnlineWithAuthentication`](crate::prelude::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);
}

/// This wraps an optional JSON blob that could be provided by an authentication provider. It can be
/// used to give details about the authentication or explain why it was rejected.
#[derive(Clone, Debug)]
pub struct AuthenticationClientFeedback {
    pub feedback: Option<serde_json::Value>,
}

pub(crate) struct LoginProvider {
    pub(crate) _provider: BoxedLoginProvider,
    pub(crate) ctx: Arc<Mutex<LoginProviderCtx>>,
}

// this inner type allows us to get a reference/ptr before the outer type is
// constructed
pub(crate) struct LoginProviderCtx {
    auth_event_handler: Pin<Box<dyn DittoAuthenticationEventHandler + 'static>>,
    authenticator: Option<DittoAuthenticator>,
    #[allow(dead_code)] // called across the FFI
    cached_expiry_time: Option<u32>,
}

impl LoginProvider {
    pub fn new(handler: impl DittoAuthenticationEventHandler + 'static) -> Self {
        let ctx = LoginProviderCtx {
            auth_event_handler: Box::pin(handler),
            authenticator: None,
            cached_expiry_time: None,
        };

        let arc_ctx = Arc::new(Mutex::new(ctx));
        // let raw_context = Arc::into_raw(arc_ctx.clone()) as *mut c_void; // this
        // calls mem::forget internally Across the FFI, this ptr will be
        // immediately `retained` establishing shared ownership of
        // the LoginProviderCtx across the FFI
        let raw_context = Arc::as_ptr(&arc_ctx) as *mut c_void;

        let c_provider = unsafe {
            ffi_sdk::ditto_auth_client_make_login_provider(
                raw_context,
                Some(LoginProviderCtx::retain),
                Some(LoginProviderCtx::release),
                Some(LoginProviderCtx::authentication_expiring),
            )
        };

        LoginProvider {
            _provider: c_provider,
            ctx: arc_ctx,
        }
    }
}

impl LoginProviderCtx {
    pub(crate) extern "C" fn retain(ctx: *mut c_void) {
        // the ctx* ptr here will (and must) have a layout determined by Arc::into_raw
        // The cast should target the inner type of the Arc, which is in this case a
        // Mutex<LoginProviderCtx> so the Arc::increment_strong_count hits the correct
        // offset
        // If the type of LoginProvider.ctx changes in the future, this must be updated
        let ptr = ctx.cast::<Mutex<LoginProviderCtx>>();
        unsafe { Arc::increment_strong_count(ptr) }; // internally calls
                                                     // Arc::from_raw
    }

    pub(crate) extern "C" fn release(ctx: *mut c_void) {
        let ptr = ctx.cast::<Mutex<LoginProviderCtx>>();
        unsafe {
            Arc::decrement_strong_count(ptr);
        } // internally calls Arc::from_raw
    }

    pub(crate) extern "C" fn authentication_expiring(ctx: *mut c_void, seconds_remaining: c_uint) {
        let ctx_ptr: *const Mutex<LoginProviderCtx> = ctx.cast();
        let arc_ctx: &Mutex<LoginProviderCtx> = unsafe { &*ctx_ptr }; // Copies the ptr to the inner type on the heap, then borrow
        let mut ctx_ref = arc_ctx.lock().expect("LoginProvider Mutex is poisoned"); // typically caused by panic while holding this lock
        match &ctx_ref.authenticator {
            Some(authn) => {
                if seconds_remaining == 0 {
                    ctx_ref
                        .auth_event_handler
                        .authentication_required(authn.retain());
                } else {
                    let duration = Duration::from_secs(seconds_remaining.into());
                    ctx_ref
                        .auth_event_handler
                        .authentication_expiring_soon(authn.retain(), duration);
                }
            }
            None => ctx_ref.cached_expiry_time = Some(seconds_remaining),
        }
    }
    pub(crate) fn set_authenticator(&mut self, authenticator: DittoAuthenticator) {
        self.authenticator = Some(authenticator);
        if let Some(authn) = &self.authenticator {
            if let Some(time) = self.cached_expiry_time {
                // easier than trying to call across the FFI
                if time == 0 {
                    self.auth_event_handler
                        .authentication_required(authn.retain());
                } else {
                    let duration = Duration::from_secs(time.into());
                    self.auth_event_handler
                        .authentication_expiring_soon(authn.retain(), duration);
                }
                self.cached_expiry_time = None; // set to None after calling
                                                // either method
            }
        }
    }
}

// TODO(pub_check)
pub struct ValidityListener(Weak<RwLock<TransportSync>>);

impl ValidityListener {
    // TODO(pub_check)
    pub fn new(
        transport: Weak<RwLock<TransportSync>>,
        auth_client_ref: impl AsRef<BoxedAuthClient>,
    ) -> Arc<ValidityListener> {
        let this = Arc::new(ValidityListener(transport));
        let this_ptr = Arc::as_ptr(&this) as *mut c_void;

        extern "C" fn on_validity_update(ctx: *mut c_void, web_valid: c_int, x509_valid: c_int) {
            let ctx_ref: &ValidityListener =
                unsafe { ctx.cast::<ValidityListener>().as_ref().expect("Got Null") }; // only safe if ValidityListener not dropped
            {
                if let Some(transport) = ctx_ref.0.upgrade() {
                    let mut lock = transport.write().expect("Poisoned Transport Lock");
                    lock.validity_updated(web_valid != 0, x509_valid != 0);
                }
            }
        }

        extern "C" fn retain(ctx: *mut c_void) {
            let ptr = ctx.cast::<ValidityListener>();
            unsafe {
                Arc::increment_strong_count(ptr);
            }
        }
        extern "C" fn release(ctx: *mut c_void) {
            let ptr = ctx.cast::<ValidityListener>();
            unsafe {
                Arc::decrement_strong_count(ptr);
            }
        }

        unsafe {
            ffi_sdk::ditto_auth_client_set_validity_listener(
                auth_client_ref.as_ref(),
                this_ptr,
                Some(retain),
                Some(release),
                Some(on_validity_update),
            );
        }
        this
    }
}

impl RefCounted for DittoAuthenticator {}
// Cloning this will just increment the count on the inner Arc<BoxedAuthClient>,
// and allow the user to provide actual credentials at a later time
#[derive(Clone)]
/// Handle to trigger authentication requests
pub struct DittoAuthenticator {
    auth_client: Arc<BoxedAuthClient>,
    pub(crate) ditto_fields: std::sync::Weak<DittoFields>,
}

impl DittoAuthenticator {
    // TODO(pub_check)
    pub fn new(auth_client: Arc<BoxedAuthClient>) -> Self {
        DittoAuthenticator {
            auth_client,
            ditto_fields: std::sync::Weak::<DittoFields>::new(),
        }
    }

    pub(crate) fn auth_client(&self) -> Arc<BoxedAuthClient> {
        self.auth_client.retain()
    }

    /// Asks the [`Ditto`](crate::prelude::Ditto) instance to make an auth request to the configured
    /// [`Identity`](crate::prelude::Identity)'s auth URL with a single token parameter.
    ///
    /// - `token`: An auth or API token you have configured.
    /// - `provider`: The name of an authentication provider web hook you have configured in Ditto.
    ///   Cloud, which will accept the `token` and contact your Auth service
    pub fn login_with_token_and_feedback(
        &self,
        token: &str,
        provider: &str,
    ) -> Result<AuthenticationClientFeedback, DittoError> {
        let c_token = char_p::new(token);
        let c_provider = char_p::new(provider);
        let status = unsafe {
            ffi_sdk::ditto_auth_client_login_with_token_and_feedback(
                &*self.auth_client,
                c_token.as_ref(),
                c_provider.as_ref(),
            )
        };
        fn parse_client_info(s: Option<safer_ffi::String>) -> AuthenticationClientFeedback {
            AuthenticationClientFeedback {
                feedback: s.map(|x| serde_json::from_str(&x).unwrap()),
            }
        }
        match status.return_code {
            0 => Ok(parse_client_info(status.client_info)),
            _ => Err(DittoError::from_authentication_feedback(parse_client_info(
                status.client_info,
            ))),
        }
    }

    /// Log out of Ditto.
    ///
    /// Shutdown all replication sessions and remove any cached authentication credentials. This
    /// does *not* remove the local data store.
    pub fn logout<R>(&self, cleanup: impl FnOnce(Ditto) -> R) -> Result<R, DittoError> {
        let fields = self
            .ditto_fields
            .upgrade()
            .expect("fields hasn't been dropped yet");
        let ditto = Ditto::new_with_fields(fields);
        let status = unsafe { ffi_sdk::ditto_auth_client_logout(&*self.auth_client) };
        if status != 0 {
            return Err(DittoError::from_ffi(ErrorKind::Authentication));
        }
        ditto.stop_sync();
        let ret = cleanup(ditto);
        Ok(ret)
    }

    /// Query whether Ditto has a valid authentication token.
    ///
    /// This will only be `true` when using an
    /// [`OnlineWithAuthentication`](crate::prelude::OnlineWithAuthentication) identity, after a
    /// successful login. If the authentication token is allowed to expire then it will return
    /// `false` instead.
    pub fn is_authenticated(&self) -> bool {
        unsafe { ffi_sdk::ditto_auth_client_is_web_valid(&*self.auth_client) != 0 }
    }

    /// The currently logged-in user ID.
    ///
    /// This will return `None` if there is no valid authentication or an
    /// [`OnlineWithAuthentication`](crate::prelude::OnlineWithAuthentication) identity is not being
    /// used.
    pub fn user_id(&self) -> Option<String> {
        unsafe { ffi_sdk::ditto_auth_client_user_id(&*self.auth_client) }.map(|c_msg| {
            let s = c_msg.to_str().to_owned();
            unsafe { ffi_sdk::ditto_c_string_free(c_msg) };
            s
        })
    }
}
