/* SPDX-License-Identifier: LGPL-2.1-or-later */

#include "libnm-client-aux-extern/nm-default-client.h"
#include "nmcs-provider-oci.h"
#include "nm-cloud-setup-utils.h"
#include "libnm-glib-aux/nm-jansson.h"

/*****************************************************************************/

#define HTTP_TIMEOUT_MS 3000

#define NM_OCI_HEADER "Authorization:Bearer Oracle"
#define NM_OCI_HOST   "169.254.169.254"
#define NM_OCI_BASE   "http://" NM_OCI_HOST

NMCS_DEFINE_HOST_BASE(_oci_base, NMCS_ENV_NM_CLOUD_SETUP_OCI_HOST, NM_OCI_BASE);

#define _oci_uri_concat(...) nmcs_utils_uri_build_concat(_oci_base(), "opc/v2/", __VA_ARGS__)

/*****************************************************************************/

struct _NMCSProviderOCI {
    NMCSProvider parent;
};

struct _NMCSProviderOCIClass {
    NMCSProviderClass parent;
};

G_DEFINE_TYPE(NMCSProviderOCI, nmcs_provider_oci, NMCS_TYPE_PROVIDER);

/*****************************************************************************/

static void
_detect_done_cb(GObject *source, GAsyncResult *result, gpointer user_data)
{
    gs_unref_object GTask *task      = user_data;
    gs_free_error GError  *get_error = NULL;
    gs_free_error GError  *error     = NULL;

    nm_http_client_poll_req_finish(NM_HTTP_CLIENT(source), result, NULL, NULL, &get_error);

    if (nm_utils_error_is_cancelled(get_error)) {
        g_task_return_error(task, g_steal_pointer(&get_error));
        return;
    }

    if (get_error) {
        nm_utils_error_set(&error,
                           NM_UTILS_ERROR_UNKNOWN,
                           "failure to get OCI instance data: %s",
                           get_error->message);
        g_task_return_error(task, g_steal_pointer(&error));
        return;
    }

    g_task_return_boolean(task, TRUE);
}

static void
detect(NMCSProvider *provider, GTask *task)
{
    NMHttpClient *http_client;
    gs_free char *uri = NULL;

    http_client = nmcs_provider_get_http_client(provider);

    nm_http_client_poll_req(http_client,
                            (uri = _oci_uri_concat("instance")),
                            HTTP_TIMEOUT_MS,
                            256 * 1024,
                            7000,
                            1000,
                            NM_MAKE_STRV(NM_OCI_HEADER),
                            NULL,
                            g_task_get_cancellable(task),
                            NULL,
                            NULL,
                            _detect_done_cb,
                            task);
}

/*****************************************************************************/

#define _VNIC_WARN(msg) _LOGW("get-config: " msg "(VNIC %s idx=%zu)", vnic_id, i)

static void
_get_config_done_cb(GObject *source, GAsyncResult *result, gpointer user_data)
{
    NMCSProviderGetConfigTaskData  *get_config_data;
    NMCSProviderGetConfigIfaceData *config_iface_data;
    gs_unref_bytes GBytes          *response = NULL;
    gs_free_error GError           *error    = NULL;
    nm_auto_decref_json json_t     *vnics    = NULL;
    gboolean                        is_baremetal;
    gs_unref_ptrarray GPtrArray    *phys_nic_macs = NULL;
    GHashTableIter                  h_iter;
    size_t                          i;

    nm_http_client_poll_req_finish(NM_HTTP_CLIENT(source), result, NULL, &response, &error);

    if (nm_utils_error_is_cancelled(error))
        return;

    get_config_data = user_data;

    if (error)
        goto out;

    vnics = json_loads(g_bytes_get_data(response, NULL), JSON_REJECT_DUPLICATES, NULL);
    if (!vnics || !json_is_array(vnics)) {
        nm_utils_error_set(&error,
                           NM_UTILS_ERROR_UNKNOWN,
                           "get-config: JSON parse failure, can't configure VNICs");
        goto out;
    }

    if (json_array_size(vnics) > 0) {
        is_baremetal = NULL != json_object_get(json_array_get(vnics, 0), "nicIndex");
        _LOGI("get-config: detected %s instance", is_baremetal ? "baremetal" : "VM");
    } else {
        is_baremetal = FALSE;
        _LOGI("get-config: empty VNICs metadata, cannot detect instance type");
    }

    if (is_baremetal)
        phys_nic_macs = g_ptr_array_sized_new(16);

    for (i = 0; i < json_array_size(vnics); i++) {
        json_t       *vnic, *field;
        const char   *vnic_id = "", *val;
        gs_free char *mac     = NULL;
        in_addr_t     addr;
        int           prefix;
        json_int_t    nic_index = -1, vlan_tag = -1;

        vnic = json_array_get(vnics, i);
        if (!json_is_object(vnic)) {
            _VNIC_WARN("JSON parse failure, ignoring VNIC");
            continue;
        }

        field   = json_object_get(vnic, "vnicId");
        vnic_id = json_is_string(field) ? json_string_value(field) : "";

        field = json_object_get(vnic, "macAddr");
        val   = json_is_string(field) ? json_string_value(field) : NULL;
        mac   = val ? nmcs_utils_hwaddr_normalize(val, json_string_length(field)) : NULL;
        if (!mac) {
            _VNIC_WARN("missing or invalid 'macAddr', ignoring VNIC");
            continue;
        }

        if (is_baremetal) {
            field     = json_object_get(vnic, "nicIndex");
            nic_index = json_is_integer(field) ? json_integer_value(field) : -1;
            if (nic_index < 0 || nic_index >= 1024) { /* 1024 = random limit to prevent abuse*/
                _VNIC_WARN("missing or invalid 'nicIndex', ignoring VNIC");
                continue;
            }

            field    = json_object_get(vnic, "vlanTag");
            vlan_tag = json_is_integer(field) ? json_integer_value(field) : -1;
            if (vlan_tag < 0) {
                _VNIC_WARN("missing or invalid 'vlanTag', ignoring VNIC");
                continue;
            }
        }

        config_iface_data = nmcs_provider_get_config_iface_data_create(get_config_data, FALSE, mac);
        config_iface_data->iface_idx = i;

        field = json_object_get(vnic, "privateIp");
        val   = json_is_string(field) ? json_string_value(field) : NULL;
        if (val && nm_inet_parse_bin(AF_INET, val, NULL, &addr)) {
            config_iface_data->has_ipv4s    = TRUE;
            config_iface_data->ipv4s_len    = 1;
            config_iface_data->ipv4s_arr    = g_new(in_addr_t, 1);
            config_iface_data->ipv4s_arr[0] = addr;
        } else {
            _VNIC_WARN("missing or invalid 'privateIp'");
        }

        field = json_object_get(vnic, "virtualRouterIp");
        val   = json_is_string(field) ? json_string_value(field) : NULL;
        if (val && nm_inet_parse_bin(AF_INET, val, NULL, &addr)) {
            config_iface_data->has_gateway = TRUE;
            config_iface_data->gateway     = addr;
        } else {
            _VNIC_WARN("missing or invalid 'virtualRouterIp'");
        }

        field = json_object_get(vnic, "subnetCidrBlock");
        val   = json_is_string(field) ? json_string_value(field) : NULL;
        if (val && nm_inet_parse_with_prefix_bin(AF_INET, val, NULL, &addr, &prefix)) {
            config_iface_data->has_cidr    = TRUE;
            config_iface_data->cidr_addr   = addr;
            config_iface_data->cidr_prefix = prefix;
        } else {
            _VNIC_WARN("missing or invalid 'subnetCidrBlock'");
        }

        if (is_baremetal) {
            gboolean is_phys_nic = vlan_tag == 0;

            /* In baremetal instances, configure VNICs' VLAN (physical NICs don't need it) */
            if (is_phys_nic) {
                config_iface_data->priv.oci.vlan_tag      = 0;
                config_iface_data->priv.oci.parent_hwaddr = NULL;
                if (nic_index >= phys_nic_macs->len)
                    g_ptr_array_set_size(phys_nic_macs,
                                         NM_MAX((guint) (nic_index + 1), phys_nic_macs->len * 2));
                phys_nic_macs->pdata[nic_index] = (gpointer) config_iface_data->hwaddr;
            } else {
                /* We might not have all the physical NICs' MACs yet, save nicIndex for later */
                config_iface_data->priv.oci.parent_hwaddr = GINT_TO_POINTER((int) nic_index);
                config_iface_data->priv.oci.vlan_tag      = vlan_tag;
            }
        }
    }

    if (is_baremetal) {
        g_hash_table_iter_init(&h_iter, get_config_data->result_dict);

        /* Now that all the metadata is processed we should have all the physical NICs' MACs */
        while (g_hash_table_iter_next(&h_iter, NULL, (gpointer *) &config_iface_data)) {
            bool is_phys_nic = config_iface_data->priv.oci.vlan_tag == 0;
            int  nic_index   = GPOINTER_TO_INT(config_iface_data->priv.oci.parent_hwaddr);

            if (is_phys_nic)
                continue;

            if (nic_index >= phys_nic_macs->len || phys_nic_macs->pdata[nic_index] == NULL) {
                _LOGW("get-config: physical NIC for nicIndex=%d not found, ignoring VNIC "
                      "(VNIC macAddr=%s)",
                      nic_index,
                      config_iface_data->hwaddr);
                g_hash_table_iter_remove(&h_iter);
                continue;
            }

            config_iface_data->priv.oci.parent_hwaddr = g_strdup(phys_nic_macs->pdata[nic_index]);
        }
    }

out:
    _nmcs_provider_get_config_task_maybe_return(get_config_data, g_steal_pointer(&error));
}

static void
get_config(NMCSProvider *provider, NMCSProviderGetConfigTaskData *get_config_data)
{
    gs_free const char *uri = NULL;

    nm_http_client_poll_req(nmcs_provider_get_http_client(provider),
                            (uri = _oci_uri_concat("vnics")),
                            HTTP_TIMEOUT_MS,
                            256 * 1024,
                            15000,
                            1000,
                            NM_MAKE_STRV(NM_OCI_HEADER),
                            NULL,
                            get_config_data->intern_cancellable,
                            NULL,
                            NULL,
                            _get_config_done_cb,
                            get_config_data);
}

/*****************************************************************************/

static void
nmcs_provider_oci_init(NMCSProviderOCI *self)
{}

static void
dispose(GObject *object)
{
    G_OBJECT_CLASS(nmcs_provider_oci_parent_class)->dispose(object);
}

static void
nmcs_provider_oci_class_init(NMCSProviderOCIClass *klass)
{
    GObjectClass      *object_class   = G_OBJECT_CLASS(klass);
    NMCSProviderClass *provider_class = NMCS_PROVIDER_CLASS(klass);

    object_class->dispose = dispose;

    provider_class->_name                 = "oci";
    provider_class->_env_provider_enabled = NMCS_ENV_NM_CLOUD_SETUP_OCI;
    provider_class->detect                = detect;
    provider_class->get_config            = get_config;
}
