/*
 * Nagios Event Radio Dispatcher
 *
 * This is a subscriber service which initiates a unix domain socket,
 * listens to it and lets other programs connect to it and subscribe
 * to various channels using a simple text-based syntax.
 *
 * This code uses the eventbroker api to get its data, which means
 * we're finally eating our own dogfood in that respect.
 *
 * Note: Disabled by default as of 4.4.0
 * ./configure --enable-nerd
 * to enable!
 */

#define _GNU_SOURCE 1
#include <stdio.h>
#include "include/config.h"
#include <sys/types.h>
#include <sys/socket.h>
#include "lib/libnagios.h"
#include "include/common.h"
#include "include/objects.h"
#include "include/broker.h"
#include "include/nebmods.h"
#include "include/nebmodules.h"
#include "include/nebstructs.h"

#ifdef ENABLE_NERD

struct nerd_channel {
	const char *name; /* name of this channel */
	const char *description; /* user-presentable string to document the purpose of this channel */
	unsigned int id; /* channel id (might vary between invocations) */
	unsigned int required_options; /* event_broker_options required for this channel */
	unsigned int num_callbacks;
	unsigned int callbacks[NEBCALLBACK_NUMITEMS];
	int (*handler)(int , void *); /* callback handler for this channel */
	objectlist *subscriptions; /* subscriber list */
};

static nebmodule nerd_mod; /* fake module to get our callbacks accepted */
static struct nerd_channel **channels;
static unsigned int num_channels, alloc_channels;
static unsigned int chan_host_checks_id, chan_service_checks_id;
static unsigned int chan_opath_checks_id;


static struct nerd_channel *find_channel(const char *name)
{
	unsigned int i;

	for(i = 0; i < num_channels; i++) {
		struct nerd_channel *chan = channels[i];
		if(!strcmp(name, chan->name)) {
			return chan;
		}
	}
	return NULL;
}

int nerd_get_channel_id(const char *name)
{
	struct nerd_channel *chan;

	chan = find_channel(name);
	if (!chan)
		return -1;

	return chan->id;
}

static struct nerd_channel *nerd_get_channel(unsigned int chan_id)
{
	return chan_id >= num_channels ? NULL : channels[chan_id];
}

objectlist *nerd_get_subscriptions(int chan_id)
{
	struct nerd_channel *chan = nerd_get_channel(chan_id);

	return chan ? chan->subscriptions : NULL;
}

static int nerd_register_channel_callbacks(struct nerd_channel *chan)
{
	unsigned int i;

	for(i = 0; i < chan->num_callbacks; i++) {
		int result = neb_register_callback(chan->callbacks[i], &nerd_mod, 0, chan->handler);
		if(result != 0) {
			logit(NSLOG_RUNTIME_ERROR, TRUE, "nerd: Failed to register callback %d for channel '%s': %d\n",
				  chan->callbacks[i], chan->name, result);
			return -1;
		}
	}
	return 0;
}

static int nerd_deregister_channel_callbacks(struct nerd_channel *chan)
{
	unsigned int i;

	for(i = 0; i < chan->num_callbacks; i++) {
		neb_deregister_callback(chan->callbacks[i], chan->handler);
	}
	return 0;
}

static struct nerd_subscription *subscribe_create(int sd, struct nerd_channel *chan, const char *fmt)
{
	struct nerd_subscription *subscr;

	if(!(subscr = calloc(1, sizeof(*subscr))))
		return NULL;

	subscr->sd = sd;
	subscr->chan = chan;
	subscr->format = fmt ? strdup(fmt) : NULL;

	return subscr;
}

static void subscribe_destroy(struct nerd_subscription *subscr)
{
	free(subscr->format);
	free(subscr);
}

static int subscribe(int sd, struct nerd_channel *chan, char *fmt)
{
	struct nerd_subscription *subscr;

	if(!(subscr = subscribe_create(sd, chan, fmt)))
		return -1;

	if(!chan->subscriptions) {
		nerd_register_channel_callbacks(chan);
	}

	prepend_object_to_objectlist(&chan->subscriptions, subscr);
	return 0;
}

static int cancel_channel_subscription(struct nerd_channel *chan, int sd)
{
	objectlist *list, *next, *prev = NULL;
	int cancelled = 0;

	if(!chan)
		return -1;

	for(list = chan->subscriptions; list; list = next) {
		struct nerd_subscription *subscr = (struct nerd_subscription *)list->object_ptr;
		next = list->next;

		if(subscr->sd == sd) {
			cancelled++;
			free(list);
			subscribe_destroy(subscr);
			if(prev) {
				prev->next = next;
			} else {
				chan->subscriptions = next;
			}
			continue;
		}
		prev = list;
	}

	if(cancelled) {
		logit(NSLOG_INFO_MESSAGE, TRUE, "nerd: Cancelled %d subscription%s to channel '%s' for %d\n",
			  cancelled, cancelled == 1 ? "" : "s", chan->name, sd);
	}

	if(chan->subscriptions == NULL)
		nerd_deregister_channel_callbacks(chan);

	return 0;
}

static int unsubscribe(int sd, struct nerd_channel *chan)
{
	objectlist *list, *next, *prev = NULL;

	for(list = chan->subscriptions; list; list = next) {
		struct nerd_subscription *subscr = (struct nerd_subscription *)list->object_ptr;
		next = list->next;
		if(subscr->sd == sd) {
			/* found it, so remove it */
			subscribe_destroy(subscr);
			free(list);
			if(!prev) {
				chan->subscriptions = next;
				continue;
			} else {
				prev->next = next;
			}
			continue;
		}
		prev = list;
	}

	if(chan->subscriptions == NULL) {
		nerd_deregister_channel_callbacks(chan);
	}
	return 0;
}

/* removes a subscriber entirely and closes its socket */
int nerd_cancel_subscriber(int sd)
{
	unsigned int i;

	for(i = 0; i < num_channels; i++) {
		cancel_channel_subscription(channels[i], sd);
	}

	iobroker_close(nagios_iobs, sd);
	return 0;
}

int nerd_broadcast(unsigned int chan_id, void *buf, unsigned int len)
{
	struct nerd_channel *chan;
	objectlist *list, *next;

	if (!(chan = nerd_get_channel(chan_id)))
		return -1;

	for(list = chan->subscriptions; list; list = next) {
		struct nerd_subscription *subscr = (struct nerd_subscription *)list->object_ptr;
		int result;

		next = list->next;

		result = send(subscr->sd, buf, len, 0);
		if(result < 0) {
			if (errno == EAGAIN)
				return 0;

			nerd_cancel_subscriber(subscr->sd);
			return 500;
		}
	}

	return 0;
}


static int chan_host_checks(int cb, void *data)
{
	nebstruct_host_check_data *ds = (nebstruct_host_check_data *)data;
	check_result *cr = (check_result *)ds->check_result_ptr;
	host *h;
	char *buf;
	int len;

	if(ds->type != NEBTYPE_HOSTCHECK_PROCESSED)
		return 0;

	if(channels[chan_host_checks_id]->subscriptions == NULL)
		return 0;

	h = (host *)ds->object_ptr;
	len = asprintf(&buf, "%s from %d -> %d: %s\n", h->name, h->last_state, h->current_state, cr->output);
	nerd_broadcast(chan_host_checks_id, buf, len);
	free(buf);
	return 0;
}

static int chan_service_checks(int cb, void *data)
{
	nebstruct_service_check_data *ds = (nebstruct_service_check_data *)data;
	check_result *cr = (check_result *)ds->check_result_ptr;
	service *s;
	char *buf;
	int len;

	if(ds->type != NEBTYPE_SERVICECHECK_PROCESSED)
		return 0;
	s = (service *)ds->object_ptr;
	len = asprintf(&buf, "%s;%s from %d -> %d: %s\n", s->host_name, s->description, s->last_state, s->current_state, cr->output);
	nerd_broadcast(chan_service_checks_id, buf, len);
	free(buf);
	return 0;
}

static char **host_parent_path_cache;
static const char *host_parent_path(host *leaf, char sep)
{
	char *ret;
	unsigned int len = 0, pos = 0;
	objectlist *stack = NULL, *list, *next;
	host *h = leaf;

	if(!h->parent_hosts)
		return h->name;

	if(!host_parent_path_cache) {
		host_parent_path_cache = calloc(num_objects.hosts, sizeof(char *));
	}
	if(host_parent_path_cache[h->id]) {
		return host_parent_path_cache[h->id];
	}

	while(h) {
		len += strlen(h->name) + 2; /* room for separator */
		prepend_object_to_objectlist(&stack, h->name);
		if(h->parent_hosts && h->parent_hosts->host_ptr)
			h = h->parent_hosts->host_ptr;
		else
			break;
	}

	ret = malloc(len + 1);
	for(list = stack; list; list = next) {
		char *ppart = (char *)list->object_ptr;
		next = list->next;
		free(list);
		ret[pos++] = sep;
		memcpy(ret + pos, ppart, strlen(ppart));
		pos += strlen(ppart);
	}
	ret[pos++] = 0;
	host_parent_path_cache[leaf->id] = ret;
	return ret;
}

static int chan_opath_checks(int cb, void *data)
{
	const int red = 0xff0000, green = 0xff00, blue = 0xff, pale = 0x555555;
	unsigned int color;
	check_result *cr;
	host *h;
	const char *name = "_HOST_";
	char *buf = NULL;

	if(cb == NEBCALLBACK_HOST_CHECK_DATA) {
		nebstruct_host_check_data *ds = (nebstruct_host_check_data *)data;

		if(ds->type != NEBTYPE_HOSTCHECK_PROCESSED)
			return 0;

		cr = ds->check_result_ptr;
		h = ds->object_ptr;
		color = red | green;
	} else if(cb == NEBCALLBACK_SERVICE_CHECK_DATA) {
	   nebstruct_service_check_data *ds = (nebstruct_service_check_data *)data;
		service *s;

		if(ds->type != NEBTYPE_SERVICECHECK_PROCESSED)
			return 0;
		s = (service *)ds->object_ptr;
		h = s->host_ptr;
		cr = ds->check_result_ptr;
		color = (red | green | blue) ^ pale;
		name = s->description;
	} else
		return 0;

	asprintf(&buf, "%lu|%s|M|%s/%s|%06X\n", cr->finish_time.tv_sec,
			 check_result_source(cr), host_parent_path(h, '/'), name, color);
	nerd_broadcast(chan_opath_checks_id, buf, strlen(buf));
	free(buf);
	return 0;
}


static int nerd_deinit(void)
{
	unsigned int i;

	if(host_parent_path_cache) {
		for(i = 0; i < num_objects.hosts; i++) {
			my_free(host_parent_path_cache[i]);
		}
		my_free(host_parent_path_cache);
	}

	for(i = 0; i < num_channels; i++) {
		struct nerd_channel *chan = channels[i];
		objectlist *list, *next;

		for(list = chan->subscriptions; list; list = next) {
			struct nerd_subscription *subscr = (struct nerd_subscription *)list->object_ptr;
			iobroker_close(nagios_iobs, subscr->sd);
			next = list->next;
			free(list);
			subscribe_destroy(subscr);
		}
		chan->subscriptions = NULL;
		my_free(chan);
	}
	my_free(channels);
	num_channels = 0;
	alloc_channels = 0;

	return 0;
}

int nerd_mkchan(const char *name, const char *description, int (*handler)(int, void *), unsigned int callbacks)
{
	struct nerd_channel *chan, **ptr;
	int i;

	if(num_channels + 1 >= alloc_channels) {
		alloc_channels = alloc_nr(alloc_channels);
		ptr = realloc(channels, alloc_channels * sizeof(struct nerd_channel *));
		if(!ptr)
			return -1;
		channels = ptr;
	}

	if(!(chan = calloc(1, sizeof(*chan))))
		return -1;

	chan->name = name;
	chan->description = description;
	chan->handler = handler;
	for(i = 0; callbacks && i < NEBCALLBACK_NUMITEMS; i++) {
		if(!(callbacks & (1 << i)))
			continue;

		chan->callbacks[chan->num_callbacks++] = i;
	}

	channels[num_channels++] = chan;

	logit(NSLOG_INFO_MESSAGE, TRUE, "nerd: Channel %s registered successfully\n", chan->name);
	return num_channels - 1;
}

#define NERD_SUBSCRIBE   0
#define NERD_UNSUBSCRIBE 1
static int nerd_qh_handler(int sd, char *request, unsigned int len)
{
	char *chan_name, *fmt;
	struct nerd_channel *chan;
	int action;

	if (!*request || !strcmp(request, "help")) {
		nsock_printf_nul(sd, "Manage subscriptions to NERD channels.\n"
			"Valid commands:\n"
			"  list                      list available channels\n"
			"  subscribe <channel>       subscribe to a channel\n"
			"  unsubscribe <channel>     unsubscribe to a channel\n");
		return 0;
	}

	if (!strcmp(request, "list")) {
		unsigned int i;
		for (i = 0; i < num_channels; i++) {
			chan = channels[i];
			nsock_printf(sd, "%-15s %s\n", chan->name, chan->description);
		}
		nsock_printf(sd, "%c", 0);
		return 0;
	}

	chan_name = strchr(request, ' ');
	if(!chan_name)
		return 400;

	*chan_name = 0;
	chan_name++;
	if(!strcmp(request, "subscribe"))
		action = NERD_SUBSCRIBE;
	else if(!strcmp(request, "unsubscribe"))
		action = NERD_UNSUBSCRIBE;
	else {
		return 400;
	}

	/* might have a format-string */
	if((fmt = strchr(chan_name, ':')))
		*(fmt++) = 0;

	chan = find_channel(chan_name);
	if(!chan) {
		return 400;
	}

	if(action == NERD_SUBSCRIBE)
		subscribe(sd, chan, fmt);
	else
		unsubscribe(sd, chan);

	return 0;
}

/* nebmod_init(), but loaded even if no modules are */
int nerd_init(void)
{
	nerd_mod.deinit_func = nerd_deinit;
	nerd_mod.filename = (char *)"NERD"; /* something to log */

	if(qh_register_handler("nerd", "Nagios Event Radio Dispatcher - Subscriber Service", 0, nerd_qh_handler) < 0) {
		logit(NSLOG_RUNTIME_ERROR, TRUE, "nerd: Failed to register with query handler\n");
		return ERROR;
	}

	neb_add_core_module(&nerd_mod);

	chan_host_checks_id = nerd_mkchan("hostchecks",
			"Host check results",
			chan_host_checks, nebcallback_flag(NEBCALLBACK_HOST_CHECK_DATA));
	chan_service_checks_id = nerd_mkchan("servicechecks",
			"Service check results",
			chan_service_checks, nebcallback_flag(NEBCALLBACK_SERVICE_CHECK_DATA));
	chan_opath_checks_id = nerd_mkchan("opathchecks",
			"Host and service checks in gource's log format",
			chan_opath_checks, nebcallback_flag(NEBCALLBACK_HOST_CHECK_DATA) | nebcallback_flag(NEBCALLBACK_SERVICE_CHECK_DATA));

	logit(NSLOG_INFO_MESSAGE, TRUE, "nerd: Fully initialized and ready to rock!\n");
	return 0;
}

#endif