/* src/modules/chathistory.c - IRCv3 CHATHISTORY command.
 * (C) Copyright 2021 Bram Matthys (Syzop) and the UnrealIRCd team
 * License: GPLv2 or later
 *
 * This implements the "CHATHISTORY" command, the CAP and 005 token.
 * https://ircv3.net/specs/extensions/chathistory
 */
#include "unrealircd.h"

ModuleHeader MOD_HEADER
= {
	"chathistory",
	"1.0",
	"IRCv3 CHATHISTORY command",
	"UnrealIRCd Team",
	"unrealircd-6",
};

/* Structs */
typedef struct ChatHistoryTarget ChatHistoryTarget;
struct ChatHistoryTarget {
	ChatHistoryTarget *prev, *next;
	char *datetime;
	char *object;
};

/* Forward declarations */
CMD_FUNC(cmd_chathistory);

/* Global variables */
long CAP_CHATHISTORY = 0L;

#define CHATHISTORY_LIMIT 50

MOD_INIT()
{
	ClientCapabilityInfo c;

	MARK_AS_OFFICIAL_MODULE(modinfo);
	CommandAdd(modinfo->handle, "CHATHISTORY", cmd_chathistory, MAXPARA, CMD_USER);

	memset(&c, 0, sizeof(c));
	c.name = "draft/chathistory";
	ClientCapabilityAdd(modinfo->handle, &c, &CAP_CHATHISTORY);
	return MOD_SUCCESS;
}

MOD_LOAD()
{
	ISupportSetFmt(modinfo->handle, "CHATHISTORY", "%d", CHATHISTORY_LIMIT);
	ISupportSetFmt(modinfo->handle, "MSGREFTYPES", "msgid,timestamp");
	return MOD_SUCCESS;
}

MOD_UNLOAD()
{
	return MOD_SUCCESS;
}

int chathistory_token(const char *str, char *token, char **store)
{
	char request[BUFSIZE];
	char *p;

	strlcpy(request, str, sizeof(request));

	p = strchr(request, '=');
	if (!p)
		return 0;
	*p = '\0'; // frag
	if (!strcmp(request, token))
	{
		*p = '='; // restore
		*store = strdup(p + 1); // can be \0
		return 1;
	}
	*p = '='; // restore
	return 0;
}

static void add_chathistory_target_list(ChatHistoryTarget *new, ChatHistoryTarget **list)
{
	ChatHistoryTarget *x, *last = NULL;

	if (!*list)
	{
		/* We are the only item. Easy. */
		*list = new;
		return;
	}

	for (x = *list; x; x = x->next)
	{
		last = x;
		if (strcmp(new->datetime, x->datetime) >= 0)
			break;
	}

	if (x)
	{
		if (x->prev)
		{
			/* We will insert ourselves just before this item */
			new->prev = x->prev;
			new->next = x;
			x->prev->next = new;
			x->prev = new;
		} else {
			/* We are the new head */
			*list = new;
			new->next = x;
			x->prev = new;
		}
	} else
	{
		/* We are the last item */
		last->next = new;
		new->prev = last;
	}
}

static void add_chathistory_target(ChatHistoryTarget **list, HistoryResult *r)
{
	MessageTag *m;
	time_t ts;
	char *datetime;
	ChatHistoryTarget *e;

	if (!r->log || !((m = find_mtag(r->log->mtags, "time"))) || !m->value)
		return;
	datetime = m->value;

	e = safe_alloc(sizeof(ChatHistoryTarget));
	safe_strdup(e->datetime, datetime);
	safe_strdup(e->object, r->object);
	add_chathistory_target_list(e, list);
}

static void chathistory_targets_send_line(Client *client, ChatHistoryTarget *r, char *batchid)
{
	MessageTag *mtags = NULL;
	MessageTag *m;

	if (!BadPtr(batchid))
	{
		mtags = safe_alloc(sizeof(MessageTag));
		mtags->name = strdup("batch");
		mtags->value = strdup(batchid);
	}

	sendto_one(client, mtags, ":%s CHATHISTORY TARGETS %s %s",
		me.name, r->object, r->datetime);

	if (mtags)
		free_message_tags(mtags);
}

void chathistory_targets(Client *client, HistoryFilter *filter, int limit)
{
	Membership *mp;
	HistoryResult *r;
	char batch[BATCHLEN+1];
	int sent = 0;
	ChatHistoryTarget *targets = NULL, *targets_next;

	/* 1. Grab all information we need */

	filter->cmd = HFC_BEFORE;
	if (strcmp(filter->timestamp_a, filter->timestamp_b) < 0)
	{
		/* Swap if needed */
		char *swap = filter->timestamp_a;
		filter->timestamp_a = filter->timestamp_b;
		filter->timestamp_b = swap;
	}
	filter->limit = 1;

	for (mp = client->user->channel; mp; mp = mp->next)
	{
		Channel *channel = mp->channel;
		r = history_request(channel->name, filter);
		if (r)
		{
			add_chathistory_target(&targets, r);
			free_history_result(r);
		}
	}

	/* 2. Now send it to the client */

	batch[0] = '\0';
	if (HasCapability(client, "batch"))
	{
		/* Start a new batch */
		generate_batch_id(batch);
		sendto_one(client, NULL, ":%s BATCH +%s draft/chathistory-targets", me.name, batch);
	}

	for (; targets; targets = targets_next)
	{
		targets_next = targets->next;
		if (++sent < limit)
			chathistory_targets_send_line(client, targets, batch);
		safe_free(targets->datetime);
		safe_free(targets->object);
		safe_free(targets);
	}

	/* End of batch */
	if (*batch)
		sendto_one(client, NULL, ":%s BATCH -%s", me.name, batch);
}

void send_empty_batch(Client *client, const char *target)
{
	char batch[BATCHLEN+1];

	if (HasCapability(client, "batch"))
	{
		generate_batch_id(batch);
		sendto_one(client, NULL, ":%s BATCH +%s chathistory %s", me.name, batch, target);
		sendto_one(client, NULL, ":%s BATCH -%s", me.name, batch);
	}
}

CMD_FUNC(cmd_chathistory)
{
	HistoryFilter *filter = NULL;
	HistoryResult *r = NULL;
	Channel *channel;

	memset(&filter, 0, sizeof(filter));

	/* This command is only for local users */
	if (!MyUser(client))
		return;

	if ((parc < 5) || BadPtr(parv[4]))
	{
		sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS :Insufficient parameters", me.name);
		return;
	}

	if (!HasCapability(client, "server-time"))
	{
		sendnotice(client, "Your IRC client does not support the 'server-time' capability");
		sendnotice(client, "https://ircv3.net/specs/extensions/server-time");
		sendnotice(client, "History request refused.");
		return;
	}

	if (!strcasecmp(parv[1], "TARGETS"))
	{
		Membership *mp;
		int limit;

		filter = safe_alloc(sizeof(HistoryFilter));
		/* Below this point, instead of 'return', use 'goto end' */

		if (!chathistory_token(parv[2], "timestamp", &filter->timestamp_a))
		{
			sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s %s :Invalid parameter, must be timestamp=xxx",
				me.name, parv[1], parv[3]);
			goto end;
		}
		if (!chathistory_token(parv[3], "timestamp", &filter->timestamp_b))
		{
			sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s %s :Invalid parameter, must be timestamp=xxx",
				me.name, parv[1], parv[4]);
			goto end;
		}
		limit = atoi(parv[4]);
		chathistory_targets(client, filter, limit);
		goto end;
	}

	/* We don't support retrieving chathistory for PM's. Send empty response/batch, similar to channels without +H. */
	if (parv[2][0] != '#')
	{
		send_empty_batch(client, parv[2]);
		return;
	}

	channel = find_channel(parv[2]);
	if (!channel)
	{
		sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_TARGET %s %s :Messages could not be retrieved, not an existing channel",
			me.name, parv[1], parv[2]);
		return;
	}

	if (!IsMember(client, channel))
	{
		sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_TARGET %s %s :Messages could not be retrieved, you are not a member",
			me.name, parv[1], parv[2]);
		return;
	}

	/* Channel is not +H? Send empty response/batch (as per IRCv3 discussion) */
	if (!has_channel_mode(channel, 'H'))
	{
		send_empty_batch(client, channel->name);
		return;
	}

	filter = safe_alloc(sizeof(HistoryFilter));
	/* Below this point, instead of 'return', use 'goto end', which takes care of the freeing of 'filter' and 'history' */

	if (!strcasecmp(parv[1], "BEFORE"))
	{
		filter->cmd = HFC_BEFORE;
		if (!chathistory_token(parv[3], "timestamp", &filter->timestamp_a) &&
		    !chathistory_token(parv[3], "msgid", &filter->msgid_a))
		{
			sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s %s :Invalid parameter, must be timestamp=xxx or msgid=xxx",
				me.name, parv[1], parv[3]);
			goto end;
		}
		filter->limit = atoi(parv[4]);
	} else
	if (!strcasecmp(parv[1], "AFTER"))
	{
		filter->cmd = HFC_AFTER;
		if (!chathistory_token(parv[3], "timestamp", &filter->timestamp_a) &&
		    !chathistory_token(parv[3], "msgid", &filter->msgid_a))
		{
			sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s %s :Invalid parameter, must be timestamp=xxx or msgid=xxx",
				me.name, parv[1], parv[3]);
			goto end;
		}
		filter->limit = atoi(parv[4]);
	} else
	if (!strcasecmp(parv[1], "LATEST"))
	{
		filter->cmd = HFC_LATEST;
		if (!chathistory_token(parv[3], "timestamp", &filter->timestamp_a) &&
		    !chathistory_token(parv[3], "msgid", &filter->msgid_a) &&
		    strcmp(parv[3], "*"))
		{
			sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s %s :Invalid parameter, must be timestamp=xxx or msgid=xxx or *",
				me.name, parv[1], parv[3]);
			goto end;
		}
		filter->limit = atoi(parv[4]);
	} else
	if (!strcasecmp(parv[1], "AROUND"))
	{
		filter->cmd = HFC_AROUND;
		if (!chathistory_token(parv[3], "timestamp", &filter->timestamp_a) &&
		    !chathistory_token(parv[3], "msgid", &filter->msgid_a))
		{
			sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s %s :Invalid parameter, must be timestamp=xxx or msgid=xxx",
				me.name, parv[1], parv[3]);
			goto end;
		}
		filter->limit = atoi(parv[4]);
	} else
	if (!strcasecmp(parv[1], "BETWEEN"))
	{
		filter->cmd = HFC_BETWEEN;
		if (BadPtr(parv[5]))
		{
			sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s :Insufficient parameters", parv[1], me.name);
			goto end;
		}
		if (!chathistory_token(parv[3], "timestamp", &filter->timestamp_a) &&
		    !chathistory_token(parv[3], "msgid", &filter->msgid_a))
		{
			sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s %s :Invalid parameter, must be timestamp=xxx or msgid=xxx",
				me.name, parv[1], parv[3]);
			goto end;
		}
		if (!chathistory_token(parv[4], "timestamp", &filter->timestamp_b) &&
		    !chathistory_token(parv[4], "msgid", &filter->msgid_b))
		{
			sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s %s :Invalid parameter, must be timestamp=xxx or msgid=xxx",
				me.name, parv[1], parv[4]);
			goto end;
		}
		filter->limit = atoi(parv[5]);
	} else {
		sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s :Invalid subcommand", me.name, parv[1]);
		goto end;
	}

	if (filter->limit <= 0)
	{
		sendto_one(client, NULL, ":%s FAIL CHATHISTORY INVALID_PARAMS %s %d :Specified limit is =<0",
			me.name, parv[1], filter->limit);
		goto end;
	}

	if (filter->limit > CHATHISTORY_LIMIT)
		filter->limit = CHATHISTORY_LIMIT;

	if ((r = history_request(channel->name, filter)))
	{
		if ((r->num_bytes*1.5) > get_sendq(client))
		{
			/* This would get rather close to max sendq. We reject
			 * the request as to not cause "Max SendQ exceeded".
			 */
			sendto_one(client, NULL, ":%s FAIL CHATHISTORY MESSAGE_ERROR %s %s :History too large to deliver",
				me.name, parv[1], channel->name);
		} else {
			/* All ok, bump fake lag and add the result.
			 * The fakelag bump under normal circumstances is tiny
			 * at like 400ms worst-case without multiline. However, it
			 * becomes a different story if nearly all lines are multilines
			 * and we get into a "worst case scenario" with amplifications
			 * where the max 50 becomes 50*15=750 lines in practice.
			 * In such a case we may bump up to 5000ms max. This should
			 * not happen under normal circumstances and only when someone
			 * with lots of time on their hands is doing something evil
			 * in that channel (with apparent consent of the ops).
			 */
			long fakelag_ms = r->num_bytes / 64;
			if (fakelag_ms > 5000)
				fakelag_ms = 5000;
			add_fake_lag(client, fakelag_ms);
			history_send_result(client, r);
		}
	}

end:
	if (filter)
		free_history_filter(filter);
	if (r)
		free_history_result(r);
}
