/**
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0.
 */

#include "credentials.h"
#include "crt.h"
#include "http_connection_manager.h"
#include "java_class_ids.h"

#include <http_proxy_options.h>
#include <jni.h>
#include <string.h>

#include <aws/auth/credentials.h>
#include <aws/common/clock.h>
#include <aws/common/string.h>
#include <aws/http/connection.h>
#include <aws/http/proxy.h>
#include <aws/io/tls_channel_handler.h>

/* on 32-bit platforms, casting pointers to longs throws a warning we don't need */
#if UINTPTR_MAX == 0xffffffff
#    if defined(_MSC_VER)
#        pragma warning(push)
#        pragma warning(disable : 4305) /* 'type cast': truncation from 'jlong' to 'jni_tls_ctx_options *' */
#    else
#        pragma GCC diagnostic push
#        pragma GCC diagnostic ignored "-Wpointer-to-int-cast"
#        pragma GCC diagnostic ignored "-Wint-to-pointer-cast"
#    endif
#endif

struct aws_credentials_provider_callback_data {
    JavaVM *jvm;
    struct aws_credentials_provider *provider;
    jweak java_crt_credentials_provider;

    jobject jni_delegate_credential_handler;
};

static void s_callback_data_clean_up(
    JNIEnv *env,
    struct aws_allocator *allocator,
    struct aws_credentials_provider_callback_data *callback_data) {

    (*env)->DeleteWeakGlobalRef(env, callback_data->java_crt_credentials_provider);
    if (callback_data->jni_delegate_credential_handler != NULL) {
        (*env)->DeleteGlobalRef(env, callback_data->jni_delegate_credential_handler);
    }

    aws_mem_release(allocator, callback_data);
}

static void s_on_shutdown_complete(void *user_data) {
    struct aws_credentials_provider_callback_data *callback_data = user_data;

    AWS_LOGF_DEBUG(AWS_LS_AUTH_CREDENTIALS_PROVIDER, "Credentials providers shutdown complete");

    // Tell the Java credentials providers that shutdown is done.  This lets it release its references.
    /********** JNI ENV ACQUIRE **********/
    JNIEnv *env = aws_jni_acquire_thread_env(callback_data->jvm);
    if (env == NULL) {
        /* If we can't get an environment, then the JVM is probably shutting down.  Don't crash. */
        return;
    }

    jobject java_crt_credentials_provider = (*env)->NewLocalRef(env, callback_data->java_crt_credentials_provider);
    if (java_crt_credentials_provider != NULL) {
        (*env)->CallVoidMethod(
            env, java_crt_credentials_provider, credentials_provider_properties.on_shutdown_complete_method_id);

        (*env)->DeleteLocalRef(env, java_crt_credentials_provider);
        AWS_FATAL_ASSERT(!aws_jni_check_and_clear_exception(env));
    }

    struct aws_allocator *allocator = aws_jni_get_allocator();
    // We're done with this callback data, clean it up.

    JavaVM *jvm = callback_data->jvm;
    s_callback_data_clean_up(env, allocator, callback_data);

    aws_jni_release_thread_env(jvm, env);
    /********** JNI ENV RELEASE **********/
}

JNIEXPORT jlong JNICALL
    Java_software_amazon_awssdk_crt_auth_credentials_StaticCredentialsProvider_staticCredentialsProviderNew(
        JNIEnv *env,
        jclass jni_class,
        jobject java_crt_credentials_provider,
        jbyteArray access_key_id,
        jbyteArray secret_access_key,
        jbyteArray session_token) {

    (void)jni_class;
    aws_cache_jni_ids(env);

    struct aws_allocator *allocator = aws_jni_get_allocator();

    struct aws_credentials_provider_callback_data *callback_data =
        aws_mem_calloc(allocator, 1, sizeof(struct aws_credentials_provider_callback_data));
    callback_data->java_crt_credentials_provider = (*env)->NewWeakGlobalRef(env, java_crt_credentials_provider);

    jint jvmresult = (*env)->GetJavaVM(env, &callback_data->jvm);
    AWS_FATAL_ASSERT(jvmresult == 0);

    struct aws_credentials_provider_static_options options;
    AWS_ZERO_STRUCT(options);
    options.access_key_id = aws_jni_byte_cursor_from_jbyteArray_acquire(env, access_key_id);
    options.secret_access_key = aws_jni_byte_cursor_from_jbyteArray_acquire(env, secret_access_key);
    if (session_token) {
        options.session_token = aws_jni_byte_cursor_from_jbyteArray_acquire(env, session_token);
    }
    options.shutdown_options.shutdown_callback = s_on_shutdown_complete;
    options.shutdown_options.shutdown_user_data = callback_data;

    struct aws_credentials_provider *provider = aws_credentials_provider_new_static(allocator, &options);
    if (provider == NULL) {
        s_callback_data_clean_up(env, allocator, callback_data);
        aws_jni_throw_runtime_exception(env, "Failed to create static credentials provider");
    } else {
        callback_data->provider = provider;
    }

    aws_jni_byte_cursor_from_jbyteArray_release(env, access_key_id, options.access_key_id);
    aws_jni_byte_cursor_from_jbyteArray_release(env, secret_access_key, options.secret_access_key);

    if (session_token) {
        aws_jni_byte_cursor_from_jbyteArray_release(env, session_token, options.session_token);
    }

    return (jlong)provider;
}

JNIEXPORT jlong JNICALL
    Java_software_amazon_awssdk_crt_auth_credentials_DefaultChainCredentialsProvider_defaultChainCredentialsProviderNew(
        JNIEnv *env,
        jclass jni_class,
        jobject java_crt_credentials_provider,
        jlong bootstrapHandle) {

    (void)jni_class;
    (void)env;
    aws_cache_jni_ids(env);

    struct aws_allocator *allocator = aws_jni_get_allocator();
    struct aws_credentials_provider_callback_data *callback_data =
        aws_mem_calloc(allocator, 1, sizeof(struct aws_credentials_provider_callback_data));
    callback_data->java_crt_credentials_provider = (*env)->NewWeakGlobalRef(env, java_crt_credentials_provider);

    jint jvmresult = (*env)->GetJavaVM(env, &callback_data->jvm);
    AWS_FATAL_ASSERT(jvmresult == 0);

    struct aws_credentials_provider_chain_default_options options;
    AWS_ZERO_STRUCT(options);
    options.bootstrap = (struct aws_client_bootstrap *)bootstrapHandle;
    options.shutdown_options.shutdown_callback = s_on_shutdown_complete;
    options.shutdown_options.shutdown_user_data = callback_data;

    struct aws_credentials_provider *provider = aws_credentials_provider_new_chain_default(allocator, &options);
    if (provider == NULL) {
        s_callback_data_clean_up(env, allocator, callback_data);
        aws_jni_throw_runtime_exception(env, "Failed to create default credentials provider chain");
    } else {
        callback_data->provider = provider;
    }

    return (jlong)provider;
}

JNIEXPORT jlong JNICALL
    Java_software_amazon_awssdk_crt_auth_credentials_ProfileCredentialsProvider_profileCredentialsProviderNew(
        JNIEnv *env,
        jclass jni_class,
        jobject java_crt_credentials_provider,
        jlong bootstrapHandle,
        jlong tls_context_handle,
        jbyteArray profile_name_override,
        jbyteArray config_file_name_override,
        jbyteArray credentials_file_name_override) {

    (void)jni_class;
    (void)env;
    aws_cache_jni_ids(env);

    struct aws_allocator *allocator = aws_jni_get_allocator();
    struct aws_credentials_provider_callback_data *callback_data =
        aws_mem_calloc(allocator, 1, sizeof(struct aws_credentials_provider_callback_data));
    callback_data->java_crt_credentials_provider = (*env)->NewWeakGlobalRef(env, java_crt_credentials_provider);

    jint jvmresult = (*env)->GetJavaVM(env, &callback_data->jvm);
    AWS_FATAL_ASSERT(jvmresult == 0);

    struct aws_credentials_provider_profile_options options;
    AWS_ZERO_STRUCT(options);
    options.bootstrap = (struct aws_client_bootstrap *)bootstrapHandle;
    options.shutdown_options.shutdown_callback = s_on_shutdown_complete;
    options.shutdown_options.shutdown_user_data = callback_data;
    options.tls_ctx = (struct aws_tls_ctx *)tls_context_handle;

    if (profile_name_override) {
        options.profile_name_override = aws_jni_byte_cursor_from_jbyteArray_acquire(env, profile_name_override);
    }

    if (config_file_name_override) {
        options.config_file_name_override = aws_jni_byte_cursor_from_jbyteArray_acquire(env, config_file_name_override);
    }

    if (credentials_file_name_override) {
        options.credentials_file_name_override =
            aws_jni_byte_cursor_from_jbyteArray_acquire(env, credentials_file_name_override);
    }

    struct aws_credentials_provider *provider = aws_credentials_provider_new_profile(allocator, &options);
    if (provider == NULL) {
        s_callback_data_clean_up(env, allocator, callback_data);
        aws_jni_throw_runtime_exception(env, "Failed to create profile credentials provider");
    } else {
        callback_data->provider = provider;
    }

    if (profile_name_override) {
        aws_jni_byte_cursor_from_jbyteArray_release(env, profile_name_override, options.profile_name_override);
    }

    if (config_file_name_override) {
        aws_jni_byte_cursor_from_jbyteArray_release(env, config_file_name_override, options.config_file_name_override);
    }

    if (credentials_file_name_override) {
        aws_jni_byte_cursor_from_jbyteArray_release(
            env, credentials_file_name_override, options.credentials_file_name_override);
    }

    return (jlong)provider;
}

JNIEXPORT jlong JNICALL
    Java_software_amazon_awssdk_crt_auth_credentials_EcsCredentialsProvider_ecsCredentialsProviderNew(
        JNIEnv *env,
        jclass jni_class,
        jobject java_crt_credentials_provider,
        jlong bootstrapHandle,
        jlong tls_context_handle,
        jbyteArray host,
        jbyteArray path_and_query,
        jbyteArray auth_token) {

    (void)jni_class;
    (void)env;
    aws_cache_jni_ids(env);

    struct aws_allocator *allocator = aws_jni_get_allocator();
    struct aws_credentials_provider_callback_data *callback_data =
        aws_mem_calloc(allocator, 1, sizeof(struct aws_credentials_provider_callback_data));
    callback_data->java_crt_credentials_provider = (*env)->NewWeakGlobalRef(env, java_crt_credentials_provider);

    jint jvmresult = (*env)->GetJavaVM(env, &callback_data->jvm);
    AWS_FATAL_ASSERT(jvmresult == 0);

    struct aws_credentials_provider_ecs_options options;
    AWS_ZERO_STRUCT(options);
    options.bootstrap = (struct aws_client_bootstrap *)bootstrapHandle;
    options.shutdown_options.shutdown_callback = s_on_shutdown_complete;
    options.shutdown_options.shutdown_user_data = callback_data;
    options.tls_ctx = (struct aws_tls_ctx *)tls_context_handle;

    if (host) {
        options.host = aws_jni_byte_cursor_from_jbyteArray_acquire(env, host);
    }

    if (path_and_query) {
        options.path_and_query = aws_jni_byte_cursor_from_jbyteArray_acquire(env, path_and_query);
    }

    if (auth_token) {
        options.auth_token = aws_jni_byte_cursor_from_jbyteArray_acquire(env, auth_token);
    }

    struct aws_credentials_provider *provider = aws_credentials_provider_new_ecs(allocator, &options);
    if (provider == NULL) {
        s_callback_data_clean_up(env, allocator, callback_data);
        aws_jni_throw_runtime_exception(env, "Failed to create ECS credentials provider");
    } else {
        callback_data->provider = provider;
    }

    if (host) {
        aws_jni_byte_cursor_from_jbyteArray_release(env, host, options.host);
    }

    if (path_and_query) {
        aws_jni_byte_cursor_from_jbyteArray_release(env, path_and_query, options.path_and_query);
    }

    if (auth_token) {
        aws_jni_byte_cursor_from_jbyteArray_release(env, auth_token, options.auth_token);
    }

    return (jlong)provider;
}

JNIEXPORT jlong JNICALL
    Java_software_amazon_awssdk_crt_auth_credentials_StsCredentialsProvider_stsCredentialsProviderNew(
        JNIEnv *env,
        jclass jni_class,
        jobject java_crt_credentials_provider,
        jlong bootstrapHandle,
        jlong tls_context_handle,
        jlong creds_provider,
        jbyteArray role_arn,
        jbyteArray session_name,
        jlong duration_seconds) {

    (void)jni_class;
    (void)env;
    aws_cache_jni_ids(env);

    struct aws_allocator *allocator = aws_jni_get_allocator();
    struct aws_credentials_provider_callback_data *callback_data =
        aws_mem_calloc(allocator, 1, sizeof(struct aws_credentials_provider_callback_data));
    callback_data->java_crt_credentials_provider = (*env)->NewWeakGlobalRef(env, java_crt_credentials_provider);

    jint jvmresult = (*env)->GetJavaVM(env, &callback_data->jvm);
    AWS_FATAL_ASSERT(jvmresult == 0);

    struct aws_credentials_provider_sts_options options;
    AWS_ZERO_STRUCT(options);
    options.bootstrap = (struct aws_client_bootstrap *)bootstrapHandle;
    options.shutdown_options.shutdown_callback = s_on_shutdown_complete;
    options.shutdown_options.shutdown_user_data = callback_data;
    options.tls_ctx = (struct aws_tls_ctx *)tls_context_handle;

    options.creds_provider = (struct aws_credentials_provider *)creds_provider;

    if (role_arn) {
        options.role_arn = aws_jni_byte_cursor_from_jbyteArray_acquire(env, role_arn);
    }

    if (session_name) {
        options.session_name = aws_jni_byte_cursor_from_jbyteArray_acquire(env, session_name);
    }

    options.duration_seconds =
        (uint16_t)aws_timestamp_convert(duration_seconds, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_SECS, NULL);

    struct aws_credentials_provider *provider = aws_credentials_provider_new_sts(allocator, &options);
    if (provider == NULL) {
        s_callback_data_clean_up(env, allocator, callback_data);
        aws_jni_throw_runtime_exception(env, "Failed to create STS credentials provider");
    } else {
        callback_data->provider = provider;
    }

    if (role_arn) {
        aws_jni_byte_cursor_from_jbyteArray_release(env, role_arn, options.role_arn);
    }

    if (session_name) {
        aws_jni_byte_cursor_from_jbyteArray_release(env, session_name, options.session_name);
    }

    return (jlong)provider;
}

JNIEXPORT jlong JNICALL
    Java_software_amazon_awssdk_crt_auth_credentials_StsWebIdentityCredentialsProvider_stsWebIdentityCredentialsProviderNew(
        JNIEnv *env,
        jclass jni_class,
        jobject java_crt_credentials_provider,
        jlong bootstrapHandle,
        jlong tls_context_handle) {

    (void)jni_class;
    (void)env;
    aws_cache_jni_ids(env);

    struct aws_allocator *allocator = aws_jni_get_allocator();
    struct aws_credentials_provider_callback_data *callback_data =
        aws_mem_calloc(allocator, 1, sizeof(struct aws_credentials_provider_callback_data));
    callback_data->java_crt_credentials_provider = (*env)->NewWeakGlobalRef(env, java_crt_credentials_provider);

    jint jvmresult = (*env)->GetJavaVM(env, &callback_data->jvm);
    AWS_FATAL_ASSERT(jvmresult == 0);

    struct aws_credentials_provider_sts_web_identity_options options;
    AWS_ZERO_STRUCT(options);
    options.bootstrap = (struct aws_client_bootstrap *)bootstrapHandle;
    options.shutdown_options.shutdown_callback = s_on_shutdown_complete;
    options.shutdown_options.shutdown_user_data = callback_data;
    options.tls_ctx = (struct aws_tls_ctx *)tls_context_handle;

    struct aws_credentials_provider *provider = aws_credentials_provider_new_sts_web_identity(allocator, &options);
    if (provider == NULL) {
        s_callback_data_clean_up(env, allocator, callback_data);
        aws_jni_throw_runtime_exception(env, "Failed to create STS web identity credentials provider");
    } else {
        callback_data->provider = provider;
    }

    return (jlong)provider;
}

JNIEXPORT jlong JNICALL
    Java_software_amazon_awssdk_crt_auth_credentials_X509CredentialsProvider_x509CredentialsProviderNew(
        JNIEnv *env,
        jclass jni_class,
        jobject java_crt_credentials_provider,
        jlong bootstrap_handle,
        jlong tls_context_handle,
        jbyteArray thing_name,
        jbyteArray role_alias,
        jbyteArray endpoint,
        jint proxy_connection_type,
        jbyteArray jni_proxy_host,
        jint jni_proxy_port,
        jlong jni_proxy_tls_context,
        jint jni_proxy_authorization_type,
        jbyteArray jni_proxy_authorization_username,
        jbyteArray jni_proxy_authorization_password) {

    (void)jni_class;
    (void)env;
    aws_cache_jni_ids(env);

    struct aws_allocator *allocator = aws_jni_get_allocator();
    struct aws_credentials_provider_callback_data *callback_data =
        aws_mem_calloc(allocator, 1, sizeof(struct aws_credentials_provider_callback_data));
    callback_data->java_crt_credentials_provider = (*env)->NewWeakGlobalRef(env, java_crt_credentials_provider);

    jint jvmresult = (*env)->GetJavaVM(env, &callback_data->jvm);
    AWS_FATAL_ASSERT(jvmresult == 0);

    struct aws_tls_connection_options tls_connection_options;
    AWS_ZERO_STRUCT(tls_connection_options);
    aws_tls_connection_options_init_from_ctx(&tls_connection_options, (struct aws_tls_ctx *)tls_context_handle);

    struct aws_credentials_provider_x509_options options;
    AWS_ZERO_STRUCT(options);
    options.bootstrap = (struct aws_client_bootstrap *)bootstrap_handle;
    options.shutdown_options.shutdown_callback = s_on_shutdown_complete;
    options.shutdown_options.shutdown_user_data = callback_data;
    options.tls_connection_options = &tls_connection_options;
    options.thing_name = aws_jni_byte_cursor_from_jbyteArray_acquire(env, thing_name);
    options.role_alias = aws_jni_byte_cursor_from_jbyteArray_acquire(env, role_alias);
    options.endpoint = aws_jni_byte_cursor_from_jbyteArray_acquire(env, endpoint);

    struct aws_tls_connection_options proxy_tls_connection_options;
    AWS_ZERO_STRUCT(proxy_tls_connection_options);
    struct aws_http_proxy_options proxy_options;
    AWS_ZERO_STRUCT(proxy_options);

    aws_http_proxy_options_jni_init(
        env,
        &proxy_options,
        proxy_connection_type,
        &proxy_tls_connection_options,
        jni_proxy_host,
        jni_proxy_port,
        jni_proxy_authorization_username,
        jni_proxy_authorization_password,
        jni_proxy_authorization_type,
        (struct aws_tls_ctx *)jni_proxy_tls_context);

    if (jni_proxy_host != NULL) {
        options.proxy_options = &proxy_options;
    }

    struct aws_credentials_provider *provider = aws_credentials_provider_new_x509(allocator, &options);
    if (provider == NULL) {
        s_callback_data_clean_up(env, allocator, callback_data);
        aws_jni_throw_runtime_exception(env, "Failed to create X509 credentials provider");
    } else {
        callback_data->provider = provider;
    }

    aws_jni_byte_cursor_from_jbyteArray_release(env, thing_name, options.thing_name);
    aws_jni_byte_cursor_from_jbyteArray_release(env, role_alias, options.role_alias);
    aws_jni_byte_cursor_from_jbyteArray_release(env, endpoint, options.endpoint);

    aws_http_proxy_options_jni_clean_up(
        env, &proxy_options, jni_proxy_host, jni_proxy_authorization_username, jni_proxy_authorization_password);

    aws_tls_connection_options_clean_up(&tls_connection_options);

    return (jlong)provider;
}

JNIEXPORT jlong JNICALL
    Java_software_amazon_awssdk_crt_auth_credentials_CachedCredentialsProvider_cachedCredentialsProviderNew(
        JNIEnv *env,
        jclass jni_class,
        jobject java_crt_credentials_provider,
        jint cached_duration_in_seconds,
        jlong native_cached_provider) {

    (void)jni_class;
    aws_cache_jni_ids(env);

    if (native_cached_provider == 0) {
        aws_jni_throw_runtime_exception(
            env, "CachedCredentialsProviderials.cachedCredentialsProviderNew: cached provider is null");
        return 0;
    }

    struct aws_allocator *allocator = aws_jni_get_allocator();

    struct aws_credentials_provider_callback_data *callback_data =
        aws_mem_calloc(allocator, 1, sizeof(struct aws_credentials_provider_callback_data));
    callback_data->java_crt_credentials_provider = (*env)->NewWeakGlobalRef(env, java_crt_credentials_provider);

    jint jvmresult = (*env)->GetJavaVM(env, &callback_data->jvm);
    AWS_FATAL_ASSERT(jvmresult == 0);

    struct aws_credentials_provider_cached_options options;
    AWS_ZERO_STRUCT(options);
    options.refresh_time_in_milliseconds =
        aws_timestamp_convert(cached_duration_in_seconds, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_MILLIS, NULL);
    options.source = (struct aws_credentials_provider *)native_cached_provider;

    options.shutdown_options.shutdown_callback = s_on_shutdown_complete;
    options.shutdown_options.shutdown_user_data = callback_data;

    struct aws_credentials_provider *provider = aws_credentials_provider_new_cached(allocator, &options);
    if (provider == NULL) {
        s_callback_data_clean_up(env, allocator, callback_data);
        aws_jni_throw_runtime_exception(env, "Failed to create cached credentials provider");
    } else {
        callback_data->provider = provider;
    }

    return (jlong)provider;
}

static int s_credentials_provider_delegate_get_credentials(
    void *delegate_user_data,
    aws_on_get_credentials_callback_fn callback,
    void *callback_user_data) {

    struct aws_credentials_provider_callback_data *callback_data = delegate_user_data;

    int return_value = AWS_OP_ERR;

    /********** JNI ENV ACQUIRE **********/
    JNIEnv *env = aws_jni_acquire_thread_env(callback_data->jvm);
    if (env == NULL) {
        /* If we can't get an environment, then the JVM is probably shutting down.  Don't crash. */
        return AWS_OP_ERR;
    }

    // Fetch credentials from java
    jobject java_credentials = (*env)->CallObjectMethod(
        env,
        callback_data->jni_delegate_credential_handler,
        credentials_handler_properties.on_handler_get_credentials_method_id);
    if (aws_jni_check_and_clear_exception(env)) {
        aws_raise_error(AWS_ERROR_HTTP_CALLBACK_FAILURE);
        goto done;
    }

    struct aws_credentials *native_credentials = aws_credentials_new_from_java_credentials(env, java_credentials);
    if (!native_credentials) {
        aws_jni_throw_runtime_exception(env, "Failed to create native credentials");
        // error has been raised from creating function
        goto done;
    }
    callback(native_credentials, AWS_ERROR_SUCCESS, callback_user_data);
    aws_credentials_release(native_credentials);

    return_value = AWS_OP_SUCCESS;

done:
    (*env)->DeleteLocalRef(env, java_credentials);

    aws_jni_release_thread_env(callback_data->jvm, env);
    /********** JNI ENV RELEASE **********/

    return return_value;
}

JNIEXPORT jlong JNICALL
    Java_software_amazon_awssdk_crt_auth_credentials_DelegateCredentialsProvider_delegateCredentialsProviderNew(
        JNIEnv *env,
        jclass jni_class,
        jobject java_crt_credentials_provider,
        jobject jni_delegate_credential_handler) {

    (void)jni_class;
    (void)env;
    aws_cache_jni_ids(env);

    struct aws_allocator *allocator = aws_jni_get_allocator();
    struct aws_credentials_provider_callback_data *callback_data =
        aws_mem_calloc(allocator, 1, sizeof(struct aws_credentials_provider_callback_data));
    callback_data->java_crt_credentials_provider = (*env)->NewWeakGlobalRef(env, java_crt_credentials_provider);
    callback_data->jni_delegate_credential_handler = (*env)->NewGlobalRef(env, jni_delegate_credential_handler);

    struct aws_credentials_provider_delegate_options options = {
        .get_credentials = s_credentials_provider_delegate_get_credentials,
        .delegate_user_data = callback_data,
        .shutdown_options =
            {
                .shutdown_callback = s_on_shutdown_complete,
                .shutdown_user_data = callback_data,
            },
    };

    jint jvmresult = (*env)->GetJavaVM(env, &callback_data->jvm);
    AWS_FATAL_ASSERT(jvmresult == 0);

    struct aws_credentials_provider *provider = aws_credentials_provider_new_delegate(allocator, &options);
    if (provider == NULL) {
        s_callback_data_clean_up(env, allocator, callback_data);
        aws_jni_throw_runtime_exception(env, "Failed to create delegate credentials provider");
    } else {
        callback_data->provider = provider;
    }

    return (jlong)provider;
}

static int s_fill_in_logins(struct aws_array_list *logins, struct aws_byte_cursor marshalled_logins) {
    struct aws_byte_cursor logins_cursor = marshalled_logins;
    uint32_t field_len = 0;

    while (logins_cursor.len > 0) {
        if (!aws_byte_cursor_read_be32(&logins_cursor, &field_len)) {
            return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT);
        }

        struct aws_byte_cursor identity_provider_name = aws_byte_cursor_advance(&logins_cursor, field_len);

        if (!aws_byte_cursor_read_be32(&logins_cursor, &field_len)) {
            return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT);
        }

        struct aws_byte_cursor identity_provider_token = aws_byte_cursor_advance(&logins_cursor, field_len);

        struct aws_cognito_identity_provider_token_pair login_pair = {
            .identity_provider_name = identity_provider_name,
            .identity_provider_token = identity_provider_token,
        };

        aws_array_list_push_back(logins, &login_pair);
    }

    return AWS_OP_SUCCESS;
}

JNIEXPORT
jlong JNICALL Java_software_amazon_awssdk_crt_auth_credentials_CognitoCredentialsProvider_cognitoCredentialsProviderNew(
    JNIEnv *env,
    jclass jni_class,
    jobject crt_credentials_provider,
    jlong native_bootstrap,
    jlong native_tls_context,
    jstring endpoint,
    jstring identity,
    jstring custom_role_arn,
    jbyteArray marshalled_logins,
    jint proxy_connection_type,
    jbyteArray proxy_host,
    jint proxy_port,
    jlong native_proxy_tls_context,
    jint proxy_authorization_type,
    jbyteArray proxy_authorization_username,
    jbyteArray proxy_authorization_password) {

    (void)jni_class;
    (void)env;
    aws_cache_jni_ids(env);

    struct aws_allocator *allocator = aws_jni_get_allocator();
    struct aws_credentials_provider *provider = NULL;
    struct aws_credentials_provider_callback_data *callback_data = NULL;

    struct aws_tls_connection_options proxy_tls_connection_options;
    AWS_ZERO_STRUCT(proxy_tls_connection_options);
    struct aws_http_proxy_options proxy_options;
    AWS_ZERO_STRUCT(proxy_options);

    struct aws_byte_cursor endpoint_cursor;
    AWS_ZERO_STRUCT(endpoint_cursor);
    struct aws_byte_cursor identity_cursor;
    AWS_ZERO_STRUCT(identity_cursor);
    struct aws_byte_cursor custom_role_arn_cursor;
    AWS_ZERO_STRUCT(custom_role_arn_cursor);
    struct aws_byte_cursor logins_cursor;
    AWS_ZERO_STRUCT(logins_cursor);

    struct aws_array_list logins;
    aws_array_list_init_dynamic(&logins, allocator, 0, sizeof(struct aws_cognito_identity_provider_token_pair));

    if (endpoint == NULL || identity == NULL) {
        goto done;
    }

    endpoint_cursor = aws_jni_byte_cursor_from_jstring_acquire(env, endpoint);
    identity_cursor = aws_jni_byte_cursor_from_jstring_acquire(env, identity);

    callback_data = aws_mem_calloc(allocator, 1, sizeof(struct aws_credentials_provider_callback_data));

    jint jvmresult = (*env)->GetJavaVM(env, &callback_data->jvm);
    AWS_FATAL_ASSERT(jvmresult == 0);
    callback_data->java_crt_credentials_provider = (*env)->NewWeakGlobalRef(env, crt_credentials_provider);

    struct aws_credentials_provider_cognito_options options = {
        .shutdown_options =
            {
                .shutdown_callback = s_on_shutdown_complete,
                .shutdown_user_data = callback_data,
            },
        .endpoint = endpoint_cursor,
        .identity = identity_cursor,
        .bootstrap = (void *)native_bootstrap,
        .tls_ctx = (void *)native_tls_context,
    };

    if (custom_role_arn != NULL) {
        custom_role_arn_cursor = aws_jni_byte_cursor_from_jstring_acquire(env, custom_role_arn);
        options.custom_role_arn = &custom_role_arn_cursor;
    }

    if (marshalled_logins != NULL) {
        logins_cursor = aws_jni_byte_cursor_from_jbyteArray_acquire(env, marshalled_logins);
        if (s_fill_in_logins(&logins, logins_cursor)) {
            goto done;
        }

        options.logins = logins.data;
        options.login_count = aws_array_list_length(&logins);
    }

    if (proxy_host != NULL) {
        aws_http_proxy_options_jni_init(
            env,
            &proxy_options,
            proxy_connection_type,
            &proxy_tls_connection_options,
            proxy_host,
            proxy_port,
            proxy_authorization_username,
            proxy_authorization_password,
            proxy_authorization_type,
            (struct aws_tls_ctx *)native_proxy_tls_context);

        options.http_proxy_options = &proxy_options;
    }

    provider = aws_credentials_provider_new_cognito(allocator, &options);
    if (provider != NULL) {
        callback_data->provider = provider;
    }

done:

    aws_jni_byte_cursor_from_jstring_release(env, endpoint, endpoint_cursor);
    aws_jni_byte_cursor_from_jstring_release(env, identity, identity_cursor);
    aws_jni_byte_cursor_from_jstring_release(env, custom_role_arn, custom_role_arn_cursor);
    aws_jni_byte_cursor_from_jbyteArray_release(env, marshalled_logins, logins_cursor);

    aws_http_proxy_options_jni_clean_up(
        env, &proxy_options, proxy_host, proxy_authorization_username, proxy_authorization_password);

    aws_array_list_clean_up(&logins);

    if (provider == NULL) {
        s_callback_data_clean_up(env, allocator, callback_data);
        aws_jni_throw_runtime_exception(env, "Failed to create native cognito credentials provider");
    }

    return (jlong)provider;
}

JNIEXPORT
void JNICALL Java_software_amazon_awssdk_crt_auth_credentials_CredentialsProvider_credentialsProviderDestroy(
    JNIEnv *env,
    jclass jni_cp,
    jobject cp_object,
    jlong cp_addr) {
    (void)jni_cp;
    (void)cp_object;
    aws_cache_jni_ids(env);

    struct aws_credentials_provider *provider = (struct aws_credentials_provider *)cp_addr;
    if (!provider) {
        aws_jni_throw_runtime_exception(
            env, "CredentialsProvider.credentialsProviderDestroy: instance should be non-null at destruction time");
        return;
    }

    aws_credentials_provider_release(provider);
}

struct aws_credentials_provider_get_credentials_callback_data {
    JavaVM *jvm;
    struct aws_credentials_provider *provider;
    jobject java_crt_credentials_provider;
    jobject java_credentials_future;
};

static void s_cp_callback_data_clean_up(
    struct aws_credentials_provider_get_credentials_callback_data *callback_data,
    JNIEnv *env) {
    if (callback_data == NULL || env == NULL) {
        return;
    }

    (*env)->DeleteGlobalRef(env, callback_data->java_crt_credentials_provider);
    (*env)->DeleteGlobalRef(env, callback_data->java_credentials_future);

    aws_credentials_provider_release(callback_data->provider);

    // We're done with this callback data, free it.
    aws_mem_release(aws_jni_get_allocator(), callback_data);
}

static void s_on_get_credentials_callback(struct aws_credentials *credentials, int error_code, void *user_data) {
    (void)error_code;

    struct aws_credentials_provider_get_credentials_callback_data *callback_data = user_data;

    /********** JNI ENV ACQUIRE **********/
    JNIEnv *env = aws_jni_acquire_thread_env(callback_data->jvm);
    if (env == NULL) {
        /* If we can't get an environment, then the JVM is probably shutting down.  Don't crash. */
        return;
    }

    jobject java_credentials = NULL;

    if (credentials) {
        java_credentials = aws_java_credentials_from_native_new(env, credentials);
    }

    (*env)->CallVoidMethod(
        env,
        callback_data->java_crt_credentials_provider,
        credentials_provider_properties.on_get_credentials_complete_method_id,
        callback_data->java_credentials_future,
        java_credentials);

    AWS_FATAL_ASSERT(!aws_jni_check_and_clear_exception(env));

    if (java_credentials != NULL) {
        (*env)->DeleteLocalRef(env, java_credentials);
    }

    JavaVM *jvm = callback_data->jvm;
    s_cp_callback_data_clean_up(callback_data, env);

    aws_jni_release_thread_env(jvm, env);
    /********** JNI ENV RELEASE **********/
}

JNIEXPORT
void JNICALL Java_software_amazon_awssdk_crt_auth_credentials_CredentialsProvider_credentialsProviderGetCredentials(
    JNIEnv *env,
    jclass jni_cp,
    jobject java_crt_credentials_provider,
    jobject java_credentials_future,
    jlong native_credentials_provider) {
    (void)jni_cp;
    aws_cache_jni_ids(env);

    struct aws_credentials_provider *provider = (struct aws_credentials_provider *)native_credentials_provider;
    if (!provider) {
        aws_jni_throw_runtime_exception(
            env, "CredentialsProvider.credentialsProviderGetCredentials: instance should be non-null");
        return;
    }

    if (java_crt_credentials_provider == NULL || java_credentials_future == NULL) {
        aws_jni_throw_runtime_exception(
            env, "CredentialsProvider.credentialsProviderGetCredentials: called with null parameters");
        return;
    }

    struct aws_allocator *allocator = aws_jni_get_allocator();
    struct aws_credentials_provider_get_credentials_callback_data *callback_data =
        aws_mem_calloc(allocator, 1, sizeof(struct aws_credentials_provider_get_credentials_callback_data));
    callback_data->java_crt_credentials_provider = (*env)->NewGlobalRef(env, java_crt_credentials_provider);
    callback_data->java_credentials_future = (*env)->NewGlobalRef(env, java_credentials_future);
    callback_data->provider = provider;

    jint jvmresult = (*env)->GetJavaVM(env, &callback_data->jvm);
    AWS_FATAL_ASSERT(jvmresult == 0);

    aws_credentials_provider_acquire(provider);

    if (aws_credentials_provider_get_credentials(provider, s_on_get_credentials_callback, callback_data)) {
        aws_jni_throw_runtime_exception(env, "CrtCredentialsProvider.credentialsProviderGetCredentials: call failure");
        /* callback will not be invoked on failure, clean up the resource here. */
        s_cp_callback_data_clean_up(callback_data, env);
    }
}

#if UINTPTR_MAX == 0xffffffff
#    if defined(_MSC_VER)
#        pragma warning(pop)
#    else
#        pragma GCC diagnostic pop
#    endif
#endif
