/*
 * Comet: a slightly advanced ircd
 * m_quarantine.c: restrict connections until they identify to services
 *
 * 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 "channel.h"
#include "chmode.h"
#include "modules.h"
#include "hook.h"
#include "client.h"
#include "ircd.h"
#include "logger.h"
#include "newconf.h"
#include "numeric.h"
#include "send.h"
#include "s_newconf.h"
#include "s_serv.h"
#include "s_user.h"

#define IsOperQuarantine(x) (HasPrivilege((x), "oper:quarantine"))
#define IsQuarantined(x) ((x)->umodes & user_modes['q'])
#define DEFAULT_JOIN_REASON "Cannot join channel (usermode +q) - you need to be logged into your NickServ account"
#define DEFAULT_MSG_REASON "Cannot send to nick/channel (usermode +q) - you need to be logged into your NickServ account"
#define DEFAULT_OTHER_MSG_REASON "Cannot send to user - they are quarantined and will be unable to respond to you"
#define DEFAULT_APPLY_MSG "You have been quarantined and must log into your NickServ account before you can join channels. Please see /STATS p for assistance."
#define DEFAULT_REMOVE_MSG "You are no longer quarantined and can freely join channels."

static const char quarantine_desc[] = "QUARANTINE system and umode +q to restrict unidentified connections.";
static char *join_reason = NULL;
static char *msg_reason = NULL;
static char *other_msg_reason = NULL;
static char *apply_msg = NULL;
static char *remove_msg = NULL;
static int part_channels = 1;
static char **allowed_channels = NULL;

static struct Client *allowed_umode_change = NULL;

static int modinit(void);
static void moddeinit(void);
static void me_quarantine(struct MsgBuf *, struct Client *, struct Client *, int, const char *[]);
static void mo_quarantine(struct MsgBuf *, struct Client *, struct Client *, int, const char *[]);
static void me_unquarantine(struct MsgBuf *, struct Client *, struct Client *, int, const char *[]);
static void mo_unquarantine(struct MsgBuf *, struct Client *, struct Client *, int, const char *[]);
static void add_quarantine(struct Client *source_p, struct Client *target_p, const char *reason);
static void remove_quarantine(struct Client *source_p, struct Client *target_p);
static void quarantine_account_change(void *);
static void quarantine_change_umode(void *);
static void quarantine_conf_info(void *);
static void quarantine_default_umode_begin(void *);
static void quarantine_default_umode_end(void *);
static void quarantine_join_channel(void *);
static void quarantine_privmsg_channel(void *);
static void quarantine_privmsg_user(void *);
static void quarantine_reset_conf(void *);
static void quarantine_set_allow_channels(void *);

struct Message quarantine_msgtab = {
	"QUARANTINE", 0, 0, 0, 0,
	{ mg_unreg, mg_not_oper, mg_not_oper, mg_ignore, { me_quarantine, 3 }, { mo_quarantine, 3 } }
};

struct Message unquarantine_msgtab = {
	"UNQUARANTINE", 0, 0, 0, 0,
	{ mg_unreg, mg_not_oper, mg_not_oper, mg_ignore, { me_unquarantine, 2 }, { mo_unquarantine, 2 } }
};

struct ConfEntry conf_quarantine_table[] = {
	{ "allow_channels", CF_QSTRING | CF_FLIST, quarantine_set_allow_channels, 0, NULL },
	{ "apply_msg", CF_QSTRING, NULL, BUFSIZE, &apply_msg },
	{ "join_reason", CF_QSTRING, NULL, BUFSIZE, &join_reason },
	{ "msg_reason", CF_QSTRING, NULL, BUFSIZE, &msg_reason },
	{ "other_msg_reason", CF_QSTRING, NULL, BUFSIZE, &other_msg_reason },
	{ "part_channels", CF_YESNO, NULL, 0, &part_channels },
	{ "remove_msg", CF_QSTRING, NULL, BUFSIZE, &remove_msg },
	{ "\0",	0, NULL, 0, NULL }
};

mapi_clist_av1 quarantine_clist[] = { &quarantine_msgtab, &unquarantine_msgtab, NULL };

mapi_hfn_list_av1 quarantine_hfn_list[] = {
	{ "account_change", quarantine_account_change },
	{ "can_join", quarantine_join_channel },
	{ "conf_read_start", quarantine_reset_conf },
	{ "doing_info_conf", quarantine_conf_info },
	{ "introduce_client", quarantine_default_umode_end },
	{ "new_local_user", quarantine_default_umode_begin, HOOK_MONITOR },
	{ "privmsg_channel", quarantine_privmsg_channel },
	{ "privmsg_user", quarantine_privmsg_user },
	{ "umode_changed", quarantine_change_umode },
	{ NULL, NULL }
};

DECLARE_MODULE_AV2(m_quarantine, modinit, moddeinit, quarantine_clist, NULL, quarantine_hfn_list, NULL, NULL, quarantine_desc);

static int
modinit(void)
{
	if (find_top_conf("quarantine") != NULL)
	{
		ierror("m_quarantine: a top conf block named quarantine already exists, unloading extension");
		return -1;
	}

	user_modes['q'] = find_umode_slot();
	if (!user_modes['q'])
	{
		ierror("m_quarantine: unable to allocate usermode slot for +q, unloading extension");
		return -1;
	}

	construct_umodebuf();
	add_top_conf("quarantine", NULL, NULL, conf_quarantine_table);
	return 0;
}

static void
moddeinit(void)
{
	user_modes['q'] = 0;
	construct_umodebuf();
	remove_top_conf("quarantine");
	quarantine_reset_conf(NULL);
}

static void
mo_quarantine(struct MsgBuf *msgbuf_p, struct Client *client_p, struct Client *source_p, int parc, const char *parv[])
{
	if (!IsOperQuarantine(source_p))
	{
		sendto_one(source_p, form_str(ERR_NOPRIVS), me.name, source_p->name, "quarantine");
		return;
	}

	struct Client *target_p = find_named_person(parv[1]);
	if (target_p == NULL)
	{
		sendto_one_numeric(source_p, ERR_NOSUCHNICK, form_str(ERR_NOSUCHNICK), parv[1]);
		return;
	}

	if (MyClient(target_p))
		add_quarantine(source_p, target_p, parv[2]);
	else
		sendto_one(target_p, ":%s ENCAP %s QUARANTINE %s :%s",
			get_id(source_p, target_p), target_p->servptr->name, get_id(target_p, target_p), parv[2]);
}

static void
me_quarantine(struct MsgBuf *msgbuf_p, struct Client *client_p, struct Client *source_p, int parc, const char *parv[])
{
	struct Client *target_p = find_named_person(parv[1]);
	if (target_p == NULL)
	{
		sendto_one_numeric(source_p, ERR_NOSUCHNICK, form_str(ERR_NOSUCHNICK), parv[1]);
		return;
	}

	if (MyClient(target_p))
		add_quarantine(source_p, target_p, parv[2]);
}

static void
mo_unquarantine(struct MsgBuf *msgbuf_p, struct Client *client_p, struct Client *source_p, int parc, const char *parv[])
{
	if (!IsOperQuarantine(source_p))
	{
		sendto_one(source_p, form_str(ERR_NOPRIVS), me.name, source_p->name, "quarantine");
		return;
	}

	struct Client *target_p = find_named_person(parv[1]);
	if (target_p == NULL)
	{
		sendto_one_numeric(source_p, ERR_NOSUCHNICK, form_str(ERR_NOSUCHNICK), parv[1]);
		return;
	}

	if (MyClient(target_p))
		remove_quarantine(source_p, target_p);
	else
		sendto_one(target_p, ":%s ENCAP %s UNQUARANTINE %s",
			get_id(source_p, target_p), target_p->servptr->name, get_id(target_p, target_p));
}

static void
me_unquarantine(struct MsgBuf *msgbuf_p, struct Client *client_p, struct Client *source_p, int parc, const char *parv[])
{
	struct Client *target_p = find_named_person(parv[1]);
	if (target_p == NULL)
	{
		sendto_one_numeric(source_p, ERR_NOSUCHNICK, form_str(ERR_NOSUCHNICK), parv[1]);
		return;
	}

	if (MyClient(target_p))
		remove_quarantine(source_p, target_p);
}

static void
add_quarantine(struct Client *source_p, struct Client *target_p, const char *reason)
{
	const char *parv[4] = { target_p->name, target_p->name, "+q", NULL };

	if (IsQuarantined(target_p))
	{
		sendto_one_notice(source_p, ":*** %s is already quarantined.",
			target_p->name);
		return;
	}

	if (!EmptyString(target_p->user->suser))
	{
		sendto_one_notice(source_p, ":*** %s is already logged into services and cannot be quarantined.",
			target_p->name);
		return;
	}

	if (IsOper(target_p))
	{
		sendto_one_notice(source_p, ":*** %s is an oper and cannot be put into quarantine.",
			target_p->name);
		return;
	}

	if (IsService(target_p))
	{
		sendto_one_notice(source_p, ":*** %s is a network service and cannot be put into quarantine.",
			target_p->name);
		return;
	}

	sendto_realops_snomask(SNO_GENERAL, L_NETWIDE, "%s has put %s into QUARANTINE [%s]",
		source_p->name, target_p->name, reason);
	sendto_one_notice(target_p, ":*** %s",
		EmptyString(apply_msg) ? DEFAULT_APPLY_MSG : apply_msg);

	allowed_umode_change = target_p;
	user_mode(target_p, target_p, 3, parv);
	allowed_umode_change = NULL;

	if (part_channels)
	{
		rb_dlink_node *ptr, *next_ptr;
		RB_DLINK_FOREACH_SAFE(ptr, next_ptr, target_p->user->channel.head)
		{
			struct membership *msptr = ptr->data;
			struct Channel *chptr = msptr->chptr;
			bool allowed = false;
			if (allowed_channels != NULL)
			{
				for (int i = 0; allowed_channels[i] != NULL; i++)
				{
					if (!irccmp(chptr->chname, allowed_channels[i]))
					{
						allowed = true;
						break;
					}
				}
			}

			if (!allowed)
			{
				sendto_server(NULL, chptr, CAP_TS6, NOCAPS, ":%s PART %s",
					use_id(source_p), chptr->chname);
				sendto_channel_local(target_p, ALL_MEMBERS, chptr, ":%s!%s@%s PART %s",
					target_p->name, target_p->username, target_p->host, chptr->chname);
				remove_user_from_channel(msptr);
			}
		}
	}
}

static void
remove_quarantine(struct Client *source_p, struct Client *target_p)
{
	const char *parv[4] = { target_p->name, target_p->name, "-q", NULL };

	if (!IsQuarantined(target_p))
	{
		sendto_one_notice(source_p, ":*** %s is not currently quarantined.",
			target_p->name);
		return;
	}

	if (source_p != target_p)
		sendto_realops_snomask(SNO_GENERAL, L_NETWIDE, "%s has removed %s from QUARANTINE",
			source_p->name, target_p->name);
	sendto_one_notice(target_p, ":*** %s",
		EmptyString(remove_msg) ? DEFAULT_REMOVE_MSG : remove_msg);

	allowed_umode_change = target_p;
	user_mode(target_p, target_p, 3, parv);
	allowed_umode_change = NULL;
}

static void
quarantine_account_change(void *data_)
{
	hook_cdata *data = data_;
	if (!MyClient(data->client) || !IsQuarantined(data->client))
		return;

	if (!EmptyString(data->client->user->suser))
		remove_quarantine(data->client, data->client);
}

static void
quarantine_change_umode(void *data_)
{
	hook_data_umode_changed *data = data_;

	if (!MyClient(data->client))
		return;

	/* If someone successfully opers, unquarantine them.
	 * Services might be down and we don't want to impede them doing what they need to do.
	 */
	if (IsOper(data->client) && IsQuarantined(data->client))
	{
		sendto_one_notice(data->client, ":*** %s",
			EmptyString(remove_msg) ? DEFAULT_REMOVE_MSG : remove_msg);
		data->client->umodes &= ~user_modes['q'];
		return;
	}

	bool quarantine_changed = ((data->client->umodes ^ data->oldumodes) & user_modes['q']) == user_modes['q'];
	if (data->client != allowed_umode_change && quarantine_changed)
	{
		if (data->oldumodes & user_modes['q'])
			data->client->umodes |= user_modes['q'];
		else
			data->client->umodes &= ~user_modes['q'];
	}
}

static void
quarantine_conf_info(void *data_)
{
	hook_data *data = data_;

	/* quarantine::allow_channels not listed, since INFO in general doesn't seem
	 * to support sending list-type conf options (e.g. no hidden_caps) */

	sendto_one(data->client, ":%s %d %s :%-30s %-16s [%s]",
		get_id(&me, data->client), RPL_INFO,
		get_id(data->client, data->client),
		"quarantine::apply_msg",
		EmptyString(apply_msg) ? DEFAULT_APPLY_MSG : apply_msg,
		"Notice sent to users that have been quarantined");

	sendto_one(data->client, ":%s %d %s :%-30s %-16s [%s]",
		get_id(&me, data->client), RPL_INFO,
		get_id(data->client, data->client),
		"quarantine::join_reason",
		EmptyString(join_reason) ? DEFAULT_JOIN_REASON : join_reason,
		"Sent to +q users when JOIN is denied");

	sendto_one(data->client, ":%s %d %s :%-30s %-16s [%s]",
		get_id(&me, data->client), RPL_INFO,
		get_id(data->client, data->client),
		"quarantine::msg_reason",
		EmptyString(msg_reason) ? DEFAULT_MSG_REASON : msg_reason,
		"Sent to +q users when sending a message is denied");

	sendto_one(data->client, ":%s %d %s :%-30s %-16s [%s]",
		get_id(&me, data->client), RPL_INFO,
		get_id(data->client, data->client),
		"quarantine::other_msg_reason",
		EmptyString(other_msg_reason) ? DEFAULT_OTHER_MSG_REASON : other_msg_reason,
		"Sent to users trying to message a +q user");

	sendto_one(data->client, ":%s %d %s :%-30s %-16s [%s]",
		get_id(&me, data->client), RPL_INFO,
		get_id(data->client, data->client),
		"quarantine::part_channels",
		part_channels ? "YES" : "NO",
		"QUARANTINE causes user to part disallowed channels");

	sendto_one(data->client, ":%s %d %s :%-30s %-16s [%s]",
		get_id(&me, data->client), RPL_INFO,
		get_id(data->client, data->client),
		"quarantine::remove_msg",
		EmptyString(remove_msg) ? DEFAULT_REMOVE_MSG : remove_msg,
		"Notice sent to users that are no longer quarantined");
}

static void
quarantine_default_umode_begin(void *data)
{
	/* if ircd.conf has +q as a default umode, and the user didn't identify
	 * during registration (via SASL or PASS), allow the +q and notify the user
	 */
	struct Client *source_p = data;
	if (!IsAnyDead(source_p) && IsQuarantined(source_p))
	{
		if (!EmptyString(source_p->user->suser))
			source_p->umodes &= ~user_modes['q'];
		else
		{
			sendto_one_notice(source_p, ":*** %s",
				EmptyString(apply_msg) ? DEFAULT_APPLY_MSG : apply_msg);
			allowed_umode_change = source_p;
		}
	}
}

static void
quarantine_default_umode_end(void *data)
{
	/* Introducing a new local user calls three hooks in the following order:
	 * 1. new_local_user
	 * 2. umode_changed
	 * 3. introduce_client
	 * We make sure we clear the allowed_umode_change after introduce_client so it can't linger
	 * after the client is introduced. Non-local users also call the latter two, but effectively
	 * no-ops (umode_changed aborts if non-local, this just sets something already NULL to NULL),
	 * so no issues there
	 */
	allowed_umode_change = NULL;
}

static void
quarantine_join_channel(void *data_)
{
	hook_data_channel *data = data_;
	if (!MyClient(data->client) || !IsQuarantined(data->client))
		return;

	/* if some other hook function already rejected this join attempt, keep that rejection */
	if (data->approved != 0)
		return;

	if (allowed_channels != NULL)
	{
		for (int i = 0; allowed_channels[i] != NULL; i++)
		{
			if (!irccmp(data->chptr->chname, allowed_channels[i]))
				return;
		}
	}

	/* Remind the user that they're quarantined */
	sendto_one_numeric(data->client, ERR_NEEDREGGEDNICK, "%s :%s",
		data->chptr->chname,
		EmptyString(join_reason) ? DEFAULT_JOIN_REASON : join_reason);
	data->approved = ERR_CUSTOM;
}

static void
quarantine_privmsg_channel(void *data_)
{
	hook_data_privmsg_channel *data = data_;
	if (!MyClient(data->source_p) || !IsQuarantined(data->source_p))
		return;

	/* if some other hook function already rejected this attempt, keep that rejection */
	if (data->approved != 0)
		return;

	if (allowed_channels != NULL)
	{
		for (int i = 0; allowed_channels[i] != NULL; i++)
		{
			if (!irccmp(data->chptr->chname, allowed_channels[i]))
				return;
		}
	}

	data->approved = ERR_CANNOTSENDTOCHAN;

	/* Don't give error messages for non-PRIVMSG since many clients autogenerate these (e.g. +typing) */
	if (data->msgtype == MESSAGE_TYPE_PRIVMSG)
		sendto_one_numeric(data->source_p, ERR_CANNOTSENDTOCHAN, "%s :%s",
			data->chptr->chname,
			EmptyString(msg_reason) ? DEFAULT_MSG_REASON : msg_reason);
}

static void
quarantine_privmsg_user(void *data_)
{
	hook_data_privmsg_user *data = data_;
	if (!MyClient(data->source_p))
		return;

	/* if some other hook function already rejected this attempt, keep that rejection */
	if (data->approved != 0)
		return;

	if (!IsQuarantined(data->source_p))
	{
		if (IsQuarantined(data->target_p) && !IsOper(data->source_p) && !IsService(data->source_p))
		{
			if (data->msgtype == MESSAGE_TYPE_PRIVMSG)
				sendto_one_numeric(data->source_p, ERR_CANNOTSENDTOCHAN, "%s :%s",
					data->target_p->name,
					EmptyString(other_msg_reason) ? DEFAULT_OTHER_MSG_REASON : other_msg_reason);
			data->approved = ERR_CANNOTSENDTOCHAN;
		}
		return;
	}

	/* let quarantined users message opers and services */
	if (IsOper(data->target_p) || IsService(data->target_p))
		return;

	data->approved = ERR_CANNOTSENDTOCHAN;

	/* Don't give error messages for non-PRIVMSG since many clients autogenerate these (e.g. +typing) */
	if (data->msgtype == MESSAGE_TYPE_PRIVMSG)
		sendto_one_numeric(data->source_p, ERR_CANNOTSENDTOCHAN, "%s :%s",
			data->target_p->name,
			EmptyString(msg_reason) ? DEFAULT_MSG_REASON : msg_reason);
}

static void
quarantine_reset_conf(void *unused)
{
	if (allowed_channels != NULL)
	{
		for (int i = 0; allowed_channels[i] != NULL; i++)
			rb_free(allowed_channels[i]);
		rb_free(allowed_channels);
		allowed_channels = NULL;
	}

	rb_free(apply_msg);
	rb_free(join_reason);
	rb_free(msg_reason);
	rb_free(other_msg_reason);
	rb_free(remove_msg);

	apply_msg = NULL;
	join_reason = NULL;
	msg_reason = NULL;
	other_msg_reason = NULL;
	remove_msg = NULL;
}

static void
quarantine_set_allow_channels(void *data)
{
	size_t n = 0;
	for (conf_parm_t *arg = data; arg; arg = arg->next)
		n++;

	allowed_channels = rb_malloc((n + 1) * sizeof(char *));

	n = 0;
	for (conf_parm_t *arg = data; arg; arg = arg->next)
		allowed_channels[n++] = rb_strdup(arg->v.string);
	allowed_channels[n] = NULL;
}
