/* SPDX-License-Identifier: LGPL-2.1-or-later */
/*
 * Copyright (C) 2019 Red Hat, Inc.
 */

#include "libnm-glib-aux/nm-default-glib-i18n-lib.h"

#include "nm-libnm-core-aux.h"

#include "libnm-core-aux-intern/nm-libnm-core-utils.h"

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

typedef enum {
    KEY_TYPE_STRING,
    KEY_TYPE_INT,
    KEY_TYPE_BOOL,
} KeyType;

typedef struct {
    const char *str_val;
    union {
        int  vint;
        bool vbool;
    } typ_val;
} ParseData;

typedef struct {
    const char *          name;
    NMTeamLinkWatcherType watcher_type;
    KeyType               key_type;
    union {
        int (*fint)(const NMTeamLinkWatcher *watcher);
        gboolean (*fbool)(const NMTeamLinkWatcher *watcher);
        const char *(*fstring)(const NMTeamLinkWatcher *watcher);
    } get_fcn;
    union {
        int  vint;
        bool vbool;
    } def_val;
} TeamLinkWatcherKeyInfo;

static gboolean
_team_link_watcher_validate_active(const NMTeamLinkWatcher *watcher)
{
    return NM_FLAGS_HAS(nm_team_link_watcher_get_flags(watcher),
                        NM_TEAM_LINK_WATCHER_ARP_PING_FLAG_VALIDATE_ACTIVE);
}

static gboolean
_team_link_watcher_validate_inactive(const NMTeamLinkWatcher *watcher)
{
    return NM_FLAGS_HAS(nm_team_link_watcher_get_flags(watcher),
                        NM_TEAM_LINK_WATCHER_ARP_PING_FLAG_VALIDATE_INACTIVE);
}

static gboolean
_team_link_watcher_send_always(const NMTeamLinkWatcher *watcher)
{
    return NM_FLAGS_HAS(nm_team_link_watcher_get_flags(watcher),
                        NM_TEAM_LINK_WATCHER_ARP_PING_FLAG_SEND_ALWAYS);
}

static const TeamLinkWatcherKeyInfo _team_link_watcher_key_infos[_NM_TEAM_LINK_WATCHER_KEY_NUM] = {

#define _KEY_INFO(key_id, _name, _watcher_type, _key_type, ...) \
    [key_id] = {.name = ""_name                                 \
                        "",                                     \
                .watcher_type = (_watcher_type),                \
                .key_type     = _key_type,                      \
                ##__VA_ARGS__}

    _KEY_INFO(NM_TEAM_LINK_WATCHER_KEY_NAME,
              "name",
              NM_TEAM_LINK_WATCHER_TYPE_ETHTOOL | NM_TEAM_LINK_WATCHER_TYPE_NSNAPING
                  | NM_TEAM_LINK_WATCHER_TYPE_ARPING,
              KEY_TYPE_STRING,
              .get_fcn.fstring = nm_team_link_watcher_get_name, ),
    _KEY_INFO(NM_TEAM_LINK_WATCHER_KEY_DELAY_UP,
              "delay-up",
              NM_TEAM_LINK_WATCHER_TYPE_ETHTOOL,
              KEY_TYPE_INT,
              .get_fcn.fint = nm_team_link_watcher_get_delay_up, ),
    _KEY_INFO(NM_TEAM_LINK_WATCHER_KEY_DELAY_DOWN,
              "delay-down",
              NM_TEAM_LINK_WATCHER_TYPE_ETHTOOL,
              KEY_TYPE_INT,
              .get_fcn.fint = nm_team_link_watcher_get_delay_down, ),
    _KEY_INFO(NM_TEAM_LINK_WATCHER_KEY_INIT_WAIT,
              "init-wait",
              NM_TEAM_LINK_WATCHER_TYPE_NSNAPING | NM_TEAM_LINK_WATCHER_TYPE_ARPING,
              KEY_TYPE_INT,
              .get_fcn.fint = nm_team_link_watcher_get_init_wait, ),
    _KEY_INFO(NM_TEAM_LINK_WATCHER_KEY_INTERVAL,
              "interval",
              NM_TEAM_LINK_WATCHER_TYPE_NSNAPING | NM_TEAM_LINK_WATCHER_TYPE_ARPING,
              KEY_TYPE_INT,
              .get_fcn.fint = nm_team_link_watcher_get_interval, ),
    _KEY_INFO(NM_TEAM_LINK_WATCHER_KEY_MISSED_MAX,
              "missed-max",
              NM_TEAM_LINK_WATCHER_TYPE_NSNAPING | NM_TEAM_LINK_WATCHER_TYPE_ARPING,
              KEY_TYPE_INT,
              .get_fcn.fint = nm_team_link_watcher_get_missed_max,
              .def_val.vint = 3, ),
    _KEY_INFO(NM_TEAM_LINK_WATCHER_KEY_TARGET_HOST,
              "target-host",
              NM_TEAM_LINK_WATCHER_TYPE_NSNAPING | NM_TEAM_LINK_WATCHER_TYPE_ARPING,
              KEY_TYPE_STRING,
              .get_fcn.fstring = nm_team_link_watcher_get_target_host, ),
    _KEY_INFO(NM_TEAM_LINK_WATCHER_KEY_VLANID,
              "vlanid",
              NM_TEAM_LINK_WATCHER_TYPE_ARPING,
              KEY_TYPE_INT,
              .get_fcn.fint = nm_team_link_watcher_get_vlanid,
              .def_val.vint = -1, ),
    _KEY_INFO(NM_TEAM_LINK_WATCHER_KEY_SOURCE_HOST,
              "source-host",
              NM_TEAM_LINK_WATCHER_TYPE_ARPING,
              KEY_TYPE_STRING,
              .get_fcn.fstring = nm_team_link_watcher_get_source_host, ),
    _KEY_INFO(NM_TEAM_LINK_WATCHER_KEY_VALIDATE_ACTIVE,
              "validate-active",
              NM_TEAM_LINK_WATCHER_TYPE_ARPING,
              KEY_TYPE_BOOL,
              .get_fcn.fbool = _team_link_watcher_validate_active, ),
    _KEY_INFO(NM_TEAM_LINK_WATCHER_KEY_VALIDATE_INACTIVE,
              "validate-inactive",
              NM_TEAM_LINK_WATCHER_TYPE_ARPING,
              KEY_TYPE_BOOL,
              .get_fcn.fbool = _team_link_watcher_validate_inactive, ),
    _KEY_INFO(NM_TEAM_LINK_WATCHER_KEY_SEND_ALWAYS,
              "send-always",
              NM_TEAM_LINK_WATCHER_TYPE_ARPING,
              KEY_TYPE_BOOL,
              .get_fcn.fbool = _team_link_watcher_send_always, ),

};

static NMTeamLinkWatcherType
_team_link_watcher_get_watcher_type_from_name(const char *name)
{
    if (name) {
        if (nm_streq(name, NM_TEAM_LINK_WATCHER_ETHTOOL))
            return NM_TEAM_LINK_WATCHER_TYPE_ETHTOOL;
        if (nm_streq(name, NM_TEAM_LINK_WATCHER_NSNA_PING))
            return NM_TEAM_LINK_WATCHER_TYPE_NSNAPING;
        if (nm_streq(name, NM_TEAM_LINK_WATCHER_ARP_PING))
            return NM_TEAM_LINK_WATCHER_TYPE_ARPING;
    }
    return NM_TEAM_LINK_WATCHER_TYPE_NONE;
}

static const char *
_parse_data_get_str(const ParseData        parse_data[static _NM_TEAM_LINK_WATCHER_KEY_NUM],
                    NMTeamLinkWatcherKeyId key_id)
{
    nm_assert(_NM_INT_NOT_NEGATIVE(key_id) && key_id < _NM_TEAM_LINK_WATCHER_KEY_NUM);
    nm_assert(_team_link_watcher_key_infos[key_id].key_type == KEY_TYPE_STRING);

    return parse_data[key_id].str_val;
}

static int
_parse_data_get_int(const ParseData        parse_data[static _NM_TEAM_LINK_WATCHER_KEY_NUM],
                    NMTeamLinkWatcherKeyId key_id)
{
    nm_assert(_NM_INT_NOT_NEGATIVE(key_id) && key_id < _NM_TEAM_LINK_WATCHER_KEY_NUM);
    nm_assert(_team_link_watcher_key_infos[key_id].key_type == KEY_TYPE_INT);

    if (parse_data[key_id].str_val)
        return parse_data[key_id].typ_val.vint;
    return _team_link_watcher_key_infos[key_id].def_val.vint;
}

static int
_parse_data_get_bool(const ParseData        parse_data[static _NM_TEAM_LINK_WATCHER_KEY_NUM],
                     NMTeamLinkWatcherKeyId key_id)
{
    nm_assert(_NM_INT_NOT_NEGATIVE(key_id) && key_id < _NM_TEAM_LINK_WATCHER_KEY_NUM);
    nm_assert(_team_link_watcher_key_infos[key_id].key_type == KEY_TYPE_BOOL);

    if (parse_data[key_id].str_val)
        return parse_data[key_id].typ_val.vbool;
    return _team_link_watcher_key_infos[key_id].def_val.vbool;
}

char *
nm_utils_team_link_watcher_to_string(const NMTeamLinkWatcher *watcher)
{
    nm_auto_free_gstring GString *str = NULL;
    const char *                  name;
    NMTeamLinkWatcherType         watcher_type;
    NMTeamLinkWatcherKeyId        key_id;

    if (!watcher)
        return NULL;

    str = g_string_new(NULL);

    name = nm_team_link_watcher_get_name(watcher);
    g_string_append_printf(str, "name=%s", name ?: "");

    watcher_type = _team_link_watcher_get_watcher_type_from_name(name);

    for (key_id = 0; key_id < _NM_TEAM_LINK_WATCHER_KEY_NUM; key_id++) {
        const TeamLinkWatcherKeyInfo *info = &_team_link_watcher_key_infos[key_id];
        const char *                  vstr;
        int                           vint;
        bool                          vbool;

        nm_assert(
            info->name && info->name
            && NM_STRCHAR_ALL(info->name, ch, ((ch >= 'a' && ch <= 'z') || NM_IN_SET(ch, '-'))));
        nm_assert(NM_IN_SET(info->key_type, KEY_TYPE_STRING, KEY_TYPE_INT, KEY_TYPE_BOOL));

        if (key_id == NM_TEAM_LINK_WATCHER_KEY_NAME)
            continue;

        if (!NM_FLAGS_ALL(info->watcher_type, watcher_type))
            continue;

        switch (info->key_type) {
        case KEY_TYPE_STRING:
            vstr = info->get_fcn.fstring(watcher);
            if (vstr) {
                g_string_append_printf(nm_gstring_add_space_delimiter(str),
                                       "%s=%s",
                                       info->name,
                                       vstr);
            }
            break;
        case KEY_TYPE_INT:
            vint = info->get_fcn.fint(watcher);
            if (vint != info->def_val.vint) {
                g_string_append_printf(nm_gstring_add_space_delimiter(str),
                                       "%s=%d",
                                       info->name,
                                       vint);
            }
            break;
        case KEY_TYPE_BOOL:
            vbool = info->get_fcn.fbool(watcher);
            if (vbool != info->def_val.vbool) {
                g_string_append_printf(nm_gstring_add_space_delimiter(str),
                                       "%s=%s",
                                       info->name,
                                       vbool ? "true" : "false");
            }
            break;
        }
    }

    return g_string_free(g_steal_pointer(&str), FALSE);
}

NMTeamLinkWatcher *
nm_utils_team_link_watcher_from_string(const char *str, GError **error)
{
    gs_free const char **  tokens                                    = NULL;
    ParseData              parse_data[_NM_TEAM_LINK_WATCHER_KEY_NUM] = {};
    NMTeamLinkWatcherType  watcher_type;
    NMTeamLinkWatcherKeyId key_id;
    gsize                  i_token;
    NMTeamLinkWatcher *    watcher;
    int                    errsv;

    g_return_val_if_fail(str, NULL);
    g_return_val_if_fail(!error || !*error, NULL);

    tokens = nm_utils_escaped_tokens_split(str, NM_ASCII_SPACES);
    if (!tokens) {
        g_set_error(error, 1, 0, "'%s' is not valid", str);
        return NULL;
    }

    for (i_token = 0; tokens[i_token]; i_token++) {
        const TeamLinkWatcherKeyInfo *info;
        const char *                  key = tokens[i_token];
        const char *                  val;

        val = strchr(key, '=');
        if (!val) {
            nm_utils_error_set(
                error,
                NM_UTILS_ERROR_UNKNOWN,
                _("'%s' is not valid: properties should be specified as 'key=value'"),
                key);
            return NULL;
        }
        ((char *) val)[0] = '\0';
        val++;

        for (key_id = 0; key_id < _NM_TEAM_LINK_WATCHER_KEY_NUM; key_id++) {
            info = &_team_link_watcher_key_infos[key_id];
            if (nm_streq(key, info->name))
                break;
        }

        if (key_id == _NM_TEAM_LINK_WATCHER_KEY_NUM) {
            nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, _("'%s' is not a valid key"), key);
            return NULL;
        }

        if (parse_data[key_id].str_val) {
            nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, _("duplicate key '%s'"), key);
            return NULL;
        }

        parse_data[key_id].str_val = val;

        if (info->key_type == KEY_TYPE_INT) {
            gint64 v64;

            v64 = _nm_utils_ascii_str_to_int64(val, 10, G_MININT, G_MAXINT, G_MAXINT64);
            if (v64 == G_MAXINT64 && ((errsv = errno) != 0)) {
                if (errsv == ERANGE) {
                    nm_utils_error_set(error,
                                       NM_UTILS_ERROR_UNKNOWN,
                                       _("number for '%s' is out of range"),
                                       key);
                } else {
                    nm_utils_error_set(error,
                                       NM_UTILS_ERROR_UNKNOWN,
                                       _("value for '%s' must be a number"),
                                       key);
                }
                return NULL;
            }
            parse_data[key_id].typ_val.vint = v64;
        } else if (info->key_type == KEY_TYPE_BOOL) {
            int vbool;

            vbool = _nm_utils_ascii_str_to_bool(val, -1);
            if (vbool == -1) {
                nm_utils_error_set(error,
                                   NM_UTILS_ERROR_UNKNOWN,
                                   _("value for '%s' must be a boolean"),
                                   key);
                return NULL;
            }
            parse_data[key_id].typ_val.vbool = vbool;
        }
    }

    if (!parse_data[NM_TEAM_LINK_WATCHER_KEY_NAME].str_val) {
        nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, _("missing 'name' attribute"));
        return NULL;
    }

    watcher_type = _team_link_watcher_get_watcher_type_from_name(
        parse_data[NM_TEAM_LINK_WATCHER_KEY_NAME].str_val);
    if (watcher_type == NM_TEAM_LINK_WATCHER_TYPE_NONE) {
        nm_utils_error_set(error,
                           NM_UTILS_ERROR_UNKNOWN,
                           _("invalid 'name' \"%s\""),
                           parse_data[NM_TEAM_LINK_WATCHER_KEY_NAME].str_val);
        return NULL;
    }

    for (key_id = 0; key_id < _NM_TEAM_LINK_WATCHER_KEY_NUM; key_id++) {
        const TeamLinkWatcherKeyInfo *info = &_team_link_watcher_key_infos[key_id];

        if (!parse_data[key_id].str_val)
            continue;
        if (!NM_FLAGS_ALL(info->watcher_type, watcher_type)) {
            nm_utils_error_set(error,
                               NM_UTILS_ERROR_UNKNOWN,
                               _("attribute '%s' is invalid for \"%s\""),
                               info->name,
                               parse_data[NM_TEAM_LINK_WATCHER_KEY_NAME].str_val);
            return NULL;
        }
    }

    switch (watcher_type) {
    case NM_TEAM_LINK_WATCHER_TYPE_ETHTOOL:
        watcher = nm_team_link_watcher_new_ethtool(
            _parse_data_get_int(parse_data, NM_TEAM_LINK_WATCHER_KEY_DELAY_UP),
            _parse_data_get_int(parse_data, NM_TEAM_LINK_WATCHER_KEY_DELAY_DOWN),
            error);
        break;
    case NM_TEAM_LINK_WATCHER_TYPE_NSNAPING:
        watcher = nm_team_link_watcher_new_nsna_ping(
            _parse_data_get_int(parse_data, NM_TEAM_LINK_WATCHER_KEY_INIT_WAIT),
            _parse_data_get_int(parse_data, NM_TEAM_LINK_WATCHER_KEY_INTERVAL),
            _parse_data_get_int(parse_data, NM_TEAM_LINK_WATCHER_KEY_MISSED_MAX),
            _parse_data_get_str(parse_data, NM_TEAM_LINK_WATCHER_KEY_TARGET_HOST),
            error);
        break;
    default:
        nm_assert(watcher_type == NM_TEAM_LINK_WATCHER_TYPE_ARPING);
        watcher = nm_team_link_watcher_new_arp_ping2(
            _parse_data_get_int(parse_data, NM_TEAM_LINK_WATCHER_KEY_INIT_WAIT),
            _parse_data_get_int(parse_data, NM_TEAM_LINK_WATCHER_KEY_INTERVAL),
            _parse_data_get_int(parse_data, NM_TEAM_LINK_WATCHER_KEY_MISSED_MAX),
            _parse_data_get_int(parse_data, NM_TEAM_LINK_WATCHER_KEY_VLANID),
            _parse_data_get_str(parse_data, NM_TEAM_LINK_WATCHER_KEY_TARGET_HOST),
            _parse_data_get_str(parse_data, NM_TEAM_LINK_WATCHER_KEY_SOURCE_HOST),
            (NM_TEAM_LINK_WATCHER_ARP_PING_FLAG_NONE
             | (_parse_data_get_bool(parse_data, NM_TEAM_LINK_WATCHER_KEY_VALIDATE_ACTIVE)
                    ? NM_TEAM_LINK_WATCHER_ARP_PING_FLAG_VALIDATE_ACTIVE
                    : NM_TEAM_LINK_WATCHER_ARP_PING_FLAG_NONE)
             | (_parse_data_get_bool(parse_data, NM_TEAM_LINK_WATCHER_KEY_VALIDATE_INACTIVE)
                    ? NM_TEAM_LINK_WATCHER_ARP_PING_FLAG_VALIDATE_INACTIVE
                    : NM_TEAM_LINK_WATCHER_ARP_PING_FLAG_NONE)
             | (_parse_data_get_bool(parse_data, NM_TEAM_LINK_WATCHER_KEY_SEND_ALWAYS)
                    ? NM_TEAM_LINK_WATCHER_ARP_PING_FLAG_SEND_ALWAYS
                    : NM_TEAM_LINK_WATCHER_ARP_PING_FLAG_NONE)),
            error);
        break;
    }

#if NM_MORE_ASSERTS > 5
    if (watcher) {
        gs_free char *                  str2                        = NULL;
        nm_auto_unref_team_link_watcher NMTeamLinkWatcher *watcher2 = NULL;
        static _nm_thread_local int                        recursive;

        nm_assert(!error || !*error);
        if (recursive == 0) {
            recursive = 1;
            str2      = nm_utils_team_link_watcher_to_string(watcher);
            nm_assert(str2);
            watcher2 = nm_utils_team_link_watcher_from_string(str2, NULL);
            nm_assert(watcher2);
            nm_assert(nm_team_link_watcher_equal(watcher, watcher2));
            nm_assert(nm_team_link_watcher_equal(watcher2, watcher));
            nm_assert(recursive == 1);
            recursive = 0;
        }
    } else
        nm_assert(!error || *error);
#endif

    return watcher;
}
