/*-
 * Scram-rs - a SCRAM authentification authorization library
 * 
 * Copyright (C) 2021  Aleksandr Morozov
 * Copyright (C) 2025 Aleksandr Morozov
 * 
 * The syslog-rs crate can be redistributed and/or modified
 * under the terms of either of the following licenses:
 *
 *   1. the Mozilla Public License Version 2.0 (the “MPL”) OR
 *
 *   2. The MIT License (MIT)
 *                     
 *   3. EUROPEAN UNION PUBLIC LICENCE v. 1.2 EUPL © the European Union 2007, 2016
 */


 #[cfg(feature = "std")]
use std::num::NonZeroU32;
#[cfg(not(feature = "std"))]
use core::num::NonZeroU32;

#[cfg(feature = "std")]
use std::str;
#[cfg(not(feature = "std"))]
use core::str;

#[cfg(feature = "std")]
use std::marker::PhantomData;
#[cfg(not(feature = "std"))]
use core::marker::PhantomData;

#[cfg(not(feature = "std"))]
use alloc::string::String;

#[cfg(not(feature = "std"))]
use alloc::boxed::Box;

use base64::Engine;
use base64::engine::general_purpose;

use crate::scram_cbh::ScramCbHelper;
pub use crate::scram_dyn::ScramServerDyn;
use crate::{scram_ierror, scram_ierror_map, ScramClientDyn, ScramKey};

use super::scram_error::{ScramResult, ScramErrorCode};
use super::scram_cb::ChannelBindType;
use super::scram_auth::ScramAuthClient;
use super::scram_hashing::ScramHashing;
use super::scram_common::ScramCommon;
use super::scram_state::ScramState;
use super::scram_parser::*;
use super::scram::{ScramNonce, ScramResultClient};

/// # A Scram Client. Should be used on the client side.
/// 
/// ## Generics 
/// 
/// * `S`- [ScramHashing] a preferable hashing algorithm.
/// 
/// * `A`- [ScramAuthClient] an adapter which is used to obtain user creds.
/// 
/// * `B` - [ScramCbHelper] a channel bind helper.
/// 
/// ```text
/// If it is required to support `channel binding`, the crate does not provide
/// a helpers to obtain required information. You must find out how to extract 
/// endpoint certificate from TLS connection.
///     i.e native_tls::TlsStream::tls_server_end_point()  
/// ```
#[derive(Debug)]
pub struct SyncScramClient<S: ScramHashing, A: ScramAuthClient, B: ScramCbHelper>
{
    /// A hasher picked
    hasher: PhantomData<S>,
    
    /// A authentification callback
    auth: A,
    
    /// A client generated/picked nonce (base64)
    client_nonce: String,
    
    /// A current state step
    state: ScramState,
    
    /// A type of the channel bind [ChannelBindType]
    chanbind: ChannelBindType,

    /// A callback to support channel bind mechanism
    chanbind_helper: B,

    /// Ignore the additional data (extensions).
    ignore_exts: bool
}

impl<S: ScramHashing, A: ScramAuthClient, B: ScramCbHelper> SyncScramClient<S, A, B>
{
    /// Creates a new client instance with borrowed arguments. Sets all fields to 
    /// default instance.
    /// 
    /// # Arguments
    /// 
    /// * `scram_auth_cli` - an authentification instance which implements [ScramAuthClient]
    /// 
    /// * `scram_nonce` - a client scram nonce [ScramNonce]
    /// 
    /// * `chan_bind_type` - picks the channel bound [ChannelBindType]. It is
    ///                     responsibility of the developer to correctly set the chan binding
    ///                     type.
    /// 
    /// * `chan_bind_helper` - a data type which implements a traint [ScramCbHelper] which
    ///                 contains functions for realization which are designed to provide the
    ///                 channel bind data to the `SCRAM` crate.
    /// 
    /// * `ignore_exts` - if set to true ignores the unsupported extensions which may be found
    ///                 after the main data. Otherwise, the error will be generated reporting 
    ///                 unexpected data.
    /// # Examples
    /// 
    /// ```ignore
    /// let cbt = ChannelBindType::None;
    ///
    /// let ac = AuthClient::new(username, password);
    /// let nonce = ScramNonce::Plain(&client_nonce_dec);
    ///
    /// let scram_res = SyncScramClient::<ScramSha256, &AuthClient, &AuthClient>::new(&ac, nonce, cbt, &ac);
    /// ```
    pub 
    fn new(
        scram_auth_cli: A, 
        scram_nonce: ScramNonce, 
        chan_bind_type: ChannelBindType,
        chan_bind_helper: B,
        ignore_exts: bool
    ) -> ScramResult<SyncScramClient<S, A, B>>
    {
        return Ok(
            Self
            {
                hasher: PhantomData,
                auth: scram_auth_cli,
                client_nonce: scram_nonce.get_nonce(),
                state: ScramState::InitClient,
                chanbind: chan_bind_type,
                chanbind_helper: chan_bind_helper,
                ignore_exts: ignore_exts
            }
        );
    }

  

    /// Checks if the client authentification was completed successfully.
    pub 
    fn is_completed(&self) -> bool
    {
        return self.state == ScramState::Completed;
    }

    /// Initializes the SCRAM negoatiation from client
    /// 
    /// # Arguments
    /// 
    /// * `to_base64` - when set to true, the generated response will be encoded to
    ///                 base64.  
    /// 
    /// # Returns
    /// * base64 encoded or plain string with formed message
    pub 
    fn init_client(&mut self) -> ScramResultClient
    {            
        let compiled = 
            [
                self.chanbind.convert2header(),
                "n=", self.auth.get_username(),
                ",r=", self.client_nonce.as_str(),
            ].concat();
        
        self.state = ScramState::WaitForServInitMsg;     
        
        return ScramResultClient::Output(compiled);
    }

    /// Decodes the response from server which is in base64 encoded and performes parsing
    /// and result computation.
    ///
    /// # Arguments
    ///
    /// * `input` - a base64 encoded response from server
    /// 
    /// # Returns
    /// 
    /// * The [Result] is returned as alias [ScramResult].
    /// 
    /// - [Result::Ok] is returned with [ScramResultClient] which contains a hint how to
    ///     act on the next step.
    /// 
    /// - [Result::Err] is returned in case of error.
    pub 
    fn parse_response_base64<T: AsRef<[u8]>>(&mut self, input: T) -> ScramResult<ScramResultClient>
    {
        let decoded = 
            general_purpose::STANDARD.decode(input)
                .map_err(|e| 
                    scram_ierror_map!(
                        ScramErrorCode::MalformedScramMsg, 
                        "base64 decode server response failed, '{}'", e
                    )
                )?;

        let dec_utf8 = 
            str::from_utf8(&decoded)
                .map_err(|e| 
                    scram_ierror_map!(
                        ScramErrorCode::MalformedScramMsg, 
                        "base64 decoded response contains invalid UTF-8 seq, '{}'", e
                    )
                )?;

        return self.parse_response(dec_utf8);
    }

    /// Performes parsing of the response from server and result computation.  
    /// It is assumed that resp is UTF-8 valid sequences
    /// 
    /// # Arguments
    /// 
    /// * `resp` - A response from client as ref str.
    /// 
    /// * `to_base64` - if set to true, will encode response into base64.
    /// 
    /// # Returns
    /// 
    /// * The [Result] is returned as alias [ScramResult].
    /// 
    /// - [Result::Ok] is returned with [ScramResultClient] which contains a hint how to
    ///     act on the next step.
    /// 
    /// - [Result::Err] is returned in case of error.
    pub 
    fn parse_response(&mut self, resp: &str) -> ScramResult<ScramResultClient>
    {
        let parsed_resp = ScramData::try_from((resp, &self.state, self.ignore_exts))?;

        match parsed_resp
        {
            ScramData::CmsgInitial{..} =>
            {
                panic!("scram: client can not handle state: {}", parsed_resp);
            },
            // Client sends response to server after intial message
            ScramData::SmsgInitial{nonce, salt, itrcnt} => 
            {
                //validate iterations
                if itrcnt == 0 || itrcnt > ScramCommon::SCRAM_MAX_ITERS
                {
                    scram_ierror!(
                        ScramErrorCode::InternalError, 
                        "iterations count is not appropriate: i='{}'", 
                        itrcnt
                    );
                }

                //todo channel bind things SASL 
                let cb_data = 
                    general_purpose::STANDARD.encode(
                        [
                            self.chanbind.convert2header().as_bytes(), 
                            self.chanbind.get_cb_data_raw(&self.chanbind_helper)?.as_slice(),
                        ]
                        .concat()
                    );

                let client_final_message_bare = ["c=", &cb_data, ",r=", nonce].concat();

                let authmsg = 
                    [
                        "n=", self.auth.get_username(),
                        ",r=", self.client_nonce.as_str(), 
                        ",", resp,
                        ",", client_final_message_bare.as_str()
                    ]
                    .concat();

                let (key_client, key_server) = 
                    self
                        .auth
                        .get_scram_keys()
                        .map_or(
                            ScramKey::default_client_server_keys(), 
                            |key| (key.get_clinet_key(), key.get_server_key())
                        ); 
                
                let salted_password = 
                    S::derive(self.auth.get_password().as_bytes(), &salt, NonZeroU32::new(itrcnt).unwrap())?;
                 
                let client_key = S::hmac(key_client, &salted_password)?;
                let server_key = S::hmac(key_server, &salted_password)?;


                let stored_key = S::hash(&client_key);
                let client_signature = S::hmac(authmsg.as_bytes(), &stored_key)?;
                let server_signature = S::hmac(authmsg.as_bytes(), &server_key)?;
                
                let client_response = 
                    [
                        &client_final_message_bare,
                        ",p=",
                        &general_purpose::STANDARD.encode(S::xor_arrays(&client_key, &client_signature)?)
                    ]
                    .concat();
                

                self.state = ScramState::WaitForServFinalMsg{server_signature: server_signature};
                
                return Ok(ScramResultClient::Output(client_response));
            },
            ScramData::CmsgFinalMessage{..} =>
            {
                panic!("scram: client can not handle state: {}", parsed_resp);
            },
            ScramData::SmsgFinalMessage{verifier, server_verifier} =>
            {
                if &verifier == server_verifier
                {
                    self.state = ScramState::Completed;

                    return Ok(ScramResultClient::Completed);
                }
                else
                {
                    scram_ierror!(
                        ScramErrorCode::VerificationError,
                        "state: '{}', server signature mismatch, server sig: '{:x?}', local: {:x?}",
                        self.state, server_verifier, verifier
                    );
                }

            }
        }
    }
}

impl<'cc, S: ScramHashing + 'cc, A: ScramAuthClient + 'cc, B: ScramCbHelper + 'cc> SyncScramClient<S, A, B>
{
    pub 
    fn make_dyn(self) -> Box<dyn ScramClientDyn + 'cc>
    {
        return Box::new(self);
    }
}

impl<S: ScramHashing, A: ScramAuthClient, B: ScramCbHelper> ScramClientDyn
for SyncScramClient<S, A, B>
{
    fn init_client_msg(&mut self) -> ScramResultClient 
    {
        return self.init_client();
    }

    fn parse_response_base64(&mut self, input: &[u8]) -> ScramResult<ScramResultClient> 
    {
        return self.parse_response_base64(input);
    }
    
    fn parse_response(&mut self, resp: &str) -> ScramResult<ScramResultClient> 
    {
        return self.parse_response(resp);
    }
}
