#include "config.h"
#include "struct.h"
#include "common.h"
#include "sys.h"
#include "numeric.h"
#include "msg.h"
#include "channel.h"
#include "proto.h"
#include <time.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#include <io.h>
#endif
#include <fcntl.h>
#include "h.h"
#ifdef STRIPBADWORDS
#include "badwords.h"
#endif
#ifdef _WIN32
#include "version.h"
#endif

/* Ok, here's our CallerID data info... Fun stuff. */
/* It's up to you to decide on this. Usually you don't want this too much more than MAXTARGETS or it won't be that much useful.
 * However, some user may want/need to /accept a whole channel of users, for example, prior to /ctcp'ing the channel itself (since replies will be private
 * and thus filtered by CALLERID. Your call. */
#define MAX_ACCEPTS 24

/* How long between CALLERID notices... This really shouldn't be too low, or malicious users can still flood out people using server-side ignore.
   If undefined, CALLERID notices will be disabled entirely. */
#define CALLERID_COOLDOWN 60

typedef struct _CallerIDLink {
	aClient* who;
#ifdef CALLERID_COOLDOWN
	time_t nextnotice;
#endif
	aClient* acceptlist[MAX_ACCEPTS];
	struct _CallerIDLink* prev;
	struct _CallerIDLink* next;
} CallerIDLink, *PCallerIDLink;

static PCallerIDLink pHead = NULL;

/* Now some general functions to deal with these. */
static PCallerIDLink CID_AllocLink(aClient* who)
{
	int i;
	PCallerIDLink p = NULL;
	if (!who) return NULL;
	if (!(p = malloc(sizeof(CallerIDLink))))
		return NULL;
	p->who = who;
	p->nextnotice = time(NULL);
	for (i = 0; i < MAX_ACCEPTS; p->acceptlist[i++] = NULL);
	p->prev = NULL;
	p->next = NULL;
	return p;
}
static PCallerIDLink CID_FindLink(aClient* who)
{
	PCallerIDLink p = NULL;
	if (!who) return NULL;
	for (p = pHead; p; p = p->next)
		if (p->who == who) return p;
	return NULL;
}
static int CID_AddLink(PCallerIDLink what)
{
	if (!what) return 0;
	if (CID_FindLink(what->who)) return 0;
	what->next = pHead;
	if (pHead) pHead->prev = what;
	pHead = what;
	return 1;
}
static int CID_DelLink(PCallerIDLink what)
{
	if (!what) return 0;
	if (what == pHead) pHead = pHead->next;
	if (what->prev) what->prev->next = what->next;
	if (what->next) what->next->prev = what->prev;
	what->prev = NULL;
	what->next = NULL;
	return 1;
}
static void CID_FreeLink(PCallerIDLink what)
{
	if (!what) return;
	CID_DelLink(what);
	free(what);
}

static int cb_local_quit(aClient* sptr, char* comment);
static Hook* LocalQuitHook = NULL;

static int cb_remote_quit(aClient* sptr, char* comment);
static Hook* RemoteQuitHook = NULL;

static int cb_local_nickchange(aClient* sptr, char* nick);
static Hook* LocalNickHook = NULL;

static int cb_remote_nickchange(aClient* cptr, aClient* sptr, char* nick);
static Hook* RemoteNickHook = NULL;

static char* cb_privmsg(aClient* cptr, aClient* sptr, aClient* acptr, char* text, int notice);
static Hook* PrivMsgHook = NULL;

static int cb_umode_change(aClient* sptr, int setflags, int usermodes);
static Hook* UmodeHook = NULL;

static long UMODE_CALLERID = 0;
#define CHUM_CALLERID 'I'	/* If you change this change VAL005_CALLERID too! */
static Umode* UmodeCallerID = NULL;

#define TOK005_CALLERID "CALLERID"
static Isupport* ISupportCallerID = NULL;

static int m_accept(aClient* cptr, aClient* sptr, int parc, char** parv);
#define MSG_ACCEPT "ACCEPT"
#define TOK_ACCEPT "ACC"
Command* CmdAccept = NULL;

static Hook *HookConfTest = NULL;
static int cb_config_test(ConfigFile *, ConfigEntry *, int, int *);

static Hook *HookConfRun = NULL;
static int cb_config_run(ConfigFile *, ConfigEntry *, int);

static Hook *HookConfRehash = NULL;
static int cb_config_rehash();

static Hook *HookStats = NULL;
static int cb_stats(aClient *sptr, char *stats);

/* RUN-TIME CONFIGURATION */
/* This is stuff from unrealircd.conf :) */

/* set {
 *     # All options are optional, and default to false.
 *     dont-filter-ircops [yes|no];    # Determines if an IRCop is affected by callerid. U:Lines will always bypass callerid.
 *     callerid-alert-notice [yes|no]; # Determines if the CALLERID alert is sent as a Server Notice or as a numeric 718.
 *     callierd-reply-notice [yes|no]; # Determines if the CALLERID reply is sent as a Server Notice or as a numeric 716/717.
 * };
 */

static int dont_filter_ircops = 0;
static int callerid_notice = 0;
static int callerid_reply_notice = 0;

static void InitConf();

ModuleHeader MOD_HEADER(callerid)
	= {
		"callerid",
		"v1.0.3",
		"callerid / server-side ignore implementation",
		"3.2-b8-1",
		NULL
	};

ModuleInfo* myinfo = NULL;

void CleanUp() {
	while (pHead) CID_FreeLink(pHead);
	if (LocalQuitHook) HookDel(LocalQuitHook);
	if (RemoteQuitHook) HookDel(RemoteQuitHook);
	if (LocalNickHook) HookDel(LocalNickHook);
	if (RemoteNickHook) HookDel(RemoteNickHook);
	if (PrivMsgHook) HookDel(PrivMsgHook);
	if (UmodeHook) HookDel(UmodeHook);
	if (UmodeCallerID) UmodeDel(UmodeCallerID);
	if (ISupportCallerID) IsupportDel(ISupportCallerID);
	if (HookStats) HookDel(HookStats);
	if (HookConfRehash) HookDel(HookConfRehash);
	if (HookConfRun) HookDel(HookConfRun);
	if (HookConfTest) HookDel(HookConfTest);
}

DLLFUNC int MOD_TEST(callerid)(ModuleInfo *modinfo)
{
	myinfo = modinfo;
	if (!(HookConfTest = HookAddEx(myinfo->handle, HOOKTYPE_CONFIGTEST, cb_config_test))) {
		CleanUp();
		config_error("callerid: Failed to add config test hook.");
		return MOD_FAILED;
	}
	return MOD_SUCCESS;
}

DLLFUNC int MOD_INIT(callerid)(ModuleInfo *modinfo)
{
	char tokval[2];
	myinfo = modinfo;
	InitConf();
	/* Config stuff... */
	if (!(HookConfRun = HookAddEx(myinfo->handle, HOOKTYPE_CONFIGRUN, cb_config_run))) {
		CleanUp();
		config_error("callerid: Failed to add config run hook.");
		return MOD_FAILED;
	}
	if (!(HookConfRehash = HookAddEx(myinfo->handle, HOOKTYPE_REHASH, cb_config_rehash))) {
		CleanUp();
		config_error("callerid: Failed to add config rehash hook.");
		return MOD_FAILED;
	}
	if (!(HookStats = HookAddEx(myinfo->handle, HOOKTYPE_STATS, cb_stats))) {
		config_error("callerid: Failed to add stats hook (non-fatal).");
		HookStats = NULL;
	}
	/* First, add the 005 Token. */
	tokval[0] = CHUM_CALLERID;
	tokval[1] = '\0';
	if (!(ISupportCallerID = IsupportAdd(myinfo->handle, TOK005_CALLERID, tokval))) {
		CleanUp();
		config_error("callerid: Failed to add 005 token 'CALLERID': %s", ModuleGetErrorStr(myinfo->handle));
		return MOD_FAILED;
	}
	/* Now the usermode. */
	if (!(UmodeCallerID = UmodeAdd(myinfo->handle, CHUM_CALLERID, UMODE_GLOBAL, umode_allow_all, &UMODE_CALLERID))) {
		CleanUp();
		config_error("callerid: Failed to add Usermode %c: %s", CHUM_CALLERID, ModuleGetErrorStr(myinfo->handle));
		return MOD_FAILED;
	}
	if (!(UmodeHook = HookAddEx(myinfo->handle, HOOKTYPE_UMODE_CHANGE, cb_umode_change))) {
		CleanUp();
		config_error("callerid: Failed to add required hook: %s", ModuleGetErrorStr(myinfo->handle));
		return MOD_FAILED;
	}
	/* Now the privmsg hook. */
	if (!(PrivMsgHook = HookAddPCharEx(myinfo->handle, HOOKTYPE_USERMSG, cb_privmsg))) {
		CleanUp();
		config_error("callerid: Failed to add required hook: %s", ModuleGetErrorStr(myinfo->handle));
		return MOD_FAILED;
	}
	/* Now the local quit hook. */
	if (!(LocalQuitHook = HookAddEx(myinfo->handle, HOOKTYPE_LOCAL_QUIT, cb_local_quit))) {
		CleanUp();
		config_error("callerid: Failed to add required hook: %s", ModuleGetErrorStr(myinfo->handle));
		return MOD_FAILED;
	}
	/* Now the remote quit hook. */
	if (!(RemoteQuitHook = HookAddEx(myinfo->handle, HOOKTYPE_REMOTE_QUIT, cb_remote_quit))) {
		CleanUp();
		config_error("callerid: Failed to add required hook: %s", ModuleGetErrorStr(myinfo->handle));
		return MOD_FAILED;
	}
	/* Now the local nickchange hook. */
	if (!(LocalNickHook = HookAddEx(myinfo->handle, HOOKTYPE_LOCAL_NICKCHANGE, cb_local_nickchange))) {
		CleanUp();
		config_error("callerid: Failed to add required hook: %s", ModuleGetErrorStr(myinfo->handle));
		return MOD_FAILED;
	}
	/* Now the remote nickchange hook. */
	if (!(RemoteNickHook = HookAddEx(myinfo->handle, HOOKTYPE_REMOTE_NICKCHANGE, cb_remote_nickchange))) {
		CleanUp();
		config_error("callerid: Failed to add required hook: %s", ModuleGetErrorStr(myinfo->handle));
		return MOD_FAILED;
	}
	if (!(CmdAccept = CommandAdd(myinfo->handle, MSG_ACCEPT, TOK_ACCEPT, m_accept, 1, 0))) {
		CleanUp();
		config_error("callerid: Failed to add command /ACCEPT: %s", ModuleGetErrorStr(myinfo->handle));
		return MOD_FAILED;
	}
	ModuleSetOptions(modinfo->handle, MOD_OPT_PERM);
	return MOD_SUCCESS;
}

DLLFUNC int MOD_LOAD(callerid)(int module_load)
{
	return MOD_SUCCESS;
}

DLLFUNC int MOD_UNLOAD(callerid)(int module_unload)
{
	CleanUp();
	return MOD_SUCCESS;
}

static void InitConf()
{
	dont_filter_ircops = 0;
	callerid_notice = 0;
	callerid_reply_notice = 0;
}

static int cb_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
{
	int errors = 0;
	if (type != CONFIG_SET) return 0;
	if (!strcmp(ce->ce_varname, "dont-filter-ircops") || !strcmp(ce->ce_varname, "callerid-notice") || !strcmp(ce->ce_varname, "callerid-reply-notice"))
		return 1;
	else
		return 0;
}

static int cb_config_run(ConfigFile *cf, ConfigEntry *ce, int type)
{
	if (type != CONFIG_SET) return 0;
	if (!strcmp(ce->ce_varname, "dont-filter-ircops"))
	{
		if (!ce->ce_vardata) {
			dont_filter_ircops = 1;
		}
		else {
			dont_filter_ircops = config_checkval(ce->ce_vardata, CFG_YESNO);
		}
		return 1;
	}
	else if (!strcmp(ce->ce_varname, "callerid-notice"))
	{
		if (!ce->ce_vardata) {
			callerid_notice = 1;
		}
		else {
			callerid_notice = config_checkval(ce->ce_vardata, CFG_YESNO);
		}
		return 1;
	}
	else if (!strcmp(ce->ce_varname, "callerid-reply-notice"))
	{
		if (!ce->ce_vardata) {
			callerid_reply_notice = 1;
		}
		else {
			callerid_reply_notice = config_checkval(ce->ce_vardata, CFG_YESNO);
		}
		return 1;
	}
	else
		return 0;
}

static int cb_config_rehash()
{
	InitConf();
	return 1;
}

static int cb_stats(aClient *sptr, char *stats)
{
	if (*stats == 'S') {
		sendto_one(sptr, ":%s %i %s :dont-filter-ircops: %d", me.name, RPL_TEXT, sptr->name, dont_filter_ircops);
		sendto_one(sptr, ":%s %i %s :callerid-notice: %d", me.name, RPL_TEXT, sptr->name, callerid_notice);
		sendto_one(sptr, ":%s %i %s :callerid-reply-notice: %d", me.name, RPL_TEXT, sptr->name, callerid_reply_notice);
	}
	return 0;
}

static int cb_local_quit(aClient* sptr, char* comment)
{
	PCallerIDLink pcid = NULL;
	int i = 0;
	if (MyClient(sptr)) {
		pcid = CID_FindLink(sptr);
		CID_FreeLink(pcid); pcid = NULL;
	}
	for (pcid = pHead; pcid; pcid = pcid->next) {
		for (i = 0; i < MAX_ACCEPTS; i++) {
			if (pcid->acceptlist[i] == sptr) {
				pcid->acceptlist[i] = NULL;
			}
		}
	}
	return HOOK_CONTINUE;
}

static int cb_remote_quit(aClient* sptr, char* comment)
{
	PCallerIDLink pcid = NULL;
	int i = 0;
	for (pcid = pHead; pcid; pcid = pcid->next) {
		for (i = 0; i < MAX_ACCEPTS; i++) {
			if (pcid->acceptlist[i] == sptr) {
				pcid->acceptlist[i] = NULL;
			}
		}
	}
	return HOOK_CONTINUE;
}

static int cb_local_nickchange(aClient* sptr, char* nick)
{
	PCallerIDLink pcid = NULL;
	int i = 0;
	for (pcid = pHead; pcid; pcid = pcid->next) {
		for (i = 0; i < MAX_ACCEPTS; i++) {
			if (pcid->acceptlist[i] == sptr) {
				pcid->acceptlist[i] = NULL;
			}
		}
	}
	if ((pcid = CID_FindLink(sptr))) {
		for (i = 0; i < MAX_ACCEPTS; i++)
			pcid->acceptlist[i] = NULL;
	}
	return HOOK_CONTINUE;
}

static int cb_remote_nickchange(aClient* cptr, aClient* sptr, char* nick)
{
	PCallerIDLink pcid = NULL;
	int i = 0;
	for (pcid = pHead; pcid; pcid = pcid->next) {
		for (i = 0; i < MAX_ACCEPTS; i++) {
			if (pcid->acceptlist[i] == sptr) {
				pcid->acceptlist[i] = NULL;
			}
		}
	}
	return HOOK_CONTINUE;
}

static char* cb_privmsg(aClient* cptr, aClient* sptr, aClient* acptr, char* text, int notice)
{
	PCallerIDLink pcid = NULL;
	int i = 0;
	time_t t = time(NULL);
	if (!(pcid = CID_FindLink(acptr))) return text;
	if (!MyClient(acptr)) return text;
	if (IsULine(sptr) || IsServer(sptr) || (dont_filter_ircops && IsAnOper(sptr))) return text;
	for (i = 0; i < MAX_ACCEPTS; i++) {
		if (pcid->acceptlist[i] == sptr)
			return text;
	}
	if (callerid_reply_notice)
		sendto_one(sptr, ":%s NOTICE %s :%s is in +%c mode (server-side ignore).", me.name, sptr->name, acptr->name, CHUM_CALLERID);
	else
		sendto_one(sptr, ":%s 716 %s %s :is in +%c mode (server-side ignore).", me.name, sptr->name, acptr->name, CHUM_CALLERID);
#ifdef CALLERID_COOLDOWN
	if (pcid->nextnotice <= t) {
		if (callerid_reply_notice)
			sendto_one(sptr, ":%s NOTICE %s :%s has been informed that you messaged them.", me.name, sptr->name, acptr->name);
		else
			sendto_one(sptr, ":%s 717 %s %s :has been informed that you messaged them.", me.name, sptr->name, acptr->name);
		if (callerid_notice)
			sendto_one(acptr, ":%s NOTICE %s :%s (%s@%s) is messaging you, and you have umode +%c.", me.name, acptr->name, sptr->name, sptr->user->username, (IsHidden(sptr) ? sptr->user->virthost : sptr->user->realhost), CHUM_CALLERID);
		else
			sendto_one(acptr, ":%s 718 %s %s %s@%s :is messaging you, and you have umode +%c.", me.name, acptr->name, sptr->name, sptr->user->username, (IsHidden(sptr) ? sptr->user->virthost : sptr->user->realhost), CHUM_CALLERID);
		pcid->nextnotice = t + CALLERID_COOLDOWN;
	}
#endif
	return NULL;
}

static int m_accept(aClient* cptr, aClient* sptr, int parc, char** parv)
{
	PCallerIDLink pcid = NULL;
	char* nick = NULL, *tmp = NULL;
	aClient* acptr = NULL;
	char* p = NULL;
	int i = 0;
	int remove = 0, found = 0; /* remove is boolean, but found is dual-purpose. If !remove, then found is an index, if remove, then found is boolean. */
	if (parc < 2) {
		sendto_one(sptr, err_str(ERR_NEEDMOREPARAMS), me.name, parv[0], "ACCEPT");
		return -1;
	}
	if (!MyClient(sptr)) return 0;
	if (!(pcid = CID_FindLink(sptr))) {
#ifdef ERR_CANNOTDOCOMMAND
		tmp = malloc(32);
		memset(tmp, 0, 32);
		snprintf(tmp, 31, "You are not +%c", CHUM_CALLERID);
		sendto_one(sptr, err_str(ERR_CANNOTDOCOMMAND), me.name, sptr->name, MSG_ACCEPT, tmp);
		free(tmp);
#else
		sendto_one(sptr, ":%s NOTICE %s :You are not +%c, so ACCEPT is meaningless for you.", me.name, sptr->name, CHUM_CALLERID);
#endif
		return 0;
	}
	if (!strcmp(parv[1], "*")) {
		/* He wants a list. */
		for (i = 0; i < MAX_ACCEPTS; i++) {
			if (pcid->acceptlist[i]) {
				sendto_one(sptr, ":%s 281 %s %s", me.name, sptr->name, pcid->acceptlist[i]->name);
			}
		}
		sendto_one(sptr, ":%s 282 %s :End of ACCEPT list", me.name, sptr->name);
		return 0;
	}
	for (tmp = parv[1]; (nick = strtoken(&p, tmp, ",")); tmp = NULL) {
		found = 0;
		remove = 0;
		if (index(nick, '?') || index(nick, '*'))
			continue;
		if (*nick == '-') {
			remove = 1;
			nick++;
		}
		if ((acptr = find_client(nick, NULL)))
		{
			if (IsServer(acptr)) continue;
			if (IsMe(acptr)) continue;
			if (!IsPerson(acptr)) continue;
			if (remove) {
				for (i = 0; i < MAX_ACCEPTS; i++) {
					if (pcid->acceptlist[i] == acptr) {
						found = 1;
						pcid->acceptlist[i] = NULL;
					}
				}
				if (!found)
					sendto_one(sptr, ":%s 458 %s %s :is not on your accept list", me.name, sptr->name, nick);
			}
			else {
				found = -1;
				for (i = 0; i < MAX_ACCEPTS; i++) {
					if (pcid->acceptlist[i] == acptr) {
						sendto_one(sptr, ":%s 457 %s %s :is already on your accept list", me.name, sptr->name, nick);
						found = -2;
						break;
					}
					else if (pcid->acceptlist[i] == NULL) {
						found = i;
					}
				}
				if (found >= 0) {
					pcid->acceptlist[found] = acptr;
				}
				else if (found == -1) {
					sendto_one(sptr, ":%s 456 %s :Accept list is full", me.name, sptr->name);
				}
			}
		}
		else {
			sendto_one(sptr, err_str(ERR_NOSUCHNICK), me.name, sptr->name, nick);
		}
		if (p) p[-1] = ',';
	}
	return 0;
}

static int cb_umode_change(aClient* sptr, int setflags, int usermodes)
{
	PCallerIDLink pcid = NULL;
	if (!MyClient(sptr)) return HOOK_CONTINUE;
	if (usermodes & UMODE_CALLERID) {
		if (!(pcid = CID_FindLink(sptr))) {
			pcid = CID_AllocLink(sptr);
			CID_AddLink(pcid);
		}
	}
	else {
		if ((pcid = CID_FindLink(sptr))) CID_FreeLink(pcid);
	}
	return HOOK_CONTINUE;
}
