/* Nickname linking module.
 *
 * 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 int32 NSLinkMax;

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

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_LINK,         -1,-1 },
    { "UNLINK",   do_unlink,   NULL,  -1,
    		NICK_HELP_UNLINK, NICK_OPER_HELP_UNLINK, },
    { "LISTLINKS",do_listlinks,NULL,  -1,
		NICK_HELP_LISTLINKS, NICK_OPER_HELP_LISTLINKS },
    { "SET MAINNICK", NULL,    NULL,  NICK_HELP_SET_MAINNICK, -1,-1 },
    { NULL }
};

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

static void do_link(User *u)
{
    char *nick = strtok(NULL, " ");
    NickInfo *ni = u->ni, *ni2;
    NickGroupInfo *ngi = u->ngi;
    int n;

    if (readonly && !is_services_admin(u)) {
	notice_lang(s_NickServ, u, NICK_LINK_DISABLED);
    } else if (!nick) {
	syntax_error(s_NickServ, u, "LINK", NICK_LINK_SYNTAX);
    } else if (strlen(nick) > protocol_nickmax) {
	notice_lang(s_NickServ, u, NICK_TOO_LONG, protocol_nickmax);
    } else if (!valid_nick(nick)) {
	notice_lang(s_NickServ, u, NICK_INVALID, nick);
    } else if (!reglink_check(u, nick, NULL, NULL)) {
	notice_lang(s_NickServ, u, NICK_CANNOT_BE_LINKED, nick);
	return;
    } 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 (irc_stricmp(u->nick, nick) == 0) {
	notice_lang(s_NickServ, u, NICK_LINK_SAME, nick);
    } else if ((ni2 = get_nickinfo(nick)) != NULL) {
	int i;
	ARRAY_SEARCH_PLAIN(ngi->nicks, nick, irc_stricmp, i);
	if (i < ngi->nicks_count)
	    notice_lang(s_NickServ, u, NICK_LINK_ALREADY_LINKED, nick);
	else if (ni2->status & NS_VERBOTEN)
	    notice_lang(s_NickServ, u, NICK_X_FORBIDDEN, nick);
	else
	    notice_lang(s_NickServ, u, NICK_X_ALREADY_REGISTERED, nick);
    } else if (get_user(nick)) {
	notice_lang(s_NickServ, u, NICK_LINK_IN_USE, nick);
    } else if (ngi->nicks_count >= NSLinkMax) {
	notice_lang(s_NickServ, u, NICK_LINK_TOO_MANY, NSLinkMax);
    } else if (NSRegEmailMax && ngi->email && !is_services_admin(u)
	       && abs(n=count_nicks_with_email(ngi->email)) >= NSRegEmailMax) {
	notice_lang(s_NickServ, u, NICK_LINK_TOO_MANY_NICKS, n,
		    NSRegEmailMax);
    } else {
	ni2 = makenick(nick, NULL);
	if (ni->last_usermask)
	    ni2->last_usermask = sstrdup(ni->last_usermask);
	if (ni->last_realmask)
	    ni2->last_realmask = sstrdup(ni->last_realmask);
	if (ni->last_realname)
	    ni2->last_realname = sstrdup(ni->last_realname);
	if (ni->last_quit)
	    ni2->last_quit = sstrdup(ni->last_quit);
	ni2->time_registered = ni2->last_seen = time(NULL);
	ni2->nickgroup = ni->nickgroup;
	put_nickinfo(ni2);
	ARRAY_EXTEND(ngi->nicks);
	strscpy(ngi->nicks[ngi->nicks_count-1], nick, NICKMAX);
	put_nickgroupinfo(ngi);
	module_log("%s!%s@%s linked nick %s to %s",
		   u->nick, u->username, u->host, nick, u->nick);
	notice_lang(s_NickServ, u, NICK_LINKED, nick);
	if (readonly)
	    notice_lang(s_NickServ, u, READ_ONLY_MODE);
    }
} /* do_link() */

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

static void do_unlink(User *u)
{
    NickInfo *ni = u->ni, *ni2;
    NickGroupInfo *ngi = u->ngi, *ngi2 = NULL;
    char *nick = strtok(NULL, " ");
    char *extra = strtok(NULL, " ");
    int is_servadmin = is_services_admin(u);
    int force = (extra != NULL && stricmp(extra,"FORCE") == 0);

    if (readonly && !is_servadmin) {
	notice_lang(s_NickServ, u, NICK_LINK_DISABLED);
    } else if (!nick || (extra && (!is_oper(u) || !force))) {
	syntax_error(s_NickServ, u, "UNLINK",
		     is_oper(u) ? NICK_UNLINK_OPER_SYNTAX
				: NICK_UNLINK_SYNTAX);
    } else if (force && !is_servadmin) {
	notice_lang(s_NickServ, u, PERMISSION_DENIED);
    } 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 (irc_stricmp(nick, u->nick) == 0) {
	notice_lang(s_NickServ, u, NICK_UNLINK_SAME);
    } else if (!(ni2 = get_nickinfo(nick)) || !ni2->nickgroup
	       || !(ngi2 = get_ngi(ni2)) || ngi2->nicks_count == 1) {
	notice_lang(s_NickServ, u, force ? NICK_UNLINK_NOT_LINKED
					 : NICK_UNLINK_NOT_LINKED_YOURS,
		    nick);
    } else if (!force && ni2->nickgroup != ni->nickgroup) {
	notice_lang(s_NickServ, u, NICK_UNLINK_NOT_LINKED_YOURS, nick);
    } else {
	int msg, i;
	char *param1;

	/* Adjust main nick if unlinking the current main nick (we set it
	 * to the caller's nick if the caller is unlinking their own nick,
	 * else the first in the list) */
	ARRAY_SEARCH_PLAIN(ngi2->nicks, ni2->nick, irc_stricmp, i);
	if (i == ngi2->mainnick) {
	    if (ngi == ngi2) {
		ARRAY_SEARCH_PLAIN(ngi->nicks, nick, irc_stricmp, i);
		if (i >= ngi->nicks_count) {
		    module_log("BUG: UNLINK: no entry in ngi->nicks[] for"
			       " nick %s", nick);
		} else {
		    ngi->mainnick = i;
		}
	    } else {
		ngi2->mainnick = 0;
	    }
	}
	/* Actually delete the nick */
	if (ni2->nickgroup != ni->nickgroup) {
	    delnick(ni2);
	    msg = NICK_X_UNLINKED;
	    param1 = ngi_mainnick(ngi2);
	} else {
	    delnick(ni2);
	    msg = NICK_UNLINKED;
	    param1 = ngi_mainnick(ngi);
	}
	notice_lang(s_NickServ, u, msg, nick, param1);
	module_log("%s!%s@%s unlinked nick %s from %s", u->nick,
		   u->username, u->host, nick, param1);
	if (readonly)
	    notice_lang(s_NickServ, u, READ_ONLY_MODE);
    }
}

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

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

    if (nick) {
	if (!is_services_admin(u)) {
	    syntax_error(s_NickServ, u, "LISTLINKS", NICK_LISTLINKS_SYNTAX);
	    return;
	} else if (!(ni = get_nickinfo(nick))) {
	    notice_lang(s_NickServ, u, NICK_X_NOT_REGISTERED, nick);
	    return;
	} else if (ni->status & NS_VERBOTEN) {
	    notice_lang(s_NickServ, u, NICK_X_FORBIDDEN, ni->nick);
	    return;
	} else if (!(ngi = get_ngi(ni))) {
	    notice_lang(s_NickServ, u, INTERNAL_ERROR);
	    return;
	}
    } else {
	if (!(ni = u->ni) || !(ngi = u->ngi) || ngi == NICKGROUPINFO_INVALID) {
	    notice_lang(s_NickServ, u, NICK_NOT_REGISTERED);
	    return;
	} else if (!user_identified(u)) {
	    notice_lang(s_NickServ, u, NICK_IDENTIFY_REQUIRED, s_NickServ);
	    return;
	}
    }

    notice_lang(s_NickServ, u, NICK_LISTLINKS_HEADER, ni->nick);
    ARRAY_FOREACH (i, ngi->nicks) {
	notice(s_NickServ, u->nick, "    %c%s",
	       i==ngi->mainnick ? '*' : ' ', ngi->nicks[i]);
    }
    notice_lang(s_NickServ, u, NICK_LISTLINKS_FOOTER, ngi->nicks_count);
}

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

/* SET MAINNICK handler */

static int do_set_mainnick(User *u, NickInfo *ni, NickGroupInfo *ngi,
			   const char *cmd, const char *param)
{
    int i;

    if (stricmp(cmd, "MAINNICK") != 0)
	return 0;
    ARRAY_SEARCH_PLAIN(ngi->nicks, param, irc_stricmp, i);
    if (i >= ngi->nicks_count) {
	notice_lang(s_NickServ, u, NICK_SET_MAINNICK_NOT_FOUND, param);
    } else {
	module_log("%s!%s@%s set main nick of %s (group %u) to %s",
		   u->nick, u->username, u->host, ngi_mainnick(ngi),
		   ngi->id, ngi->nicks[i]);
	ngi->mainnick = i;
	put_nickgroupinfo(ngi);
	notice_lang(s_NickServ, u, NICK_SET_MAINNICK_CHANGED, param);
    }
    return 1;
}

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

const int32 module_version = MODULE_VERSION_CODE;

ConfigDirective module_config[] = {
    { "NSLinkMax",        { { CD_POSINT, CF_DIRREQ, &NSLinkMax } } },
    { NULL }
};

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

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

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

    if (NSLinkMax > MAX_NICKCOUNT) {
	module_log("NSLinkMax upper-bounded at MAX_NICKCOUNT (%d)",
		   MAX_NICKCOUNT);
	NSLinkMax = MAX_NICKCOUNT;
    }

    if (find_module("nickserv/oldlink")) {
	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(module_nickserv, "SET", do_set_mainnick)) {
	module_log("Unable to add callbacks");
	exit_module(0);
	return 0;
    }

    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_nickserv) {
	remove_callback(module_nickserv, "SET", do_set_mainnick);
	unregister_commands(module_nickserv, cmds);
	unuse_module(module_nickserv);
	module_nickserv = NULL;
    }

    return 1;
}

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


syntax highlighted by Code2HTML, v. 0.9.1