/*
 *   This program is is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation; either version 2 of the License, or (at
 *   your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program; if not, write to the Free Software
 *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
 */

/**
 * $Id: c584ec6440eca43d302d3a8a6c9d5fa9745d7a5f $
 * @file collectd.c
 * @brief Helper functions to enabled radsniff to talk to collectd
 *
 * @copyright 2013 Arran Cudbard-Bell (a.cudbardb@freeradius.org)
 */
#include <assert.h>
#include <ctype.h>

#ifdef HAVE_COLLECTDC_H
#include <collectd/client.h>
#include <freeradius-devel/util/syserror.h>
#include "radsniff.h"

/** Copy a 64bit unsigned integer into a double
 *
 */
/*
static void _copy_uint64_to_double(UNUSED rs_t *conf, rs_stats_value_tmpl_t *tmpl)
{
	assert(tmpl->src);
	assert(tmpl->dst);

	*((double *) tmpl->dst) = *((uint64_t *) tmpl->src);
}
*/

/*
static void _copy_uint64_to_uint64(UNUSED rs_t *conf, rs_stats_value_tmpl_t *tmpl)
{
	assert(tmpl->src);
	assert(tmpl->dst);

	*((uint64_t *) tmpl->dst) = *((uint64_t *) tmpl->src);
}
*/

static void _copy_double_to_double(UNUSED rs_t *conf, rs_stats_value_tmpl_t *tmpl)
{
	assert(tmpl->src);
	assert(tmpl->dst);

	*((double *) tmpl->dst) = *((double*) tmpl->src);
}


/** Allocates a stats template which describes a single gauge/counter
 *
 * This is just intended to simplify allocating a fairly complex memory structure
 * src and dst pointers must be set
 *
 * @param ctx Context to allocate collectd struct in.
 * @param conf Radsniff configuration.
 * @param plugin_instance usually the type of packet (in our case).
 * @param type string, the name of a collection of stats e.g. exchange
 * @param type_instance the name of the counter/gauge within the collection e.g. latency.
 * @param stats structure to derive statistics from.
 * @param values Value templates used to populate lcc_value_list.
 * @return
 *	- New #rs_stats_tmpl_t on success.
 *	- NULL on failure.
 */
static rs_stats_tmpl_t *rs_stats_collectd_init(TALLOC_CTX *ctx, rs_t *conf,
					       char const *plugin_instance,
					       char const *type, char const *type_instance,
					       void *stats,
					       rs_stats_value_tmpl_t const *values)
{
	static char hostname[255];
	static char fqdn[LCC_NAME_LEN];

	size_t len;
	int i;
	char *p;

	rs_stats_tmpl_t *tmpl;
	lcc_value_list_t *value;

	assert(conf);
	assert(type);
	assert(type_instance);

	for (len = 0; values[len].src; len++) {} ;
	assert(len > 0);

	/*
	 *	Initialise hostname once so we don't call gethostname every time
	 */
	if (*fqdn == '\0') {
		int ret;
		struct addrinfo hints, *info = NULL;

		if (gethostname(hostname, sizeof(hostname)) < 0) {
			ERROR("Error getting hostname: %s", fr_syserror(errno));

			return NULL;
		}

		memset(&hints, 0, sizeof hints);
		hints.ai_family = AF_UNSPEC; /*either IPV4 or IPV6*/
		hints.ai_socktype = SOCK_STREAM;
		hints.ai_flags = AI_CANONNAME;

		if ((ret = getaddrinfo(hostname, "radius", &hints, &info)) != 0) {
			ERROR("Error getting hostname: %s", gai_strerror(ret));
			return NULL;
		}

		strlcpy(fqdn, info->ai_canonname, sizeof(fqdn));

		freeaddrinfo(info);
	}

	tmpl = talloc_zero(ctx, rs_stats_tmpl_t);
	if (!tmpl) {
		return NULL;
	}

	tmpl->value_tmpl = talloc_zero_array(tmpl, rs_stats_value_tmpl_t, len);
	if (!tmpl->value_tmpl) {
		goto error;
	}

	tmpl->stats = stats;

	value = talloc_zero(tmpl, lcc_value_list_t);
	if (!value) {
		goto error;
	}
	tmpl->value = value;

	value->interval = conf->stats.interval;
	value->values_len = len;

	value->values_types = talloc_zero_array(value, int, len);
	if (!value->values_types) {
		goto error;
	}

	value->values = talloc_zero_array(value, value_t, len);
	if (!value->values) {
		goto error;
	}

	for (i = 0; i < (int) len; i++) {
		assert(values[i].src);
		assert(values[i].cb);

		tmpl->value_tmpl[i] = values[i];
		switch (tmpl->value_tmpl[i].type) {
		case LCC_TYPE_COUNTER:
			tmpl->value_tmpl[i].dst = &value->values[i].counter;
			break;

		case LCC_TYPE_GAUGE:
			tmpl->value_tmpl[i].dst = &value->values[i].gauge;
			break;

		case LCC_TYPE_DERIVE:
			tmpl->value_tmpl[i].dst = &value->values[i].derive;
			break;

		case LCC_TYPE_ABSOLUTE:
			tmpl->value_tmpl[i].dst = &value->values[i].absolute;
			break;

		default:
			assert(0);
		}
		value->values_types[i] = tmpl->value_tmpl[i].type;
	}

	/*
	 *	These should be OK as is
	 */
	strlcpy(value->identifier.host, fqdn, sizeof(value->identifier.host));

	/*
	 *	Plugin is ASCII only and no '/'
	 */
	fr_snprint(value->identifier.plugin, sizeof(value->identifier.plugin),
		  conf->stats.prefix, strlen(conf->stats.prefix), '\0');
	for (p = value->identifier.plugin; *p; ++p) {
		if ((*p == '-') || (*p == '/'))*p = '_';
	}

	/*
	 *	Plugin instance is ASCII only (assuming printable only) and no '/'
	 */
	fr_snprint(value->identifier.plugin_instance, sizeof(value->identifier.plugin_instance),
		  plugin_instance, strlen(plugin_instance), '\0');
	for (p = value->identifier.plugin_instance; *p; ++p) {
		if ((*p == '-') || (*p == '/')) *p = '_';
	}

	/*
	 *	Type is ASCII only (assuming printable only) and no '/' or '-'
	 */
	fr_snprint(value->identifier.type, sizeof(value->identifier.type),
		  type, strlen(type), '\0');
	for (p = value->identifier.type; *p; ++p) {
		if ((*p == '-') || (*p == '/')) *p = '_';
	}

	fr_snprint(value->identifier.type_instance, sizeof(value->identifier.type_instance),
		  type_instance, strlen(type_instance), '\0');
	for (p = value->identifier.type_instance; *p; ++p) {
		if ((*p == '-') || (*p == '/')) *p = '_';
	}


	return tmpl;

error:
	talloc_free(tmpl);
	return NULL;
}


/** Setup stats templates for latency
 *
 */
rs_stats_tmpl_t *rs_stats_collectd_init_latency(TALLOC_CTX *ctx, rs_stats_tmpl_t **out, rs_t *conf,
						char const *type, rs_latency_t *stats, fr_radius_packet_code_t code)
{
	rs_stats_tmpl_t **tmpl = out, *last;
	char *p;
	char buffer[LCC_NAME_LEN];
	rs_stats_value_tmpl_t rtx[(RS_RETRANSMIT_MAX + 1) + 1 + 1];	// RTX bins + 0 bin + lost + NULL
	int i;

	/* not static so were thread safe */
	rs_stats_value_tmpl_t const _packet_count[] = {
		{ &stats->interval.received, LCC_TYPE_GAUGE, _copy_double_to_double, NULL },
		{ &stats->interval.linked, LCC_TYPE_GAUGE, _copy_double_to_double, NULL },
		{ &stats->interval.unlinked, LCC_TYPE_GAUGE, _copy_double_to_double, NULL },
		{ &stats->interval.reused, LCC_TYPE_GAUGE, _copy_double_to_double, NULL },
		{ NULL, 0, NULL, NULL }
	};

	rs_stats_value_tmpl_t const _latency[] = {
		{ &stats->latency_smoothed, LCC_TYPE_GAUGE, _copy_double_to_double, NULL },
		{ &stats->interval.latency_average, LCC_TYPE_GAUGE, _copy_double_to_double, NULL },
		{ &stats->interval.latency_high, LCC_TYPE_GAUGE, _copy_double_to_double, NULL },
		{ &stats->interval.latency_low, LCC_TYPE_GAUGE, _copy_double_to_double, NULL },
		{ NULL, 0, NULL, NULL }
	};

#define INIT_STATS(_ti, _v) do {\
		strlcpy(buffer, fr_radius_packet_name[code], sizeof(buffer)); \
		for (p = buffer; *p; ++p) *p = tolower((uint8_t) *p);\
		last = *tmpl = rs_stats_collectd_init(ctx, conf, type, _ti, buffer, stats, _v);\
		if (!*tmpl) {\
			TALLOC_FREE(*out);\
			return NULL;\
		}\
		tmpl = &(*tmpl)->next;\
		ctx = *tmpl;\
		} while (0)


	INIT_STATS("radius_count", _packet_count);
	INIT_STATS("radius_latency", _latency);

	for (i = 0; i < (RS_RETRANSMIT_MAX + 1); i++) {
		rtx[i].src = &stats->interval.rt[i];
		rtx[i].type = LCC_TYPE_GAUGE;
		rtx[i].cb = _copy_double_to_double;
		rtx[i].dst = NULL;
	}

	rtx[i].src = &stats->interval.lost;
	rtx[i].type = LCC_TYPE_GAUGE;
	rtx[i].cb = _copy_double_to_double;
	rtx[i].dst = NULL;

	memset(&rtx[++i], 0, sizeof(rs_stats_value_tmpl_t));

	INIT_STATS("radius_rtx", rtx);

	return last;
}

/** Refresh and send the stats to the collectd server
 *
 */
void rs_stats_collectd_do_stats(rs_t *conf, rs_stats_tmpl_t *tmpls, struct timeval *now)
{
	rs_stats_tmpl_t *tmpl = tmpls;
	char identifier[6 * LCC_NAME_LEN];
	int i;

	while (tmpl) {
		/*
		 *	Refresh the value of whatever were sending
		 */
		for (i = 0; i < (int) tmpl->value->values_len; i++) {
			tmpl->value_tmpl[i].cb(conf, &tmpl->value_tmpl[i]);
		}

		tmpl->value->time = now->tv_sec;

		lcc_identifier_to_string(conf->stats.handle, identifier, sizeof(identifier), &tmpl->value->identifier);

		if (lcc_putval(conf->stats.handle, tmpl->value) < 0) {
			char const *error;

			error = lcc_strerror(conf->stats.handle);
			ERROR("Failed PUTVAL \"%s\" interval=%i %" PRIu64 " : %s",
			      identifier,
			      (int) tmpl->value->interval,
			      (uint64_t) tmpl->value->time,
			      error ? error : "unknown error");
		}

		tmpl = tmpl->next;
	}
}

/** Connect to a collectd server for stats output
 *
 * @param[in,out] conf radsniff configuration, we write the generated handle here.
 * @return
 *	- 0 on success
 *	- -1 on failure.
 */
int rs_stats_collectd_open(rs_t *conf)
{
	assert(conf->stats.collectd);

	/*
	 *	Tear down stale connections gracefully.
	 */
	rs_stats_collectd_close(conf);

	/*
	 *	There's no way to get the error from the connection handle
	 *	because it's freed on failure, before lcc returns.
	 */
	if (lcc_connect(conf->stats.collectd, &conf->stats.handle) < 0) {
		ERROR("Failed opening connection to collectd: %s", fr_syserror(errno));
		return -1;
	}
	DEBUG2("Connected to \"%s\"", conf->stats.collectd);

	assert(conf->stats.handle);
	return 0;
}

/** Close connection
 *
 * @param[in,out] conf radsniff configuration.
 * @return
 *	- 0 on success.
 *	- -1 on failure.
 */
int rs_stats_collectd_close(rs_t *conf)
{
	int ret = 0;

	assert(conf->stats.collectd);

	if (conf->stats.handle) {
		ret = lcc_disconnect(conf->stats.handle);
		conf->stats.handle = NULL;
	}

	return ret;
}
#endif
