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

#include "alloc-util.h"
#include "format-table.h"
#include "json-util.h"
#include "log.h"
#include "networkctl.h"
#include "networkctl-dump-util.h"
#include "networkctl-lldp.h"
#include "networkctl-util.h"
#include "stdio-util.h"
#include "string-util.h"
#include "strv.h"
#include "terminal-util.h"
#include "varlink-util.h"

typedef struct InterfaceInfo {
        int ifindex;
        const char *ifname;
        char **altnames;
        sd_json_variant *v;
} InterfaceInfo;

static void interface_info_done(InterfaceInfo *p) {
        if (!p)
                return;

        strv_free(p->altnames);
        sd_json_variant_unref(p->v);
}

static const sd_json_dispatch_field interface_info_dispatch_table[] = {
        { "InterfaceIndex",            _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_ifindex,         offsetof(InterfaceInfo, ifindex),  SD_JSON_MANDATORY|SD_JSON_RELAX },
        { "InterfaceName",             SD_JSON_VARIANT_STRING,        sd_json_dispatch_const_string, offsetof(InterfaceInfo, ifname),   SD_JSON_MANDATORY               },
        { "InterfaceAlternativeNames", SD_JSON_VARIANT_ARRAY,         sd_json_dispatch_strv,         offsetof(InterfaceInfo, altnames), 0                               },
        { "Neighbors",                 SD_JSON_VARIANT_ARRAY,         sd_json_dispatch_variant,      offsetof(InterfaceInfo, v),        0                               },
        {},
};

typedef struct LLDPNeighborInfo {
        const char *chassis_id;
        const char *port_id;
        const char *port_description;
        const char *system_name;
        const char *system_description;
        uint16_t capabilities;
        uint16_t vlan_id;
} LLDPNeighborInfo;

static const sd_json_dispatch_field lldp_neighbor_dispatch_table[] = {
        { "ChassisID",           SD_JSON_VARIANT_STRING,        sd_json_dispatch_const_string, offsetof(LLDPNeighborInfo, chassis_id),         0 },
        { "PortID",              SD_JSON_VARIANT_STRING,        sd_json_dispatch_const_string, offsetof(LLDPNeighborInfo, port_id),            0 },
        { "PortDescription",     SD_JSON_VARIANT_STRING,        sd_json_dispatch_const_string, offsetof(LLDPNeighborInfo, port_description),   0 },
        { "SystemName",          SD_JSON_VARIANT_STRING,        sd_json_dispatch_const_string, offsetof(LLDPNeighborInfo, system_name),        0 },
        { "SystemDescription",   SD_JSON_VARIANT_STRING,        sd_json_dispatch_const_string, offsetof(LLDPNeighborInfo, system_description), 0 },
        { "EnabledCapabilities", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint16,       offsetof(LLDPNeighborInfo, capabilities),       0 },
        { "VlanID",              _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint16,       offsetof(LLDPNeighborInfo, vlan_id),            0 },
        {},
};

int dump_lldp_neighbors(sd_varlink *vl, Table *table, int ifindex) {
        _cleanup_strv_free_ char **buf = NULL;
        sd_json_variant *reply;
        int r;

        assert(vl);
        assert(table);
        assert(ifindex > 0);

        r = varlink_callbo_and_log(
                        vl,
                        "io.systemd.Network.GetLLDPNeighbors",
                        &reply,
                        SD_JSON_BUILD_PAIR_INTEGER("InterfaceIndex", ifindex));
        if (r < 0)
                return r;

        sd_json_variant *i;
        JSON_VARIANT_ARRAY_FOREACH(i, sd_json_variant_by_key(reply, "Neighbors")) {
                _cleanup_(interface_info_done) InterfaceInfo info = {};

                r = sd_json_dispatch(i, interface_info_dispatch_table, SD_JSON_LOG|SD_JSON_ALLOW_EXTENSIONS, &info);
                if (r < 0)
                        return r;

                if (info.ifindex != ifindex)
                        continue;

                sd_json_variant *neighbor;
                JSON_VARIANT_ARRAY_FOREACH(neighbor, info.v) {
                        LLDPNeighborInfo neighbor_info = {};

                        r = sd_json_dispatch(neighbor, lldp_neighbor_dispatch_table, SD_JSON_LOG|SD_JSON_ALLOW_EXTENSIONS, &neighbor_info);
                        if (r < 0)
                                return r;

                        r = strv_extendf(&buf, "%s%s%s%s on port %s%s%s%s",
                                         strna(neighbor_info.system_name),
                                         isempty(neighbor_info.system_description) ? "" : " (",
                                         strempty(neighbor_info.system_description),
                                         isempty(neighbor_info.system_description) ? "" : ")",
                                         strna(neighbor_info.port_id),
                                         isempty(neighbor_info.port_description) ? "" : " (",
                                         strempty(neighbor_info.port_description),
                                         isempty(neighbor_info.port_description) ? "" : ")");
                        if (r < 0)
                                return log_oom();
                }
        }

        return dump_list(table, "Connected To", buf);
}

static char* lldp_capabilities_to_string(uint64_t x) {
        static const char characters[] = {
                'o', 'p', 'b', 'w', 'r', 't', 'd', 'a', 'c', 's', 'm',
        };
        char *ret;
        unsigned i;

        ret = new(char, ELEMENTSOF(characters) + 1);
        if (!ret)
                return NULL;

        for (i = 0; i < ELEMENTSOF(characters); i++)
                ret[i] = (x & (1U << i)) ? characters[i] : '.';

        ret[i] = 0;
        return ret;
}

static void lldp_capabilities_legend(uint16_t x) {
        unsigned cols = columns();
        static const char* const table[] = {
                "o - Other",
                "p - Repeater",
                "b - Bridge",
                "w - WLAN Access Point",
                "r - Router",
                "t - Telephone",
                "d - DOCSIS cable device",
                "a - Station",
                "c - Customer VLAN",
                "s - Service VLAN",
                "m - Two-port MAC Relay (TPMR)",
        };

        if (x == 0)
                return;

        printf("\nCapability Flags:\n");
        for (unsigned w = 0, i = 0; i < ELEMENTSOF(table); i++)
                if (x & (1U << i) || arg_all) {
                        bool newline;

                        newline = w + strlen(table[i]) + (w == 0 ? 0 : 2) > cols;
                        if (newline)
                                w = 0;
                        w += printf("%s%s%s", newline ? "\n" : "", w == 0 ? "" : "; ", table[i]);
                }
        puts("");
}

static bool interface_match_pattern(const InterfaceInfo *info, char * const *patterns) {
        assert(info);

        if (strv_isempty(patterns))
                return true;

        if (strv_fnmatch(patterns, info->ifname))
                return true;

        char str[DECIMAL_STR_MAX(int)];
        xsprintf(str, "%i", info->ifindex);
        if (strv_fnmatch(patterns, str))
                return true;

        STRV_FOREACH(a, info->altnames)
                if (strv_fnmatch(patterns, *a))
                        return true;

        return false;
}

static int dump_lldp_neighbors_json(sd_json_variant *reply, char * const *patterns) {
        _cleanup_(sd_json_variant_unrefp) sd_json_variant *array = NULL, *v = NULL;
        int r;

        assert(reply);

        if (strv_isempty(patterns))
                return sd_json_variant_dump(reply, arg_json_format_flags, NULL, NULL);

        /* Filter and dump the result. */

        sd_json_variant *i;
        JSON_VARIANT_ARRAY_FOREACH(i, sd_json_variant_by_key(reply, "Neighbors")) {
                _cleanup_(interface_info_done) InterfaceInfo info = {};

                r = sd_json_dispatch(i, interface_info_dispatch_table, SD_JSON_LOG|SD_JSON_ALLOW_EXTENSIONS, &info);
                if (r < 0)
                        return r;

                if (!interface_match_pattern(&info, patterns))
                        continue;

                r = sd_json_variant_append_array(&array, i);
                if (r < 0)
                        return log_error_errno(r, "Failed to append json variant to array: %m");
        }

        r = sd_json_buildo(
                        &v,
                        SD_JSON_BUILD_PAIR_CONDITION(sd_json_variant_is_blank_array(array), "Neighbors", SD_JSON_BUILD_EMPTY_ARRAY),
                        SD_JSON_BUILD_PAIR_CONDITION(!sd_json_variant_is_blank_array(array), "Neighbors", SD_JSON_BUILD_VARIANT(array)));
        if (r < 0)
                return log_error_errno(r, "Failed to build json varinat: %m");

        return sd_json_variant_dump(v, arg_json_format_flags, NULL, NULL);
}

int verb_link_lldp_status(int argc, char *argv[], uintptr_t _data, void *userdata) {
        _cleanup_(sd_varlink_flush_close_unrefp) sd_varlink *vl = NULL;
        _cleanup_(table_unrefp) Table *table = NULL;
        sd_json_variant *reply;
        uint64_t all = 0;
        TableCell *cell;
        size_t m = 0;
        int r;

        r = varlink_connect_networkd(&vl);
        if (r < 0)
                return r;

        r = varlink_call_and_log(vl, "io.systemd.Network.GetLLDPNeighbors", NULL, &reply);
        if (r < 0)
                return r;

        if (sd_json_format_enabled(arg_json_format_flags))
                return dump_lldp_neighbors_json(reply, strv_skip(argv, 1));

        pager_open(arg_pager_flags);

        table = table_new("index",
                          "link",
                          "system-name",
                          "system-description",
                          "chassis-id",
                          "port-id",
                          "port-description",
                          "caps");
        if (!table)
                return log_oom();

        if (arg_full)
                table_set_width(table, 0);

        table_set_header(table, arg_legend);
        table_set_ersatz_string(table, TABLE_ERSATZ_DASH);
        table_set_sort(table, (size_t) 0, (size_t) 2);
        table_hide_column_from_display(table, (size_t) 0);

        /* Make the capabilities not truncated */
        assert_se(cell = table_get_cell(table, 0, 7));
        table_set_minimum_width(table, cell, 11);

        sd_json_variant *i;
        JSON_VARIANT_ARRAY_FOREACH(i, sd_json_variant_by_key(reply, "Neighbors")) {
                _cleanup_(interface_info_done) InterfaceInfo info = {};

                r = sd_json_dispatch(i, interface_info_dispatch_table, SD_JSON_LOG|SD_JSON_ALLOW_EXTENSIONS, &info);
                if (r < 0)
                        return r;

                if (!interface_match_pattern(&info, strv_skip(argv, 1)))
                        continue;

                sd_json_variant *neighbor;
                JSON_VARIANT_ARRAY_FOREACH(neighbor, info.v) {
                        LLDPNeighborInfo neighbor_info = {};

                        r = sd_json_dispatch(neighbor, lldp_neighbor_dispatch_table, SD_JSON_LOG|SD_JSON_ALLOW_EXTENSIONS, &neighbor_info);
                        if (r < 0)
                                return r;

                        all |= neighbor_info.capabilities;

                        _cleanup_free_ char *cap_str = lldp_capabilities_to_string(neighbor_info.capabilities);

                        r = table_add_many(table,
                                           TABLE_INT,    info.ifindex,
                                           TABLE_STRING, info.ifname,
                                           TABLE_STRING, neighbor_info.system_name,
                                           TABLE_STRING, neighbor_info.system_description,
                                           TABLE_STRING, neighbor_info.chassis_id,
                                           TABLE_STRING, neighbor_info.port_id,
                                           TABLE_STRING, neighbor_info.port_description,
                                           TABLE_STRING, cap_str);
                        if (r < 0)
                                return table_log_add_error(r);

                        m++;
                }
        }

        r = table_print_or_warn(table);
        if (r < 0)
                return r;

        if (arg_legend) {
                lldp_capabilities_legend(all);
                printf("\n%zu neighbor(s) listed.\n", m);
        }

        return 0;
}
