// Copyright 2023 Salesforce, Inc. All rights reserved.
use std::fmt;
use std::ptr::null_mut;

use crate::api::status::Status;
use crate::api::types::Bytes;

const LOG_FUNCTION_NAME: &str = "flex_log";
const CREATE_SERVICE_FUNCTION_NAME: &str = "flex_create_service";
const GET_METADATA_FUNCTION_NAME: &str = "flex_get_policy_metadata";
const GET_CONFIGURATION_FUNCTION_NAME: &str = "flex_get_policy_configuration";
const GET_ENV_FUNCTION_NAME: &str = "flex_get_env";

// Due to a linker error in Windows (https://github.com/rust-lang/rust/issues/86125) that fails when
// importing a crate that has external symbols, even when they are not being used, we need to compile
// any extern function just for wasm32 targets and create a non-extern stub of the function for the
// other targets in order to be able to run cargo test (which does not and can not use wasm32 as
// compilation target)
#[cfg(target_arch = "wasm32")]
extern "C" {
    fn proxy_call_foreign_function(
        name_data: *const u8,
        name_size: usize,
        args_data: *const u8,
        args_size: usize,
        return_data: *mut *mut u8,
        return_size: *mut usize,
    ) -> Status;
}

#[cfg(not(target_arch = "wasm32"))]
fn proxy_call_foreign_function(
    _name_data: *const u8,
    _name_size: usize,
    _args_data: *const u8,
    _args_size: usize,
    _return_data: *mut *mut u8,
    _return_size: *mut usize,
) -> Status {
    unimplemented!("Not implemented for current target")
}

fn raw_flex_call(name: &str, args: &[&str]) -> Result<Option<Bytes>, Status> {
    let mut return_data: *mut u8 = null_mut();
    let mut return_size: usize = 0;

    let arg = to_json_array(args)?;

    unsafe {
        match proxy_call_foreign_function(
            name.as_ptr(),
            name.len(),
            arg.as_ptr(),
            arg.len(),
            &mut return_data,
            &mut return_size,
        ) {
            Status::Ok => Ok((!return_data.is_null())
                .then(|| Bytes::from_raw_parts(return_data, return_size, return_size))),
            Status::NotFound => Ok(None),
            status => Err(status),
        }
    }
}

fn to_json_array(args: &[&str]) -> Result<String, Status> {
    serde_json::to_string(args).map_err(|_| Status::ParseFailure)
}

#[derive(Debug)]
pub enum FlexLogLevel {
    Trace,
    Debug,
    Info,
    Warn,
    Error,
}

impl fmt::Display for FlexLogLevel {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.as_str())
    }
}

impl FlexLogLevel {
    pub fn as_str(&self) -> &'static str {
        match self {
            FlexLogLevel::Trace => "trace",
            FlexLogLevel::Debug => "debug",
            FlexLogLevel::Info => "info",
            FlexLogLevel::Warn => "warn",
            FlexLogLevel::Error => "error",
        }
    }
}

/// Logging ABI functions
pub fn log(level: FlexLogLevel, msg: &str) {
    raw_flex_call(LOG_FUNCTION_NAME, &[level.as_str(), msg]).unwrap_or_default();
}

/// Registering services ABI functions
pub fn service_create(name: &str, ns: &str, uri: &str) -> Result<(), Status> {
    raw_flex_call(CREATE_SERVICE_FUNCTION_NAME, &[name, ns, uri]).map(|_| ())
}

/// Environment ABI functions
#[allow(dead_code)]
pub fn get_env(name: &str) -> Option<String> {
    match raw_flex_call(GET_ENV_FUNCTION_NAME, &[name]) {
        Ok(Some(v)) => String::from_utf8(v).ok(),
        Ok(None) | Err(_) => None,
    }
}

/// Configuration ABI functions
pub fn get_configuration() -> Result<Option<Bytes>, Status> {
    raw_flex_call(GET_CONFIGURATION_FUNCTION_NAME, &[])
}

/// Metadata ABI functions
pub fn get_metadata() -> Result<Option<Bytes>, Status> {
    raw_flex_call(GET_METADATA_FUNCTION_NAME, &[])
}
