use_prelude!();
use std::sync::{Arc, OnceLock};

use super::JsonObject;
use crate::transport::ConnectionType;

/// Contains information about a remote peer that has requested a connection.
///
/// Connection requests and their authorization are scoped to a specific Ditto
/// peer and connection type.
///
/// Information about a peer Ditto is currently trying to connect to. This struct is used by the
/// [`Presence::set_connection_request_handler()`] callback to decide whether the Ditto client
/// should connect to a given peer or not.
pub struct ConnectionRequest {
    raw: Arc<repr_c::Box<ffi_sdk::FfiConnectionRequest>>,
    // An optional map of peer-provided information, signed by the peer.
    cached_peer_metadata: OnceLock<Arc<JsonObject>>,
    // An optional map of identity service-provided information, signed by the identity
    // service.
    cached_identity_service_metadata: OnceLock<Arc<JsonObject>>,
    // The connecting peer's (pub) key (can be empty for legacy peers).
    cached_peer_key: OnceLock<Arc<str>>,
}

impl ConnectionRequest {
    pub(crate) fn new(raw: repr_c::Box<ffi_sdk::FfiConnectionRequest>) -> Self {
        Self {
            raw: Arc::new(raw),
            cached_peer_metadata: <_>::default(),
            cached_identity_service_metadata: <_>::default(),
            cached_peer_key: <_>::default(),
        }
    }

    pub(crate) fn raw(&self) -> Arc<repr_c::Box<ffi_sdk::FfiConnectionRequest>> {
        self.raw.retain()
    }

    /// JSON-encoded metadata associated with the remote peer.
    ///
    /// JSON string representing an empty dictionary if the remote peer has not
    /// set any metadata.
    ///
    /// Set peer metadata for the local peer using
    /// [`Presence::set_peer_metadata()`] or
    /// [`Presence::set_peer_metadata_json_str()`].
    pub fn peer_metadata_json_str(&self) -> String {
        String::from_utf8(
            ffi_sdk::dittoffi_connection_request_peer_metadata_json(&*self.raw).to_vec(),
        )
        .expect("UTF-8")
    }

    /// [`DeserializeOwned`] convenience wrapper around [`Self::peer_metadata_json_str()`].
    pub fn peer_metadata_serde<T: DeserializeOwned>(&self) -> Result<T> {
        let value = ::serde_json::from_str(&self.peer_metadata_json_str())?;
        Ok(value)
    }

    /// Metadata associated with the remote peer.
    ///
    /// Empty dictionary if the remote peer has not set any metadata.
    ///
    /// Set peer metadata for the local peer using
    /// [`Presence::set_peer_metadata()`] or
    /// [`Presence::set_peer_metadata_json_str()`].
    ///
    /// Convenience property that wraps [`Self::peer_metadata_json_str()`].
    pub fn peer_metadata(&self) -> Arc<JsonObject> {
        self.cached_peer_metadata
            .get_or_init(|| {
                Arc::new(
                    self.peer_metadata_serde()
                        .expect("incorrect json from `dittoffi`"),
                )
            })
            .retain()
    }

    /// JSON-encoded metadata for the remote peer that is provided by the
    /// identity service.
    ///
    /// Use an authentication webhook to set this value. See Ditto's online
    /// documentation for more information on how to configure an authentication
    /// webhook.
    pub fn identity_service_metadata_json_str(&self) -> String {
        String::from_utf8(
            ffi_sdk::dittoffi_connection_request_identity_service_metadata_json(&*self.raw)
                .to_vec(),
        )
        .expect("UTF-8")
    }

    /// [`DeserializeOwned`] convenience wrapper around
    /// [`Self::identity_service_metadata_json_str()`].
    pub fn identity_service_metadata_serde<T: DeserializeOwned>(&self) -> Result<T> {
        let value = ::serde_json::from_str(&self.identity_service_metadata_json_str())?;
        Ok(value)
    }

    /// Metadata for the remote peer that is provided by the identity service.
    ///
    /// Use an authentication webhook to set this value. See Ditto's online
    /// documentation for more information on how to configure an authentication
    /// webhook.
    ///
    /// Convenience property that wraps [`Self::identity_service_metadata_json_str()`].
    pub fn identity_service_metadata(&self) -> Arc<JsonObject> {
        self.cached_identity_service_metadata
            .get_or_init(|| {
                Arc::new(
                    self.identity_service_metadata_serde()
                        .expect("incorrect json from `dittoffi`"),
                )
            })
            .retain()
    }

    /// The unique peer key of the remote peer.
    ///
    /// - See also: [`crate::transport::Peer::peer_key_string`] for more information on peer keys.
    pub fn peer_key_string(&self) -> Arc<str> {
        self.cached_peer_key
            .get_or_init(|| {
                ffi_sdk::dittoffi_connection_request_peer_key_string(&*self.raw)
                    .to_str()
                    .into()
            })
            .retain()
    }

    /// The network transport of this connection request.
    ///
    /// Expect to receive separate connection requests for each network
    /// transport that connects the local and remote peer.
    pub fn connection_type(&self) -> ConnectionType {
        ConnectionType::from_ffi(::ffi_sdk::dittoffi_connection_request_connection_type(
            &*self.raw,
        ))
    }
}

#[derive(Debug)]
/// Private, to future-proof the API (_e.g._, imagine `Deny { reason: … }` or whatnot).
pub(crate) enum ConnectionRequestAuthorizationInner {
    Allow { _future_fields: () },
    Deny { _future_fields: () },
}

/// Indicates whether a connection request should be authorized.
#[derive(Debug)]
pub struct ConnectionRequestAuthorization(pub(crate) ConnectionRequestAuthorizationInner);

impl ConnectionRequestAuthorization {
    #![allow(non_upper_case_globals)]

    /// The connection request will be allowed.
    pub const Allow: Self = Self(ConnectionRequestAuthorizationInner::Allow { _future_fields: () });

    /// The connection request will be denied.
    pub const Deny: Self = Self(ConnectionRequestAuthorizationInner::Deny { _future_fields: () });

    pub(crate) fn into_ffi(self, raw: &ffi_sdk::FfiConnectionRequest) {
        let ffi_authorization = match self.0 {
            ConnectionRequestAuthorizationInner::Allow { _future_fields: () } => {
                ffi_sdk::ConnectionRequestAuthorization::Allow
            }
            ConnectionRequestAuthorizationInner::Deny { _future_fields: () } => {
                ffi_sdk::ConnectionRequestAuthorization::Deny
            }
        };
        ffi_sdk::dittoffi_connection_request_authorize(raw, ffi_authorization);
    }
}
