/*
 * Comet: a slightly advanced ircd
 * m_batch.c: provides support for client-initiated and propagated BATCH commands
 *
 * Copyright (c) 2025 Ryan Schmidt <skizzerz@skizzerz.net>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include "stdinc.h"
#include "client.h"
#include "hash.h"
#include "ircd.h"
#include "numeric.h"
#include "s_conf.h"
#include "s_serv.h"
#include "send.h"
#include "msg.h"
#include "parse.h"
#include "hook.h"
#include "modules.h"
#include "batch.h"
#include "response.h"

static const char batch_desc[] =
	"Provides the BATCH command for client-initiated or propagated batches";

static struct ev_entry *timeout_ev;

static int batch_modinit(void);
static void batch_moddeinit(void);
static void process_batch_tag(void *);
static void handle_batch_line(void *);
static void handle_client_exit(void *);
static void m_queue(struct MsgBuf *msgbuf, struct Client *client_p, struct Client *source_p, int parc, const char *parv[]);
static void m_batch(struct MsgBuf *msgbuf, struct Client *client_p, struct Client *source_p, int parc, const char *parv[]);

static struct Message batch_msgtab = {
	"BATCH", 0, 0, 0, 0,
	{mg_unreg, {m_batch, 2}, {m_batch, 2}, {m_batch, 2}, mg_ignore, {m_batch, 2}}
};

mapi_clist_av1 batch_clist[] = { &batch_msgtab, NULL };

mapi_hfn_list_av1 batch_hfnlist[] = {
	{ "message_handler", handle_batch_line },
	{ "message_tag", process_batch_tag },
	{ "client_exit", handle_client_exit },
	{ NULL, NULL }
};

DECLARE_MODULE_AV2(batch, batch_modinit, batch_moddeinit, batch_clist, NULL, batch_hfnlist, NULL, NULL, batch_desc);

static struct
{
	const struct Client *incoming_client;
	const struct MsgBuf *incoming_message;
} saved_global_context;

/* Restore global contextual pointers with the opening BATCH message */
static void
restore_global_context(struct Client *client_p, struct Batch *batch)
{
	saved_global_context.incoming_client = incoming_client;
	saved_global_context.incoming_message = incoming_message;
	incoming_client = client_p;
	incoming_message = &batch->start->msg;
}

static void
reset_global_context(void)
{
	incoming_client = saved_global_context.incoming_client;
	incoming_message = saved_global_context.incoming_message;
}

static void
batch_timeout(void *arg)
{
	/* non-NULL arg indicates this is being called during module reload so we need to "time out" all batches */
	rb_dlink_node *cptr, *ptr, *next_ptr;
	struct Client *client;
	time_t now = rb_current_time();

	RB_DLINK_FOREACH(cptr, lclient_list.head)
	{
		client = cptr->data;
		RB_DLINK_FOREACH_SAFE(ptr, next_ptr, client->localClient->pending_batches.head)
		{
			struct Batch* batch = ptr->data;
			if (arg != NULL || batch->expires <= now)
			{
				restore_global_context(client, batch);
				struct ResponseInfo *info = resume_response_batch(batch->response_info);
				sendto_one(client, ":%s FAIL BATCH TIMEOUT %s :Batch timed out",
					me.name, batch->tag);
				client->localClient->pending_batch_lines -= batch->len + 1;
				batch_free(batch, info);
				reset_global_context();
				rb_dlinkDestroy(ptr, &client->localClient->pending_batches);
			}
		}
	}

	if (arg != NULL)
	{
		/* get rid of server batches only on module reload */
		RB_DLINK_FOREACH(cptr, serv_list.head)
		{
			client = cptr->data;
			RB_DLINK_FOREACH_SAFE(ptr, next_ptr, client->localClient->pending_batches.head)
			{
				batch_free(ptr->data, outgoing_response_info);
				rb_dlinkDestroy(ptr, &client->localClient->pending_batches);
			}

			client->localClient->pending_batch_lines = 0;
		}
	}
}

static void
handle_client_exit(void *data_)
{
	hook_data_client_exit *data = data_;
	rb_dlink_node *ptr, *next_ptr;

	if (!MyConnect(data->target))
		return;

	RB_DLINK_FOREACH_SAFE(ptr, next_ptr, data->target->localClient->pending_batches.head)
	{
		batch_free(ptr->data, outgoing_response_info);
		rb_dlinkDestroy(ptr, &data->target->localClient->pending_batches);
	}

	/* probably unnecessary but makes me feel better */
	data->target->localClient->pending_batch_lines = 0;
}

static void
finish_batch(struct Client *client_p, struct Client *source_p, struct Batch *batch)
{
	const struct BatchHandler *handler = get_batch_handler(batch->type);
	rb_dlink_node *ptr, *next_ptr;
	struct ResponseInfo *info;

	/* abort unfinished nested batches under this one */
	RB_DLINK_FOREACH_SAFE(ptr, next_ptr, client_p->localClient->pending_batches.head)
	{
		struct Batch *other = ptr->data;
		if (other->parent == batch)
		{
			restore_global_context(client_p, other);
			info = resume_response_batch(batch->response_info);
			if (IsClient(source_p))
				sendto_one(source_p, ":%s FAIL BATCH INCOMPLETE %s :Nested batch not finished before enclosing batch",
					me.name, other->tag);

			client_p->localClient->pending_batch_lines -= other->len + 1;
			batch_free(other, info);
			reset_global_context();
			rb_dlinkDestroy(ptr, &client_p->localClient->pending_batches);
		}
	}

	client_p->localClient->pending_batch_lines -= batch->len + 1;
	restore_global_context(client_p, batch);
	info = resume_response_batch(batch->response_info);

	if (handler == NULL)
	{
		/* getting here means that we originally accepted this batch type but no longer recognize it;
		 * e.g. the supporting module got unloaded between batch start and finish.
		 * There isn't a good solution here so just reject it and let the client send any fallbacks separately. */
		if (IsClient(source_p))
			sendto_one(source_p, ":%s FAIL BATCH UNKNOWN_TYPE %s %s :Unrecognized batch type",
				me.name, batch->tag, batch->type);

		batch_free(batch, info);
		reset_global_context();
		return;
	}

	handler->handler(client_p, source_p, batch, handler->userdata);

	/* handle child batches */
	if (!(handler->flags & BATCH_FLAG_SKIP_CHILDREN))
	{
		RB_DLINK_FOREACH_SAFE(ptr, next_ptr, batch->children.head)
		{
			struct Batch *child = ptr->data;
			rb_dlinkDestroy(ptr, &batch->children);
			/* global context isn't a stack, so ensure we reset to base before recursing */
			resume_response_batch(info);
			reset_global_context();
			finish_batch(client_p, source_p, child);
			restore_global_context(client_p, batch);
			info = resume_response_batch(batch->response_info);
		}
	}

	batch_free(batch, info);
	reset_global_context();
}

static void
handle_batch_line(void *data_)
{
	hook_data *data = data_;
	struct MsgBuf *msgbuf = data->arg1;
	struct MessageEntry *ehandler = data->arg2;

	if (msgbuf_get_tag(msgbuf, "batch") != NULL && strcasecmp(msgbuf->cmd, "BATCH") != 0)
		ehandler->handler = m_queue;
}

static void
m_queue(struct MsgBuf *msgbuf, struct Client *client_p, struct Client *source_p, int parc, const char *parv[])
{
	rb_dlink_node *ptr;
	struct BatchMessage *msg;
	const char *tag = msgbuf_get_tag(msgbuf, "batch");

	if (EmptyString(tag))
		return;

	RB_DLINK_FOREACH(ptr, client_p->localClient->pending_batches.head)
	{
		struct Batch *batch = ptr->data;
		if (!strcmp(batch->tag, tag))
		{
			msg = allocate_batch_message(msgbuf);
			rb_dlinkAddTailAlloc(msg, &batch->messages);
			batch->len++;
			client_p->localClient->pending_batch_lines++;
			return;
		}
	}
}

static void
m_batch(struct MsgBuf *msgbuf, struct Client *client_p, struct Client *source_p, int parc, const char *parv[])
{
	struct Batch *batch = NULL, *parent = NULL;
	rb_dlink_node *ptr;
	bool found = false;
	bool adding = *parv[1] == '+';
	const char *batch_tag = msgbuf_get_tag(msgbuf, "batch");

	if (batch_tag != NULL)
	{
		bool found_parent = false;
		RB_DLINK_FOREACH(ptr, client_p->localClient->pending_batches.head)
		{
			parent = ptr->data;
			if (!strcmp(parent->tag, batch_tag))
			{
				found_parent = true;
				break;
			}
		}

		if (!found_parent)
			return;
	}

	if (EmptyString(parv[1]) || parc < (adding ? 3 : 2))
	{
		if (IsClient(source_p))
			sendto_one(source_p, form_str(ERR_NEEDMOREPARAMS), me.name, source_p->name, "BATCH");
		return;
	}

	if (adding && EmptyString(parv[2]))
	{
		if (IsClient(source_p))
			sendto_one(source_p, form_str(ERR_NEEDMOREPARAMS), me.name, source_p->name, "BATCH");
		return;
	}

	if ((!adding && *parv[1] != '-') || EmptyString(parv[1] + 1))
	{
		if (IsClient(source_p))
			sendto_one(source_p, ":%s FAIL BATCH INVALID_REFTAG %s :Invalid reference tag",
				me.name, parv[1]);
		return;
	}

	RB_DLINK_FOREACH(ptr, client_p->localClient->pending_batches.head)
	{
		batch = ptr->data;
		if (!strcmp(batch->tag, parv[1] + 1))
		{
			found = true;
			break;
		}
	}

	if (adding)
	{
		if (found)
		{
			if (IsClient(source_p))
				sendto_one(source_p, ":%s FAIL BATCH INVALID_REFTAG %s :Reference tag already exists",
					me.name, parv[1]);
			return;
		}

		if (get_batch_handler(parv[2]) == NULL)
		{
			if (IsClient(source_p))
				sendto_one(source_p, ":%s FAIL BATCH UNKNOWN_TYPE %s %s :Unrecognized batch type",
					me.name, parv[1], parv[2]);
			return;
		}

		if (parent != NULL)
		{
			const struct BatchHandler *parent_handler = get_batch_handler(parent->type);
			if (parent_handler == NULL)
			{
				/* parent batch type got unregistered while we were in the middle of receiving this batch... */
				if (IsClient(source_p))
					sendto_one(source_p, ":%s FAIL BATCH UNKNOWN_TYPE %s %s :Unrecognized batch type",
						me.name, parent->tag, parent->type);
				return;
			}

			bool allowed = (parent_handler->flags & BATCH_FLAG_ALLOW_ALL) == BATCH_FLAG_ALLOW_ALL;
			const char *error = NULL;
			if (!allowed && parent_handler->child_allowed != NULL)
				allowed = parent_handler->child_allowed(client_p, source_p, parent, msgbuf, parent_handler->userdata, &error);

			if (!allowed)
			{
				if (IsClient(source_p))
				{
					if (error != NULL)
						sendto_one(source_p, ":%s FAIL BATCH %s", me.name, error);
					else
						sendto_one(source_p, ":%s FAIL BATCH INVALID_NESTING %s %s %s :The parent batch type does not allow this type to be nested under it",
							me.name, parv[1], parent->type, parv[2]);
				}
				return;
			}
		}

		batch = batch_init(msgbuf);
		batch->parent = parent;
		rb_dlinkAddAlloc(batch, &client_p->localClient->pending_batches);
		client_p->localClient->pending_batch_lines++;

		/* postpone any labeled-response to when the batch completes */
		batch->response_info = suspend_response_batch();
	}
	else
	{
		if (!found)
		{
			if (IsClient(source_p))
				sendto_one(source_p, ":%s FAIL BATCH INVALID_REFTAG %s :Invalid reference tag",
					me.name, parv[1]);
			return;
		}

		rb_dlinkDestroy(ptr, &client_p->localClient->pending_batches);

		if (batch->parent != NULL)
		{
			/* Finished a nested batch but the outer batch isn't complete yet, don't process just yet
			 * Instead, add this batch to the parent so it gets completed alongside the parent
			 */
			rb_dlinkAddTailAlloc(batch, &batch->parent->children);
			/* we deducted this earlier but the batch is still pending, so re-add the consumed lines */
			client_p->localClient->pending_batch_lines += batch->len + 1;
		}
		else
		{
			finish_batch(client_p, source_p, batch);
		}
	}
}

static void
process_batch_tag(void *data_)
{
	hook_data_message_tag *data = data_;

	if (!strcmp(data->key, "batch"))
	{
		if (IsServer(data->client) || !EmptyString(data->value))
		{
			data->approved = MESSAGE_TAG_ALLOW;
			data->capmask = CLICAP_BATCH;
		}
	}
}

static int
batch_modinit(void)
{
	timeout_ev = rb_event_addish("batch-timeout", &batch_timeout, NULL, 30);
	return 1;
}

static void
batch_moddeinit(void)
{
	batch_timeout((void *)1);
	rb_event_delete(timeout_ev);
}
