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

#include "auth.h"
#include "checksums.h"
#include "crypto.h"
#include "event_stream.h"
#include "http_connection.h"
#include "http_connection_manager.h"
#include "http_headers.h"
#include "http_message.h"
#include "http_stream.h"
#include "io.h"
#include "logger.h"
#include "mqtt5_client.h"
#include "mqtt_client.h"
#include "mqtt_client_connection.h"
#include "mqtt_request_response.h"

#include <aws/cal/cal.h>

#include <aws/common/clock.h>
#include <aws/common/environment.h>
#include <aws/common/logging.h>
#include <aws/common/mutex.h>
#include <aws/common/ref_count.h>
#include <aws/common/rw_lock.h>
#include <aws/common/system_info.h>

#include <aws/event-stream/event_stream.h>

#include <aws/io/event_loop.h>
#include <aws/io/tls_channel_handler.h>

#include <aws/http/http.h>

#include <aws/auth/auth.h>

#include <uv.h>

/*
 * This is a multi-line comment to ensure that the static assert does not collide with the static asserts in
 * aws/common/macro.h.
 *
 */
AWS_STATIC_ASSERT(NAPI_VERSION >= 4);

#define AWS_DEFINE_ERROR_INFO_CRT_NODEJS(CODE, STR) AWS_DEFINE_ERROR_INFO(CODE, STR, "aws-crt-nodejs")

/* TODO:
 * Hardcoded enum value for `napi_no_external_buffers_allowed`.
 * The enum `napi_no_external_buffers_allowed` is introduced in node21 and backport
 * to node 14.21.2, 16.19.0, 18.13.0.
 * Use `napi_no_external_buffers_allowed` for external buffer related changes after bump to node 21 */
#define NAPI_NO_EXTERNAL_BUFFER_ENUM_VALUE 22

static bool s_tsfn_enabled = false;
static struct aws_rw_lock s_tsfn_lock;

/* clang-format off */
static struct aws_error_info s_errors[] = {
    AWS_DEFINE_ERROR_INFO_CRT_NODEJS(
        AWS_CRT_NODEJS_ERROR_THREADSAFE_FUNCTION_NULL_NAPI_ENV,
        "There was an attempt to execute a thread-safe napi function binding with a null napi environment.  This is usually due to the function binding being released by a shutdown/cleanup process while the execution is waiting in the queue."),
    AWS_DEFINE_ERROR_INFO_CRT_NODEJS(
        AWS_CRT_NODEJS_ERROR_NAPI_FAILURE,
        "A N-API API call failed"),
    AWS_DEFINE_ERROR_INFO_CRT_NODEJS(
        AWS_CRT_NODEJS_ERROR_EVENT_STREAM_USER_CLOSE,
        "User invoked close on an eventstream connection."),
};
/* clang-format on */

static struct aws_error_info_list s_error_list = {
    .error_list = s_errors,
    .count = sizeof(s_errors) / sizeof(struct aws_error_info),
};

static struct aws_log_subject_info s_log_subject_infos[] = {
    DEFINE_LOG_SUBJECT_INFO(AWS_LS_NODEJS_CRT_GENERAL, "node", "General Node/N-API events"),
};

static struct aws_log_subject_info_list s_log_subject_list = {
    .subject_list = s_log_subject_infos,
    .count = AWS_ARRAY_SIZE(s_log_subject_infos),
};

static uv_loop_t *s_node_uv_loop = NULL;
static struct aws_event_loop *s_node_uv_event_loop = NULL;
static struct aws_event_loop_group *s_node_uv_elg = NULL;
static struct aws_host_resolver *s_default_host_resolver = NULL;
static struct aws_client_bootstrap *s_default_client_bootstrap = NULL;

int aws_napi_attach_object_property_boolean(napi_value object, napi_env env, const char *key_name, bool value) {
    if (key_name == NULL) {
        return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT);
    }

    napi_value napi_boolean = NULL;

    AWS_NAPI_CALL(env, napi_get_boolean(env, value, &napi_boolean), {
        return aws_raise_error(AWS_CRT_NODEJS_ERROR_NAPI_FAILURE);
    });
    AWS_NAPI_CALL(env, napi_set_named_property(env, object, key_name, napi_boolean), {
        return aws_raise_error(AWS_CRT_NODEJS_ERROR_NAPI_FAILURE);
    });

    return AWS_OP_SUCCESS;
}

int aws_napi_attach_object_property_optional_boolean(
    napi_value object,
    napi_env env,
    const char *key_name,
    const bool *value) {
    if (value == NULL) {
        return AWS_OP_SUCCESS;
    }

    return aws_napi_attach_object_property_boolean(object, env, key_name, *value);
}

/*
 * IEEE fp double analysis says 2 ^ 53 is the maximum sequential value we could support, but the napi docs for
 * napi_create_int64() say (2 ^ 53 - 1) so we'll use their more conservative bound.
 */
#define MAX_ALLOWED_UINT64_T_TO_DOUBLE ((((uint64_t)1) << 53) - 1)

int aws_napi_attach_object_property_u64(napi_value object, napi_env env, const char *key_name, uint64_t value) {
    if (key_name == NULL) {
        return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT);
    }

    if (value > MAX_ALLOWED_UINT64_T_TO_DOUBLE) {
        return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT);
    }

    napi_value napi_i64 = NULL;

    AWS_NAPI_CALL(env, napi_create_int64(env, (int64_t)value, &napi_i64), {
        return aws_raise_error(AWS_CRT_NODEJS_ERROR_NAPI_FAILURE);
    });
    AWS_NAPI_CALL(env, napi_set_named_property(env, object, key_name, napi_i64), {
        return aws_raise_error(AWS_CRT_NODEJS_ERROR_NAPI_FAILURE);
    });

    return AWS_OP_SUCCESS;
}

int aws_napi_attach_object_property_optional_u64(
    napi_value object,
    napi_env env,
    const char *key_name,
    const uint64_t *value) {
    if (value == NULL) {
        return AWS_OP_SUCCESS;
    }

    return aws_napi_attach_object_property_u64(object, env, key_name, *value);
}

int aws_napi_attach_object_property_u32(napi_value object, napi_env env, const char *key_name, uint32_t value) {
    if (key_name == NULL) {
        return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT);
    }

    napi_value napi_u32 = NULL;

    AWS_NAPI_CALL(
        env, napi_create_uint32(env, value, &napi_u32), { return aws_raise_error(AWS_CRT_NODEJS_ERROR_NAPI_FAILURE); });
    AWS_NAPI_CALL(env, napi_set_named_property(env, object, key_name, napi_u32), {
        return aws_raise_error(AWS_CRT_NODEJS_ERROR_NAPI_FAILURE);
    });

    return AWS_OP_SUCCESS;
}

int aws_napi_attach_object_property_optional_u32(
    napi_value object,
    napi_env env,
    const char *key_name,
    const uint32_t *value) {
    if (value == NULL) {
        return AWS_OP_SUCCESS;
    }

    return aws_napi_attach_object_property_u32(object, env, key_name, *value);
}

int aws_napi_attach_object_property_i32(napi_value object, napi_env env, const char *key_name, int32_t value) {
    if (key_name == NULL) {
        return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT);
    }

    napi_value napi_i32 = NULL;

    AWS_NAPI_CALL(
        env, napi_create_int32(env, value, &napi_i32), { return aws_raise_error(AWS_CRT_NODEJS_ERROR_NAPI_FAILURE); });
    AWS_NAPI_CALL(env, napi_set_named_property(env, object, key_name, napi_i32), {
        return aws_raise_error(AWS_CRT_NODEJS_ERROR_NAPI_FAILURE);
    });

    return AWS_OP_SUCCESS;
}

int aws_napi_attach_object_property_u16(napi_value object, napi_env env, const char *key_name, uint16_t value) {
    return aws_napi_attach_object_property_u32(object, env, key_name, (uint32_t)value);
}

int aws_napi_attach_object_property_optional_u16(
    napi_value object,
    napi_env env,
    const char *key_name,
    const uint16_t *value) {
    if (value == NULL) {
        return AWS_OP_SUCCESS;
    }

    return aws_napi_attach_object_property_u16(object, env, key_name, *value);
}

int aws_napi_attach_object_property_string(
    napi_value object,
    napi_env env,
    const char *key_name,
    struct aws_byte_cursor value) {

    if (key_name == NULL) {
        return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT);
    }

    napi_value napi_string = NULL;

    AWS_NAPI_CALL(env, napi_create_string_utf8(env, (const char *)(value.ptr), value.len, &napi_string), {
        return aws_raise_error(AWS_CRT_NODEJS_ERROR_NAPI_FAILURE);
    });
    AWS_NAPI_CALL(env, napi_set_named_property(env, object, key_name, napi_string), {
        return aws_raise_error(AWS_CRT_NODEJS_ERROR_NAPI_FAILURE);
    });

    return AWS_OP_SUCCESS;
}

int aws_napi_attach_object_property_optional_string(
    napi_value object,
    napi_env env,
    const char *key_name,
    const struct aws_byte_cursor *value) {
    if (value == NULL) {
        return AWS_OP_SUCCESS;
    }

    return aws_napi_attach_object_property_string(object, env, key_name, *value);
}

static void s_finalize_external_binary_byte_buf(napi_env env, void *finalize_data, void *finalize_hint) {
    (void)env;
    (void)finalize_data;

    struct aws_byte_buf *buffer = finalize_hint;
    if (buffer == NULL) {
        return;
    }

    struct aws_allocator *allocator = buffer->allocator;
    aws_byte_buf_clean_up(buffer);
    aws_mem_release(allocator, buffer);
}

int aws_napi_attach_object_property_binary_as_finalizable_external(
    napi_value object,
    napi_env env,
    const char *key_name,
    struct aws_byte_buf *data_buffer) {

    if (key_name == NULL) {
        return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT);
    }

    napi_value napi_binary = NULL;

    AWS_NAPI_ENSURE(
        env,
        aws_napi_create_external_arraybuffer(
            env,
            data_buffer->buffer,
            data_buffer->len,
            s_finalize_external_binary_byte_buf,
            data_buffer,
            &napi_binary));

    AWS_NAPI_CALL(env, napi_set_named_property(env, object, key_name, napi_binary), {
        return aws_raise_error(AWS_CRT_NODEJS_ERROR_NAPI_FAILURE);
    });

    return AWS_OP_SUCCESS;
}

enum aws_napi_get_named_property_result aws_napi_get_named_property(
    napi_env env,
    napi_value object,
    const char *name,
    napi_valuetype type,
    napi_value *result) {

    if (name == NULL) {
        return AWS_NGNPR_NO_VALUE;
    }

    bool has_property = false;
    if (napi_has_named_property(env, object, name, &has_property) || !has_property) {
        return AWS_NGNPR_NO_VALUE;
    }

    napi_value property = NULL;
    if (napi_get_named_property(env, object, name, &property)) {
        return AWS_NGNPR_NO_VALUE;
    }

    if (type != napi_undefined) {
        napi_valuetype property_type = napi_undefined;
        if (napi_typeof(env, property, &property_type)) {
            return AWS_NGNPR_INVALID_VALUE;
        }

        if (property_type != type) {
            return AWS_NGNPR_INVALID_VALUE;
        }
    }

    *result = property;
    return AWS_NGNPR_VALID_VALUE;
}

enum aws_napi_get_named_property_result aws_napi_get_named_property_as_uint16(
    napi_env env,
    napi_value object,
    const char *name,
    uint16_t *result) {

    napi_value node_result;
    enum aws_napi_get_named_property_result get_property_result =
        aws_napi_get_named_property(env, object, name, napi_number, &node_result);
    if (get_property_result != AWS_NGNPR_VALID_VALUE) {
        return get_property_result;
    }

    int64_t int_value = 0;
    AWS_NAPI_CALL(env, napi_get_value_int64(env, node_result, &int_value), { return AWS_NGNPR_INVALID_VALUE; });

    if (int_value < 0 || int_value > UINT16_MAX) {
        return AWS_NGNPR_INVALID_VALUE;
    }

    *result = (uint16_t)int_value;
    return AWS_NGNPR_VALID_VALUE;
}

enum aws_napi_get_named_property_result aws_napi_get_named_property_as_uint32(
    napi_env env,
    napi_value object,
    const char *name,
    uint32_t *result) {

    napi_value node_result;
    enum aws_napi_get_named_property_result get_property_result =
        aws_napi_get_named_property(env, object, name, napi_number, &node_result);
    if (get_property_result != AWS_NGNPR_VALID_VALUE) {
        return get_property_result;
    }

    int64_t int_value = 0;
    AWS_NAPI_CALL(env, napi_get_value_int64(env, node_result, &int_value), { return AWS_NGNPR_INVALID_VALUE; });

    if (int_value < 0 || int_value > UINT32_MAX) {
        return AWS_NGNPR_INVALID_VALUE;
    }

    *result = (uint32_t)int_value;
    return AWS_NGNPR_VALID_VALUE;
}

enum aws_napi_get_named_property_result aws_napi_get_named_property_as_uint64(
    napi_env env,
    napi_value object,
    const char *name,
    uint64_t *result) {

    napi_value node_result;
    enum aws_napi_get_named_property_result get_property_result =
        aws_napi_get_named_property(env, object, name, napi_number, &node_result);
    if (get_property_result != AWS_NGNPR_VALID_VALUE) {
        return get_property_result;
    }

    int64_t int_value = 0;
    AWS_NAPI_CALL(env, napi_get_value_int64(env, node_result, &int_value), { return AWS_NGNPR_INVALID_VALUE; });

    if (int_value < 0) {
        return AWS_NGNPR_INVALID_VALUE;
    }

    *result = (uint64_t)int_value;
    return AWS_NGNPR_VALID_VALUE;
}

enum aws_napi_get_named_property_result aws_napi_get_named_property_as_uint8(
    napi_env env,
    napi_value object,
    const char *name,
    uint8_t *result) {

    napi_value node_result;
    enum aws_napi_get_named_property_result get_property_result =
        aws_napi_get_named_property(env, object, name, napi_number, &node_result);
    if (get_property_result != AWS_NGNPR_VALID_VALUE) {
        return get_property_result;
    }

    int64_t int_value = 0;
    AWS_NAPI_CALL(env, napi_get_value_int64(env, node_result, &int_value), { return AWS_NGNPR_INVALID_VALUE; });

    if (int_value < 0 || int_value > UINT8_MAX) {
        return AWS_NGNPR_INVALID_VALUE;
    }

    *result = (uint8_t)int_value;
    return AWS_NGNPR_VALID_VALUE;
}

enum aws_napi_get_named_property_result aws_napi_get_named_property_as_int8(
    napi_env env,
    napi_value object,
    const char *name,
    int8_t *result) {

    napi_value node_result;
    enum aws_napi_get_named_property_result get_property_result =
        aws_napi_get_named_property(env, object, name, napi_number, &node_result);
    if (get_property_result != AWS_NGNPR_VALID_VALUE) {
        return get_property_result;
    }

    int64_t int_value = 0;
    AWS_NAPI_CALL(env, napi_get_value_int64(env, node_result, &int_value), { return AWS_NGNPR_INVALID_VALUE; });

    if (int_value < INT8_MIN || int_value > INT8_MAX) {
        return AWS_NGNPR_INVALID_VALUE;
    }

    *result = (int8_t)int_value;
    return AWS_NGNPR_VALID_VALUE;
}

enum aws_napi_get_named_property_result aws_napi_get_named_property_as_int16(
    napi_env env,
    napi_value object,
    const char *name,
    int16_t *result) {

    napi_value node_result;
    enum aws_napi_get_named_property_result get_property_result =
        aws_napi_get_named_property(env, object, name, napi_number, &node_result);
    if (get_property_result != AWS_NGNPR_VALID_VALUE) {
        return get_property_result;
    }

    int64_t int_value = 0;
    AWS_NAPI_CALL(env, napi_get_value_int64(env, node_result, &int_value), { return AWS_NGNPR_INVALID_VALUE; });

    if (int_value < INT16_MIN || int_value > INT16_MAX) {
        return AWS_NGNPR_INVALID_VALUE;
    }

    *result = (int16_t)int_value;
    return AWS_NGNPR_VALID_VALUE;
}

enum aws_napi_get_named_property_result aws_napi_get_named_property_as_int32(
    napi_env env,
    napi_value object,
    const char *name,
    int32_t *result) {

    napi_value node_result;
    enum aws_napi_get_named_property_result get_property_result =
        aws_napi_get_named_property(env, object, name, napi_number, &node_result);
    if (get_property_result != AWS_NGNPR_VALID_VALUE) {
        return get_property_result;
    }

    int64_t int_value = 0;
    AWS_NAPI_CALL(env, napi_get_value_int64(env, node_result, &int_value), { return AWS_NGNPR_INVALID_VALUE; });

    if (int_value < INT32_MIN || int_value > INT32_MAX) {
        return AWS_NGNPR_INVALID_VALUE;
    }

    *result = (int32_t)int_value;
    return AWS_NGNPR_VALID_VALUE;
}

enum aws_napi_get_named_property_result aws_napi_get_named_property_as_int64(
    napi_env env,
    napi_value object,
    const char *name,
    int64_t *result) {

    napi_value node_result;
    enum aws_napi_get_named_property_result get_property_result =
        aws_napi_get_named_property(env, object, name, napi_number, &node_result);
    if (get_property_result != AWS_NGNPR_VALID_VALUE) {
        return get_property_result;
    }

    AWS_NAPI_CALL(env, napi_get_value_int64(env, node_result, result), { return AWS_NGNPR_INVALID_VALUE; });

    return AWS_NGNPR_VALID_VALUE;
}

enum aws_napi_get_named_property_result aws_napi_get_named_property_as_boolean(
    napi_env env,
    napi_value object,
    const char *name,
    bool *result) {
    napi_value node_result;
    enum aws_napi_get_named_property_result get_property_result =
        aws_napi_get_named_property(env, object, name, napi_boolean, &node_result);
    if (get_property_result != AWS_NGNPR_VALID_VALUE) {
        return get_property_result;
    }

    AWS_NAPI_CALL(env, napi_get_value_bool(env, node_result, result), { return AWS_NGNPR_INVALID_VALUE; });

    return AWS_NGNPR_VALID_VALUE;
}

enum aws_napi_get_named_property_result aws_napi_get_named_property_boolean_as_uint8(
    napi_env env,
    napi_value object,
    const char *name,
    uint8_t *result) {
    napi_value node_result;
    enum aws_napi_get_named_property_result get_property_result =
        aws_napi_get_named_property(env, object, name, napi_boolean, &node_result);
    if (get_property_result != AWS_NGNPR_VALID_VALUE) {
        return get_property_result;
    }

    bool bool_result = false;
    AWS_NAPI_CALL(env, napi_get_value_bool(env, node_result, &bool_result), { return AWS_NGNPR_INVALID_VALUE; });

    *result = bool_result ? 1 : 0;

    return AWS_NGNPR_VALID_VALUE;
}

enum aws_napi_get_named_property_result aws_napi_get_named_property_as_bytebuf(
    napi_env env,
    napi_value object,
    const char *name,
    napi_valuetype type,
    struct aws_byte_buf *result) {

    napi_value node_result;
    enum aws_napi_get_named_property_result get_property_result =
        aws_napi_get_named_property(env, object, name, type, &node_result);
    if (get_property_result != AWS_NGNPR_VALID_VALUE) {
        return get_property_result;
    }

    AWS_NAPI_CALL(env, aws_byte_buf_init_from_napi(result, env, node_result), { return AWS_NGNPR_INVALID_VALUE; });

    return AWS_NGNPR_VALID_VALUE;
}

enum aws_napi_get_named_property_result aws_napi_get_named_property_buffer_length(
    napi_env env,
    napi_value object,
    const char *name,
    napi_valuetype type,
    size_t *length_out) {

    struct aws_byte_buf buffer;
    AWS_ZERO_STRUCT(buffer);

    enum aws_napi_get_named_property_result result =
        aws_napi_get_named_property_as_bytebuf(env, object, name, type, &buffer);
    if (result == AWS_NGNPR_VALID_VALUE) {
        *length_out = buffer.len;
    }

    aws_byte_buf_clean_up(&buffer);

    return result;
}

static int s_typed_array_element_type_to_byte_length(napi_typedarray_type type, size_t *element_length) {
    switch (type) {
        case napi_int8_array:
        case napi_uint8_array:
        case napi_uint8_clamped_array:
            *element_length = 1;
            return AWS_OP_SUCCESS;

        case napi_int16_array:
        case napi_uint16_array:
            *element_length = 2;
            return AWS_OP_SUCCESS;

        case napi_int32_array:
        case napi_uint32_array:
        case napi_float32_array:
            *element_length = 4;
            return AWS_OP_SUCCESS;

        case napi_float64_array:
        case 9:  /*napi_bigint64_array */
        case 10: /*napi_biguint64_array*/
            *element_length = 8;
            return AWS_OP_SUCCESS;
    }

    return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT);
}

napi_status aws_byte_buf_init_from_napi(struct aws_byte_buf *buf, napi_env env, napi_value node_str) {

    AWS_ASSERT(buf);

    napi_valuetype type = napi_undefined;
    AWS_NAPI_CALL(env, napi_typeof(env, node_str, &type), { return status; });

    if (type == napi_string) {

        size_t length = 0;
        AWS_NAPI_CALL(env, napi_get_value_string_utf8(env, node_str, NULL, 0, &length), { return status; });

        /* Node requires that the null terminator be written */
        if (aws_byte_buf_init(buf, aws_napi_get_allocator(), length + 1)) {
            return napi_generic_failure;
        }

        AWS_NAPI_CALL(env, napi_get_value_string_utf8(env, node_str, (char *)buf->buffer, buf->capacity, &buf->len), {
            return status;
        });
        AWS_ASSERT(length == buf->len);
        return napi_ok;

    } else if (type == napi_object) {

        bool is_expected = false;

        /* Try ArrayBuffer */
        AWS_NAPI_CALL(env, napi_is_arraybuffer(env, node_str, &is_expected), { return status; });
        if (is_expected) {
            napi_status status = napi_get_arraybuffer_info(env, node_str, (void **)&buf->buffer, &buf->len);
            buf->capacity = buf->len;
            return status;
        }

        /* Try DataView */
        AWS_NAPI_CALL(env, napi_is_dataview(env, node_str, &is_expected), { return status; });
        if (is_expected) {
            napi_status status = napi_get_dataview_info(env, node_str, &buf->len, (void **)&buf->buffer, NULL, NULL);
            buf->capacity = buf->len;
            return status;
        }

        /* Try TypedArray */
        AWS_NAPI_CALL(env, napi_is_typedarray(env, node_str, &is_expected), { return status; });
        if (is_expected) {
            napi_typedarray_type array_type = napi_uint8_array;
            size_t length = 0;
            AWS_NAPI_CALL(
                env, napi_get_typedarray_info(env, node_str, &array_type, &length, (void **)&buf->buffer, NULL, NULL), {
                    return status;
                });

            size_t element_size = 0;
            if (s_typed_array_element_type_to_byte_length(array_type, &element_size)) {
                return napi_invalid_arg;
            }

            buf->len = length * element_size;
            buf->capacity = buf->len;

            return napi_ok;
        }
    }

    return napi_invalid_arg;
}

int aws_napi_value_get_storage_length(napi_env env, napi_value value, size_t *storage_length) {

    napi_valuetype type = napi_undefined;
    AWS_NAPI_CALL(env, napi_typeof(env, value, &type), { return aws_raise_error(AWS_CRT_NODEJS_ERROR_NAPI_FAILURE); });

    if (type == napi_string) {
        AWS_NAPI_CALL(env, napi_get_value_string_utf8(env, value, NULL, 0, storage_length), {
            return aws_raise_error(AWS_CRT_NODEJS_ERROR_NAPI_FAILURE);
        });

        return AWS_OP_SUCCESS;
    } else if (type == napi_object) {
        /* Try ArrayBuffer */
        bool is_array_buffer = false;
        AWS_NAPI_CALL(env, napi_is_arraybuffer(env, value, &is_array_buffer), {
            return aws_raise_error(AWS_CRT_NODEJS_ERROR_NAPI_FAILURE);
        });
        if (is_array_buffer) {
            void *buffer = NULL;
            AWS_NAPI_CALL(env, napi_get_arraybuffer_info(env, value, &buffer, storage_length), {
                return aws_raise_error(AWS_CRT_NODEJS_ERROR_NAPI_FAILURE);
            });

            return AWS_OP_SUCCESS;
        }

        /* Try DataView */
        bool is_data_view = false;
        AWS_NAPI_CALL(env, napi_is_dataview(env, value, &is_data_view), {
            return aws_raise_error(AWS_CRT_NODEJS_ERROR_NAPI_FAILURE);
        });
        if (is_data_view) {
            AWS_NAPI_CALL(env, napi_get_dataview_info(env, value, storage_length, NULL, NULL, NULL), {
                return aws_raise_error(AWS_CRT_NODEJS_ERROR_NAPI_FAILURE);
            });

            return AWS_OP_SUCCESS;
        }

        /* Try TypedArray */
        bool is_typed_array = false;
        AWS_NAPI_CALL(env, napi_is_typedarray(env, value, &is_typed_array), {
            return aws_raise_error(AWS_CRT_NODEJS_ERROR_NAPI_FAILURE);
        });
        if (is_typed_array) {
            napi_typedarray_type array_type = napi_uint8_array;
            size_t length = 0;
            AWS_NAPI_CALL(env, napi_get_typedarray_info(env, value, &array_type, &length, NULL, NULL, NULL), {
                return aws_raise_error(AWS_CRT_NODEJS_ERROR_NAPI_FAILURE);
            });

            size_t element_size = 0;
            if (s_typed_array_element_type_to_byte_length(array_type, &element_size)) {
                return napi_invalid_arg;
            }

            *storage_length = length * element_size;

            return AWS_OP_SUCCESS;
        }
    }

    return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT);
}

int aws_napi_value_bytebuf_append(
    napi_env env,
    napi_value value,
    struct aws_byte_buf *output_buffer,
    struct aws_byte_cursor *bytes_written_cursor) {

    AWS_ZERO_STRUCT(*bytes_written_cursor);

    napi_valuetype type = napi_undefined;
    AWS_NAPI_CALL(env, napi_typeof(env, value, &type), { return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); });

    size_t open_bytes = output_buffer->capacity - output_buffer->len;
    uint8_t *open_space = output_buffer->buffer + output_buffer->len;

    if (type == napi_string) {
        size_t bytes_written = 0;
        AWS_NAPI_CALL(env, napi_get_value_string_utf8(env, value, (char *)open_space, open_bytes, &bytes_written), {
            return aws_raise_error(AWS_CRT_NODEJS_ERROR_NAPI_FAILURE);
        });

        /* technically, we might have been truncated, but there's no way to tell */
        bytes_written_cursor->ptr = open_space;
        bytes_written_cursor->len = bytes_written;
        output_buffer->len += bytes_written;

        return AWS_OP_SUCCESS;
    } else if (type == napi_object) {
        /* Try ArrayBuffer */
        bool is_array_buffer = false;
        AWS_NAPI_CALL(env, napi_is_arraybuffer(env, value, &is_array_buffer), {
            return aws_raise_error(AWS_CRT_NODEJS_ERROR_NAPI_FAILURE);
        });
        if (is_array_buffer) {
            void *buffer = NULL;
            size_t buffer_length = 0;
            AWS_NAPI_CALL(env, napi_get_arraybuffer_info(env, value, &buffer, &buffer_length), {
                return aws_raise_error(AWS_CRT_NODEJS_ERROR_NAPI_FAILURE);
            });

            bytes_written_cursor->ptr = buffer;
            bytes_written_cursor->len = buffer_length;

            return aws_byte_buf_append_and_update(output_buffer, bytes_written_cursor);
        }

        /* Try DataView */
        bool is_data_view = false;
        AWS_NAPI_CALL(env, napi_is_dataview(env, value, &is_data_view), {
            return aws_raise_error(AWS_CRT_NODEJS_ERROR_NAPI_FAILURE);
        });
        if (is_data_view) {
            void *buffer = NULL;
            size_t buffer_length = 0;
            AWS_NAPI_CALL(env, napi_get_dataview_info(env, value, &buffer_length, &buffer, NULL, NULL), {
                return aws_raise_error(AWS_CRT_NODEJS_ERROR_NAPI_FAILURE);
            });

            bytes_written_cursor->ptr = buffer;
            bytes_written_cursor->len = buffer_length;

            return aws_byte_buf_append_and_update(output_buffer, bytes_written_cursor);
        }

        bool is_typed_array = false;
        AWS_NAPI_CALL(env, napi_is_typedarray(env, value, &is_typed_array), {
            return aws_raise_error(AWS_CRT_NODEJS_ERROR_NAPI_FAILURE);
        });
        if (is_typed_array) {
            napi_typedarray_type array_type = napi_uint8_array;
            size_t length = 0;
            uint8_t *buffer = NULL;
            AWS_NAPI_CALL(
                env, napi_get_typedarray_info(env, value, &array_type, &length, (void **)&buffer, NULL, NULL), {
                    return aws_raise_error(AWS_CRT_NODEJS_ERROR_NAPI_FAILURE);
                });

            size_t element_size = 0;
            if (s_typed_array_element_type_to_byte_length(array_type, &element_size)) {
                return napi_invalid_arg;
            }

            bytes_written_cursor->ptr = buffer;
            bytes_written_cursor->len = element_size * length;

            return aws_byte_buf_append_and_update(output_buffer, bytes_written_cursor);
        }
    }

    return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT);
}

struct aws_string *aws_string_new_from_napi(napi_env env, napi_value node_str) {

    struct aws_byte_buf temp_buf;
    if (aws_byte_buf_init_from_napi(&temp_buf, env, node_str)) {
        return NULL;
    }

    struct aws_string *string = aws_string_new_from_array(aws_napi_get_allocator(), temp_buf.buffer, temp_buf.len);
    aws_byte_buf_clean_up(&temp_buf);
    return string;
}

napi_status aws_napi_create_dataview_from_byte_cursor(
    napi_env env,
    const struct aws_byte_cursor *cur,
    napi_value *result) {

    void *data = NULL;
    napi_value arraybuffer;
    AWS_NAPI_CALL(env, napi_create_arraybuffer(env, cur->len, &data, &arraybuffer), { return status; });

    struct aws_byte_buf arraybuffer_buf = aws_byte_buf_from_empty_array(data, cur->len);
    struct aws_byte_cursor input = *cur;
    if (!aws_byte_buf_write_from_whole_cursor(&arraybuffer_buf, input)) {
        return napi_generic_failure;
    }

    AWS_NAPI_CALL(env, napi_create_dataview(env, cur->len, arraybuffer, 0, result), { return status; });

    return napi_ok;
}

bool aws_napi_is_null_or_undefined(napi_env env, napi_value value) {

    napi_valuetype type = napi_undefined;
    if (napi_ok != napi_typeof(env, value, &type)) {
        return true;
    }

    return type == napi_null || type == napi_undefined;
}

int aws_napi_get_property_array_size(
    napi_env env,
    napi_value object,
    const char *property_name,
    size_t *array_size_out) {
    napi_value napi_array = NULL;
    enum aws_napi_get_named_property_result get_result =
        aws_napi_get_named_property(env, object, property_name, napi_object, &napi_array);
    if (get_result == AWS_NGNPR_NO_VALUE) {
        *array_size_out = 0;
        return AWS_OP_SUCCESS;
    } else if (get_result == AWS_NGNPR_INVALID_VALUE) {
        return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT);
    }

    uint32_t array_size = 0;
    AWS_NAPI_CALL(env, napi_get_array_length(env, napi_array, &array_size), {
        return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT);
    });

    *array_size_out = (size_t)array_size;

    return AWS_OP_SUCCESS;
}

void s_aws_enable_threadsafe_function(void) {
    aws_rw_lock_wlock(&s_tsfn_lock);
    s_tsfn_enabled = true;
    aws_rw_lock_wunlock(&s_tsfn_lock);
}

void s_aws_disable_threadsafe_function(void) {
    aws_rw_lock_wlock(&s_tsfn_lock);
    s_tsfn_enabled = false;
    aws_rw_lock_wunlock(&s_tsfn_lock);
}

napi_value aws_napi_disable_threadsafe_function(napi_env env, napi_callback_info info) {
    (void)info;
    if (env == NULL) {
        aws_raise_error(AWS_CRT_NODEJS_ERROR_THREADSAFE_FUNCTION_NULL_NAPI_ENV);
        return NULL;
    }
    s_aws_disable_threadsafe_function();
    return NULL;
}

void aws_napi_throw_last_error(napi_env env) {
    const int error_code = aws_last_error();
    napi_throw_error(env, aws_error_str(error_code), aws_error_debug_str(error_code));
}

void aws_napi_throw_last_error_with_context(napi_env env, const char *context) {
    const int error_code = aws_last_error();

    char full_msg[1024];
    snprintf(
        full_msg,
        AWS_ARRAY_SIZE(full_msg),
        "%s : (%s - %s)",
        context,
        aws_error_str(error_code),
        aws_error_debug_str(error_code));
    napi_throw_error(env, aws_error_str(error_code), full_msg);
}

struct uv_loop_s *aws_napi_get_node_uv_loop(void) {
    return s_node_uv_loop;
}

struct aws_event_loop *aws_napi_get_node_event_loop(void) {
    return s_node_uv_event_loop;
}

struct aws_event_loop_group *aws_napi_get_node_elg(void) {
    return s_node_uv_elg;
}

struct aws_client_bootstrap *aws_napi_get_default_client_bootstrap(void) {
    return s_default_client_bootstrap;
}

/* The napi_status enum has grown, and is not bound by N-API versioning */
#if defined(__clang__) || defined(__GNUC__)
#    pragma GCC diagnostic push
#    pragma GCC diagnostic ignored "-Wswitch"
#endif

const char *aws_napi_status_to_str(napi_status status) {
    const char *reason = "UNKNOWN";
    switch (status) {
        case napi_ok:
            reason = "OK";
            break;
        case napi_invalid_arg:
            reason = "napi_invalid_arg: an invalid argument was supplied";
            break;
        case napi_object_expected:
            reason = "napi_object_expected";
            break;
        case napi_string_expected:
            reason = "napi_name_expected";
            break;
        case napi_name_expected:
            reason = "napi_name_expected";
            break;
        case napi_function_expected:
            reason = "napi_function_expected";
            break;
        case napi_number_expected:
            reason = "napi_number_expected";
            break;
        case napi_boolean_expected:
            reason = "napi_boolean_expected";
            break;
        case napi_array_expected:
            reason = "napi_array_expected";
            break;
        case napi_generic_failure:
            reason = "napi_generic_failure";
            break;
        case napi_pending_exception:
            reason = "napi_pending_exception";
            break;
        case napi_cancelled:
            reason = "napi_cancelled";
            break;
        case napi_escape_called_twice:
            reason = "napi_escape_called_twice";
            break;
        case napi_handle_scope_mismatch:
            reason = "napi_handle_scope_mismatch";
            break;
        case napi_callback_scope_mismatch:
            reason = "napi_callback_scope_mismatch";
            break;
        case napi_queue_full:
            reason = "napi_queue_full";
            break;
        case napi_closing:
            reason = "napi_closing";
            break;
        case napi_bigint_expected:
            reason = "napi_bigint_expected";
            break;
        case NAPI_NO_EXTERNAL_BUFFER_ENUM_VALUE:
            reason = "napi_no_external_buffers_allowed";
            break;
    }
    return reason;
}

#if defined(__clang__) || defined(__GNUC__)
#    pragma GCC diagnostic pop
#endif

static void s_handle_failed_callback(napi_env env, napi_value function, napi_status reason) {
    /* Figure out if there's an exception pending, if so, no callbacks will ever succeed again until it's cleared */
    bool pending_exception = reason == napi_pending_exception;
    AWS_NAPI_ENSURE(env, napi_is_exception_pending(env, &pending_exception));
    /* if there's no pending exception, but failure occurred, log what we can find and get out */
    if (!pending_exception) {
        const napi_extended_error_info *node_error_info = NULL;
        AWS_NAPI_ENSURE(env, napi_get_last_error_info(env, &node_error_info));
        AWS_NAPI_LOGF_ERROR(
            "Extended error info: engine_error_code=%u error_code=%s error_message=%s",
            node_error_info->engine_error_code,
            aws_napi_status_to_str(node_error_info->error_code),
            node_error_info->error_message);
        return;
    }
    /* get the current exception and report it, and clear it so that execution can continue */
    napi_value node_exception = NULL;
    AWS_NAPI_ENSURE(env, napi_get_and_clear_last_exception(env, &node_exception));

    /* figure out what the exception is */
    bool is_error = false;
    AWS_NAPI_ENSURE(env, napi_is_error(env, node_exception, &is_error));

    /*
     * Convert the function to a string. If it's a lambda, this will produce the source of the lambda, if
     * it's a class function or free function, it will produce the name
     */
    napi_value node_function_str = NULL;
    AWS_NAPI_ENSURE(env, napi_coerce_to_string(env, function, &node_function_str));
    struct aws_string *function_str = aws_string_new_from_napi(env, node_function_str);
    if (function_str) {
        AWS_NAPI_LOGF_ERROR("Calling %s", aws_string_c_str(function_str));
    }

    /* If it's an Error, extract info from it and log it */
    if (is_error) {
        /* get the Error.message field */
        napi_value node_message = NULL;
        AWS_NAPI_ENSURE(env, napi_get_named_property(env, node_exception, "message", &node_message));

        /* extract and log the message */
        struct aws_string *message = aws_string_new_from_napi(env, node_message);
        if (message) {
            AWS_NAPI_LOGF_ERROR("Error: %s", aws_string_bytes(message));
            aws_string_destroy(message);
        } else {
            AWS_NAPI_LOGF_ERROR("aws_string_new_from_napi(exception.message) failed");
            return;
        }

        /* get the Error.stack field */
        napi_value node_stack = NULL;
        AWS_NAPI_ENSURE(env, napi_get_named_property(env, node_exception, "stack", &node_stack));

        /* extract and log the stack trace */
        struct aws_string *stacktrace = aws_string_new_from_napi(env, node_stack);
        if (stacktrace) {
            AWS_NAPI_LOGF_ERROR("Stack:\n%s", aws_string_bytes(stacktrace));
            aws_string_destroy(stacktrace);
        } else {
            AWS_NAPI_LOGF_ERROR("aws_string_new_from_napi(exception.stack) failed");
            return;
        }

        /* the Error has been reported and cleared, that's all we can do */
        return;
    }

    /* The last thing thrown was some other sort of object/primitive, so convert it to a string and log it */
    napi_value node_error_str = NULL;
    AWS_NAPI_ENSURE(env, napi_coerce_to_string(env, node_exception, &node_error_str));

    struct aws_string *error_str = aws_string_new_from_napi(env, node_error_str);
    if (error_str) {
        AWS_NAPI_LOGF_ERROR("Error: %s", aws_string_bytes(error_str));
    } else {
        AWS_NAPI_LOGF_ERROR("aws_string_new_from_napi(ToString(exception)) failed");
        return;
    }
}

napi_status aws_napi_create_external_arraybuffer(
    napi_env env,
    void *external_data,
    size_t byte_length,
    napi_finalize finalize_cb,
    void *finalize_hint,
    napi_value *result) {

    napi_status external_buffer_status =
        napi_create_external_arraybuffer(env, external_data, byte_length, finalize_cb, finalize_hint, result);

    if (external_buffer_status == NAPI_NO_EXTERNAL_BUFFER_ENUM_VALUE) {

        // The external buffer is disabled, manually copy the external_data into Node
        void *napi_buf_data = NULL;
        napi_status create_arraybuffer_status = napi_create_arraybuffer(env, byte_length, &napi_buf_data, result);

        if (create_arraybuffer_status != napi_ok) {
            AWS_NAPI_LOGF_ERROR(
                "napi_create_arraybuffer (in aws_napi_create_external_arraybuffer) failed with : %s",
                aws_napi_status_to_str(create_arraybuffer_status));
            return create_arraybuffer_status;
        }

        memcpy(napi_buf_data, external_data, byte_length);

        // As the data has been copied into the Node, invoke the finalize callback to make sure the
        // data is released.
        finalize_cb(env, finalize_hint, finalize_hint);
    }

    return napi_ok;
}

napi_status aws_napi_dispatch_threadsafe_function(
    napi_env env,
    napi_threadsafe_function tsfn,
    napi_value this_ptr,
    napi_value function,
    size_t argc,
    napi_value *argv) {

    aws_rw_lock_rlock(&s_tsfn_lock);
    napi_status result = napi_ok;
    if (s_tsfn_enabled) {
        napi_status call_status = napi_ok;
        if (!this_ptr) {
            AWS_NAPI_ENSURE(env, napi_get_undefined(env, &this_ptr));
        }
        AWS_NAPI_CALL(env, napi_call_function(env, this_ptr, function, argc, argv, NULL), {
            call_status = status;
            s_handle_failed_callback(env, function, status);
        });
        /* main thread can exit now */
        napi_unref_threadsafe_function(env, tsfn);
        /* Must always decrement the ref count, or the function will be pinned */
        napi_status release_status = napi_release_threadsafe_function(tsfn, napi_tsfn_release);
        result = (call_status != napi_ok) ? call_status : release_status;
    }
    aws_rw_lock_runlock(&s_tsfn_lock);
    return result;
}

napi_status aws_napi_create_threadsafe_function(
    napi_env env,
    napi_value function,
    const char *name,
    napi_threadsafe_function_call_js call_js,
    void *context,
    napi_threadsafe_function *result) {

    napi_value resource_name = NULL;
    AWS_NAPI_ENSURE(env, napi_create_string_utf8(env, name, NAPI_AUTO_LENGTH, &resource_name));

    return napi_create_threadsafe_function(
        env,
        function,
        NULL /*async_resource*/,
        resource_name,
        0 /*max_queue_size - 0 means no limit*/,
        1 /*initial_thread_count*/,
        NULL /*thread_finalize_data*/,
        NULL /*thread_finalize_cb*/,
        context,
        call_js,
        result);
}

napi_status aws_napi_release_threadsafe_function(
    napi_threadsafe_function function,
    napi_threadsafe_function_release_mode mode) {
    napi_status result = napi_ok;
    aws_rw_lock_rlock(&s_tsfn_lock);
    if (s_tsfn_enabled && function) {
        result = napi_release_threadsafe_function(function, mode);
    }
    aws_rw_lock_runlock(&s_tsfn_lock);
    return result;
}

napi_status aws_napi_acquire_threadsafe_function(napi_threadsafe_function function) {
    napi_status result = napi_ok;
    aws_rw_lock_rlock(&s_tsfn_lock);
    if (s_tsfn_enabled && function) {
        result = napi_acquire_threadsafe_function(function);
    }
    aws_rw_lock_runlock(&s_tsfn_lock);
    return result;
}

napi_status aws_napi_unref_threadsafe_function(napi_env env, napi_threadsafe_function function) {
    napi_status result = napi_ok;
    aws_rw_lock_rlock(&s_tsfn_lock);
    if (s_tsfn_enabled && function) {
        result = napi_unref_threadsafe_function(env, function);
    }
    aws_rw_lock_runlock(&s_tsfn_lock);
    return result;
}

napi_status aws_napi_queue_threadsafe_function(napi_threadsafe_function function, void *user_data) {
    napi_status result = napi_ok;
    aws_rw_lock_rlock(&s_tsfn_lock);
    if (s_tsfn_enabled && function) {
        /* increase the ref count, gets decreased when the call completes */
        AWS_NAPI_ENSURE(NULL, napi_acquire_threadsafe_function(function));
        result = napi_call_threadsafe_function(function, user_data, napi_tsfn_nonblocking);
    }
    aws_rw_lock_runlock(&s_tsfn_lock);
    return result;
}

AWS_STATIC_STRING_FROM_LITERAL(s_mem_tracing_env_var, "AWS_CRT_MEMORY_TRACING");
static struct aws_allocator *s_allocator = NULL;
struct aws_allocator *aws_napi_get_allocator(void) {
    if (AWS_UNLIKELY(s_allocator == NULL)) {
        struct aws_string *value = NULL;
        if (aws_get_environment_value(aws_default_allocator(), s_mem_tracing_env_var, &value) || value == NULL) {
            return s_allocator = aws_default_allocator();
        }

        int level = atoi(aws_string_c_str(value));
        if (level < AWS_MEMTRACE_NONE || level > AWS_MEMTRACE_STACKS) {
            /* this can't go through logging, because it happens before logging is set up */
            fprintf(
                stderr,
                "AWS_CRT_MEMORY_TRACING is set to invalid value: %s, must be 0 (none), 1 (bytes), or 2 (stacks)",
                aws_string_bytes(value));
            level = AWS_MEMTRACE_NONE;
        }
        s_allocator = aws_mem_tracer_new(aws_default_allocator(), NULL, level, 16);
    }
    return s_allocator;
}

napi_value aws_napi_native_memory(napi_env env, napi_callback_info info) {
    (void)info;
    napi_value node_allocated = NULL;
    size_t allocated = 0;
    if (aws_napi_get_allocator() != aws_default_allocator()) {
        allocated = aws_mem_tracer_bytes(aws_napi_get_allocator());
    }
    AWS_NAPI_CALL(env, napi_create_int64(env, allocated, &node_allocated), { return NULL; });
    return node_allocated;
}

napi_value aws_napi_native_memory_dump(napi_env env, napi_callback_info info) {
    (void)info;
    (void)env;
    if (aws_napi_get_allocator() != aws_default_allocator()) {
        aws_mem_tracer_dump(aws_napi_get_allocator());
    }
    return NULL;
}

#if defined(_WIN32)
#    include <windows.h>
static LONG WINAPI s_print_stack_trace(struct _EXCEPTION_POINTERS *exception_pointers) {
    aws_backtrace_print(stderr, exception_pointers);
    return EXCEPTION_EXECUTE_HANDLER;
}
#elif defined(AWS_HAVE_EXECINFO)
#    include <signal.h>
static void s_print_stack_trace(int sig, siginfo_t *sig_info, void *user_data) {
    (void)sig;
    (void)sig_info;
    (void)user_data;
    aws_backtrace_print(stderr, sig_info);
    exit(-1);
}
#endif

static void s_install_crash_handler(void) {
#if defined(_WIN32)
    SetUnhandledExceptionFilter(s_print_stack_trace);
#elif defined(AWS_HAVE_EXECINFO)
    struct sigaction sa;
    memset(&sa, 0, sizeof(struct sigaction));
    sigemptyset(&sa.sa_mask);

    sa.sa_flags = SA_NODEFER;
    sa.sa_sigaction = s_print_stack_trace;

    sigaction(SIGSEGV, &sa, NULL);
    sigaction(SIGABRT, &sa, NULL);
    sigaction(SIGILL, &sa, NULL);
    sigaction(SIGBUS, &sa, NULL);
#endif
}

static void s_uninstall_crash_handler(void) {
#if defined(_WIN32)
    SetUnhandledExceptionFilter(NULL);
#elif defined(AWS_HAVE_EXECINFO)
    sigaction(SIGSEGV, NULL, NULL);
    sigaction(SIGABRT, NULL, NULL);
    sigaction(SIGILL, NULL, NULL);
    sigaction(SIGBUS, NULL, NULL);
#endif
}

static struct aws_mutex s_module_lock = AWS_MUTEX_INIT;
static uint32_t s_module_initialize_count = 0;

static void s_napi_context_finalize(napi_env env, void *user_data, void *finalize_hint) {
    (void)env;
    (void)finalize_hint;

    aws_mutex_lock(&s_module_lock);
    AWS_FATAL_ASSERT(s_module_initialize_count > 0);
    --s_module_initialize_count;

    if (s_module_initialize_count == 0) {

        aws_client_bootstrap_release(s_default_client_bootstrap);
        s_default_client_bootstrap = NULL;

        aws_host_resolver_release(s_default_host_resolver);
        s_default_host_resolver = NULL;

        aws_event_loop_group_release(s_node_uv_elg);
        s_node_uv_elg = NULL;

        aws_thread_join_all_managed();

        aws_unregister_log_subject_info_list(&s_log_subject_list);
        aws_unregister_error_info(&s_error_list);
        aws_event_stream_library_clean_up();
        aws_auth_library_clean_up();
        aws_mqtt_library_clean_up();

        s_uninstall_crash_handler();
        // clean up threadsafe function lock
        aws_rw_lock_clean_up(&s_tsfn_lock);
    }

    struct aws_napi_context *ctx = user_data;
    aws_napi_logger_destroy(ctx->logger);
    struct aws_allocator *ctx_allocator = ctx->allocator;
    aws_mem_release(ctx->allocator, ctx);

    if (s_module_initialize_count == 0) {
        if (ctx_allocator != aws_default_allocator()) {
            if (s_allocator == ctx_allocator) {
                s_allocator = NULL;
            }
            aws_mem_tracer_destroy(ctx_allocator);
        }
    }
    aws_mutex_unlock(&s_module_lock);
}

static struct aws_napi_context *s_napi_context_new(struct aws_allocator *allocator, napi_env env, napi_value exports) {
    struct aws_napi_context *ctx = aws_mem_calloc(allocator, 1, sizeof(struct aws_napi_context));
    AWS_FATAL_ASSERT(ctx && "Failed to initialize napi context");
    ctx->allocator = allocator;

    /* bind the context to exports, thus binding its lifetime to that object */
    AWS_NAPI_ENSURE(env, napi_wrap(env, exports, ctx, s_napi_context_finalize, NULL, NULL));

    ctx->logger = aws_napi_logger_new(allocator, env);

    return ctx;
}

/** Helper for creating and registering a function */
static bool s_create_and_register_function(
    napi_env env,
    napi_value exports,
    napi_callback fn,
    const char *fn_name,
    size_t fn_name_len) {

    napi_value napi_fn;
    AWS_NAPI_CALL(env, napi_create_function(env, fn_name, fn_name_len, fn, NULL, &napi_fn), {
        napi_throw_error(env, NULL, "Unable to wrap native function");
        return false;
    });

    AWS_NAPI_CALL(env, napi_set_named_property(env, exports, fn_name, napi_fn), {
        napi_throw_error(env, NULL, "Unable to populate exports");
        return false;
    });

    return true;
}

#if defined(__clang__) || defined(__GNUC__)
/* Suppress compiler warnings about NAPI_MODULE_INIT().
 * See: https://github.com/nodejs/node/pull/49103 */
#    pragma GCC diagnostic ignored "-Wstrict-prototypes"
#endif

/* napi_value */ NAPI_MODULE_INIT() /* (napi_env env, napi_value exports) */ {

    aws_mutex_lock(&s_module_lock);

    struct aws_allocator *allocator = aws_napi_get_allocator();

    if (s_module_initialize_count == 0) {
        aws_rw_lock_init(&s_tsfn_lock);
        s_aws_enable_threadsafe_function();

        s_install_crash_handler();

        aws_mqtt_library_init(allocator);
        aws_auth_library_init(allocator);
        aws_event_stream_library_init(allocator);
        aws_register_error_info(&s_error_list);
        aws_register_log_subject_info_list(&s_log_subject_list);

        /* Initialize the event loop group */
        /*
         * We don't currently support multi-init of the module, but we should.
         * Things that would need to be solved:
         *    (1) global objects (event loop group, logger, allocator, more)
         *    (2) multi-init/multi-cleanup of aws-c-*
         *    (3) allocator cross-talk/lifetimes
         */
        AWS_FATAL_ASSERT(s_node_uv_elg == NULL);
        s_node_uv_elg = aws_event_loop_group_new_default(allocator, 1, NULL);
        AWS_FATAL_ASSERT(s_node_uv_elg != NULL);

        /*
         * Default host resolver and client bootstrap to use if none specific at the javascript level.  In most
         * cases the user doesn't even need to know about these, so let's let them leave it out completely.
         */
        AWS_FATAL_ASSERT(s_default_host_resolver == NULL);

        struct aws_host_resolver_default_options resolver_options = {
            .max_entries = 64,
            .el_group = s_node_uv_elg,
        };
        s_default_host_resolver = aws_host_resolver_new_default(allocator, &resolver_options);
        AWS_FATAL_ASSERT(s_default_host_resolver != NULL);

        AWS_FATAL_ASSERT(s_default_client_bootstrap == NULL);

        struct aws_client_bootstrap_options bootstrap_options = {
            .event_loop_group = s_node_uv_elg,
            .host_resolver = s_default_host_resolver,
        };

        s_default_client_bootstrap = aws_client_bootstrap_new(allocator, &bootstrap_options);

        AWS_FATAL_ASSERT(s_default_client_bootstrap != NULL);
    }

    ++s_module_initialize_count;

    aws_mutex_unlock(&s_module_lock);

    /* context is bound to exports, will be cleaned up by finalizer */
    s_napi_context_new(allocator, env, exports);

    napi_value null;
    napi_get_null(env, &null);

#define CREATE_AND_REGISTER_FN(fn)                                                                                     \
    if (!s_create_and_register_function(env, exports, aws_napi_##fn, #fn, sizeof(#fn))) {                              \
        return null;                                                                                                   \
    }

    /* Common */
    CREATE_AND_REGISTER_FN(native_memory)
    CREATE_AND_REGISTER_FN(native_memory_dump)
    CREATE_AND_REGISTER_FN(error_code_to_string)
    CREATE_AND_REGISTER_FN(error_code_to_name)
    CREATE_AND_REGISTER_FN(disable_threadsafe_function)

    /* IO */
    CREATE_AND_REGISTER_FN(io_logging_enable)
    CREATE_AND_REGISTER_FN(is_alpn_available)
    CREATE_AND_REGISTER_FN(io_client_bootstrap_new)
    CREATE_AND_REGISTER_FN(io_tls_cipher_preference_is_supported)
    CREATE_AND_REGISTER_FN(io_tls_ctx_new)
    CREATE_AND_REGISTER_FN(io_tls_connection_options_new);
    CREATE_AND_REGISTER_FN(io_socket_options_new)
    CREATE_AND_REGISTER_FN(io_input_stream_new)
    CREATE_AND_REGISTER_FN(io_input_stream_append)
    CREATE_AND_REGISTER_FN(io_pkcs11_lib_new)
    CREATE_AND_REGISTER_FN(io_pkcs11_lib_close)

    /* MQTT Request Response */
    CREATE_AND_REGISTER_FN(mqtt_request_response_client_new_from_5)
    CREATE_AND_REGISTER_FN(mqtt_request_response_client_new_from_311)
    CREATE_AND_REGISTER_FN(mqtt_request_response_client_close)
    CREATE_AND_REGISTER_FN(mqtt_request_response_client_submit_request)
    CREATE_AND_REGISTER_FN(mqtt_streaming_operation_new)
    CREATE_AND_REGISTER_FN(mqtt_streaming_operation_open)
    CREATE_AND_REGISTER_FN(mqtt_streaming_operation_close)

    /* MQTT5 Client */
    CREATE_AND_REGISTER_FN(mqtt5_client_new)
    CREATE_AND_REGISTER_FN(mqtt5_client_start)
    CREATE_AND_REGISTER_FN(mqtt5_client_stop)
    CREATE_AND_REGISTER_FN(mqtt5_client_subscribe)
    CREATE_AND_REGISTER_FN(mqtt5_client_unsubscribe)
    CREATE_AND_REGISTER_FN(mqtt5_client_publish)
    CREATE_AND_REGISTER_FN(mqtt5_client_get_queue_statistics)
    CREATE_AND_REGISTER_FN(mqtt5_client_invoke_publish_acknowledgement)
    CREATE_AND_REGISTER_FN(mqtt5_client_close)

    /* MQTT Client */
    CREATE_AND_REGISTER_FN(mqtt_client_new)

    /* MQTT Client Connection */
    CREATE_AND_REGISTER_FN(mqtt_client_connection_new)
    CREATE_AND_REGISTER_FN(mqtt_client_connection_connect)
    CREATE_AND_REGISTER_FN(mqtt_client_connection_reconnect)
    CREATE_AND_REGISTER_FN(mqtt_client_connection_publish)
    CREATE_AND_REGISTER_FN(mqtt_client_connection_subscribe)
    CREATE_AND_REGISTER_FN(mqtt_client_connection_on_message)
    CREATE_AND_REGISTER_FN(mqtt_client_connection_on_closed)
    CREATE_AND_REGISTER_FN(mqtt_client_connection_unsubscribe)
    CREATE_AND_REGISTER_FN(mqtt_client_connection_disconnect)
    CREATE_AND_REGISTER_FN(mqtt_client_connection_close)
    CREATE_AND_REGISTER_FN(mqtt_client_connection_get_queue_statistics)

    /* Crypto */
    CREATE_AND_REGISTER_FN(hash_md5_new)
    CREATE_AND_REGISTER_FN(hash_sha1_new)
    CREATE_AND_REGISTER_FN(hash_sha256_new)
    CREATE_AND_REGISTER_FN(hash_update)
    CREATE_AND_REGISTER_FN(hash_digest)
    CREATE_AND_REGISTER_FN(hash_md5_compute)
    CREATE_AND_REGISTER_FN(hash_sha1_compute)
    CREATE_AND_REGISTER_FN(hash_sha256_compute)
    CREATE_AND_REGISTER_FN(hmac_sha256_new)
    CREATE_AND_REGISTER_FN(hmac_update)
    CREATE_AND_REGISTER_FN(hmac_digest)
    CREATE_AND_REGISTER_FN(hmac_sha256_compute)

    /* Checksums */
    CREATE_AND_REGISTER_FN(checksums_crc32)
    CREATE_AND_REGISTER_FN(checksums_crc32c)
    CREATE_AND_REGISTER_FN(checksums_crc64nvme)

    /* HTTP */
    CREATE_AND_REGISTER_FN(http_proxy_options_new)
    CREATE_AND_REGISTER_FN(http_connection_new)
    CREATE_AND_REGISTER_FN(http_connection_close)
    CREATE_AND_REGISTER_FN(http_stream_new)
    CREATE_AND_REGISTER_FN(http_stream_activate)
    CREATE_AND_REGISTER_FN(http_stream_close)
    CREATE_AND_REGISTER_FN(http_connection_manager_new)
    CREATE_AND_REGISTER_FN(http_connection_manager_close)
    CREATE_AND_REGISTER_FN(http_connection_manager_acquire)
    CREATE_AND_REGISTER_FN(http_connection_manager_release)

    /* Event stream */
    CREATE_AND_REGISTER_FN(event_stream_client_connection_new)
    CREATE_AND_REGISTER_FN(event_stream_client_connection_connect)
    CREATE_AND_REGISTER_FN(event_stream_client_connection_close)
    CREATE_AND_REGISTER_FN(event_stream_client_connection_close_internal)
    CREATE_AND_REGISTER_FN(event_stream_client_connection_send_protocol_message)
    CREATE_AND_REGISTER_FN(event_stream_client_stream_new)
    CREATE_AND_REGISTER_FN(event_stream_client_stream_close)
    CREATE_AND_REGISTER_FN(event_stream_client_stream_activate)
    CREATE_AND_REGISTER_FN(event_stream_client_stream_send_message)

#undef CREATE_AND_REGISTER_FN

    AWS_NAPI_ENSURE(env, aws_napi_http_headers_bind(env, exports));
    AWS_NAPI_ENSURE(env, aws_napi_http_message_bind(env, exports));
    AWS_NAPI_ENSURE(env, aws_napi_auth_bind(env, exports));

    return exports;
}
