/*****************************************************************************
 *
 * INFLUX_PERF_API_V1.C - NEB module that posts performance data to InfluxDB v1 using line protocol
 *
 * Copyright (c) 2024 Nagios Core Contributors
 *
 * Description:
 *
 * This NEB module sends Nagios monitoring performance data to InfluxDB using the line
 * protocol format via HTTP POST using libcurl. The InfluxDB write endpoint URL is
 * specified as an argument when loading the module.
 *
 * Requirements:
 *
 *     libcurl development headers (libcurl-devel or libcurl4-openssl-dev)
 *
 * Instructions:
 *
 * Compile with the following command:
 *
 *     cd nagioscore
 *     ./configure
 *     cd module
 *     gcc -shared -fPIC -o influx_perf_api_v1.o influx_perf_api_v1.c -I.. -lcurl -lpthread
 *
 * Or if using the Makefile, ensure it links with -lcurl:
 *
 *     make influx_perf_api_v1.o LDFLAGS="-lcurl"
 *
 * Load in nagios.cfg with URL argument:
 *
 *     broker_module=/path/to/influx_perf_api_v1.o http://influxdb:8086/write?db=nagios
 *
 * Optional arguments (space-separated):
 *     url - InfluxDB write endpoint URL (required, first argument)
 *     timeout=N - Timeout in seconds (default: 5)
 *     batch=N - Batch N events before sending (default: 10, max: 100)
 *     ssl_verify=0|1 - Verify SSL certificates (default: 1)
 *     ca_cert=PATH - Path to CA certificate bundle
 *     client_cert=PATH - Path to client certificate file
 *     client_key=PATH - Path to client private key file
 *
 * Example:
 *     broker_module=/path/to/influx_perf_api_v1.o http://influxdb:8086/write?db=nagios timeout=10 batch=10
 *     broker_module=/path/to/influx_perf_api_v1.o https://influxdb:8086/write?db=nagios ssl_verify=0
 *     broker_module=/path/to/influx_perf_api_v1.o https://influxdb:8086/write?db=nagios ca_cert=/etc/ssl/ca.pem
 *
 *****************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <pthread.h>
#include <ctype.h>
#include <curl/curl.h>

/* include (minimum required) event broker header files */
#include "include/nebmodules.h"
#include "include/nebcallbacks.h"

/* include other event broker header files that we need for our work */
#include "include/nebstructs.h"
#include "include/broker.h"

/* include some Nagios stuff as well */
#include "include/config.h"
#include "include/common.h"
#include "include/nagios.h"

/* specify event broker API version (required) */
NEB_API_VERSION(CURRENT_NEB_API_VERSION);

#define MAX_LINE_SIZE 8192
#define MAX_BATCH_SIZE 100
#define DEFAULT_TIMEOUT 5
#define DEFAULT_BATCH_SIZE 10
#define MAX_URL_LENGTH 2048
#define MAX_PERFDATA_METRICS 64

/* parsed performance data metric */
typedef struct {
	char label[256];
	double value;
	char uom[32];
	char raw_value[64];
	double warn;
	double crit;
	double min;
	double max;
	int has_warn;
	int has_crit;
	int has_min;
	int has_max;
} perfdata_metric;

/* module configuration */
typedef struct {
	char *url;
	int timeout;
	int batch_size;
	char **batch_buffer;
	int batch_count;
	int ssl_verify;
	char *ca_cert;
	char *client_cert;
	char *client_key;
} influxdb_config;

void *influxdb_module_handle = NULL;
influxdb_config config;
pthread_mutex_t batch_mutex = PTHREAD_MUTEX_INITIALIZER;

/* function prototypes */
int influxdb_handle_data(int, void *);
void influx_escape_tag(const char *src, char *dest, size_t dest_size);
void influx_escape_field(const char *src, char *dest, size_t dest_size);
long long get_timestamp_ns(struct timeval *tv);
int send_line_to_server(const char *line_data);
int send_batch_to_server(void);
void add_to_batch(const char *line_data);
void parse_arguments(char *args);
void cleanup_config(void);
int validate_url(const char *url);
int parse_perfdata_metrics(const char *perf_data, perfdata_metric *metrics, int max_metrics);
const char *host_state_name(int state_id);
const char *service_state_name(int state_id);
void format_iso_timestamp(time_t t, char *buf, size_t buf_size);


/* validate URL format */
int validate_url(const char *url) {
	if (url == NULL || strlen(url) == 0) {
		return 0;
	}

	if (strlen(url) > MAX_URL_LENGTH) {
		write_to_all_logs("influxdb: ERROR - URL exceeds maximum length", NSLOG_RUNTIME_ERROR);
		return 0;
	}

	/* Check for basic http:// or https:// prefix */
	if (strncmp(url, "http://", 7) != 0 && strncmp(url, "https://", 8) != 0) {
		write_to_all_logs("influxdb: ERROR - URL must start with http:// or https://", NSLOG_RUNTIME_ERROR);
		return 0;
	}

	return 1;
}

/* return human-readable host state name */
const char *host_state_name(int state_id) {
	switch (state_id) {
		case 0:  return "UP";
		case 1:  return "DOWN";
		case 2:  return "UNREACHABLE";
		default: return "DOWN";
	}
}

/* return human-readable service state name */
const char *service_state_name(int state_id) {
	switch (state_id) {
		case 0:  return "OK";
		case 1:  return "WARNING";
		case 2:  return "CRITICAL";
		case 3:  return "UNKNOWN";
		default: return "UNKNOWN";
	}
}

/* format a time_t as ISO 8601 UTC string: YYYY-MM-DDTHH:MM:SSZ */
void format_iso_timestamp(time_t t, char *buf, size_t buf_size) {
	struct tm tm_buf;

	if (buf == NULL || buf_size == 0) {
		return;
	}

	if (gmtime_r(&t, &tm_buf) == NULL) {
		buf[0] = '\0';
		return;
	}

	strftime(buf, buf_size, "%Y-%m-%dT%H:%M:%SZ", &tm_buf);
}

/* parse module arguments */
void parse_arguments(char *args) {
	char *token;
	char *saveptr;
	char *args_copy;
	int first_arg = 1;

	/* set defaults */
	config.url = NULL;
	config.timeout = DEFAULT_TIMEOUT;
	config.batch_size = DEFAULT_BATCH_SIZE;
	config.batch_buffer = NULL;
	config.batch_count = 0;
	config.ssl_verify = 1;
	config.ca_cert = NULL;
	config.client_cert = NULL;
	config.client_key = NULL;

	if (args == NULL || strlen(args) == 0) {
		write_to_all_logs("influxdb: ERROR - No URL specified. Usage: broker_module=/path/to/influx_perf_api_v1.o <url> [timeout=N] [batch=N]", NSLOG_RUNTIME_ERROR);
		return;
	}

	args_copy = strdup(args);
	if (args_copy == NULL) {
		write_to_all_logs("influxdb: ERROR - Failed to allocate memory for argument parsing", NSLOG_RUNTIME_ERROR);
		return;
	}

	token = strtok_r(args_copy, " \t", &saveptr);

	while (token != NULL) {
		if (first_arg) {
			/* First argument is the URL */
			config.url = strdup(token);
			if (config.url == NULL) {
				write_to_all_logs("influxdb: ERROR - Failed to allocate memory for URL", NSLOG_RUNTIME_ERROR);
			} else if (!validate_url(config.url)) {
				free(config.url);
				config.url = NULL;
			}
			first_arg = 0;
		} else if (strncmp(token, "timeout=", 8) == 0) {
			config.timeout = atoi(token + 8);
			if (config.timeout < 1) config.timeout = DEFAULT_TIMEOUT;
		} else if (strncmp(token, "batch=", 6) == 0) {
			config.batch_size = atoi(token + 6);
			if (config.batch_size < 1) config.batch_size = 1;
			if (config.batch_size > MAX_BATCH_SIZE) config.batch_size = MAX_BATCH_SIZE;
		} else if (strncmp(token, "ssl_verify=", 11) == 0) {
			config.ssl_verify = atoi(token + 11);
		} else if (strncmp(token, "ca_cert=", 8) == 0) {
			config.ca_cert = strdup(token + 8);
			if (config.ca_cert == NULL) {
				write_to_all_logs("influxdb: ERROR - Failed to allocate memory for ca_cert", NSLOG_RUNTIME_ERROR);
			}
		} else if (strncmp(token, "client_cert=", 12) == 0) {
			config.client_cert = strdup(token + 12);
			if (config.client_cert == NULL) {
				write_to_all_logs("influxdb: ERROR - Failed to allocate memory for client_cert", NSLOG_RUNTIME_ERROR);
			}
		} else if (strncmp(token, "client_key=", 11) == 0) {
			config.client_key = strdup(token + 11);
			if (config.client_key == NULL) {
				write_to_all_logs("influxdb: ERROR - Failed to allocate memory for client_key", NSLOG_RUNTIME_ERROR);
			}
		}
		token = strtok_r(NULL, " \t", &saveptr);
	}

	free(args_copy);

	/* allocate batch buffer if batching is enabled */
	if (config.batch_size > 1) {
		config.batch_buffer = (char **)calloc(config.batch_size, sizeof(char *));
		if (config.batch_buffer == NULL) {
			write_to_all_logs("influxdb: ERROR - Failed to allocate batch buffer", NSLOG_RUNTIME_ERROR);
			config.batch_size = 1;
		}
	}
}

/* cleanup configuration */
void cleanup_config(void) {
	int i;

	if (config.url) {
		free(config.url);
		config.url = NULL;
	}

	if (config.ca_cert) {
		free(config.ca_cert);
		config.ca_cert = NULL;
	}

	if (config.client_cert) {
		free(config.client_cert);
		config.client_cert = NULL;
	}

	if (config.client_key) {
		free(config.client_key);
		config.client_key = NULL;
	}

	pthread_mutex_lock(&batch_mutex);
	if (config.batch_buffer) {
		/* send any remaining batched events */
		if (config.batch_count > 0) {
			send_batch_to_server();
		}
		/* free batch buffer */
		for (i = 0; i < config.batch_size; i++) {
			if (config.batch_buffer[i]) {
				free(config.batch_buffer[i]);
			}
		}
		free(config.batch_buffer);
		config.batch_buffer = NULL;
	}
	pthread_mutex_unlock(&batch_mutex);

	/* destroy the mutex */
	pthread_mutex_destroy(&batch_mutex);
}

/* this function gets called when the module is loaded by the event broker */
int nebmodule_init(int flags, char *args, nebmodule *handle) {
	char temp_buffer[1024];

	/* save our handle */
	influxdb_module_handle = handle;

	/* initialize libcurl globally */
	if (curl_global_init(CURL_GLOBAL_ALL) != 0) {
		write_to_all_logs("influxdb: ERROR - Failed to initialize libcurl", NSLOG_RUNTIME_ERROR);
		return -1;
	}

	/* set some info - this is completely optional */
	neb_set_module_info(influxdb_module_handle, NEBMODULE_MODINFO_TITLE, "influx_perf_api_v1");
	neb_set_module_info(influxdb_module_handle, NEBMODULE_MODINFO_AUTHOR, "Nagios Core Contributors");
	neb_set_module_info(influxdb_module_handle, NEBMODULE_MODINFO_COPYRIGHT, "Copyright (c) 2026");
	neb_set_module_info(influxdb_module_handle, NEBMODULE_MODINFO_VERSION, "1.0");
	neb_set_module_info(influxdb_module_handle, NEBMODULE_MODINFO_LICENSE, "GPL v2");
	neb_set_module_info(influxdb_module_handle, NEBMODULE_MODINFO_DESC, "Posts Nagios performance data to InfluxDB using line protocol");

	/* parse arguments */
	parse_arguments(args);

	if (config.url == NULL) {
		write_to_all_logs("influxdb: ERROR - Module not initialized. No URL specified.", NSLOG_RUNTIME_ERROR);
		return -1;
	}

	/* log module initialization */
	snprintf(temp_buffer, sizeof(temp_buffer) - 1,
		"influxdb: Module initialized - URL=%s, timeout=%ds, batch=%d, ssl_verify=%d",
		config.url, config.timeout, config.batch_size, config.ssl_verify);
	temp_buffer[sizeof(temp_buffer) - 1] = '\0';
	write_to_all_logs(temp_buffer, NSLOG_INFO_MESSAGE);

	/* log SSL certificate configuration if present */
	if (config.ca_cert || config.client_cert) {
		snprintf(temp_buffer, sizeof(temp_buffer) - 1,
			"influxdb: SSL config - ca_cert=%s, client_cert=%s, client_key=%s",
			config.ca_cert ? config.ca_cert : "none",
			config.client_cert ? config.client_cert : "none",
			config.client_key ? config.client_key : "none");
		temp_buffer[sizeof(temp_buffer) - 1] = '\0';
		write_to_all_logs(temp_buffer, NSLOG_INFO_MESSAGE);
	}

	/* register for service check and host check callbacks only */
	if (neb_register_callback(NEBCALLBACK_SERVICE_CHECK_DATA, influxdb_module_handle, 0, influxdb_handle_data) != OK) {
		write_to_all_logs("influxdb: Failed to register callback for SERVICE_CHECK_DATA", NSLOG_RUNTIME_ERROR);
	}
	if (neb_register_callback(NEBCALLBACK_HOST_CHECK_DATA, influxdb_module_handle, 0, influxdb_handle_data) != OK) {
		write_to_all_logs("influxdb: Failed to register callback for HOST_CHECK_DATA", NSLOG_RUNTIME_ERROR);
	}

	write_to_all_logs("influxdb: Registered for service check and host check callbacks", NSLOG_INFO_MESSAGE);

	return 0;
}

/* this function gets called when the module is unloaded by the event broker */
int nebmodule_deinit(int flags, int reason) {
	/* deregister for events we previously registered for */
	neb_deregister_callback(NEBCALLBACK_SERVICE_CHECK_DATA, influxdb_handle_data);
	neb_deregister_callback(NEBCALLBACK_HOST_CHECK_DATA, influxdb_handle_data);

	/* log module shutdown */
	write_to_all_logs("influxdb: Module deinitialized", NSLOG_INFO_MESSAGE);

	/* cleanup */
	cleanup_config();

	/* cleanup libcurl */
	curl_global_cleanup();

	return 0;
}

/* escape special characters in InfluxDB tag keys and values */
void influx_escape_tag(const char *src, char *dest, size_t dest_size) {
	size_t i = 0, j = 0;

	if (src == NULL || dest == NULL || dest_size == 0) {
		if (dest && dest_size > 0)
			dest[0] = '\0';
		return;
	}

	/* In InfluxDB line protocol, tag keys and values must escape: commas and equal signs
	 * Spaces, tabs, and dashes are replaced with underscores */
	while (src[i] != '\0' && j < dest_size - 2) {
		if (src[i] == ' ' || src[i] == '\t' || src[i] == '-') {
			dest[j++] = '_';
		} else if (src[i] == ',' || src[i] == '=') {
			if (j < dest_size - 3) {
				dest[j++] = '\\';
				dest[j++] = src[i];
			}
		} else {
			dest[j++] = src[i];
		}
		i++;
	}
	dest[j] = '\0';
}

/* escape special characters in InfluxDB field string values */
void influx_escape_field(const char *src, char *dest, size_t dest_size) {
	size_t i = 0, j = 0;

	if (src == NULL || dest == NULL || dest_size == 0) {
		if (dest && dest_size > 0)
			dest[0] = '\0';
		return;
	}

	/* In InfluxDB line protocol, field string values must escape: double quotes and backslashes */
	while (src[i] != '\0' && j < dest_size - 2) {
		if (src[i] == '"' || src[i] == '\\') {
			if (j < dest_size - 3) {
				dest[j++] = '\\';
				dest[j++] = src[i];
			}
		} else {
			dest[j++] = src[i];
		}
		i++;
	}
	dest[j] = '\0';
}

/* convert timestamp to nanoseconds for InfluxDB */
long long get_timestamp_ns(struct timeval *tv) {
	if (tv) {
		return ((long long)tv->tv_sec * 1000000000LL) + ((long long)tv->tv_usec * 1000LL);
	} else {
		return (long long)time(NULL) * 1000000000LL;
	}
}

/* parse Nagios performance data into an array of perfdata_metric structs
 * Format: label=value[UOM];[warn];[crit];[min];[max]
 * Labels may be single-quoted per the Nagios plugin API: 'label with spaces'=value
 * Multiple metrics separated by spaces
 * Returns number of metrics parsed, or -1 on error
 */
int parse_perfdata_metrics(const char *perf_data, perfdata_metric *metrics, int max_metrics) {
	const char *p;
	const char *label_start;
	const char *label_end;
	const char *val_start;
	const char *val_end;
	char value_str[256];
	char *endptr;
	size_t label_len;
	size_t value_len;
	int metric_count = 0;
	int i;

	if (perf_data == NULL || metrics == NULL || max_metrics <= 0) {
		return -1;
	}

	p = perf_data;

	while (*p != '\0' && metric_count < max_metrics) {
		/* skip leading whitespace between metrics */
		while (*p == ' ' || *p == '\t') {
			p++;
		}
		if (*p == '\0') {
			break;
		}

		/* Extract label - may be single-quoted per Nagios plugin API:
		 * 'label with spaces'=value[UOM];[warn];[crit];[min];[max] */
		if (*p == '\'') {
			/* Quoted label: find closing quote */
			p++; /* skip opening quote */
			label_start = p;
			while (*p != '\0' && *p != '\'') {
				p++;
			}
			label_end = p;
			if (*p == '\'') {
				p++; /* skip closing quote */
			}
		} else {
			/* Unquoted label: read until '=' */
			label_start = p;
			while (*p != '\0' && *p != '=') {
				p++;
			}
			label_end = p;
		}

		/* Expect '=' after label */
		if (*p != '=') {
			/* Malformed metric, skip to next whitespace */
			while (*p != '\0' && *p != ' ' && *p != '\t') {
				p++;
			}
			continue;
		}
		p++; /* skip '=' */

		/* Copy label into struct */
		label_len = label_end - label_start;
		if (label_len >= sizeof(metrics[0].label)) {
			label_len = sizeof(metrics[0].label) - 1;
		}
		memcpy(metrics[metric_count].label, label_start, label_len);
		metrics[metric_count].label[label_len] = '\0';

		/* Sanitize label: replace whitespace and dashes with underscores */
		{
			int li;
			for (li = 0; metrics[metric_count].label[li] != '\0'; li++) {
				char c = metrics[metric_count].label[li];
				if (c == ' ' || c == '\t' || c == '-') {
					metrics[metric_count].label[li] = '_';
				}
			}
		}

		/* Extract value+UOM (up to semicolon or whitespace) */
		val_start = p;
		while (*p != '\0' && *p != ';' && *p != ' ' && *p != '\t') {
			p++;
		}
		val_end = p;

		/* Parse semicolon-separated threshold fields: ;warn;crit;min;max */
		metrics[metric_count].has_warn = 0;
		metrics[metric_count].has_crit = 0;
		metrics[metric_count].has_min = 0;
		metrics[metric_count].has_max = 0;
		{
			/* threshold_ptrs[0]=warn, [1]=crit, [2]=min, [3]=max */
			double *threshold_vals[] = {
				&metrics[metric_count].warn, &metrics[metric_count].crit,
				&metrics[metric_count].min, &metrics[metric_count].max
			};
			int *threshold_flags[] = {
				&metrics[metric_count].has_warn, &metrics[metric_count].has_crit,
				&metrics[metric_count].has_min, &metrics[metric_count].has_max
			};
			int ti = 0;

			while (*p == ';' && ti < 4) {
				const char *field_start;
				p++; /* skip ';' */
				field_start = p;
				while (*p != '\0' && *p != ';' && *p != ' ' && *p != '\t') {
					p++;
				}
				if (p > field_start) {
					char field_str[64];
					size_t flen = p - field_start;
					if (flen >= sizeof(field_str)) {
						flen = sizeof(field_str) - 1;
					}
					memcpy(field_str, field_start, flen);
					field_str[flen] = '\0';
					*threshold_vals[ti] = strtod(field_str, &endptr);
					if (endptr != field_str) {
						*threshold_flags[ti] = 1;
					}
				}
				ti++;
			}
		}

		/* Skip any remaining characters in this metric */
		while (*p != '\0' && *p != ' ' && *p != '\t') {
			p++;
		}

		/* Copy value+UOM string and preserve as raw_value before UOM stripping */
		value_len = val_end - val_start;
		if (value_len >= sizeof(value_str)) {
			value_len = sizeof(value_str) - 1;
		}
		memcpy(value_str, val_start, value_len);
		value_str[value_len] = '\0';

		{
			size_t raw_len = value_len;
			if (raw_len >= sizeof(metrics[0].raw_value)) {
				raw_len = sizeof(metrics[0].raw_value) - 1;
			}
			memcpy(metrics[metric_count].raw_value, value_str, raw_len);
			metrics[metric_count].raw_value[raw_len] = '\0';
		}

		/* Extract UOM suffix (letters/% at end) into struct, then strip from value */
		metrics[metric_count].uom[0] = '\0';
		i = (int)strlen(value_str) - 1;
		while (i >= 0 && (isalpha((unsigned char)value_str[i]) || value_str[i] == '%')) {
			i--;
		}
		if (i + 1 < (int)strlen(value_str)) {
			size_t uom_len = strlen(value_str) - (i + 1);
			if (uom_len >= sizeof(metrics[0].uom)) {
				uom_len = sizeof(metrics[0].uom) - 1;
			}
			memcpy(metrics[metric_count].uom, value_str + i + 1, uom_len);
			metrics[metric_count].uom[uom_len] = '\0';
			value_str[i + 1] = '\0';
		}

		/* Parse the numeric value using strtod for error detection */
		if (strlen(value_str) == 0) {
			continue;
		}
		metrics[metric_count].value = strtod(value_str, &endptr);
		if (endptr == value_str) {
			/* No valid number parsed, skip */
			continue;
		}

		metric_count++;
	}

	return metric_count;
}

/* callback function to discard response data from curl */
static size_t write_callback(void *contents, size_t size, size_t nmemb, void *userp) {
	/* discard the data by just returning the size */
	return size * nmemb;
}

/* send line protocol data to InfluxDB using libcurl */
int send_line_to_server(const char *line_data) {
	CURL *curl;
	CURLcode res;
	char error_buffer[CURL_ERROR_SIZE];
	char log_buffer[512];

	if (config.url == NULL || line_data == NULL) {
		return -1;
	}

	/* initialize curl handle */
	curl = curl_easy_init();
	if (curl == NULL) {
		write_to_all_logs("influxdb: ERROR - Failed to initialize curl", NSLOG_RUNTIME_ERROR);
		return -1;
	}

	/* set error buffer */
	error_buffer[0] = '\0';
	curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, error_buffer);

	/* set URL */
	curl_easy_setopt(curl, CURLOPT_URL, config.url);

	/* set POST method */
	curl_easy_setopt(curl, CURLOPT_POST, 1L);

	/* set POST data */
	curl_easy_setopt(curl, CURLOPT_POSTFIELDS, line_data);
	curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)strlen(line_data));

	/* set timeout */
	curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)config.timeout);

	/* set write callback to discard response body (we don't need it) */
	curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
	curl_easy_setopt(curl, CURLOPT_WRITEDATA, NULL);

	/* SSL verification */
	if (config.ssl_verify == 0) {
		curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
		curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
	} else {
		curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
		curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
	}

	/* CA certificate */
	if (config.ca_cert) {
		curl_easy_setopt(curl, CURLOPT_CAINFO, config.ca_cert);
	}

	/* client certificate */
	if (config.client_cert) {
		curl_easy_setopt(curl, CURLOPT_SSLCERT, config.client_cert);
	}

	/* client key */
	if (config.client_key) {
		curl_easy_setopt(curl, CURLOPT_SSLKEY, config.client_key);
	}

	/* perform the request */
	res = curl_easy_perform(curl);

	/* check for errors */
	if (res != CURLE_OK) {
		size_t len = strlen(error_buffer);
		if (len > 0) {
			snprintf(log_buffer, sizeof(log_buffer),
				"influxdb: ERROR - curl_easy_perform() failed: %s", error_buffer);
		} else {
			snprintf(log_buffer, sizeof(log_buffer),
				"influxdb: ERROR - curl_easy_perform() failed: %s",
				curl_easy_strerror(res));
		}
		write_to_all_logs(log_buffer, NSLOG_RUNTIME_ERROR);
		curl_easy_cleanup(curl);
		return -1;
	}

	/* cleanup */
	curl_easy_cleanup(curl);
	return 0;
}

/* send batched events to server */
int send_batch_to_server(void) {
	char *batch_lines;
	size_t total_size = 0;
	size_t offset = 0;
	int i;

	if (config.batch_count == 0) {
		return 0;
	}

	/* calculate total size needed (each line + newline + null terminator) */
	total_size = config.batch_count + 1; /* for newlines and null terminator */
	for (i = 0; i < config.batch_count; i++) {
		total_size += strlen(config.batch_buffer[i]);
	}

	/* allocate buffer for batch line protocol data */
	batch_lines = (char *)malloc(total_size);
	if (batch_lines == NULL) {
		write_to_all_logs("influxdb: ERROR - Failed to allocate batch buffer", NSLOG_RUNTIME_ERROR);
		return -1;
	}

	/* build line protocol batch (newline-separated) */
	offset = 0;
	for (i = 0; i < config.batch_count; i++) {
		offset += snprintf(batch_lines + offset, total_size - offset, "%s\n", config.batch_buffer[i]);
	}

	/* send to server */
	send_line_to_server(batch_lines);

	/* cleanup batch buffer */
	for (i = 0; i < config.batch_count; i++) {
		free(config.batch_buffer[i]);
		config.batch_buffer[i] = NULL;
	}
	config.batch_count = 0;

	free(batch_lines);
	return 0;
}

/* add event to batch buffer */
void add_to_batch(const char *line_data) {
	char *line_copy;

	if (config.batch_size <= 1) {
		/* no batching, send immediately */
		send_line_to_server(line_data);
		return;
	}

	pthread_mutex_lock(&batch_mutex);

	/* add to batch buffer */
	line_copy = strdup(line_data);
	if (line_copy == NULL) {
		write_to_all_logs("influxdb: ERROR - Failed to allocate memory for batch item", NSLOG_RUNTIME_ERROR);
		pthread_mutex_unlock(&batch_mutex);
		return;
	}

	config.batch_buffer[config.batch_count] = line_copy;
	config.batch_count++;

	/* send batch if full */
	if (config.batch_count >= config.batch_size) {
		send_batch_to_server();
	}

	pthread_mutex_unlock(&batch_mutex);
}

/* handle data from Nagios daemon - called for all registered events */
int influxdb_handle_data(int event_type, void *data) {
	nebstruct_service_check_data *svcdata = NULL;
	nebstruct_host_check_data *hostdata = NULL;

	char line_buffer[MAX_LINE_SIZE];
	char escaped_host[2048];
	char escaped_service[2048];
	char escaped_metric[512];
	char escaped_uom[64];
	char escaped_raw[128];
	char iso_buf[32];
	long long timestamp_ns;
	perfdata_metric metrics[MAX_PERFDATA_METRICS];
	int metric_count;
	int m;
	const char *state_name;

	if (config.url == NULL || data == NULL) {
		return 0;
	}

	/* Extract and format data based on event type in InfluxDB line protocol format */
	switch(event_type) {

		case NEBCALLBACK_SERVICE_CHECK_DATA:
			svcdata = (nebstruct_service_check_data *)data;
			/* Only log processed service checks to reduce volume */
			if (svcdata->type != NEBTYPE_SERVICECHECK_PROCESSED) {
				return 0;
			}

			timestamp_ns = get_timestamp_ns(&svcdata->timestamp);
			format_iso_timestamp(svcdata->timestamp.tv_sec, iso_buf, sizeof(iso_buf));
			state_name = service_state_name(svcdata->state);

			/* Escape host and service tags once */
			escaped_host[0] = '\0';
			escaped_service[0] = '\0';
			if (svcdata->host_name) {
				influx_escape_tag(svcdata->host_name, escaped_host, sizeof(escaped_host));
			}
			if (svcdata->service_description) {
				influx_escape_tag(svcdata->service_description, escaped_service, sizeof(escaped_service));
			}

			/* nagios_service_state */
			snprintf(line_buffer, sizeof(line_buffer),
				"nagios_service_state,host=%s,service=%s,state=%s,state_id=%d "
				"value=%di,state_name=\"%s\" %lld",
				escaped_host, escaped_service, state_name, svcdata->state,
				svcdata->state, state_name, timestamp_ns);
			add_to_batch(line_buffer);

			/* nagios_service_check_timestamp */
			snprintf(line_buffer, sizeof(line_buffer),
				"nagios_service_check_timestamp,host=%s,service=%s "
				"timestamp=%.3f,check_time=\"%s\",latency=%.6f,execution_time=%.6f %lld",
				escaped_host, escaped_service,
				(double)svcdata->timestamp.tv_sec, iso_buf,
				svcdata->latency, svcdata->execution_time, timestamp_ns);
			add_to_batch(line_buffer);

			/* nagios_service_info */
			snprintf(line_buffer, sizeof(line_buffer),
				"nagios_service_info,host=%s,service=%s,state=%s "
				"state_id=\"%d\",check_time=\"%s\" %lld",
				escaped_host, escaped_service, state_name,
				svcdata->state, iso_buf, timestamp_ns);
			add_to_batch(line_buffer);

			/* Performance data and threshold lines (only when perfdata exists) */
			if (svcdata->perf_data != NULL && strlen(svcdata->perf_data) > 0) {
				metric_count = parse_perfdata_metrics(svcdata->perf_data, metrics, MAX_PERFDATA_METRICS);
				for (m = 0; m < metric_count; m++) {
					influx_escape_tag(metrics[m].label, escaped_metric, sizeof(escaped_metric));
					influx_escape_tag(metrics[m].uom[0] ? metrics[m].uom : "none",
						escaped_uom, sizeof(escaped_uom));
					influx_escape_field(metrics[m].raw_value, escaped_raw, sizeof(escaped_raw));

					/* nagios_performance_data */
					snprintf(line_buffer, sizeof(line_buffer),
						"nagios_performance_data,host=%s,service=%s,metric=%s,unit=%s "
						"value=%.15g,raw_value=\"%s\" %lld",
						escaped_host, escaped_service, escaped_metric, escaped_uom,
						metrics[m].value, escaped_raw, timestamp_ns);
					add_to_batch(line_buffer);

					/* nagios_performance_thresholds (one line per present threshold) */
					if (metrics[m].has_warn) {
						snprintf(line_buffer, sizeof(line_buffer),
							"nagios_performance_thresholds,host=%s,service=%s,metric=%s,threshold_type=warning "
							"value=%.6f %lld",
							escaped_host, escaped_service, escaped_metric,
							metrics[m].warn, timestamp_ns);
						add_to_batch(line_buffer);
					}
					if (metrics[m].has_crit) {
						snprintf(line_buffer, sizeof(line_buffer),
							"nagios_performance_thresholds,host=%s,service=%s,metric=%s,threshold_type=critical "
							"value=%.6f %lld",
							escaped_host, escaped_service, escaped_metric,
							metrics[m].crit, timestamp_ns);
						add_to_batch(line_buffer);
					}
					if (metrics[m].has_min) {
						snprintf(line_buffer, sizeof(line_buffer),
							"nagios_performance_thresholds,host=%s,service=%s,metric=%s,threshold_type=min "
							"value=%.6f %lld",
							escaped_host, escaped_service, escaped_metric,
							metrics[m].min, timestamp_ns);
						add_to_batch(line_buffer);
					}
					if (metrics[m].has_max) {
						snprintf(line_buffer, sizeof(line_buffer),
							"nagios_performance_thresholds,host=%s,service=%s,metric=%s,threshold_type=max "
							"value=%.6f %lld",
							escaped_host, escaped_service, escaped_metric,
							metrics[m].max, timestamp_ns);
						add_to_batch(line_buffer);
					}
				}
			}
			return 0;

		case NEBCALLBACK_HOST_CHECK_DATA:
			hostdata = (nebstruct_host_check_data *)data;
			/* Only log processed host checks */
			if (hostdata->type != NEBTYPE_HOSTCHECK_PROCESSED) {
				return 0;
			}

			timestamp_ns = get_timestamp_ns(&hostdata->timestamp);
			format_iso_timestamp(hostdata->timestamp.tv_sec, iso_buf, sizeof(iso_buf));
			state_name = host_state_name(hostdata->state);

			/* Escape host tag once */
			escaped_host[0] = '\0';
			if (hostdata->host_name) {
				influx_escape_tag(hostdata->host_name, escaped_host, sizeof(escaped_host));
			}

			/* nagios_host_state */
			snprintf(line_buffer, sizeof(line_buffer),
				"nagios_host_state,host=%s,state=%s,state_id=%d "
				"value=%di,state_name=\"%s\" %lld",
				escaped_host, state_name, hostdata->state,
				hostdata->state, state_name, timestamp_ns);
			add_to_batch(line_buffer);

			/* nagios_host_check_timestamp */
			snprintf(line_buffer, sizeof(line_buffer),
				"nagios_host_check_timestamp,host=%s "
				"timestamp=%.3f,check_time=\"%s\",latency=%.6f,execution_time=%.6f %lld",
				escaped_host,
				(double)hostdata->timestamp.tv_sec, iso_buf,
				hostdata->latency, hostdata->execution_time, timestamp_ns);
			add_to_batch(line_buffer);

			/* nagios_host_info */
			snprintf(line_buffer, sizeof(line_buffer),
				"nagios_host_info,host=%s,state=%s "
				"state_id=\"%d\",check_time=\"%s\" %lld",
				escaped_host, state_name,
				hostdata->state, iso_buf, timestamp_ns);
			add_to_batch(line_buffer);

			/* Performance data and threshold lines (only when perfdata exists) */
			if (hostdata->perf_data && strlen(hostdata->perf_data) > 0) {
				metric_count = parse_perfdata_metrics(hostdata->perf_data, metrics, MAX_PERFDATA_METRICS);
				for (m = 0; m < metric_count; m++) {
					influx_escape_tag(metrics[m].label, escaped_metric, sizeof(escaped_metric));
					influx_escape_tag(metrics[m].uom[0] ? metrics[m].uom : "none",
						escaped_uom, sizeof(escaped_uom));
					influx_escape_field(metrics[m].raw_value, escaped_raw, sizeof(escaped_raw));

					/* nagios_performance_data with service=_host_check_ */
					snprintf(line_buffer, sizeof(line_buffer),
						"nagios_performance_data,host=%s,service=_host_check_,metric=%s,unit=%s "
						"value=%.15g,raw_value=\"%s\" %lld",
						escaped_host, escaped_metric, escaped_uom,
						metrics[m].value, escaped_raw, timestamp_ns);
					add_to_batch(line_buffer);

					/* nagios_performance_thresholds (one line per present threshold) */
					if (metrics[m].has_warn) {
						snprintf(line_buffer, sizeof(line_buffer),
							"nagios_performance_thresholds,host=%s,service=_host_check_,metric=%s,threshold_type=warning "
							"value=%.6f %lld",
							escaped_host, escaped_metric,
							metrics[m].warn, timestamp_ns);
						add_to_batch(line_buffer);
					}
					if (metrics[m].has_crit) {
						snprintf(line_buffer, sizeof(line_buffer),
							"nagios_performance_thresholds,host=%s,service=_host_check_,metric=%s,threshold_type=critical "
							"value=%.6f %lld",
							escaped_host, escaped_metric,
							metrics[m].crit, timestamp_ns);
						add_to_batch(line_buffer);
					}
					if (metrics[m].has_min) {
						snprintf(line_buffer, sizeof(line_buffer),
							"nagios_performance_thresholds,host=%s,service=_host_check_,metric=%s,threshold_type=min "
							"value=%.6f %lld",
							escaped_host, escaped_metric,
							metrics[m].min, timestamp_ns);
						add_to_batch(line_buffer);
					}
					if (metrics[m].has_max) {
						snprintf(line_buffer, sizeof(line_buffer),
							"nagios_performance_thresholds,host=%s,service=_host_check_,metric=%s,threshold_type=max "
							"value=%.6f %lld",
							escaped_host, escaped_metric,
							metrics[m].max, timestamp_ns);
						add_to_batch(line_buffer);
					}
				}
			}
			return 0;

		default:
			return 0;
	}
}
