/*
** CBLM v3
** $Id: data.c,v 1.10 2006/06/10 05:49:15 sella Exp $
** Copyright (c) 2004 James M. Sella. All Rights Reserved.
** Released under the GPL Version 2 License.
** http://www.digitalgenesis.com
** 
** Cheap Bastard Latency Monitor v3
*/

#include "data.h"
#include "cblm.h"
#include "probe.h"
#include "prefs.h"
#include "db.h"
#include "util.h"
#include "data_hash.h"

/* Global variables */
extern struct args_t args;
extern struct prefs_t prefs;
extern struct probe_t *probes;
extern int running, restart;
extern pthread_mutex_t probe_mutex, packet_list_mutex, db_mutex, hash_mutex;

extern MYSQL mysql;

void process_packet(struct packet_t *packet) {
	char buf[INET_ADDRSTRLEN];
	long jtr, tt, last, last_warn, last_alarm;
	struct data_t d, *data;
	struct timeval tx, tv;
	time_t now = time(NULL);

	pthread_mutex_lock(&hash_mutex);

	/* Build struct to search with */
	d.dst = packet->dst;
	d.tos = packet->tx_tos;
	d.pri = packet->tx_pri;

	if ((data = hash_find(&d)) == NULL) {
		/* Initialize memory. */
		memset(&d, 0, sizeof(struct data_t));
		d.src = packet->src;
		d.dst = packet->dst;
		d.tx = ntohs(packet->tot);
		d.set = packet->set;
		d.tos = packet->tx_tos;
		d.pri = packet->tx_pri;
		d.rtt_min = d.ott_min = d.jtr_min = LONG_MAX;

		/* Setup data pointer. */
		data = &d;

		/* Insert into hash */
		hash_add(data);
	}

	/* Are we still on the same set? */
	if (packet->set + 1 == data->set) {
		if (prefs.trk_ooo == 1) {
			data->pkt_ooo++; /* Track out of order packet. */

			if (now > data->last_warn || args.debug >= DEBUG_DUMP) {
				data->last_warn = now + WARNING_DELAY;

				syslog(LOG_WARNING, "Recieved a packet from a previous set. Discarding packet from %s", inet_ntop(AF_INET, &(packet->dst), buf, sizeof(buf)));
				
			}
		}

		goto error;
	} else if (packet->set != data->set) {
		/* Finalize the data. */
		if (data->rx > 0) {
			data->ott_avg = data->ott_avg / data->rx;
			data->rtt_avg = data->rtt_avg / data->rx;
			data->jtr_avg = data->jtr_avg / data->rx;

			data->ott_min = data->ott_min == LONG_MAX ? 0 : data->ott_min;
			data->rtt_min = data->rtt_min == LONG_MAX ? 0 : data->rtt_min;
			data->jtr_min = data->jtr_min == LONG_MAX ? 0 : data->jtr_min;

			write_data(data);
			summarize_data();
		}
		/* Save last warn/alarm */
		last_warn = data->last_warn;
		last_alarm = data->last_alarm;

		/* Reset data */
		memset(data, 0, sizeof(struct data_t));

		/* Restore last warn/alarm */
		data->last_warn = last_warn;
		data->last_alarm = last_alarm;

		/* Initialize the new data set. */
		data->src = packet->src;
		data->dst = packet->dst;
		data->tx = ntohs(packet->tot);
		data->set = packet->set;
		data->tos = packet->tx_tos;
		data->rtt_min = data->ott_min = data->jtr_min = LONG_MAX;
	}

	/* Track out of order packets */
	if (prefs.trk_ooo == 1) {
		if (ntohs(packet->seq) < data->seq) {
			data->pkt_ooo++;

			if (now > data->last_warn || args.debug >= DEBUG_DUMP) {
				data->last_warn = now + WARNING_DELAY;

				syslog(LOG_WARNING, "Out of order packet received from %s (packet: %lu, data: %lu)", inet_ntop(AF_INET, &(packet->dst), buf, sizeof(buf)), packet->seq, data->seq);
			}
		}
	}

	/* Track duplicate packets */
	if (prefs.trk_dup == 1) {
		if (ntohs(packet->seq) == data->seq && packet->seq != 0) {
			data->pkt_dup++;

			if (now > data->last_warn || args.debug >= DEBUG_DUMP) {
				data->last_warn = now + WARNING_DELAY;

				syslog(LOG_WARNING, "Recieved a duplicate response from %s.", inet_ntop(AF_INET, &(packet->dst), buf, sizeof(buf)));
			}
		}
	}

	/* Recieved */
	data->rx++;
	data->seq = ntohs(packet->seq);

	/* Capture the tx time. */
	tx = ntohtv(packet->tx);

	/* RTT */
	tv = ntohtv(packet->rx);
	subtract_timeval(&tv, &tx);
	tt = last = tvtousec(tv);
	if (tt < MAX_RTT) {
		data->rtt_min = tt < data->rtt_min && tt > 0 ? tt : data->rtt_min;
		data->rtt_max = tt > data->rtt_max && tt > 0 ? tt : data->rtt_max;
		data->rtt_avg = tt > 0 ? data->rtt_avg + tt : data->rtt_avg;
	} else if (now > data->last_warn || args.debug >= DEBUG_DUMP) {
		data->last_warn = now + WARNING_DELAY;

		syslog(LOG_WARNING, "RTT to %s of %d (usec) is over max %d (usec). Discarding sample.", inet_ntop(AF_INET, &(packet->dst), buf, sizeof(buf)), tt, MAX_RTT);
	}

	/* OTT */
	tv = ntohtv(packet->ox);
	subtract_timeval(&tv, &tx);
	tt = tvtousec(tv);
	if (prefs.trk_ott == 1) {
		if (tt < last && tt < MAX_OTT) { /* OTT can't be greater than the RTT, or MAX_OTT. */
			data->ott_min = tt < data->ott_min && tt > 0 ? tt : data->ott_min;
			data->ott_max = tt > data->ott_max && tt > 0 ? tt : data->ott_max;
			data->ott_avg = tt > 0 ? data->ott_avg + tt : data->ott_avg;
		} else if (now > data->last_warn || args.debug >= DEBUG_DUMP) {
			data->last_warn = now + WARNING_DELAY;

			syslog(LOG_WARNING, "OTT to %s of %d (usec) is over max %d or RTT %d (usec). Discarding sample.", inet_ntop(AF_INET, &(packet->dst), buf, sizeof(buf)), tt, MAX_OTT, last);
		}
	}

	/* JTR (RTT) */
	if (prefs.trk_jtr == 1) {
		if (data->rtt_lst == 0) {
			data->rtt_lst = tt;
		} else {
			jtr = abs(data->rtt_lst - tt);
			if (jtr < MAX_JTR) {
				data->jtr_min = jtr < data->jtr_min ? jtr : data->jtr_min;
				data->jtr_max = jtr > data->jtr_max ? jtr : data->jtr_max;
				data->jtr_avg += jtr;
			} else if (now > data->last_warn || args.debug >= DEBUG_DUMP) {
				data->last_warn = now + WARNING_DELAY;

				syslog(LOG_WARNING, "Jitter to %s of %d (usec) is over max %d (usec). Discarding sample.\n", inet_ntop(AF_INET, &(packet->dst), buf, sizeof(buf)), jtr, MAX_JTR);
			}
		}
	}

	if (prefs.trk_tos == 1) {
		/* ToS A to Z error. */
		if (packet->tx_tos != packet->ox_tos) {
			data->tos_a2z++;

			if (now > data->last_warn || args.debug >= DEBUG_DUMP) {
				data->last_warn = now + WARNING_DELAY;

				syslog(LOG_WARNING, "ToS marking transmitted to %s changed from %u to %u (failed A to Z).\n", inet_ntop(AF_INET, &(packet->dst), buf, sizeof(buf)), packet->tx_tos, packet->ox_tos);
			}
		}

		/* ToS Z to A error. */
		if (packet->tx_tos != packet->rx_tos) {
			data->tos_z2a++;

			if (now > data->last_warn || args.debug >= DEBUG_DUMP) {
				data->last_warn = now + WARNING_DELAY;

				syslog(LOG_WARNING, "ToS marking transmitted from %s changed from %u to %u (failed Z to A).", inet_ntop(AF_INET, &(packet->dst), buf, sizeof(buf)), packet->tx_tos, packet->rx_tos);
			}
		}
	}

	if (prefs.trk_pri == 1) {
		/* Prioroty A to Z error. */
		if (packet->tx_pri != packet->ox_pri) {
			data->pri_a2z++;

			if (now > data->last_warn || args.debug >= DEBUG_DUMP) {
				data->last_warn = now + WARNING_DELAY;

				syslog(LOG_WARNING, "Priority marking transmitted to %s changed from %u to %u (failed A to Z).\n", inet_ntop(AF_INET, &(packet->dst), buf, sizeof(buf)), packet->tx_pri, packet->ox_pri);
			}
		}

		/* Priority Z to A error. */
		if (packet->tx_pri != packet->rx_pri) {
			data->pri_z2a++;

			if (now > data->last_warn || args.debug >= DEBUG_DUMP) {
				data->last_warn = now + WARNING_DELAY;

				syslog(LOG_WARNING, "Priority marking transmitted from %s changed from %u to %u (failed Z to A).", inet_ntop(AF_INET, &(packet->dst), buf, sizeof(buf)), packet->tx_pri, packet->rx_pri);
			}
		}
	}

	if (args.debug >= DEBUG_DUMP) {
		syslog(LOG_DEBUG, "Processed packet");
		display_data(data);
	}

error:
	pthread_mutex_unlock(&hash_mutex);
}

void write_data(struct data_t *data) {
	int found = 0;
	char sql[1024], src[INET_ADDRSTRLEN], dst[INET_ADDRSTRLEN], rtt[132], ott[132], jtr[132];
	MYSQL_RES* res;
	struct data_t d;

	inet_ntop(AF_INET, &data->src, src, sizeof(src));
	inet_ntop(AF_INET, &data->dst, dst, sizeof(dst));

	if (args.debug >= DEBUG_INTERNAL) {
		syslog(LOG_INFO, "Writting data for destination %s with ToS %u, PRI %u.", dst, data->tos, data->pri);
	}

	if (args.debug >= DEBUG_DUMP) {
		display_data(data);
	}

   pthread_mutex_lock(&db_mutex);

	/* Verify destination is still a registered probe. */
	snprintf(sql, sizeof(sql), "SELECT id FROM probes WHERE ip = INET_ATON('%s') LIMIT 1", dst);
	if (db_query(&res, sql) != 0) {
		goto exit;
	}
	found = mysql_num_rows(res);
	mysql_free_result(res);


	/* Write data if destination is still a registered probe. */
	if (found != 1) {
		if (args.debug >= DEBUG_NOTICE) {
			syslog(LOG_NOTICE, "Discard data for %s. Its no longer a registered probe.", dst);
		}
	} else {
		/* FIXME: This should be one snprintf() statement, however I keep getting corrupt jtr values if I do that. */
		snprintf(rtt, sizeof(rtt), "%d, %d, %lu", data->rtt_min, data->rtt_max, data->rtt_avg);
		snprintf(ott, sizeof(ott), "%d, %d, %lu", data->ott_min, data->ott_max, data->ott_avg);
		snprintf(jtr, sizeof(jtr), "%d, %d, %lu", data->jtr_min, data->jtr_max, data->jtr_avg);

		snprintf(sql, sizeof(sql), "INSERT INTO data (src, dst, tos, pri, secs, tx, rx, rtt_min, rtt_max, rtt_avg, ott_min, ott_max, ott_avg, jtr_min, jtr_max, jtr_avg, tos_a2z, tos_z2a, pri_a2z, pri_z2a, pkt_ooo, pkt_dup) VALUES (INET_ATON('%s'), INET_ATON('%s'), %lu, %lu, %lu, %lu, %lu, %s, %s, %s, %lu, %lu, %lu, %lu, %lu, %lu)", src, dst, data->tos, data->pri, DATA_INTERVAL_MIN, data->tx, data->rx, rtt, ott, jtr, data->tos_a2z, data->tos_z2a, data->pri_a2z, data->pri_z2a, data->pkt_ooo, data->pkt_dup);

		if (db_query(&res, sql) != 0) {
			goto exit;
		}
		mysql_free_result(res);
	}

exit:
   pthread_mutex_unlock(&db_mutex);
}

void summarize_data() {
	char sql[1024];
	static time_t last_summary = 0;
	long last_min = 0, last_hour = 0, last_day = 0;
	MYSQL_RES* res;
   MYSQL_ROW row;

	/* Don't try to summarize if it isn't time. */
	if (time(NULL) < last_summary) {
		return;
	}

	if (args.debug >= DEBUG_INTERNAL) {
		syslog(LOG_INFO, "Starting to summarize samples in database.");
	}

	/* Update time for next summary attempt. */
	last_summary = time(NULL) + SUMMARY_INTERVAL;

   pthread_mutex_lock(&db_mutex);

	/* Others may read, but may not write to data. */
	if (db_query(&res, "LOCK TABLES data WRITE, data AS data_src WRITE, state WRITE, prefs WRITE") != 0) {
		goto unlock;
	}
	mysql_free_result(res);

	/* Grab intervals update times. */
	if (db_query(&res, "SELECT var, val - UNIX_TIMESTAMP() FROM state WHERE var = 'last_min' OR var = 'last_hour' OR var = 'last_day'") != 0) {
		syslog(LOG_CRIT, "summarize_data() failed to select from state table.");

		goto unlock;
	}

	while (row = mysql_fetch_row(res)) {
		if (args.debug >= DEBUG_DUMP) {
			syslog(LOG_DEBUG, "read val: %s, var: %s", row[0], row[1]);
		}

		if (!strncmp(row[0], "last_min", 8)) {
			last_min = atol(row[1]);
		} else if (!strncmp(row[0], "last_hour", 9)) {
			last_hour = atol(row[1]);
		} else if (!strncmp(row[0], "last_day", 8)) {
			last_day = atol(row[1]);
		}
	}
	mysql_free_result(res);

	/* Delete old 60 sec samples. */
	if (last_min < 1) {
		snprintf(sql, sizeof(sql), "DELETE FROM data WHERE secs = %lu AND UNIX_TIMESTAMP(timestamp) < UNIX_TIMESTAMP() - %lu", DATA_INTERVAL_MIN, DATA_INTERVAL_HOUR * DATA_MULTIPLIER);
		if (db_query(&res, sql) != 0) {
			syslog(LOG_CRIT, "summarize_data() failed to delete stale minute entries from data table.");

			goto unlock;
		}
		mysql_free_result(res);

		/* Delete old value, if any. */
		if (db_query(&res, "DELETE FROM state WHERE var = 'last_min'") != 0) {
			syslog(LOG_CRIT, "summarize_data() failed to delete 'last_min' from state table.");

			goto unlock;
		}
		mysql_free_result(res);

		/* Insert new time to summarize this interval. */
		snprintf(sql, sizeof(sql), "INSERT INTO state (var, val) VALUES('last_min', UNIX_TIMESTAMP() + %lu)", SUMMARY_INTERVAL);
		if (db_query(&res, sql) != 0) {
			syslog(LOG_CRIT, "summarize_data() failed to insert 'last_min' into state table.");

			goto unlock;
		}
		mysql_free_result(res);
	}

	/* Roll up 60 seconds samples into hour samples, and delete old hour samples. */
	if (last_hour < 1) {
		snprintf(sql, sizeof(sql), "INSERT INTO data (timestamp, src, dst, tos, pri, secs, tx, rx, rtt_min, rtt_max, rtt_avg, ott_min, ott_max, ott_avg, jtr_min, jtr_max, jtr_avg, tos_a2z, tos_z2a, pri_a2z, pri_z2a, pkt_ooo, pkt_dup) SELECT MAX(timestamp), src, dst, tos, pri, %lu, SUM(tx), SUM(rx), MIN(rtt_min), MAX(rtt_max), AVG(rtt_avg), MIN(ott_min), MAX(ott_max), AVG(ott_avg), MIN(jtr_min), MAX(jtr_max), AVG(jtr_avg), SUM(tos_a2z), SUM(tos_z2a), SUM(pri_a2z), SUM(pri_z2a), SUM(pkt_ooo), SUM(pkt_dup) FROM data AS data_src WHERE UNIX_TIMESTAMP(timestamp) > UNIX_TIMESTAMP() - %lu AND secs = %lu GROUP by src, dst, tos, pri ORDER by src, dst", DATA_INTERVAL_HOUR, DATA_INTERVAL_HOUR, DATA_INTERVAL_MIN);
		if (db_query(&res, sql) != 0) {
			syslog(LOG_CRIT, "summarize_data() failed to insert rolled up minute data into data table.");

			goto unlock;
		}
		mysql_free_result(res);

		snprintf(sql, sizeof(sql), "DELETE FROM data WHERE secs = %lu AND UNIX_TIMESTAMP(timestamp) < UNIX_TIMESTAMP() - %lu", DATA_INTERVAL_HOUR, DATA_INTERVAL_DAY * DATA_MULTIPLIER);
		if (db_query(&res, sql) != 0) {
			syslog(LOG_CRIT, "summarize_data() failed to delete stale hourly entries from data table.");

			goto unlock;
		}
		mysql_free_result(res);

		/* Delete old value, if any. */
		if (db_query(&res, "DELETE FROM state WHERE var = 'last_hour'") != 0) {
			syslog(LOG_CRIT, "summarize_data() failed to delete 'last_hour' from state table.");

			goto unlock;
		}
		mysql_free_result(res);

		/* Insert new time to summarize this interval. */
		snprintf(sql, sizeof(sql), "INSERT INTO state (var, val) VALUES('last_hour', UNIX_TIMESTAMP() + %lu)", DATA_INTERVAL_HOUR);
		if (db_query(&res, sql) != 0) {
			syslog(LOG_CRIT, "summarize_data() failed to insert 'last_hour' into state table.");

			goto unlock;
		}
		mysql_free_result(res);
	}

	/* Roll up 3600 seconds samples into day samples, and delete old day samples. */
	if (last_day < 1) {
		snprintf(sql, sizeof(sql), "INSERT INTO data (timestamp, src, dst, tos, pri, secs, tx, rx, rtt_min, rtt_max, rtt_avg, ott_min, ott_max, ott_avg, jtr_min, jtr_max, jtr_avg, tos_a2z, tos_z2a, pri_a2z, pri_z2a, pkt_ooo, pkt_dup) SELECT MAX(timestamp), src, dst, tos, pri, %lu, SUM(tx), SUM(rx), MIN(rtt_min), MAX(rtt_max), AVG(rtt_avg), MIN(ott_min), MAX(ott_max), AVG(ott_avg), MIN(jtr_min), MAX(jtr_max), AVG(jtr_avg), SUM(tos_a2z), SUM(tos_z2a), SUM(pri_a2z), SUM(pri_z2a), SUM(pkt_ooo), SUM(pkt_dup) FROM data AS data_src WHERE UNIX_TIMESTAMP(timestamp) > UNIX_TIMESTAMP() - %lu AND secs = %lu GROUP by src, dst, tos, pri ORDER by src, dst", DATA_INTERVAL_DAY, DATA_INTERVAL_DAY, DATA_INTERVAL_HOUR);
		if (db_query(&res, sql) != 0) {
			syslog(LOG_CRIT, "summarize_data() failed to insert rolled up hourly data into data table.");

			goto unlock;
		}
		mysql_free_result(res);

		snprintf(sql, sizeof(sql), "DELETE FROM data WHERE secs = %lu AND UNIX_TIMESTAMP(timestamp) < UNIX_TIMESTAMP() - %lu", DATA_INTERVAL_DAY, DATA_EXPIRATION);
		if (db_query(&res, sql) != 0) {
			syslog(LOG_CRIT, "summarize_data() failed to delete stale daily entries from data table.");

			goto unlock;
		}
		mysql_free_result(res);

		/* Delete old value, if any. */
		if (db_query(&res, "DELETE FROM state WHERE var = 'last_day'") != 0) {
			syslog(LOG_CRIT, "summarize_data() failed to delete 'last_day' from state table.");

			goto unlock;
		}
		mysql_free_result(res);

		/* Insert new time to summarize this interval. */
		snprintf(sql, sizeof(sql), "INSERT INTO state (var, val) VALUES('last_day', UNIX_TIMESTAMP() + %lu)", DATA_INTERVAL_DAY);
		if (db_query(&res, sql) != 0) {
			syslog(LOG_CRIT, "summarize_data() failed to insert 'last_day' into state table.");

			goto unlock;
		}
		mysql_free_result(res);

		if (db_query(&res, "UNLOCK TABLES") != 0) {
			syslog(LOG_CRIT, "summarize_data() failed to unlock data, state and prefs tables.");

			goto exit;
		}
		mysql_free_result(res);

		if (args.debug >= DEBUG_INTERNAL) {
			syslog(LOG_INFO, "Optimizing database tables.");
		}

		if (db_query(&res, "OPTIMIZE TABLE data") != 0) {
			syslog(LOG_CRIT, "summarize_data() failed to optimize data tables.");

			goto unlock;
		}
		mysql_free_result(res);
	}

	if (last_min < 1 || last_hour < 1 || last_day < 1) {
		/* Allow other probes to do next update. */
		last_summary += random() % (SUMMARY_INTERVAL / 8);
	}

unlock:
	if (db_query(&res, "UNLOCK TABLES") != 0) {
		syslog(LOG_CRIT, "summarize_data() failed to unlock data, state and prefs tables.");

		goto exit;
	}
	mysql_free_result(res);

exit:
   pthread_mutex_unlock(&db_mutex);

	if (args.debug >= DEBUG_INTERNAL) {
		syslog(LOG_INFO, "Completed summarization of samples in database. (Processed %s%s%s%s)", last_min < 1 ? "min " : "", last_hour < 1 ? "hour " : "", last_day < 1 ? "day " : "", last_min > 0 && last_hour > 0 && last_day > 0 ? "none" : "");
	}
}

/*
** Local Variables:
** c-basic-offset: 3
** tab-width: 3
** End:
** vim: noet ts=3 sw=3
*/
