/*
 * This file is part of RTRlib.
 *
 * This file is subject to the terms and conditions of the MIT license.
 * See the file LICENSE in the top level directory for more details.
 *
 * Website: http://rtrlib.realmv6.org/
 */

#include <stdlib.h>
#include <pthread.h>
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>

#include "rtrlib/lib/alloc_utils_private.h"
#include "rtrlib/lib/log_private.h"
#include "rtrlib/pfx/pfx_private.h"
#include "rtrlib/rtr/rtr_private.h"
#include "rtrlib/rtr_mgr_private.h"
#include "rtrlib/rtrlib_export_private.h"
#include "rtrlib/spki/hashtable/ht-spkitable_private.h"
#include "rtrlib/transport/transport_private.h"

#define MGR_DBG(fmt, ...) lrtr_dbg("RTR_MGR: " fmt, ## __VA_ARGS__)
#define MGR_DBG1(a) lrtr_dbg("RTR_MGR: " a)

static const char * const mgr_str_status[] = {
	[RTR_MGR_CLOSED] = "RTR_MGR_CLOSED",
	[RTR_MGR_CONNECTING] = "RTR_MGR_CONNECTING",
	[RTR_MGR_ESTABLISHED] = "RTR_MGR_ESTABLISHED",
	[RTR_MGR_ERROR] = "RTR_MGR_ERROR",
};

static int rtr_mgr_config_cmp(const void *a, const void *b);
static int rtr_mgr_config_cmp_tommy(const void *a, const void *b);
static bool rtr_mgr_config_status_is_synced(const struct rtr_mgr_group *group);
static void rtr_mgr_cb(const struct rtr_socket *sock,
		       const enum rtr_socket_state state,
		       void *data_config, void *data_group);

static void set_status(const struct rtr_mgr_config *conf,
		       struct rtr_mgr_group *group,
		       enum rtr_mgr_status mgr_status,
		       const struct rtr_socket *rtr_sock)
{
	MGR_DBG("Group(%u) status changed to: %s", group->preference,
		rtr_mgr_status_to_str(mgr_status));

	group->status = mgr_status;
	if (conf->status_fp)
		conf->status_fp(group, mgr_status,
				rtr_sock, conf->status_fp_data);
}

static int rtr_mgr_start_sockets(struct rtr_mgr_group *group)
{
	for (unsigned int i = 0; i < group->sockets_len; i++) {
		if (rtr_start(group->sockets[i]) != 0) {
			MGR_DBG1("rtr_mgr: Error starting rtr_socket pthread");
			return RTR_ERROR;
		}
	}
	group->status = RTR_MGR_CONNECTING;
	return RTR_SUCCESS;
}

static int rtr_mgr_init_sockets(struct rtr_mgr_group *group,
				struct rtr_mgr_config *config,
				const unsigned int refresh_interval,
				const unsigned int expire_interval,
				const unsigned int retry_interval,
				enum rtr_interval_mode iv_mode)
{
	for (unsigned int i = 0; i < group->sockets_len; i++) {
		enum rtr_rtvals err_code = rtr_init(group->sockets[i], NULL,
						    config->pfx_table,
						    config->spki_table,
						    refresh_interval,
						    expire_interval,
						    retry_interval, iv_mode,
						    rtr_mgr_cb, config, group);
		if (err_code)
			return err_code;
	}
	return RTR_SUCCESS;
}

bool rtr_mgr_config_status_is_synced(const struct rtr_mgr_group *group)
{
	for (unsigned int i = 0; i < group->sockets_len; i++) {
		enum rtr_socket_state state = group->sockets[i]->state;

		if ((group->sockets[i]->last_update == 0) ||
		    ((state != RTR_ESTABLISHED) && (state != RTR_RESET) &&
		     (state != RTR_SYNC)))
			return false;
	}
	return true;
}

static void rtr_mgr_close_less_preferable_groups(const struct rtr_socket *sock,
						 struct rtr_mgr_config *config,
						 struct rtr_mgr_group *group)
{
	pthread_mutex_lock(&config->mutex);
	tommy_node *node = tommy_list_head(&config->groups->list);

	while (node) {
		struct rtr_mgr_group_node *group_node = node->data;
		struct rtr_mgr_group *current_group = group_node->group;

		if ((current_group->status != RTR_MGR_CLOSED) &&
		    (current_group != group) &&
		    (current_group->preference > group->preference)) {
			for (unsigned int j = 0;
				j < current_group->sockets_len; j++) {
				rtr_stop(current_group->sockets[j]);
			}
			set_status(config, current_group, RTR_MGR_CLOSED, sock);
		}
	node = node->next;
	}
	pthread_mutex_unlock(&config->mutex);
}

static struct rtr_mgr_group *get_best_inactive_rtr_mgr_group(
						struct rtr_mgr_config *config,
						struct rtr_mgr_group *group)
{
	pthread_mutex_lock(&config->mutex);
	tommy_node *node = tommy_list_head(&config->groups->list);

	while (node) {
		struct rtr_mgr_group_node *group_node = node->data;
		struct rtr_mgr_group *current_group = group_node->group;
			if ((current_group != group) &&
			    (current_group->status == RTR_MGR_CLOSED)) {
				pthread_mutex_unlock(&config->mutex);
				return current_group;
			}
		node = node->next;
	}
		pthread_mutex_unlock(&config->mutex);
	return NULL;
}

static bool is_some_rtr_mgr_group_established(struct rtr_mgr_config *config)
{
	pthread_mutex_lock(&config->mutex);
	tommy_node *node = tommy_list_head(&config->groups->list);

	while (node) {
		struct rtr_mgr_group_node *group_node = node->data;

		if (group_node->group->status == RTR_MGR_ESTABLISHED) {
			pthread_mutex_unlock(&config->mutex);
			return true;
		}
		node = node->next;
	}
	pthread_mutex_unlock(&config->mutex);
	return false;
}

static inline void _rtr_mgr_cb_state_shutdown(const struct rtr_socket *sock,
					      struct rtr_mgr_config *config,
					      struct rtr_mgr_group *group)
{
	bool all_down = true;

	for (unsigned int i = 0; i < group->sockets_len; i++) {
		if (group->sockets[i]->state != RTR_SHUTDOWN) {
			all_down = false;
			break;
		}
	}
	if (all_down)
		set_status(config, group,
			   RTR_MGR_CLOSED, sock);
	else
		set_status(config, group,
			   group->status, sock);
}

static inline void _rtr_mgr_cb_state_established(const struct rtr_socket *sock,
						 struct rtr_mgr_config *config,
						 struct rtr_mgr_group *group)
{
	/* socket established a connection to the rtr_server */
	if (group->status == RTR_MGR_CONNECTING) {
		/*
		 * if previous state was CONNECTING, check if all
		 * other sockets in the group also have a established
		 * connection, if yes change group state to ESTABLISHED
		 */
		if (rtr_mgr_config_status_is_synced(group)) {
			set_status(config, group,
				   RTR_MGR_ESTABLISHED, sock);
			rtr_mgr_close_less_preferable_groups(sock, config,
							     group);
		} else {
			set_status(config, group,
				   RTR_MGR_CONNECTING, sock);
		}
	} else if (group->status == RTR_MGR_ERROR) {
		/* if previous state was ERROR, only change state to
		 * ESTABLISHED if all other more preferable socket
		 * groups are also in ERROR or SHUTDOWN state
		 */
		bool all_error = true;

		pthread_mutex_lock(&config->mutex);
		tommy_node *node = tommy_list_head(&config->groups->list);

		while (node) {
			struct rtr_mgr_group_node *group_node = node->data;
			struct rtr_mgr_group *current_group = group_node->group;

			if ((current_group != group) &&
			    (current_group->status != RTR_MGR_ERROR) &&
			    (current_group->status != RTR_MGR_CLOSED) &&
			    (current_group->preference < group->preference)) {
				all_error = false;
			}
			node = node->next;
		}
		pthread_mutex_unlock(&config->mutex);

		if (all_error && rtr_mgr_config_status_is_synced(group)) {
			set_status(config, group, RTR_MGR_ESTABLISHED, sock);
			rtr_mgr_close_less_preferable_groups(sock, config,
							     group);
		} else {
			set_status(config, group, RTR_MGR_ERROR, sock);
		}
	}
}

static inline void _rtr_mgr_cb_state_connecting(const struct rtr_socket *sock,
						struct rtr_mgr_config *config,
						struct rtr_mgr_group *group)
{
	if (group->status == RTR_MGR_ERROR)
		set_status(config, group,
			   RTR_MGR_ERROR, sock);
	else
		set_status(config, group,
			   RTR_MGR_CONNECTING, sock);
}

static inline void _rtr_mgr_cb_state_error(const struct rtr_socket *sock,
					   struct rtr_mgr_config *config,
					   struct rtr_mgr_group *group)
{
	set_status(config, group, RTR_MGR_ERROR, sock);

	if (!is_some_rtr_mgr_group_established(config)) {
		struct rtr_mgr_group *next_group =
			get_best_inactive_rtr_mgr_group(config, group);

		if (next_group)
			rtr_mgr_start_sockets(next_group);
		else
			MGR_DBG1("No other inactive groups found");
	}
}

static void rtr_mgr_cb(const struct rtr_socket *sock,
		       const enum rtr_socket_state state,
		       void *data_config, void *data_group)
{
	if (state == RTR_SHUTDOWN)
		MGR_DBG1("Received RTR_SHUTDOWN callback");

	struct rtr_mgr_config *config = data_config;
	struct rtr_mgr_group *group = data_group;

	if (!group) {
		MGR_DBG1("ERROR: Socket has no group");
		return;
	}

	switch (state) {
	case RTR_SHUTDOWN:
		_rtr_mgr_cb_state_shutdown(sock, config, group);
		break;
	case RTR_ESTABLISHED:
		_rtr_mgr_cb_state_established(sock, config, group);
		break;
	case RTR_CONNECTING:
		_rtr_mgr_cb_state_connecting(sock, config, group);
		break;
	case RTR_ERROR_FATAL:
	case RTR_ERROR_TRANSPORT:
	case RTR_ERROR_NO_DATA_AVAIL:
		_rtr_mgr_cb_state_error(sock, config, group);
		break;
	default:
		set_status(config, group, group->status, sock);
	}
}

int rtr_mgr_config_cmp(const void *a, const void *b)
{
	const struct rtr_mgr_group *ar = a;
	const struct rtr_mgr_group *br = b;

	if (ar->preference > br->preference)
		return 1;
	else if (ar->preference < br->preference)
		return -1;
	return 0;
}

int rtr_mgr_config_cmp_tommy(const void *a, const void *b)
{
	const struct rtr_mgr_group_node *ar = a;
	const struct rtr_mgr_group_node *br = b;

	return rtr_mgr_config_cmp(ar->group, br->group);
}

RTRLIB_EXPORT int rtr_mgr_init(struct rtr_mgr_config **config_out,
			       struct rtr_mgr_group groups[],
			       const unsigned int groups_len,
			       const unsigned int refresh_interval,
			       const unsigned int expire_interval,
			       const unsigned int retry_interval,
			       const pfx_update_fp update_fp,
			       const spki_update_fp spki_update_fp,
			       const rtr_mgr_status_fp status_fp,
			       void *status_fp_data)
{
	enum rtr_rtvals err_code = RTR_ERROR;
	enum rtr_interval_mode iv_mode = RTR_INTERVAL_MODE_DEFAULT_MIN_MAX;
	struct pfx_table *pfxt = NULL;
	struct spki_table *spki_table = NULL;
	struct rtr_mgr_config *config = NULL;
	struct rtr_mgr_group *cg =  NULL;
	struct rtr_mgr_group_node *group_node;
	uint8_t last_preference = UINT8_MAX;

	*config_out = NULL;

	if (groups_len == 0) {
		MGR_DBG1("Error Empty rtr_mgr_group array");
		return RTR_ERROR;
	}

	*config_out = config = lrtr_malloc(sizeof(*config));
	if (!config)
		return RTR_ERROR;

	config->len = groups_len;

	if (pthread_mutex_init(&config->mutex, NULL) != 0) {
		MGR_DBG1("Mutex initialization failed");
		goto err;
	}
	/* sort array in asc order, so we can check for dupl. pref */
	qsort(groups, groups_len,
	      sizeof(struct rtr_mgr_group), &rtr_mgr_config_cmp);

	/* Check that the groups have unique pref and at least one socket */
	for (unsigned int i = 0; i < config->len; i++) {
		if ((i > 0) && (groups[i].preference == last_preference)) {
			MGR_DBG1("Error Same preference for 2 socket groups!");
			goto err;
		}
		if (groups[i].sockets_len == 0) {
			MGR_DBG1("Error Empty sockets array in socket group!");
			goto err;
		}

		last_preference = groups[i].preference;
	}

	/* Init data structures that we need to pass to the sockets */
	pfxt = lrtr_malloc(sizeof(*pfxt));
	if (!pfxt)
		goto err;
	pfx_table_init(pfxt, update_fp);

	spki_table = lrtr_malloc(sizeof(*spki_table));
	if (!spki_table)
		goto err;
	spki_table_init(spki_table, spki_update_fp);

	config->pfx_table = pfxt;
	config->spki_table = spki_table;

	/* Copy the groups from the array into linked list config->groups */
	config->len = groups_len;
	config->groups = lrtr_malloc(sizeof(*config->groups));
	if (!config->groups)
		goto err;
	config->groups->list = NULL;

	for (unsigned int i = 0; i < groups_len; i++) {
		cg = lrtr_malloc(sizeof(struct rtr_mgr_group));
		if (!cg)
			goto err;

		memcpy(cg, &groups[i], sizeof(struct rtr_mgr_group));

		cg->status = RTR_MGR_CLOSED;
		err_code = rtr_mgr_init_sockets(cg, config, refresh_interval,
						expire_interval,
						retry_interval,
						iv_mode);
		if (err_code)
			goto err;

		group_node = lrtr_malloc(sizeof(struct rtr_mgr_group_node));
		if (!group_node)
			goto err;

		group_node->group = cg;
		tommy_list_insert_tail(&config->groups->list, &group_node->node,
				       group_node);
	}
	/* Our linked list should be sorted already, since the groups array was
	 * sorted. However, for safety reasons we sort again.
	 */
	tommy_list_sort(&config->groups->list, &rtr_mgr_config_cmp_tommy);

	config->status_fp_data = status_fp_data;
	config->status_fp = status_fp;
	return RTR_SUCCESS;

err:
	if (spki_table)
		spki_table_free(spki_table);
	if (pfxt)
		pfx_table_free(pfxt);
	lrtr_free(pfxt);
	lrtr_free(spki_table);

	lrtr_free(cg);

	lrtr_free(config->groups);
	lrtr_free(config);
	config = NULL;
	*config_out = NULL;

	return err_code;
}

RTRLIB_EXPORT struct rtr_mgr_group *rtr_mgr_get_first_group(
		struct rtr_mgr_config *config)
{
	tommy_node *head = tommy_list_head(&config->groups->list);
	struct rtr_mgr_group_node *group_node = head->data;

	return group_node->group;
}

RTRLIB_EXPORT int rtr_mgr_start(struct rtr_mgr_config *config)
{
	MGR_DBG("%s()", __func__);
	struct rtr_mgr_group *best_group = rtr_mgr_get_first_group(config);

	return rtr_mgr_start_sockets(best_group);
}

RTRLIB_EXPORT bool rtr_mgr_conf_in_sync(struct rtr_mgr_config *config)
{
	pthread_mutex_lock(&config->mutex);
	tommy_node *node = tommy_list_head(&config->groups->list);

	while (node) {
		bool all_sync = true;
		struct rtr_mgr_group_node *group_node = node->data;

		for (unsigned int j = 0; all_sync &&
		     (j < group_node->group->sockets_len); j++) {
			if (group_node->group->sockets[j]->last_update == 0)
				all_sync = false;
		}
		if (all_sync) {
			pthread_mutex_unlock(&config->mutex);
			return true;
		}
		node = node->next;
	}
	pthread_mutex_unlock(&config->mutex);
	return false;
}

RTRLIB_EXPORT void rtr_mgr_free(struct rtr_mgr_config *config)
{
	MGR_DBG("%s()", __func__);
	pthread_mutex_lock(&config->mutex);

	pfx_table_free(config->pfx_table);
	spki_table_free(config->spki_table);
	lrtr_free(config->spki_table);
	lrtr_free(config->pfx_table);

	/* Free linked list */
	tommy_node *head = tommy_list_head(&config->groups->list);

	while (head) {
		tommy_node *tmp = head;
		struct rtr_mgr_group_node *group_node = tmp->data;

		head = head->next;
		for (unsigned int j = 0; j < group_node->group->sockets_len;
		     j++) {
			tr_free(group_node->group->sockets[j]->tr_socket);
		}

		lrtr_free(group_node->group);
		lrtr_free(group_node);
	}

	lrtr_free(config->groups);

	pthread_mutex_unlock(&config->mutex);
	pthread_mutex_destroy(&config->mutex);
	lrtr_free(config);
}

/* cppcheck-suppress unusedFunction */
RTRLIB_EXPORT inline int rtr_mgr_validate(struct rtr_mgr_config *config,
					  const uint32_t asn,
					  const struct lrtr_ip_addr *prefix,
					  const uint8_t mask_len,
					  enum pfxv_state *result)
{
	return pfx_table_validate(config->pfx_table, asn, prefix, mask_len,
				  result);
}

/* cppcheck-suppress unusedFunction */
RTRLIB_EXPORT inline int rtr_mgr_get_spki(struct rtr_mgr_config *config,
					  const uint32_t asn,
					  uint8_t *ski,
					  struct spki_record **result,
					  unsigned int *result_count)
{
	return spki_table_get_all(config->spki_table,
				  asn, ski, result, result_count);
}

RTRLIB_EXPORT void rtr_mgr_stop(struct rtr_mgr_config *config)
{
	pthread_mutex_lock(&config->mutex);
	tommy_node *node = tommy_list_head(&config->groups->list);

	MGR_DBG("%s()", __func__);
	while (node) {
		struct rtr_mgr_group_node *group_node = node->data;

		for (unsigned int j = 0; j < group_node->group->sockets_len;
		     j++) {
			rtr_stop(group_node->group->sockets[j]);
		}
	node = node->next;
	}
	pthread_mutex_unlock(&config->mutex);
}

/* cppcheck-suppress unusedFunction */
RTRLIB_EXPORT int rtr_mgr_add_group(
		struct rtr_mgr_config *config,
		const struct rtr_mgr_group *group)
{
	unsigned int refresh_iv = 3600;
	unsigned int retry_iv = 600;
	unsigned int expire_iv = 7200;
	enum rtr_interval_mode iv_mode = RTR_INTERVAL_MODE_DEFAULT_MIN_MAX;
	enum rtr_rtvals err_code = RTR_ERROR;
	struct rtr_mgr_group_node *new_group_node = NULL;
	struct rtr_mgr_group *new_group = NULL;
	struct rtr_mgr_group_node *gnode;

	pthread_mutex_lock(&config->mutex);

	tommy_node *node = tommy_list_head(&config->groups->list);

	while (node) {
		gnode = node->data;
		if (gnode->group->preference == group->preference) {
			MGR_DBG1("Group with preference value already exists!");
			err_code = RTR_INVALID_PARAM;
			goto err;
		}

		//TODO This is not pretty. It wants to get the same intervals
		//that are being used by other groups. Maybe intervals should
		//be store globally/per-group/per-socket?
		if (gnode->group->sockets[0]->refresh_interval)
			refresh_iv = gnode->group->sockets[0]->refresh_interval;
		if (gnode->group->sockets[0]->retry_interval)
			retry_iv = gnode->group->sockets[0]->retry_interval;
		if (gnode->group->sockets[0]->expire_interval)
			expire_iv = gnode->group->sockets[0]->expire_interval;
		node = node->next;
	}
	new_group = lrtr_malloc(sizeof(struct rtr_mgr_group));

	if (!new_group)
		goto err;

	memcpy(new_group, group, sizeof(struct rtr_mgr_group));
	new_group->status = RTR_MGR_CLOSED;

	err_code = rtr_mgr_init_sockets(new_group, config, refresh_iv,
					expire_iv, retry_iv, iv_mode);
	if (err_code)
		goto err;

	new_group_node = lrtr_malloc(sizeof(struct rtr_mgr_group_node));
	if (!new_group_node)
		goto err;

	new_group_node->group = new_group;
	tommy_list_insert_tail(&config->groups->list, &new_group_node->node,
			       new_group_node);
	config->len++;

	MGR_DBG("Group with preference %d successfully added!",
		new_group->preference);

	tommy_list_sort(&config->groups->list, &rtr_mgr_config_cmp_tommy);

	struct rtr_mgr_group *best_group = rtr_mgr_get_first_group(config);

	if (best_group->status == RTR_MGR_CLOSED)
		rtr_mgr_start_sockets(best_group);

	pthread_mutex_unlock(&config->mutex);
	return RTR_SUCCESS;

err:
	pthread_mutex_unlock(&config->mutex);

	if (new_group)
		lrtr_free(new_group);

	return err_code;
}

/* cppcheck-suppress unusedFunction */
RTRLIB_EXPORT int rtr_mgr_remove_group(
		struct rtr_mgr_config *config,
		unsigned int preference)
{
	pthread_mutex_lock(&config->mutex);
	tommy_node *remove_node = NULL;
	tommy_node *node = tommy_list_head(&config->groups->list);
	struct rtr_mgr_group_node *group_node;
	struct rtr_mgr_group *remove_group;

	if (config->len == 1) {
		MGR_DBG1("Cannot remove last remaining group!");
		pthread_mutex_unlock(&config->mutex);
		return RTR_ERROR;
	}

	// Find the node of the group we want to remove
	while (node && !remove_node) {
		group_node = node->data;
		if (group_node->group->preference == preference)
			remove_node = node;
		node = node->next;
	}

	if (!remove_node) {
		MGR_DBG1("The group that should be removed does not exist!");
		pthread_mutex_unlock(&config->mutex);
		return RTR_ERROR;
	}

	group_node = remove_node->data;
	remove_group = group_node->group;
	tommy_list_remove_existing(&config->groups->list, remove_node);
	config->len--;
	MGR_DBG("Group with preference %d successfully removed!", preference);
	//tommy_list_sort(&config->groups->list, &rtr_mgr_config_cmp);
	pthread_mutex_unlock(&config->mutex);

	//If group isn't closed, make it so!
	if (remove_group->status != RTR_MGR_CLOSED) {
		for (unsigned int j = 0; j < remove_group->sockets_len; j++) {
			rtr_stop(remove_group->sockets[j]);
			tr_free(remove_group->sockets[j]->tr_socket);
		}
		set_status(config, remove_group, RTR_MGR_CLOSED, NULL);
	}

	struct rtr_mgr_group *best_group = rtr_mgr_get_first_group(config);

	if (best_group->status == RTR_MGR_CLOSED)
		rtr_mgr_start_sockets(best_group);

	lrtr_free(group_node->group);
	lrtr_free(group_node);
	return RTR_SUCCESS;
}

// TODO: write test for this function.
/* cppcheck-suppress unusedFunction */
RTRLIB_EXPORT int rtr_mgr_for_each_group(
		struct rtr_mgr_config *config,
		void (fp)(const struct rtr_mgr_group *group, void *data),
		void *data)
{
	tommy_node *node = tommy_list_head(&config->groups->list);

	while (node) {
		struct rtr_mgr_group_node *group_node = node->data;

		fp(group_node->group, data);
		node = node->next;
	}

	return RTR_SUCCESS;
}

RTRLIB_EXPORT const char *rtr_mgr_status_to_str(enum rtr_mgr_status status)
{
	return mgr_str_status[status];
}

/* cppcheck-suppress unusedFunction */
RTRLIB_EXPORT inline void rtr_mgr_for_each_ipv4_record(
		struct rtr_mgr_config *config,
		void (fp)(
			const struct pfx_record *,
			void *data),
		void *data)
{
	pfx_table_for_each_ipv4_record(config->pfx_table,
				       fp, data);
}

/* cppcheck-suppress unusedFunction */
RTRLIB_EXPORT inline void rtr_mgr_for_each_ipv6_record(
		struct rtr_mgr_config *config,
		void (fp)(
			const struct pfx_record *,
			void *data),
		void *data)
{
	pfx_table_for_each_ipv6_record(config->pfx_table,
				       fp, data);
}
