/*
 * Comet: a slightly advanced ircd
 * response.c: labeled-response helpers
 *
 * Copyright (c) 2026 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 "rb_dictionary.h"
#include "batch.h"
#include "client.h"
#include "response.h"
#include "send.h"
#include "s_assert.h"
#include "s_serv.h"

/* number of seconds we'll wait for remote servers to finish sending their responses
 * before we abort a pending remote labeled-response batch
 */
#define RESPONSE_EXPIRY 10

struct ResponseInfo *outgoing_response_info;
static rb_dictionary *pending_responses;

static int
response_cmp(const void *s1, const void *s2)
{
	return strcmp(s1, s2);
}

static void
cleanup_pending_responses(void *unused)
{
	struct ResponseInfo *response;
	rb_dictionary_iter iter;
	rb_dlink_list freelist = { NULL, NULL, 0 };
	rb_dlink_node *ptr, *nptr;
	time_t now = rb_current_time();

	/* RB_DICTIONARY_FOREACH is not safe for deletion, so need to do this in two passes */
	RB_DICTIONARY_FOREACH(response, &iter, pending_responses)
	{
		if (!(response->flags & RESPONSE_FLAG_NO_EXPIRE) && response->expires < now)
		{
			rb_dlinkAddAlloc(response, &freelist);
		}
	}

	RB_DLINK_FOREACH_SAFE(ptr, nptr, freelist.head)
	{
		response = ptr->data;
		if (response->remote_response > 0)
			sendto_one(response->source_p, ":%s BATCH -%s", me.name, response->batch);
		rb_dlinkDestroy(ptr, &freelist);
		rb_dictionary_delete(pending_responses, response->batch);
		free_response_batch(response, outgoing_response_info);
	}
}

void
init_response(void)
{
	pending_responses = rb_dictionary_create("remote labeled responses", response_cmp);

	rb_event_addish("cleanup_pending_responses", &cleanup_pending_responses, NULL, 10);
}

void
begin_local_response_batch(void)
{
	if (outgoing_response_info == NULL || !MyConnect(outgoing_response_info->source_p))
		return;

	if (!EmptyString(outgoing_response_info->batch))
		return;

	char *batch_id = rb_malloc(BATCH_ID_LEN);
	generate_batch_id(batch_id, BATCH_ID_LEN);
	outgoing_response_info->batch = batch_id;
	sendto_one(outgoing_response_info->source_p, ":%s BATCH +%s labeled-response",
		me.name, outgoing_response_info->batch);
}

void
begin_remote_response_batch(int server_count, const char *mask)
{
	if (outgoing_response_info == NULL || !MyConnect(outgoing_response_info->source_p))
		return;

	rb_dlink_node *node = rb_make_rb_dlink_node();
	outgoing_response_info->remote_node = node;

	if (EmptyString(outgoing_response_info->batch))
	{
		/* creating a new response batch */
		char *batch_id = rb_malloc(BATCH_ID_LEN);
		generate_batch_id(batch_id, BATCH_ID_LEN);
		outgoing_response_info->batch = batch_id;
		outgoing_response_info->remote_response = server_count;
		outgoing_response_info->mask = rb_strdup(mask);
		outgoing_response_info->expires = rb_current_time() + RESPONSE_EXPIRY;
		rb_dlinkAdd(outgoing_response_info, node, &outgoing_response_info->source_p->localClient->pending_remote_responses);
		rb_dictionary_add(pending_responses, outgoing_response_info->batch, outgoing_response_info);
		sendto_one(outgoing_response_info->source_p, ":%s BATCH +%s labeled-response",
			me.name, outgoing_response_info->batch);
	}
	else
	{
		/* can only upgrade local batches to remote */
		s_assert(outgoing_response_info->remote_response == 0);

		outgoing_response_info->remote_response += server_count;
		outgoing_response_info->mask = rb_strdup(mask);
		outgoing_response_info->expires = rb_current_time() + RESPONSE_EXPIRY;
		rb_dlinkAdd(outgoing_response_info, node, &outgoing_response_info->source_p->localClient->pending_remote_responses);
		rb_dictionary_add(pending_responses, outgoing_response_info->batch, outgoing_response_info);
	}
}

struct ResponseInfo *
get_remote_response_batch(const char *batch)
{
	return rb_dictionary_retrieve(pending_responses, batch);
}

struct ResponseInfo *
resume_response_batch(struct ResponseInfo *response)
{
	struct ResponseInfo *saved = outgoing_response_info;
	/* already set? no need to mess with clicaps if so */
	if (outgoing_response_info == response)
		return saved;

	if (outgoing_response_info != NULL && MyConnect(outgoing_response_info->source_p))
		ClearClientCap(outgoing_response_info->source_p, CLICAP_RECEIVE_LABEL);

	outgoing_response_info = response;
	if (response != NULL && MyClient(response->source_p) && IsClientCapable(response->source_p, CLICAP_LABELED_RESPONSE | CLICAP_BATCH))
		SetClientCap(response->source_p, CLICAP_RECEIVE_LABEL);

	return saved;
}

void
free_response_batch(struct ResponseInfo *response, struct ResponseInfo *resume)
{
	bool do_resume = response == outgoing_response_info;
	if (response == NULL)
		return;

	if (response == resume)
		resume = NULL;

	if (MyConnect(response->source_p))
	{
		ClearClientCap(response->source_p, CLICAP_RECEIVE_LABEL);

		if (response->remote_node != NULL)
			rb_dlinkDestroy(response->remote_node, &response->source_p->localClient->pending_remote_responses);
	}

	if (!EmptyString(response->batch))
		rb_dictionary_delete(pending_responses, response->batch);

	if (!(response->flags & RESPONSE_FLAG_STATIC))
	{
		rb_free(response->batch);
		rb_free(response->label);
		rb_free(response->mask);
		rb_free(response);
	}

	if (do_resume)
	{
		outgoing_response_info = resume;
		if (resume != NULL && MyClient(resume->source_p) && IsClientCapable(resume->source_p, CLICAP_LABELED_RESPONSE | CLICAP_BATCH))
			SetClientCap(resume->source_p, CLICAP_RECEIVE_LABEL);
	}
}

int
count_match_servs(struct Client *one, const char *mask, uint64_t cap, uint64_t negcap)
{
	int count = 0;
	rb_dlink_node *ptr;

	if (EmptyString(mask))
		return 0;

	RB_DLINK_FOREACH(ptr, global_serv_list.head)
	{
		struct Client *target_p = ptr->data;
		if (IsMe(target_p))
			continue;

		if (target_p->from == one->from)
			continue;

		if (!match(mask, target_p->name))
			continue;

		if (cap && !IsServerCapable(target_p->from, cap))
			continue;

		if (negcap && !NotServerCapable(target_p->from, negcap))
			continue;

		count++;
	}

	return count;
}
