/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "path-util.h"
#include "array.h"
#include "istream.h"
#include "ostream.h"
#include "file-dotlock.h"
#include "str.h"
#include "strescape.h"
#include "master-instance.h"

#include <unistd.h>
#include <fcntl.h>

struct master_instance_list {
	pool_t pool;
	const char *path;

	ARRAY(struct master_instance) instances;

	bool locked:1;
	bool config_paths_changed:1;
};

struct master_instance_list_iter {
	struct master_instance_list *list;
	unsigned int idx;
};

static const struct dotlock_settings dotlock_set = {
	.timeout = 10,
	.stale_timeout = 60,
	.use_io_notify = TRUE,
};

struct master_instance_list *master_instance_list_init(const char *path)
{
	struct master_instance_list *list;
	pool_t pool;

	pool = pool_alloconly_create(MEMPOOL_GROWING"master instances", 256);
	list = p_new(pool, struct master_instance_list, 1);
	list->pool = pool;
	list->path = p_strdup(pool, path);
	p_array_init(&list->instances, pool, 8);
	return list;
}

void master_instance_list_deinit(struct master_instance_list **_list)
{
	struct master_instance_list *list = *_list;

	*_list = NULL;
	pool_unref(&list->pool);
}

static void
master_instance_update_config_path(struct master_instance_list *list,
				   struct master_instance *inst)
{
	const char *path, *config_path, *error;

	/* update instance's config path if it has changed */
	path = t_strconcat(inst->base_dir, "/"PACKAGE".conf", NULL);
	if (t_readlink(path, &config_path, &error) < 0) {
		/* The link may not exist, ignore the error. */
		if (errno != ENOENT)
			i_error("t_readlink(%s) failed: %s", path, error);
		return;
	}
	if (null_strcmp(inst->config_path, config_path) != 0) {
		inst->config_path = p_strdup(list->pool, config_path);
		list->config_paths_changed = TRUE;
	}
}

static int
master_instance_list_add_line(struct master_instance_list *list,
			      const char *line)
{
	struct master_instance *inst;
	const char *const *args;
	time_t last_used;

	/* <last used> <name> <base dir> [<config path>] */
	args = t_strsplit_tabescaped(line);
	if (str_array_length(args) < 3)
		return -1;
	if (str_to_time(args[0], &last_used) < 0)
		return -1;

	inst = array_append_space(&list->instances);
	inst->last_used = last_used;
	inst->name = p_strdup(list->pool, args[1]);
	inst->base_dir = p_strdup(list->pool, args[2]);
	inst->config_path = p_strdup_empty(list->pool, args[3]);
	master_instance_update_config_path(list, inst);
	return 0;
}

static int master_instance_list_refresh(struct master_instance_list *list)
{
	struct istream *input;
	const char *line;
	int fd, ret = 0;

	array_clear(&list->instances);

	fd = open(list->path, O_RDONLY);
	if (fd == -1) {
		if (errno == ENOENT)
			return 0;

		i_error("open(%s) failed: %m", list->path);
		return -1;
	}
	input = i_stream_create_fd_autoclose(&fd, SIZE_MAX);
	while ((line = i_stream_read_next_line(input)) != NULL) T_BEGIN {
		if (master_instance_list_add_line(list, line) < 0)
			i_error("Invalid line in %s: %s", list->path, line);
	} T_END;
	if (input->stream_errno != 0) {
		i_error("read(%s) failed: %s", list->path,
			i_stream_get_error(input));
		ret = -1;
	}
	i_stream_destroy(&input);
	return ret;
}

static int
master_instance_list_write(struct master_instance_list *list,
			   int fd, const char *path)
{
	struct ostream *output;
	const struct master_instance *inst;
	string_t *str = t_str_new(128);
	int ret = 0;

	output = o_stream_create_fd(fd, 0);
	o_stream_cork(output);
	array_foreach(&list->instances, inst) {
		str_truncate(str, 0);
		str_printfa(str, "%ld\t", (long)inst->last_used);
		str_append_tabescaped(str, inst->name);
		str_append_c(str, '\t');
		str_append_tabescaped(str, inst->base_dir);
		str_append_c(str, '\t');
		if (inst->config_path != NULL)
			str_append_tabescaped(str, inst->config_path);
		str_append_c(str, '\n');
		o_stream_nsend(output, str_data(str), str_len(str));
	}
	if (o_stream_finish(output) < 0) {
		i_error("write(%s) failed: %s", path, o_stream_get_error(output));
		ret = -1;
	}
	o_stream_destroy(&output);
	return ret;
}

static int master_instance_write_init(struct master_instance_list *list,
				      struct dotlock **dotlock_r)
{
	int fd;

	i_assert(!list->locked);

	*dotlock_r = NULL;

	fd = file_dotlock_open_mode(&dotlock_set, list->path, 0, 0644,
				    (uid_t)-1, (gid_t)-1, dotlock_r);
	if (fd == -1) {
		i_error("file_dotlock_open(%s) failed: %m", list->path);
		return -1;
	}
	if (master_instance_list_refresh(list) < 0) {
		file_dotlock_delete(dotlock_r);
		return -1;
	}
	list->locked = TRUE;
	return fd;
}

static int master_instance_write_finish(struct master_instance_list *list,
					int fd, struct dotlock **dotlock)
{
	const char *lock_path = file_dotlock_get_lock_path(*dotlock);
	int ret;

	i_assert(list->locked);

	T_BEGIN {
		ret = master_instance_list_write(list, fd, lock_path);
	} T_END;

	list->locked = FALSE;
	if (ret < 0) {
		file_dotlock_delete(dotlock);
		return -1;
	}
	if (fdatasync(fd) < 0) {
		i_error("fdatasync(%s) failed: %m", lock_path);
		file_dotlock_delete(dotlock);
		return -1;
	}
	list->config_paths_changed = FALSE;
	return file_dotlock_replace(dotlock, 0);
}

static struct master_instance *
master_instance_find(struct master_instance_list *list,
		     const char *base_dir)
{
	struct master_instance *inst;

	array_foreach_modifiable(&list->instances, inst) {
		if (strcmp(inst->base_dir, base_dir) == 0)
			return inst;
	}
	return NULL;
}

int master_instance_list_update(struct master_instance_list *list,
				const char *base_dir)
{
	struct master_instance *inst;
	struct dotlock *dotlock;
	int fd;

	if ((fd = master_instance_write_init(list, &dotlock)) == -1)
		return -1;

	inst = master_instance_find(list, base_dir);
	if (inst == NULL) {
		inst = array_append_space(&list->instances);
		inst->name = "";
		inst->base_dir = p_strdup(list->pool, base_dir);
	}
	inst->last_used = time(NULL);
	master_instance_update_config_path(list, inst);

	return master_instance_write_finish(list, fd, &dotlock);
}

int master_instance_list_set_name(struct master_instance_list *list,
				  const char *base_dir, const char *name)
{
	const struct master_instance *orig_inst;
	struct master_instance *inst;
	struct dotlock *dotlock;
	int fd;

	i_assert(*name != '\0');

	if ((fd = master_instance_write_init(list, &dotlock)) == -1)
		return -1;

	orig_inst = master_instance_list_find_by_name(list, name);
	if (orig_inst != NULL &&
	    strcmp(orig_inst->base_dir, base_dir) != 0) {
		/* name already used */
		file_dotlock_delete(&dotlock);
		list->locked = FALSE;
		return 0;
	}

	inst = master_instance_find(list, base_dir);
	if (inst == NULL) {
		inst = array_append_space(&list->instances);
		inst->base_dir = p_strdup(list->pool, base_dir);
	}
	inst->name = p_strdup(list->pool, name);
	inst->last_used = time(NULL);

	return master_instance_write_finish(list, fd, &dotlock) < 0 ? -1 : 1;
}

int master_instance_list_remove(struct master_instance_list *list,
				const char *base_dir)
{
	struct dotlock *dotlock;
	const struct master_instance *instances;
	unsigned int i, count;
	int fd;

	if ((fd = master_instance_write_init(list, &dotlock)) == -1)
		return -1;

	instances = array_get(&list->instances, &count);
	for (i = 0; i < count; i++) {
		if (strcmp(instances[i].base_dir, base_dir) == 0) {
			array_delete(&list->instances, i, 1);
			break;
		}
	}

	if (i == count) {
		file_dotlock_delete(&dotlock);
		list->locked = FALSE;
		return 0;
	}
	return master_instance_write_finish(list, fd, &dotlock) < 0 ? -1 : 1;
}

static int
master_instance_list_refresh_and_update(struct master_instance_list *list)
{
	struct dotlock *dotlock;
	int fd;

	if (master_instance_list_refresh(list) < 0)
		return -1;
	if (list->config_paths_changed && !list->locked) {
		/* write new config paths */
		if ((fd = master_instance_write_init(list, &dotlock)) == -1)
			return -1;
		if (master_instance_write_finish(list, fd, &dotlock) < 0)
			return -1;
	}
	return 0;
}

const struct master_instance *
master_instance_list_find_by_name(struct master_instance_list *list,
				  const char *name)
{
	const struct master_instance *inst;

	i_assert(*name != '\0');

	if (array_count(&list->instances) == 0)
		(void)master_instance_list_refresh_and_update(list);

	array_foreach(&list->instances, inst) {
		if (strcmp(inst->name, name) == 0)
			return inst;
	}
	return NULL;
}

struct master_instance_list_iter *
master_instance_list_iterate_init(struct master_instance_list *list)
{
	struct master_instance_list_iter *iter;

	iter = i_new(struct master_instance_list_iter, 1);
	iter->list = list;
	(void)master_instance_list_refresh_and_update(list);
	return iter;
}

const struct master_instance *
master_instance_iterate_list_next(struct master_instance_list_iter *iter)
{
	if (iter->idx == array_count(&iter->list->instances))
		return NULL;
	return array_idx(&iter->list->instances, iter->idx++);
}

void master_instance_iterate_list_deinit(struct master_instance_list_iter **_iter)
{
	struct master_instance_list_iter *iter = *_iter;

	*_iter = NULL;

	i_free(iter);
}
