import sys
from copy import deepcopy
from datetime import datetime
from random import randint

import main
from core.relay import sendRelayNotification
from modules import chankeep, counters, helpers, monitor, regproc, userinfo
from twisted.internet import reactor
from twisted.internet.defer import Deferred
from twisted.internet.protocol import ReconnectingClientFactory
from twisted.internet.task import LoopingCall
from twisted.words.protocols.irc import (
    IRCBadMessage,
    IRCClient,
    lowDequote,
    numeric_to_symbolic,
)
from utils.dedup import dedup
from utils.logging.debug import debug, trace
from utils.logging.log import error, log, warn
from utils.logging.send import sendAll
from utils.parsing import parsen


# Copied from the Twisted source so we can fix a bug
def parsemsg(s):
    """
    Breaks a message from an IRC server into its prefix, command, and
    arguments.
    @param s: The message to break.
    @type s: L{bytes}
    @return: A tuple of (prefix, command, args).
    @rtype: L{tuple}
    """
    prefix = ""
    trailing = []
    if not s:
        raise IRCBadMessage("Empty line.")
    if s[0:1] == ":":
        prefix, s = s[1:].split(" ", 1)
    if s.find(" :") != -1:
        s, trailing = s.split(" :", 1)
        args = s.split(" ")  # Twisted bug fixed by adding an argument to split()
        args.append(trailing)
    else:
        args = s.split(" ")  # And again
    command = args.pop(0)
    return prefix, command, args


class IRCBot(IRCClient):
    def __init__(self, net, num):
        self.isconnected = False
        self.channels = []
        self.net = net
        self.authenticated = not regproc.needToAuth(self.net)
        self.num = num
        self.buffer = ""
        self.name = net + str(num)
        alias = main.alias[num]
        relay = main.network[self.net].relays[num]
        self.netinst = main.network[self.net]
        self.nickname = alias["nick"]
        self.realname = alias["realname"]
        self.username = alias["nick"].lower() + "/" + relay["net"]
        self.password = main.config["Relay"]["Password"]
        self.userinfo = None  #
        self.fingerReply = None  #
        self.versionName = None  # Don't give out information
        self.versionNum = None  #
        self.versionEnv = None  #
        self.sourceURL = None  #

        self._getWho = {}  # LoopingCall objects -- needed to be able to stop them

        self._tempWho = {}  # temporary storage for gathering WHO info
        self._tempNames = {}  # temporary storage for gathering NAMES info
        self._tempList = ([], [])  # temporary storage for gathering LIST info
        self.listOngoing = False  # we are currently receiving a LIST
        self.listRetried = False  # we asked and got nothing so asked again
        self.listAttempted = False  # we asked for a list
        self.listSimple = False  # after asking again we got the list, so use the simple
        #  syntax from now on
        self.wantList = (
            False  # we want to send a LIST, but not all relays are active yet
        )
        self.chanlimit = 0
        self.prefix = {}
        self.servername = None

        self._regAttempt = None
        self._negativePass = None

    def lineReceived(self, line):
        if bytes != str and isinstance(line, bytes):
            # decode bytes from transport to unicode
            line = line.decode("utf-8", "replace")

        line = lowDequote(line)
        try:
            prefix, command, params = parsemsg(line)
            if command in numeric_to_symbolic:
                command = numeric_to_symbolic[command]
            try:
                self.handleCommand(command, prefix, params)
            except Exception as err:
                error(err)
        except IRCBadMessage:
            self.badMessage(line, *sys.exc_info())

    def joinChannels(self, channels):
        sleeptime = 0.0
        increment = 0.8
        for i in channels:
            if i not in self.channels:
                if self.net in main.blacklist.keys():
                    if i in main.blacklist[self.net]:
                        debug(
                            "Not joining blacklisted channel %s on %s - %i"
                            % (i, self.net, self.num)
                        )
                        continue
                trace(
                    self.net, "-", self.num, ": joining", i, "in", sleeptime, "seconds"
                )
                reactor.callLater(sleeptime, self.join, i)
                sleeptime += increment
                if sleeptime == 10:
                    sleeptime = 0.0
                    increment = 0.7
                increment += 0.1
            else:
                error(
                    "%s - Cannot join channel we are already on: %s - %i"
                    % (i, self.net, self.num)
                )

    def checkChannels(self):
        if chankeep.allRelaysActive(self.net):
            debug(f"checkChannels() all relays active for {self.net}")
        else:
            debug(
                "checkChannels() skipping channel check as we have inactive relays: %s - %i"
                % (self.net, self.num)
            )
            return
        if self.net in main.TempChan.keys():
            if self.num in main.TempChan[self.net].keys():
                self.joinChannels(main.TempChan[self.net][self.num])
                del main.TempChan[self.net][self.num]
                if not main.TempChan[self.net]:
                    del main.TempChan[self.net]

    def event(self, **cast):
        if "ts" not in cast.keys():
            cast["ts"] = int(datetime.now().timestamp())

        # remove odd stuff
        for i in list(
            cast.keys()
        ):  # Make a copy of the .keys() as Python 3 cannot handle iterating over
            if cast[i] == "":  # a dictionary that changes length with each iteration
                del cast[i]
        # remove server stuff
        if "muser" in cast.keys():
            if cast["muser"] == self.servername:
                cast["type"] = "conn"
        if "channel" in cast.keys():
            if cast["channel"] == "*":
                cast["type"] = "conn"
        ##

        # expand out the hostmask
        if not {"nick", "ident", "host"}.issubset(set(cast.keys())):
            if "muser" in cast.keys():
                cast["nick"], cast["ident"], cast["host"] = parsen(cast["muser"])

        # handle ZNC stuff
        if {"nick", "ident", "host", "msg"}.issubset(set(cast)):
            if cast["ident"] == "znc" and cast["host"] == "znc.in":
                cast["type"] = "znc"
                cast["num"] = self.num
                del cast["nick"]
                del cast["ident"]
                del cast["host"]
                del cast["channel"]
                del cast["muser"]
                if "Disconnected from IRC" in cast["msg"]:
                    log("ZNC disconnected on %s - %i" % (self.net, self.num))
                    self.isconnected = False
                    self.authenticated = False
                if "Connected!" in cast["msg"]:
                    log("ZNC connected on %s - %i" % (self.net, self.num))
                    self.isconnected = True
                if "could not be joined, disabling it" in cast["msg"]:
                    error(cast["msg"])
        #

        # don't reprocess the same message twice
        # if the type is in that list, it's already been here, don't run it again
        if not cast["type"] in {"query", "self", "highlight", "znc", "who", "conn"}:
            cast["num"] = self.num
            if "channel" in cast.keys():
                if cast["type"] == "mode":
                    if cast["channel"].lower() == self.nickname.lower():
                        # castDup = deepcopy(cast)
                        cast["mtype"] = cast["type"]
                        cast["type"] = "self"
                        # self.event(**castDup)
                    if cast["modearg"]:  # check if modearg is non-NoneType
                        if self.nickname.lower() == cast["modearg"].lower():
                            castDup = deepcopy(cast)
                            castDup["mtype"] = cast["type"]
                            castDup["type"] = "highlight"
                            self.event(**castDup)
                else:
                    if cast["channel"].lower() == self.nickname.lower() or not cast[
                        "channel"
                    ].startswith("#"):
                        cast["mtype"] = cast["type"]
                        cast["type"] = "query"
                        # self.event(**castDup)
                        # Don't call self.event for this one because queries are not events on a
                        #  channel, but we still want to see them
                    if cast["channel"] == "AUTH":
                        cast["type"] = "conn"
                        cast["mtype"] = cast["type"]

            # TODO: better way to do this
            # as we changed the types above, check again
            if not cast["type"] in {"query", "self", "highlight", "znc", "who", "conn"}:
                # we have been kicked
                if "user" in cast.keys():
                    if cast["user"].lower() == self.nickname.lower():
                        castDup = deepcopy(cast)
                        castDup["mtype"] = cast["type"]
                        castDup["type"] = "self"
                        if self.net in main.blacklist.keys():
                            if cast["channel"] not in main.blacklist[self.net]:
                                main.blacklist[self.net].append(cast["channel"])
                        else:
                            main.blacklist[self.net] = [cast["channel"]]
                        main.saveConf("blacklist")
                        self.event(**castDup)

                # we sent a message/left/joined/kick someone/quit
                if "nick" in cast.keys():
                    if cast["nick"].lower() == self.nickname.lower():
                        castDup = deepcopy(cast)
                        castDup["mtype"] = cast["type"]
                        castDup["type"] = "self"

                # we have been mentioned in a msg/notice/action/part/quit/topic message
                if "msg" in cast.keys():  # Don't highlight queries
                    if cast["msg"] is not None:
                        if self.nickname.lower() in cast["msg"].lower():
                            castDup = deepcopy(cast)
                            castDup["mtype"] = cast["type"]
                            castDup["type"] = "highlight"
                            self.event(**castDup)

        if "net" not in cast.keys():
            cast["net"] = self.net
        if "num" not in cast.keys():
            cast["num"] = self.num
        if not self.authenticated:
            regproc.registerTest(cast)
        counters.event(self.net, cast["type"])
        monitor.event(self.net, cast)

    def privmsg(self, user, channel, msg):
        self.event(type="msg", muser=user, channel=channel, msg=msg)

    def noticed(self, user, channel, msg):
        self.event(type="notice", muser=user, channel=channel, msg=msg)

    def action(self, user, channel, msg):
        self.event(type="action", muser=user, channel=channel, msg=msg)

    def sendmsg(self, channel, msg, in_query=False):
        query = f"{self.nickname}!*@*"
        res = userinfo.getWhoSingle(self.net, query)
        if not res:
            res = []
        us = list(res)
        if len(us) > 0:
            hostmask = us[0]
        else:
            # Close enough...
            hostmask = f"{self.nickname}!{self.nickname}@{self.servername}"
            warn(f"Could not get a hostname, using {hostmask}")
        nick, ident, host = parsen(hostmask)
        # We sent someone a query reply
        if in_query:
            self.event(
                type="self",
                mtype="msg",
                channel=self.nickname,
                nick=channel,
                ident=ident,
                host=host,
                msg=msg,
            )
        else:
            self.event(
                type="self",
                mtype="msg",
                channel=channel,
                nick=self.nickname,
                ident=ident,
                host=host,
                msg=msg,
            )
            self.event(
                type="msg",
                channel=channel,
                nick=self.nickname,
                ident=ident,
                host=host,
                msg=msg,
            )
        self.msg(channel, msg)

    def get(self, var):
        try:
            result = getattr(self, var)
        except AttributeError:
            result = None
        return result

    def setNick(self, nickname):
        self._attemptedNick = nickname
        self.sendLine("NICK %s" % nickname)
        self.nickname = nickname

    def alterCollidedNick(self, nickname):
        newnick = nickname + "_"
        return newnick

    def nickChanged(self, olduser, newnick):
        self.nickname = newnick
        self.event(type="self", mtype="nick", muser=olduser, user=newnick)

    def irc_ERR_NICKNAMEINUSE(self, prefix, params):
        self._attemptedNick = self.alterCollidedNick(self._attemptedNick)
        self.setNick(self._attemptedNick)

    def irc_ERR_PASSWDMISMATCH(self, prefix, params):
        log("%s - %i: password mismatch as %s" % (self.net, self.num, self.username))
        sendAll(
            "%s - %i: password mismatch as %s" % (self.net, self.num, self.username)
        )

    def _who(self, channel):
        d = Deferred()
        if channel not in self._tempWho:
            self._tempWho[channel] = ([], [])
        self._tempWho[channel][0].append(d)
        self.sendLine("WHO %s" % channel)
        return d

    def who(self, channel):
        self._who(channel).addCallback(self.got_who)

    def irc_RPL_WHOREPLY(self, prefix, params):
        channel = params[1]
        ident = params[2]
        host = params[3]
        server = params[4]
        nick = params[5]
        status = params[6]
        realname = params[7]
        if channel not in self._tempWho:
            return
        n = self._tempWho[channel][1]
        n.append([nick, nick, host, server, status, realname])
        self.event(
            type="who",
            nick=nick,
            ident=ident,
            host=host,
            realname=realname,
            channel=channel,
            server=server,
            status=status,
        )

    def irc_RPL_ENDOFWHO(self, prefix, params):
        channel = params[1]
        if channel not in self._tempWho:
            return
        callbacks, info = self._tempWho[channel]
        for cb in callbacks:
            cb.callback((channel, info))
        del self._tempWho[channel]

    def got_who(self, whoinfo):
        userinfo.initialUsers(self.net, whoinfo[0], whoinfo[1])

    def sanit(self, data):
        if len(data) >= 1:
            if data[0] in self.prefix.keys():
                return (
                    self.prefix[data[0]],
                    data[1:],
                )  # would use a set but it's possible these are the same
            return (None, data)
        else:
            return (None, False)

    def names(self, channel):
        d = Deferred()
        if channel not in self._tempNames:
            self._tempNames[channel] = ([], [])
        self._tempNames[channel][0].append(d)
        self.sendLine("NAMES %s" % channel)
        return d

    def irc_RPL_NAMREPLY(self, prefix, params):
        channel = params[2]
        nicklist = params[3].split(" ")
        if channel not in self._tempNames:
            return
        n = self._tempNames[channel][1]
        n.append(nicklist)

    def irc_RPL_ENDOFNAMES(self, prefix, params):
        channel = params[1]
        if channel not in self._tempNames:
            return
        callbacks, namelist = self._tempNames[channel]
        for cb in callbacks:
            cb.callback((channel, namelist))
        del self._tempNames[channel]

    def got_names(self, nicklist):
        newNicklist = []
        for i in nicklist[1]:
            for x in i:
                mode, nick = self.sanit(x)
                if nick:
                    newNicklist.append((mode, nick))
        userinfo.initialNames(self.net, nicklist[0], newNicklist)

    def myInfo(self, servername, version, umodes, cmodes):
        self.servername = servername

    def _list(self, noargs):
        d = Deferred()
        self._tempList = ([], [])
        self._tempList[0].append(d)
        if self.listSimple:
            self.sendLine("LIST")
            return d  # return early if we know what to do

        if noargs:
            self.sendLine("LIST")
        else:
            self.sendLine("LIST >0")
        return d

    def list(self, noargs=False, nocheck=False):
        if not self.authenticated:
            debug("Will not send LIST, unauthenticated: %s - %i" % (self.net, self.num))
            return
        if self.listAttempted:
            debug(
                "List request dropped, already asked for LIST - %s - %i"
                % (self.net, self.num)
            )
            return
        else:
            self.listAttempted = True
        if self.listOngoing:
            debug(
                "LIST request dropped, already ongoing - %s - %i" % (self.net, self.num)
            )
            return
        else:
            if nocheck:
                allRelays = True  # override the system - if this is
            else:  # specified, we already did this
                allRelays = chankeep.allRelaysActive(self.net)
            if not allRelays:
                self.wantList = True
                debug("Not all relays were active for LIST request")
                return
            self._list(noargs).addCallback(self.got_list)

    def irc_RPL_LISTSTART(self, prefix, params):
        self.listAttempted = False
        self.listOngoing = True
        self.wantList = False

    def irc_RPL_LIST(self, prefix, params):
        channel = params[1]
        users = params[2]
        topic = params[3]
        self._tempList[1].append([channel, users, topic])

    def irc_RPL_LISTEND(self, prefix, params):
        if (
            not len(self._tempList[0]) > 0
        ):  # there are no callbacks, can't do anything there
            debug("We didn't ask for this LIST, discarding")
            self._tempList[0].clear()
            self._tempList[1].clear()
            return
        callbacks, info = self._tempList
        self.listOngoing = False
        for cb in callbacks:
            cb.callback((info))
        noResults = False
        if len(self._tempList[1]) == 0:
            noResults = True
        self._tempList[0].clear()
        self._tempList[1].clear()
        if noResults:
            if self.listRetried:
                warn("LIST still empty after retry: %s - %i" % (self.net, self.num))
                self.listRetried = False
                return
            else:
                self.list(True)
                self.listRetried = True
        else:
            if self.listRetried:
                self.listRetried = False
                debug(
                    "List received after retry - defaulting to simple list syntax: %s - %i"
                    % (self.net, self.num)
                )
                self.listSimple = True

    def got_list(self, listinfo):
        if len(listinfo) == 0:  # probably ngircd not supporting LIST >0
            return
        if main.config["ChanKeep"]["Enabled"]:
            chankeep.initialList(self.net, self.num, listinfo)

    def recheckList(self):
        if not main.config["ChanKeep"]["Enabled"]:
            return
        allRelays = chankeep.allRelaysActive(self.net)
        debug(f"recheckList() all relays for {self.net} {allRelays}")
        if allRelays:
            debug(f"recheckList() all relays active for {self.net}")
            first_relay = helpers.get_first_relay(self.net)
            debug(f"recheckList() first relay for {self.net}: {first_relay.num}")
            if first_relay:
                if first_relay.wantList is True:
                    first_relay.list(nocheck=True)
                    debug(
                        f"recheckList() asking for a list for {self.net} after final relay {self.num} connected"
                    )
                else:
                    debug(
                        f"recheckList() first relay wantList is False for {self.net} ({first_relay.num})"
                    )
            # name = self.net + "1"

        # if self.num == 1:  # Only one instance should do a list
        if helpers.is_first_relay(self.net, self.num):
            debug(f"recheckList() we are the first relay for {self.net} ({self.num})")
            if self.chanlimit:
                if allRelays:
                    self.list()
                    debug(
                        f"recheckList() requested a list for {self.net} from {self.num}"
                    )
                else:
                    debug(f"recheckList() not all relays active for {self.net}")
                    self.wantList = True
            else:
                debug("recheckList() aborting LIST due to bad chanlimit")
        self.checkChannels()

    def seed_chanlimit(self, chanlimit):
        if not main.network[self.net].relays[self.num][
            "registered"
        ]:  # TODO: add check for register request sent, only send it once
            if main.config["AutoReg"]:
                if not self.authenticated:
                    self._regAttempt = reactor.callLater(
                        5, regproc.registerAccount, self.net, self.num
                    )
                    # regproc.registerAccount(self.net, self.num)
        try:
            self.chanlimit = int(chanlimit)
        except TypeError:
            warn("Invalid chanlimit: %s" % chanlimit)
        if self.chanlimit == 0:
            self.chanlimit = 200  # don't take the piss if it's not limited
        net_inst_chanlimit = self.netinst.chanlimit
        if net_inst_chanlimit:
            if self.chanlimit > net_inst_chanlimit:
                self.chanlimit = net_inst_chanlimit
                # warn(f"Chanlimit on {self.net} too high, setting to {self.chanlimit}")

        if not regproc.needToRegister(
            self.net
        ):  # if we need to register, only recheck on auth confirmation
            if main.config["ChanKeep"]["Enabled"]:
                self.recheckList()

    def seed_prefix(self, prefix):
        prefix = prefix.replace(")", "")
        prefix = prefix.replace("(", "")
        length = len(prefix)
        half = int(length / 2)
        prefixToMode = dict(zip(prefix[half:], prefix[:half]))
        self.prefix = prefixToMode

    def isupport(self, options):
        interested = {"CHANLIMIT", "MAXCHANNELS", "PREFIX"}
        newOptions = {x for x in options if any(y in x for y in interested)}
        if len(newOptions) == 0:
            return
        if not self.isconnected:
            log("endpoint connected: %s - %i" % (self.net, self.num))
            self.isconnected = True
        for i in newOptions:
            if i.startswith("PREFIX"):
                if "=" in i:
                    split = i.split("=")
                    if len(split) == 2:
                        prefix = split[1]
                        self.seed_prefix(prefix)
            elif i.startswith("CHANLIMIT"):
                if ":" in i:
                    split = i.split(":")
                    if len(split) >= 2:
                        chanlimit = split[1]
                        self.seed_chanlimit(chanlimit)
            elif i.startswith("MAXCHANNELS"):
                if "=" in i:
                    split = i.split("=")
                    if len(split) == 2:
                        chanlimit = split[1]
                        self.seed_chanlimit(chanlimit)

    # We need to override these functions as Twisted discards
    #  the hostname and other useful information in the functions
    #  that these call by default
    def irc_JOIN(self, prefix, params):
        nick = prefix.split("!")[0]
        channel = params[-1]
        if nick == self.nickname:
            self.joined(channel)
        else:
            self.userJoined(prefix, channel)

    def irc_PART(self, prefix, params):
        nick = prefix.split("!")[0]
        channel = params[0]
        if len(params) >= 2:
            message = params[1]
        else:
            message = None
        if nick == self.nickname:
            self.left(prefix, channel, message)
        else:
            self.userLeft(prefix, channel, message)

    def irc_QUIT(self, prefix, params):
        # nick = prefix.split("!")[0]
        self.userQuit(prefix, params[0])

    def irc_NICK(self, prefix, params):
        nick = prefix.split("!", 1)[0]
        if nick == self.nickname:
            self.nickChanged(prefix, params[0])
        else:
            self.userRenamed(prefix, params[0])

    def irc_KICK(self, prefix, params):
        channel = params[0]
        kicked = params[1]
        message = params[-1]
        # Checks on whether it was us that was kicked are done in userKicked
        self.userKicked(kicked, channel, prefix, message)

    def irc_TOPIC(self, prefix, params):
        channel = params[0]
        newtopic = params[1]
        self.topicUpdated(prefix, channel, newtopic)

    # End of Twisted hackery

    def regPing(self, negativepass=None, reset=True):
        if not main.config["AutoReg"]:
            return
        if self.authenticated:
            return
        if not regproc.needToAuth(self.net):
            self.authenticated = True
            return
        sinst = regproc.substitute(self.net, self.num)
        if not sinst:
            error(f"regPing() {self.net}: registration ping failed for {self.num}")
            return
        if reset:
            self._negativePass = None

        debug(
            f"regPing() {self.net} - {self.num}: _negativePass:{self._negativePass} negativepass:{negativepass}"
        )
        if self._negativePass is None:
            # We have failed, the blacklisted message has been found
            if negativepass is False:
                self._negativePass = False
                debug(
                    (
                        f"regPing() {self.net} - {self.num} not passed negative:checknegativemsg "
                        f"check, {sinst['checknegativemsg']} present in message"
                    )
                )
                return
            # End of negative output reached with no blacklisted message
            elif negativepass is True:
                if self._negativePass is None:  # check if it's not failed yet
                    self._negativePass = True
                    debug(
                        (
                            f"regPing() {self.net} - {self.num} passed negative:checkendnegative "
                            f"check, {sinst['checkendnegative']} present in message"
                        )
                    )
                else:
                    debug(
                        f"regPing() {self.net}: negative registration check - {self.num}"
                    )
                    return
            else:
                if sinst["negative"]:
                    self.msg(sinst["entity"], sinst["negativemsg"])
                    debug(
                        (
                            f"regPing() {self.net}: sent negativemsg "
                            f"'{sinst['negativemsg']}' to {sinst['entity']} - {self.num}"
                        )
                    )
                    return
                else:
                    self._negativePass = (
                        True  # if it's disabled, we still want the next block to run
                    )

        # Only run if negativepass has completed, or exempted as above
        if self._negativePass:
            if sinst["check"]:
                if sinst["ping"]:
                    self.msg(sinst["entity"], sinst["pingmsg"])
                    debug(
                        f"regPing() {self.net}: sent ping '{sinst['pingmsg']}' to {sinst['entity']} - {self.num}"
                    )
                    return
            else:
                self.authenticated = True

    def signedOn(self):
        log("signed on: %s - %i" % (self.net, self.num))
        ctime = str(datetime.now().isoformat())
        sendRelayNotification(
            {
                "type": "conn",
                "net": self.net,
                "num": self.num,
                "status": "signedon",
                "ts": ctime,
            }
        )
        if not self.authenticated:
            reactor.callLater(10, self.regPing)

    def setup_who_loop(self, channel):
        # if main.config["Toggles"]["Who"]:
        lc = LoopingCall(self.who, channel)
        self._getWho[channel] = lc
        intrange = main.config["Tweaks"]["Delays"]["WhoRange"]
        minint = main.config["Tweaks"]["Delays"]["WhoLoop"]
        interval = randint(minint, minint + intrange)
        lc.start(interval)

    def joined(self, channel):
        if channel not in self.channels:
            self.channels.append(channel)
        self.names(channel).addCallback(self.got_names)
        if main.config["Toggles"]["Who"]:
            reactor.callLater(
                main.config["Tweaks"]["Delays"]["WhoDelay"],
                self.setup_who_loop,
                channel,
            )
            # lc = LoopingCall(self.who, channel)
            # self._getWho[channel] = lc
            # intrange = main.config["Tweaks"]["Delays"]["WhoRange"]
            # minint = main.config["Tweaks"]["Delays"]["WhoLoop"]
            # interval = randint(minint, minint + intrange)
            # lc.start(interval)

    def botLeft(self, channel):
        if channel in self.channels:
            self.channels.remove(channel)
        if channel in self._getWho.keys():
            lc = self._getWho[channel]
            lc.stop()
            del self._getWho[channel]
        userinfo.delChannels(
            self.net, [channel]
        )  # < we do not need to deduplicate this
        # log("Can no longer cover %s, removing records" % channel)#  as it will only be matched once --
        #  other bots have different nicknames so

    def left(self, user, channel, message):  # even if they saw it, they wouldn't react
        self.event(type="part", muser=user, channel=channel, msg=message)
        self.botLeft(channel)

    def userJoined(self, user, channel):
        self.event(type="join", muser=user, channel=channel)

    def userLeft(self, user, channel, message):
        self.event(type="part", muser=user, channel=channel, msg=message)

    def userQuit(self, user, quitMessage):
        self.chanlessEvent({"type": "quit", "muser": user, "msg": quitMessage})

    def userKicked(self, kickee, channel, kicker, message):
        if kickee.lower() == self.nickname.lower():
            self.botLeft(channel)
        self.event(type="kick", muser=kicker, channel=channel, msg=message, user=kickee)

    def chanlessEvent(self, cast):
        cast["ts"] = int(datetime.now().timestamp())
        cast["nick"], cast["ident"], cast["host"] = parsen(cast["muser"])
        if dedup(self.name, cast):  # Needs to be kept self.name until the dedup
            #  function is converted to the new net, num
            #  format
            return  # stop right there sir!
        chans = userinfo.getChanList(self.net, cast["nick"])
        if chans is None:
            error("No channels returned for chanless event: %s" % cast)
            #    self.event(**cast) -- no, should NEVER happen
            return
        # getChansSingle returns all channels of the user, we only want to use
        #  ones we have common with them
        realChans = set(chans).intersection(set(self.channels))
        for i in realChans:
            cast["channel"] = i
            self.event(**cast)

    def userRenamed(self, oldname, newname):
        self.chanlessEvent({"type": "nick", "muser": oldname, "user": newname})

    def topicUpdated(self, user, channel, newTopic):
        self.event(type="topic", muser=user, channel=channel, msg=newTopic)

    def modeChanged(self, user, channel, toset, modes, args):
        argList = list(args)
        modeList = [i for i in modes]
        for a, m in zip(argList, modeList):
            self.event(
                type="mode",
                muser=user,
                channel=channel,
                mode=m,
                status=toset,
                modearg=a,
            )


# TODO: strip out relay functionality
class IRCBotFactory(ReconnectingClientFactory):
    def __init__(self, net, num=None, relayCommands=None, user=None, stage2=None):
        if net is None:
            self.num = num
            self.net = None
            self.name = "relay - %i" % num
            self.relay = True
        else:
            self.name = net + str(num)
            self.num = num
            self.net = net
            self.relay = False
        self.client = None
        self.maxDelay = main.config["Tweaks"]["Delays"]["MaxDelay"]
        self.initialDelay = main.config["Tweaks"]["Delays"]["InitialDelay"]
        self.factor = main.config["Tweaks"]["Delays"]["Factor"]
        self.jitter = main.config["Tweaks"]["Delays"]["Jitter"]

        self.relayCommands, self.user, self.stage2 = relayCommands, user, stage2

    def buildProtocol(self, addr):
        entry = IRCBot(self.net, self.num)
        main.IRCPool[self.name] = entry

        self.client = entry
        return entry

    def clientConnectionLost(self, connector, reason):
        if not self.relay:
            userinfo.delChannels(self.net, self.client.channels)
        if self.client is not None:
            self.client.isconnected = False
            self.client.authenticated = False
            self.client.channels = []
        error = reason.getErrorMessage()
        log("%s - %i: connection lost: %s" % (self.net, self.num, error))
        sendAll("%s - %i: connection lost: %s" % (self.net, self.num, error))
        ctime = str(datetime.now().isoformat())
        sendRelayNotification(
            {
                "type": "conn",
                "net": self.net,
                "num": self.num,
                "status": "lost",
                "message": error,
                "ts": ctime,
            }
        )
        self.retry(connector)
        # ReconnectingClientFactory.clientConnectionLost(self, connector, reason)

    def clientConnectionFailed(self, connector, reason):
        if self.client is not None:
            self.client.isconnected = False
            self.client.authenticated = False
            self.client.channels = []
        error = reason.getErrorMessage()
        log("%s - %i: connection failed: %s" % (self.net, self.num, error))
        if not self.relay:
            sendAll("%s - %s: connection failed: %s" % (self.net, self.num, error))
            ctime = str(datetime.now().isoformat())
            sendRelayNotification(
                {
                    "type": "conn",
                    "net": self.net,
                    "num": self.num,
                    "status": "failed",
                    "message": error,
                    "ts": ctime,
                }
            )
            self.retry(connector)
        # ReconnectingClientFactory.clientConnectionFailed(self, connector, reason)
