/*
 * SPDX-License-Identifier: ISC
 * SPDX-URL: https://spdx.org/licenses/ISC.html
 *
 * Copyright (C) 2005-2009 William Pitcock <nenolod -at- nenolod.net>
 * Copyright (C) 2017 Austin Ellis <siniStar@IRC4Fun.net>
 *
 * Allows requesting a vhost for a nick/account
 */

#include <atheme.h>
#include "hostserv.h"

struct hsrequest
{
	char *nick;
	char *vhost;
	time_t vhost_ts;
	char *creator;
};

static bool no_subsequent_requests;
static bool request_per_nick;
static struct service *hostsvs;

static unsigned int ratelimit_count = 0;
static time_t ratelimit_firsttime = 0;

static mowgli_list_t hs_reqlist;
static char *groupmemo;

static void
write_hsreqdb(struct database_handle *db)
{
	mowgli_node_t *n;

	MOWGLI_ITER_FOREACH(n, hs_reqlist.head)
	{
		struct hsrequest *l = n->data;

		db_start_row(db, "HR");
		db_write_word(db, l->nick);
		db_write_word(db, l->vhost);
		db_write_time(db, l->vhost_ts);
		db_write_word(db, l->creator);
		db_commit_row(db);
	}
}

static void
db_h_hr(struct database_handle *db, const char *type)
{
	const char *nick = db_sread_word(db);
	const char *vhost = db_sread_word(db);
	time_t vhost_ts = db_sread_time(db);
	const char *creator = db_sread_word(db);

	struct hsrequest *const l = smalloc(sizeof *l);
	l->nick = sstrdup(nick);
	l->vhost = sstrdup(vhost);
	l->vhost_ts = vhost_ts;
	l->creator = sstrdup(creator);
	mowgli_node_add(l, mowgli_node_create(), &hs_reqlist);
}

static void
remove_request_from_list(mowgli_node_t *const restrict n)
{
	struct hsrequest *const l = n->data;

	sfree(l->nick);
	sfree(l->vhost);
	sfree(l->creator);
	sfree(l);

	mowgli_node_delete(n, &hs_reqlist);
	mowgli_node_free(n);
}

static void
nick_drop_request(struct hook_user_req *hdata)
{
	mowgli_node_t *n;
	struct hsrequest *l;

	MOWGLI_ITER_FOREACH(n, hs_reqlist.head)
	{
		l = n->data;
		if (!irccasecmp(l->nick, hdata->mn->nick))
		{
			slog(LG_REGISTER, "VHOSTREQ:DROPNICK: \2%s\2 \2%s\2", l->nick, l->vhost);

			remove_request_from_list(n);
			return;
		}
	}
}

static void
account_drop_request(struct myuser *mu)
{
	mowgli_node_t *n;
	struct hsrequest *l;

	MOWGLI_ITER_FOREACH(n, hs_reqlist.head)
	{
		l = n->data;
		if (!irccasecmp(l->nick, entity(mu)->name))
		{
			slog(LG_REGISTER, "VHOSTREQ:DROPACCOUNT: \2%s\2 \2%s\2", l->nick, l->vhost);

			remove_request_from_list(n);
			return;
		}
	}
}

static void
account_delete_request(struct myuser *mu)
{
	mowgli_node_t *n;
	struct hsrequest *l;

	MOWGLI_ITER_FOREACH(n, hs_reqlist.head)
	{
		l = n->data;
		if (!irccasecmp(l->nick, entity(mu)->name))
		{
			slog(LG_REGISTER, "VHOSTREQ:EXPIRE: \2%s\2 \2%s\2", l->nick, l->vhost);

			remove_request_from_list(n);
			return;
		}
	}
}

static void
osinfo_hook(struct sourceinfo *si)
{
	return_if_fail(si != NULL);

	// Can't think of a better way to phrase this, feel free to fix if you can.
	command_success_nodata(si, _("Requested vHosts will be per-nick: %s"), request_per_nick ? _("Yes") : _("No"));
}

static void ATHEME_FATTR_PRINTF(2, 3)
send_group_memo(struct sourceinfo *si, const char *memo, ...)
{
	struct service *msvs;
	va_list va;
	char buf[BUFSIZE];

	return_if_fail(si != NULL);
	return_if_fail(memo != NULL);

	va_start(va, memo);
	vsnprintf(buf, BUFSIZE, memo, va);
	va_end(va);

	if ((msvs = service_find("memoserv")) == NULL)
		return;
	else
	{
		char cmdbuf[BUFSIZE];

		mowgli_strlcpy(cmdbuf, groupmemo, BUFSIZE);
		mowgli_strlcat(cmdbuf, " ", BUFSIZE);
		mowgli_strlcat(cmdbuf, buf, BUFSIZE);

		command_exec_split(msvs, si, "SEND", cmdbuf, msvs->commands);
	}
}

// REQUEST <host>
static void
hs_cmd_request(struct sourceinfo *si, int parc, char *parv[])
{
	char *host = parv[0];
	const char *target;
	struct mynick *mn;
	struct myentity *mt;
	struct myuser *mu;
	struct myentity_iteration_state state;
	char buf[BUFSIZE], strfbuf[BUFSIZE];
	struct metadata *md, *md_timestamp, *md_assigner;
	mowgli_node_t *n;
	struct hsrequest *l;
	struct hook_host_request hdata;
	int matches = 0;

	if (!host)
	{
		command_fail(si, fault_needmoreparams, STR_INSUFFICIENT_PARAMS, "REQUEST");
		command_fail(si, fault_needmoreparams, _("Syntax: REQUEST <vhost>"));
		return;
	}

	if ((unsigned int)(CURRTIME - ratelimit_firsttime) > config_options.ratelimit_period)
	{
		ratelimit_count = 0;
		ratelimit_firsttime = CURRTIME;
	}

	if (metadata_find(si->smu, "private:restrict:setter"))
	{
		command_fail(si, fault_noprivs, _("You have been restricted from requesting vhosts by network staff."));
		return;
	}

	md = metadata_find(si->smu, "private:usercloak-timestamp");

	if (md != NULL && config_options.vhost_change > 0 && (atoi(md->value) + config_options.vhost_change) > CURRTIME)
	{
		command_fail(si, fault_noprivs, _("You must wait at least \2%u\2 days between changes to your vHost."),
			(config_options.vhost_change / SECONDS_PER_DAY));
		return;
	}

	if (request_per_nick)
	{
		target = si->su != NULL ? si->su->nick : "?";
		mn = mynick_find(target);
		if (mn == NULL)
		{
			command_fail(si, fault_nosuch_target, STR_IS_NOT_REGISTERED, target);
			return;
		}
		if (mn->owner != si->smu)
		{
			command_fail(si, fault_noprivs, _("Nick \2%s\2 is not registered to your account."), mn->nick);
			return;
		}

		snprintf(buf, sizeof buf, "private:usercloak:%s", target);
		md = metadata_find(si->smu, buf);
		if (md != NULL && !strcmp(host, md->value))
		{
			command_success_nodata(si, _("\2%s\2 is already the active vhost for your nick."), host);
			return;
		}
	}
	else
	{
		target = entity(si->smu)->name;

		md = metadata_find(si->smu, "private:usercloak");
		if (md != NULL && !strcmp(host, md->value))
		{
			command_success_nodata(si, _("\2%s\2 is already the active vhost for your account."), host);
			return;
		}
	}

	if (!check_vhost_validity(si, host))
		return;

	MYENTITY_FOREACH_T(mt, &state, ENT_USER)
	{
		mu = user(mt);
		md = metadata_find(mu, "private:usercloak");
		if (md != NULL && !match(host, md->value))
		{
			command_fail(si, fault_noprivs, _("\2%s\2 is already assigned to another user.  You will need to request a \2different\2 vhost or see network staff."), host);
			logcommand(si, CMDLOG_REQUEST, "REQUEST:FAILED: \2%s\2 is already assigned to another user.", host);
				return;
		}
		MOWGLI_ITER_FOREACH(n, mu->nicks.head)
		{
			snprintf(buf, BUFSIZE, "%s:%s", "private:usercloak", ((struct mynick *)(n->data))->nick);
			md = metadata_find(mu, buf);
			if (md == NULL)
				continue;
			if (!match(host, md->value))
			{
			command_fail(si, fault_noprivs, _("\2%s\2 is already assigned to another user.  You will need to request a \2different\2 vhost or see network staff."), host);
			logcommand(si, CMDLOG_REQUEST, "REQUEST:FAILED: \2%s\2 is already assigned to another user.", host);
				return;
			}
		}
	}

	hdata.host = host;
	hdata.si = si;
	hdata.approved = 0;

	/* keep target and si seperate so modules that use the hook
	 * can see if it's an account or nick requesting the host
	 */
	hdata.target = target;
	hook_call_host_request(&hdata);

	if (hdata.approved != 0)
		return;

	// search for it
	MOWGLI_ITER_FOREACH(n, hs_reqlist.head)
	{
		l = n->data;

		if (!irccasecmp(l->nick, target))
		{
			if (no_subsequent_requests)
			{
				command_fail(si, fault_badparams, _("You already have an outstanding vhost request. "
				                                    "Please wait for network staff to approve or "
				                                    "reject it."));
				return;
			}
			if (!strcmp(host, l->vhost))
			{
				command_success_nodata(si, _("You have already requested vhost \2%s\2."), host);
				return;
			}
			if (ratelimit_count > config_options.ratelimit_uses && !has_priv(si, PRIV_FLOOD))
			{
				command_fail(si, fault_toomany, _("The system is currently too busy to process your vHost request, please try again later."));
				slog(LG_INFO, "VHOSTREQUEST:THROTTLED: %s", si->su->nick);
				return;
			}
			sfree(l->vhost);
			l->vhost = sstrdup(host);
			l->vhost_ts = CURRTIME;

			command_success_nodata(si, _("You have requested vhost \2%s\2."), host);

			if (groupmemo != NULL)
				send_group_memo(si, "[auto memo] Please review \2%s\2 for me!", host);

			logcommand(si, CMDLOG_REQUEST, "REQUEST: \2%s\2", host);
			if (config_options.ratelimit_uses && config_options.ratelimit_period)
				ratelimit_count++;
			return;
		}
	}

	if (ratelimit_count > config_options.ratelimit_uses && !has_priv(si, PRIV_FLOOD))
	{
		command_fail(si, fault_toomany, _("The system is currently too busy to process your vHost request, please try again later."));
		slog(LG_INFO, "VHOSTREQUEST:THROTTLED: %s", si->su->nick);
		return;
	}

	l = smalloc(sizeof *l);
	l->nick = sstrdup(target);
	l->vhost = sstrdup(host);
	l->vhost_ts = CURRTIME;;
	l->creator = sstrdup(get_source_name(si));

	n = mowgli_node_create();
	mowgli_node_add(l, n, &hs_reqlist);

	command_success_nodata(si, _("You have requested vhost \2%s\2."), host);

        if (groupmemo != NULL)
                send_group_memo(si, "[auto memo] Please review \2%s\2 for me!", host);

	logcommand(si, CMDLOG_REQUEST, "REQUEST: \2%s\2", host);
	if (config_options.ratelimit_uses && config_options.ratelimit_period)
		ratelimit_count++;
	return;
}

// ACTIVATE <nick>
static void
hs_cmd_activate(struct sourceinfo *si, int parc, char *parv[])
{
	char *nick = parv[0];
	struct user *u;
	char buf[BUFSIZE];
	struct hsrequest *l;
	mowgli_node_t *n, *tn;

	if (!nick)
	{
		command_fail(si, fault_needmoreparams, STR_INSUFFICIENT_PARAMS, "ACTIVATE");
		command_fail(si, fault_needmoreparams, _("Syntax: ACTIVATE <nick>"));
		return;
	}

	MOWGLI_ITER_FOREACH_SAFE(n, tn, hs_reqlist.head)
	{
		l = n->data;

		if (!irccasecmp(l->nick, nick))
		{
			if ((u = user_find_named(nick)) != NULL)
				notice(si->service->nick, u->nick, "[auto memo] Your requested vhost \2%s\2 for nick \2%s\2 has been approved.", l->vhost, nick);

			// VHOSTNICK command below will generate snoop
			logcommand(si, CMDLOG_REQUEST, "ACTIVATE: \2%s\2 for \2%s\2", l->vhost, nick);
			snprintf(buf, BUFSIZE, "%s %s", l->nick, l->vhost);
			remove_request_from_list(n);

			command_exec_split(si->service, si, request_per_nick ? "VHOSTNICK" : "VHOST", buf, si->service->commands);
			return;
		}

		if (!irccasecmp("*", nick))
		{
			if ((u = user_find_named(l->nick)) != NULL)
				notice(si->service->nick, u->nick, "[auto memo] Your requested vhost \2%s\2 for nick \2%s\2 has been approved.", l->vhost, l->nick);

			// VHOSTNICK command below will generate snoop
			logcommand(si, CMDLOG_REQUEST, "ACTIVATE: \2%s\2 for \2%s\2", l->vhost, l->nick);
			snprintf(buf, BUFSIZE, "%s %s", l->nick, l->vhost);
			remove_request_from_list(n);

			command_exec_split(si->service, si, request_per_nick ? "VHOSTNICK" : "VHOST", buf, si->service->commands);

			if (hs_reqlist.count == 0)
				return;
		}

	}

	command_success_nodata(si, _("Nick \2%s\2 not found in vhost request database."), nick);
}

// REJECT <nick>
static void
hs_cmd_reject(struct sourceinfo *si, int parc, char *parv[])
{
	char *nick = parv[0];
	char *reason = parv[1];
	struct user *u;
	char buf[BUFSIZE];
	struct hsrequest *l;
	mowgli_node_t *n, *tn;
	bool silent = false;

	if (!nick)
	{
		command_fail(si, fault_needmoreparams, STR_INSUFFICIENT_PARAMS, "REJECT");
		command_fail(si, fault_needmoreparams, _("Syntax: REJECT <nick> [reason]"));
		return;
	}

	if (reason && strlen(reason) > 150)
	{
		command_fail(si, fault_badparams, _("Reason too long. It must be 150 characters or less."));
		return;
	}

	if (reason && strcasecmp(reason, "SILENT") == 0)
		silent = true;

	MOWGLI_ITER_FOREACH_SAFE(n, tn, hs_reqlist.head)
	{
		struct service *svs;

		l = n->data;
		if (!irccasecmp(l->nick, nick))
		{
			if (! silent && (svs = service_find("memoserv")) != NULL)
			{
				if (reason)
					snprintf(buf, BUFSIZE, "%s [auto memo] Your requested vhost \2%s\2 for nick \2%s\2 has been rejected due to: %s", nick, l->vhost, nick, reason);
				else
					snprintf(buf, BUFSIZE, "%s [auto memo] Your requested vhost \2%s\2 for nick \2%s\2 has been rejected.", nick, l->vhost, nick);

				command_exec_split(svs, si, "SEND", buf, svs->commands);
			}
			else if (! silent && (u = user_find_named(nick)) != NULL)
			{
				if (reason)
					notice(si->service->nick, u->nick, "[auto memo] Your requested vhost \2%s\2 for nick \2%s\2 has been rejected due to: %s", l->vhost, nick, reason);
				else
					notice(si->service->nick, u->nick, "[auto memo] Your requested vhost \2%s\2 for nick \2%s\2 has been rejected.", l->vhost, nick);
			}

			if (reason && ! silent)
				logcommand(si, CMDLOG_REQUEST, "REJECT: \2%s\2 for \2%s\2, Reason: \2%s\2", l->vhost, nick, reason);
			else
				logcommand(si, CMDLOG_REQUEST, "REJECT: \2%s\2 for \2%s\2", l->vhost, nick);

			remove_request_from_list(n);
			return;
		}

		if (!irccasecmp("*", nick))
		{
			if (! silent && (svs = service_find("memoserv")) != NULL)
			{
				if (reason)
					snprintf(buf, BUFSIZE, "%s [auto memo] Your requested vhost \2%s\2 for nick \2%s\2 has been rejected due to: %s", nick, l->vhost, nick, reason);
				else
					snprintf(buf, BUFSIZE, "%s [auto memo] Your requested vhost \2%s\2 for nick \2%s\2 has been rejected.", nick, l->vhost, nick);

				command_exec_split(svs, si, "SEND", buf, svs->commands);
			}
			else if (! silent && (u = user_find_named(l->nick)) != NULL)
			{
				if (reason)
					notice(si->service->nick, u->nick, "[auto memo] Your requested vhost \2%s\2 for nick \2%s\2 has been rejected due to: %s", l->vhost, nick, reason);
				else
					notice(si->service->nick, u->nick, "[auto memo] Your requested vhost \2%s\2 for nick \2%s\2 has been rejected.", l->vhost, nick);
			}

			if (reason && ! silent)
				logcommand(si, CMDLOG_REQUEST, "REJECT: \2%s\2 for \2%s\2, Reason: \2%s\2", l->vhost, l->nick, reason);
			else
				logcommand(si, CMDLOG_REQUEST, "REJECT: \2%s\2 for \2%s\2", l->vhost, l->nick);

			remove_request_from_list(n);

			if (hs_reqlist.count == 0)
				return;
		}
	}

	command_success_nodata(si, _("Nick \2%s\2 not found in vhost request database."), nick);
}

// WAITING
static void
hs_cmd_waiting(struct sourceinfo *si, int parc, char *parv[])
{
	struct hsrequest *l;
	mowgli_node_t *n;
	char buf[BUFSIZE];
	struct tm *tm;

	MOWGLI_ITER_FOREACH(n, hs_reqlist.head)
	{
		l = n->data;

		tm = localtime(&l->vhost_ts);
		strftime(buf, BUFSIZE, TIME_FORMAT, tm);
		command_success_nodata(si, _("Nick: \2%s\2, vHost: \2%s\2 (%s - %s)"),
			l->nick, l->vhost, l->creator, buf);
	}
	command_success_nodata(si, _("End of list."));
	logcommand(si, CMDLOG_GET, "WAITING");
}

static struct command hs_request = {
	.name           = "REQUEST",
	.desc           = N_("Requests new virtual hostname for current nick."),
	.access         = AC_AUTHENTICATED,
	.maxparc        = 2,
	.cmd            = &hs_cmd_request,
	.help           = { .path = "hostserv/request" },
};

static struct command hs_waiting = {
	.name           = "WAITING",
	.desc           = N_("Lists vhosts currently waiting for activation."),
	.access         = PRIV_USER_VHOST,
	.maxparc        = 1,
	.cmd            = &hs_cmd_waiting,
	.help           = { .path = "hostserv/waiting" },
};

static struct command hs_reject = {
	.name           = "REJECT",
	.desc           = N_("Reject the requested vhost for the given nick."),
	.access         = PRIV_USER_VHOST,
	.maxparc        = 2,
	.cmd            = &hs_cmd_reject,
	.help           = { .path = "hostserv/reject" },
};

static struct command hs_activate = {
	.name           = "ACTIVATE",
	.desc           = N_("Activate the requested vhost for a given nick."),
	.access         = PRIV_USER_VHOST,
	.maxparc        = 2,
	.cmd            = &hs_cmd_activate,
	.help           = { .path = "hostserv/activate" },
};

static void
mod_init(struct module *const restrict m)
{
	if (!module_find_published("backend/opensex"))
	{
		slog(LG_INFO, "Module %s requires use of the OpenSEX database backend, refusing to load.", m->name);
		m->mflags |= MODFLAG_FAIL;
		return;
	}

	MODULE_TRY_REQUEST_DEPENDENCY(m, "hostserv/main")

	hostsvs = service_find("hostserv");

	hook_add_user_drop(account_drop_request);
	hook_add_nick_ungroup(nick_drop_request);
	hook_add_myuser_delete(account_delete_request);
	hook_add_operserv_info(osinfo_hook);
	hook_add_db_write(write_hsreqdb);

	add_dupstr_conf_item("REGGROUP", &hostsvs->conf_table, 0, &groupmemo, NULL);

	db_register_type_handler("HR", db_h_hr);

 	service_named_bind_command("hostserv", &hs_request);
	service_named_bind_command("hostserv", &hs_waiting);
	service_named_bind_command("hostserv", &hs_reject);
	service_named_bind_command("hostserv", &hs_activate);

	add_bool_conf_item("NO_SUBSEQUENT_REQUESTS", &hostsvs->conf_table, 0, &no_subsequent_requests, false);
	add_bool_conf_item("REQUEST_PER_NICK", &hostsvs->conf_table, 0, &request_per_nick, false);

	m->mflags |= MODFLAG_DBHANDLER;
}

static void
mod_deinit(const enum module_unload_intent ATHEME_VATTR_UNUSED intent)
{

}

SIMPLE_DECLARE_MODULE_V1("hostserv/request", MODULE_UNLOAD_CAPABILITY_NEVER)
