/*-
 * 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::fmt;
#[cfg(not(feature = "std"))]
use core::fmt;

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

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

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

use crate::{scram_ierror, ScramRuntimeError};

use super::scram_error::{ScramResult, ScramErrorCode};
use super::scram_common::ScramCommon;

pub use super::scram_sync;

#[cfg(feature = "with_async")]
pub use super::scram_async;



/// A state encoder for Scram Client.
/// There is an internal state machine which automatically changes the
///  state. The timeout and other things must be implemented separatly.
///  The state from internal state machine is not exposed. If needed can
///  be maintained separatly.
/// 
/// A [ScramResultServer::Error] indicates an error i.e internal, protocol
///     violation or other. If the user program does not maintain its own
///     signalling (i.e encapsulation or error reporting like postgresql)
///     then the error message can be extracted and sent. The error will be
///     handled by the scram library.
#[derive(Debug)]
pub enum ScramResultServer
{
    /// A data to send back
    Data(String),

    /// - Error during processing. Use the same method `encode_base64` to
    ///  extract message and send back to client.
    /// - Authentification error which will be indicated as `VerificationError`.
    Error(ScramRuntimeError),
    
    /// A final message to send and continue, auth was successfull.
    Final(String)
}

impl ScramResultServer
{
    /// Tells if result does not contain error.
    pub 
    fn is_ok(&self) -> bool
    {
        match *self
        {
            Self::Error(_) => return false,
            _ => return true
        }
    }

    /// Tells if result is error.
    pub 
    fn is_err(&self) -> bool
    {
        return !self.is_ok();
    }
    
    /// Extracts error.
    pub 
    fn err(self) -> Option<ScramRuntimeError>
    {
        let Self::Error(err) = self
        else
        {
            return None;
        };

        return Some(err);
    }   

    /// Extracts error.
    pub 
    fn get_err(&self) -> Option<&ScramRuntimeError>
    {
        let Self::Error(err) = self
        else
        {
            return None;
        };

        return Some(err);
    }   

    /// Encodes the raw result to base64.
    pub 
    fn encode_base64(&self) -> String
    {
        match *self
        {
            Self::Data(ref raw_data) =>  
                general_purpose::STANDARD.encode(raw_data),
            Self::Error(ref err) => 
                general_purpose::STANDARD.encode(err.serv_err_value()),
            Self::Final(ref raw_data) => 
                general_purpose::STANDARD.encode(raw_data),
        }
    }

    /// Returns the ref [str] to raw output.
    pub 
    fn get_raw_output(&self) -> &str
    {
        match *self
        {
            Self::Data(ref raw_data) =>  raw_data.as_str(),
            Self::Error(ref err) => err.serv_err_value(),
            Self::Final(ref raw_data) => raw_data.as_str(),
        }
    }
}

/// A state encoder for Scram Client.
/// There is an internal state machine which automatically changes the
///  state. The timeout and other things must be implemented separatly.
///  The state from internal state machine is not exposed. If needed can
///  be maintained separatly.
#[derive(PartialEq, Eq, Clone, Debug)]
pub enum ScramResultClient
{
    /// A response was composed and stored in raw format
    Output( String ),

    /// Final stage, no more parsing is required, auth was successful.
    /// If auth failed an error will be thrown.
    Completed
}

impl fmt::Display for ScramResultClient
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result 
    {
        match self
        {
            Self::Output(s) => 
                write!(f, "{}", s),
            Self::Completed => 
                write!(f, "[COMPLETED]")
        }
    }
}

impl ScramResultClient
{
    /// Tells if the current status is [ScramResultClient::Output]
    pub 
    fn is_output(&self) -> bool
    {
        return 
            mem::discriminant(self) == mem::discriminant(&ScramResultClient::Output( String::new() ));
    }

    /// Tells is current status is [ScramResultClient::Completed].
    pub 
    fn is_final(&self) -> bool
    {
        return 
            mem::discriminant(self) == mem::discriminant(&ScramResultClient::Completed);
    }

    /// Unwraps the result which should be sent to server. The result is raw and
    ///  not encoded to base64!
    /// 
    /// # Returns
    /// 
    /// * [ScramResult]
    ///     - Ok() with payload (raw output)
    ///     - Err(e) [ScramErrorCode::AuthSeqCompleted] if called on state
    ///         [ScramResultClient::Completed].
    pub 
    fn unwrap_output(self) -> ScramResult<String>
    {
        match self
        {
            ScramResultClient::Output(output) => 
                return Ok(output),
            ScramResultClient::Completed => 
                scram_ierror!(ScramErrorCode::AuthSeqCompleted, "completed, nothing to extract"),
        }
    }

    /// Unwraps the result which should be sent to server and returns a ref
    ///  [str]. The result is raw and not encoded to base64!
    /// 
    /// # Returns
    /// 
    /// * [ScramResult]
    ///     - Ok() with payload (raw output)
    ///     - Err(e) [ScramErrorCode::AuthSeqCompleted] if called on state
    ///         [ScramResultClient::Completed].
    pub 
    fn get_output(&self) -> ScramResult<&str>
    {
        match self
        {
            ScramResultClient::Output(output) => 
                return Ok(output.as_str()),
            ScramResultClient::Completed => 
                scram_ierror!(ScramErrorCode::AuthSeqCompleted, "completed, nothing to extract"),
        }
    }

    /// Encodes the output to base64.
    /// 
    /// # Returns
    /// 
    /// * [ScramResult]
    ///     - Ok() with payload (encoded to base64 output)
    ///     - Err(e) [ScramErrorCode::AuthSeqCompleted] if called on state
    ///         [ScramResultClient::Completed].
    pub 
    fn encode_output_base64(&self) -> ScramResult<String>
    {
        match self
        {
            ScramResultClient::Output(output) => 
                return Ok(general_purpose::STANDARD.encode(output)),
            ScramResultClient::Completed => 
                scram_ierror!(ScramErrorCode::AuthSeqCompleted, "completed, nothing to extract"),
        }
    }
}

/// A SCRAM nonce initialization and customization.
/// Use implemented functions, don't use enum fields directly.
/// 
/// * '0' - a base64 encoded nonce.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScramNonce(String);

impl ScramNonce
{
    #[cfg(feature = "with_capi")]
    pub(crate) 
    fn empty() -> Self
    {
        return ScramNonce(String::new());
    }

    /// Initialize ScramNonce so the data will be autogenerated
    pub 
    fn none() -> ScramResult<Self>
    {
        let sn = 
            ScramNonce(
                general_purpose::STANDARD.encode(
                    ScramCommon::sc_random(ScramCommon::SCRAM_RAW_NONCE_LEN)?
                )
            );

        return Ok(sn);
    }

    /// Initialize ScramNonce with plain data.
    pub 
    fn plain(p: &[u8]) -> ScramResult<ScramNonce>
    {
        if p.len() > ScramCommon::SCRAM_RAW_NONCE_LEN
        {
            scram_ierror!(
                ScramErrorCode::InternalError,
                "nonce length is > {}, actual: '{}'", 
                ScramCommon::SCRAM_RAW_NONCE_LEN, p.len()
            );
        }

        let sn = 
            ScramNonce(general_purpose::STANDARD.encode(p));

        return Ok(sn);
    }

    /// Initialize ScramNonce with base64 encoded nonce. 
    pub 
    fn base64(b: &str) -> ScramResult<ScramNonce>
    {
        if b.len() == 0
        {
            scram_ierror!(ScramErrorCode::InternalError, "base64 nonce length is 0");
        }
        /*
        let _ = general_purpose::STANDARD
            .decode(b)
            .map_err(|e|
                scram_ierror_map!(ScramErrorCode::InternalError, "base64 decode: {}", e)
            )?;
        */
        let sn = ScramNonce(b.to_string());

        return Ok(sn);
    }

    /// Consumes the nonce from inner.
    pub 
    fn get_nonce(self) -> String
    {
        return self.0;
    }
}
