// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// Copyright by contributors to this project.
// SPDX-License-Identifier: (Apache-2.0 OR MIT)

//! Definitions to build a [`Client`].
//!
//! See [`ClientBuilder`].

use crate::{
    cipher_suite::CipherSuite,
    client::Client,
    client_config::ClientConfig,
    extension::ExtensionType,
    group::{
        mls_rules::{DefaultMlsRules, MlsRules},
        proposal::ProposalType,
    },
    identity::CredentialType,
    identity::SigningIdentity,
    protocol_version::ProtocolVersion,
    psk::{ExternalPskId, PreSharedKey},
    storage_provider::in_memory::{
        InMemoryGroupStateStorage, InMemoryKeyPackageStorage, InMemoryPreSharedKeyStorage,
    },
    time::MlsTime,
    tree_kem::{Capabilities, Lifetime},
    Sealed,
};

use alloc::vec::Vec;
use core::time::Duration;

#[cfg(feature = "sqlite")]
use mls_rs_provider_sqlite::{
    SqLiteDataStorageEngine, SqLiteDataStorageError,
    {
        connection_strategy::ConnectionStrategy,
        storage::{SqLiteGroupStateStorage, SqLiteKeyPackageStorage, SqLitePreSharedKeyStorage},
    },
};

#[cfg(feature = "private_message")]
pub use crate::group::padding::PaddingMode;

/// Base client configuration type when instantiating `ClientBuilder`
pub type BaseConfig = Config<
    InMemoryKeyPackageStorage,
    InMemoryPreSharedKeyStorage,
    InMemoryGroupStateStorage,
    Missing,
    DefaultMlsRules,
    Missing,
>;

/// Base client configuration type when instantiating `ClientBuilder`
pub type BaseInMemoryConfig = Config<
    InMemoryKeyPackageStorage,
    InMemoryPreSharedKeyStorage,
    InMemoryGroupStateStorage,
    Missing,
    Missing,
    Missing,
>;

pub type EmptyConfig = Config<Missing, Missing, Missing, Missing, Missing, Missing>;

/// Base client configuration that is backed by SQLite storage.
#[cfg(feature = "sqlite")]
pub type BaseSqlConfig = Config<
    SqLiteKeyPackageStorage,
    SqLitePreSharedKeyStorage,
    SqLiteGroupStateStorage,
    Missing,
    DefaultMlsRules,
    Missing,
>;

/// Builder for [`Client`]
///
/// This is returned by [`Client::builder`] and allows to tweak settings the `Client` will use. At a
/// minimum, the builder must be told the [`CryptoProvider`] and [`IdentityProvider`] to use. Other
/// settings have default values. This means that the following
/// methods must be called before [`ClientBuilder::build`]:
///
/// - To specify the [`CryptoProvider`]: [`ClientBuilder::crypto_provider`]
/// - To specify the [`IdentityProvider`]: [`ClientBuilder::identity_provider`]
///
/// # Example
///
/// ```
/// use mls_rs::{
///     Client,
///     identity::{SigningIdentity, basic::{BasicIdentityProvider, BasicCredential}},
///     CipherSuite,
/// };
///
/// use mls_rs_crypto_openssl::OpensslCryptoProvider;
///
/// // Replace by code to load the certificate and secret key
/// let secret_key = b"never hard-code secrets".to_vec().into();
/// let public_key = b"test invalid public key".to_vec().into();
/// let basic_identity = BasicCredential::new(b"name".to_vec());
/// let signing_identity = SigningIdentity::new(basic_identity.into_credential(), public_key);
///
///
/// let _client = Client::builder()
///     .crypto_provider(OpensslCryptoProvider::default())
///     .identity_provider(BasicIdentityProvider::new())
///     .signing_identity(signing_identity, secret_key, CipherSuite::CURVE25519_AES128)
///     .build();
/// ```
///
/// # Spelling out a `Client` type
///
/// There are two main ways to spell out a `Client` type if needed (e.g. function return type).
///
/// The first option uses `impl MlsConfig`:
/// ```
/// use mls_rs::{
///     Client,
///     client_builder::MlsConfig,
///     identity::{SigningIdentity, basic::{BasicIdentityProvider, BasicCredential}},
///     CipherSuite,
/// };
///
/// use mls_rs_crypto_openssl::OpensslCryptoProvider;
///
/// fn make_client() -> Client<impl MlsConfig> {
///     // Replace by code to load the certificate and secret key
///     let secret_key = b"never hard-code secrets".to_vec().into();
///     let public_key = b"test invalid public key".to_vec().into();
///     let basic_identity = BasicCredential::new(b"name".to_vec());
///     let signing_identity = SigningIdentity::new(basic_identity.into_credential(), public_key);
///
///     Client::builder()
///         .crypto_provider(OpensslCryptoProvider::default())
///         .identity_provider(BasicIdentityProvider::new())
///         .signing_identity(signing_identity, secret_key, CipherSuite::CURVE25519_AES128)
///         .build()
/// }
///```
///
/// The second option is more verbose and consists in writing the full `Client` type:
/// ```
/// use mls_rs::{
///     Client,
///     client_builder::{BaseConfig, WithIdentityProvider, WithCryptoProvider},
///     identity::{SigningIdentity, basic::{BasicIdentityProvider, BasicCredential}},
///     CipherSuite,
/// };
///
/// use mls_rs_crypto_openssl::OpensslCryptoProvider;
///
/// type MlsClient = Client<
///     WithIdentityProvider<
///         BasicIdentityProvider,
///         WithCryptoProvider<OpensslCryptoProvider, BaseConfig>,
///     >,
/// >;
///
/// fn make_client_2() -> MlsClient {
///     // Replace by code to load the certificate and secret key
///     let secret_key = b"never hard-code secrets".to_vec().into();
///     let public_key = b"test invalid public key".to_vec().into();
///     let basic_identity = BasicCredential::new(b"name".to_vec());
///     let signing_identity = SigningIdentity::new(basic_identity.into_credential(), public_key);
///
///     Client::builder()
///         .crypto_provider(OpensslCryptoProvider::default())
///         .identity_provider(BasicIdentityProvider::new())
///         .signing_identity(signing_identity, secret_key, CipherSuite::CURVE25519_AES128)
///         .build()
/// }
///
/// ```
#[derive(Debug)]
pub struct ClientBuilder<C>(C);

impl Default for ClientBuilder<BaseConfig> {
    fn default() -> Self {
        Self::new()
    }
}

impl<C> ClientBuilder<C> {
    pub(crate) fn from_config(c: C) -> Self {
        Self(c)
    }
}

impl ClientBuilder<BaseConfig> {
    /// Create a new client builder with default in-memory providers
    pub fn new() -> Self {
        Self(Config(ConfigInner {
            settings: Default::default(),
            key_package_repo: Default::default(),
            psk_store: Default::default(),
            group_state_storage: Default::default(),
            identity_provider: Missing,
            mls_rules: DefaultMlsRules::new(),
            crypto_provider: Missing,
            signer: Default::default(),
            signing_identity: Default::default(),
            version: ProtocolVersion::MLS_10,
        }))
    }
}

impl ClientBuilder<EmptyConfig> {
    pub fn new_empty() -> Self {
        Self(Config(ConfigInner {
            settings: Default::default(),
            key_package_repo: Missing,
            psk_store: Missing,
            group_state_storage: Missing,
            identity_provider: Missing,
            mls_rules: Missing,
            crypto_provider: Missing,
            signer: Default::default(),
            signing_identity: Default::default(),
            version: ProtocolVersion::MLS_10,
        }))
    }
}

#[cfg(feature = "sqlite")]
impl ClientBuilder<BaseSqlConfig> {
    /// Create a new client builder with SQLite storage providers.
    pub fn new_sqlite<CS: ConnectionStrategy>(
        storage: SqLiteDataStorageEngine<CS>,
    ) -> Result<Self, SqLiteDataStorageError> {
        Ok(Self(Config(ConfigInner {
            settings: Default::default(),
            key_package_repo: storage.key_package_storage()?,
            psk_store: storage.pre_shared_key_storage()?,
            group_state_storage: storage.group_state_storage()?,
            identity_provider: Missing,
            mls_rules: DefaultMlsRules::new(),
            crypto_provider: Missing,
            signer: Default::default(),
            signing_identity: Default::default(),
            version: ProtocolVersion::MLS_10,
        })))
    }
}

impl<C: IntoConfig> ClientBuilder<C> {
    /// Add an extension type to the list of extension types supported by the client.
    pub fn extension_type(self, type_: ExtensionType) -> ClientBuilder<IntoConfigOutput<C>> {
        self.extension_types(Some(type_))
    }

    /// Add multiple extension types to the list of extension types supported by the client.
    pub fn extension_types<I>(self, types: I) -> ClientBuilder<IntoConfigOutput<C>>
    where
        I: IntoIterator<Item = ExtensionType>,
    {
        let mut c = self.0.into_config();
        c.0.settings.extension_types.extend(types);
        ClientBuilder(c)
    }

    /// Add a custom proposal type to the list of proposals types supported by the client.
    pub fn custom_proposal_type(self, type_: ProposalType) -> ClientBuilder<IntoConfigOutput<C>> {
        self.custom_proposal_types(Some(type_))
    }

    /// Add multiple custom proposal types to the list of proposal types supported by the client.
    pub fn custom_proposal_types<I>(self, types: I) -> ClientBuilder<IntoConfigOutput<C>>
    where
        I: IntoIterator<Item = ProposalType>,
    {
        let mut c = self.0.into_config();
        c.0.settings.custom_proposal_types.extend(types);
        ClientBuilder(c)
    }

    /// Add a protocol version to the list of protocol versions supported by the client.
    ///
    /// If no protocol version is explicitly added, the client will support all protocol versions
    /// supported by this crate.
    pub fn protocol_version(self, version: ProtocolVersion) -> ClientBuilder<IntoConfigOutput<C>> {
        self.protocol_versions(Some(version))
    }

    /// Add multiple protocol versions to the list of protocol versions supported by the client.
    ///
    /// If no protocol version is explicitly added, the client will support all protocol versions
    /// supported by this crate.
    pub fn protocol_versions<I>(self, versions: I) -> ClientBuilder<IntoConfigOutput<C>>
    where
        I: IntoIterator<Item = ProtocolVersion>,
    {
        let mut c = self.0.into_config();
        c.0.settings.protocol_versions.extend(versions);
        ClientBuilder(c)
    }

    /// Set the lifetime duration of key packages generated by the client.
    pub fn key_package_lifetime(self, lifetime: Duration) -> ClientBuilder<IntoConfigOutput<C>> {
        let mut c = self.0.into_config();
        c.0.settings.lifetime = lifetime;
        ClientBuilder(c)
    }

    /// Set the key package repository to be used by the client.
    ///
    /// By default, an in-memory repository is used.
    pub fn key_package_repo<K>(self, key_package_repo: K) -> ClientBuilder<WithKeyPackageRepo<K, C>>
    where
        K: KeyPackageStorage,
    {
        let Config(c) = self.0.into_config();

        ClientBuilder(Config(ConfigInner {
            settings: c.settings,
            key_package_repo,
            psk_store: c.psk_store,
            group_state_storage: c.group_state_storage,
            identity_provider: c.identity_provider,
            mls_rules: c.mls_rules,
            crypto_provider: c.crypto_provider,
            signer: c.signer,
            signing_identity: c.signing_identity,
            version: c.version,
        }))
    }

    /// Set the PSK store to be used by the client.
    ///
    /// By default, an in-memory store is used.
    pub fn psk_store<P>(self, psk_store: P) -> ClientBuilder<WithPskStore<P, C>>
    where
        P: PreSharedKeyStorage,
    {
        let Config(c) = self.0.into_config();

        ClientBuilder(Config(ConfigInner {
            settings: c.settings,
            key_package_repo: c.key_package_repo,
            psk_store,
            group_state_storage: c.group_state_storage,
            identity_provider: c.identity_provider,
            mls_rules: c.mls_rules,
            crypto_provider: c.crypto_provider,
            signer: c.signer,
            signing_identity: c.signing_identity,
            version: c.version,
        }))
    }

    /// Set the group state storage to be used by the client.
    ///
    /// By default, an in-memory storage is used.
    pub fn group_state_storage<G>(
        self,
        group_state_storage: G,
    ) -> ClientBuilder<WithGroupStateStorage<G, C>>
    where
        G: GroupStateStorage,
    {
        let Config(c) = self.0.into_config();

        ClientBuilder(Config(ConfigInner {
            settings: c.settings,
            key_package_repo: c.key_package_repo,
            psk_store: c.psk_store,
            group_state_storage,
            identity_provider: c.identity_provider,
            crypto_provider: c.crypto_provider,
            mls_rules: c.mls_rules,
            signer: c.signer,
            signing_identity: c.signing_identity,
            version: c.version,
        }))
    }

    /// Set the identity validator to be used by the client.
    pub fn identity_provider<I>(
        self,
        identity_provider: I,
    ) -> ClientBuilder<WithIdentityProvider<I, C>>
    where
        I: IdentityProvider,
    {
        let Config(c) = self.0.into_config();

        ClientBuilder(Config(ConfigInner {
            settings: c.settings,
            key_package_repo: c.key_package_repo,
            psk_store: c.psk_store,
            group_state_storage: c.group_state_storage,
            identity_provider,
            mls_rules: c.mls_rules,
            crypto_provider: c.crypto_provider,
            signer: c.signer,
            signing_identity: c.signing_identity,
            version: c.version,
        }))
    }

    /// Set the crypto provider to be used by the client.
    pub fn crypto_provider<Cp>(
        self,
        crypto_provider: Cp,
    ) -> ClientBuilder<WithCryptoProvider<Cp, C>>
    where
        Cp: CryptoProvider,
    {
        let Config(c) = self.0.into_config();

        ClientBuilder(Config(ConfigInner {
            settings: c.settings,
            key_package_repo: c.key_package_repo,
            psk_store: c.psk_store,
            group_state_storage: c.group_state_storage,
            identity_provider: c.identity_provider,
            mls_rules: c.mls_rules,
            crypto_provider,
            signer: c.signer,
            signing_identity: c.signing_identity,
            version: c.version,
        }))
    }

    /// Set the user-defined proposal rules to be used by the client.
    ///
    /// User-defined rules are used when sending and receiving commits before
    /// enforcing general MLS protocol rules. If the rule set returns an error when
    /// receiving a commit, the entire commit is considered invalid. If the
    /// rule set would return an error when sending a commit, individual proposals
    /// may be filtered out to compensate.
    pub fn mls_rules<Pr>(self, mls_rules: Pr) -> ClientBuilder<WithMlsRules<Pr, C>>
    where
        Pr: MlsRules,
    {
        let Config(c) = self.0.into_config();

        ClientBuilder(Config(ConfigInner {
            settings: c.settings,
            key_package_repo: c.key_package_repo,
            psk_store: c.psk_store,
            group_state_storage: c.group_state_storage,
            identity_provider: c.identity_provider,
            mls_rules,
            crypto_provider: c.crypto_provider,
            signer: c.signer,
            signing_identity: c.signing_identity,
            version: c.version,
        }))
    }

    /// Set the protocol version used by the client. By default, the client uses version MLS 1.0
    pub fn used_protocol_version(
        self,
        version: ProtocolVersion,
    ) -> ClientBuilder<IntoConfigOutput<C>> {
        let mut c = self.0.into_config();
        c.0.version = version;
        ClientBuilder(c)
    }

    /// Set the signing identity used by the client as well as the matching signer and cipher suite.
    /// This must be called in order to create groups and key packages.
    pub fn signing_identity(
        self,
        signing_identity: SigningIdentity,
        signer: SignatureSecretKey,
        cipher_suite: CipherSuite,
    ) -> ClientBuilder<IntoConfigOutput<C>> {
        let mut c = self.0.into_config();
        c.0.signer = Some(signer);
        c.0.signing_identity = Some((signing_identity, cipher_suite));
        ClientBuilder(c)
    }

    /// Set the signer used by the client. This must be called in order to join groups.
    pub fn signer(self, signer: SignatureSecretKey) -> ClientBuilder<IntoConfigOutput<C>> {
        let mut c = self.0.into_config();
        c.0.signer = Some(signer);
        ClientBuilder(c)
    }

    #[cfg(any(test, feature = "test_util"))]
    pub(crate) fn key_package_not_before(
        self,
        key_package_not_before: MlsTime,
    ) -> ClientBuilder<IntoConfigOutput<C>> {
        let mut c = self.0.into_config();
        c.0.settings.key_package_not_before = Some(key_package_not_before);
        ClientBuilder(c)
    }
}

impl<C: IntoConfig> ClientBuilder<C>
where
    C::KeyPackageRepository: KeyPackageStorage + Clone,
    C::PskStore: PreSharedKeyStorage + Clone,
    C::GroupStateStorage: GroupStateStorage + Clone,
    C::IdentityProvider: IdentityProvider + Clone,
    C::MlsRules: MlsRules + Clone,
    C::CryptoProvider: CryptoProvider + Clone,
{
    pub(crate) fn build_config(self) -> IntoConfigOutput<C> {
        let mut c = self.0.into_config();

        if c.0.settings.protocol_versions.is_empty() {
            c.0.settings.protocol_versions = ProtocolVersion::all().collect();
        }

        c
    }

    /// Build a client.
    ///
    /// See [`ClientBuilder`] documentation if the return type of this function needs to be spelled
    /// out.
    pub fn build(self) -> Client<IntoConfigOutput<C>> {
        let mut c = self.build_config();
        let version = c.0.version;
        let signer = c.0.signer.take();
        let signing_identity = c.0.signing_identity.take();

        Client::new(c, signer, signing_identity, version)
    }
}

impl<C: IntoConfig<PskStore = InMemoryPreSharedKeyStorage>> ClientBuilder<C> {
    /// Add a PSK to the in-memory PSK store.
    pub fn psk(
        self,
        psk_id: ExternalPskId,
        psk: PreSharedKey,
    ) -> ClientBuilder<IntoConfigOutput<C>> {
        let mut c = self.0.into_config();
        c.0.psk_store.insert(psk_id, psk);
        ClientBuilder(c)
    }
}

/// Marker type for required `ClientBuilder` services that have not been specified yet.
#[derive(Debug)]
pub struct Missing;

/// Change the key package repository used by a client configuration.
///
/// See [`ClientBuilder::key_package_repo`].
pub type WithKeyPackageRepo<K, C> = Config<
    K,
    <C as IntoConfig>::PskStore,
    <C as IntoConfig>::GroupStateStorage,
    <C as IntoConfig>::IdentityProvider,
    <C as IntoConfig>::MlsRules,
    <C as IntoConfig>::CryptoProvider,
>;

/// Change the PSK store used by a client configuration.
///
/// See [`ClientBuilder::psk_store`].
pub type WithPskStore<P, C> = Config<
    <C as IntoConfig>::KeyPackageRepository,
    P,
    <C as IntoConfig>::GroupStateStorage,
    <C as IntoConfig>::IdentityProvider,
    <C as IntoConfig>::MlsRules,
    <C as IntoConfig>::CryptoProvider,
>;

/// Change the group state storage used by a client configuration.
///
/// See [`ClientBuilder::group_state_storage`].
pub type WithGroupStateStorage<G, C> = Config<
    <C as IntoConfig>::KeyPackageRepository,
    <C as IntoConfig>::PskStore,
    G,
    <C as IntoConfig>::IdentityProvider,
    <C as IntoConfig>::MlsRules,
    <C as IntoConfig>::CryptoProvider,
>;

/// Change the identity validator used by a client configuration.
///
/// See [`ClientBuilder::identity_provider`].
pub type WithIdentityProvider<I, C> = Config<
    <C as IntoConfig>::KeyPackageRepository,
    <C as IntoConfig>::PskStore,
    <C as IntoConfig>::GroupStateStorage,
    I,
    <C as IntoConfig>::MlsRules,
    <C as IntoConfig>::CryptoProvider,
>;

/// Change the proposal rules used by a client configuration.
///
/// See [`ClientBuilder::mls_rules`].
pub type WithMlsRules<Pr, C> = Config<
    <C as IntoConfig>::KeyPackageRepository,
    <C as IntoConfig>::PskStore,
    <C as IntoConfig>::GroupStateStorage,
    <C as IntoConfig>::IdentityProvider,
    Pr,
    <C as IntoConfig>::CryptoProvider,
>;

/// Change the crypto provider used by a client configuration.
///
/// See [`ClientBuilder::crypto_provider`].
pub type WithCryptoProvider<Cp, C> = Config<
    <C as IntoConfig>::KeyPackageRepository,
    <C as IntoConfig>::PskStore,
    <C as IntoConfig>::GroupStateStorage,
    <C as IntoConfig>::IdentityProvider,
    <C as IntoConfig>::MlsRules,
    Cp,
>;

/// Helper alias for `Config`.
pub type IntoConfigOutput<C> = Config<
    <C as IntoConfig>::KeyPackageRepository,
    <C as IntoConfig>::PskStore,
    <C as IntoConfig>::GroupStateStorage,
    <C as IntoConfig>::IdentityProvider,
    <C as IntoConfig>::MlsRules,
    <C as IntoConfig>::CryptoProvider,
>;

/// Helper alias to make a `Config` from a `ClientConfig`
pub type MakeConfig<C> = Config<
    <C as ClientConfig>::KeyPackageRepository,
    <C as ClientConfig>::PskStore,
    <C as ClientConfig>::GroupStateStorage,
    <C as ClientConfig>::IdentityProvider,
    <C as ClientConfig>::MlsRules,
    <C as ClientConfig>::CryptoProvider,
>;

impl<Kpr, Ps, Gss, Ip, Pr, Cp> ClientConfig for ConfigInner<Kpr, Ps, Gss, Ip, Pr, Cp>
where
    Kpr: KeyPackageStorage + Clone,
    Ps: PreSharedKeyStorage + Clone,
    Gss: GroupStateStorage + Clone,
    Ip: IdentityProvider + Clone,
    Pr: MlsRules + Clone,
    Cp: CryptoProvider + Clone,
{
    type KeyPackageRepository = Kpr;
    type PskStore = Ps;
    type GroupStateStorage = Gss;
    type IdentityProvider = Ip;
    type MlsRules = Pr;
    type CryptoProvider = Cp;

    fn supported_extensions(&self) -> Vec<ExtensionType> {
        self.settings.extension_types.clone()
    }

    fn supported_protocol_versions(&self) -> Vec<ProtocolVersion> {
        self.settings.protocol_versions.clone()
    }

    fn key_package_repo(&self) -> Self::KeyPackageRepository {
        self.key_package_repo.clone()
    }

    fn mls_rules(&self) -> Self::MlsRules {
        self.mls_rules.clone()
    }

    fn secret_store(&self) -> Self::PskStore {
        self.psk_store.clone()
    }

    fn group_state_storage(&self) -> Self::GroupStateStorage {
        self.group_state_storage.clone()
    }

    fn identity_provider(&self) -> Self::IdentityProvider {
        self.identity_provider.clone()
    }

    fn crypto_provider(&self) -> Self::CryptoProvider {
        self.crypto_provider.clone()
    }

    fn lifetime(&self, timestamp: Option<MlsTime>) -> Lifetime {
        #[cfg(feature = "std")]
        let now_timestamp = MlsTime::now();

        #[cfg(not(feature = "std"))]
        let now_timestamp = MlsTime::from(0);

        let now_timestamp = if let Some(now_time) = timestamp {
            now_time
        } else {
            now_timestamp
        };

        #[cfg(test)]
        let now_timestamp = self
            .settings
            .key_package_not_before
            .unwrap_or(now_timestamp);

        Lifetime {
            not_before: now_timestamp,
            not_after: now_timestamp + self.settings.lifetime,
        }
    }

    fn supported_custom_proposals(&self) -> Vec<crate::group::proposal::ProposalType> {
        self.settings.custom_proposal_types.clone()
    }
}

impl<Kpr, Ps, Gss, Ip, Pr, Cp> Sealed for Config<Kpr, Ps, Gss, Ip, Pr, Cp> {}

impl<Kpr, Ps, Gss, Ip, Pr, Cp> MlsConfig for Config<Kpr, Ps, Gss, Ip, Pr, Cp>
where
    Kpr: KeyPackageStorage + Clone,

    Ps: PreSharedKeyStorage + Clone,
    Gss: GroupStateStorage + Clone,
    Ip: IdentityProvider + Clone,
    Pr: MlsRules + Clone,
    Cp: CryptoProvider + Clone,
{
    type Output = ConfigInner<Kpr, Ps, Gss, Ip, Pr, Cp>;

    fn get(&self) -> &Self::Output {
        &self.0
    }
}

/// Helper trait to allow consuming crates to easily write a client type as `Client<impl MlsConfig>`
///
/// It is not meant to be implemented by consuming crates. `T: MlsConfig` implies `T: ClientConfig`.
pub trait MlsConfig: Clone + Send + Sync + Sealed {
    #[doc(hidden)]
    type Output: ClientConfig;

    #[doc(hidden)]
    fn get(&self) -> &Self::Output;
}

/// Blanket implementation so that `T: MlsConfig` implies `T: ClientConfig`
impl<T: MlsConfig> ClientConfig for T {
    type KeyPackageRepository = <T::Output as ClientConfig>::KeyPackageRepository;
    type PskStore = <T::Output as ClientConfig>::PskStore;
    type GroupStateStorage = <T::Output as ClientConfig>::GroupStateStorage;
    type IdentityProvider = <T::Output as ClientConfig>::IdentityProvider;
    type MlsRules = <T::Output as ClientConfig>::MlsRules;
    type CryptoProvider = <T::Output as ClientConfig>::CryptoProvider;

    fn supported_extensions(&self) -> Vec<ExtensionType> {
        self.get().supported_extensions()
    }

    fn supported_custom_proposals(&self) -> Vec<ProposalType> {
        self.get().supported_custom_proposals()
    }

    fn supported_protocol_versions(&self) -> Vec<ProtocolVersion> {
        self.get().supported_protocol_versions()
    }

    fn key_package_repo(&self) -> Self::KeyPackageRepository {
        self.get().key_package_repo()
    }

    fn mls_rules(&self) -> Self::MlsRules {
        self.get().mls_rules()
    }

    fn secret_store(&self) -> Self::PskStore {
        self.get().secret_store()
    }

    fn group_state_storage(&self) -> Self::GroupStateStorage {
        self.get().group_state_storage()
    }

    fn identity_provider(&self) -> Self::IdentityProvider {
        self.get().identity_provider()
    }

    fn crypto_provider(&self) -> Self::CryptoProvider {
        self.get().crypto_provider()
    }

    fn lifetime(&self, timestamp: Option<MlsTime>) -> Lifetime {
        self.get().lifetime(timestamp)
    }

    fn capabilities(&self) -> Capabilities {
        self.get().capabilities()
    }

    fn version_supported(&self, version: ProtocolVersion) -> bool {
        self.get().version_supported(version)
    }

    fn supported_credential_types(&self) -> Vec<CredentialType> {
        self.get().supported_credential_types()
    }
}

#[derive(Clone, Debug)]
pub(crate) struct Settings {
    pub(crate) extension_types: Vec<ExtensionType>,
    pub(crate) protocol_versions: Vec<ProtocolVersion>,
    pub(crate) custom_proposal_types: Vec<ProposalType>,
    pub(crate) lifetime: Duration,
    #[cfg(any(test, feature = "test_util"))]
    pub(crate) key_package_not_before: Option<MlsTime>,
}

impl Default for Settings {
    fn default() -> Self {
        Self {
            extension_types: Default::default(),
            protocol_versions: Default::default(),
            lifetime: 365 * 24 * Duration::from_secs(3600),
            custom_proposal_types: Default::default(),
            #[cfg(any(test, feature = "test_util"))]
            key_package_not_before: None,
        }
    }
}

pub(crate) fn recreate_config<T: ClientConfig>(
    c: T,
    signer: Option<SignatureSecretKey>,
    signing_identity: Option<(SigningIdentity, CipherSuite)>,
    version: ProtocolVersion,
    timestamp: Option<MlsTime>,
) -> MakeConfig<T> {
    Config(ConfigInner {
        settings: Settings {
            extension_types: c.supported_extensions(),
            protocol_versions: c.supported_protocol_versions(),
            custom_proposal_types: c.supported_custom_proposals(),
            lifetime: {
                let l = c.lifetime(timestamp);
                l.not_after - l.not_before
            },
            #[cfg(any(test, feature = "test_util"))]
            key_package_not_before: None,
        },
        key_package_repo: c.key_package_repo(),
        psk_store: c.secret_store(),
        group_state_storage: c.group_state_storage(),
        identity_provider: c.identity_provider(),
        mls_rules: c.mls_rules(),
        crypto_provider: c.crypto_provider(),
        signer,
        signing_identity,
        version,
    })
}

/// Definitions meant to be private that are inaccessible outside this crate. They need to be marked
/// `pub` because they appear in public definitions.
mod private {
    use mls_rs_core::{
        crypto::{CipherSuite, SignatureSecretKey},
        identity::SigningIdentity,
        protocol_version::ProtocolVersion,
    };

    use crate::client_builder::{IntoConfigOutput, Settings};

    #[derive(Clone, Debug)]
    pub struct Config<Kpr, Ps, Gss, Ip, Pr, Cp>(pub(crate) ConfigInner<Kpr, Ps, Gss, Ip, Pr, Cp>);

    #[derive(Clone, Debug)]
    pub struct ConfigInner<Kpr, Ps, Gss, Ip, Pr, Cp> {
        pub(crate) settings: Settings,
        pub(crate) key_package_repo: Kpr,
        pub(crate) psk_store: Ps,
        pub(crate) group_state_storage: Gss,
        pub(crate) identity_provider: Ip,
        pub(crate) mls_rules: Pr,
        pub(crate) crypto_provider: Cp,
        pub(crate) signer: Option<SignatureSecretKey>,
        pub(crate) signing_identity: Option<(SigningIdentity, CipherSuite)>,
        pub(crate) version: ProtocolVersion,
    }

    pub trait IntoConfig {
        type KeyPackageRepository;
        type PskStore;
        type GroupStateStorage;
        type IdentityProvider;
        type MlsRules;
        type CryptoProvider;

        fn into_config(self) -> IntoConfigOutput<Self>;
    }

    impl<Kpr, Ps, Gss, Ip, Pr, Cp> IntoConfig for Config<Kpr, Ps, Gss, Ip, Pr, Cp> {
        type KeyPackageRepository = Kpr;
        type PskStore = Ps;
        type GroupStateStorage = Gss;
        type IdentityProvider = Ip;
        type MlsRules = Pr;
        type CryptoProvider = Cp;

        fn into_config(self) -> Self {
            self
        }
    }
}

use mls_rs_core::{
    crypto::{CryptoProvider, SignatureSecretKey},
    group::GroupStateStorage,
    identity::IdentityProvider,
    key_package::KeyPackageStorage,
    psk::PreSharedKeyStorage,
};
use private::{Config, ConfigInner, IntoConfig};

#[cfg(test)]
pub(crate) mod test_utils {
    use crate::{
        client_builder::{BaseConfig, ClientBuilder, WithIdentityProvider},
        crypto::test_utils::TestCryptoProvider,
        identity::{
            basic::BasicIdentityProvider,
            test_utils::{get_test_signing_identity, BasicWithCustomProvider},
        },
        CipherSuite,
    };

    use super::WithCryptoProvider;

    pub type TestClientConfig = WithIdentityProvider<
        BasicWithCustomProvider,
        WithCryptoProvider<TestCryptoProvider, BaseConfig>,
    >;

    pub type TestClientBuilder = ClientBuilder<TestClientConfig>;

    impl TestClientBuilder {
        pub fn new_for_test() -> Self {
            ClientBuilder::new()
                .crypto_provider(TestCryptoProvider::new())
                .identity_provider(BasicWithCustomProvider::new(BasicIdentityProvider::new()))
        }

        #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
        pub async fn with_random_signing_identity(
            self,
            identity: &str,
            cipher_suite: CipherSuite,
        ) -> Self {
            let (signing_identity, signer) =
                get_test_signing_identity(cipher_suite, identity.as_bytes()).await;
            self.signing_identity(signing_identity, signer, cipher_suite)
        }
    }
}
