/*****************************************************************************
* Copyright (c) 2026 Nagios Enterprises, LLC. All rights reserved.
*****************************************************************************/

const char *progname = "check_throughput";
const char *copyright = "2026";
const char *email = "support@nagios.com";

#include "common.h"
#include "utils.h"
#include <sys/stat.h>
#include <unistd.h>
#include <ctype.h>
#include <sqlite3.h>
#include <stdarg.h>
#include <strings.h>
#include <net-snmp/net-snmp-config.h>
#include <net-snmp/net-snmp-includes.h>

#ifndef LOCALEDIR
#define LOCALEDIR "/usr/local/nagios/share/locale"
#endif

#define DEFAULT_COMMUNITY "public"
#define DEFAULT_PORT "161"
#define DEFAULT_PROTOCOL "1"
#define DEFAULT_RETRIES 5
#define DEFAULT_AUTH_PROTOCOL "SHA"
#define DEFAULT_PRIV_PROTOCOL "AES"
#define DEFAULT_UNITS "mbps"

#define OID_IF_NAME  "1.3.6.1.2.1.31.1.1.1.1"
#define OID_IF_DESC  "1.3.6.1.2.1.2.2.1.2"
#define OID_IF_ALIAS "1.3.6.1.2.1.31.1.1.1.18"

#define LOG_PATH "/usr/local/nagios/var/check_throughput.log"

static FILE *logfp = NULL;

static void open_log(void) {
	logfp = fopen(LOG_PATH, "a");
}

static void close_log(void) {
	if (logfp) { fclose(logfp); logfp = NULL; }
}

/* Write a timestamped, host-tagged log line */
static void write_log(const char *host, const char *fmt, ...) {
	if (!logfp) return;
	time_t now = time(NULL);
	char tbuf[32];
	strftime(tbuf, sizeof(tbuf), "%Y-%m-%d %H:%M:%S", localtime(&now));
	fprintf(logfp, "[%s] [%s] ", tbuf, host ? host : "-");
	va_list ap;
	va_start(ap, fmt);
	vfprintf(logfp, fmt, ap);
	va_end(ap);
	fprintf(logfp, "\n");
	fflush(logfp);
}

/* Longopts-only arguments */
#define L_COUNTER_BITS CHAR_MAX+1
#define L_DB_PATH      CHAR_MAX+2
#define L_I_NUM        CHAR_MAX+3
#define L_I_NAME       CHAR_MAX+4
#define L_I_DESC       CHAR_MAX+5
#define L_I_ALIAS      CHAR_MAX+6

typedef struct {
	char  *server_address;
	char  *community;
	char  *context;
	char  *proto;
	char  *seclevel;
	char  *secname;
	char  *authproto;
	char  *privproto;
	char  *authpasswd;
	char  *privpasswd;
	char  *port;
	int    retries;
	int    verbose;
	char  *ip_version;
} SnmpOpts;

typedef struct {
	char       *oid_index;          /* interface value appended to counter OIDs */
	char       *i_name;             /* --i-name value */
	char       *i_desc;             /* --i-desc value */
	char       *i_alias;            /* --i-alias value */
	char       *db_path;
	char       *custom_db_path;
	int         explicit_counter_bits;
	char       *units;
	char       *warning_thresholds;
	char       *critical_thresholds;
	thresholds *thr_in;
	thresholds *thr_out;
} CheckOpts;

static SnmpOpts snmp = {
	.ip_version = "",
};
static CheckOpts cfg = {0};

int process_arguments (int, char **, SnmpOpts *, CheckOpts *);
int validate_arguments (SnmpOpts *, CheckOpts *);
void print_usage (void);

typedef struct {
	time_t             timestamp;
	unsigned long long in_octets;
	unsigned long long out_octets;
	int                counter_bits;
	char               stored_i_num[64];   /* numeric ifIndex from previous run */
	char               stored_i_name[256]; /* ifName from previous run */
	char               stored_i_desc[256]; /* ifDescr from previous run */
	char               stored_i_alias[256];/* ifAlias from previous run */
} OctetData;

const char *determine_db_path(const CheckOpts *cfg, int verbose);
void  sanitize_host_for_table(const char *host, char *out, size_t out_sz);
int   db_read_state(const char *db_path, const char *host, const char *iface, OctetData *data, int verbose);
int   db_write_state(const char *db_path, const char *host, const char *iface, const OctetData *data,
                     const char *i_num, const char *i_name, const char *i_desc, const char *i_alias, int verbose);
char *resolve_label_to_index(const char *label, const char *walk_oid, netsnmp_session *ss);
unsigned long long calculate_wraparound(unsigned long long current, unsigned long long previous, int counter_bits);
double calc_bw(unsigned long long octet_diff, time_t time_diff, const char *unit);

/* Map protocol name to Net-SNMP auth OID */
static const oid *lookup_auth_proto(const char *name, size_t *len) {
	struct proto_entry { const char *label; const oid *proto_oid; size_t oid_len; };
	static const struct proto_entry protos[] = {
#ifndef NETSNMP_DISABLE_MD5
		{"MD5",     usmHMACMD5AuthProtocol,        OID_LENGTH(usmHMACMD5AuthProtocol)},
#endif
		{"SHA",     usmHMACSHA1AuthProtocol,        OID_LENGTH(usmHMACSHA1AuthProtocol)},
		{"SHA1",    usmHMACSHA1AuthProtocol,        OID_LENGTH(usmHMACSHA1AuthProtocol)},
		{"SHA-224", usmHMAC128SHA224AuthProtocol,    OID_LENGTH(usmHMAC128SHA224AuthProtocol)},
		{"SHA-256", usmHMAC192SHA256AuthProtocol,    OID_LENGTH(usmHMAC192SHA256AuthProtocol)},
		{"SHA-384", usmHMAC256SHA384AuthProtocol,    OID_LENGTH(usmHMAC256SHA384AuthProtocol)},
		{"SHA-512", usmHMAC384SHA512AuthProtocol,    OID_LENGTH(usmHMAC384SHA512AuthProtocol)},
		{"SHA224",  usmHMAC128SHA224AuthProtocol,    OID_LENGTH(usmHMAC128SHA224AuthProtocol)},
		{"SHA256",  usmHMAC192SHA256AuthProtocol,    OID_LENGTH(usmHMAC192SHA256AuthProtocol)},
		{"SHA384",  usmHMAC256SHA384AuthProtocol,    OID_LENGTH(usmHMAC256SHA384AuthProtocol)},
		{"SHA512",  usmHMAC384SHA512AuthProtocol,    OID_LENGTH(usmHMAC384SHA512AuthProtocol)},
	};
	for (size_t idx = 0; idx < sizeof(protos)/sizeof(protos[0]); idx++)
		if (protos[idx].proto_oid && strcasecmp(name, protos[idx].label) == 0) { *len = protos[idx].oid_len; return protos[idx].proto_oid; }
	*len = 10; return usmHMACSHA1AuthProtocol;
}

/* Map protocol name to Net-SNMP priv OID */
static const oid *lookup_priv_proto(const char *name, size_t *len) {
	struct proto_entry { const char *label; const oid *proto_oid; size_t oid_len; };
	static const struct proto_entry protos[] = {
		{"AES",       usmAESPrivProtocol,          OID_LENGTH(usmAESPrivProtocol)},
		{"AES128",    usmAESPrivProtocol,          OID_LENGTH(usmAESPrivProtocol)},
		{"AES-128",   usmAESPrivProtocol,          OID_LENGTH(usmAESPrivProtocol)},
#ifdef NETSNMP_DRAFT_BLUMENTHAL_AES_04
		{"AES-192",   usmAES192PrivProtocol,        OID_LENGTH(usmAES192PrivProtocol)},
		{"AES-256",   usmAES256PrivProtocol,        OID_LENGTH(usmAES256PrivProtocol)},
		{"AES-192-C", usmAES192CiscoPrivProtocol,  OID_LENGTH(usmAES192CiscoPrivProtocol)},
		{"AES-256-C", usmAES256CiscoPrivProtocol,  OID_LENGTH(usmAES256CiscoPrivProtocol)},
		{"AES192",    usmAES192PrivProtocol,        OID_LENGTH(usmAES192PrivProtocol)},
		{"AES256",    usmAES256PrivProtocol,        OID_LENGTH(usmAES256PrivProtocol)},
		{"AES192C",   usmAES192CiscoPrivProtocol,  OID_LENGTH(usmAES192CiscoPrivProtocol)},
		{"AES256C",   usmAES256CiscoPrivProtocol,  OID_LENGTH(usmAES256CiscoPrivProtocol)},
#endif
#ifndef NETSNMP_DISABLE_DES
		{"DES",       usmDESPrivProtocol,          OID_LENGTH(usmDESPrivProtocol)},
#endif
	};
	for (size_t idx = 0; idx < sizeof(protos)/sizeof(protos[0]); idx++)
		if (protos[idx].proto_oid && strcasecmp(name, protos[idx].label) == 0) { *len = protos[idx].oid_len; return protos[idx].proto_oid; }
	*len = 10; return usmAESPrivProtocol;
}

static netsnmp_session *open_snmp_session(const SnmpOpts *opts) {
	netsnmp_session session;
	snmp_sess_init(&session);
	char *hostport;
	xasprintf(&hostport, "%s%s:%s", opts->ip_version, opts->server_address, opts->port);
	session.peername = hostport;
	session.timeout = ((long)timeout_interval / opts->retries + 1) * 1000000L;
	session.retries = opts->retries;
	if (strcmp(opts->proto, "3") == 0) {
		session.version = SNMP_VERSION_3;
		if (strcmp(opts->seclevel, "authPriv") == 0)
			session.securityLevel = SNMP_SEC_LEVEL_AUTHPRIV;
		else if (strcmp(opts->seclevel, "authNoPriv") == 0)
			session.securityLevel = SNMP_SEC_LEVEL_AUTHNOPRIV;
		else
			session.securityLevel = SNMP_SEC_LEVEL_NOAUTH;
		session.securityName = strdup(opts->secname);
		session.securityNameLen = strlen(opts->secname);
		if (opts->context) {
			session.contextName = strdup(opts->context);
			session.contextNameLen = strlen(opts->context);
		}
		if (session.securityLevel >= SNMP_SEC_LEVEL_AUTHNOPRIV) {
			size_t plen;
			session.securityAuthProto = (oid *)lookup_auth_proto(opts->authproto, &plen);
			session.securityAuthProtoLen = plen;
			session.securityAuthKeyLen = USM_AUTH_KU_LEN;
			if (generate_Ku(session.securityAuthProto, session.securityAuthProtoLen,
			                (u_char *)opts->authpasswd, strlen(opts->authpasswd),
			                session.securityAuthKey, &session.securityAuthKeyLen) != SNMPERR_SUCCESS)
				return NULL;
		}
		if (session.securityLevel == SNMP_SEC_LEVEL_AUTHPRIV) {
			size_t plen;
			session.securityPrivProto = (oid *)lookup_priv_proto(opts->privproto, &plen);
			session.securityPrivProtoLen = plen;
			session.securityPrivKeyLen = USM_PRIV_KU_LEN;
			if (generate_Ku(session.securityAuthProto, session.securityAuthProtoLen,
			                (u_char *)opts->privpasswd, strlen(opts->privpasswd),
			                session.securityPrivKey, &session.securityPrivKeyLen) != SNMPERR_SUCCESS)
				return NULL;
		}
	} else {
		session.version = (strcmp(opts->proto, "2c") == 0) ? SNMP_VERSION_2c : SNMP_VERSION_1;
		session.community = (u_char *)strdup(opts->community);
		session.community_len = strlen(opts->community);
	}
	SOCK_STARTUP;
	return snmp_open(&session);
}


int
main (int argc, char **argv)
{
	time_t current_time;
	netsnmp_session *ss;

	OctetData current = {0};
	OctetData previous = {0};
	int state = STATE_UNKNOWN;
	int have_previous = 0;

	char oid_in[256], oid_out[256];
	char oid_name[256], oid_desc[256], oid_alias[256];
	char fetched_name[256] = "", fetched_desc[256] = "", fetched_alias[256] = "";

	setlocale (LC_ALL, "");
	bindtextdomain (PACKAGE, LOCALEDIR);
	textdomain (PACKAGE);

	snmp.port = DEFAULT_PORT;
	snmp.retries = DEFAULT_RETRIES;

	np_init( (char *) progname, argc, argv );
	argv = np_extra_opts (&argc, argv, progname);
	np_set_args(argc, argv);

	time(&current_time);

	if (process_arguments (argc, argv, &snmp, &cfg) == ERROR)
		usage4 (_("Could not parse arguments"));

	if (cfg.db_path == NULL)
		cfg.db_path = (char *)determine_db_path(&cfg, snmp.verbose);

	if (snmp.verbose)
		open_log();
	write_log(snmp.server_address, "--- check started (iface=%s, mode=%s, db=%s)",
	          cfg.oid_index,
	          cfg.i_name ? "i-name" : cfg.i_desc ? "i-desc" : cfg.i_alias ? "i-alias" : "i-num",
	          cfg.db_path);

	init_snmp("check_throughput");
	ss = open_snmp_session(&snmp);
	if (!ss) {
		write_log(snmp.server_address, "Failed to open SNMP session");
		close_log();
		printf("UNKNOWN - Failed to open SNMP session to %s\n", snmp.server_address);
		exit(STATE_UNKNOWN);
	}

	const char *primary_label = cfg.i_name  ? cfg.i_name  :
	                            cfg.i_desc  ? cfg.i_desc  :
	                            cfg.i_alias ? cfg.i_alias : NULL;
	const char *iface_label = primary_label ? primary_label : cfg.oid_index;

	if (primary_label) {
		if (db_read_state(cfg.db_path, snmp.server_address, primary_label, &previous, snmp.verbose) == STATE_OK
		    && previous.stored_i_num[0] != '\0') {
			cfg.oid_index = strdup(previous.stored_i_num);
			have_previous = 1;
			write_log(snmp.server_address, "DB hit: using stored ifIndex %s for '%s'", cfg.oid_index, iface_label);
			if (snmp.verbose)
				printf("Using stored ifIndex %s for interface '%s'\n", cfg.oid_index, iface_label);
		} else {
			write_log(snmp.server_address, "No DB entry for '%s', performing SNMP walk", primary_label);
			const char *walk_oid = cfg.i_name  ? OID_IF_NAME  :
			                       cfg.i_desc  ? OID_IF_DESC  : OID_IF_ALIAS;
			char *resolved = resolve_label_to_index(primary_label, walk_oid, ss);
			if (resolved == NULL) {
				write_log(snmp.server_address, "Walk FAILED: could not resolve '%s'", iface_label);
				close_log();
				printf("UNKNOWN - Could not resolve interface '%s' via SNMP walk\n", iface_label);
				exit(STATE_UNKNOWN);
			}
			write_log(snmp.server_address, "Walk resolved '%s' -> ifIndex %s", primary_label, resolved);
			cfg.oid_index = resolved;
		}
	} else {
		if (db_read_state(cfg.db_path, snmp.server_address, cfg.oid_index, &previous, snmp.verbose) == STATE_OK) {
			have_previous = 1;
			write_log(snmp.server_address, "DB hit for ifIndex %s (stored name='%s', desc='%s')",
			          cfg.oid_index, previous.stored_i_name, previous.stored_i_desc);
		}
	}

	const char *db_key = primary_label ? primary_label : cfg.oid_index;
	int use_64 = (cfg.explicit_counter_bits != 32);

	snprintf(oid_in,  sizeof(oid_in),  "%s.%s",
	         use_64 ? "1.3.6.1.2.1.31.1.1.1.6"  : "1.3.6.1.2.1.2.2.1.10", cfg.oid_index);
	snprintf(oid_out, sizeof(oid_out), "%s.%s",
	         use_64 ? "1.3.6.1.2.1.31.1.1.1.10" : "1.3.6.1.2.1.2.2.1.16", cfg.oid_index);
	current.counter_bits = use_64 ? 64 : 32;

	snprintf(oid_name,  sizeof(oid_name),  OID_IF_NAME  ".%s", cfg.oid_index);
	snprintf(oid_desc,  sizeof(oid_desc),  OID_IF_DESC  ".%s", cfg.oid_index);
	snprintf(oid_alias, sizeof(oid_alias), OID_IF_ALIAS ".%s", cfg.oid_index);

	write_log(snmp.server_address, "snmpget ifIndex=%s (counter_bits=%d)", cfg.oid_index, current.counter_bits);
	if (snmp.verbose)
		printf("snmpget %s:%s %s %s %s %s %s\n", snmp.server_address, snmp.port,
		       oid_in, oid_out, oid_name, oid_desc, oid_alias);

	current.timestamp = current_time;
	{
		netsnmp_pdu *pdu = snmp_pdu_create(SNMP_MSG_GET);
		netsnmp_pdu *response = NULL;
		oid parsed_oid[MAX_OID_LEN];
		size_t plen;
		const char *oid_strs[] = {oid_in, oid_out, oid_name, oid_desc, oid_alias};
		for (int j = 0; j < 5; j++) {
			plen = MAX_OID_LEN;
			if (snmp_parse_oid(oid_strs[j], parsed_oid, &plen))
				snmp_add_null_var(pdu, parsed_oid, plen);
		}

		int status = snmp_synch_response(ss, pdu, &response);
		if (status == STAT_TIMEOUT) {
			write_log(snmp.server_address, "snmpget TIMEOUT");
			if (response) snmp_free_pdu(response);
			close_log();
			printf("%s - SNMP timeout querying %s\n", state_text(timeout_state), snmp.server_address);
			exit(timeout_state);
		}
		if (status != STAT_SUCCESS || !response || response->errstat != SNMP_ERR_NOERROR) {
			write_log(snmp.server_address, "snmpget ERROR (status=%d)", status);
			if (response) snmp_free_pdu(response);
			close_log();
			printf("UNKNOWN - SNMP error querying %s\n", snmp.server_address);
			exit(STATE_UNKNOWN);
		}

		int resp_idx = 0, counter_no_such = 0, got_counters = 0;
		netsnmp_variable_list *vars;
		for (vars = response->variables; vars; vars = vars->next_variable, resp_idx++) {
			if (vars->type == SNMP_NOSUCHINSTANCE || vars->type == SNMP_NOSUCHOBJECT ||
			    vars->type == SNMP_ENDOFMIBVIEW) {
				if (resp_idx < 2) counter_no_such = 1;
				continue;
			}
			if (resp_idx < 2) {
				if (vars->type != ASN_COUNTER && vars->type != ASN_COUNTER64) {
					snmp_free_pdu(response);
					printf("UNKNOWN - Expected Counter type for OID %s\n", oid_strs[resp_idx]);
					exit(STATE_UNKNOWN);
				}
				unsigned long long val;
				if (vars->type == ASN_COUNTER64)
					val = ((unsigned long long)vars->val.counter64->high << 32) | vars->val.counter64->low;
				else
					val = (unsigned long long)*vars->val.integer;
				if (resp_idx == 0) current.in_octets = val;
				else current.out_octets = val;
				got_counters++;
			} else if (vars->type == ASN_OCTET_STR && vars->val.string) {
				char *dest = (resp_idx == 2) ? fetched_name : (resp_idx == 3) ? fetched_desc : fetched_alias;
				size_t len = vars->val_len < 255 ? vars->val_len : 255;
				while (len > 0 && vars->val.string[len - 1] == '\0') len--;
				memcpy(dest, vars->val.string, len);
				dest[len] = '\0';
			}
		}
		snmp_free_pdu(response);

		write_log(snmp.server_address, "Parsed: in_octets=%llu out_octets=%llu name='%s' desc='%s' alias='%s'",
		          current.in_octets, current.out_octets, fetched_name, fetched_desc, fetched_alias);

		if (primary_label && have_previous) {
			int needs_rewalk = counter_no_such;

			if (!needs_rewalk) {
				const char *check_val = cfg.i_name  ? fetched_name  :
				                        cfg.i_desc  ? fetched_desc  :
				                                      fetched_alias;
				if (check_val[0] != '\0' && strcmp(check_val, primary_label) != 0) {
					write_log(snmp.server_address, "Label mismatch: expected '%s', got '%s' — re-walking",
					          primary_label, check_val);
					needs_rewalk = 1;
				}
			}
			if (counter_no_such)
				write_log(snmp.server_address, "Counter returned 'No Such' on ifIndex %s — re-walking", cfg.oid_index);

			if (needs_rewalk) {
				const char *walk_oid = cfg.i_name  ? OID_IF_NAME  :
				                       cfg.i_desc  ? OID_IF_DESC  : OID_IF_ALIAS;
				char *resolved = resolve_label_to_index(primary_label, walk_oid, ss);
				if (resolved == NULL) {
					write_log(snmp.server_address, "Re-walk FAILED: '%s' no longer on device (was index %s)",
					          iface_label, previous.stored_i_num);
					close_log();
					printf("UNKNOWN - Interface '%s' (stored index %s) no longer found on device\n",
						iface_label, previous.stored_i_num);
					exit(STATE_UNKNOWN);
				}
				write_log(snmp.server_address, "Re-walk resolved '%s': ifIndex %s -> %s",
				          primary_label, previous.stored_i_num, resolved);
				OctetData reset = {0};
				reset.timestamp = current_time;
				reset.counter_bits = (cfg.explicit_counter_bits == 32) ? 32 : 64;
				cfg.oid_index = resolved;
				if (db_write_state(cfg.db_path, snmp.server_address, primary_label, &reset,
				                   resolved, cfg.i_name, cfg.i_desc, cfg.i_alias, snmp.verbose) != STATE_OK)
					write_log(snmp.server_address, "DB write FAILED (re-walk reset) for '%s' db=%s", iface_label, cfg.db_path);
				close_log();
				printf("OK - Interface '%s' ifIndex changed from %s to %s; new baseline established\n",
					iface_label, previous.stored_i_num, resolved);
				exit(STATE_OK);
			}
			write_log(snmp.server_address, "Label match confirmed for '%s' on ifIndex %s", primary_label, cfg.oid_index);
		}

		if (!primary_label && have_previous) {
			int meta_changed = 0;
			if (fetched_name[0] && previous.stored_i_name[0] && strcmp(fetched_name, previous.stored_i_name) != 0)
				meta_changed = 1;
			else if (fetched_desc[0] && previous.stored_i_desc[0] && strcmp(fetched_desc, previous.stored_i_desc) != 0)
				meta_changed = 1;

			if (meta_changed) {
				write_log(snmp.server_address, "Interface changed on ifIndex %s: name '%s'->'%s', desc '%s'->'%s'",
				          cfg.oid_index, previous.stored_i_name, fetched_name,
				          previous.stored_i_desc, fetched_desc);
				OctetData reset = {0};
				reset.timestamp = current_time;
				reset.counter_bits = current.counter_bits;
				if (db_write_state(cfg.db_path, snmp.server_address, db_key, &reset, NULL,
				                   fetched_name[0] ? fetched_name : NULL,
				                   fetched_desc[0] ? fetched_desc : NULL,
				                   fetched_alias[0] ? fetched_alias : NULL, snmp.verbose) != STATE_OK)
					write_log(snmp.server_address, "DB write FAILED (metadata change reset) for '%s' db=%s", db_key, cfg.db_path);
				close_log();
				printf("OK - Interface at ifIndex %s changed (was '%s', now '%s'); new baseline established\n",
					cfg.oid_index,
					previous.stored_i_name[0] ? previous.stored_i_name : previous.stored_i_desc,
					fetched_name[0] ? fetched_name : fetched_desc);
				exit(STATE_OK);
			}
		}

		if (counter_no_such || got_counters < 2) {
			printf(_("UNKNOWN - Failed to parse counter values from SNMP output\n"));
			exit(STATE_UNKNOWN);
		}
	}

	/* First run or wizard-seeded: establish baseline */
	if (!have_previous)
		have_previous = (db_read_state(cfg.db_path, snmp.server_address, db_key, &previous, snmp.verbose) == STATE_OK);
	if (!have_previous || previous.timestamp == 0) {
		write_log(snmp.server_address, "%s — writing baseline for '%s' (ifIndex %s)",
		          have_previous ? "Wizard-seeded record" : "First run", iface_label, cfg.oid_index);
		if (db_write_state(cfg.db_path, snmp.server_address, db_key, &current,
		                   cfg.oid_index,
		                   fetched_name[0] ? fetched_name : NULL,
		                   fetched_desc[0] ? fetched_desc : NULL,
		                   fetched_alias[0] ? fetched_alias : NULL, snmp.verbose) != STATE_OK)
			write_log(snmp.server_address, "DB write FAILED (baseline) for '%s' db=%s", db_key, cfg.db_path);
		close_log();
		printf("OK - Baseline established for %s interface %s (ifindex %s)\n",
			snmp.server_address, iface_label, cfg.oid_index);
		exit(STATE_OK);
	}

	const char *unit_str = (cfg.units && *cfg.units) ? cfg.units : DEFAULT_UNITS;
	time_t time_diff = current.timestamp - previous.timestamp;

	if (time_diff <= 0) {
		printf(_("UNKNOWN - Time delta between checks is zero or negative\n"));
		exit(STATE_UNKNOWN);
	}

	double val_in  = calc_bw(calculate_wraparound(current.in_octets,  previous.in_octets,  current.counter_bits), time_diff, unit_str);
	double val_out = calc_bw(calculate_wraparound(current.out_octets, previous.out_octets, current.counter_bits), time_diff, unit_str);

	state = max_state(
		cfg.thr_in  ? get_status(val_in,  cfg.thr_in)  : STATE_OK,
		cfg.thr_out ? get_status(val_out, cfg.thr_out) : STATE_OK);

	if (db_write_state(cfg.db_path, snmp.server_address, db_key, &current,
	                   cfg.oid_index,
	                   fetched_name[0] ? fetched_name : NULL,
	                   fetched_desc[0] ? fetched_desc : NULL,
	                   fetched_alias[0] ? fetched_alias : NULL, snmp.verbose) != STATE_OK)
		write_log(snmp.server_address, "DB write FAILED (throughput update) for '%s' db=%s", db_key, cfg.db_path);

	write_log(snmp.server_address, "Result: %s in=%.2f%s out=%.2f%s (dt=%lds, ifIndex=%s)",
	          state_text(state), val_in, unit_str, val_out, unit_str,
	          (long)time_diff, cfg.oid_index);
	close_log();

	printf("%s - In: %.2f %s, Out: %.2f %s | in=%.2f%s;%s;%s out=%.2f%s;%s;%s\n",
		state_text(state),
		val_in,  unit_str,
		val_out, unit_str,
		val_in,  unit_str,
		cfg.thr_in && cfg.thr_in->warning_string  ? cfg.thr_in->warning_string  : "",
		cfg.thr_in && cfg.thr_in->critical_string ? cfg.thr_in->critical_string : "",
		val_out, unit_str,
		cfg.thr_out && cfg.thr_out->warning_string  ? cfg.thr_out->warning_string  : "",
		cfg.thr_out && cfg.thr_out->critical_string ? cfg.thr_out->critical_string : "");

	exit(state);
}


int
process_arguments (int argc, char **argv, SnmpOpts *snmp, CheckOpts *cfg)
{
	int c;
	int option = 0;

	static struct option longopts[] = {
		STD_LONG_OPTS,
		{"community",     required_argument, 0, 'C'},
		{"units",         required_argument, 0, 'u'},
		{"port",          required_argument, 0, 'p'},
		{"retries",       required_argument, 0, 'e'},
		{"protocol",      required_argument, 0, 'P'},
		{"context",       required_argument, 0, 'N'},
		{"seclevel",      required_argument, 0, 'L'},
		{"secname",       required_argument, 0, 'U'},
		{"authproto",     required_argument, 0, 'a'},
		{"privproto",     required_argument, 0, 'x'},
		{"authpasswd",    required_argument, 0, 'A'},
		{"privpasswd",    required_argument, 0, 'X'},
		{"counter-bits",  required_argument, 0, L_COUNTER_BITS},
		{"db-path",       required_argument, 0, L_DB_PATH},
		{"i-num",         required_argument, 0, L_I_NUM},
		{"i-name",        required_argument, 0, L_I_NAME},
		{"i-desc",        required_argument, 0, L_I_DESC},
		{"i-alias",       required_argument, 0, L_I_ALIAS},
		{"ipv4",          no_argument,       0, '4'},
		{"ipv6",          no_argument,       0, '6'},
		{0, 0, 0, 0}
	};

	if (argc < 2)
		return ERROR;

	while (1) {
		c = getopt_long(argc, argv,
		    "hvVO46t:c:w:H:C:u:p:e:P:N:L:U:a:x:A:X:",
		    longopts, &option);

		if (c == -1 || c == EOF)
			break;

		switch (c) {
		case '?':
			usage5();
		case 'h':
			print_usage();
			exit(STATE_OK);
		case 'V':
			print_revision(progname, VERSION);
			exit(STATE_OK);
		case 'v':
			snmp->verbose++;
			break;
		case 'H':
			snmp->server_address = optarg;
			break;
		case 'C':
			snmp->community = optarg;
			break;
		case 'u':
			cfg->units = optarg;
			break;
		case 'p':
			snmp->port = optarg;
			break;
		case 'e':
			if (!is_integer(optarg))
				usage2(_("Retries must be a positive integer"), optarg);
			else
				snmp->retries = atoi(optarg);
			break;
		case 't':
			timeout_interval = parse_timeout_string(optarg);
			break;
		case 'w':
			cfg->warning_thresholds = optarg;
			break;
		case 'c':
			cfg->critical_thresholds = optarg;
			break;
		case 'P':
			snmp->proto = optarg;
			break;
		case 'N':
			snmp->context = optarg;
			break;
		case 'L':
			snmp->seclevel = optarg;
			break;
		case 'U':
			snmp->secname = optarg;
			break;
		case 'a':
			snmp->authproto = optarg;
			break;
		case 'x':
			snmp->privproto = optarg;
			break;
		case 'A':
			snmp->authpasswd = optarg;
			break;
		case 'X':
			snmp->privpasswd = optarg;
			break;
		case L_I_NUM:
			if (cfg->oid_index)
				usage4(_("Only one interface option allowed: --i-num, --i-name, --i-desc, or --i-alias"));
			cfg->oid_index = optarg;
			break;
		case L_I_NAME:
			if (cfg->oid_index)
				usage4(_("Only one interface option allowed: --i-num, --i-name, --i-desc, or --i-alias"));
			cfg->i_name = optarg;
			cfg->oid_index = optarg;
			break;
		case L_I_DESC:
			if (cfg->oid_index)
				usage4(_("Only one interface option allowed: --i-num, --i-name, --i-desc, or --i-alias"));
			cfg->i_desc = optarg;
			cfg->oid_index = optarg;
			break;
		case L_I_ALIAS:
			if (cfg->oid_index)
				usage4(_("Only one interface option allowed: --i-num, --i-name, --i-desc, or --i-alias"));
			cfg->i_alias = optarg;
			cfg->oid_index = optarg;
			break;
		case L_COUNTER_BITS:
			cfg->explicit_counter_bits = atoi(optarg);
			if (cfg->explicit_counter_bits != 32 && cfg->explicit_counter_bits != 64)
				usage2(_("--counter-bits must be 32 or 64"), optarg);
			break;
		case L_DB_PATH:
			cfg->custom_db_path = optarg;
			break;
		case '4':
			break;
		case '6':
			xasprintf(&snmp->ip_version, "udp6:");
			break;
		}
	}

	if (snmp->server_address == NULL)
		snmp->server_address = argv[optind];

	if (snmp->community == NULL)
		snmp->community = strdup(DEFAULT_COMMUNITY);

	return validate_arguments(snmp, cfg);
}


int
validate_arguments (SnmpOpts *snmp, CheckOpts *cfg)
{
	if (snmp->server_address == NULL)
		die(STATE_UNKNOWN, _("No host specified\n"));

	if (cfg->oid_index == NULL)
		die(STATE_UNKNOWN, _("An interface option is required: -i/--i-num, --i-name, --i-desc, or --i-alias\n"));

	if (cfg->units != NULL) {
		static const char *valid_units[] = {
			"bps", "kbps", "mbps", "gbps",
			"Bps", "KBps", "MBps", "GBps", NULL};
		int i, valid = 0;
		for (i = 0; valid_units[i]; i++) {
			if (strcmp(cfg->units, valid_units[i]) == 0) { valid = 1; break; }
		}
		if (!valid)
			die(STATE_UNKNOWN, _("Invalid units '%s'. Must be: bps, kbps, mbps, gbps, Bps, KBps, MBps, or GBps\n"), cfg->units);
	}

	if (cfg->warning_thresholds || cfg->critical_thresholds) {
		char *w_in_s  = NULL, *w_out_s  = NULL;
		char *c_in_s  = NULL, *c_out_s  = NULL;
		char *comma;

		if (cfg->warning_thresholds) {
			cfg->warning_thresholds = strdup(cfg->warning_thresholds);
			w_in_s = cfg->warning_thresholds;
			if ((comma = strchr(w_in_s, ',')) != NULL) {
				*comma  = '\0';
				w_out_s = comma + 1;
			}
		}
		if (cfg->critical_thresholds) {
			cfg->critical_thresholds = strdup(cfg->critical_thresholds);
			c_in_s = cfg->critical_thresholds;
			if ((comma = strchr(c_in_s, ',')) != NULL) {
				*comma  = '\0';
				c_out_s = comma + 1;
			}
		}

		set_thresholds(&cfg->thr_in, w_in_s, c_in_s);

		if (w_out_s || c_out_s)
			set_thresholds(&cfg->thr_out, w_out_s, c_out_s);
		else
			set_thresholds(&cfg->thr_out, w_in_s, c_in_s);
	}

	if (snmp->proto == NULL)
		xasprintf(&snmp->proto, DEFAULT_PROTOCOL);

	if (strcmp(snmp->proto, "1") != 0 && strcmp(snmp->proto, "2c") != 0 && strcmp(snmp->proto, "3") != 0)
		usage2(_("Invalid SNMP version"), snmp->proto);

	if (strcmp(snmp->proto, "3") == 0) {
		if (snmp->seclevel == NULL)
			xasprintf(&snmp->seclevel, "noAuthNoPriv");
		if (snmp->secname == NULL)
			die(STATE_UNKNOWN, _("Required parameter: %s\n"), "secname");
		if (strcmp(snmp->seclevel, "authNoPriv") == 0 || strcmp(snmp->seclevel, "authPriv") == 0) {
			if (snmp->authproto == NULL) xasprintf(&snmp->authproto, DEFAULT_AUTH_PROTOCOL);
			if (snmp->authpasswd == NULL) die(STATE_UNKNOWN, _("Required parameter: %s\n"), "authpasswd");
			if (strcmp(snmp->seclevel, "authPriv") == 0) {
				if (snmp->privproto == NULL) xasprintf(&snmp->privproto, DEFAULT_PRIV_PROTOCOL);
				if (snmp->privpasswd == NULL) die(STATE_UNKNOWN, _("Required parameter: %s\n"), "privpasswd");
				{
					int priv_ok = (strcasecmp(snmp->privproto, "AES")     == 0 ||
					               strcasecmp(snmp->privproto, "AES128")  == 0 ||
					               strcasecmp(snmp->privproto, "AES-128") == 0
#ifndef NETSNMP_DISABLE_DES
					               || strcasecmp(snmp->privproto, "DES") == 0
#endif
#ifdef NETSNMP_DRAFT_BLUMENTHAL_AES_04
					               || strcasecmp(snmp->privproto, "AES192")    == 0
					               || strcasecmp(snmp->privproto, "AES-192")   == 0
					               || strcasecmp(snmp->privproto, "AES256")    == 0
					               || strcasecmp(snmp->privproto, "AES-256")   == 0
					               || strcasecmp(snmp->privproto, "AES192C")   == 0
					               || strcasecmp(snmp->privproto, "AES-192-C") == 0
					               || strcasecmp(snmp->privproto, "AES256C")   == 0
					               || strcasecmp(snmp->privproto, "AES-256-C") == 0
#endif
					               );
					if (!priv_ok)
						die(STATE_UNKNOWN,
						    "Invalid privproto '%s'. Supported on this build: "
						    "AES AES128 AES-128"
#ifndef NETSNMP_DISABLE_DES
						    " DES"
#endif
#ifdef NETSNMP_DRAFT_BLUMENTHAL_AES_04
						    " AES192 AES-192 AES256 AES-256"
						    " AES192C AES-192-C AES256C AES-256-C"
#else
						    " (AES192/AES256 variants are not available:"
						    " this build of Net-SNMP was compiled without"
						    " Blumenthal AES support)"
#endif
						    "\n", snmp->privproto);
				}
			}
		} else if (strcmp(snmp->seclevel, "noAuthNoPriv") != 0) {
			usage2(_("Invalid seclevel"), snmp->seclevel);
		}
	}

	return OK;
}


void
print_usage (void)
{
	print_revision(progname, VERSION);
	printf(COPYRIGHT, copyright, email);
	printf("%s\n", _("Monitor interface throughput via SNMP (ifInOctets/ifOutOctets or ifHCIn/ifHCOut)"));
	printf("\n\n");
	printf("%s\n", _("Usage:"));
	printf("%s -H <host> {--i-num INDEX | --i-name NAME | --i-desc DESC | --i-alias ALIAS} [-w warn] [-c crit]\n", progname);
	printf("[-C community] [-u units] [-p port] [-P protocol] [-e retries]\n");
	printf("[-N context] [-L seclevel] [-U secname] [-a authproto] [-A authpasswd]\n");
	printf("[-x privproto] [-X privpasswd] [--counter-bits 32|64]\n");
	printf("[--db-path PATH]\n");
	printf(UT_HELP_VRSN);
	printf(UT_EXTRA_OPTS);
	printf(UT_IPv46);
	printf(UT_HOST_PORT, 'p', DEFAULT_PORT);

	printf (" %s\n", "-P, --protocol [1|2c|3]");
	printf ("    %s\n", _("SNMP protocol version"));
	printf (" %s\n", "-C, --community STRING");
	printf ("    %s (%s \"%s\")\n", _("Community string for SNMP communication"), _("default is"), DEFAULT_COMMUNITY);
	printf (" %s\n", "-N, --context CONTEXT");
	printf ("    %s\n", _("SNMPv3 context"));
	printf (" %s\n", "-L, --seclevel [noAuthNoPriv|authNoPriv|authPriv]");
	printf ("    %s\n", _("SNMPv3 security level"));
	printf (" %s\n", "-U, --secname USERNAME");
	printf ("    %s\n", _("SNMPv3 username"));
	printf (" %s\n", "-a, --authproto [MD5|SHA|SHA224|SHA256|SHA384|SHA512]");
	printf ("    %s\n", _("SNMPv3 auth protocol"));
	printf (" %s\n", "-x, --privproto [AES|AES192|AES192C|AES256|AES256C]");
	printf ("    %s\n", _("SNMPv3 priv protocol"));
	printf (" %s\n", "-A, --authpasswd PASSWORD");
	printf ("    %s\n", _("SNMPv3 authentication password"));
	printf (" %s\n", "-X, --privpasswd PASSWORD");
	printf ("    %s\n", _("SNMPv3 privacy password"));

	printf ("\n");
	printf (" %s\n", "--i-num INDEX");
	printf ("    %s\n", _("Interface index (numeric ifIndex) appended to the counter OIDs (required)."));
	printf (" %s\n", "--i-name NAME");
	printf ("    %s\n", _("Identify interface by ifName (e.g. 'Gi0/1'). Must be the exact ifName string."));
	printf (" %s\n", "--i-desc DESCRIPTION");
	printf ("    %s\n", _("Identify interface by ifDescr (e.g. 'GigabitEthernet0/1')."));
	printf (" %s\n", "--i-alias ALIAS");
	printf ("    %s\n", _("Identify interface by ifAlias (user-assigned description)."));
	printf ("    %s\n", _("Note: --i-name, --i-desc, and --i-alias perform an SNMP walk to resolve the\n"
			"      label to a numeric ifIndex before polling.  If the resolved index differs\n"
			"      from the stored index, the ifIndex changed (device reboot) and a new\n"
			"      baseline is automatically established."));
	printf (" %s\n", "-u, --units STRING");
	printf ("    %s (%s \"%s\")\n",
		_("Units for throughput output"), _("default is"), DEFAULT_UNITS);
	printf ("    %s\n", _("Options: bps, kbps, mbps, gbps (bits/sec) or Bps, KBps, MBps, GBps (bytes/sec)"));
	printf (" %s\n", "-w, --warning THRESHOLD[,THRESHOLD]");
	printf ("    %s\n", _("Warning threshold for inbound[,outbound] throughput"));
	printf (" %s\n", "-c, --critical THRESHOLD[,THRESHOLD]");
	printf ("    %s\n", _("Critical threshold for inbound[,outbound] throughput"));
	printf (" %s\n", "--counter-bits BITS");
	printf ("    %s\n", _("Counter size: 32 or 64 bits (default: 64)"));
	printf ("    %s\n", _("Use 32 for ifIn/OutOctets, 64 for ifHCIn/OutOctets"));
	printf (" %s\n", "--db-path PATH");
	printf ("    %s\n", _("Custom path for SQLite database (directory or full file path)"));
	printf ("    %s\n", _("Default: /usr/local/nagios/var, then /var/tmp"));

	printf(UT_CONN_TIMEOUT, DEFAULT_SOCKET_TIMEOUT);
	printf (" %s\n", "-e, --retries INTEGER");
	printf ("    %s\n", _("Number of SNMP retries (default 5)"));
	printf(UT_VERBOSE);

	printf("\n%s\n", _("Notes:"));
	printf(" %s\n", _("- On the first run a baseline is established and the plugin returns OK."));
	printf(" %s\n", _("- Subsequent runs calculate the rate of change between the two most recent samples."));
	printf(" %s\n", _("- State is stored in a local SQLite database (one row per host+interface)."));
	printf(" %s\n", _("- Counter wrap-around is handled for both 32-bit and 64-bit counters."));
	printf(" %s\n", _("- Default counter size is 64-bit; use --counter-bits=32 for 32-bit counters."));
	printf(" %s\n", _("- Use --i-name/--i-desc/--i-alias to identify by label (walks to resolve ifIndex)."));

	printf(UT_SUPPORT);
}


char *resolve_label_to_index(const char *label, const char *walk_oid, netsnmp_session *ss)
{
	oid root[MAX_OID_LEN], name[MAX_OID_LEN];
	size_t rootlen = MAX_OID_LEN, name_len;
	char *result = NULL;

	if (!snmp_parse_oid(walk_oid, root, &rootlen))
		return NULL;
	memcpy(name, root, rootlen * sizeof(oid));
	name_len = rootlen;

	while (1) {
		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 || response->errstat != SNMP_ERR_NOERROR) {
			if (response) snmp_free_pdu(response);
			break;
		}

		netsnmp_variable_list *vars = response->variables;
		if (!vars || vars->type == SNMP_ENDOFMIBVIEW || vars->type == SNMP_NOSUCHOBJECT ||
		    vars->name_length < rootlen ||
		    memcmp(root, vars->name, rootlen * sizeof(oid)) != 0) {
			snmp_free_pdu(response);
			break;
		}

		if (vars->type == ASN_OCTET_STR && vars->val.string) {
			size_t vlen = vars->val_len;
			while (vlen > 0 && vars->val.string[vlen - 1] == '\0') vlen--;
			while (vlen > 0 && isspace(vars->val.string[vlen - 1])) vlen--;
			if (strlen(label) == vlen && memcmp(vars->val.string, label, vlen) == 0) {
				char idx[32];
				snprintf(idx, sizeof(idx), "%lu", (unsigned long)vars->name[vars->name_length - 1]);
				result = strdup(idx);
				snmp_free_pdu(response);
				break;
			}
		}

		memcpy(name, vars->name, vars->name_length * sizeof(oid));
		name_len = vars->name_length;
		snmp_free_pdu(response);
	}

	return result;
}


void sanitize_host_for_table(const char *host, char *out, size_t out_sz) {
	if (out_sz == 0) return;
	size_t j = snprintf(out, out_sz, "host_");
	if (j >= out_sz) return;
	for (size_t i = 0; host[i] && j < out_sz - 1; i++)
		out[j++] = (isalnum((unsigned char)host[i]) || host[i] == '_') ? host[i] : '_';
	out[j] = '\0';
}

const char *determine_db_path(const CheckOpts *cfg, int verbose) {
	static char db_file[512];
	const char *paths[] = {"/usr/local/nagios/var", "/var/tmp"};
	const char *db_name = "nagios_snmp_throughput.db";
	int i;

	if (cfg->custom_db_path != NULL) {
		struct stat st;
		if (stat(cfg->custom_db_path, &st) == 0 && S_ISDIR(st.st_mode))
			snprintf(db_file, sizeof(db_file), "%s/%s", cfg->custom_db_path, db_name);
		else
			snprintf(db_file, sizeof(db_file), "%s", cfg->custom_db_path);
		if (verbose > 1) printf("DEBUG: Using custom database path: %s\n", db_file);
		return db_file;
	}

	mkdir(paths[0], 0755);

	for (i = 0; i < 2; i++) {
		if (access(paths[i], W_OK) == 0) {
			snprintf(db_file, sizeof(db_file), "%s/%s", paths[i], db_name);
			if (verbose > 1) printf("DEBUG: Using database path: %s\n", db_file);
			return db_file;
		}
	}
	/* Fallback: use first path even if not writable — sqlite open will fail with a logged error */
	snprintf(db_file, sizeof(db_file), "%s/%s", paths[0], db_name);
	return db_file;
}

int db_read_state(const char *path, const char *host, const char *iface, OctetData *data, int verbose) {
	sqlite3 *db;
	sqlite3_stmt *stmt;
	int rc;
	char tbl[128];
	char query[256];

	sanitize_host_for_table(host, tbl, sizeof(tbl));
	snprintf(query, sizeof(query),
		"SELECT timestamp, in_octets, out_octets, counter_bits, "
		"COALESCE(i_num,''), COALESCE(i_name,''), COALESCE(i_desc,''), COALESCE(i_alias,'') "
		"FROM \"%s\" WHERE interface = ?", tbl);

	rc = sqlite3_open_v2(path, &db, SQLITE_OPEN_READONLY, NULL);
	if (rc != SQLITE_OK) {
		if (verbose) fprintf(stderr, "Cannot open database: %s\n", sqlite3_errmsg(db));
		sqlite3_close(db);
		return STATE_UNKNOWN;
	}
	sqlite3_busy_timeout(db, 5000);
	rc = sqlite3_prepare_v2(db, query, -1, &stmt, 0);
	if (rc != SQLITE_OK) {
		if (verbose) fprintf(stderr, "Prepare failed: %s\n", sqlite3_errmsg(db));
		sqlite3_close(db);
		return STATE_UNKNOWN;
	}
	sqlite3_bind_text(stmt, 1, iface, -1, SQLITE_STATIC);
	rc = sqlite3_step(stmt);
	if (rc == SQLITE_ROW) {
		data->timestamp    = sqlite3_column_int64(stmt, 0);
		data->in_octets    = sqlite3_column_int64(stmt, 1);
		data->out_octets   = sqlite3_column_int64(stmt, 2);
		data->counter_bits = sqlite3_column_int  (stmt, 3);
		#define SCOL(f, idx) do { const char *s = (const char *)sqlite3_column_text(stmt, idx); \
			snprintf(data->f, sizeof(data->f), "%s", s ? s : ""); } while(0)
		SCOL(stored_i_num, 4); SCOL(stored_i_name, 5);
		SCOL(stored_i_desc, 6); SCOL(stored_i_alias, 7);
		#undef SCOL
		sqlite3_finalize(stmt);
		sqlite3_close(db);
		return STATE_OK;
	}
	sqlite3_finalize(stmt);
	sqlite3_close(db);
	return STATE_UNKNOWN;
}

int db_write_state(const char *path, const char *host, const char *iface, const OctetData *data,
                   const char *lbl_num, const char *lbl_name, const char *lbl_desc, const char *lbl_alias, int verbose) {
	sqlite3 *db;
	sqlite3_stmt *stmt;
	char *err_msg = NULL;
	int rc;
	char tbl[128];

	sanitize_host_for_table(host, tbl, sizeof(tbl));
	rc = sqlite3_open_v2(path, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL);
	if (rc != SQLITE_OK) {
		write_log(host, "db_write_state: cannot open '%s': %s", path, sqlite3_errmsg(db));
		sqlite3_close(db);
		return STATE_UNKNOWN;
	}
	if (sqlite3_db_readonly(db, "main") > 0) {
		write_log(host, "db_write_state: '%s' opened read-only — "
		          "nagios user lacks write permission; fix with: "
		          "chown nagios:nagios '%s' && chmod 0644 '%s'",
		          path, path, path);
		sqlite3_close(db);
		return STATE_UNKNOWN;
	}
	sqlite3_busy_timeout(db, 5000);

	/* WAL mode: readers never block writers and writers never block readers.
	 * This prevents SQLITE_BUSY on DB connections on Ubuntu 22.
	*/
	sqlite3_exec(db, "PRAGMA journal_mode=WAL", 0, 0, NULL);

	char ddl[512];
	snprintf(ddl, sizeof(ddl),
		"CREATE TABLE IF NOT EXISTS \"%s\" ("
		"interface TEXT NOT NULL PRIMARY KEY, i_num TEXT, i_name TEXT, i_desc TEXT, i_alias TEXT, "
		"timestamp INTEGER NOT NULL, in_octets INTEGER NOT NULL, out_octets INTEGER NOT NULL, "
		"counter_bits INTEGER NOT NULL)", tbl);
	rc = sqlite3_exec(db, ddl, 0, 0, &err_msg);
	if (rc != SQLITE_OK) {
		write_log(host, "db_write_state: CREATE TABLE failed: %s", err_msg);
		sqlite3_free(err_msg);
		sqlite3_close(db);
		return STATE_UNKNOWN;
	}

	char query[1024];
	snprintf(query, sizeof(query),
		"INSERT INTO \"%s\" "
		"(interface, i_num, i_name, i_desc, i_alias, timestamp, in_octets, out_octets, counter_bits) "
		"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) "
		"ON CONFLICT(interface) DO UPDATE SET "
		"i_num=excluded.i_num, i_name=excluded.i_name, i_desc=excluded.i_desc, "
		"i_alias=CASE WHEN excluded.i_alias='' THEN i_alias ELSE excluded.i_alias END, "
		"timestamp=excluded.timestamp, in_octets=excluded.in_octets, "
		"out_octets=excluded.out_octets, counter_bits=excluded.counter_bits", tbl);
	rc = sqlite3_prepare_v2(db, query, -1, &stmt, 0);
	if (rc != SQLITE_OK) {
		write_log(host, "db_write_state: prepare failed for '%s': %s", iface, sqlite3_errmsg(db));
		sqlite3_close(db);
		return STATE_UNKNOWN;
	}
	sqlite3_bind_text (stmt, 1, iface,                      -1, SQLITE_STATIC);
	sqlite3_bind_text (stmt, 2, lbl_num   ? lbl_num   : "", -1, SQLITE_STATIC);
	sqlite3_bind_text (stmt, 3, lbl_name  ? lbl_name  : "", -1, SQLITE_STATIC);
	sqlite3_bind_text (stmt, 4, lbl_desc  ? lbl_desc  : "", -1, SQLITE_STATIC);
	sqlite3_bind_text (stmt, 5, lbl_alias ? lbl_alias : "", -1, SQLITE_STATIC);
	sqlite3_bind_int64(stmt, 6, data->timestamp);
	sqlite3_bind_int64(stmt, 7, data->in_octets);
	sqlite3_bind_int64(stmt, 8, data->out_octets);
	sqlite3_bind_int  (stmt, 9, data->counter_bits);

	rc = sqlite3_step(stmt);
	if (rc != SQLITE_DONE)
		write_log(host, "db_write_state: step failed for '%s': %s", iface, sqlite3_errmsg(db));
	sqlite3_finalize(stmt);
	sqlite3_close(db);
	return (rc == SQLITE_DONE) ? STATE_OK : STATE_UNKNOWN;
}

unsigned long long calculate_wraparound(unsigned long long current,
                                        unsigned long long previous,
                                        int counter_bits) {
	if (current >= previous) return current - previous;
	return (counter_bits == 64) ? 0ULL : ((0xFFFFFFFFUL - previous) + current + 1);
}

/* Returns throughput scaled to the requested unit */
double calc_bw(unsigned long long octet_diff, time_t time_diff, const char *unit) {
	if (time_diff <= 0) return 0.0;
	double bps = (double)octet_diff * (unit && strstr(unit, "Bp") ? 1.0 : 8.0) / (double)time_diff;
	if (unit) {
		if      (strcmp(unit, "kbps") == 0 || strcmp(unit, "KBps") == 0) bps = bps / 1e3;
		else if (strcmp(unit, "mbps") == 0 || strcmp(unit, "MBps") == 0) bps = bps / 1e6;
		else if (strcmp(unit, "gbps") == 0 || strcmp(unit, "GBps") == 0) bps = bps / 1e9;
	}
	return bps < 0.0 ? 0.0 : bps;
}
