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

#include "libnm-client-aux-extern/nm-default-client.h"

#include "libnm-client-aux-extern/nm-libnm-aux.h"

#include <linux/rtnetlink.h>

#include "nm-cloud-setup-utils.h"
#include "nmcs-provider-ec2.h"
#include "nmcs-provider-gcp.h"
#include "nmcs-provider-azure.h"
#include "nmcs-provider-aliyun.h"
#include "nmcs-provider-oci.h"
#include "libnm-core-aux-intern/nm-libnm-core-utils.h"

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

typedef struct {
    GCancellable *cancellable;
    gboolean      enabled;
    gboolean      signal_received;
} SigTermData;

typedef struct {
    SigTermData  *sigterm_data;
    GMainLoop    *main_loop;
    GCancellable *cancellable;
    NMCSProvider *provider_result;
    guint         detect_count;
    gboolean      any_provider_enabled;
} ProviderDetectData;

static void
_provider_detect_cb(GObject *source, GAsyncResult *result, gpointer user_data)
{
    gs_unref_object NMCSProvider *provider = NMCS_PROVIDER(source);
    gs_free_error GError         *error    = NULL;
    ProviderDetectData           *dd       = user_data;
    gboolean                      success;

    nm_assert(dd->detect_count > 0);
    dd->detect_count--;

    success = nmcs_provider_detect_finish(provider, result, &error);

    nm_assert(success != (!!error));

    if (nm_utils_error_is_cancelled(error)) {
        _LOGD("provider %s detection cancelled", nmcs_provider_get_name(provider));
        goto out;
    }
    if (error) {
        if (nm_g_error_matches(error, NM_UTILS_ERROR, NM_UTILS_ERROR_NOT_READY)) {
            /* This error tells us, that the provider was not enabled in configuration. */
        } else
            dd->any_provider_enabled = TRUE;
        _LOGI("provider %s not detected: %s", nmcs_provider_get_name(provider), error->message);
        goto out;
    }

    _LOGI("provider %s detected", nmcs_provider_get_name(provider));
    dd->provider_result = g_steal_pointer(&provider);
    g_cancellable_cancel(dd->cancellable);

out:
    if (dd->detect_count == 0) {
        if (!dd->provider_result) {
            NMLogLevel level = LOGL_INFO;

            if (dd->any_provider_enabled && !dd->sigterm_data->signal_received)
                level = LOGL_WARN;

            _NMLOG(level, "no provider detected");
        }
        g_main_loop_quit(dd->main_loop);
    }
}

static void
_provider_detect_sigterm_cb(GCancellable *source, gpointer user_data)
{
    ProviderDetectData *dd = user_data;

    g_cancellable_cancel(dd->cancellable);
    g_clear_object(&dd->provider_result);
}

static NMCSProvider *
_provider_detect(SigTermData *sigterm_data)
{
    nm_auto_unref_gmainloop GMainLoop *main_loop   = g_main_loop_new(NULL, FALSE);
    gs_unref_object GCancellable      *cancellable = g_cancellable_new();
    gs_unref_object NMHttpClient      *http_client = NULL;
    ProviderDetectData                 dd          = {
                                 .sigterm_data         = sigterm_data,
                                 .cancellable          = cancellable,
                                 .main_loop            = main_loop,
                                 .detect_count         = 0,
                                 .provider_result      = NULL,
                                 .any_provider_enabled = FALSE,
    };
    const GType gtypes[] = {
        NMCS_TYPE_PROVIDER_EC2,
        NMCS_TYPE_PROVIDER_GCP,
        NMCS_TYPE_PROVIDER_AZURE,
        NMCS_TYPE_PROVIDER_ALIYUN,
        NMCS_TYPE_PROVIDER_OCI,
    };
    int    i;
    gulong cancellable_signal_id;

    cancellable_signal_id = g_cancellable_connect(sigterm_data->cancellable,
                                                  G_CALLBACK(_provider_detect_sigterm_cb),
                                                  &dd,
                                                  NULL);
    if (!cancellable_signal_id)
        goto out;

    http_client = nmcs_wait_for_objects_register(nm_http_client_new());

    for (i = 0; i < G_N_ELEMENTS(gtypes); i++) {
        NMCSProvider *provider;

        provider = g_object_new(gtypes[i], NMCS_PROVIDER_HTTP_CLIENT, http_client, NULL);
        nmcs_wait_for_objects_register(provider);

        _LOGD("start detecting %s provider...", nmcs_provider_get_name(provider));
        dd.detect_count++;
        nmcs_provider_detect(provider, cancellable, _provider_detect_cb, &dd);
    }

    if (dd.detect_count > 0)
        g_main_loop_run(main_loop);

out:
    nm_clear_g_signal_handler(sigterm_data->cancellable, &cancellable_signal_id);
    return dd.provider_result;
}

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

static const NMUtilsNamedValue *gl_interfaces_map = NULL;

static NMUtilsNamedValue *
_map_interfaces_parse(void)
{
    gs_free const char **split = NULL;
    NMUtilsNamedValue   *map_interfaces;
    const char          *env_var;
    gsize                i;
    gsize                j;
    gsize                alloc_len;

    env_var = g_getenv(NMCS_ENV_NM_CLOUD_SETUP_MAP_INTERFACES);

    if (nm_str_is_empty(env_var))
        return NULL;

    split = nm_strsplit_set_full(env_var, ";", NM_STRSPLIT_SET_FLAGS_STRSTRIP);

    alloc_len = NM_PTRARRAY_LEN(split) + 1u;

    map_interfaces = g_new(NMUtilsNamedValue, alloc_len);

    _LOGD("test: map interfaces via NM_CLOUD_SETUP_MAP_INTERFACES=\"%s\"", env_var);

    for (i = 0, j = 0; split && split[i]; i++) {
        NMUtilsNamedValue *m;
        const char        *str = split[i];
        char              *hwaddr;
        const char        *s;

        s = strchr(str, '=');
        if (!s || str == s)
            continue;

        hwaddr = nmcs_utils_hwaddr_normalize(&s[1], -1);
        if (!hwaddr)
            continue;

        nm_assert(j < alloc_len);
        m = &map_interfaces[j++];

        *m = (NMUtilsNamedValue) {
            .name      = g_strndup(str, s - str),
            .value_str = hwaddr,
        };

        _LOGD("test:  map \"%s\" -> %s", m->name, m->value_str);
    }

    nm_assert(j < alloc_len);
    map_interfaces[j++] = (NMUtilsNamedValue) {
        .name      = NULL,
        .value_str = NULL,
    };

    return g_steal_pointer(&map_interfaces);
}

static const char *
_device_get_hwaddr(NMDevice *device)
{
    const NMUtilsNamedValue *map = NULL;

    nm_assert(NM_IS_DEVICE_ETHERNET(device) || NM_IS_DEVICE_MACVLAN(device)
              || NM_IS_DEVICE_VLAN(device));

    if (NM_IS_DEVICE_ETHERNET(device)) {
        /* Ethernet interfaces in cloud environments are identified by their permanent
         * MAC address.
         *
         * For testing, we can set NMCS_ENV_NM_CLOUD_SETUP_MAP_INTERFACES
         * to a ';' separate list of "$INTERFACE=$HWADDR", which means that we
         * pretend that device with ip-interface "$INTERFACE" has the specified permanent
         * MAC address. */

        map = gl_interfaces_map;
        if (G_UNLIKELY(map)) {
            const char *const iface = nm_device_get_iface(NM_DEVICE(device));

            /* For testing, the device<->hwaddr is remapped and the actual permanent
             * MAC address of the device ignored. This mapping is configured via
             * NMCS_ENV_NM_CLOUD_SETUP_MAP_INTERFACES environment variable. */
            if (!iface)
                return NULL;

            for (; map->name; map++) {
                if (nm_streq(map->name, iface))
                    return map->value_str;
            }

            return NULL;
        }

        return nm_device_ethernet_get_permanent_hw_address(NM_DEVICE_ETHERNET(device));
    } else {
        return nm_device_get_hw_address(device);
    }
}

static char **
_nmc_get_ethernet_hwaddrs(NMClient *nmc)
{
    gs_unref_ptrarray GPtrArray *hwaddrs = NULL;
    const GPtrArray             *devices;
    char                       **hwaddrs_v;
    gs_free char                *str = NULL;
    guint                        i;

    devices = nm_client_get_devices(nmc);

    for (i = 0; i < devices->len; i++) {
        NMDevice   *device = devices->pdata[i];
        const char *hwaddr;
        char       *s;

        if (!NM_IS_DEVICE_ETHERNET(device))
            continue;

        if (nm_device_get_state(device) < NM_DEVICE_STATE_UNAVAILABLE)
            continue;

        hwaddr = _device_get_hwaddr(device);
        if (!hwaddr)
            continue;

        s = nmcs_utils_hwaddr_normalize(hwaddr, -1);
        if (!s)
            continue;

        if (!hwaddrs)
            hwaddrs = g_ptr_array_new_with_free_func(g_free);
        g_ptr_array_add(hwaddrs, s);
    }

    if (!hwaddrs) {
        _LOGD("found interfaces: none");
        return NULL;
    }

    g_ptr_array_add(hwaddrs, NULL);
    hwaddrs_v = (char **) g_ptr_array_free(g_steal_pointer(&hwaddrs), FALSE);

    _LOGD("found interfaces: %s", (str = g_strjoinv(", ", hwaddrs_v)));

    return hwaddrs_v;
}

static NMDevice *
_nmc_get_device_by_hwaddr(NMClient *nmc, const GType type_device, const char *hwaddr)
{
    const GPtrArray *devices;
    guint            i;

    devices = nm_client_get_devices(nmc);

    for (i = 0; i < devices->len; i++) {
        NMDevice     *device = devices->pdata[i];
        const char   *hwaddr_dev;
        gs_free char *s = NULL;

        if (!G_TYPE_CHECK_INSTANCE_TYPE(device, type_device))
            continue;

        hwaddr_dev = _device_get_hwaddr(device);
        if (!hwaddr_dev)
            continue;

        s = nmcs_utils_hwaddr_normalize(hwaddr_dev, -1);
        if (s && nm_streq(s, hwaddr))
            return device;
    }

    return NULL;
}

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

typedef struct {
    GMainLoop                   *main_loop;
    NMCSProviderGetConfigResult *result;
} GetConfigData;

static void
_get_config_cb(GObject *source, GAsyncResult *res, gpointer user_data)
{
    GetConfigData                                                            *data   = user_data;
    nm_auto_free_nmcs_provider_get_config_result NMCSProviderGetConfigResult *result = NULL;
    gs_free_error GError                                                     *error  = NULL;

    result = nmcs_provider_get_config_finish(NMCS_PROVIDER(source), res, &error);

    if (!result) {
        if (!nm_utils_error_is_cancelled(error))
            _LOGI("failure to get meta data: %s", error->message);
    } else
        _LOGD("meta data received");

    data->result = g_steal_pointer(&result);
    g_main_loop_quit(data->main_loop);
}

static NMCSProviderGetConfigResult *
_get_config(GCancellable *sigterm_cancellable, NMCSProvider *provider, NMClient *nmc)
{
    nm_auto_unref_gmainloop GMainLoop *main_loop = g_main_loop_new(NULL, FALSE);
    GetConfigData                      data      = {
                                  .main_loop = main_loop,
    };
    gs_strfreev char **hwaddrs = NULL;

    hwaddrs = _nmc_get_ethernet_hwaddrs(nmc);

    nmcs_provider_get_config(provider,
                             TRUE,
                             (const char *const *) hwaddrs,
                             sigterm_cancellable,
                             _get_config_cb,
                             &data);

    g_main_loop_run(main_loop);

    return data.result;
}

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

static gboolean
_nmc_skip_connection_by_user_data(NMConnection *connection)
{
    NMSettingUser *s_user;
    const char    *v;

#define USER_TAG_SKIP "org.freedesktop.nm-cloud-setup.skip"

    nm_assert(nm_setting_user_check_key(USER_TAG_SKIP, NULL));

    s_user = NM_SETTING_USER(nm_connection_get_setting(connection, NM_TYPE_SETTING_USER));
    if (s_user) {
        v = nm_setting_user_get_data(s_user, USER_TAG_SKIP);
        if (_nm_utils_ascii_str_to_bool(v, FALSE))
            return TRUE;
    }

    return FALSE;
}

static gboolean
_nmc_skip_connection_by_type(NMConnection *connection, const char *connection_type)
{
    if (!nm_streq0(nm_connection_get_connection_type(connection), connection_type))
        return TRUE;
    if (!nm_connection_get_setting_ip4_config(connection))
        return TRUE;

    return FALSE;
}

static void
_nmc_mangle_connection(NMDevice                             *device,
                       NMConnection                         *connection,
                       const NMCSProviderGetConfigResult    *result,
                       const NMCSProviderGetConfigIfaceData *config_data,
                       gboolean                             *out_skipped_single_addr,
                       gboolean                             *out_changed)
{
    NMSettingIPConfig           *s_ip;
    NMActiveConnection          *ac;
    NMConnection                *remote_connection;
    NMSettingIPConfig           *remote_s_ip = NULL;
    gsize                        i;
    gboolean                     addrs_changed  = FALSE;
    gboolean                     rules_changed  = FALSE;
    gboolean                     routes_changed = FALSE;
    gs_unref_ptrarray GPtrArray *addrs_new      = NULL;
    gs_unref_ptrarray GPtrArray *rules_new      = NULL;
    gs_unref_ptrarray GPtrArray *routes_new     = NULL;

    NM_SET_OUT(out_skipped_single_addr, FALSE);
    NM_SET_OUT(out_changed, FALSE);

    if (nm_streq(nm_connection_get_connection_type(connection), NM_SETTING_MACVLAN_SETTING_NAME)) {
        /* The MACVLAN just sits in between, no L3 configuration on it */
        return;
    } else if (!nm_streq(nm_connection_get_connection_type(connection),
                         NM_SETTING_VLAN_SETTING_NAME)) {
        /* Preserve existing L3 configuration if not a VLAN */
        if (device) {
            if ((ac = nm_device_get_active_connection(device))
                && (remote_connection = NM_CONNECTION(nm_active_connection_get_connection(ac))))
                remote_s_ip = nm_connection_get_setting_ip4_config(remote_connection);
        }
    }

    s_ip = nm_connection_get_setting_ip4_config(connection);
    nm_assert(NM_IS_SETTING_IP4_CONFIG(s_ip));

    addrs_new = g_ptr_array_new_full(config_data->ipv4s_len, (GDestroyNotify) nm_ip_address_unref);
    rules_new =
        g_ptr_array_new_full(config_data->ipv4s_len, (GDestroyNotify) nm_ip_routing_rule_unref);
    routes_new =
        g_ptr_array_new_full(nm_g_ptr_array_len(config_data->iproutes) + !!config_data->ipv4s_len,
                             (GDestroyNotify) nm_ip_route_unref);

    if (remote_s_ip) {
        guint len;
        guint j;

        len = nm_setting_ip_config_get_num_addresses(remote_s_ip);
        for (j = 0; j < len; j++) {
            g_ptr_array_add(addrs_new,
                            nm_ip_address_dup(nm_setting_ip_config_get_address(remote_s_ip, j)));
        }

        len = nm_setting_ip_config_get_num_routes(remote_s_ip);
        for (j = 0; j < len; j++) {
            g_ptr_array_add(routes_new,
                            nm_ip_route_dup(nm_setting_ip_config_get_route(remote_s_ip, j)));
        }

        len = nm_setting_ip_config_get_num_routing_rules(remote_s_ip);
        for (j = 0; j < len; j++) {
            g_ptr_array_add(
                rules_new,
                nm_ip_routing_rule_ref(nm_setting_ip_config_get_routing_rule(remote_s_ip, j)));
        }
    }

    if (result->num_valid_ifaces <= 1 && result->num_ipv4s <= 1) {
        /* this setup only has one interface and one IPv4 address (or less).
         * We don't need to configure policy routing in this case. */
        NM_SET_OUT(out_skipped_single_addr, TRUE);
    } else if (config_data->has_ipv4s && config_data->has_cidr) {
        gs_unref_hashtable GHashTable *unique_subnets = g_hash_table_new(nm_direct_hash, NULL);
        NMIPAddress                   *addr_entry;
        NMIPRoute                     *route_entry;
        NMIPRoutingRule               *rule_entry;
        in_addr_t                      gateway;
        char                           sbuf[NM_INET_ADDRSTRLEN];

        for (i = 0; i < config_data->ipv4s_len; i++) {
            addr_entry = nm_ip_address_new_binary(AF_INET,
                                                  &config_data->ipv4s_arr[i],
                                                  config_data->cidr_prefix,
                                                  NULL);
            nm_assert(addr_entry);
            g_ptr_array_add(addrs_new, addr_entry);
        }

        if (config_data->has_gateway && config_data->gateway) {
            gateway = config_data->gateway;
        } else {
            gateway =
                nm_ip4_addr_clear_host_address(config_data->cidr_addr, config_data->cidr_prefix);
            if (config_data->cidr_prefix < 32)
                ((guint8 *) &gateway)[3] += 1;
        }

        for (i = 0; i < config_data->ipv4s_len; i++) {
            in_addr_t a = config_data->ipv4s_arr[i];

            a = nm_ip4_addr_clear_host_address(a, config_data->cidr_prefix);

            G_STATIC_ASSERT_EXPR(sizeof(gsize) >= sizeof(in_addr_t));
            if (g_hash_table_add(unique_subnets, GSIZE_TO_POINTER(a))) {
                route_entry =
                    nm_ip_route_new_binary(AF_INET, &a, config_data->cidr_prefix, NULL, 10, NULL);
                nm_ip_route_set_attribute(route_entry,
                                          NM_IP_ROUTE_ATTRIBUTE_TABLE,
                                          g_variant_new_uint32(30200 + config_data->iface_idx));
                g_ptr_array_add(routes_new, route_entry);
            }

            rule_entry = nm_ip_routing_rule_new(AF_INET);
            nm_ip_routing_rule_set_priority(rule_entry, 30200 + config_data->iface_idx);
            nm_ip_routing_rule_set_from(rule_entry,
                                        nm_inet4_ntop(config_data->ipv4s_arr[i], sbuf),
                                        32);
            nm_ip_routing_rule_set_table(rule_entry, 30200 + config_data->iface_idx);
            nm_assert(nm_ip_routing_rule_validate(rule_entry, NULL));
            g_ptr_array_add(rules_new, rule_entry);
        }

        rule_entry = nm_ip_routing_rule_new(AF_INET);
        nm_ip_routing_rule_set_priority(rule_entry, 30350);
        nm_ip_routing_rule_set_table(rule_entry, RT_TABLE_MAIN);
        nm_ip_routing_rule_set_suppress_prefixlength(rule_entry, 0);
        nm_assert(nm_ip_routing_rule_validate(rule_entry, NULL));
        g_ptr_array_add(rules_new, rule_entry);

        route_entry = nm_ip_route_new_binary(AF_INET, &nm_ip_addr_zero, 0, &gateway, 10, NULL);
        nm_ip_route_set_attribute(route_entry,
                                  NM_IP_ROUTE_ATTRIBUTE_TABLE,
                                  g_variant_new_uint32(30400 + config_data->iface_idx));
        g_ptr_array_add(routes_new, route_entry);

        for (i = 0; i < config_data->ipv4s_len; i++) {
            rule_entry = nm_ip_routing_rule_new(AF_INET);
            nm_ip_routing_rule_set_priority(rule_entry, 30400 + config_data->iface_idx);
            nm_ip_routing_rule_set_from(rule_entry,
                                        nm_inet4_ntop(config_data->ipv4s_arr[i], sbuf),
                                        32);
            nm_ip_routing_rule_set_table(rule_entry, 30400 + config_data->iface_idx);
            nm_assert(nm_ip_routing_rule_validate(rule_entry, NULL));
            g_ptr_array_add(rules_new, rule_entry);
        }
    }

    for (i = 0; i < nm_g_ptr_array_len(config_data->iproutes); i++)
        g_ptr_array_add(routes_new, _nm_ip_route_ref(config_data->iproutes->pdata[i]));

    addrs_changed = nmcs_setting_ip_replace_ipv4_addresses(s_ip,
                                                           (NMIPAddress **) addrs_new->pdata,
                                                           addrs_new->len);

    routes_changed = nmcs_setting_ip_replace_ipv4_routes(s_ip,
                                                         (NMIPRoute **) routes_new->pdata,
                                                         routes_new->len);

    rules_changed = nmcs_setting_ip_replace_ipv4_rules(s_ip,
                                                       (NMIPRoutingRule **) rules_new->pdata,
                                                       rules_new->len);

    NM_SET_OUT(out_changed, addrs_changed || routes_changed || rules_changed);
}

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

static gboolean
_config_existing(SigTermData                          *sigterm_data,
                 const NMCSProviderGetConfigIfaceData *config_data,
                 NMClient                             *nmc,
                 const NMCSProviderGetConfigResult    *result,
                 const char                           *connection_type,
                 NMDevice                             *device)
{
    const char                   *hwaddr             = config_data->hwaddr;
    gs_unref_object NMConnection *applied_connection = NULL;
    guint64                       applied_version_id;
    gs_free_error GError         *error = NULL;
    gboolean                      changed;
    gboolean                      skipped_single_addr;
    gboolean                      version_id_changed;
    guint                         try_count;
    gboolean                      any_changes;
    gboolean                      maybe_no_preserved_external_ip;

    _LOGD("config device %s: configuring \"%s\" (%s)...",
          hwaddr,
          nm_device_get_iface(device) ?: "/unknown/",
          nm_object_get_path(NM_OBJECT(device)));

    try_count   = 0;
    any_changes = FALSE;

try_again:
    g_clear_object(&applied_connection);
    g_clear_error(&error);

    applied_connection = nmcs_device_get_applied_connection(device,
                                                            sigterm_data->cancellable,
                                                            &applied_version_id,
                                                            &error);
    if (!applied_connection) {
        if (!nm_utils_error_is_cancelled(error))
            _LOGD("config device %s: device has no applied connection (%s). Skip",
                  hwaddr,
                  error->message);
        return any_changes;
    }

    if (_nmc_skip_connection_by_user_data(applied_connection)) {
        _LOGD("config device %s: skip applied connection due to user data %s",
              hwaddr,
              USER_TAG_SKIP);
        return any_changes;
    }

    if (_nmc_skip_connection_by_type(applied_connection, connection_type)) {
        _LOGD("config device %s: device has no suitable applied connection. Skip", hwaddr);
        return any_changes;
    }

    _nmc_mangle_connection(device,
                           applied_connection,
                           result,
                           config_data,
                           &skipped_single_addr,
                           &changed);

    if (!changed) {
        if (skipped_single_addr) {
            _LOGD("config device %s: device needs no update to applied connection \"%s\" (%s) "
                  "because there are not multiple IP addresses. Skip",
                  hwaddr,
                  nm_connection_get_id(applied_connection),
                  nm_connection_get_uuid(applied_connection));
        } else {
            _LOGD(
                "config device %s: device needs no update to applied connection \"%s\" (%s). Skip",
                hwaddr,
                nm_connection_get_id(applied_connection),
                nm_connection_get_uuid(applied_connection));
        }
        return any_changes;
    }

    _LOGD("config device %s: reapply connection \"%s\" (%s)",
          hwaddr,
          nm_connection_get_id(applied_connection),
          nm_connection_get_uuid(applied_connection));

    /* we are about to call Reapply(). Even if that fails, it counts as if we changed something. */
    any_changes = TRUE;

    /* "preserve-external-ip" flag was only introduced in 1.41.6 (but maybe backported!).
     * If we run 1.41.6+, we are sure that it's gonna work. Otherwise, we take into account
     * that the call might fail due to the invalid flag and we retry. */
    maybe_no_preserved_external_ip =
        (nmc_client_has_version_info_v(nmc) < NM_ENCODE_VERSION(1, 41, 6));

    /* Once we start reconfiguring the system, we cannot abort in the middle. From now on,
     * any SIGTERM gets ignored until we are done.  */
    sigterm_data->enabled = FALSE;

    if (!nmcs_device_reapply(device,
                             NULL,
                             applied_connection,
                             applied_version_id,
                             maybe_no_preserved_external_ip,
                             &version_id_changed,
                             &error)) {
        if (version_id_changed && try_count < 5) {
            _LOGD("config device %s: applied connection changed in the meantime. Retry...", hwaddr);
            try_count++;
            goto try_again;
        }

        if (!nm_utils_error_is_cancelled(error)) {
            _LOGD("config device %s: failure to reapply connection \"%s\" (%s): %s",
                  hwaddr,
                  nm_connection_get_id(applied_connection),
                  nm_connection_get_uuid(applied_connection),
                  error->message);
        }
        return TRUE;
    }

    _LOGD("config device %s: connection \"%s\" (%s) reapplied",
          hwaddr,
          nm_connection_get_id(applied_connection),
          nm_connection_get_uuid(applied_connection));

    return TRUE;
}

static gboolean
_config_ethernet(SigTermData                          *sigterm_data,
                 const NMCSProviderGetConfigIfaceData *config_data,
                 NMClient                             *nmc,
                 const NMCSProviderGetConfigResult    *result)
{
    gs_unref_object NMDevice *device = NULL;

    device = nm_g_object_ref(
        _nmc_get_device_by_hwaddr(nmc, NM_TYPE_DEVICE_ETHERNET, config_data->hwaddr));
    if (!device) {
        _LOGD("config device %s: skip because device not found", config_data->hwaddr);
        return FALSE;
    }

    return _config_existing(sigterm_data,
                            config_data,
                            nmc,
                            result,
                            NM_SETTING_WIRED_SETTING_NAME,
                            device);
}

static gboolean
_oci_new_vlan_dev(SigTermData                          *sigterm_data,
                  const NMCSProviderGetConfigIfaceData *config_data,
                  NMClient                             *nmc,
                  const NMCSProviderGetConfigResult    *result,
                  const char                           *connection_type,
                  const char                           *parent_hwaddr)
{
    const char                         *hwaddr            = config_data->hwaddr;
    gs_unref_object NMConnection       *connection        = NULL;
    gs_unref_object NMActiveConnection *active_connection = NULL;
    gs_free_error GError               *error             = NULL;
    gs_free char                       *macvlan_name      = NULL;
    gs_free char                       *connection_id     = NULL;
    char                               *ifname            = NULL;
    const char                         *macvlan_parent    = NULL;
    const char                         *wired_mac_addr    = NULL;
    const NMUtilsNamedValue            *map               = NULL;
    const char                         *ip4_config_method;
    NMSetting                          *s_user;

    connection = nm_simple_connection_new();

    macvlan_name  = g_strdup_printf("macvlan%ld", config_data->iface_idx);
    connection_id = g_strdup_printf("%s%ld", connection_type, config_data->iface_idx);

    wired_mac_addr = parent_hwaddr;
    if (nm_streq(connection_type, NM_SETTING_MACVLAN_SETTING_NAME)) {
        /* In NM-ci, use macvlan.parent instead of wired.mac-address for parent matching
         * because we are faking the MAC addresses via NM_CLOUD_SETUP_MAP_INTERFACES.
         * The daemon still needs the real MAC, not the mapped one, so it won't work. */
        map = gl_interfaces_map;
        if (G_UNLIKELY(map)) {
            for (; map->name; map++) {
                if (nm_streq(map->value_str, parent_hwaddr)) {
                    macvlan_parent = map->name;
                    wired_mac_addr = NULL;
                    break;
                }
            }
        }

        nm_connection_add_setting(connection,
                                  g_object_new(NM_TYPE_SETTING_MACVLAN,
                                               NM_SETTING_MACVLAN_MODE,
                                               NM_SETTING_MACVLAN_MODE_VEPA,
                                               NM_SETTING_MACVLAN_PARENT,
                                               macvlan_parent,
                                               NULL));
        nm_connection_add_setting(connection,
                                  g_object_new(NM_TYPE_SETTING_IP6_CONFIG,
                                               NM_SETTING_IP_CONFIG_METHOD,
                                               NM_SETTING_IP6_CONFIG_METHOD_DISABLED,
                                               NULL));
        ip4_config_method = NM_SETTING_IP4_CONFIG_METHOD_DISABLED;
        ifname            = macvlan_name;
    } else if (nm_streq(connection_type, NM_SETTING_VLAN_SETTING_NAME)) {
        nm_connection_add_setting(connection,
                                  g_object_new(NM_TYPE_SETTING_VLAN,
                                               NM_SETTING_VLAN_PARENT,
                                               macvlan_name,
                                               NM_SETTING_VLAN_ID,
                                               config_data->priv.oci.vlan_tag,
                                               NULL));
        ip4_config_method = NM_SETTING_IP4_CONFIG_METHOD_MANUAL;
    } else {
        g_return_val_if_reached(FALSE);
    }

    nm_connection_add_setting(connection,
                              g_object_new(NM_TYPE_SETTING_CONNECTION,
                                           NM_SETTING_CONNECTION_ID,
                                           connection_id,
                                           NM_SETTING_CONNECTION_TYPE,
                                           connection_type,
                                           NM_SETTING_CONNECTION_INTERFACE_NAME,
                                           ifname,
                                           NULL));
    nm_connection_add_setting(connection,
                              g_object_new(NM_TYPE_SETTING_IP4_CONFIG,
                                           NM_SETTING_IP_CONFIG_METHOD,
                                           ip4_config_method,
                                           NULL));

    nm_connection_add_setting(connection,
                              g_object_new(NM_TYPE_SETTING_WIRED,
                                           NM_SETTING_WIRED_MAC_ADDRESS,
                                           wired_mac_addr,
                                           NM_SETTING_WIRED_CLONED_MAC_ADDRESS,
                                           hwaddr,
                                           NULL));

    s_user = nm_setting_user_new();
    nm_connection_add_setting(connection, s_user);
    nm_setting_user_set_data(NM_SETTING_USER(s_user),
                             "org.freedesktop.NetworkManager.origin",
                             "nm-cloud-setup",
                             NULL);

    _nmc_mangle_connection(NULL, connection, result, config_data, NULL, NULL);

    _LOGD("config device %s: creating %s connection for VLAN %d on %s...",
          hwaddr,
          ifname ?: connection_type,
          config_data->priv.oci.vlan_tag,
          parent_hwaddr);

    active_connection = nmcs_add_and_activate(nmc, NULL, connection, &error);
    if (active_connection == NULL) {
        if (!nm_utils_error_is_cancelled(error)) {
            _LOGD("config device %s: failure to activate connection: %s", hwaddr, error->message);
        }
        return FALSE;
    }

    _LOGD("config device %s: connection \"%s\" (%s) created",
          hwaddr,
          nm_active_connection_get_id(active_connection),
          nm_active_connection_get_uuid(active_connection));

    return TRUE;
}

static gboolean
_oci_config_vnic_dev(SigTermData                          *sigterm_data,
                     const NMCSProviderGetConfigIfaceData *config_data,
                     NMClient                             *nmc,
                     const NMCSProviderGetConfigResult    *result,
                     const GType                           device_type,
                     const char                           *connection_type,
                     const char                           *parent_hwaddr)
{
    gs_unref_object NMDevice *device = NULL;

    device = nm_g_object_ref(_nmc_get_device_by_hwaddr(nmc, device_type, config_data->hwaddr));
    if (device) {
        /* There is a device. Modify and reapply the currently applied connection. */
        return _config_existing(sigterm_data, config_data, nmc, result, connection_type, device);
    } else {
        /* There is no device, but we're configuring a VLAN.
         * We can just go ahead and create one with a new connection. */
        return _oci_new_vlan_dev(sigterm_data,
                                 config_data,
                                 nmc,
                                 result,
                                 connection_type,
                                 parent_hwaddr);
    }
}

static gboolean
_config_one(SigTermData                       *sigterm_data,
            NMCSProvider                      *provider,
            NMClient                          *nmc,
            const NMCSProviderGetConfigResult *result,
            guint                              idx)
{
    const NMCSProviderGetConfigIfaceData *config_data = result->iface_datas_arr[idx];
    gboolean                              any_changes;

    g_main_context_iteration(NULL, FALSE);

    if (g_cancellable_is_cancelled(sigterm_data->cancellable))
        return FALSE;

    if (!nmcs_provider_get_config_iface_data_is_valid(config_data)) {
        _LOGD("config device %s: skip because meta data not successfully fetched",
              config_data->hwaddr);
        return FALSE;
    }

    if (config_data->iface_idx >= 100) {
        /* since we use the iface_idx to select a table number, the range is limited from
         * 0 to 99. Note that the providers are required to provide increasing numbers,
         * so this means we bail out after the first 100 devices.  */
        _LOGD("config device %s: skip because number of supported interfaces reached",
              config_data->hwaddr);
        return FALSE;
    }

    if (NMCS_IS_PROVIDER_OCI(provider) && config_data->priv.oci.vlan_tag != 0) {
        if (config_data->priv.oci.parent_hwaddr == NULL) {
            _LOGW("config device %s: has vlan id %d but no parent device",
                  config_data->hwaddr,
                  config_data->priv.oci.vlan_tag);
            return FALSE;
        }

        /* MACVLAN first, because VLAN is on top of it. */
        any_changes = _oci_config_vnic_dev(sigterm_data,
                                           config_data,
                                           nmc,
                                           result,
                                           NM_TYPE_DEVICE_MACVLAN,
                                           NM_SETTING_MACVLAN_SETTING_NAME,
                                           config_data->priv.oci.parent_hwaddr);
        any_changes += _oci_config_vnic_dev(sigterm_data,
                                            config_data,
                                            nmc,
                                            result,
                                            NM_TYPE_DEVICE_VLAN,
                                            NM_SETTING_VLAN_SETTING_NAME,
                                            config_data->hwaddr);

    } else {
        any_changes = _config_ethernet(sigterm_data, config_data, nmc, result);
    }

    return any_changes;
}

static gboolean
_config_all(SigTermData                       *sigterm_data,
            NMCSProvider                      *provider,
            NMClient                          *nmc,
            const NMCSProviderGetConfigResult *result)
{
    gboolean any_changes = FALSE;
    guint    i;

    for (i = 0; i < result->n_iface_datas; i++) {
        if (_config_one(sigterm_data, provider, nmc, result, i))
            any_changes = TRUE;
    }

    return any_changes;
}

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

static gboolean
sigterm_handler(gpointer user_data)
{
    SigTermData *sigterm_data = user_data;

    _LOGD("SIGTERM received (%s) (%s)",
          sigterm_data->signal_received ? "first time" : "again",
          sigterm_data->enabled ? "cancel operation" : "ignore");

    sigterm_data->signal_received = TRUE;

    if (sigterm_data->enabled)
        g_cancellable_cancel(sigterm_data->cancellable);
    return G_SOURCE_CONTINUE;
}

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

int
main(int argc, const char *const *argv)
{
    gs_unref_object GCancellable              *sigterm_cancellable                   = NULL;
    nm_auto_destroy_and_unref_gsource GSource *sigterm_source                        = NULL;
    gs_unref_object NMCSProvider              *provider                              = NULL;
    gs_unref_object NMClient                  *nmc                                   = NULL;
    nm_auto_free_nmcs_provider_get_config_result NMCSProviderGetConfigResult *result = NULL;
    gs_free_error GError                                                     *error  = NULL;
    SigTermData                                                               sigterm_data;

    _nm_logging_enabled_init(g_getenv(NMCS_ENV_NM_CLOUD_SETUP_LOG));

    _LOGD("nm-cloud-setup %s starting...", NM_DIST_VERSION);

    if (argc != 1) {
        g_printerr("%s: no command line arguments supported\n", argv[0]);
        return EXIT_FAILURE;
    }

    sigterm_cancellable = g_cancellable_new();

    sigterm_data = (SigTermData) {
        .cancellable     = sigterm_cancellable,
        .enabled         = TRUE,
        .signal_received = FALSE,
    };
    sigterm_source = nm_g_unix_signal_add_source(SIGTERM, sigterm_handler, &sigterm_data);

    provider = _provider_detect(&sigterm_data);
    if (!provider)
        goto done;

    nmc_client_new_waitsync(sigterm_cancellable,
                            &nmc,
                            &error,
                            NM_CLIENT_INSTANCE_FLAGS,
                            (guint) NM_CLIENT_INSTANCE_FLAGS_NO_AUTO_FETCH_PERMISSIONS,
                            NULL);

    nmcs_wait_for_objects_register(nmc);
    nmcs_wait_for_objects_register(nm_client_get_context_busy_watcher(nmc));

    if (error) {
        if (!nm_utils_error_is_cancelled(error))
            _LOGI("failure to talk to NetworkManager: %s", error->message);
        goto done;
    }

    if (!nm_client_get_nm_running(nmc)) {
        _LOGI("NetworkManager is not running");
        goto done;
    }

    /* Initialize map used in test scenarios. */
    gl_interfaces_map = _map_interfaces_parse();

    result = _get_config(sigterm_cancellable, provider, nmc);
    if (!result)
        goto done;

    if (_config_all(&sigterm_data, provider, nmc, result))
        _LOGI("some changes were applied for provider %s", nmcs_provider_get_name(provider));
    else
        _LOGD("no changes were applied for provider %s", nmcs_provider_get_name(provider));

done:
    nm_clear_pointer(&result, nmcs_provider_get_config_result_free);
    g_clear_object(&nmc);
    g_clear_object(&provider);

    if (!nmcs_wait_for_objects_iterate_until_done(NULL, 2000)) {
        _LOGE("shutdown: timeout waiting to application to quit. This is a bug");
        nm_assert_not_reached();
    }

    nm_clear_g_source_inst(&sigterm_source);
    g_clear_object(&sigterm_cancellable);

    return 0;
}
