/*
 * snmp_interface_walk - SNMP helper for NagiosXI.
 *
 * Modes (-m flag):
 *   test  - SNMP GET on sysDescr.0 to verify connectivity.
 *   get   - SNMP GET on one or more arbitrary OIDs (-o).
 *   walk  - Walk IF-MIB OIDs and output JSON interface data (default).
 *
 * By performing SNMP operations in C via libnetsnmp we get native support
 * for AES-192/256 and all SHA variants, removing the limitation of PHP's
 * built-in SNMP extension.  This binary is used by multiple NagiosXI
 * config wizards wherever SNMP connectivity or data gathering is needed.
 *
 * Output: JSON object on stdout.
 * Exit codes: 0 = success, 1 = SNMP error, 2 = usage error.
 *
 * Copyright (c) 2026 Nagios Enterprises, LLC. All rights reserved.
 * Licensed under the GPL v2.
 */

/* Ensure POSIX/BSD types (u_char, u_long, etc.) are available in strict modes */
#ifndef _DEFAULT_SOURCE
#define _DEFAULT_SOURCE
#endif

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <ctype.h>
#include <unistd.h>
#include <errno.h>

#include <net-snmp/net-snmp-config.h>
#include <net-snmp/net-snmp-includes.h>

/*
 * On net-snmp 5.9+ (EL9), SHA-2 auth protocols and AES-192/256
 * privacy protocols are available.  On net-snmp 5.8 (EL8) they
 * are not.  The Makefile probes for their availability and passes
 * -DHAVE_USM_SHA2 and/or -DHAVE_USM_AES_EXT accordingly.
 */

/* ------------------------------------------------------------------ */
/* Exit codes                                                          */
/* ------------------------------------------------------------------ */
#define EXIT_OK      0
#define EXIT_SNMP    1
#define EXIT_USAGE   2

/* ------------------------------------------------------------------ */
/* Test OID (sysDescr.0)                                               */
/* ------------------------------------------------------------------ */
#define SYSDESCR_OID "1.3.6.1.2.1.1.1.0"

/* ------------------------------------------------------------------ */
/* Modes                                                               */
/* ------------------------------------------------------------------ */
enum run_mode { MODE_WALK, MODE_TEST, MODE_GET };

/* ------------------------------------------------------------------ */
/* Arbitrary GET OIDs (for -m get -o OID ...)                          */
/* ------------------------------------------------------------------ */
#define MAX_GET_OIDS 256
static const char *get_oids[MAX_GET_OIDS];
static int get_oid_count = 0;

/* ------------------------------------------------------------------ */
/* IF-MIB OIDs we walk                                                 */
/* ------------------------------------------------------------------ */
static const char *walk_oids[] = {
    "1.3.6.1.2.1.2.2.1.2",       /* ifDescr        */
    "1.3.6.1.2.1.31.1.1.1.1",    /* ifName         */
    "1.3.6.1.2.1.31.1.1.1.18",   /* ifAlias        */
    "1.3.6.1.2.1.2.2.1.5",       /* ifSpeed        */
    "1.3.6.1.2.1.31.1.1.1.15",   /* ifHighSpeed    */
    "1.3.6.1.2.1.2.2.1.3",       /* ifType         */
    "1.3.6.1.2.1.2.2.1.8",       /* ifOperStatus   */
    "1.3.6.1.2.1.2.2.1.7",       /* ifAdminStatus  */
    "1.3.6.1.2.1.2.2.1.10",      /* ifInOctets     */
    "1.3.6.1.2.1.2.2.1.16",      /* ifOutOctets    */
    "1.3.6.1.2.1.31.1.1.1.6",    /* ifHCInOctets   */
    "1.3.6.1.2.1.31.1.1.1.10",   /* ifHCOutOctets  */
    NULL
};

/* Symbolic names matching the OIDs above (same order) */
static const char *walk_names[] = {
    "ifDescr", "ifName", "ifAlias", "ifSpeed", "ifHighSpeed",
    "ifType", "ifOperStatus", "ifAdminStatus",
    "ifInOctets", "ifOutOctets", "ifHCInOctets", "ifHCOutOctets",
    NULL
};

/* ------------------------------------------------------------------ */
/* Per-interface data collected from the walk                          */
/* ------------------------------------------------------------------ */
#define MAX_INTERFACES 4096
#define MAX_VAL_LEN    512

typedef struct {
    int  index;
    int  populated;

    char description[MAX_VAL_LEN];
    char name[MAX_VAL_LEN];
    char alias_val[MAX_VAL_LEN];
    char speed[MAX_VAL_LEN];
    char high_speed[MAX_VAL_LEN];
    char type[MAX_VAL_LEN];
    char oper_status[MAX_VAL_LEN];
    char admin_status[MAX_VAL_LEN];

    /* Full OID strings for the wizard to reference */
    char oid_oper_status[MAX_VAL_LEN];

    /* Counter values */
    char in_octets_32[MAX_VAL_LEN];
    char out_octets_32[MAX_VAL_LEN];
    char in_octets_64[MAX_VAL_LEN];
    char out_octets_64[MAX_VAL_LEN];

    /* Full OID strings for in/out octets */
    char oid_in_32[MAX_VAL_LEN];
    char oid_out_32[MAX_VAL_LEN];
    char oid_in_64[MAX_VAL_LEN];
    char oid_out_64[MAX_VAL_LEN];

    int  has_32bit;
    int  has_64bit;
} InterfaceData;

static InterfaceData ifaces[MAX_INTERFACES];
static int iface_count = 0;

/* ------------------------------------------------------------------ */
/* Lookup / create an interface slot by ifIndex                        */
/* ------------------------------------------------------------------ */
static InterfaceData *get_iface(int idx) {
    for (int i = 0; i < iface_count; i++) {
        if (ifaces[i].index == idx)
            return &ifaces[i];
    }
    if (iface_count >= MAX_INTERFACES)
        return NULL;
    InterfaceData *ifd = &ifaces[iface_count++];
    memset(ifd, 0, sizeof(*ifd));
    ifd->index = idx;
    ifd->populated = 1;
    return ifd;
}

/* ------------------------------------------------------------------ */
/* Protocol lookup helpers (same tables as check_throughput)            */
/* ------------------------------------------------------------------ */
static const oid *lookup_auth_proto(const char *label, size_t *len) {
    struct { const char *name; const oid *pr; size_t plen; } map[] = {
#ifndef NETSNMP_DISABLE_MD5
        {"MD5",     usmHMACMD5AuthProtocol,       10},
#endif
        {"SHA",     usmHMACSHA1AuthProtocol,       10},
        {"SHA1",    usmHMACSHA1AuthProtocol,       10},
#ifdef HAVE_USM_SHA2
        {"SHA-224", usmHMAC128SHA224AuthProtocol,  10},
        {"SHA-256", usmHMAC192SHA256AuthProtocol,  10},
        {"SHA-384", usmHMAC256SHA384AuthProtocol,  10},
        {"SHA-512", usmHMAC384SHA512AuthProtocol,  10},
        {"SHA224",  usmHMAC128SHA224AuthProtocol,  10},
        {"SHA256",  usmHMAC192SHA256AuthProtocol,  10},
        {"SHA384",  usmHMAC256SHA384AuthProtocol,  10},
        {"SHA512",  usmHMAC384SHA512AuthProtocol,  10},
#endif
    };
    for (size_t i = 0; i < sizeof(map)/sizeof(map[0]); i++) {
        if (map[i].pr && strcasecmp(label, map[i].name) == 0) {
            *len = map[i].plen;
            return map[i].pr;
        }
    }
    *len = 10;
    return usmHMACSHA1AuthProtocol;
}

static const oid *lookup_priv_proto(const char *label, size_t *len) {
    struct { const char *name; const oid *pr; size_t plen; } map[] = {
        {"AES",       usmAESPrivProtocol,          10},
        {"AES128",    usmAESPrivProtocol,          10},
        {"AES-128",   usmAESPrivProtocol,          10},
#ifdef HAVE_USM_AES_EXT
        {"AES-192",   usmAES192PrivProtocol,        9},
        {"AES-256",   usmAES256PrivProtocol,        9},
        {"AES-192-C", usmAES192CiscoPrivProtocol,  11},
        {"AES-256-C", usmAES256CiscoPrivProtocol,  11},
        {"AES192",    usmAES192PrivProtocol,        9},
        {"AES256",    usmAES256PrivProtocol,        9},
        {"AES192C",   usmAES192CiscoPrivProtocol,  11},
        {"AES256C",   usmAES256CiscoPrivProtocol,  11},
#endif
#ifndef NETSNMP_DISABLE_DES
        {"DES",       usmDESPrivProtocol,          10},
#endif
    };
    for (size_t i = 0; i < sizeof(map)/sizeof(map[0]); i++) {
        if (map[i].pr && strcasecmp(label, map[i].name) == 0) {
            *len = map[i].plen;
            return map[i].pr;
        }
    }
    *len = 10;
    return usmAESPrivProtocol;
}

/* ------------------------------------------------------------------ */
/* JSON output helpers                                                 */
/* ------------------------------------------------------------------ */
static void json_escape(const char *s, FILE *fp) {
    for (; *s; s++) {
        switch (*s) {
            case '"':  fputs("\\\"", fp); break;
            case '\\': fputs("\\\\", fp); break;
            case '\b': fputs("\\b", fp);  break;
            case '\f': fputs("\\f", fp);  break;
            case '\n': fputs("\\n", fp);  break;
            case '\r': fputs("\\r", fp);  break;
            case '\t': fputs("\\t", fp);  break;
            default:
                if ((unsigned char)*s < 0x20)
                    fprintf(fp, "\\u%04x", (unsigned char)*s);
                else
                    fputc(*s, fp);
                break;
        }
    }
}

static void json_kv(const char *key, const char *val, FILE *fp, int comma) {
    fprintf(fp, "\"");
    json_escape(key, fp);
    fprintf(fp, "\":\"");
    json_escape(val, fp);
    fprintf(fp, "\"");
    if (comma) fprintf(fp, ",");
}

static void json_kv_bool(const char *key, int val, FILE *fp, int comma) {
    fprintf(fp, "\"%s\":%s", key, val ? "true" : "false");
    if (comma) fprintf(fp, ",");
}

/* ------------------------------------------------------------------ */
/* Extract the trailing integer index from a dotted OID                */
/* e.g. "1.3.6.1.2.1.2.2.1.2.17" -> 17                               */
/* Returns -1 on failure.                                              */
/* ------------------------------------------------------------------ */
static int oid_last_index(const oid *name, size_t name_len) {
    if (name_len == 0) return -1;
    return (int)name[name_len - 1];
}

/* ------------------------------------------------------------------ */
/* Render a variable's value as a plain string                         */
/* Strips type prefixes so the JSON is clean.                          */
/* ------------------------------------------------------------------ */
static void var_to_string(netsnmp_variable_list *var, char *buf, size_t bufsz) {
    buf[0] = '\0';
    if (!var->val.string) return;
    switch (var->type) {
        case ASN_OCTET_STR:
        case ASN_BIT_STR: {
            size_t cplen = var->val_len < bufsz - 1 ? var->val_len : bufsz - 1;
            memcpy(buf, var->val.string, cplen);
            buf[cplen] = '\0';
            break;
        }
        case ASN_INTEGER:
            snprintf(buf, bufsz, "%ld", *var->val.integer);
            break;
        case ASN_GAUGE:
        case ASN_COUNTER:
        case ASN_TIMETICKS:
        case ASN_UINTEGER:
            snprintf(buf, bufsz, "%lu", (unsigned long)*var->val.integer);
            break;
        case ASN_COUNTER64: {
            struct counter64 *c64 = var->val.counter64;
            unsigned long long val = ((unsigned long long)c64->high << 32) | c64->low;
            snprintf(buf, bufsz, "%llu", val);
            break;
        }
        case ASN_OBJECT_ID: {
            /* Print dotted OID */
            char *p = buf;
            size_t remain = bufsz;
            for (size_t i = 0; i < var->val_len / sizeof(oid) && remain > 1; i++) {
                int n = snprintf(p, remain, "%s%lu", i ? "." : "",
                                (unsigned long)var->val.objid[i]);
                if (n < 0 || (size_t)n >= remain) break;
                p += n;
                remain -= n;
            }
            break;
        }
        default: {
            /* Fallback: use sprint_realloc_value and strip the type prefix */
            u_char *sprint_buf = NULL;
            size_t sprint_buf_len = 0;
            size_t sprint_out_len = 0;
            int allow_realloc = 1;
            sprint_realloc_value(&sprint_buf, &sprint_buf_len, &sprint_out_len,
                                allow_realloc, var->name, var->name_length, var);
            if (sprint_buf) {
                /* Strip "TYPE: " prefix */
                char *colon = strchr((char *)sprint_buf, ':');
                const char *src = (colon && colon[1] == ' ') ? colon + 2 : (char *)sprint_buf;
                snprintf(buf, bufsz, "%s", src);
                free(sprint_buf);
            }
            break;
        }
    }
}

/* ------------------------------------------------------------------ */
/* Build a dotted OID string from oid array                            */
/* ------------------------------------------------------------------ */
static void oid_to_dotted(const oid *name, size_t name_len, char *buf, size_t bufsz) {
    char *p = buf;
    size_t remain = bufsz;
    for (size_t i = 0; i < name_len && remain > 1; i++) {
        int n = snprintf(p, remain, "%s%lu", i ? "." : "", (unsigned long)name[i]);
        if (n < 0 || (size_t)n >= remain) break;
        p += n;
        remain -= n;
    }
}

/* ------------------------------------------------------------------ */
/* Reconstruct a MIB-style OID string for JSON output                  */
/* e.g. walk_name="ifOperStatus", ifIndex=3 -> "IF-MIB::ifOperStatus.3" */
/* ------------------------------------------------------------------ */
static void build_mib_oid(const char *walk_name, int ifindex, char *buf, size_t bufsz) {
    snprintf(buf, bufsz, "IF-MIB::%s.%d", walk_name, ifindex);
}

/* ------------------------------------------------------------------ */
/* Perform an SNMP GET on sysDescr.0 (test mode)                       */
/* Returns 0 on success, -1 on failure.                                */
/* ------------------------------------------------------------------ */
static int do_test(netsnmp_session *ss) {
    oid name[MAX_OID_LEN];
    size_t name_len = MAX_OID_LEN;

    if (!snmp_parse_oid(SYSDESCR_OID, name, &name_len)) {
        printf("{\"status\":\"error\",\"message\":\"Failed to parse sysDescr OID\"}\n");
        return -1;
    }

    netsnmp_pdu *pdu = snmp_pdu_create(SNMP_MSG_GET);
    snmp_add_null_var(pdu, name, name_len);

    netsnmp_pdu *response = NULL;
    int status = snmp_synch_response(ss, pdu, &response);

    if (status != STAT_SUCCESS || !response) {
        if (response) snmp_free_pdu(response);
        printf("{\"status\":\"error\",\"message\":\"SNMP GET failed - device did not respond\"}\n");
        return -1;
    }

    if (response->errstat != SNMP_ERR_NOERROR) {
        printf("{\"status\":\"error\",\"message\":\"SNMP error: ");
        json_escape(snmp_errstring(response->errstat), stdout);
        printf("\"}\n");
        snmp_free_pdu(response);
        return -1;
    }

    netsnmp_variable_list *vars = response->variables;
    if (!vars || vars->type == SNMP_NOSUCHOBJECT ||
        vars->type == SNMP_NOSUCHINSTANCE || vars->type == SNMP_ENDOFMIBVIEW) {
        printf("{\"status\":\"error\",\"message\":\"sysDescr not available on device\"}\n");
        snmp_free_pdu(response);
        return -1;
    }

    char valbuf[MAX_VAL_LEN];
    var_to_string(vars, valbuf, sizeof(valbuf));

    printf("{\"status\":\"ok\",");
    json_kv("sysDescr", valbuf, stdout, 0);
    printf("}\n");

    snmp_free_pdu(response);
    return 0;
}

/* ------------------------------------------------------------------ */
/* Perform SNMP GET on one or more arbitrary OIDs (get mode)           */
/* Returns 0 on success, -1 on failure.                                */
/* ------------------------------------------------------------------ */
static int do_get(netsnmp_session *ss) {
    if (get_oid_count == 0) {
        printf("{\"status\":\"error\",\"message\":\"No OIDs specified (-o)\"}\n");
        return -1;
    }

    netsnmp_pdu *pdu = snmp_pdu_create(SNMP_MSG_GET);

    /* Heap-allocate to avoid a huge stack frame (256 * 128 * sizeof(oid)) */
    oid (*parsed)[MAX_OID_LEN] = malloc(sizeof(oid[MAX_OID_LEN]) * get_oid_count);
    size_t *parsed_len = malloc(sizeof(size_t) * get_oid_count);
    if (!parsed || !parsed_len) {
        free(parsed);
        free(parsed_len);
        snmp_free_pdu(pdu);
        printf("{\"status\":\"error\",\"message\":\"Memory allocation failed\"}\n");
        return -1;
    }

    for (int i = 0; i < get_oid_count; i++) {
        parsed_len[i] = MAX_OID_LEN;
        if (!snmp_parse_oid(get_oids[i], parsed[i], &parsed_len[i])) {
            free(parsed);
            free(parsed_len);
            snmp_free_pdu(pdu);
            printf("{\"status\":\"error\",\"message\":\"Failed to parse OID: ");
            json_escape(get_oids[i], stdout);
            printf("\"}\n");
            return -1;
        }
        snmp_add_null_var(pdu, parsed[i], parsed_len[i]);
    }

    free(parsed);
    free(parsed_len);

    netsnmp_pdu *response = NULL;
    int status = snmp_synch_response(ss, pdu, &response);

    if (status != STAT_SUCCESS || !response) {
        if (response) snmp_free_pdu(response);
        printf("{\"status\":\"error\",\"message\":\"SNMP GET failed - device did not respond\"}\n");
        return -1;
    }

    if (response->errstat != SNMP_ERR_NOERROR) {
        printf("{\"status\":\"error\",\"message\":\"SNMP error: ");
        json_escape(snmp_errstring(response->errstat), stdout);
        printf("\"}\n");
        snmp_free_pdu(response);
        return -1;
    }

    printf("{\"status\":\"ok\",\"results\":{");

    int first = 1;
    for (netsnmp_variable_list *vars = response->variables; vars; vars = vars->next_variable) {
        if (vars->type == SNMP_NOSUCHOBJECT ||
            vars->type == SNMP_NOSUCHINSTANCE ||
            vars->type == SNMP_ENDOFMIBVIEW)
            continue;

        if (!first) printf(",");
        first = 0;

        /* Key is the dotted OID */
        char oidbuf[MAX_VAL_LEN];
        oid_to_dotted(vars->name, vars->name_length, oidbuf, sizeof(oidbuf));

        char valbuf[MAX_VAL_LEN];
        var_to_string(vars, valbuf, sizeof(valbuf));

        json_kv(oidbuf, valbuf, stdout, 0);
    }

    printf("}}\n");

    snmp_free_pdu(response);
    return 0;
}

/* ------------------------------------------------------------------ */
/* Perform a single SNMP walk of one OID subtree                       */
/* ------------------------------------------------------------------ */
static int do_walk(netsnmp_session *ss, const char *oid_str, const char *oid_name) {
    oid root[MAX_OID_LEN];
    size_t rootlen = MAX_OID_LEN;

    if (!snmp_parse_oid(oid_str, root, &rootlen)) {
        /* OID parse failed - skip silently */
        return -1;
    }

    oid name[MAX_OID_LEN];
    size_t name_len = rootlen;
    memcpy(name, root, rootlen * sizeof(oid));

    int running = 1;
    while (running) {
        netsnmp_pdu *pdu = snmp_pdu_create(SNMP_MSG_GETNEXT);
        snmp_add_null_var(pdu, name, name_len);

        netsnmp_pdu *response = NULL;
        int status = snmp_synch_response(ss, pdu, &response);

        if (status != STAT_SUCCESS || !response) {
            if (response) snmp_free_pdu(response);
            break;
        }

        if (response->errstat != SNMP_ERR_NOERROR) {
            snmp_free_pdu(response);
            break;
        }

        for (netsnmp_variable_list *vars = response->variables; vars; vars = vars->next_variable) {
            /* Check we're still under the root OID */
            if (vars->name_length < rootlen ||
                memcmp(root, vars->name, rootlen * sizeof(oid)) != 0) {
                running = 0;
                break;
            }

            /* End-of-MIB markers */
            if (vars->type == SNMP_ENDOFMIBVIEW ||
                vars->type == SNMP_NOSUCHOBJECT ||
                vars->type == SNMP_NOSUCHINSTANCE) {
                running = 0;
                break;
            }

            int ifindex = oid_last_index(vars->name, vars->name_length);
            if (ifindex < 0) continue;

            InterfaceData *ifd = get_iface(ifindex);
            if (!ifd) continue;

            char valbuf[MAX_VAL_LEN];
            var_to_string(vars, valbuf, sizeof(valbuf));

            char oidbuf[MAX_VAL_LEN];
            build_mib_oid(oid_name, ifindex, oidbuf, sizeof(oidbuf));

            if (strcmp(oid_name, "ifDescr") == 0) {
                snprintf(ifd->description, sizeof(ifd->description), "%s", valbuf);
            } else if (strcmp(oid_name, "ifName") == 0) {
                snprintf(ifd->name, sizeof(ifd->name), "%s", valbuf);
            } else if (strcmp(oid_name, "ifAlias") == 0) {
                snprintf(ifd->alias_val, sizeof(ifd->alias_val), "%s", valbuf);
            } else if (strcmp(oid_name, "ifSpeed") == 0) {
                snprintf(ifd->speed, sizeof(ifd->speed), "%s", valbuf);
            } else if (strcmp(oid_name, "ifHighSpeed") == 0) {
                /* Overwrite speed with Mbps value from ifHighSpeed */
                snprintf(ifd->high_speed, sizeof(ifd->high_speed), "%s", valbuf);
            } else if (strcmp(oid_name, "ifType") == 0) {
                snprintf(ifd->type, sizeof(ifd->type), "%s", valbuf);
            } else if (strcmp(oid_name, "ifOperStatus") == 0) {
                snprintf(ifd->oper_status, sizeof(ifd->oper_status), "%s", valbuf);
                snprintf(ifd->oid_oper_status, sizeof(ifd->oid_oper_status), "%s", oidbuf);
            } else if (strcmp(oid_name, "ifAdminStatus") == 0) {
                snprintf(ifd->admin_status, sizeof(ifd->admin_status), "%s", valbuf);
            } else if (strcmp(oid_name, "ifInOctets") == 0) {
                snprintf(ifd->in_octets_32, sizeof(ifd->in_octets_32), "%s", valbuf);
                snprintf(ifd->oid_in_32, sizeof(ifd->oid_in_32), "%s", oidbuf);
                ifd->has_32bit = 1;
            } else if (strcmp(oid_name, "ifOutOctets") == 0) {
                snprintf(ifd->out_octets_32, sizeof(ifd->out_octets_32), "%s", valbuf);
                snprintf(ifd->oid_out_32, sizeof(ifd->oid_out_32), "%s", oidbuf);
            } else if (strcmp(oid_name, "ifHCInOctets") == 0) {
                snprintf(ifd->in_octets_64, sizeof(ifd->in_octets_64), "%s", valbuf);
                snprintf(ifd->oid_in_64, sizeof(ifd->oid_in_64), "%s", oidbuf);
                ifd->has_64bit = 1;
            } else if (strcmp(oid_name, "ifHCOutOctets") == 0) {
                snprintf(ifd->out_octets_64, sizeof(ifd->out_octets_64), "%s", valbuf);
                snprintf(ifd->oid_out_64, sizeof(ifd->oid_out_64), "%s", oidbuf);
            }

            /* Advance the walk cursor */
            memcpy(name, vars->name, vars->name_length * sizeof(oid));
            name_len = vars->name_length;
        }

        snmp_free_pdu(response);
    }

    return 0;
}

/* ------------------------------------------------------------------ */
/* Usage / help                                                        */
/* ------------------------------------------------------------------ */
static void usage(void) {
    fprintf(stderr,
        "Usage: snmp_interface_walk -H <host> [-m mode] [options]\n"
        "\n"
        "Modes (-m):\n"
        "  test          SNMP GET on sysDescr.0 - verify device responds\n"
        "  get           SNMP GET on arbitrary OIDs (use -o for each OID)\n"
        "  walk          Walk IF-MIB OIDs for interface discovery (default)\n"
        "\n"
        "General options:\n"
        "  -H HOST       Hostname or IP address (required)\n"
        "  -p PORT       SNMP port (default: 161)\n"
        "  -v VERSION    SNMP version: 1, 2c, 3 (default: 2c)\n"
        "  -C COMMUNITY  Community string (v1/v2c, default: public)\n"
        "  -t TIMEOUT    Timeout in seconds (default: 5)\n"
        "  -r RETRIES    Number of retries (default: 1)\n"
        "  -o OID        OID to query (get mode, repeatable)\n"
        "\n"
        "SNMPv3 options:\n"
        "  -L LEVEL      Security level: noAuthNoPriv|authNoPriv|authPriv\n"
        "  -U USERNAME   Security name\n"
        "  -a PROTOCOL   Auth protocol: MD5|SHA|SHA224|SHA256|SHA384|SHA512\n"
        "  -A PASSPHRASE Auth passphrase\n"
        "  -x PROTOCOL   Privacy protocol: AES|AES128|AES192|AES256|AES192C|AES256C|DES\n"
        "  -X PASSPHRASE Privacy passphrase\n"
        "\n"
        "Output: JSON object on stdout.\n"
        "Exit codes: 0 = success, 1 = SNMP error, 2 = usage error.\n"
    );
}

/* ------------------------------------------------------------------ */
/* Main                                                                */
/* ------------------------------------------------------------------ */
int main(int argc, char *argv[]) {
    char *hostname    = NULL;
    char *port_str    = "161";
    int   port_explicit = 0;
    char *version_str = "2c";
    char *community   = "public";
    int   timeout_sec = 5;
    int   retries     = 1;
    enum run_mode mode = MODE_WALK;

    /* SNMPv3 */
    char *seclevel    = "authPriv";
    char *secname     = NULL;
    char *authproto   = "SHA";
    char *authpass    = NULL;
    char *privproto   = "AES";
    char *privpass    = NULL;

    int opt;
    while ((opt = getopt(argc, argv, "H:p:v:C:t:r:L:U:a:A:x:X:m:o:h")) != -1) {
        switch (opt) {
        case 'H': hostname    = optarg; break;
        case 'p': port_str    = optarg; port_explicit = 1; break;
        case 'v': version_str = optarg; break;
        case 'C': community   = optarg; break;
        case 't': timeout_sec = atoi(optarg); break;
        case 'r': retries     = atoi(optarg); break;
        case 'L': seclevel    = optarg; break;
        case 'U': secname     = optarg; break;
        case 'a': authproto   = optarg; break;
        case 'A': authpass    = optarg; break;
        case 'x': privproto   = optarg; break;
        case 'X': privpass    = optarg; break;
        case 'm':
            if (strcasecmp(optarg, "test") == 0)
                mode = MODE_TEST;
            else if (strcasecmp(optarg, "get") == 0)
                mode = MODE_GET;
            else if (strcasecmp(optarg, "walk") == 0)
                mode = MODE_WALK;
            else {
                fprintf(stderr, "Error: unknown mode '%s' (use test, get, or walk)\n", optarg);
                usage();
                return EXIT_USAGE;
            }
            break;
        case 'o':
            if (get_oid_count < MAX_GET_OIDS)
                get_oids[get_oid_count++] = optarg;
            else {
                fprintf(stderr, "Error: too many -o OIDs (max %d)\n", MAX_GET_OIDS);
                return EXIT_USAGE;
            }
            break;
        case 'h':
            usage();
            return EXIT_USAGE;
        default:
            usage();
            return EXIT_USAGE;
        }
    }

    if (!hostname) {
        fprintf(stderr, "Error: -H hostname is required\n");
        usage();
        return EXIT_USAGE;
    }

    if (timeout_sec < 1) timeout_sec = 5;
    if (retries < 0) retries = 1;

    /* ---------------------------------------------------------- */
    /* Build the SNMP session                                      */
    /* ---------------------------------------------------------- */
    init_snmp("snmp_interface_walk");
    netsnmp_ds_set_boolean(NETSNMP_DS_LIBRARY_ID,
                           NETSNMP_DS_LIB_DONT_PERSIST_STATE, 1);
    netsnmp_ds_set_boolean(NETSNMP_DS_LIBRARY_ID,
                           NETSNMP_DS_LIB_DISABLE_PERSISTENT_LOAD, 1);

    netsnmp_session session;
    snmp_sess_init(&session);

    char hostport[1024];
    if (port_explicit)
        snprintf(hostport, sizeof(hostport), "%s:%s", hostname, port_str);
    else
        snprintf(hostport, sizeof(hostport), "%s", hostname);
    session.peername = hostport;
    session.timeout  = (long)timeout_sec * 1000000L;
    session.retries  = retries;

    if (strcmp(version_str, "3") == 0) {
        session.version = SNMP_VERSION_3;

        if (strcasecmp(seclevel, "authPriv") == 0)
            session.securityLevel = SNMP_SEC_LEVEL_AUTHPRIV;
        else if (strcasecmp(seclevel, "authNoPriv") == 0)
            session.securityLevel = SNMP_SEC_LEVEL_AUTHNOPRIV;
        else
            session.securityLevel = SNMP_SEC_LEVEL_NOAUTH;

        if (secname) {
            session.securityName    = secname;
            session.securityNameLen = strlen(secname);
        }

        if (session.securityLevel >= SNMP_SEC_LEVEL_AUTHNOPRIV && authpass) {
            size_t plen;
            session.securityAuthProto    = (oid *)lookup_auth_proto(authproto, &plen);
            session.securityAuthProtoLen = plen;
            session.securityAuthKeyLen   = USM_AUTH_KU_LEN;
            if (generate_Ku(session.securityAuthProto, session.securityAuthProtoLen,
                            (u_char *)authpass, strlen(authpass),
                            session.securityAuthKey,
                            &session.securityAuthKeyLen) != SNMPERR_SUCCESS) {
                fprintf(stderr, "{\"error\":\"Failed to generate auth key\"}\n");
                return EXIT_SNMP;
            }
        }

        if (session.securityLevel == SNMP_SEC_LEVEL_AUTHPRIV && privpass) {
            if (!session.securityAuthProto || session.securityAuthProtoLen == 0) {
                fprintf(stderr, "{\"error\":\"Auth protocol required for authPriv (-a and -A)\"}"
                        "\n");
                return EXIT_SNMP;
            }
            size_t plen;
            session.securityPrivProto    = (oid *)lookup_priv_proto(privproto, &plen);
            session.securityPrivProtoLen = plen;
            session.securityPrivKeyLen   = USM_PRIV_KU_LEN;
            if (generate_Ku(session.securityAuthProto, session.securityAuthProtoLen,
                            (u_char *)privpass, strlen(privpass),
                            session.securityPrivKey,
                            &session.securityPrivKeyLen) != SNMPERR_SUCCESS) {
                fprintf(stderr, "{\"error\":\"Failed to generate priv key\"}\n");
                return EXIT_SNMP;
            }
        }
    } else {
        session.version = (strcmp(version_str, "2c") == 0)
                          ? SNMP_VERSION_2c : SNMP_VERSION_1;
        session.community     = (u_char *)community;
        session.community_len = strlen(community);
    }

    SOCK_STARTUP;
    netsnmp_session *ss = snmp_open(&session);
    if (!ss) {
        printf("{\"status\":\"error\",\"message\":\"Failed to open SNMP session to ");
        json_escape(hostname, stdout);
        printf("\"}\n");
        SOCK_CLEANUP;
        return EXIT_SNMP;
    }

    /* ---------------------------------------------------------- */
    /* Execute the requested mode                                  */
    /* ---------------------------------------------------------- */
    int rc = EXIT_OK;

    if (mode == MODE_TEST) {
        rc = (do_test(ss) == 0) ? EXIT_OK : EXIT_SNMP;
        snmp_close(ss);
        SOCK_CLEANUP;
        return rc;
    }

    if (mode == MODE_GET) {
        rc = (do_get(ss) == 0) ? EXIT_OK : EXIT_SNMP;
        snmp_close(ss);
        SOCK_CLEANUP;
        return rc;
    }

    /* MODE_WALK: walk each IF-MIB OID */
    for (int i = 0; walk_oids[i]; i++) {
        do_walk(ss, walk_oids[i], walk_names[i]);
    }

    snmp_close(ss);
    SOCK_CLEANUP;

    /* ---------------------------------------------------------- */
    /* Output JSON                                                 */
    /* ---------------------------------------------------------- */
    if (iface_count == 0) {
        printf("{\"interfaces\":{}}\n");
        return EXIT_SNMP;
    }

    printf("{\"interfaces\":{");

    int first = 1;
    for (int i = 0; i < iface_count; i++) {
        InterfaceData *ifd = &ifaces[i];
        if (!ifd->populated) continue;

        if (!first) printf(",");
        first = 0;

        /* Use the speed: prefer ifHighSpeed (Mbps) when available */
        char speed_str[MAX_VAL_LEN];
        if (ifd->high_speed[0] != '\0') {
            snprintf(speed_str, sizeof(speed_str), "%s Mbps", ifd->high_speed);
        } else {
            snprintf(speed_str, sizeof(speed_str), "%s", ifd->speed);
        }

        printf("\"%d\":{", ifd->index);
        printf("\"index\":\"%d\",", ifd->index);
        json_kv("description",     ifd->description,     stdout, 1);
        json_kv("name",            ifd->name,            stdout, 1);
        json_kv("alias",           ifd->alias_val,       stdout, 1);
        json_kv("speed",           speed_str,             stdout, 1);
        json_kv("type",            ifd->type,            stdout, 1);
        json_kv("oper_status",     ifd->oper_status,     stdout, 1);
        json_kv("oper_status_oid", ifd->oid_oper_status, stdout, 1);
        json_kv("admin_status",    ifd->admin_status,    stdout, 1);
        json_kv("oid_in_32",       ifd->oid_in_32,       stdout, 1);
        json_kv("oid_out_32",      ifd->oid_out_32,      stdout, 1);
        json_kv("oid_in_64",       ifd->oid_in_64,       stdout, 1);
        json_kv("oid_out_64",      ifd->oid_out_64,      stdout, 1);
        json_kv("in_octets_32",    ifd->in_octets_32,    stdout, 1);
        json_kv("out_octets_32",   ifd->out_octets_32,   stdout, 1);
        json_kv("in_octets_64",    ifd->in_octets_64,    stdout, 1);
        json_kv("out_octets_64",   ifd->out_octets_64,   stdout, 1);
        json_kv_bool("has_32bit",  ifd->has_32bit,       stdout, 1);
        json_kv_bool("has_64bit",  ifd->has_64bit,       stdout, 0);
        printf("}");
    }

    printf("}}\n");
    return EXIT_OK;
}
