/*
 * Robust channel forwarding system
 * (C) Copyright 2019 Syzop, 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"

#define MAX_EB_LEN 128 // Max extban length

ModuleHeader MOD_HEADER = {
	"chanmodes/link",
	"5.0",
	"Channel Mode +L",
	"UnrealIRCd Team",
	"unrealircd-6",
};

Cmode_t EXTMODE_LINK = 0L;

typedef struct {
	char linked[CHANNELLEN + 1];
} aModeLEntry;

typedef enum {
	LINKTYPE_BAN = 1, // +b
	LINKTYPE_INVITE = 2, // +i
	LINKTYPE_OPER = 3, // +O
	LINKTYPE_SECURE = 4, // +z
	LINKTYPE_REG = 5, // +R
	LINKTYPE_LIMIT = 6, // +l
	LINKTYPE_BADKEY = 7, // +k
} linkType;

int cmodeL_is_ok(Client *client, Channel *channel, char mode, const char *para, int type, int what);
void *cmodeL_put_param(void *r_in, const char *param);
const char *cmodeL_get_param(void *r_in);
const char *cmodeL_conv_param(const char *param_in, Client *client, Channel *channel);
int cmodeL_free_param(void *r, int soft);
void *cmodeL_dup_struct(void *r_in);
int cmodeL_sjoin_check(Channel *channel, void *ourx, void *theirx);

int extban_link_syntax(Client *client, int checkt, const char *reason);
int extban_link_is_ok(BanContext *b);
const char *extban_link_conv_param(BanContext *b, Extban *extban);
int link_doforward(Client *client, Channel *channel, const char *linked, linkType linktype);
int link_pre_localjoin_cb(Client *client, Channel *channel, const char *key);

MOD_INIT()
{
	CmodeInfo req;
	ExtbanInfo req_extban;

	MARK_AS_OFFICIAL_MODULE(modinfo);

	memset(&req, 0, sizeof(req));
	req.paracount = 1;
	req.is_ok = cmodeL_is_ok;
	req.letter = 'L';
	req.unset_with_param = 1; /* Oh yeah, we are special! */
	req.put_param = cmodeL_put_param;
	req.get_param = cmodeL_get_param;
	req.conv_param = cmodeL_conv_param;
	req.free_param = cmodeL_free_param;
	req.dup_struct = cmodeL_dup_struct;
	req.sjoin_check = cmodeL_sjoin_check;
	CmodeAdd(modinfo->handle, req, &EXTMODE_LINK);

	memset(&req_extban, 0, sizeof(ExtbanInfo));
	req_extban.letter = 'f';
	req_extban.name = "forward";
	req_extban.is_ok = extban_link_is_ok;
	req_extban.conv_param = extban_link_conv_param;
	req_extban.options = EXTBOPT_ACTMODIFIER;
	if (!ExtbanAdd(modinfo->handle, req_extban))
	{
		config_error("could not register extended ban type");
		return MOD_FAILED;
	}

	HookAdd(modinfo->handle, HOOKTYPE_PRE_LOCAL_JOIN, -99, link_pre_localjoin_cb);
	return MOD_SUCCESS;
}

MOD_LOAD()
{
	return MOD_SUCCESS;
}


MOD_UNLOAD()
{
	return MOD_SUCCESS;
}

int cmodeL_is_ok(Client *client, Channel *channel, char mode, const char *para, int type, int what)
{
	if ((type == EXCHK_ACCESS) || (type == EXCHK_ACCESS_ERR))
	{
		if (IsUser(client) && check_channel_access(client, channel, "oaq"))
			return EX_ALLOW;
		if (type == EXCHK_ACCESS_ERR) /* can only be due to being halfop */
			sendnumeric(client, ERR_NOTFORHALFOPS, 'L');
		return EX_DENY;
	} else
	if (type == EXCHK_PARAM)
	{
		/* Check parameter.. syntax is +L #channel */
		if (strchr(para, ','))
			return EX_DENY; /* multiple channels not permitted */
		if (!valid_channelname(para))
		{
			if (MyUser(client))
				sendnumeric(client, ERR_NOSUCHCHANNEL, para);
			return EX_DENY;
		}

		if (find_channel(para) == channel)
		{
			if (MyUser(client))
				sendnumeric(client, ERR_CANNOTCHANGECHANMODE, 'L',
					   "a channel cannot be linked to itself");
			return EX_DENY;
		}
		return EX_ALLOW;
	}

	/* fallthrough -- should not be used */
	return EX_DENY;
}

void *cmodeL_put_param(void *r_in, const char *param)
{
	aModeLEntry *r = (aModeLEntry *)r_in;

	if (!r)
	{
		/* Need to create one */
		r = safe_alloc(sizeof(aModeLEntry));
	}
	strlcpy(r->linked, param, sizeof(r->linked));
	return (void *)r;
}

const char *cmodeL_get_param(void *r_in)
{
	aModeLEntry *r = (aModeLEntry *)r_in;
	static char retbuf[CHANNELLEN+1];

	if (!r)
		return NULL;

	strlcpy(retbuf, r->linked, sizeof(retbuf));
	return retbuf;
}

/** Convert parameter to something proper.
 * NOTE: client may be NULL
 */
const char *cmodeL_conv_param(const char *param, Client *client, Channel *channel)
{
	if (!valid_channelname(param))
		return NULL;

	return param;
}

int cmodeL_free_param(void *r, int soft)
{
	safe_free(r);
	return 0;
}

void *cmodeL_dup_struct(void *r_in)
{
	aModeLEntry *r = (aModeLEntry *)r_in;
	aModeLEntry *w = safe_alloc(sizeof(aModeLEntry));

	memcpy(w, r, sizeof(aModeLEntry));
	return (void *)w;
}

int cmodeL_sjoin_check(Channel *channel, void *ourx, void *theirx)
{
	aModeLEntry *our = (aModeLEntry *)ourx;
	aModeLEntry *their = (aModeLEntry *)theirx;

	if (!strcmp(our->linked, their->linked))
		return EXSJ_SAME;
	if (strcmp(our->linked, their->linked) > 0)
		return EXSJ_WEWON;
	return EXSJ_THEYWON;
}

int extban_link_syntax(Client *client, int checkt, const char *reason)
{
	if (MyUser(client) && (checkt == EXBCHK_PARAM))
	{
		sendnotice(client, "Error when setting ban: %s", reason);
		sendnotice(client, "  Syntax: +b ~f:#channel:mask");
		sendnotice(client, "Examples:");
		sendnotice(client, "  +b ~f:#public:*!*@badisp.org");
		sendnotice(client, "  +b ~f:#public:~c:#badchannel");
		sendnotice(client, "Multiple channels are not supported");
		sendnotice(client, "Valid masks are nick!user@host or another extban type such as ~a, ~c, ~S, etc");
	}
	return 0; // Reject ban
}

int extban_link_is_ok(BanContext *b)
{
	static char paramtmp[MAX_EB_LEN + 1];
	char *matchby; // Matching method, such as 'n!u@h'
	char *chan;

	// Always permit deletion
	if (b->what == MODE_DEL)
		return 1;

	if (b->ban_type != EXBTYPE_BAN)
	{
		if (b->is_ok_check == EXBCHK_PARAM)
			sendnotice(b->client, "Ban type ~f only works with bans (+b) and not with exceptions or invex (+e/+I)");
		return 0; // Reject
	}

	strlcpy(paramtmp, b->banstr, sizeof(paramtmp)); // Work on a size-truncated copy
	chan = paramtmp;
	matchby = strchr(paramtmp, ':');
	if (!matchby || !matchby[1])
		return extban_link_syntax(b->client, b->is_ok_check, "Invalid syntax");
	*matchby++ = '\0';

	if (*chan != '#' || strchr(b->banstr, ','))
		return extban_link_syntax(b->client, b->is_ok_check, "Invalid channel");

	b->banstr = matchby;
	if (extban_is_ok_nuh_extban(b) == 0)
		return extban_link_syntax(b->client, b->is_ok_check, "Invalid matcher");

	return 1; // Is ok
}

const char *extban_link_conv_param(BanContext *b, Extban *extban)
{
	static char retbuf[MAX_EB_LEN + 1];
	char paramtmp[MAX_EB_LEN + 1];
	char tmpmask[MAX_EB_LEN + 1];
	char *matchby; // Matching method, such as 'n!u@h'
	const char *newmask; // Cleaned matching method, such as 'n!u@h'
	char *chan;

	strlcpy(paramtmp, b->banstr, sizeof(paramtmp)); // Work on a size-truncated copy
	chan = paramtmp;
	matchby = strchr(paramtmp, ':');
	if (!matchby || !matchby[1])
		return NULL;
	*matchby++ = '\0';

	if (!valid_channelname(chan))
		return NULL;

	b->banstr = matchby;
	newmask = extban_conv_param_nuh_or_extban(b, extban);
	if (BadPtr(newmask))
		return NULL;

	snprintf(retbuf, sizeof(retbuf), "%s:%s", chan, newmask);
	return retbuf;
}

int link_doforward(Client *client, Channel *channel, const char *linked, linkType type)
{
	char linked_channel_buffer[CHANNELLEN+1];
	char desc[64];
	const char *parv[3];

	switch (type)
	{
		case LINKTYPE_BAN:
			strncpy(desc, "you are banned", sizeof(desc));
			break;

		case LINKTYPE_INVITE:
			strncpy(desc, "channel is invite only", sizeof(desc));
			break;

		case LINKTYPE_OPER:
			strncpy(desc, "channel is oper only", sizeof(desc));
			break;

		case LINKTYPE_SECURE:
			strncpy(desc, "channel requires a secure connection", sizeof(desc));
			break;

		case LINKTYPE_REG:
			strncpy(desc, "channel requires registration", sizeof(desc));
			break;

		case LINKTYPE_LIMIT:
			strncpy(desc, "channel has become full", sizeof(desc));
			break;

		case LINKTYPE_BADKEY:
			strncpy(desc, "invalid channel key", sizeof(desc));
			break;

		default:
			strncpy(desc, "no reason specified", sizeof(desc));
			break;
	}

	sendto_one(client, NULL,
	           ":%s %d %s %s %s :[Link] Cannot join channel %s (%s) -- transferring you to %s",
	           me.name, ERR_LINKCHANNEL, client->name, channel->name, linked,
	           channel->name, desc, linked);

	strlcpy(linked_channel_buffer, linked, sizeof(linked_channel_buffer));
	parv[0] = client->name;
	parv[1] = linked_channel_buffer;
	parv[2] = NULL;

	do_join(client, 2, parv);

	return HOOK_DENY; // Original channel join = ignored
}

int link_pre_localjoin_cb(Client *client, Channel *channel, const char *key)
{
	const char *linked;
	int canjoin;
	char *error = NULL;

	// User might already be on this channel, let's also exclude any possible services bots early
	if (IsULine(client) || find_membership_link(client->user->channel, channel))
		return HOOK_CONTINUE;

	// Extbans take precedence over +L #channel and other restrictions,
	// only /INVITE from chanop bypasses:
	if (!is_invited(client, channel))
	{
		Ban *ban;
		BanContext *b = safe_alloc(sizeof(BanContext));
		char bantmp[MAX_EB_LEN + 1];
		char *banchan;
		char *banmask;

		b->client = client;
		b->channel = channel;
		b->ban_check_types = BANCHK_JOIN;
		b->ban_type = EXBTYPE_BAN;

		for (ban = channel->banlist; ban; ban = ban->next)
		{
			if (!strncmp(ban->banstr, "~f:", 3))
			{
				strlcpy(bantmp, ban->banstr + 3, sizeof(bantmp));
			} else
			if (!strncmp(ban->banstr, "~forward:", 9))
			{
				strlcpy(bantmp, ban->banstr + 9, sizeof(bantmp));
			} else
			if (!strncmp(ban->banstr, "~t:", 3))
			{
				/* A timed ban, but is it for us? Need to parse a little:
				 * ~t:dddd:~f:...
				 */
				char *p = strchr(ban->banstr + 3, ':');
				if (p && !strncmp(p, ":~f:", 4))
				{
					strlcpy(bantmp, p + 4, sizeof(bantmp));
				} else
				if (p && !strncmp(p, ":~forward:", 10))
				{
					strlcpy(bantmp, p + 10, sizeof(bantmp));
				} else {
					/* Not for us - some other ~t ban */
					continue;
				}
			} else
			if (!strncmp(ban->banstr, "~time:", 6))
			{
				/* A timed ban, but is it for us? Need to parse a little:
				 * ~t:dddd:~f:...
				 */
				char *p = strchr(ban->banstr + 6, ':');
				if (p && !strncmp(p, ":~f:", 4))
				{
					strlcpy(bantmp, p + 4, sizeof(bantmp));
				} else
				if (p && !strncmp(p, ":~forward:", 10))
				{
					strlcpy(bantmp, p + 10, sizeof(bantmp));
				} else {
					/* Not for us - some other ~t ban */
					continue;
				}
			} else
			{
				/* Not for us */
				continue;
			}
			banchan = bantmp;
			banmask = strchr(bantmp, ':');
			if (!banmask || !banmask[1])
				continue;
			*banmask++ = '\0';

			b->banstr = banmask;
			if (ban_check_mask(b))
			{
				/* Forward ban matched, now check for +e */
				Ban *ex = NULL;
				b->ban_type = EXBTYPE_EXCEPT;
				for (ex = channel->exlist; ex; ex = ex->next)
				{
					b->banstr = ex->banstr;
					if (ban_check_mask(b))
					{
						/* except matched, break inner loop */
						break;
					}
				}
				if (ex == NULL)
				{
					/* A ~forward ban matched, go for it.. */
					safe_free(b);
					return link_doforward(client, channel, banchan, LINKTYPE_BAN);
				} else {
					/* Break the outer loop as well: the user is exempt,
					 * so it makes no sense to check other bans anymore.
					 * no "safe_free(b);" here because that is taken
					 * care of further down.
					 */
					break;
				}
			}
		}

		safe_free(b);
	}

	// Either +L is not set, or it is set but the parameter isn't stored somehow
	if (!(channel->mode.mode & EXTMODE_LINK) || !(linked = cm_getparameter(channel, 'L')))
		return HOOK_CONTINUE;

	// can_join() actually returns 0 if we *can* join a channel, so we don't need to bother checking any further conditions
	if (!(canjoin = can_join(client, channel, key, &error)))
		return HOOK_CONTINUE;

	// Oper only channel
	if (has_channel_mode(channel, 'O') && !IsOper(client))
		return link_doforward(client, channel, linked, LINKTYPE_OPER);

	// TLS connected users only
	if (has_channel_mode(channel, 'z') && !IsSecureConnect(client))
		return link_doforward(client, channel, linked, LINKTYPE_SECURE);

	// Registered/identified users only
	if (has_channel_mode(channel, 'R') && !IsLoggedIn(client))
		return link_doforward(client, channel, linked, LINKTYPE_REG);

	// For a couple of conditions we can use the return value from can_join() =]
	switch(canjoin) {
		// Any ban other than our own ~f: extban
		case ERR_BANNEDFROMCHAN:
			return link_doforward(client, channel, linked, LINKTYPE_BAN);

		// Invite only
		case ERR_INVITEONLYCHAN:
			return link_doforward(client, channel, linked, LINKTYPE_INVITE);

		// User limit
		case ERR_CHANNELISFULL:
			return link_doforward(client, channel, linked, LINKTYPE_LIMIT);

		// Specified channel key doesn't match
		case ERR_BADCHANNELKEY:
			return link_doforward(client, channel, linked, LINKTYPE_BADKEY);

		default:
			break;
	}

	// Let any other errors be handled by their respective modules
	return HOOK_CONTINUE;
}
