/*
 * Easily remove *-Lines in bulk
 * (C) Copyright 2019 Gottem and the UnrealIRCd team
 *
 * 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 1, 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., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#include "unrealircd.h"

ModuleHeader MOD_HEADER = {
	"rmtkl",
	"1.4",
	"Adds /rmtkl command to easily remove *-Lines in bulk",
	"Gottem and the UnrealIRCd Team",
	"unrealircd-6",
};

#define IsParam(x) (parc > (x) && !BadPtr(parv[(x)]))
#define IsNotParam(x) (parc <= (x) || BadPtr(parv[(x)]))

typedef struct {
	int type;
	char flag;
	char *txt;
	char *operpriv;
} TKLType;

static void dump_str(Client *client, const char **buf);
static TKLType *find_TKLType_by_flag(char flag);
void rmtkl_check_options(const char *param, int *skipperm, int *silent);
int rmtkl_tryremove(Client *client, TKLType *tkltype, TKL *tkl, const char *uhmask, const char *commentmask, int skipperm, int silent);
CMD_FUNC(rmtkl);

TKLType tkl_types[] = {
	{ TKL_KILL, 'k', "K-Line", "server-ban:kline:remove" },
	{ TKL_ZAP, 'z',	"Z-Line", "server-ban:zline:local:remove" },
	{ TKL_KILL | TKL_GLOBAL, 'G', "G-Line", "server-ban:gline:remove" },
	{ TKL_ZAP | TKL_GLOBAL, 'Z', "Global Z-Line", "server-ban:zline:global:remove" },
	{ TKL_SHUN | TKL_GLOBAL, 's', "Shun", "server-ban:shun:remove" },
//	{ TKL_SPAMF | TKL_GLOBAL, 'F', "Global Spamfilter", "server-ban:spamfilter:remove" }, TODO: re-add spamfilter support
	{ 0, 0, "Unknown *-Line", 0 },
};

static const char *rmtkl_help[] = {
	"*** \002Help on /rmtkl\002 *** ",
	"Removes all TKLs matching the given conditions from the local server, or the entire",
	"network if it's a global-type ban.",
	"Syntax:",
	"    \002/rmtkl\002 \037user@host\037 \037type\037 [\037comment\037] [\037-skipperm\037] [\037-silent\037]",
	"The \037user@host\037 field is a wildcard mask to match the target of a ban.",
	"The \037type\037 field may contain any number of the following characters:",
	"    k, z, G, Z, s, F and *",
	"    These correspond to (local) K-Line, (local) Z-Line, G-Line, Global Z-Line, (global) Shun and (global) Spamfilter",
	"    (asterisk includes every type besides F)",
	"The \037comment\037 field is also a wildcard mask to match the reason text of a ban. If specified, it must always",
	"come \037before\037 the options starting with \002-\002.",
	"Examples:",
	"    - \002/rmtkl * *\002",
	"        [remove \037all\037 supported TKLs except spamfilters]",
	"    - \002/rmtkl *@*.mx GZ\002 * -skipperm",
	"        [remove all Mexican G/Z-Lines while skipping over permanent ones]",
/*	"    - \002/rmtkl * * *Zombie*\002",
	"        [remove all non-spamfilter bans having \037Zombie\037 in the reason field]", TODO: re-add spamfilter support  */
	"*** \002End of help\002 ***",
	NULL
};

MOD_INIT()
{
	MARK_AS_OFFICIAL_MODULE(modinfo);
	if (CommandExists("RMTKL"))
	{
		config_error("Command RMTKL already exists");
		return MOD_FAILED;
	}
	CommandAdd(modinfo->handle, "RMTKL", rmtkl, 5, CMD_USER);
	return MOD_SUCCESS;
}

MOD_LOAD()
{
	if (ModuleGetError(modinfo->handle) != MODERR_NOERROR)
	{
		config_error("A critical error occurred when loading module %s: %s", MOD_HEADER.name, ModuleGetErrorStr(modinfo->handle));
		return MOD_FAILED;
	}
	return MOD_SUCCESS;
}

MOD_UNLOAD()
{
	return MOD_SUCCESS;
}

static void dump_str(Client *client, const char **buf)
{
	if (!MyUser(client))
		return;

	// Using sendto_one() instead of sendnumericfmt() because the latter strips indentation and stuff ;]
	for (; *buf != NULL; buf++)
		sendto_one(client, NULL, ":%s %03d %s :%s", me.name, RPL_TEXT, client->name, *buf);

	// Let user take 8 seconds to read it
	add_fake_lag(client, 8000);
}

static TKLType *find_TKLType_by_flag(char flag)
{
	TKLType *t;
	for (t = tkl_types; t->type; t++)
		if (t->flag == flag)
			break;
	return t;
}

void rmtkl_check_options(const char *param, int *skipperm, int *silent) {
	if (!strcasecmp("-skipperm", param))
		*skipperm = 1;
	if (!strcasecmp("-silent", param))
		*silent = 1;
}

int rmtkl_tryremove(Client *client, TKLType *tkltype, TKL *tkl, const char *uhmask, const char *commentmask, int skipperm, int silent)
{
	if (tkl->type != tkltype->type)
		return 0;

	// Let's not touch Q-Lines
	if (tkl->type & TKL_NAME)
		return 0;

	/* Don't touch TKL's that were added through config */
	if (tkl->flags & TKL_FLAG_CONFIG)
		return 0;

	if (TKLIsSpamfilter(tkl))
	{
#if 0
//FIXME: re-add spamfilter support
		// Is a spamfilter added through IRC, we can remove this if the "user" mask matches the reason
		if (!match_simple(uhmask, tkl->reason))
			return 0;
#endif
	} else
	if (TKLIsServerBan(tkl))
	{
		if (!match_simple(uhmask, make_user_host(tkl->ptr.serverban->usermask, tkl->ptr.serverban->hostmask)))
			return 0;

		if (commentmask && !match_simple(commentmask, tkl->ptr.serverban->reason))
			return 0;
	} else
		return 0;

	if (skipperm && tkl->expire_at == 0)
		return 0;

	if (!silent)
		sendnotice_tkl_del(client->name, tkl);

	RunHook(HOOKTYPE_TKL_DEL, client, tkl);

	if (tkl->type & TKL_SHUN)
		tkl_check_local_remove_shun(tkl);
	tkl_del_line(tkl);
	return 1;
}

CMD_FUNC(rmtkl)
{
	TKL *tkl, *next;
	TKLType *tkltype;
	const char *types, *uhmask, *commentmask, *p;
	char tklchar;
	int tklindex, tklindex2, skipperm, silent;
	unsigned int count;
	char broadcast[BUFSIZE];

	if (!IsULine(client) && !IsOper(client))
	{
		sendnumeric(client, ERR_NOPRIVILEGES);
		return;
	}

	if (IsNotParam(1))
	{
		dump_str(client, rmtkl_help);
		return;
	}

	if (IsNotParam(2))
	{
		sendnotice(client, "Not enough parameters. Type /RMTKL for help.");
		return;
	}

	uhmask = parv[1];
	types = parv[2];
	commentmask = NULL;
	skipperm = 0;
	silent = 0;
	count = 0;
	snprintf(broadcast, sizeof(broadcast), ":%s RMTKL %s %s", client->name, types, uhmask);

	// Check for optionals
	if (IsParam(3))
	{
		// Comment mask, if specified, always goes third
		if (*parv[3] != '-')
			commentmask = parv[3];
		else
			rmtkl_check_options(parv[3], &skipperm, &silent);
		ircsnprintf(broadcast, sizeof(broadcast), "%s %s", broadcast, parv[3]);
	}
	if (IsParam(4))
	{
		rmtkl_check_options(parv[4], &skipperm, &silent);
		ircsnprintf(broadcast, sizeof(broadcast), "%s %s", broadcast, parv[4]);
	}
	if (IsParam(5))
	{
		rmtkl_check_options(parv[5], &skipperm, &silent);
		ircsnprintf(broadcast, sizeof(broadcast), "%s %s", broadcast, parv[5]);
	}

	// Wildcard resolves to everything but 'F', since spamfilters are a bit special
	if (strchr(types, '*'))
		types = "kzGZs";

	// Make sure the oper actually has the privileges to remove the *-Lines he wants
	if (!IsULine(client))
	{
		for (p = types; *p; p++)
		{
			tkltype = find_TKLType_by_flag(*p);
			if (!tkltype->type)
				continue;

			if (!ValidatePermissionsForPath(tkltype->operpriv, client, NULL, NULL, NULL))
			{
				sendnumeric(client, ERR_NOPRIVILEGES);
				return;
			}
		}
	}

	// Broadcast the command to other servers *before* we proceed with removal
	sendto_server(NULL, 0, 0, NULL, "%s", broadcast);

	// Loop over all supported types
	for (tkltype = tkl_types; tkltype->type; tkltype++) {
		if (!strchr(types, tkltype->flag))
			continue;

		// Loop over all TKL entries, first try the ones in the hash table
		tklchar = tkl_typetochar(tkltype->type);
		tklindex = tkl_ip_hash_type(tklchar);
		if (tklindex >= 0)
		{
			for (tklindex2 = 0; tklindex2 < TKLIPHASHLEN2; tklindex2++)
			{
				for (tkl = tklines_ip_hash[tklindex][tklindex2]; tkl; tkl = next)
				{
					next = tkl->next;
					count += rmtkl_tryremove(client, tkltype, tkl, uhmask, commentmask, skipperm, silent);
				}
			}
		}

		// Then the regular *-Lines (not an else because certain TKLs might have a hash as well as a plain linked list)
		tklindex = tkl_hash(tklchar);
		for (tkl = tklines[tklindex]; tkl; tkl = next)
		{
			next = tkl->next;
			count += rmtkl_tryremove(client, tkltype, tkl, uhmask, commentmask, skipperm, silent);
		}
	}

	unreal_log(ULOG_INFO, "tkl", "RMTKL_COMMAND", client,
	           "[rmtkl] $client removed $tkl_removed_count TKLine(s) using /RMTKL",
	           log_data_integer("tkl_removed_count", count));
}
