/* Copyright (c) Dovecot authors, see top-level COPYING file */

#include "stats-common.h"
#include "array.h"
#include "llist.h"
#include "hash.h"
#include "str.h"
#include "strescape.h"
#include "lib-event-private.h"
#include "event-filter.h"
#include "ostream.h"
#include "connection.h"
#include "master-service.h"
#include "stats-event-category.h"
#include "stats-metrics.h"
#include "stats-settings.h"
#include "client-writer.h"

#define STATS_UPDATE_CLIENTS_DELAY_MSECS 1000

struct stats_event {
	struct stats_event *prev, *next;

	uint64_t id;
	struct event *event;
};

struct writer_client {
	struct connection conn;

	struct stats_event *events;
	HASH_TABLE(struct stats_event *, struct stats_event *) events_hash;
};

static struct timeout *to_update_clients;
static struct connection_list *writer_clients = NULL;

static void client_writer_send_handshake(struct writer_client *client)
{
	string_t *filter = t_str_new(128);
	string_t *str = t_str_new(128);

	event_filter_export(stats_metrics_get_event_filter(stats_metrics), filter);

	str_append(str, "FILTER\t");
	str_append_tabescaped(str, str_c(filter));
	str_append_c(str, '\n');
	o_stream_nsend(client->conn.output, str_data(str), str_len(str));
}

static unsigned int stats_event_hash(const struct stats_event *event)
{
	return (unsigned int)event->id;
}

static int stats_event_cmp(const struct stats_event *event1,
			   const struct stats_event *event2)
{
	return event1->id == event2->id ? 0 : 1;
}

void client_writer_create(int fd)
{
	struct writer_client *client;

	client = i_new(struct writer_client, 1);
	hash_table_create(&client->events_hash, default_pool, 0,
			  stats_event_hash, stats_event_cmp);

	connection_init_server(writer_clients, &client->conn,
			       "stats", fd, fd);
	client_writer_send_handshake(client);
}

static void writer_client_destroy(struct connection *conn)
{
	struct writer_client *client = (struct writer_client *)conn;
	struct stats_event *event, *next;

	for (event = client->events; event != NULL; event = next) {
		next = event->next;
		event_unref(&event->event);
		i_free(event);
	}
	hash_table_destroy(&client->events_hash);

	connection_deinit(conn);
	i_free(conn);

	master_service_client_connection_destroyed(master_service);
}

static struct stats_event *
writer_client_find_event(struct writer_client *client, uint64_t event_id)
{
	struct stats_event lookup_event = { .id = event_id };
	return hash_table_lookup(client->events_hash, &lookup_event);
}

static bool
writer_client_run_event(struct writer_client *client,
			uint64_t parent_event_id, const char *const *args,
			struct event **event_r, const char **error_r)
{
	struct event *parent_event;
	unsigned int log_type;

	if (parent_event_id == 0)
		parent_event = NULL;
	else {
		struct stats_event *stats_parent_event =
			writer_client_find_event(client, parent_event_id);
		if (stats_parent_event == NULL) {
			*error_r = "Unknown parent event ID";
			return FALSE;
		}
		parent_event = stats_parent_event->event;
	}
	if (args[0] == NULL || str_to_uint(args[0], &log_type) < 0 ||
	    log_type >= LOG_TYPE_COUNT) {
		*error_r = "Invalid log type";
		return FALSE;
	}
	const struct failure_context ctx = {
		.type = (enum log_type)log_type
	};
	args++;

	struct event *event = event_create(parent_event);
	if (!event_import_unescaped(event, args, error_r)) {
		event_unref(&event);
		return FALSE;
	}
	stats_metrics_event(stats_metrics, event, &ctx);
	*event_r = event;
	return TRUE;
}

static bool
writer_client_input_event(struct writer_client *client,
			  const char *const *args, const char **error_r)
{
	struct event *event, *global_event = NULL;
	uint64_t parent_event_id, global_event_id;
	bool ret;

	if (args[1] == NULL || str_to_uint64(args[0], &global_event_id) < 0) {
		*error_r = "Invalid global event ID";
		return FALSE;
	}
	if (args[1] == NULL || str_to_uint64(args[1], &parent_event_id) < 0) {
		*error_r = "Invalid parent ID";
		return FALSE;
	}

	if (global_event_id != 0) {
		struct stats_event *stats_global_event =
			writer_client_find_event(client, global_event_id);
		if (stats_global_event == NULL) {
			*error_r = "Unknown global event ID";
			return FALSE;
		}
		global_event = stats_global_event->event;
		event_push_global(global_event);
	}

	ret = writer_client_run_event(client, parent_event_id, args+2,
				      &event, error_r);
	if (global_event != NULL)
		event_pop_global(global_event);
	if (!ret)
		return FALSE;
	event_unref(&event);
	return TRUE;
}

static bool
writer_client_input_event_begin(struct writer_client *client,
				const char *const *args, const char **error_r)
{
	struct event *event;
	struct stats_event *stats_event;
	uint64_t event_id, parent_event_id;

	if (args[0] == NULL || args[1] == NULL ||
	    str_to_uint64(args[0], &event_id) < 0 ||
	    str_to_uint64(args[1], &parent_event_id) < 0) {
		*error_r = "Invalid event IDs";
		return FALSE;
	}
	if (writer_client_find_event(client, event_id) != NULL) {
		*error_r = "Duplicate event ID";
		return FALSE;
	}
	if (!writer_client_run_event(client, parent_event_id, args+2, &event, error_r))
		return FALSE;

	stats_event = i_new(struct stats_event, 1);
	stats_event->id = event_id;
	stats_event->event = event;
	DLLIST_PREPEND(&client->events, stats_event);
	hash_table_insert(client->events_hash, stats_event, stats_event);
	return TRUE;
}

static bool
writer_client_input_event_update(struct writer_client *client,
				 const char *const *args, const char **error_r)
{
	struct stats_event *stats_event, *parent_stats_event;
	struct event *parent_event;
	uint64_t event_id, parent_event_id;

	if (args[0] == NULL || args[1] == NULL ||
	    str_to_uint64(args[0], &event_id) < 0 ||
	    str_to_uint64(args[1], &parent_event_id) < 0) {
		*error_r = "Invalid event IDs";
		return FALSE;
	}
	stats_event = writer_client_find_event(client, event_id);
	if (stats_event == NULL) {
		*error_r = "Unknown event ID";
		return FALSE;
	}
	parent_stats_event = parent_event_id == 0 ? NULL :
		writer_client_find_event(client, parent_event_id);
	parent_event = parent_stats_event == NULL ? NULL :
		parent_stats_event->event;
	if (stats_event->event->parent != parent_event) {
		*error_r = "Event unexpectedly changed parent";
		return FALSE;
	}
	return event_import_unescaped(stats_event->event, args+2, error_r);
}

static bool
writer_client_input_event_end(struct writer_client *client,
			      const char *const *args, const char **error_r)
{
	struct stats_event *stats_event;
	uint64_t event_id;

	if (args[0] == NULL || str_to_uint64(args[0], &event_id) < 0) {
		*error_r = "Invalid event ID";
		return FALSE;
	}
	stats_event = writer_client_find_event(client, event_id);
	if (stats_event == NULL) {
		*error_r = "Unknown event ID";
		return FALSE;
	}

	DLLIST_REMOVE(&client->events, stats_event);
	hash_table_remove(client->events_hash, stats_event);
	event_unref(&stats_event->event);
	i_free(stats_event);
	return TRUE;
}

static bool
writer_client_input_category(struct writer_client *client ATTR_UNUSED,
			     const char *const *args, const char **error_r)
{
	struct event_category *category, *parent;

	if (args[0] == NULL) {
		*error_r = "Missing category name";
		return FALSE;
	}
	if (args[1] == NULL)
		parent = NULL;
	else if ((parent = event_category_find_registered(args[1])) == NULL) {
		*error_r = "Unknown parent category";
		return FALSE;
	}

	category = event_category_find_registered(args[0]);
	if (category == NULL) {
		/* new category - create */
		stats_event_category_register(args[0], parent);
	} else if (category->parent != parent) {
		*error_r = t_strdup_printf(
			"Category parent '%s' changed to '%s'",
			category->parent == NULL ? "" : category->parent->name,
			parent == NULL ? "" : parent->name);
		return FALSE;
	} else {
		/* duplicate - ignore */
		return TRUE;
	}
	return TRUE;
}

static int
writer_client_input_args(struct connection *conn, const char *const *args)
{
	struct writer_client *client = (struct writer_client *)conn;
	const char *error, *cmd = args[0];
	bool ret;

	if (cmd == NULL) {
		e_error(conn->event, "Client sent empty line");
		return 1;
	}
	if (strcmp(cmd, "EVENT") == 0)
		ret = writer_client_input_event(client, args+1, &error);
	else if (strcmp(cmd, "BEGIN") == 0)
		ret = writer_client_input_event_begin(client, args+1, &error);
	else if (strcmp(cmd, "UPDATE") == 0)
		ret = writer_client_input_event_update(client, args+1, &error);
	else if (strcmp(cmd, "END") == 0)
		ret = writer_client_input_event_end(client, args+1, &error);
	else if (strcmp(cmd, "CATEGORY") == 0)
		ret = writer_client_input_category(client, args+1, &error);
	else {
		error = "Unknown command";
		ret = FALSE;
	}
	if (!ret) {
		e_error(conn->event,
			"Client sent invalid input for %s: %s (input: %s)",
			cmd, error, t_strarray_join(args, "\t"));
		return -1;
	}
	return 1;
}

static struct connection_settings client_set = {
	.service_name_in = "stats-client",
	.service_name_out = "stats-server",
	.major_version = 4,
	.minor_version = 0,

	.input_max_size = 1024*128, /* "big enough" */
	.output_max_size = SIZE_MAX,
	.client = FALSE,
};

static const struct connection_vfuncs client_vfuncs = {
	.destroy = writer_client_destroy,
	.input_args = writer_client_input_args,
};

static void
client_writer_update_connections_internal(void *context ATTR_UNUSED)
{
	struct connection *conn;
	for (conn = writer_clients->connections; conn != NULL; conn = conn->next) {
		struct writer_client *client =
			container_of(conn, struct writer_client, conn);
		client_writer_send_handshake(client);
	}
	timeout_remove(&to_update_clients);
}

void client_writer_update_connections(void)
{
	if (to_update_clients != NULL)
		return;
	to_update_clients = timeout_add(STATS_UPDATE_CLIENTS_DELAY_MSECS,
					client_writer_update_connections_internal,
					NULL);
}

void client_writers_init(void)
{
	writer_clients = connection_list_init(&client_set, &client_vfuncs);
}

void client_writers_deinit(void)
{
	timeout_remove(&to_update_clients);
	connection_list_deinit(&writer_clients);
}
