use libthreema_macros::concat_fixed_bytes;
use tracing::{debug, info};

use crate::{
    common::{
        Nonce,
        keys::{ClientKey, PublicKey},
    },
    crypto::{
        aead::AeadInPlace as _,
        cipher::KeyInit as _,
        digest::{MAC_256_LENGTH, Mac as _},
        salsa20::XSalsa20Poly1305,
        x25519,
    },
    csp::{
        ClientCookie, ClientSequenceNumber, Cookie, CspProtocolContext, CspProtocolError,
        CspProtocolInternalErrorCause, ServerCookie, ServerSequenceNumber, TemporaryClientKey,
        TemporaryServerKey,
        payload::{
            IncomingPayload, OutgoingPayload,
            handshake::{Extensions, LoginAck, LoginData, ServerChallengeResponse},
        },
    },
    utils::{debug::Name as _, sequence_numbers::SequenceNumberValue},
};

/// Concatenate a cookie and a sequence number to create a nonce.
///
/// Note: The sequence number is not incremented within this function!
#[inline]
#[expect(clippy::needless_pass_by_value, reason = "Prevent sequence number re-use")]
fn create_nonce(cookie: Cookie, sequence_number: SequenceNumberValue<u64>) -> Nonce {
    Nonce(concat_fixed_bytes!(cookie.0, sequence_number.0.to_le_bytes()))
}

/// Decrypt an incoming `server-challenge-response`.
///
/// Try all permanent server keys of `context`, and returns an error if none of them could be used
/// to decrypt the `server-challenge-response`.
pub(super) fn decrypt_server_challenge_response(
    context: &CspProtocolContext,
    temporary_client_key: &TemporaryClientKey,
    server_cookie: &ServerCookie,
    server_sequence_number: &mut ServerSequenceNumber,
    mut server_challenge_response_box: Vec<u8>,
) -> Result<(PublicKey, Vec<u8>), CspProtocolError> {
    // Compute the nonce once. Secure because we use different public keys for the same nonce.
    let nonce = create_nonce(server_cookie.0, server_sequence_number.0.get_and_increment()?);

    // Try to decrypt the server challenge response with all available permanent server keys.
    for permanent_server_key in &context.permanent_server_keys {
        let cipher = XSalsa20Poly1305::new(
            x25519::SharedSecretHSalsa20::from(
                temporary_client_key
                    .0
                    .diffie_hellman(&permanent_server_key.0)
                    .ok_or(CspProtocolError::InvalidParameter(
                        "Non-contributory permanent public server key",
                    ))?,
            )
            .as_bytes()
            .into(),
        );
        match cipher.decrypt_in_place((&nonce).into(), &[], &mut server_challenge_response_box) {
            Ok(()) => {
                debug!(?permanent_server_key, "Selected permanent server key");
                return Ok((*permanent_server_key, server_challenge_response_box));
            },
            Err(_) => {
                info!(mismatching_permanent_server_key = ?permanent_server_key,
                    "Decrypting server-challenge-response with server public key failed. \
                     Trying next one (if any)."
                );
            },
        }
    }

    // None of the permanent server keys was able to decrypt the challenge response.
    Err(CspProtocolError::DecryptionFailed {
        name: ServerChallengeResponse::NAME,
    })
}

struct VouchCipher {
    temporary_server_key: TemporaryServerKey,
    temporary_client_key_public: x25519::PublicKey,
}
impl VouchCipher {
    fn vouch_session(
        &self,
        client_key: &ClientKey,
        permanent_server_key: &PublicKey,
        server_cookie: &ServerCookie,
    ) -> Option<[u8; MAC_256_LENGTH]> {
        // Obtain the CSP authentication secret (aka _vouch key_)
        let vouch_key =
            client_key.derive_csp_authentication_key(permanent_server_key, &self.temporary_server_key.0)?;

        // Compute the vouch from the vouch key and server_cookie || temporary_client_key_public
        Some(
            vouch_key
                .0
                .chain_update(server_cookie.0.0)
                .chain_update(self.temporary_client_key_public.as_bytes())
                .finalize()
                .into_bytes()
                .into(),
        )
    }
}

pub(super) struct SessionCipher {
    client_cookie: ClientCookie,
    client_sequence_number: ClientSequenceNumber,
    server_cookie: ServerCookie,
    server_sequence_number: ServerSequenceNumber,
    cipher: XSalsa20Poly1305,
}
impl SessionCipher {
    /// Encrypt outgoing data in-place.
    fn encrypt(&mut self, name: &'static str, mut data: Vec<u8>) -> Result<Vec<u8>, CspProtocolError> {
        let nonce = create_nonce(
            self.client_cookie.0,
            self.client_sequence_number.0.get_and_increment()?,
        );
        self.cipher
            .encrypt_in_place((&nonce).into(), &[], &mut data)
            .map_err(|_| {
                CspProtocolError::InternalError(CspProtocolInternalErrorCause::EncryptionFailed { name })
            })?;
        Ok(data)
    }

    /// Decrypt incoming data in-place.
    fn decrypt(&mut self, name: &'static str, mut data: Vec<u8>) -> Result<Vec<u8>, CspProtocolError> {
        let nonce = create_nonce(
            self.server_cookie.0,
            self.server_sequence_number.0.get_and_increment()?,
        );
        self.cipher
            .decrypt_in_place((&nonce).into(), &[], &mut data)
            .map_err(|_| CspProtocolError::DecryptionFailed { name })?;
        Ok(data)
    }
}

pub(super) struct LoginBoxes {
    pub(super) login_data_box: Vec<u8>,
    pub(super) extensions_box: Vec<u8>,
}

pub(super) struct LoginCipher {
    vouch_cipher: VouchCipher,
    session_cipher: SessionCipher,
}
impl LoginCipher {
    /// Create the cipher needed to encrypt `login` contents.
    pub(super) fn new(
        temporary_client_key: &TemporaryClientKey,
        client_cookie: ClientCookie,
        client_sequence_number: ClientSequenceNumber,
        server_cookie: ServerCookie,
        server_sequence_number: ServerSequenceNumber,
        temporary_server_key: TemporaryServerKey,
    ) -> Option<Self> {
        let temporary_client_key_public = x25519::PublicKey::from(&temporary_client_key.0);
        let session_key = x25519::SharedSecretHSalsa20::from(
            temporary_client_key.0.diffie_hellman(&temporary_server_key.0.0)?,
        );
        let session_cipher = SessionCipher {
            client_cookie,
            client_sequence_number,
            server_cookie,
            server_sequence_number,
            cipher: XSalsa20Poly1305::new(session_key.as_bytes().into()),
        };
        let vouch_cipher = VouchCipher {
            temporary_server_key,
            temporary_client_key_public,
        };
        Some(Self {
            vouch_cipher,
            session_cipher,
        })
    }

    /// Dissolve the cipher, returning the wrapped [`SessionCipher`].
    pub(super) fn dissolve(self) -> SessionCipher {
        self.session_cipher
    }

    /// Create a vouch MAC for use in the `login`.
    #[inline]
    pub(super) fn vouch_session(
        &self,
        client_key: &ClientKey,
        permanent_server_key: &PublicKey,
    ) -> Option<[u8; MAC_256_LENGTH]> {
        self.vouch_cipher.vouch_session(
            client_key,
            permanent_server_key,
            &self.session_cipher.server_cookie,
        )
    }

    /// Encrypt data of the `login` message in-place.
    #[inline]
    pub(super) fn encrypt_login(
        &mut self,
        login_data: Vec<u8>,
        extensions: Vec<u8>,
    ) -> Result<LoginBoxes, CspProtocolError> {
        let login_data_box = self.session_cipher.encrypt(LoginData::NAME, login_data)?;
        let extensions_box = self.session_cipher.encrypt(Extensions::NAME, extensions)?;
        Ok(LoginBoxes {
            login_data_box,
            extensions_box,
        })
    }
}

pub(super) struct LoginAckCipher {
    session_cipher: SessionCipher,
}
impl LoginAckCipher {
    /// Create the cipher needed to decrypt `login-ack` contents.
    pub(super) fn new(session_cipher: SessionCipher) -> LoginAckCipher {
        Self { session_cipher }
    }

    /// Dissolve the cipher, returning the wrapped [`SessionCipher`].
    pub(super) fn dissolve(self) -> SessionCipher {
        self.session_cipher
    }

    /// Decrypt data of the `login-ack` message in-place.
    pub(super) fn decrypt(&mut self, login_ack_box: Vec<u8>) -> Result<Vec<u8>, CspProtocolError> {
        self.session_cipher.decrypt(LoginAck::NAME, login_ack_box)
    }
}

/// Cipher to encrypt/decrypt outgoing/incoming payloads.
pub(super) struct PayloadCipher(SessionCipher);
impl PayloadCipher {
    /// Create the cipher needed to encrypt/decrypt payloads.
    pub(super) fn new(session_cipher: SessionCipher) -> Self {
        Self(session_cipher)
    }

    /// Encrypt an outgoing payload in-place.
    #[inline]
    pub(super) fn encrypt_payload(&mut self, payload: Vec<u8>) -> Result<Vec<u8>, CspProtocolError> {
        self.0.encrypt(OutgoingPayload::NAME, payload)
    }

    /// Decrypt an incoming payload in-place.
    #[inline]
    pub(super) fn decrypt_payload(&mut self, payload: Vec<u8>) -> Result<Vec<u8>, CspProtocolError> {
        self.0.decrypt(IncomingPayload::NAME, payload)
    }
}

#[cfg(test)]
mod tests {
    use assert_matches::assert_matches;
    use derive_builder::Builder;
    use rstest::rstest;

    use crate::{
        common::keys::PublicKey,
        crypto::{aead::Aead as _, cipher::KeyInit as _, salsa20, x25519},
        csp::{
            Cookie, CspProtocolError, SequenceNumberU64, ServerChallengeResponse, ServerCookie,
            ServerSequenceNumber, TemporaryClientKey, cipher::create_nonce,
            decrypt_server_challenge_response, tests::ContextBuilder,
        },
        utils::{debug::Name as _, sequence_numbers::SequenceNumberValue},
    };

    #[derive(Builder)]
    #[builder(pattern = "owned")]
    struct ServerChallengeResponseTestContext {
        #[builder(default = "x25519::StaticSecret::from([1; x25519::KEY_LENGTH])")]
        temporary_server_key: x25519::StaticSecret,

        #[builder(default = "TemporaryClientKey(x25519::StaticSecret::from([2; x25519::KEY_LENGTH]))")]
        temporary_client_key: TemporaryClientKey,

        #[builder(default = "ServerCookie(Cookie([1; Cookie::LENGTH]))")]
        server_cookie: ServerCookie,

        #[builder(default = "1")]
        server_sequence_number: u64,

        #[builder(default = b"so ein quatsch".to_vec())]
        server_challenge_response: Vec<u8>,
    }
    impl ServerChallengeResponseTestContext {
        fn temporary_server_key_public(&self) -> PublicKey {
            PublicKey(x25519::PublicKey::from(&self.temporary_server_key))
        }

        fn server_sequence_number(&self) -> ServerSequenceNumber {
            ServerSequenceNumber(SequenceNumberU64::new(self.server_sequence_number))
        }

        fn create_server_challenge_response(&self) -> Vec<u8> {
            let cipher = salsa20::XSalsa20Poly1305::new(
                x25519::SharedSecretHSalsa20::from(
                    self.temporary_server_key
                        .diffie_hellman(&x25519::PublicKey::from(&self.temporary_client_key.0))
                        .unwrap(),
                )
                .as_bytes()
                .into(),
            );
            let nonce = create_nonce(
                self.server_cookie.0,
                SequenceNumberValue(self.server_sequence_number),
            );
            cipher
                .encrypt((&nonce).into(), self.server_challenge_response.as_slice())
                .unwrap()
        }
    }

    #[rstest]
    #[case(vec![PublicKey::from([1; PublicKey::LENGTH])])]
    #[case(vec![PublicKey::from([1; PublicKey::LENGTH]), PublicKey::from([2; PublicKey::LENGTH])])]
    fn server_challenge_response_invalid_mismatching_permanent_server_keys(
        #[case] permanent_server_keys: Vec<PublicKey>,
    ) {
        let server_challenge_response_context = ServerChallengeResponseTestContextBuilder::default()
            .build()
            .unwrap();
        let csp_context = ContextBuilder::default()
            .with_permanent_server_keys(permanent_server_keys)
            .build();

        // Decrypting the challenge response should fail after trying all of them.
        let result = decrypt_server_challenge_response(
            &csp_context,
            &server_challenge_response_context.temporary_client_key,
            &server_challenge_response_context.server_cookie,
            &mut server_challenge_response_context.server_sequence_number(),
            server_challenge_response_context.create_server_challenge_response(),
        );
        assert_matches!(
            result,
            Err(CspProtocolError::DecryptionFailed {
                name: ServerChallengeResponse::NAME,
            })
        );
    }

    #[rstest]
    #[case(vec![PublicKey::from([0; PublicKey::LENGTH])])]
    #[case(vec![PublicKey::from([1; PublicKey::LENGTH]), PublicKey::from([0; PublicKey::LENGTH])])]
    fn server_challenge_response_invalid_non_contributory_permanent_server_key(
        #[case] permanent_server_keys: Vec<PublicKey>,
    ) {
        let server_challenge_response_context = ServerChallengeResponseTestContextBuilder::default()
            .build()
            .unwrap();
        let csp_context = ContextBuilder::default()
            .with_permanent_server_keys(permanent_server_keys)
            .build();

        // Decrypting the challenge response should fail because the key is non-contributory.
        let result = decrypt_server_challenge_response(
            &csp_context,
            &server_challenge_response_context.temporary_client_key,
            &server_challenge_response_context.server_cookie,
            &mut server_challenge_response_context.server_sequence_number(),
            server_challenge_response_context.create_server_challenge_response(),
        );
        assert_eq!(
            result,
            Err(CspProtocolError::InvalidParameter(
                "Non-contributory permanent public server key"
            ))
        );
    }

    #[test]
    fn server_challenge_response_valid() {
        let server_challenge_response_context = ServerChallengeResponseTestContextBuilder::default()
            .build()
            .unwrap();
        let csp_context = ContextBuilder::default()
            .with_permanent_server_keys(vec![
                server_challenge_response_context.temporary_server_key_public(),
            ])
            .build();

        // Decrypting the challenge response should be successful.
        let (selected_permanent_server_key, server_challenge_response) = decrypt_server_challenge_response(
            &csp_context,
            &server_challenge_response_context.temporary_client_key,
            &server_challenge_response_context.server_cookie,
            &mut server_challenge_response_context.server_sequence_number(),
            server_challenge_response_context.create_server_challenge_response(),
        )
        .unwrap();
        assert_eq!(
            selected_permanent_server_key,
            server_challenge_response_context.temporary_server_key_public()
        );
        assert_eq!(
            server_challenge_response,
            server_challenge_response_context.server_challenge_response
        );
    }
}
