/*-
 * 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::ffi::CString;
#[cfg(not(feature = "std"))]
use alloc::ffi::CString;
use base64::{engine::general_purpose, Engine};

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



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


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

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

#[cfg(feature = "use_ring")]
use crate::{ScramSha1Ring, ScramSha256Ring, ScramSha512Ring};

use crate::
{
    ChannelBindType, SCRAM_TYPES, ScramAuthClient, ScramCbHelper, ScramClientDyn, ScramKey, ScramNonce, ScramResultClient, ScramSha1RustNative, ScramSha3_512RustNative, ScramSha256RustNative, ScramSha512RustNative, ScramTypeAlias, capi_scram_err_map, scram_sync::SyncScramClient
};

use super::
{
    common, 
    error::{CApiScramResult, CApiScramRuntimeError}, 
    nonce::CApiNonce, 
    CApiScramHasherProvider
};

const CLIENT_MARKER: u64 = 0xc0FFEEAA_12345678;

#[derive(Debug)]
pub struct CApiAuthClient
{
    username: String,
    authzid: Option<String>,
    password: String,
    key: Option<Box<ScramKey>>,
}

impl ScramAuthClient for CApiAuthClient
{
    fn get_username(&self) -> &str
    {
        return &self.username;
    }

    fn get_authzid(&self) -> Option<&str> 
    {
        return self.authzid.as_ref().map(|v| v.as_str());
    }

    fn get_password(&self) -> &str
    {
        return &self.password;
    }

    fn get_scram_keys(&self) -> Option<&ScramKey> 
    {
        return self.key.as_ref().map(|v| v.as_ref());
    }
}


impl CApiAuthClient
{ 
    fn new(
        plain_username: *const c_char,
        plain_authzid: *const c_char,
        plain_password: *const c_char,
        scram_key: *mut ScramKey,
    ) -> CApiScramResult<Self>
    {
        let p_username = 
            common::cstr_to_string(plain_username, "plain text username")?;

        let p_authzid = 
            if plain_authzid.is_null() == false
            {
                Some(common::cstr_to_string(plain_authzid, "plain text username")?)
            }
            else
            {
                None            
            };
            
        let p_password = 
            common::cstr_to_string(plain_password, "plain text password")?;

        let p_key = 
            if scram_key.is_null() == false
            {
                Some(unsafe{ Box::from_raw(scram_key) })
            }
            else
            {
                None
            };

        let ret =  
            Self
            {
                username: p_username,
                authzid: p_authzid,
                password: p_password,
                key: p_key
            };

        return Ok(ret);
    }
}

#[derive(Debug)]
struct CApiCbClient {}

impl ScramCbHelper for CApiCbClient
{
    
}

impl CApiCbClient
{
    fn new() -> Self
    {
        return Self{};
    }
}

#[repr(C)]
pub struct CApiScramClient
{
    dyn_client: Box<dyn ScramClientDyn>,
    marker: u64,
}

impl CApiScramClient
{
    pub 
    fn new(client: Box<dyn ScramClientDyn>) -> *mut Self
    {
        let api_client = 
            CApiScramClient
            {
                dyn_client: client,
                marker: CLIENT_MARKER,
            };

        return Box::into_raw(Box::new(api_client));
    }
}


#[unsafe(no_mangle)]
pub unsafe extern "C" 
fn capi_scram_client_init(
    hash_provider: CApiScramHasherProvider,
    scram_type: *const c_char,
    plain_username: *const c_char,
    plain_authzid: *const c_char,
    plain_password: *const c_char,
    scram_key: *mut ScramKey,
    client_nonce: *mut CApiNonce,
    ignore_exts: bool,
    client: *mut *mut CApiScramClient,
    error: *mut *mut CApiScramRuntimeError
) -> i32
{
    if client.is_null() == true
    {
        return -7;
    }
    else if hash_provider.validate() == false
    {
        return -1;
    }

    let scram_type_res  =
        match common::cstr_to_string(scram_type, "scram_type is not valid")
        {
            Ok(r) => r,
            Err(e) =>
            {
                unsafe 
                { 
                    if let Some(err) = error.as_mut()
                    {
                        *err = e.into_capi();
                    }
                }

                return 1;
            }
        };

    let scramtype = 
        match SCRAM_TYPES.get_scramtype(scram_type_res).map_err(|e|capi_scram_err_map!(e))
        {
            Ok(r) => r,
            Err(e) =>
            {
                unsafe 
                { 
                    if let Some(err) = error.as_mut()
                    {
                        *err = e.into_capi();
                    }
                }

                return 1;
            }
        };

    let nonce = 
        if client_nonce.is_null() == true
        {
            match ScramNonce::none().map_err(|e| capi_scram_err_map!(e))
            {
                Ok(r) => r,
                Err(e) =>
                {
                    unsafe 
                    { 
                        if let Some(err) = error.as_mut()
                        {
                            *err = e.into_capi();
                        }
                    }

                    return 1;
                }
            }
        }
        else
        {
            unsafe 
            { 
                match client_nonce.as_mut().unwrap().take()
                {
                    Ok(r) => r,
                    Err(e) =>
                    {
                        if let Some(err) = error.as_mut()
                        {
                            *err = e.into_capi();
                        }

                        return 1;
                    }
                }
            }
        };

    let auth = 
        match CApiAuthClient::new(plain_username, plain_authzid, plain_password, scram_key)
        {
            Ok(r) => r,
            Err(e) =>
            {
                unsafe 
                { 
                    if let Some(err) = error.as_mut()
                    {
                        *err = e.into_capi();
                    }
                }

                return 1;
            }
        };

    let clientcb = CApiCbClient::new();
    
    let res = 
        match (hash_provider, scramtype.scram_alias)
        {
            #[cfg(not(feature = "exclude_sha1"))]
            (CApiScramHasherProvider::RustNative, ScramTypeAlias::Sha1) =>
            {
                SyncScramClient
                    ::<ScramSha1RustNative, CApiAuthClient, CApiCbClient>
                    ::new(auth, nonce, ChannelBindType::None, clientcb, ignore_exts)
                        .map_err(|e|
                            capi_scram_err_map!(e)
                        )
                        .map(|c| c.make_dyn())
            },
            (CApiScramHasherProvider::RustNative, ScramTypeAlias::Sha256 | ScramTypeAlias::Sha256Plus) => 
            {
                SyncScramClient
                    ::<ScramSha256RustNative, CApiAuthClient, CApiCbClient>
                    ::new(auth, nonce, ChannelBindType::None, clientcb, ignore_exts)
                        .map_err(|e|
                            capi_scram_err_map!(e)
                        )
                        .map(|c| c.make_dyn())
            },
            (CApiScramHasherProvider::RustNative, ScramTypeAlias::Sha512 | ScramTypeAlias::Sha512Plus) =>
            {
                SyncScramClient
                    ::<ScramSha512RustNative, CApiAuthClient, CApiCbClient>
                    ::new(auth, nonce, ChannelBindType::None, clientcb, ignore_exts)
                        .map_err(|e|
                            capi_scram_err_map!(e)
                        )
                        .map(|c| c.make_dyn())
            },
            (CApiScramHasherProvider::RustNative, ScramTypeAlias::Sha3_512 | ScramTypeAlias::Sha3_512Plus) =>
            {
                SyncScramClient
                    ::<ScramSha3_512RustNative, CApiAuthClient, CApiCbClient>
                    ::new(auth, nonce, ChannelBindType::None, clientcb, ignore_exts)
                        .map_err(|e|
                            capi_scram_err_map!(e)
                        )
                        .map(|c| c.make_dyn())
            },

            #[cfg(all(feature = "use_ring", not(feature = "exclude_sha1")))]
            (CApiScramHasherProvider::RustRing, ScramTypeAlias::Sha1) =>
            {
                SyncScramClient
                    ::<ScramSha1Ring, CApiAuthClient, CApiCbClient>
                    ::new(auth, nonce, ChannelBindType::None, clientcb, ignore_exts)
                        .map_err(|e|
                            capi_scram_err_map!(e)
                        )
                        .map(|c| c.make_dyn())
            },

            #[cfg(all(not(feature = "use_ring"), not(feature = "exclude_sha1")))]
            (CApiScramHasherProvider::RustRing, ScramTypeAlias::Sha1) =>
            {
                crate::capi_scram_err_res!(crate::ScramErrorCode::FeatureNotSupported, crate::ScramServerError::None, "Sha1Ring not available")
            },

            #[cfg(feature = "use_ring")]
            (CApiScramHasherProvider::RustRing, ScramTypeAlias::Sha256 | ScramTypeAlias::Sha256Plus) =>
            {
                SyncScramClient
                    ::<ScramSha256Ring, CApiAuthClient, CApiCbClient>
                    ::new(auth, nonce, ChannelBindType::None, clientcb, ignore_exts)
                        .map_err(|e|
                            capi_scram_err_map!(e)
                        )
                        .map(|c| c.make_dyn())
            },

            #[cfg(not(feature = "use_ring"))]
            (CApiScramHasherProvider::RustRing, ScramTypeAlias::Sha256 | ScramTypeAlias::Sha256Plus) =>
            {
                crate::capi_scram_err_res!(crate::ScramErrorCode::FeatureNotSupported, crate::ScramServerError::None, "Sha256Ring not available")
            },

            #[cfg(feature = "use_ring")]
            (CApiScramHasherProvider::RustRing, ScramTypeAlias::Sha512 | ScramTypeAlias::Sha512Plus) =>
            {
                SyncScramClient
                    ::<ScramSha512Ring, CApiAuthClient, CApiCbClient>
                    ::new(auth, nonce, ChannelBindType::None, clientcb, ignore_exts)
                        .map_err(|e|
                            capi_scram_err_map!(e)
                        )
                        .map(|c| c.make_dyn())
            },

            #[cfg(feature = "use_ring")]
            (CApiScramHasherProvider::RustRing, ScramTypeAlias::Sha3_512 | ScramTypeAlias::Sha3_512Plus) =>
            {
                crate::capi_scram_err_res!(crate::ScramErrorCode::FeatureNotSupported, crate::ScramServerError::None, "RING does not support sha3")
            }

            #[cfg(not(feature = "use_ring"))]
            (CApiScramHasherProvider::RustRing, ScramTypeAlias::Sha512 | ScramTypeAlias::Sha512Plus) =>
            {
                crate::capi_scram_err_res!(crate::ScramErrorCode::FeatureNotSupported, crate::ScramServerError::None, "Sha512Ring not available")
            },

            #[cfg(not(feature = "use_ring"))]
            (CApiScramHasherProvider::RustRing, ScramTypeAlias::Sha3_512 | ScramTypeAlias::Sha3_512Plus) =>
            {
                crate::capi_scram_err_res!(crate::ScramErrorCode::FeatureNotSupported, crate::ScramServerError::None, "RING does not support sha3")
            }
        };

    if res.is_ok() == true
    {
        unsafe { *client.as_mut().unwrap() = CApiScramClient::new(res.ok().unwrap()) };
        return 0;
    }
    else
    {
        unsafe 
        { 
            if let Some(err) = error.as_mut()
            {
                *err = res.err().unwrap().into_capi();
            }
        }

        return 1;
    };
}   




#[unsafe(no_mangle)]
pub unsafe extern "C" 
fn capi_scram_client_free(client: *mut CApiScramClient)
{
    if client.is_null() == true
    {
        return;
    }

    let marker = unsafe { client.as_ref().unwrap().marker };
    if marker != CLIENT_MARKER
    {
        panic!("Assertion trap: invalid client marker: {:X} {:X}", marker, CLIENT_MARKER);
    }

    let boxed_client = unsafe { Box::from_raw(client)};


    drop(boxed_client);

    return;
}

#[derive(PartialEq, Eq)]
pub struct CApiScramResultClient
{
    res: ScramResultClient,
    plain: Option<CString>,
    base64: Option<CString>,
}

impl From<ScramResultClient> for CApiScramResultClient
{
    fn from(value: ScramResultClient) -> Self 
    {
        return Self{ res: value, plain: None, base64: None };
    }
}

#[unsafe(no_mangle)]
pub unsafe extern "C" 
fn capi_scram_client_result_is_final(msg_res: *mut CApiScramResultClient) -> bool
{
    if msg_res.is_null() == true
    {
        return false;
    }

    return ScramResultClient::Completed == unsafe { msg_res.as_ref().unwrap()}.res;
}

#[unsafe(no_mangle)]
pub unsafe extern "C" 
fn capi_scram_client_result_plain(msg_res: *mut CApiScramResultClient, msg_raw_out: *mut *const c_char) -> i32
{
    if msg_res.is_null() == true
    {
        return -1;
    }
    else if msg_raw_out.is_null() == true
    {
        return -1;
    }

    let this = unsafe { msg_res.as_mut().unwrap()};

    if this.res.is_output() == false
    {
        return 1;
    };
    
    

    let plain_msg = 
        if let Some(plain) = this.plain.as_ref()
        {
            plain.as_ptr()
        }
        else
        {
            let plain = CString::new(this.res.get_output().unwrap().as_bytes()).unwrap();
            let ret_plain = plain.as_ptr();
            this.plain.replace(plain);

            ret_plain
        };

    unsafe { *(msg_raw_out.as_mut().unwrap()) = plain_msg };

    return 0;
}

#[unsafe(no_mangle)]
pub unsafe extern "C" 
fn capi_scram_client_result_base64(msg_res: *mut CApiScramResultClient, msg_base64_out: *mut *const c_char) -> i32
{
    if msg_res.is_null() == true
    {
        return -1;
    }
    else if msg_base64_out.is_null() == true
    {
        return -2;
    }

    let this = unsafe { msg_res.as_mut().unwrap() };

    if this.res.is_output() == false
    {
        return 1;
    };

    let base_msg = 
        if let Some(base) = this.base64.as_ref()
        {
            base.as_ptr()
        }
        else
        {
            let base = 
                CString::new(
                    general_purpose::STANDARD.encode(this.res.get_output().unwrap().as_bytes())
                )
                .unwrap();

            let ret_base = base.as_ptr();
            this.base64.replace(base);

            ret_base
        };

    unsafe { *msg_base64_out = base_msg };
    
    return 0;
}

#[unsafe(no_mangle)]
pub unsafe extern "C" 
fn capi_scram_client_result_free(msg_res: *mut CApiScramResultClient)
{
    if msg_res.is_null() == true
    {
        return;
    }

    drop(unsafe { Box::from_raw(msg_res) });

    return;
}

#[unsafe(no_mangle)]
pub unsafe extern "C" 
fn capi_scram_client_init_msg(client: *mut CApiScramClient, init_msg: *mut *mut CApiScramResultClient) -> i32
{
    if client.is_null() == true
    {
        return -1;
    }
    else if init_msg.is_null() == true
    {
        return -1;
    }

    let msg = 
        unsafe { client.as_mut().unwrap().dyn_client.init_client_msg()};

    unsafe 
    { 
        *(init_msg.as_mut().unwrap()) = Box::into_raw(Box::new(CApiScramResultClient::from(msg)));
    }

    return 0;
}

#[unsafe(no_mangle)]
pub unsafe extern "C" 
fn capi_scram_client_parse_resp_plain(client: *mut CApiScramClient, resp_plain: *const c_char, 
    res_out: *mut *mut CApiScramResultClient, error_out: *mut *mut CApiScramRuntimeError) -> i32
{
    if client.is_null() == true
    {
        return -1;
    }
    else if res_out.is_null() == true
    {
        return -3;
    }

    let resp_res = 
        match common::cstr_to_string(resp_plain, "resp_plain is not valid")
        {
            Ok(r) => r,
            Err(e) => 
            {
                unsafe 
                { 
                    if let Some(err) = error_out.as_mut()
                    {
                        *err = e.into_capi();
                    }
                }

                return 1;
            }
        };
            

    let msg = 
        unsafe 
        { 
            client
                .as_mut()
                .unwrap()
                .dyn_client
                .parse_response(&resp_res)
                .map_err(|e|
                    capi_scram_err_map!(e)
                )
                .map(|res| 
                    Box::into_raw(Box::new(CApiScramResultClient::from(res)))
                )
        };

    if msg.is_ok() == true
    {
        unsafe { *res_out.as_mut().unwrap() = msg.ok().unwrap() };
        
        return 0;
    }
    else
    {
        unsafe 
        { 
            if let Some(err) = error_out.as_mut()
            {
                *err = msg.err().unwrap().into_capi();
            }
        }

        return 1;
    }
}


#[unsafe(no_mangle)]
pub unsafe extern "C" 
fn capi_scram_client_parse_resp_base64(client: *mut CApiScramClient, resp_b64: *const u8, resp_len: usize,
    res_out: *mut *mut CApiScramResultClient, error_out: *mut *mut CApiScramRuntimeError) -> i32
{
    if client.is_null() == true
    {
        return -1;
    }
    else if res_out.is_null() == true
    {
        return -4;
    }


    let resp = 
        match common::carr_to_slice(resp_b64, resp_len, "resp is not valid")
        {
            Ok(r) => r,
            Err(e) => 
            {
                unsafe 
                { 
                    if let Some(err) = error_out.as_mut()
                    {
                        *err = e.into_capi();
                    }
                }

                return 1;
            }
        };      

    let msg = 
        unsafe 
        { 
            client
                .as_mut()
                .unwrap()
                .dyn_client
                .parse_response_base64(resp)
                .map_err(|e: crate::ScramRuntimeError|
                    capi_scram_err_map!(e)
                )
                .map(|res| Box::into_raw(Box::new(CApiScramResultClient::from(res))))
        };

    if msg.is_ok() == true
    {
        unsafe { *res_out.as_mut().unwrap() = msg.ok().unwrap() };
        
        return 0;
    }
    else
    {
        unsafe 
        { 
            if let Some(err) = error_out.as_mut()
            {
                *err = msg.err().unwrap().into_capi();
            }
        }

        return 1;
    }
}

