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

#include "imap-common.h"
#include "seq-range-array.h"
#include "time-util.h"
#include "imap-commands.h"
#include "imap-quote.h"
#include "imap-list.h"
#include "str.h"
#include "mail-search-build.h"
#include "imap-search-args.h"
#include "imap-seqset.h"
#include "imap-fetch.h"
#include "imap-sync.h"


struct imap_select_context {
	struct client_command_context *cmd;
	struct mail_namespace *ns;
	struct mailbox *box;

	struct imap_fetch_context *fetch_ctx;

	uint32_t qresync_uid_validity;
	uint64_t qresync_modseq;
	ARRAY_TYPE(seq_range) qresync_known_uids;
	ARRAY_TYPE(uint32_t) qresync_sample_seqset;
	ARRAY_TYPE(uint32_t) qresync_sample_uidset;

	bool condstore:1;
};

static int select_qresync_get_uids(struct imap_select_context *ctx,
				   const ARRAY_TYPE(seq_range) *seqset,
				   const ARRAY_TYPE(seq_range) *uidset)
{
	const struct seq_range *uid_range;
	struct seq_range_iter seq_iter;
	unsigned int i, uid_count, diff, n = 0;
	uint32_t seq;

	/* change all n:m ranges to n,m and store the results */
	uid_range = array_get(uidset, &uid_count);

	seq_range_array_iter_init(&seq_iter, seqset);
	i_array_init(&ctx->qresync_sample_uidset, uid_count);
	i_array_init(&ctx->qresync_sample_seqset, uid_count);
	for (i = 0; i < uid_count; i++) {
		if (!seq_range_array_iter_nth(&seq_iter, n++, &seq))
			return -1;
		array_push_back(&ctx->qresync_sample_uidset,
				&uid_range[i].seq1);
		array_push_back(&ctx->qresync_sample_seqset, &seq);

		diff = uid_range[i].seq2 - uid_range[i].seq1;
		if (diff > 0) {
			n += diff - 1;
			if (!seq_range_array_iter_nth(&seq_iter, n++, &seq))
				return -1;

			array_push_back(&ctx->qresync_sample_uidset,
					&uid_range[i].seq2);
			array_push_back(&ctx->qresync_sample_seqset, &seq);
		}
	}
	if (seq_range_array_iter_nth(&seq_iter, n, &seq))
		return -1;
	return 0;
}

static bool
select_parse_qresync_known_set(struct imap_select_context *ctx,
			       const struct imap_arg *args,
			       const char **error_r)
{
	ARRAY_TYPE(seq_range) seqset, uidset;
	const char *str;

	t_array_init(&seqset, 32);
	if (!imap_arg_get_atom(args, &str) ||
	    imap_seq_set_nostar_parse(str, &seqset) < 0) {
		*error_r = "Invalid QRESYNC known-sequence-set";
		return FALSE;
	}
	args++;

	t_array_init(&uidset, 32);
	if (!imap_arg_get_atom(args, &str) ||
	    imap_seq_set_nostar_parse(str, &uidset) < 0) {
		*error_r = "Invalid QRESYNC known-uid-set";
		return FALSE;
	}
	args++;

	if (select_qresync_get_uids(ctx, &seqset, &uidset) < 0) {
		*error_r = "Invalid QRESYNC sets";
		return FALSE;
	}
	if (!IMAP_ARG_IS_EOL(args)) {
		*error_r = "Too many parameters to QRESYNC known set";
		return FALSE;
	}
	return TRUE;
}

static bool
select_parse_qresync(struct imap_select_context *ctx,
		     const struct imap_arg *args, const char **error_r)
{
	const struct imap_arg *list_args;
	const char *str;
	unsigned int count;

	if (!client_has_enabled(ctx->cmd->client, imap_feature_qresync)) {
		*error_r = "QRESYNC not enabled";
		return FALSE;
	}
	if (!imap_arg_get_list_full(args, &args, &count)) {
		*error_r = "QRESYNC parameters missing";
		return FALSE;
	}

	if (!imap_arg_get_atom(&args[0], &str) ||
	    str_to_uint32(str, &ctx->qresync_uid_validity) < 0 ||
	    !imap_arg_get_atom(&args[1], &str) ||
	    str_to_uint64(str, &ctx->qresync_modseq) < 0) {
		*error_r = "Invalid QRESYNC parameters";
		return FALSE;
	}
	args += 2;

	i_array_init(&ctx->qresync_known_uids, 64);
	if (imap_arg_get_atom(args, &str)) {
		if (imap_seq_set_nostar_parse(str, &ctx->qresync_known_uids) < 0) {
			*error_r = "Invalid QRESYNC known-uids";
			return FALSE;
		}
		args++;
	} else {
		seq_range_array_add_range(&ctx->qresync_known_uids,
					  1, (uint32_t)-1);
	}
	if (imap_arg_get_list(args, &list_args)) {
		if (!select_parse_qresync_known_set(ctx, list_args, error_r))
			return FALSE;
		args++;
	}
	if (!IMAP_ARG_IS_EOL(args)) {
		*error_r = "Invalid QRESYNC parameters";
		return FALSE;
	}
	return TRUE;
}

static bool
select_parse_options(struct imap_select_context *ctx,
		     const struct imap_arg *args, const char **error_r)
{
	const char *name;

	while (!IMAP_ARG_IS_EOL(args)) {
		if (!imap_arg_get_atom(args, &name)) {
			*error_r = "SELECT options contain non-atoms.";
			return FALSE;
		}
		name = t_str_ucase(name);
		args++;

		if (strcmp(name, "CONDSTORE") == 0)
			ctx->condstore = TRUE;
		else if (strcmp(name, "QRESYNC") == 0) {
			if (!select_parse_qresync(ctx, args, error_r))
				return FALSE;
			args++;
		} else {
			*error_r = "Unknown SELECT option";
			return FALSE;
		}
	}
	return TRUE;
}

static void select_context_free(struct imap_select_context *ctx)
{
	if (array_is_created(&ctx->qresync_known_uids))
		array_free(&ctx->qresync_known_uids);
	if (array_is_created(&ctx->qresync_sample_seqset))
		array_free(&ctx->qresync_sample_seqset);
	if (array_is_created(&ctx->qresync_sample_uidset))
		array_free(&ctx->qresync_sample_uidset);
}

static void cmd_select_finish(struct imap_select_context *ctx, int ret)
{
	const char *resp_code;

	if (ret < 0) {
		if (ctx->box != NULL)
			mailbox_free(&ctx->box);
		ctx->cmd->client->mailbox = NULL;
	} else {
		resp_code = mailbox_is_readonly(ctx->box) ?
			"READ-ONLY" : "READ-WRITE";
		client_send_tagline(ctx->cmd, t_strdup_printf(
			"OK [%s] %s completed", resp_code,
			ctx->cmd->client->mailbox_examined ? "Examine" : "Select"));
	}
	select_context_free(ctx);
}

static bool cmd_select_continue(struct client_command_context *cmd)
{
        struct imap_select_context *ctx = cmd->context;
	int ret;

	if (imap_fetch_more(ctx->fetch_ctx, cmd) == 0) {
		/* unfinished */
		return FALSE;
	}

	ret = imap_fetch_end(ctx->fetch_ctx);
	if (ret < 0)
		client_send_box_error(ctx->cmd, ctx->box);
	imap_fetch_free(&ctx->fetch_ctx);
	cmd_select_finish(ctx, ret);
	return TRUE;
}

static int select_qresync(struct imap_select_context *ctx)
{
	struct imap_fetch_context *fetch_ctx;
	struct mail_search_args *search_args;
	struct imap_fetch_qresync_args qresync_args;
	int ret;

	search_args = mail_search_build_init();
	search_args->args = p_new(search_args->pool, struct mail_search_arg, 1);
	search_args->args->type = SEARCH_UIDSET;
	search_args->args->value.seqset = ctx->qresync_known_uids;
	imap_search_add_changed_since(search_args, ctx->qresync_modseq);

	i_zero(&qresync_args);
	qresync_args.qresync_sample_seqset = &ctx->qresync_sample_seqset;
	qresync_args.qresync_sample_uidset = &ctx->qresync_sample_uidset;

	if (imap_fetch_send_vanished(ctx->cmd->client, ctx->box,
				     search_args, &qresync_args) < 0) {
		mail_search_args_unref(&search_args);
		return -1;
	}

	fetch_ctx = imap_fetch_alloc(ctx->cmd->client, ctx->cmd->pool,
		t_strdup_printf("%s %s", ctx->cmd->name, ctx->cmd->args),
		ctx->cmd->utf8);

	imap_fetch_init_nofail_handler(fetch_ctx, imap_fetch_uid_init);
	imap_fetch_init_nofail_handler(fetch_ctx, imap_fetch_flags_init);
	imap_fetch_init_nofail_handler(fetch_ctx, imap_fetch_modseq_init);

	imap_fetch_begin(fetch_ctx, ctx->box, search_args);
	mail_search_args_unref(&search_args);

	if (imap_fetch_more(fetch_ctx, ctx->cmd) == 0) {
		/* unfinished */
		ctx->fetch_ctx = fetch_ctx;
		ctx->cmd->state = CLIENT_COMMAND_STATE_WAIT_OUTPUT;

		ctx->cmd->func = cmd_select_continue;
		ctx->cmd->context = ctx;
		return 0;
	}
	ret = imap_fetch_end(fetch_ctx);
	imap_fetch_free(&fetch_ctx);
	return ret < 0 ? -1 : 1;
}

static int select_send_untagged_list(struct client *client, struct mailbox *box,
				     const char *old_vname)
{
	enum mailbox_info_flags mailbox_flags;
	if (mailbox_list_mailbox(mailbox_get_namespace(box)->list,
				 mailbox_get_name(box), &mailbox_flags) < 0)
		return -1;

	string_t *str = t_str_new(128);
	str_append(str, "* LIST (");
	imap_mailbox_flags2str(str, mailbox_flags);
	str_append(str, ") \"");

	char ns_sep = mail_namespace_get_sep(mailbox_get_namespace(box));
	if (ns_sep == '\\')
		str_append_c(str, '\\');
	str_append_c(str, ns_sep);
	str_append(str, "\" ");

	const char *vname = mailbox_get_vname(box);
	imap_append_astring(str, vname, IMAP_QUOTE_FLAG_UTF8);

	if (old_vname != NULL) {
		str_append(str, " (\"OLDNAME\" (");
		imap_append_astring(str, old_vname, IMAP_QUOTE_FLAG_UTF8);
		str_append(str, "))");
	}

	client_send_line(client, str_c(str));
	return 0;
}

static int
select_open(struct imap_select_context *ctx, const char *mailbox, bool readonly)
{
	struct client *client = ctx->cmd->client;
	struct mailbox_status status;
	enum mailbox_flags flags = 0;
	int ret = 0;

	if (readonly)
		flags |= MAILBOX_FLAG_READONLY;
	else
		flags |= MAILBOX_FLAG_DROP_RECENT;
	ctx->box = mailbox_alloc(ctx->ns->list, mailbox, flags);

	bool imap4rev2_enabled = (client_enabled_mailbox_features(client) &
				  MAILBOX_FEATURE_IMAP4REV2) != 0;

	const char *old_vname = NULL;
	if (imap4rev2_enabled &&
	    mailbox_was_vname_changed_by_nfc(ctx->box, &old_vname)) {
		/* suppress untagged LIST from lower layers, as we are
		   expected to respond based on the discrepancy between
		   the one in the command and the actual, not between old
		   vs new storage. Also, IMAP4rev2 requires exact flags,
		   which the notification does not provide */
		mailbox_suppress_nfc_name_change_notification(ctx->box);
	}

	event_add_str(ctx->cmd->global_event, "mailbox",
		      mailbox_get_vname(ctx->box));
	if (mailbox_open(ctx->box) < 0) {
		client_send_box_error(ctx->cmd, ctx->box);
		mailbox_free(&ctx->box);
		return -1;
	}

	ret = mailbox_enable(ctx->box, client_enabled_mailbox_features(client));
	if (ret < 0 ||
	    mailbox_sync(ctx->box, MAILBOX_SYNC_FLAG_FULL_READ) < 0) {
		client_send_box_error(ctx->cmd, ctx->box);
		return -1;
	}
	mailbox_get_open_status(ctx->box, STATUS_MESSAGES | STATUS_RECENT |
				STATUS_FIRST_UNSEEN_SEQ | STATUS_UIDVALIDITY |
				STATUS_UIDNEXT | STATUS_KEYWORDS |
				STATUS_HIGHESTMODSEQ, &status);

	client->mailbox = ctx->box;
	client->mailbox_examined = readonly;
	client->messages_count = status.messages;
	client->recent_count = status.recent;
	client->uidvalidity = status.uidvalidity;
	client->notify_uidnext = status.uidnext;

	client_update_mailbox_flags(client, status.keywords);
	client_send_mailbox_flags(client, TRUE);

	if (imap4rev2_enabled &&
	    (select_send_untagged_list(client, ctx->box, old_vname) < 0)) {
		client_send_list_error(ctx->cmd,
				       mailbox_get_namespace(ctx->box)->list);
		return -1;
	}

	client_send_line(client,
		t_strdup_printf("* %u EXISTS", status.messages));
	if (!imap4rev2_enabled) {
		client_send_line(client,
				 t_strdup_printf("* %u RECENT", status.recent));
	}

	if (!imap4rev2_enabled && status.first_unseen_seq != 0) {
		client_send_line(client,
			t_strdup_printf("* OK [UNSEEN %u] First unseen.",
					status.first_unseen_seq));
	}

	client_send_line(client,
			 t_strdup_printf("* OK [UIDVALIDITY %u] UIDs valid",
					 status.uidvalidity));

	client_send_line(client,
			 t_strdup_printf("* OK [UIDNEXT %u] Predicted next UID",
					 status.uidnext));

	client->nonpermanent_modseqs = status.nonpermanent_modseqs;
	if (status.nonpermanent_modseqs) {
		client_send_line(client,
				 "* OK [NOMODSEQ] No permanent modsequences");
	} else if (!status.no_modseq_tracking) {
		client_send_line(client,
			t_strdup_printf("* OK [HIGHESTMODSEQ %"PRIu64"] Highest",
					status.highest_modseq));
		client->sync_last_full_modseq = status.highest_modseq;
	}

	if (ctx->qresync_uid_validity == status.uidvalidity &&
	    status.uidvalidity != 0 && !client->nonpermanent_modseqs) {
		if ((ret = select_qresync(ctx)) < 0) {
			client_send_box_error(ctx->cmd, ctx->box);
			return -1;
		}
	} else {
		ret = 1;
	}
	return ret;
}

static void close_selected_mailbox(struct client *client)
{
	if (client->mailbox == NULL)
		return;

	imap_client_close_mailbox(client);
	/* CLOSED response is required by QRESYNC */
	client_send_line(client, "* OK [CLOSED] Previous mailbox closed.");
}

bool cmd_select_full(struct client_command_context *cmd, bool readonly)
{
	struct client *client = cmd->client;
	struct imap_select_context *ctx;
	const struct imap_arg *args, *list_args;
	const char *mailbox, *client_error;
	int ret;

	/* <mailbox> [(optional parameters)] */
	if (!client_read_args(cmd, 0, 0, &args))
		return FALSE;

	if (!imap_arg_get_astring(args, &mailbox)) {
		close_selected_mailbox(client);
		client_send_command_error(cmd, "Invalid arguments.");
		return FALSE;
	}

	ctx = p_new(cmd->pool, struct imap_select_context, 1);
	ctx->cmd = cmd;
	ctx->ns = client_find_namespace_full(cmd, &mailbox, &client_error);
	if (ctx->ns == NULL) {
		/* send * OK [CLOSED] before the tagged reply */
		close_selected_mailbox(client);
		client_send_tagline(cmd, client_error);
		return TRUE;
	}

	if (imap_arg_get_list(&args[1], &list_args)) {
		if (!select_parse_options(ctx, list_args, &client_error)) {
			select_context_free(ctx);
			/* send * OK [CLOSED] before the tagged reply */
			close_selected_mailbox(client);
			client_send_command_error(ctx->cmd, client_error);
			return TRUE;
		}
	}

	i_assert(client->mailbox_change_lock == NULL);
	client->mailbox_change_lock = cmd;

	close_selected_mailbox(client);

	if (ctx->condstore) {
		/* Enable while no mailbox is opened to avoid sending
		   HIGHESTMODSEQ for previously opened mailbox */
		(void)client_enable(client, imap_feature_condstore);
	}

	ret = select_open(ctx, mailbox, readonly);
	if (ret == 0)
		return FALSE;
	cmd_select_finish(ctx, ret);
	return TRUE;
}

bool cmd_select(struct client_command_context *cmd)
{
	return cmd_select_full(cmd, FALSE);
}
