/* Copyright (c) 2007-2016 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "dbox-attachment.h"
#include "sdbox-storage.h"
#include "sdbox-file.h"
#include "sdbox-sync.h"

#define SDBOX_REBUILD_COUNT 3

static void
dbox_sync_file_move_if_needed(struct dbox_file *file,
			      enum sdbox_sync_entry_type type)
{
	struct stat st;
	bool move_to_alt = type == SDBOX_SYNC_ENTRY_TYPE_MOVE_TO_ALT;
	bool deleted;

	if (move_to_alt == dbox_file_is_in_alt(file) &&
	    !move_to_alt) {
		/* unopened dbox files default to primary dir.
		   stat the file to update its location. */
		(void)dbox_file_stat(file, &st);

	}
	if (move_to_alt != dbox_file_is_in_alt(file)) {
		/* move the file. if it fails, nothing broke so
		   don't worry about it. */
		if (dbox_file_open(file, &deleted) > 0 && !deleted)
			(void)sdbox_file_move(file, move_to_alt);
	}
}

static void sdbox_sync_file(struct sdbox_sync_context *ctx,
			    uint32_t seq, uint32_t uid,
			    enum sdbox_sync_entry_type type)
{
	struct dbox_file *file;
	enum modify_type modify_type;

	switch (type) {
	case SDBOX_SYNC_ENTRY_TYPE_EXPUNGE:
		if (!mail_index_transaction_is_expunged(ctx->trans, seq)) {
			mail_index_expunge(ctx->trans, seq);
			array_append(&ctx->expunged_uids, &uid, 1);
		}
		break;
	case SDBOX_SYNC_ENTRY_TYPE_MOVE_FROM_ALT:
	case SDBOX_SYNC_ENTRY_TYPE_MOVE_TO_ALT:
		/* update flags in the sync transaction, mainly to make
		   sure that these alt changes get marked as synced
		   and won't be retried */
		modify_type = type == SDBOX_SYNC_ENTRY_TYPE_MOVE_TO_ALT ?
			MODIFY_ADD : MODIFY_REMOVE;
		mail_index_update_flags(ctx->trans, seq, modify_type,
					(enum mail_flags)DBOX_INDEX_FLAG_ALT);
		file = sdbox_file_init(ctx->mbox, uid);
		dbox_sync_file_move_if_needed(file, type);
		dbox_file_unref(&file);
		break;
	}
}

static void sdbox_sync_add(struct sdbox_sync_context *ctx,
			   const struct mail_index_sync_rec *sync_rec)
{
	uint32_t uid;
	enum sdbox_sync_entry_type type;
	uint32_t seq, seq1, seq2;

	if (sync_rec->type == MAIL_INDEX_SYNC_TYPE_EXPUNGE) {
		/* we're interested */
		type = SDBOX_SYNC_ENTRY_TYPE_EXPUNGE;
	} else if (sync_rec->type == MAIL_INDEX_SYNC_TYPE_FLAGS) {
		/* we care only about alt flag changes */
		if ((sync_rec->add_flags & DBOX_INDEX_FLAG_ALT) != 0)
			type = SDBOX_SYNC_ENTRY_TYPE_MOVE_TO_ALT;
		else if ((sync_rec->remove_flags & DBOX_INDEX_FLAG_ALT) != 0)
			type = SDBOX_SYNC_ENTRY_TYPE_MOVE_FROM_ALT;
		else
			return;
	} else {
		/* not interested */
		return;
	}

	if (!mail_index_lookup_seq_range(ctx->sync_view,
					 sync_rec->uid1, sync_rec->uid2,
					 &seq1, &seq2)) {
		/* already expunged everything. nothing to do. */
		return;
	}

	for (seq = seq1; seq <= seq2; seq++) {
		mail_index_lookup_uid(ctx->sync_view, seq, &uid);
		sdbox_sync_file(ctx, seq, uid, type);
	}
}

static int sdbox_sync_index(struct sdbox_sync_context *ctx)
{
	struct mailbox *box = &ctx->mbox->box;
	const struct mail_index_header *hdr;
	struct mail_index_sync_rec sync_rec;
	uint32_t seq1, seq2;

	hdr = mail_index_get_header(ctx->sync_view);
	if (hdr->uid_validity == 0) {
		/* newly created index file */
		if (hdr->next_uid == 1) {
			/* could be just a race condition where we opened the
			   mailbox between mkdir and index creation. fix this
			   silently. */
			if (sdbox_mailbox_create_indexes(box, NULL, ctx->trans) < 0)
				return -1;
			return 1;
		}
		mail_storage_set_critical(box->storage,
			"sdbox %s: Broken index: missing UIDVALIDITY",
			mailbox_get_path(box));
		sdbox_set_mailbox_corrupted(box);
		return 0;
	}

	/* mark the newly seen messages as recent */
	if (mail_index_lookup_seq_range(ctx->sync_view, hdr->first_recent_uid,
					hdr->next_uid, &seq1, &seq2))
		mailbox_recent_flags_set_seqs(box, ctx->sync_view, seq1, seq2);

	while (mail_index_sync_next(ctx->index_sync_ctx, &sync_rec))
		sdbox_sync_add(ctx, &sync_rec);
	return 1;
}

static void dbox_sync_file_expunge(struct sdbox_sync_context *ctx,
				   uint32_t uid)
{
	struct mailbox *box = &ctx->mbox->box;
	struct dbox_file *file;
	struct sdbox_file *sfile;
	int ret;

	file = sdbox_file_init(ctx->mbox, uid);
	sfile = (struct sdbox_file *)file;
	if (file->storage->attachment_dir != NULL)
		ret = sdbox_file_unlink_with_attachments(sfile);
	else
		ret = dbox_file_unlink(file);

	/* do sync_notify only when the file was unlinked by us */
	if (ret > 0 && box->v.sync_notify != NULL)
		box->v.sync_notify(box, uid, MAILBOX_SYNC_TYPE_EXPUNGE);
	dbox_file_unref(&file);
}

static void dbox_sync_expunge_files(struct sdbox_sync_context *ctx)
{
	const uint32_t *uidp;

	/* NOTE: Index is no longer locked. Multiple processes may be unlinking
	   the files at the same time. */
	ctx->mbox->box.tmp_sync_view = ctx->sync_view;
	array_foreach(&ctx->expunged_uids, uidp) T_BEGIN {
		dbox_sync_file_expunge(ctx, *uidp);
	} T_END;
	if (ctx->mbox->box.v.sync_notify != NULL)
		ctx->mbox->box.v.sync_notify(&ctx->mbox->box, 0, 0);
	ctx->mbox->box.tmp_sync_view = NULL;
}

static int
sdbox_refresh_header(struct sdbox_mailbox *mbox, bool retry, bool log_error)
{
	struct mail_index_view *view;
	struct sdbox_index_header hdr;
	bool need_resize;
	int ret;

	view = mail_index_view_open(mbox->box.index);
	ret = sdbox_read_header(mbox, &hdr, log_error, &need_resize);
	mail_index_view_close(&view);

	if (ret < 0 && retry) {
		mail_index_refresh(mbox->box.index);
		return sdbox_refresh_header(mbox, FALSE, log_error);
	}
	return ret;
}

int sdbox_sync_begin(struct sdbox_mailbox *mbox, enum sdbox_sync_flags flags,
		     struct sdbox_sync_context **ctx_r)
{
	struct mail_storage *storage = mbox->box.storage;
	struct sdbox_sync_context *ctx;
	enum mail_index_sync_flags sync_flags;
	unsigned int i;
	int ret;
	bool rebuild, force_rebuild;

	force_rebuild = (flags & SDBOX_SYNC_FLAG_FORCE_REBUILD) != 0;
	rebuild = force_rebuild ||
		mbox->corrupted_rebuild_count != 0 ||
		sdbox_refresh_header(mbox, TRUE, FALSE) < 0;

	ctx = i_new(struct sdbox_sync_context, 1);
	ctx->mbox = mbox;
	ctx->flags = flags;
	i_array_init(&ctx->expunged_uids, 32);

	sync_flags = index_storage_get_sync_flags(&mbox->box);
	if (!rebuild && (flags & SDBOX_SYNC_FLAG_FORCE) == 0)
		sync_flags |= MAIL_INDEX_SYNC_FLAG_REQUIRE_CHANGES;
	if ((flags & SDBOX_SYNC_FLAG_FSYNC) != 0)
		sync_flags |= MAIL_INDEX_SYNC_FLAG_FSYNC;
	/* don't write unnecessary dirty flag updates */
	sync_flags |= MAIL_INDEX_SYNC_FLAG_AVOID_FLAG_UPDATES;

	for (i = 0;; i++) {
		ret = index_storage_expunged_sync_begin(&mbox->box,
				&ctx->index_sync_ctx, &ctx->sync_view,
				&ctx->trans, sync_flags);
		if (mail_index_reset_fscked(mbox->box.index))
			sdbox_set_mailbox_corrupted(&mbox->box);
		if (ret <= 0) {
			array_free(&ctx->expunged_uids);
			i_free(ctx);
			*ctx_r = NULL;
			return ret;
		}

		if (rebuild)
			ret = 0;
		else {
			if ((ret = sdbox_sync_index(ctx)) > 0)
				break;
		}

		/* failure. keep the index locked while we're doing a
		   rebuild. */
		if (ret == 0) {
			if (i >= SDBOX_REBUILD_COUNT) {
				mail_storage_set_critical(storage,
					"sdbox %s: Index keeps breaking",
					mailbox_get_path(&ctx->mbox->box));
				ret = -1;
			} else {
				/* do a full resync and try again. */
				rebuild = FALSE;
				ret = sdbox_sync_index_rebuild(mbox,
							       force_rebuild);
			}
		}
		mail_index_sync_rollback(&ctx->index_sync_ctx);
		if (ret < 0) {
			index_storage_expunging_deinit(&ctx->mbox->box);
			array_free(&ctx->expunged_uids);
			i_free(ctx);
			return -1;
		}
	}

	*ctx_r = ctx;
	return 0;
}

int sdbox_sync_finish(struct sdbox_sync_context **_ctx, bool success)
{
	struct sdbox_sync_context *ctx = *_ctx;
	int ret = success ? 0 : -1;

	*_ctx = NULL;

	if (success) {
		mail_index_view_ref(ctx->sync_view);

		if (mail_index_sync_commit(&ctx->index_sync_ctx) < 0) {
			mailbox_set_index_error(&ctx->mbox->box);
			ret = -1;
		} else {
			dbox_sync_expunge_files(ctx);
			mail_index_view_close(&ctx->sync_view);
		}
	} else {
		mail_index_sync_rollback(&ctx->index_sync_ctx);
	}

	index_storage_expunging_deinit(&ctx->mbox->box);
	array_free(&ctx->expunged_uids);
	i_free(ctx);
	return ret;
}

int sdbox_sync(struct sdbox_mailbox *mbox, enum sdbox_sync_flags flags)
{
	struct sdbox_sync_context *sync_ctx;

	if (sdbox_sync_begin(mbox, flags, &sync_ctx) < 0)
		return -1;

	if (sync_ctx == NULL)
		return 0;
	return sdbox_sync_finish(&sync_ctx, TRUE);
}

struct mailbox_sync_context *
sdbox_storage_sync_init(struct mailbox *box, enum mailbox_sync_flags flags)
{
	struct sdbox_mailbox *mbox = (struct sdbox_mailbox *)box;
	enum sdbox_sync_flags sdbox_sync_flags = 0;
	int ret = 0;

	if (!box->opened) {
		if (mailbox_open(box) < 0)
			ret = -1;
	}

	if (mail_index_reset_fscked(box->index))
		sdbox_set_mailbox_corrupted(box);
	if (ret == 0 && (index_mailbox_want_full_sync(&mbox->box, flags) ||
			 mbox->corrupted_rebuild_count != 0)) {
		if ((flags & MAILBOX_SYNC_FLAG_FORCE_RESYNC) != 0)
			sdbox_sync_flags |= SDBOX_SYNC_FLAG_FORCE_REBUILD;
		ret = sdbox_sync(mbox, sdbox_sync_flags);
	}

	return index_mailbox_sync_init(box, flags, ret < 0);
}
