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

use alloc::vec::Vec;
use core::fmt::{self, Debug};
use mls_rs_codec::{MlsDecode, MlsEncode, MlsSize};
use mls_rs_core::extension::{ExtensionType, MlsCodecExtension};

use mls_rs_core::{group::ProposalType, identity::CredentialType};

#[cfg(feature = "by_ref_proposal")]
use mls_rs_core::{
    extension::ExtensionList,
    identity::{IdentityProvider, SigningIdentity},
    time::MlsTime,
};

use crate::group::ExportedTree;

use mls_rs_core::crypto::HpkePublicKey;

/// Application specific identifier.
///
/// A custom application level identifier that can be optionally stored
/// within the `leaf_node_extensions` of a group [Member](crate::group::Member).
#[cfg_attr(
    all(feature = "ffi", not(test)),
    safer_ffi_gen::ffi_type(clone, opaque)
)]
#[derive(Clone, PartialEq, Eq, MlsSize, MlsEncode, MlsDecode)]
pub struct ApplicationIdExt {
    /// Application level identifier presented by this extension.
    #[mls_codec(with = "mls_rs_codec::byte_vec")]
    pub identifier: Vec<u8>,
}

impl Debug for ApplicationIdExt {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("ApplicationIdExt")
            .field(
                "identifier",
                &mls_rs_core::debug::pretty_bytes(&self.identifier),
            )
            .finish()
    }
}

#[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen)]
impl ApplicationIdExt {
    /// Create a new application level identifier extension.
    pub fn new(identifier: Vec<u8>) -> Self {
        ApplicationIdExt { identifier }
    }

    /// Get the application level identifier presented by this extension.
    #[cfg(feature = "ffi")]
    pub fn identifier(&self) -> &[u8] {
        &self.identifier
    }
}

impl MlsCodecExtension for ApplicationIdExt {
    fn extension_type() -> ExtensionType {
        ExtensionType::APPLICATION_ID
    }
}

/// Representation of an MLS ratchet tree.
///
/// Used to provide new members
/// a copy of the current group state in-band.
#[cfg_attr(
    all(feature = "ffi", not(test)),
    safer_ffi_gen::ffi_type(clone, opaque)
)]
#[derive(Clone, Debug, PartialEq, MlsSize, MlsEncode, MlsDecode)]
pub struct RatchetTreeExt {
    pub tree_data: ExportedTree<'static>,
}

#[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen)]
impl RatchetTreeExt {
    /// Required custom extension types.
    #[cfg(feature = "ffi")]
    pub fn tree_data(&self) -> &ExportedTree<'static> {
        &self.tree_data
    }
}

impl MlsCodecExtension for RatchetTreeExt {
    fn extension_type() -> ExtensionType {
        ExtensionType::RATCHET_TREE
    }
}

/// Require members to have certain capabilities.
///
/// Used within a
/// [Group Context Extensions Proposal](crate::group::proposal::Proposal)
/// in order to require that all current and future members of a group MUST
/// support specific extensions, proposals, or credentials.
///
/// # Warning
///
/// Extension, proposal, and credential types defined by the MLS RFC and
/// provided are considered required by default and should NOT be used
/// within this extension.
#[cfg_attr(
    all(feature = "ffi", not(test)),
    safer_ffi_gen::ffi_type(clone, opaque)
)]
#[derive(Clone, Debug, PartialEq, Eq, MlsSize, MlsEncode, MlsDecode, Default)]
pub struct RequiredCapabilitiesExt {
    pub extensions: Vec<ExtensionType>,
    pub proposals: Vec<ProposalType>,
    pub credentials: Vec<CredentialType>,
}

#[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen)]
impl RequiredCapabilitiesExt {
    /// Create a required capabilities extension.
    pub fn new(
        extensions: Vec<ExtensionType>,
        proposals: Vec<ProposalType>,
        credentials: Vec<CredentialType>,
    ) -> Self {
        Self {
            extensions,
            proposals,
            credentials,
        }
    }

    /// Required custom extension types.
    #[cfg(feature = "ffi")]
    pub fn extensions(&self) -> &[ExtensionType] {
        &self.extensions
    }

    /// Required custom proposal types.
    #[cfg(feature = "ffi")]
    pub fn proposals(&self) -> &[ProposalType] {
        &self.proposals
    }

    /// Required custom credential types.
    #[cfg(feature = "ffi")]
    pub fn credentials(&self) -> &[CredentialType] {
        &self.credentials
    }
}

impl MlsCodecExtension for RequiredCapabilitiesExt {
    fn extension_type() -> ExtensionType {
        ExtensionType::REQUIRED_CAPABILITIES
    }
}

/// External public key used for [External Commits](crate::Client::commit_external).
///
/// This proposal type is optionally provided as part of a
/// [Group Info](crate::group::Group::group_info_message).
#[cfg_attr(
    all(feature = "ffi", not(test)),
    safer_ffi_gen::ffi_type(clone, opaque)
)]
#[derive(Clone, Debug, PartialEq, Eq, MlsSize, MlsEncode, MlsDecode)]
pub struct ExternalPubExt {
    /// Public key to be used for an external commit.
    #[mls_codec(with = "mls_rs_codec::byte_vec")]
    pub external_pub: HpkePublicKey,
}

#[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen)]
impl ExternalPubExt {
    /// Get the public key to be used for an external commit.
    #[cfg(feature = "ffi")]
    pub fn external_pub(&self) -> &HpkePublicKey {
        &self.external_pub
    }
}

impl MlsCodecExtension for ExternalPubExt {
    fn extension_type() -> ExtensionType {
        ExtensionType::EXTERNAL_PUB
    }
}

/// Enable proposals by an [ExternalClient](crate::external_client::ExternalClient).
#[cfg(feature = "by_ref_proposal")]
#[cfg_attr(
    all(feature = "ffi", not(test)),
    safer_ffi_gen::ffi_type(clone, opaque)
)]
#[derive(Clone, Debug, PartialEq, Eq, MlsSize, MlsEncode, MlsDecode)]
#[non_exhaustive]
pub struct ExternalSendersExt {
    pub allowed_senders: Vec<SigningIdentity>,
}

#[cfg(feature = "by_ref_proposal")]
#[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen)]
impl ExternalSendersExt {
    pub fn new(allowed_senders: Vec<SigningIdentity>) -> Self {
        Self { allowed_senders }
    }

    #[cfg(feature = "ffi")]
    pub fn allowed_senders(&self) -> &[SigningIdentity] {
        &self.allowed_senders
    }

    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
    pub(crate) async fn verify_all<I: IdentityProvider>(
        &self,
        provider: &I,
        timestamp: Option<MlsTime>,
        group_context_extensions: &ExtensionList,
    ) -> Result<(), I::Error> {
        for id in self.allowed_senders.iter() {
            provider
                .validate_external_sender(id, timestamp, Some(group_context_extensions))
                .await?;
        }

        Ok(())
    }
}

#[cfg(feature = "by_ref_proposal")]
impl MlsCodecExtension for ExternalSendersExt {
    fn extension_type() -> ExtensionType {
        ExtensionType::EXTERNAL_SENDERS
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    use crate::tree_kem::node::NodeVec;
    #[cfg(feature = "by_ref_proposal")]
    use crate::{
        client::test_utils::TEST_CIPHER_SUITE, identity::test_utils::get_test_signing_identity,
    };

    use mls_rs_core::extension::MlsExtension;

    use mls_rs_core::identity::BasicCredential;

    use alloc::vec;

    #[cfg(target_arch = "wasm32")]
    use wasm_bindgen_test::wasm_bindgen_test as test;

    #[test]
    fn test_application_id_extension() {
        let test_id = vec![0u8; 32];
        let test_extension = ApplicationIdExt {
            identifier: test_id.clone(),
        };

        let as_extension = test_extension.into_extension().unwrap();

        assert_eq!(as_extension.extension_type, ExtensionType::APPLICATION_ID);

        let restored = ApplicationIdExt::from_extension(&as_extension).unwrap();
        assert_eq!(restored.identifier, test_id);
    }

    #[test]
    fn test_ratchet_tree() {
        let ext = RatchetTreeExt {
            tree_data: ExportedTree::new(NodeVec::from(vec![None, None])),
        };

        let as_extension = ext.clone().into_extension().unwrap();
        assert_eq!(as_extension.extension_type, ExtensionType::RATCHET_TREE);

        let restored = RatchetTreeExt::from_extension(&as_extension).unwrap();
        assert_eq!(ext, restored)
    }

    #[test]
    fn test_required_capabilities() {
        let ext = RequiredCapabilitiesExt {
            extensions: vec![0.into(), 1.into()],
            proposals: vec![42.into(), 43.into()],
            credentials: vec![BasicCredential::credential_type()],
        };

        let as_extension = ext.clone().into_extension().unwrap();

        assert_eq!(
            as_extension.extension_type,
            ExtensionType::REQUIRED_CAPABILITIES
        );

        let restored = RequiredCapabilitiesExt::from_extension(&as_extension).unwrap();
        assert_eq!(ext, restored)
    }

    #[cfg(feature = "by_ref_proposal")]
    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
    async fn test_external_senders() {
        let identity = get_test_signing_identity(TEST_CIPHER_SUITE, &[1]).await.0;
        let ext = ExternalSendersExt::new(vec![identity]);

        let as_extension = ext.clone().into_extension().unwrap();

        assert_eq!(as_extension.extension_type, ExtensionType::EXTERNAL_SENDERS);

        let restored = ExternalSendersExt::from_extension(&as_extension).unwrap();
        assert_eq!(ext, restored)
    }

    #[test]
    fn test_external_pub() {
        let ext = ExternalPubExt {
            external_pub: vec![0, 1, 2, 3].into(),
        };

        let as_extension = ext.clone().into_extension().unwrap();
        assert_eq!(as_extension.extension_type, ExtensionType::EXTERNAL_PUB);

        let restored = ExternalPubExt::from_extension(&as_extension).unwrap();
        assert_eq!(ext, restored)
    }
}
