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

#include "lib.h"
#include "llist.h"
#include "array.h"
#include "net.h"
#include "istream.h"
#include "mail-user.h"
#include "mail-error.h"
#include "mail-storage.h"
#include "imap-url.h"
#include "imap-msgpart-url.h"
#include "imap-urlauth-private.h"
#include "imap-urlauth-fetch.h"
#include "imap-urlauth-connection.h"

struct imap_urlauth_fetch_url {
	struct imap_urlauth_fetch_url *prev, *next;

	char *url;
	enum imap_urlauth_fetch_flags flags;
};

struct imap_urlauth_fetch {
	unsigned int refcount;
	struct imap_urlauth_context *uctx;

	imap_urlauth_fetch_callback_t *callback;
	void *context;

	/* local urls */
	struct imap_urlauth_fetch_url *local_urls_head, *local_urls_tail;
	struct imap_msgpart_url *local_url;

	unsigned int pending_requests;

	struct {
		char *url;
		enum imap_urlauth_fetch_flags flags;

		struct istream *input;
		uoff_t size;

		char *bodypartstruct;
		char *error;

		bool succeeded:1;
		bool binary_has_nuls:1;
	} pending_reply;

	bool failed:1;
	bool waiting_local:1;
	bool waiting_service:1;
};

static void imap_urlauth_fetch_abort_local(struct imap_urlauth_fetch *ufetch)
{
	struct imap_urlauth_fetch_url *url, *url_next;

	if (ufetch->local_url != NULL) {
		ufetch->pending_requests--;
		imap_msgpart_url_free(&ufetch->local_url);
	}

	i_free_and_null(ufetch->pending_reply.url);
	i_free_and_null(ufetch->pending_reply.bodypartstruct);
	i_free_and_null(ufetch->pending_reply.error);
	i_stream_unref(&ufetch->pending_reply.input);

	url = ufetch->local_urls_head;
	for (; url != NULL; url = url_next) {
		url_next = url->next;
		i_free(url->url);
		i_free(url);
		ufetch->pending_requests--;
	}
	ufetch->local_urls_head = ufetch->local_urls_tail = NULL;
}

static void imap_urlauth_fetch_abort(struct imap_urlauth_fetch *ufetch)
{
	if (ufetch->pending_requests > 0)
		imap_urlauth_request_abort_by_context(ufetch->uctx->conn, ufetch);

	imap_urlauth_fetch_abort_local(ufetch);

	i_assert(ufetch->pending_requests == 0);
}

static void imap_urlauth_fetch_fail(struct imap_urlauth_fetch *ufetch)
{
	imap_urlauth_fetch_abort(ufetch);
	ufetch->failed = TRUE;
}

struct imap_urlauth_fetch *
imap_urlauth_fetch_init(struct imap_urlauth_context *uctx,
			imap_urlauth_fetch_callback_t *callback, void *context)
{
	struct imap_urlauth_fetch *ufetch;

	ufetch = i_new(struct imap_urlauth_fetch, 1);
	ufetch->refcount = 1;
	ufetch->uctx = uctx;
	ufetch->callback = callback;
	ufetch->context = context;
	return ufetch;
}

static void imap_urlauth_fetch_ref(struct imap_urlauth_fetch *ufetch)
{
	i_assert(ufetch->refcount > 0);
	ufetch->refcount++;
}

static void imap_urlauth_fetch_unref(struct imap_urlauth_fetch **_ufetch)
{
	struct imap_urlauth_fetch *ufetch = *_ufetch;

	i_assert(ufetch->refcount > 0);

	*_ufetch = NULL;
	if (--ufetch->refcount > 0)
		return;

	ufetch->refcount++;
	imap_urlauth_fetch_abort(ufetch);
	ufetch->refcount--;
	i_assert(ufetch->refcount == 0);

	/* dont leave the connection in limbo; make sure continue is called */
	if (ufetch->waiting_service)
		imap_urlauth_connection_continue(ufetch->uctx->conn);
	i_free(ufetch);
}

void imap_urlauth_fetch_deinit(struct imap_urlauth_fetch **_ufetch)
{
	imap_urlauth_fetch_unref(_ufetch);
}

static void
imap_urlauth_fetch_error(struct imap_urlauth_fetch *ufetch, const char *url,
			 enum imap_urlauth_fetch_flags url_flags,
			 const char *error)
{
	struct imap_urlauth_fetch_reply reply;
	int ret;

	ufetch->pending_requests--;

	i_zero(&reply);
	reply.url = url;
	reply.flags = url_flags;
	reply.succeeded = FALSE;
	reply.error = error;

	T_BEGIN {
		ret = ufetch->callback(&reply, ufetch->pending_requests == 0,
				       ufetch->context);
	} T_END;

	if (ret == 0) {
		ufetch->waiting_local = TRUE;
		ufetch->pending_requests++;
	} else if (ret < 0) {
		imap_urlauth_fetch_fail(ufetch);
	}
}

static void
imap_urlauth_fetch_local(struct imap_urlauth_fetch *ufetch, const char *url,
			 enum imap_urlauth_fetch_flags url_flags,
			 struct imap_url *imap_url)
{
	struct imap_urlauth_fetch_reply reply;
	struct imap_msgpart_open_result mpresult;
	const char *error, *errormsg = NULL, *bpstruct = NULL;
	enum mail_error error_code;
	struct imap_msgpart_url *mpurl = NULL;
	bool utf8 = HAS_ALL_BITS(url_flags, IMAP_URLAUTH_FETCH_FLAG_UTF8);
	enum imap_quote_flags qflags = (utf8 ? IMAP_QUOTE_FLAG_UTF8 : 0);
	int ret;

	bool success = TRUE;
	struct event *event = ufetch->uctx->user->event;

	e_debug(event, "Fetching local URLAUTH %s", url);

	if (url_flags == 0)
		url_flags = IMAP_URLAUTH_FETCH_FLAG_BODY;

	/* fetch URL */
	if (imap_url == NULL) {
		ret = imap_urlauth_fetch(ufetch->uctx, url,
					 &mpurl, &error_code, &error);
	} else {
		ret = imap_urlauth_fetch_parsed(ufetch->uctx, imap_url,
						&mpurl, &error_code, &error);
	}
	if (ret <= 0) {
		errormsg = t_strdup_printf("Failed to fetch URLAUTH \"%s\": %s",
					   url, error);
		e_debug(event, "%s", errormsg);
		success = FALSE;
	}

	/* fetch metadata */
	if (success && (url_flags & IMAP_URLAUTH_FETCH_FLAG_BINARY) != 0)
		imap_msgpart_url_set_decode_to_binary(mpurl);
	if (success &&
	    (url_flags & IMAP_URLAUTH_FETCH_FLAG_BODYPARTSTRUCTURE) != 0) {
		ret = imap_msgpart_url_get_bodypartstructure(mpurl, qflags,
							     &bpstruct, &error);
		if (ret <= 0) {
			errormsg = t_strdup_printf(
				"Failed to read URLAUTH \"%s\": %s",
				url, error);
			e_debug(event, "%s", errormsg);
			success = FALSE;
		}
	}

	/* if requested, read the message part the URL points to */
	i_zero(&mpresult);
	if (success && ((url_flags & IMAP_URLAUTH_FETCH_FLAG_BODY) != 0 ||
			(url_flags & IMAP_URLAUTH_FETCH_FLAG_BINARY) != 0)) {
		ret = imap_msgpart_url_read_part(mpurl, &mpresult, &error);
		if (ret <= 0) {
			errormsg = t_strdup_printf(
				"Failed to read URLAUTH \"%s\": %s",
				url, error);
			e_debug(event, "%s", errormsg);
			success = FALSE;
		}
	}

	if (success) {
		if (bpstruct != NULL) {
			e_debug(event,
				"Fetched URLAUTH yielded BODYPARTSTRUCTURE (%s)",
				bpstruct);
		}
		if (mpresult.size == 0 || mpresult.input == NULL)
			e_debug(event, "Fetched URLAUTH yielded empty result");
		else {
			e_debug(event,
				"Fetched URLAUTH yielded %"PRIuUOFF_T" bytes "
				"of %smessage data", mpresult.size,
				(mpresult.binary_decoded_input_has_nuls ?
				 "binary " : ""));
		}
	}

	ufetch->pending_requests--;

	i_zero(&reply);
	reply.url = url;
	reply.flags = url_flags;
	reply.error = errormsg;
	reply.succeeded = success;

	reply.bodypartstruct = bpstruct;
	reply.binary_has_nuls = mpresult.binary_decoded_input_has_nuls;
	reply.size = mpresult.size;
	reply.input = mpresult.input;

	ret = ufetch->callback(&reply, ufetch->pending_requests == 0,
			       ufetch->context);
	if (ret == 0) {
		ufetch->local_url = mpurl;
		ufetch->waiting_local = TRUE;
		ufetch->pending_requests++;
	} else {

		if (mpurl != NULL)
			imap_msgpart_url_free(&mpurl);
		if (ret < 0)
			imap_urlauth_fetch_fail(ufetch);
	}
}

static int
imap_urlauth_fetch_request_callback(struct imap_urlauth_fetch_reply *reply,
				    void *context)
{
	struct imap_urlauth_fetch *ufetch = context;
	struct imap_urlauth_fetch_reply error_reply;
	int ret = 1;

	if (ufetch->waiting_local && reply != NULL) {
		i_assert(ufetch->pending_reply.url == NULL);
		ufetch->pending_reply.url = i_strdup(reply->url);
		ufetch->pending_reply.flags = reply->flags;
		ufetch->pending_reply.bodypartstruct =
			i_strdup(reply->bodypartstruct);
		ufetch->pending_reply.error = i_strdup(reply->error);
		if (reply->input != NULL) {
			ufetch->pending_reply.input = reply->input;
			i_stream_ref(ufetch->pending_reply.input);
		}
		ufetch->pending_reply.size = reply->size;
		ufetch->pending_reply.succeeded = reply->succeeded;
		ufetch->pending_reply.binary_has_nuls = reply->binary_has_nuls;
		ufetch->waiting_service = TRUE;
		return 0;
	}

	ufetch->waiting_local = FALSE;
	ufetch->pending_requests--;

	imap_urlauth_fetch_ref(ufetch);

	if (!ufetch->failed) {
		bool last = ufetch->pending_requests == 0 || reply == NULL;

		if (reply == NULL) {
			i_zero(&error_reply);
			reply = &error_reply;
		}
		ret = ufetch->callback(reply, last, ufetch->context);
	}

	/* report failure only once */
	if (ret < 0 || reply == NULL) {
		if (!ufetch->failed)
			imap_urlauth_fetch_abort_local(ufetch);
		ufetch->failed = TRUE;
	} else if (ret == 0) {
		ufetch->waiting_service = TRUE;
		ufetch->pending_requests++;
	}

	imap_urlauth_fetch_unref(&ufetch);
	return ret;
}

int imap_urlauth_fetch_url(struct imap_urlauth_fetch *ufetch, const char *url,
			   enum imap_urlauth_fetch_flags url_flags)
{
	struct imap_urlauth_context *uctx = ufetch->uctx;
	enum imap_url_parse_flags url_parse_flags =
		IMAP_URL_PARSE_ALLOW_URLAUTH;
	struct mail_user *mail_user = uctx->user;
	struct imap_url *imap_url;
	const char *error, *errormsg;

	/* parse the url */
	if (imap_url_parse(url, NULL, url_parse_flags, &imap_url, &error) < 0) {
		errormsg = t_strdup_printf(
			"Failed to fetch URLAUTH \"%s\": %s", url, error);
		e_debug(mail_user->event, "%s", errormsg);
		ufetch->pending_requests++;
		imap_urlauth_fetch_ref(ufetch);
		imap_urlauth_fetch_error(ufetch, url, url_flags, errormsg);
		imap_urlauth_fetch_unref(&ufetch);
		return 1;
	}

	return imap_urlauth_fetch_url_parsed(ufetch, url, imap_url, url_flags);
}

int imap_urlauth_fetch_url_parsed(struct imap_urlauth_fetch *ufetch,
				  const char *url, struct imap_url *imap_url,
				  enum imap_urlauth_fetch_flags url_flags)
{
	struct imap_urlauth_context *uctx = ufetch->uctx;
	struct mail_user *mail_user = uctx->user;
	const char *error, *errormsg;
	int ret = 0;

	ufetch->failed = FALSE;
	ufetch->pending_requests++;

	imap_urlauth_fetch_ref(ufetch);

	/* if access user and target user match, handle fetch request locally */
	if (imap_url->userid != NULL &&
		strcmp(mail_user->username, imap_url->userid) == 0) {

		if (ufetch->waiting_local) {
			struct imap_urlauth_fetch_url *url_local;

			url_local = i_new(struct imap_urlauth_fetch_url, 1);
			url_local->url = i_strdup(url);
			url_local->flags = url_flags;

			DLLIST2_APPEND(&ufetch->local_urls_head,
				       &ufetch->local_urls_tail, url_local);
		} else T_BEGIN {
			imap_urlauth_fetch_local(ufetch, url,
						 url_flags, imap_url);
		} T_END;
		imap_url = NULL;
	/* don't try to fetch remote URLs that are already known to fail access */
	} else if (!imap_urlauth_check(uctx, imap_url, TRUE, &error)) {
		errormsg = t_strdup_printf(
			"Failed to fetch URLAUTH \"%s\": %s", url, error);
		e_debug(mail_user->event, "%s", errormsg);
		imap_urlauth_fetch_error(ufetch, url, url_flags, errormsg);
		imap_url = NULL;
	}

	/* create request for url */
	if (imap_url != NULL && imap_url->userid != NULL) {
		i_assert(uctx->conn != NULL);
		(void)imap_urlauth_request_new(
			uctx->conn, imap_url->userid, url, url_flags,
			imap_urlauth_fetch_request_callback, ufetch);
		i_assert(uctx->conn != NULL);
		if (imap_urlauth_connection_connect(uctx->conn) < 0)
			ret = -1;
	}
	if (ret >= 0)
		ret = (ufetch->pending_requests > 0 ? 0 : 1);

	imap_urlauth_fetch_unref(&ufetch);
	return ret;
}

static bool imap_urlauth_fetch_do_continue(struct imap_urlauth_fetch *ufetch)
{
	struct imap_urlauth_fetch_url *url, *url_next;
	int ret;

	if (ufetch->failed)
		return FALSE;

	if (!ufetch->waiting_local && !ufetch->waiting_service) {
		/* not currently waiting for anything */
		return ufetch->pending_requests > 0;
	}

	/* we finished a request */
	ufetch->pending_requests--;

	if (!ufetch->waiting_local) {
		/* not waiting for local request handling */
		ufetch->waiting_service = FALSE;
		imap_urlauth_connection_continue(ufetch->uctx->conn);
		return ufetch->pending_requests > 0;
	}

	/* finished local request */
	if (ufetch->local_url != NULL) {
		imap_msgpart_url_free(&ufetch->local_url);
	}
	ufetch->waiting_local = FALSE;

	/* handle pending remote reply */
	if (ufetch->pending_reply.url != NULL) {
		struct imap_urlauth_fetch_reply reply;

		ufetch->pending_requests--;

		i_zero(&reply);
		reply.url = ufetch->pending_reply.url;
		reply.flags = ufetch->pending_reply.flags;
		reply.bodypartstruct = ufetch->pending_reply.bodypartstruct;
		reply.error = ufetch->pending_reply.error;
		reply.input = ufetch->pending_reply.input;
		reply.size = ufetch->pending_reply.size;
		reply.succeeded = ufetch->pending_reply.succeeded;
		reply.binary_has_nuls = ufetch->pending_reply.binary_has_nuls;

		ret = ufetch->callback(&reply, ufetch->pending_requests == 0,
				       ufetch->context);

		if (ufetch->pending_reply.url != NULL)
			i_free(ufetch->pending_reply.url);
		if (ufetch->pending_reply.input != NULL)
			i_stream_unref(&ufetch->pending_reply.input);
		if (ufetch->pending_reply.bodypartstruct != NULL)
			i_free(ufetch->pending_reply.bodypartstruct);
		if (ufetch->pending_reply.error != NULL)
			i_free(ufetch->pending_reply.error);

		if (ret < 0) {
			imap_urlauth_fetch_fail(ufetch);
			return FALSE;
		}

		if (ret == 0) {
			ufetch->waiting_service = TRUE;
			ufetch->pending_requests++;
			return TRUE;
		}

		ufetch->waiting_service = FALSE;
		imap_urlauth_connection_continue(ufetch->uctx->conn);
	}

	/* handle pending local urls */
	url = ufetch->local_urls_head;
	while (url != NULL) {
		url_next = url->next;
		T_BEGIN {
			imap_urlauth_fetch_local(ufetch, url->url,
						 url->flags, NULL);
		} T_END;
		DLLIST2_REMOVE(&ufetch->local_urls_head,
			       &ufetch->local_urls_tail, url);
		i_free(url->url);
		i_free(url);
		if (ufetch->waiting_local)
			return TRUE;
		url = url_next;
	}

	return ufetch->pending_requests > 0;
}

bool imap_urlauth_fetch_continue(struct imap_urlauth_fetch *ufetch)
{
	bool pending;

	imap_urlauth_fetch_ref(ufetch);
	pending = imap_urlauth_fetch_do_continue(ufetch);
	imap_urlauth_fetch_unref(&ufetch);

	return pending;
}

bool imap_urlauth_fetch_is_pending(struct imap_urlauth_fetch *ufetch)
{
	return ufetch->pending_requests > 0;
}
