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

/*
   IMAP clients can work in many different ways. There are basically 2
   types:

   1. Online clients that ask for the same information multiple times (e.g.
      webmails, Pine)

   2. Offline clients that usually download first some of the interesting
      message headers and only after that the message bodies (possibly
      automatically, or possibly only when the user opens the mail). Most
      non-webmail IMAP clients behave like this.

   Cache file is extremely helpful with the type 1 clients. The first time
   that client requests message headers or some other metadata they're
   stored into the cache file. The second time they ask for the same
   information Dovecot can now get it quickly from the cache file instead
   of opening the message and parsing the headers.

   For type 2 clients the cache file is also somewhat helpful if client
   fetches any initial metadata. Some of the information is helpful in any
   case, for example it's required to know the message's virtual size when
   downloading the message with IMAP. Without the virtual size being in cache
   Dovecot first has to read the whole message first to calculate it, which
   increases CPU usage.

   Only the specified fields that client(s) have asked for earlier are
   stored into cache file. This allows Dovecot to be adaptive to different
   clients' needs and still not waste disk space (and cause extra disk
   I/O!) for fields that client never needs.

   Dovecot can cache fields either permanently or temporarily. Temporarily
   cached fields are dropped from the cache file after about a week.
   Dovecot uses two rules to determine when data should be cached
   permanently instead of temporarily:

   1. Client accessed messages in non-sequential order within this session.
      This most likely means it doesn't have a local cache.

   2. Client accessed a message older than one week.

   These rules might not always work optimally, so Dovecot also re-evaluates
   the caching decisions once in a while:

   - When caching decision is YES (permanently cache the field), the field's
     last_used is updated only when the caching decision has been verified to
     be correct.

   - When caching decision is TEMP, the last_used is updated whenever the field
     is accessed.

   - When last_used becomes 30 days old (or unaccessed_field_drop_secs) a
     YES caching decision is changed to TEMP.

   - When last_used becomes 60 days old (or 2*unaccessed_field_drop_secs) a
     TEMP caching decision is changed to NO.
*/

#include "lib.h"
#include "ioloop.h"
#include "mail-cache-private.h"

const char *mail_cache_decision_to_string(enum mail_cache_decision_type dec)
{
	switch (dec & ENUM_NEGATE(MAIL_CACHE_DECISION_FORCED)) {
	case MAIL_CACHE_DECISION_NO:
		return "no";
	case MAIL_CACHE_DECISION_TEMP:
		return "temp";
	case MAIL_CACHE_DECISION_YES:
		return "yes";
	}
	i_unreached();
}

struct event_passthrough *
mail_cache_decision_changed_event(struct mail_cache *cache, struct event *event,
				  unsigned int field)
{
	return event_create_passthrough(event)->
		set_name("mail_cache_decision_changed")->
		add_str("field", cache->fields[field].field.name)->
		add_int("last_used", cache->fields[field].field.last_used);
}

static void
mail_cache_update_last_used(struct mail_cache *cache, unsigned int field)
{
	cache->fields[field].field.last_used = ioloop_time32;
	if (cache->field_file_map[field] != (uint32_t)-1)
		cache->field_header_write_pending = TRUE;
}

void mail_cache_decision_state_update(struct mail_cache_view *view,
				      uint32_t seq, unsigned int field)
{
	struct mail_cache *cache = view->cache;
	enum mail_cache_decision_type dec;
	const struct mail_index_header *hdr;
	uint32_t uid;

	i_assert(field < cache->fields_count);

	if (view->no_decision_updates)
		return;

	dec = cache->fields[field].field.decision;
	if (dec == (MAIL_CACHE_DECISION_NO | MAIL_CACHE_DECISION_FORCED)) {
		/* don't update last_used */
		return;
	}

	/* update last_used about once a day */
	bool last_used_need_update =
		ioloop_time - cache->fields[field].field.last_used > 3600*24;

	if (dec == MAIL_CACHE_DECISION_NO ||
	    (dec & MAIL_CACHE_DECISION_FORCED) != 0) {
		/* a) forced decision
		   b) not cached, mail_cache_decision_add() will handle this */
		if (last_used_need_update)
			mail_cache_update_last_used(cache, field);
		return;
	}
	if (dec == MAIL_CACHE_DECISION_YES) {
		if (!last_used_need_update)
			return;
		/* update last_used only when we can confirm that the YES
		   decision is still correct. */
	} else {
		/* see if we want to change decision from TEMP to YES */
		i_assert(dec == MAIL_CACHE_DECISION_TEMP);
		if (last_used_need_update)
			mail_cache_update_last_used(cache, field);
	}

	mail_index_lookup_uid(view->view, seq, &uid);
	hdr = mail_index_get_header(view->view);

	if (uid >= cache->fields[field].uid_highwater &&
	    uid >= hdr->day_first_uid[7]) {
		cache->fields[field].uid_highwater = uid;
	} else if (dec == MAIL_CACHE_DECISION_YES) {
		/* Confirmed that we still want to preserve YES as cache
		   decision. We can update last_used now. */
		i_assert(last_used_need_update);
		mail_cache_update_last_used(cache, field);
	} else {
		/* a) nonordered access within this session. if client doesn't
		      request messages in growing order, we assume it doesn't
		      have a permanent local cache.
		   b) accessing message older than one week. assume it's a
		      client with no local cache. if it was just a new client
		      generating the local cache for the first time, we'll
		      drop back to TEMP within few months. */
		i_assert(dec == MAIL_CACHE_DECISION_TEMP);
		cache->fields[field].field.decision = MAIL_CACHE_DECISION_YES;
		cache->fields[field].decision_dirty = TRUE;
		cache->field_header_write_pending = TRUE;

		const char *reason = uid < hdr->day_first_uid[7] ?
			"old_mail" : "unordered_access";
		struct event_passthrough *e =
			mail_cache_decision_changed_event(
				view->cache, view->cache->event, field)->
			add_str("reason", reason)->
			add_int("uid", uid)->
			add_str("old_decision", "temp")->
			add_str("new_decision", "yes");
		e_debug(e->event(), "Changing field %s decision temp -> yes (uid=%u)",
			cache->fields[field].field.name, uid);
	}
}

static unsigned int mail_cache_count_alive_headers(struct mail_cache *cache)
{
	unsigned int count = 0;
	for (unsigned int index = 0; index < cache->fields_count; ++index) {
		if (cache->fields[index].field.type == MAIL_CACHE_FIELD_HEADER &&
		    (cache->fields[index].field.decision &
		     ENUM_NEGATE(MAIL_CACHE_DECISION_FORCED)) !=
						MAIL_CACHE_DECISION_NO)
			++count;
	}
	return count;
}

bool mail_cache_headers_check_capped(struct mail_cache *cache)
{
	struct mail_index_cache_optimization_settings *set =
		&cache->index->optimization_set.cache;

	if (set->max_headers_count == 0) return FALSE;
	if (cache->headers_capped) return TRUE;

	unsigned int count = mail_cache_count_alive_headers(cache);
	cache->headers_capped = count >= set->max_headers_count;
	return cache->headers_capped;
}

static struct event_passthrough *
mail_cache_decision_rejected_event(struct mail_cache *cache, unsigned int field,
				   const char *reason)
{
	return event_create_passthrough(cache->event)->
		set_name("mail_cache_decision_rejected")->
		add_str("field", cache->fields[field].field.name)->
		add_str("reason", reason);
}

void mail_cache_decision_add(struct mail_cache_view *view, uint32_t seq,
			     unsigned int field, bool *rejected_r)
{
	struct mail_cache *cache = view->cache;
	struct mail_cache_field_private *priv;
	uint32_t uid;

	i_assert(field < cache->fields_count);

	*rejected_r = FALSE;

	if (view->no_decision_updates)
		return;

	priv = &cache->fields[field];
	if (priv->field.decision != MAIL_CACHE_DECISION_NO &&
	    priv->field.last_used != 0) {
		/* a) forced decision
		   b) we're already caching it, so it just wasn't in cache */
		return;
	}

	/* field used the first time */
	if (priv->field.decision == MAIL_CACHE_DECISION_NO) {
		if (mail_cache_headers_check_capped(view->cache)) {
			*rejected_r = TRUE;

			const char *reason = "too_many_headers";
			struct event_passthrough *e =
				mail_cache_decision_rejected_event(
					cache, field, reason);
			e_debug(e->event(),
				"Cache rejected header '%s': %s",
				priv->field.name, reason);
			return;
		}

		priv->field.decision = MAIL_CACHE_DECISION_TEMP;
	}
	priv->field.last_used = ioloop_time;
	priv->decision_dirty = TRUE;
	cache->field_header_write_pending = TRUE;

	mail_index_lookup_uid(view->view, seq, &uid);
	priv->uid_highwater = uid;

	const char *new_decision =
		mail_cache_decision_to_string(priv->field.decision);
	struct event_passthrough *e =
		mail_cache_decision_changed_event(cache, cache->event, field)->
		add_str("reason", "add")->
		add_int("uid", uid)->
		add_str("old_decision", "no")->
		add_str("new_decision", new_decision);
	e_debug(e->event(), "Adding field %s to cache for the first time (uid=%u)",
		priv->field.name, uid);
}

int mail_cache_decisions_copy(struct mail_cache *src, struct mail_cache *dst)
{
	if (mail_cache_open_and_verify(src) < 0)
		return -1;
	if (MAIL_CACHE_IS_UNUSABLE(src))
		return 0; /* no caching decisions */

	pool_t pool;
	unsigned int count = 0;
	struct mail_cache_field *fields =
		mail_cache_register_get_list(src, &pool, &count);
	i_assert(fields != NULL || count == 0);
	if (count > 0)
		mail_cache_register_fields(dst, fields, count,
					   unsafe_data_stack_pool);

	/* Destination cache isn't expected to exist yet, so use purging
	   to create it. Setting field_header_write_pending also guarantees
	   that the fields are updated even if the cache was already created
	   and no purging was done. */
	dst->field_header_write_pending = TRUE;
	pool_unref(&pool);
	return mail_cache_purge(dst, 0, "copy cache decisions");
}
