/* Nickname linking module (4.x compatibility version).
 *
 * IRC Services is copyright (c) 1996-2007 Andrew Church.
 *     E-mail: <achurch@achurch.org>
 * Parts written by Andrew Kempe and others.
 * This program is free but copyrighted software; see the file COPYING for
 * details.
 */

#include "services.h"
#include "modules.h"
#include "conffile.h"
#include "language.h"
#include "commands.h"
#include "modules/operserv/operserv.h"
#include "modules/chanserv/chanserv.h"

#include "nickserv.h"
#include "ns-local.h"

/*************************************************************************/

static Module *module;
static Module *module_nickserv;
static Module *module_chanserv;

/*************************************************************************/

static void do_link(User *u);
static void do_unlink(User *u);
static void do_listlinks(User *u);

static Command cmds[] = {
    { "LINK",     do_link,     NULL,  NICK_HELP_OLD_LINK,     -1,-1 },
    { "UNLINK",   do_unlink,   NULL,  -1,
    		NICK_HELP_OLD_UNLINK, NICK_OPER_HELP_OLD_UNLINK, },
    { "LISTLINKS",do_listlinks,is_services_admin, -1,
		-1, NICK_OPER_HELP_OLD_LISTLINKS },
    { NULL }
};

/*************************************************************************/
/******************************** Imports ********************************/
/*************************************************************************/

static int (*p_check_channel_limit)(NickGroupInfo *ngi, int *max_ret);
static int my_check_channel_limit(NickGroupInfo *ngi, int *max_ret)
{
    if (p_check_channel_limit)
	return p_check_channel_limit(ngi, max_ret);
    else
	return -1;
}
#define check_channel_limit my_check_channel_limit

/*************************************************************************/
/*************************** Command functions ***************************/
/*************************************************************************/

static void do_link(User *u)
{
    char *nick = strtok(NULL, " ");
    char *pass = strtok_remaining();
    NickInfo *ni = u->ni, *target;
    NickGroupInfo *ngi = u->ngi;

    if (readonly && !is_services_admin(u)) {
	notice_lang(s_NickServ, u, NICK_LINK_DISABLED);
	return;
    }

    if (!pass) {
	syntax_error(s_NickServ, u, "LINK", NICK_OLD_LINK_SYNTAX);

    } else if (!ni || !ngi || ngi == NICKGROUPINFO_INVALID) {
	notice_lang(s_NickServ, u, NICK_NOT_REGISTERED);

    } else if (!user_identified(u)) {
	notice_lang(s_NickServ, u, NICK_IDENTIFY_REQUIRED, s_NickServ);

    } else if (!(target = get_nickinfo(nick))) {
	notice_lang(s_NickServ, u, NICK_X_NOT_REGISTERED, nick);

    } else if (target == ni) {
	notice_lang(s_NickServ, u, NICK_OLD_LINK_SAME, nick);

    } else if (target->status & NS_VERBOTEN) {
	notice_lang(s_NickServ, u, NICK_X_FORBIDDEN, nick);

    } else if (!nick_check_password(u, target, pass, "LINK",
				    NICK_LINK_FAILED)) {
	return;

    } else {
	NickGroupInfo *target_ngi = get_ngi(target);
	ChannelInfo *ci;
	User *u2;
	int max, i, n, res;

	/* Make sure the target nickgroup actually exists. */
	if (!target_ngi || target_ngi == NICKGROUPINFO_INVALID) {
	    notice_lang(s_NickServ, u, INTERNAL_ERROR);
	    return;
	}

	/* Make sure the target isn't suspended. */
	if (target_ngi->suspendinfo) {
	    notice_lang(s_NickServ, u, NICK_X_SUSPENDED, nick);
	    return;
	}

	/* Check for exceeding the per-email nick registration limit. */
	if (NSRegEmailMax && target_ngi->email && !is_services_admin(u)
	 && abs(n=count_nicks_with_email(target_ngi->email)) >= NSRegEmailMax
	) {
	    notice_lang(s_NickServ, u, NICK_LINK_TOO_MANY_NICKS, n,
			NSRegEmailMax);
	    return;
	}

	/* Check for exceeding the channel registration limit. */
	target_ngi->channels_count += ngi->channels_count;
	res = check_channel_limit(target_ngi, &max);
	target_ngi->channels_count -= ngi->channels_count;
	if (res >= 0) {
	    notice_lang(s_NickServ, u, NICK_OLD_LINK_TOO_MANY_CHANNELS,
			nick, max);
	    return;
	}

	/* Put each of the old group's nicks in the new group */
	ARRAY_FOREACH (i, ngi->nicks) {
	    NickInfo *ni2 = get_nickinfo_noexpire(ngi->nicks[i]);
	    ARRAY_EXTEND(target_ngi->nicks);
	    strscpy(target_ngi->nicks[target_ngi->nicks_count-1],
		    ngi->nicks[i], NICKMAX);
	    ni2->nickgroup = target_ngi->id;
	    put_nickinfo(ni2);
	}

	/* Append old group's list of owned channels to new group */
	ARRAY_FOREACH (i, ngi->channels) {
	    ARRAY_EXTEND(target_ngi->channels);
	    strscpy(target_ngi->channels[target_ngi->channels_count-1],
		    ngi->channels[i], CHANMAX);
	}

	/* Merge memos */
	if (ngi->memos.memos_count) {
	    int i, num;
	    Memo *memo;
	    if (target_ngi->memos.memos_count) {
		num = 0;
		ARRAY_FOREACH (i, target_ngi->memos.memos) {
		    if (target_ngi->memos.memos[i].number > num)
			num = target_ngi->memos.memos[i].number;
		}
		num++;
		target_ngi->memos.memos =
		    srealloc(target_ngi->memos.memos,
			     sizeof(Memo) * (ngi->memos.memos_count +
					     target_ngi->memos.memos_count));
	    } else {
		num = 1;
		target_ngi->memos.memos =
		    smalloc(sizeof(Memo) * ngi->memos.memos_count);
		target_ngi->memos.memos_count = 0;
	    }
	    memo = target_ngi->memos.memos + target_ngi->memos.memos_count;
	    ARRAY_FOREACH (i, ngi->memos.memos) {
		*memo = ngi->memos.memos[i];
		memo->number = num++;
		memo++;
	    }
	    target_ngi->memos.memos_count += ngi->memos.memos_count;
	    ngi->memos.memos_count = 0;
	    free(ngi->memos.memos);
	    ngi->memos.memos = NULL;
	}

	/* Fix up channel access lists -- this takes a long time */
	for (ci = first_channelinfo(); ci; ci = next_channelinfo()) {
	    ARRAY_FOREACH (i, ci->access) {
		if (ci->access[i].nickgroup == ngi->id)
		    ci->access[i].nickgroup = target_ngi->id;
	    }
	}

	/* Fix up users' NickGroupInfo pointers */
	for (u2 = first_user(); u2; u2 = next_user()) {
	    if (u2->ngi == ngi)
		u2->ngi = target_ngi;
	}

	/* Finally, delete the old nickgroup and store the new one */
	del_nickgroupinfo(ngi);
	free_nickgroupinfo(ngi);
	put_nickgroupinfo(target_ngi);

	/* Tell everybody about it */
	module_log("%s!%s@%s linked nick %s to %s",
		   u->nick, u->username, u->host, u->nick, nick);
	notice_lang(s_NickServ, u, NICK_OLD_LINKED, nick);
	if (readonly)
	    notice_lang(s_NickServ, u, READ_ONLY_MODE);
    }
} /* do_link() */

/*************************************************************************/

static void do_unlink(User *u)
{
    NickInfo *ni;
    NickGroupInfo *ngi = NULL, *new_ngi;
    char *nick = strtok(NULL, " ");
    char *pass = strtok_remaining();
    int msg = -1;  /* -1 = flag meaning "error, abort" */
    char *msgparam[2] = {NULL, NULL};
    int i;

    if (readonly && !is_services_admin(u)) {
	notice_lang(s_NickServ, u, NICK_LINK_DISABLED);
	return;
    }

    if (nick) {
	int is_servadmin = is_services_admin(u);
	ni = get_nickinfo(nick);
	if (!ni) {
	    notice_lang(s_NickServ, u, NICK_X_NOT_REGISTERED, nick);
	} else if (ni->status & NS_VERBOTEN) {
	    notice_lang(s_NickServ, u, NICK_X_FORBIDDEN, ni->nick);
	} else if (!(ngi = get_ngi(ni))) {
	    notice_lang(s_NickServ, u, INTERNAL_ERROR);
	} else if (ngi->nicks_count <= 1) {
	    notice_lang(s_NickServ, u, NICK_UNLINK_NOT_LINKED, nick);
	} else if (!is_servadmin && !pass) {
	    syntax_error(s_NickServ, u, "UNLINK", NICK_OLD_UNLINK_SYNTAX);
	} else if (!is_servadmin &&
		   !nick_check_password(u, ni, pass, "UNLINK",
					NICK_UNLINK_FAILED)) {
	    return;
	} else {
	    msg = NICK_X_UNLINKED;
	    msgparam[0] = ni->nick;
	}
    } else {
	ni = u->ni;
	ngi = u->ngi;
	if (!ni || !ngi || ngi == NICKGROUPINFO_INVALID) {
	    notice_lang(s_NickServ, u, NICK_NOT_REGISTERED);
	} else if (!user_identified(u)) {
	    notice_lang(s_NickServ, u, NICK_IDENTIFY_REQUIRED, s_NickServ);
	} else if (ngi->nicks_count <= 1) {
	    notice_lang(s_NickServ, u, NICK_OLD_UNLINK_NOT_LINKED);
	} else {
	    msg = NICK_OLD_UNLINKED;
	    msgparam[0] = NULL;
	}
    }
    if (msg >= 0) {
	if (msgparam[0])
	    msgparam[1] = ngi_mainnick(ngi);
	else
	    msgparam[0] = ngi_mainnick(ngi);
	/* Set up new nick group */
	new_ngi = new_nickgroupinfo(ni->nick);
	ARRAY_EXTEND(new_ngi->nicks);
	strscpy(new_ngi->nicks[0], ni->nick, NICKMAX);
	strscpy(new_ngi->pass, ngi->pass, PASSMAX);
	if (ngi->url)
	    new_ngi->url = sstrdup(ngi->url);
	if (ngi->email)
	    new_ngi->email = sstrdup(ngi->email);
	if (ngi->info)
	    new_ngi->info = sstrdup(ngi->info);
	new_ngi->authcode = ngi->authcode;
	new_ngi->authset = ngi->authset;
	new_ngi->flags = ngi->flags;
	new_ngi->os_priv = ngi->os_priv;
	new_ngi->channelmax = ngi->channelmax;
	new_ngi->memos.memomax = ngi->memos.memomax;
	new_ngi->language = ngi->language;
	if (ngi->access_count) {
	    new_ngi->access =
		smalloc(sizeof(*new_ngi->access) * ngi->access_count);
	    ARRAY_FOREACH (i, ngi->access) {
		new_ngi->access[i] = sstrdup(ngi->access[i]);
	    }
	}
	u->ngi = new_ngi;
	add_nickgroupinfo(new_ngi);
	/* Update NickInfo with new group ID */
	ni->nickgroup = new_ngi->id;
	put_nickinfo(ni);
	/* Remove nick from old nick group */
	ARRAY_SEARCH_PLAIN(ngi->nicks, ni->nick, irc_stricmp, i);
	if (i < ngi->nicks_count) {
	    ARRAY_REMOVE(ngi->nicks, i);
	} else {
	    module_log("UNLINK %s by %s: nick not found in old nickgroup %u!",
		       ni->nick, u->nick, ngi->id);
	}
	/* Tell people about it */
	notice_lang(s_NickServ, u, msg, msgparam[0], msgparam[1]);
	module_log("%s!%s@%s unlinked nick %s from %s", u->nick,
		   u->username, u->host, u->nick, ngi_mainnick(ngi));
	if (readonly)
	    notice_lang(s_NickServ, u, READ_ONLY_MODE);
    }
}

/*************************************************************************/

static void do_listlinks(User *u)
{
    char *nick = strtok(NULL, " ");
    char *param = strtok(NULL, " ");
    NickInfo *ni;
    NickGroupInfo *ngi;
    int i;

    if (!nick || param) {
	syntax_error(s_NickServ, u, "LISTLINKS", NICK_OLD_LISTLINKS_SYNTAX);

    } else if (!(ni = get_nickinfo(nick))) {
	notice_lang(s_NickServ, u, NICK_X_NOT_REGISTERED, nick);

    } else if (ni->status & NS_VERBOTEN) {
	notice_lang(s_NickServ, u, NICK_X_FORBIDDEN, ni->nick);

    } else if (!(ngi = get_ngi(ni))) {
	notice_lang(s_NickServ, u, INTERNAL_ERROR);

    } else {
	notice_lang(s_NickServ, u, NICK_LISTLINKS_HEADER, ni->nick);
	ARRAY_FOREACH (i, ngi->nicks) {
	    if (irc_stricmp(ngi->nicks[i], ni->nick) != 0)
		notice(s_NickServ, u->nick, "    %s", ngi->nicks[i]);
	}
	notice_lang(s_NickServ, u, NICK_LISTLINKS_FOOTER, ngi->nicks_count-1);
    }
}

/*************************************************************************/
/***************************** Module stuff ******************************/
/*************************************************************************/

const int32 module_version = MODULE_VERSION_CODE;

ConfigDirective module_config[] = {
    { NULL }
};

static int old_NICK_DROPPED   = -1;
static int old_NICK_X_DROPPED = -1;

/*************************************************************************/

static int do_load_module(Module *mod, const char *modname)
{
    if (strcmp(modname, "chanserv/main") == 0) {
	module_chanserv = mod;
	p_check_channel_limit = get_module_symbol(mod, "check_channel_limit");
	if (!p_check_channel_limit) {
	    module_log("Unable to resolve symbol `check_channel_limit' in"
		       " module `chanserv/main'");
	}
    }
    return 0;
}

/*************************************************************************/

static int do_unload_module(Module *mod)
{
    if (mod == module_chanserv) {
	p_check_channel_limit = NULL;
	module_chanserv = NULL;
    }
    return 0;
}

/*************************************************************************/

int init_module(Module *module_)
{
    module = module_;

    if (find_module("nickserv/link")) {
	module_log("link and oldlink modules cannot be loaded at the same"
		   " time");
	return 0;
    }

    module_nickserv = find_module("nickserv/main");
    if (!module_nickserv) {
	module_log("Main NickServ module not loaded");
	return 0;
    }
    use_module(module_nickserv);

    if (!register_commands(module_nickserv, cmds)) {
	module_log("Unable to register commands");
	exit_module(0);
	return 0;
    }

    if (!add_callback(NULL, "load module", do_load_module)
     || !add_callback(NULL, "unload module", do_unload_module)
    ) {
	exit_module(0);
	return 0;
    }

    module_ = find_module("chanserv/main");
    if (module_)
	do_load_module(module_, "chanserv/main");

    old_NICK_DROPPED = setstring(NICK_DROPPED, NICK_DROPPED_LINKS);
    old_NICK_X_DROPPED = setstring(NICK_X_DROPPED, NICK_X_DROPPED_LINKS);

    return 1;
}

/*************************************************************************/

int exit_module(int shutdown_unused)
{
#ifdef CLEAN_COMPILE
    shutdown_unused = shutdown_unused;
#endif

    if (old_NICK_DROPPED >= 0) {
	setstring(NICK_DROPPED, old_NICK_DROPPED);
	old_NICK_DROPPED = -1;
    }
    if (old_NICK_X_DROPPED >= 0) {
	setstring(NICK_X_DROPPED, old_NICK_X_DROPPED);
	old_NICK_X_DROPPED = -1;
    }

    if (module_chanserv)
	do_unload_module(module_chanserv);

    remove_callback(NULL, "unload module", do_unload_module);
    remove_callback(NULL, "load module", do_load_module);

    if (module_nickserv) {
	unregister_commands(module_nickserv, cmds);
	unuse_module(module_nickserv);
	module_nickserv = NULL;
    }

    return 1;
}

/*************************************************************************/


syntax highlighted by Code2HTML, v. 0.9.1