/* 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 "libnm-core-aux-intern/nm-libnm-core-utils.h"

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

typedef struct {
    GMainLoop    *main_loop;
    GCancellable *cancellable;
    NMCSProvider *provider_result;
    guint         detect_count;
} 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;
    gboolean                      success;

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

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

    if (nm_utils_error_is_cancelled(error))
        return;

    dd = user_data;

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

    if (error) {
        _LOGI("provider %s not detected: %s", nmcs_provider_get_name(provider), error->message);
        if (dd->detect_count > 0) {
            /* wait longer. */
            return;
        }

        _LOGI("no provider detected");
        goto done;
    }

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

done:
    g_cancellable_cancel(dd->cancellable);
    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);
    dd->detect_count = 0;
    g_main_loop_quit(dd->main_loop);
}

static NMCSProvider *
_provider_detect(GCancellable *sigterm_cancellable)
{
    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          = {
                                 .cancellable     = cancellable,
                                 .main_loop       = main_loop,
                                 .detect_count    = 0,
                                 .provider_result = NULL,
    };
    const GType gtypes[] = {
        NMCS_TYPE_PROVIDER_EC2,
        NMCS_TYPE_PROVIDER_GCP,
        NMCS_TYPE_PROVIDER_AZURE,
        NMCS_TYPE_PROVIDER_ALIYUN,
    };
    int    i;
    gulong cancellable_signal_id;

    cancellable_signal_id = g_cancellable_connect(sigterm_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_cancellable, &cancellable_signal_id);
    return dd.provider_result;
}

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

static char **
_nmc_get_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 = nm_device_ethernet_get_permanent_hw_address(NM_DEVICE_ETHERNET(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 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 (!NM_IS_DEVICE_ETHERNET(device))
            continue;

        hwaddr_dev = nm_device_ethernet_get_permanent_hw_address(NM_DEVICE_ETHERNET(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_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)
{
    if (!nm_streq0(nm_connection_get_connection_type(connection), NM_SETTING_WIRED_SETTING_NAME))
        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);

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

    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);

    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(config_data->iproutes_len + !!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 < config_data->iproutes_len; ++i)
        g_ptr_array_add(routes_new, config_data->iproutes_arr[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_one(GCancellable                      *sigterm_cancellable,
            NMClient                          *nmc,
            const NMCSProviderGetConfigResult *result,
            guint                              idx)
{
    const NMCSProviderGetConfigIfaceData *config_data        = result->iface_datas_arr[idx];
    const char                           *hwaddr             = config_data->hwaddr;
    gs_unref_object NMDevice             *device             = NULL;
    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 = FALSE;
    gboolean                              maybe_no_preserved_external_ip;

    g_main_context_iteration(NULL, FALSE);

    if (g_cancellable_is_cancelled(sigterm_cancellable))
        return FALSE;

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

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

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

    try_count = 0;

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

    applied_connection = nmcs_device_get_applied_connection(device,
                                                            sigterm_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)) {
        _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));

    if (!nmcs_device_reapply(device,
                             sigterm_cancellable,
                             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 any_changes;
    }

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

    return any_changes;
}

static gboolean
_config_all(GCancellable                      *sigterm_cancellable,
            NMClient                          *nmc,
            const NMCSProviderGetConfigResult *result)
{
    gboolean any_changes = FALSE;
    guint    i;

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

    return any_changes;
}

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

static gboolean
sigterm_handler(gpointer user_data)
{
    GCancellable *sigterm_cancellable = user_data;

    if (!g_cancellable_is_cancelled(sigterm_cancellable)) {
        _LOGD("SIGTERM received");
        g_cancellable_cancel(user_data);
    } else
        _LOGD("SIGTERM received (again)");
    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;

    _nm_logging_enabled_init(g_getenv(NMCS_ENV_VARIABLE("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_source = nm_g_unix_signal_add_source(SIGTERM, sigterm_handler, sigterm_cancellable);

    provider = _provider_detect(sigterm_cancellable);
    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;
    }

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

    if (_config_all(sigterm_cancellable, 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;
}
