/*
 * Copyright (C) 2004-2025 ZNC, see the NOTICE file for details.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <znc/IRCNetwork.h>
#include <znc/User.h>
#include <znc/FileUtils.h>
#include <znc/Config.h>
#include <znc/IRCSock.h>
#include <znc/Server.h>
#include <znc/Chan.h>
#include <znc/Query.h>
#include <znc/Message.h>
#include <algorithm>
#include <memory>

using std::vector;
using std::set;

class CIRCNetworkPingTimer : public CCron {
  public:
    CIRCNetworkPingTimer(CIRCNetwork* pNetwork)
        : CCron(), m_pNetwork(pNetwork) {
        SetName("CIRCNetworkPingTimer::" +
                m_pNetwork->GetUser()->GetUsername() + "::" +
                m_pNetwork->GetName());
        Start(m_pNetwork->GetUser()->GetPingSlack());
    }

    ~CIRCNetworkPingTimer() override {}

    CIRCNetworkPingTimer(const CIRCNetworkPingTimer&) = delete;
    CIRCNetworkPingTimer& operator=(const CIRCNetworkPingTimer&) = delete;

  protected:
    void RunJob() override {
        CIRCSock* pIRCSock = m_pNetwork->GetIRCSock();
        auto uFrequency = m_pNetwork->GetUser()->GetPingFrequency();

        if (pIRCSock &&
            pIRCSock->GetTimeSinceLastDataTransaction() >= uFrequency) {
            pIRCSock->PutIRCQuick("PING :ZNC");
        }

        const vector<CClient*>& vClients = m_pNetwork->GetClients();
        for (CClient* pClient : vClients) {
            if (pClient->GetTimeSinceLastDataTransaction() >= uFrequency) {
                pClient->PutClient("PING :ZNC");
            }
        }

        // Restart timer for the case if the period had changed. Usually this is
        // noop
        Start(m_pNetwork->GetUser()->GetPingSlack());
    }

  private:
    CIRCNetwork* m_pNetwork;
};

class CIRCNetworkJoinTimer : public CCron {
    constexpr static int JOIN_FREQUENCY = 30 /* seconds */;

  public:
    CIRCNetworkJoinTimer(CIRCNetwork* pNetwork)
        : CCron(), m_bDelayed(false), m_pNetwork(pNetwork) {
        SetName("CIRCNetworkJoinTimer::" +
                m_pNetwork->GetUser()->GetUsername() + "::" +
                m_pNetwork->GetName());
        Start(JOIN_FREQUENCY);
    }

    ~CIRCNetworkJoinTimer() override {}

    CIRCNetworkJoinTimer(const CIRCNetworkJoinTimer&) = delete;
    CIRCNetworkJoinTimer& operator=(const CIRCNetworkJoinTimer&) = delete;

    void Delay(unsigned short int uDelay) {
        m_bDelayed = true;
        Start(uDelay);
    }

  protected:
    void RunJob() override {
        if (m_bDelayed) {
            m_bDelayed = false;
            Start(JOIN_FREQUENCY);
        }
        if (m_pNetwork->IsIRCConnected()) {
            m_pNetwork->JoinChans();
        }
    }

  private:
    bool m_bDelayed;
    CIRCNetwork* m_pNetwork;
};

bool CIRCNetwork::IsValidNetwork(const CString& sNetwork) {
    // ^[-\w]+$

    if (sNetwork.empty()) {
        return false;
    }

    const char* p = sNetwork.c_str();
    while (*p) {
        if (*p != '_' && *p != '-' && !isalnum(*p)) {
            return false;
        }

        p++;
    }

    return true;
}

CIRCNetwork::CIRCNetwork(CUser* pUser, const CString& sName)
    : m_sName(sName),
      m_pUser(nullptr),
      m_sNick(""),
      m_sAltNick(""),
      m_sIdent(""),
      m_sRealName(""),
      m_sBindHost(""),
      m_sEncoding(""),
      m_sQuitMsg(""),
      m_ssTrustedFingerprints(),
      m_pModules(new CModules),
      m_vClients(),
      m_pIRCSock(nullptr),
      m_vChans(),
      m_vQueries(),
      m_sChanPrefixes(""),
      m_bIRCConnectEnabled(true),
      m_bTrustAllCerts(false),
      m_bTrustPKI(true),
      m_sIRCServer(""),
      m_vServers(),
      m_uServerIdx(0),
      m_IRCNick(),
      m_bIRCAway(false),
      m_fFloodRate(2),
      m_uFloodBurst(9),
      m_RawBuffer(),
      m_MotdBuffer(),
      m_NoticeBuffer(),
      m_pPingTimer(nullptr),
      m_pJoinTimer(nullptr),
      m_uJoinDelay(0),
      m_uBytesRead(0),
      m_uBytesWritten(0) {
    SetUser(pUser);

    // This should be more than enough raws, especially since we are buffering
    // the MOTD separately
    m_RawBuffer.SetLineCount(100, true);
    // This should be more than enough motd lines
    m_MotdBuffer.SetLineCount(200, true);
    m_NoticeBuffer.SetLineCount(250, true);

    m_pPingTimer = new CIRCNetworkPingTimer(this);
    CZNC::Get().GetManager().AddCron(m_pPingTimer);

    m_pJoinTimer = new CIRCNetworkJoinTimer(this);
    CZNC::Get().GetManager().AddCron(m_pJoinTimer);

    SetIRCConnectEnabled(true);
}

CIRCNetwork::CIRCNetwork(CUser* pUser, const CIRCNetwork& Network)
    : CIRCNetwork(pUser, "") {
    Clone(Network);
}

void CIRCNetwork::Clone(const CIRCNetwork& Network, bool bCloneName) {
    if (bCloneName) {
        m_sName = Network.GetName();
    }

    m_fFloodRate = Network.GetFloodRate();
    m_uFloodBurst = Network.GetFloodBurst();
    m_uJoinDelay = Network.GetJoinDelay();

    SetNick(Network.GetNick());
    SetAltNick(Network.GetAltNick());
    SetIdent(Network.GetIdent());
    SetRealName(Network.GetRealName());
    SetBindHost(Network.GetBindHost());
    SetEncoding(Network.GetEncoding());
    SetQuitMsg(Network.GetQuitMsg());
    m_ssTrustedFingerprints = Network.m_ssTrustedFingerprints;

    // Servers
    const vector<CServer*>& vServers = Network.GetServers();
    CString sServer;
    CServer* pCurServ = GetCurrentServer();

    if (pCurServ) {
        sServer = pCurServ->GetName();
    }

    DelServers();

    for (CServer* pServer : vServers) {
        AddServer(*pServer);
    }

    m_uServerIdx = 0;
    for (size_t a = 0; a < m_vServers.size(); a++) {
        if (sServer.Equals(m_vServers[a]->GetName())) {
            m_uServerIdx = a + 1;
            break;
        }
    }
    if (m_uServerIdx == 0) {
        m_uServerIdx = m_vServers.size();
        CIRCSock* pSock = GetIRCSock();

        if (pSock) {
            PutStatus(
                t_s("Jumping servers because this server is no longer in the "
                    "list"));
            pSock->Quit();
        }
    }
    // !Servers

    // Chans
    const vector<CChan*>& vChans = Network.GetChans();
    for (CChan* pNewChan : vChans) {
        CChan* pChan = FindChan(pNewChan->GetName());

        if (pChan) {
            pChan->SetInConfig(pNewChan->InConfig());
        } else {
            AddChan(pNewChan->GetName(), pNewChan->InConfig());
        }
    }

    for (CChan* pChan : m_vChans) {
        CChan* pNewChan = Network.FindChan(pChan->GetName());

        if (!pNewChan) {
            pChan->SetInConfig(false);
        } else {
            pChan->Clone(*pNewChan);
        }
    }
    // !Chans

    // Modules
    set<CString> ssUnloadMods;
    CModules& vCurMods = GetModules();
    const CModules& vNewMods = Network.GetModules();

    for (CModule* pNewMod : vNewMods) {
        CString sModRet;
        CModule* pCurMod = vCurMods.FindModule(pNewMod->GetModName());

        if (!pCurMod) {
            vCurMods.LoadModule(pNewMod->GetModName(), pNewMod->GetArgs(),
                                CModInfo::NetworkModule, m_pUser, this,
                                sModRet);
        } else if (pNewMod->GetArgs() != pCurMod->GetArgs()) {
            vCurMods.ReloadModule(pNewMod->GetModName(), pNewMod->GetArgs(),
                                  m_pUser, this, sModRet);
        }
    }

    for (CModule* pCurMod : vCurMods) {
        CModule* pNewMod = vNewMods.FindModule(pCurMod->GetModName());

        if (!pNewMod) {
            ssUnloadMods.insert(pCurMod->GetModName());
        }
    }

    for (const CString& sMod : ssUnloadMods) {
        vCurMods.UnloadModule(sMod);
    }
    // !Modules

    SetIRCConnectEnabled(Network.GetIRCConnectEnabled());
}

CIRCNetwork::~CIRCNetwork() {
    if (m_pIRCSock) {
        CZNC::Get().GetManager().DelSockByAddr(m_pIRCSock);
        m_pIRCSock = nullptr;
    }

    // Delete clients
    while (!m_vClients.empty()) {
        CZNC::Get().GetManager().DelSockByAddr(m_vClients[0]);
    }
    m_vClients.clear();

    // Delete servers
    DelServers();

    // Delete modules (this unloads all modules)
    delete m_pModules;
    m_pModules = nullptr;

    // Delete Channels
    for (CChan* pChan : m_vChans) {
        delete pChan;
    }
    m_vChans.clear();

    // Delete Queries
    for (CQuery* pQuery : m_vQueries) {
        delete pQuery;
    }
    m_vQueries.clear();

    CUser* pUser = GetUser();
    SetUser(nullptr);

    // Make sure we are not in the connection queue
    CZNC::Get().GetConnectionQueue().remove(this);

    CZNC::Get().GetManager().DelCronByAddr(m_pPingTimer);
    CZNC::Get().GetManager().DelCronByAddr(m_pJoinTimer);

    if (pUser) {
        pUser->AddBytesRead(m_uBytesRead);
        pUser->AddBytesWritten(m_uBytesWritten);
    } else {
        CZNC::Get().AddBytesRead(m_uBytesRead);
        CZNC::Get().AddBytesWritten(m_uBytesWritten);
    }
}

void CIRCNetwork::DelServers() {
    for (CServer* pServer : m_vServers) {
        delete pServer;
    }
    m_vServers.clear();
}

CString CIRCNetwork::GetNetworkPath() const {
    CString sNetworkPath = m_pUser->GetUserPath() + "/networks/" + m_sName;

    if (!CFile::Exists(sNetworkPath)) {
        CDir::MakeDir(sNetworkPath);
    }

    return sNetworkPath;
}

namespace {
template <class T>
struct TOption {
    const char* name;
    void (CIRCNetwork::*pSetter)(T);
};
}

bool CIRCNetwork::ParseConfig(CConfig* pConfig, CString& sError,
                              bool bUpgrade) {
    VCString vsList;

    if (!bUpgrade) {
        TOption<const CString&> StringOptions[] = {
            {"nick", &CIRCNetwork::SetNick},
            {"altnick", &CIRCNetwork::SetAltNick},
            {"ident", &CIRCNetwork::SetIdent},
            {"realname", &CIRCNetwork::SetRealName},
            {"bindhost", &CIRCNetwork::SetBindHost},
            {"encoding", &CIRCNetwork::SetEncoding},
            {"quitmsg", &CIRCNetwork::SetQuitMsg},
        };
        TOption<bool> BoolOptions[] = {
            {"ircconnectenabled", &CIRCNetwork::SetIRCConnectEnabled},
            {"trustallcerts", &CIRCNetwork::SetTrustAllCerts},
            {"trustpki", &CIRCNetwork::SetTrustPKI},
        };
        TOption<double> DoubleOptions[] = {
            {"floodrate", &CIRCNetwork::SetFloodRate},
        };
        TOption<short unsigned int> SUIntOptions[] = {
            {"floodburst", &CIRCNetwork::SetFloodBurst},
            {"joindelay", &CIRCNetwork::SetJoinDelay},
        };

        for (const auto& Option : StringOptions) {
            CString sValue;
            if (pConfig->FindStringEntry(Option.name, sValue))
                (this->*Option.pSetter)(sValue);
        }

        for (const auto& Option : BoolOptions) {
            CString sValue;
            if (pConfig->FindStringEntry(Option.name, sValue))
                (this->*Option.pSetter)(sValue.ToBool());
        }

        for (const auto& Option : DoubleOptions) {
            double fValue;
            if (pConfig->FindDoubleEntry(Option.name, fValue))
                (this->*Option.pSetter)(fValue);
        }

        for (const auto& Option : SUIntOptions) {
            unsigned short value;
            if (pConfig->FindUShortEntry(Option.name, value))
                (this->*Option.pSetter)(value);
        }

        pConfig->FindStringVector("loadmodule", vsList);
        for (const CString& sValue : vsList) {
            CString sModName = sValue.Token(0);
            CString sNotice = "Loading network module [" + sModName + "]";

            // XXX Legacy crap, added in ZNC 0.203, modified in 0.207
            // Note that 0.203 == 0.207
            if (sModName == "away") {
                sNotice =
                    "NOTICE: [away] was renamed, loading [awaystore] instead";
                sModName = "awaystore";
            }

            // XXX Legacy crap, added in ZNC 0.207
            if (sModName == "autoaway") {
                sNotice =
                    "NOTICE: [autoaway] was renamed, loading [awaystore] "
                    "instead";
                sModName = "awaystore";
            }

            // XXX Legacy crap, added in 1.1; fakeonline module was dropped in
            // 1.0 and returned in 1.1
            if (sModName == "fakeonline") {
                sNotice =
                    "NOTICE: [fakeonline] was renamed, loading "
                    "[modules_online] instead";
                sModName = "modules_online";
            }

            CString sModRet;
            CString sArgs = sValue.Token(1, true);

            bool bModRet = LoadModule(sModName, sArgs, sNotice, sModRet);

            if (!bModRet) {
                // Q is removed in znc 1.8
                if (sModName == "q") {
                    CUtils::PrintError(
                        "NOTICE: [q] is unavailable, cannot load.");
                    CUtils::PrintError(
                        "NOTICE: [q] is removed in this release of ZNC. Please "
                        "either remove it from your config or install it as a "
                        "third party module with the same name.");
                    CUtils::PrintError(
                        "NOTICE: More info can be found on "
                        "https://wiki.znc.in/Q");
                    return false;
                }

                // Partyline is removed in znc 1.8
                if (sModName == "partyline") {
                    CUtils::PrintError(
                        "NOTICE: [partyline] is unavailable, cannot load.");
                    CUtils::PrintError(
                        "NOTICE: [partyline] is removed in this release"
                        " of ZNC. Please either remove it from your config or "
                        "install it as a third party module with the same "
                        "name.");
                    CUtils::PrintError(
                        "NOTICE: More info can be found on "
                        "https://wiki.znc.in/Partyline");
                    return false;
                }

                // XXX The awaynick module was retired in 1.6 (still available
                // as external module)
                if (sModName == "awaynick") {
                    // load simple_away instead, unless it's already on the list
                    bool bFound = false;
                    for (const CString& sLoadMod : vsList) {
                        if (sLoadMod.Token(0).Equals("simple_away")) {
                            bFound = true;
                        }
                    }
                    if (!bFound) {
                        sNotice =
                            "NOTICE: awaynick was retired, loading network "
                            "module [simple_away] instead; if you still need "
                            "awaynick, install it as an external module";
                        sModName = "simple_away";
                        // not a fatal error if simple_away is not available
                        LoadModule(sModName, sArgs, sNotice, sModRet);
                    }
                } else {
                    sError = sModRet;
                    return false;
                }
            }
        }
    }

    pConfig->FindStringVector("server", vsList);
    CUtils::PrintAction("Adding " + CString(vsList.size()) + " servers");
    for (const CString& sServer : vsList) {
        CUtils::PrintStatus(AddServer(sServer));
    }

    pConfig->FindStringVector("trustedserverfingerprint", vsList);
    for (const CString& sFP : vsList) {
        AddTrustedFingerprint(sFP);
    }

    pConfig->FindStringVector("chan", vsList);
    for (const CString& sChan : vsList) {
        AddChan(sChan, true);
    }

    CConfig::SubConfig subConf;
    CConfig::SubConfig::const_iterator subIt;

    pConfig->FindSubConfig("chan", subConf);
    for (subIt = subConf.begin(); subIt != subConf.end(); ++subIt) {
        const CString& sChanName = subIt->first;
        CConfig* pSubConf = subIt->second.m_pSubConfig;
        CChan* pChan = new CChan(sChanName, this, true, pSubConf);

        if (!pSubConf->empty()) {
            sError = "Unhandled lines in config for User [" +
                     m_pUser->GetUsername() + "], Network [" + GetName() +
                     "], Channel [" + sChanName + "]!";
            CUtils::PrintError(sError);

            CZNC::DumpConfig(pSubConf);
            delete pChan;
            return false;
        }

        // Save the channel name, because AddChan
        // deletes the CChannel*, if adding fails
        sError = pChan->GetName();
        if (!AddChan(pChan)) {
            sError = "Channel [" + sError + "] defined more than once";
            CUtils::PrintError(sError);
            return false;
        }
        sError.clear();
    }

    return true;
}

CConfig CIRCNetwork::ToConfig() const {
    CConfig config;

    if (!m_sNick.empty()) {
        config.AddKeyValuePair("Nick", m_sNick);
    }

    if (!m_sAltNick.empty()) {
        config.AddKeyValuePair("AltNick", m_sAltNick);
    }

    if (!m_sIdent.empty()) {
        config.AddKeyValuePair("Ident", m_sIdent);
    }

    if (!m_sRealName.empty()) {
        config.AddKeyValuePair("RealName", m_sRealName);
    }
    if (!m_sBindHost.empty()) {
        config.AddKeyValuePair("BindHost", m_sBindHost);
    }

    config.AddKeyValuePair("IRCConnectEnabled",
                           CString(GetIRCConnectEnabled()));
    config.AddKeyValuePair("TrustAllCerts", CString(GetTrustAllCerts()));
    config.AddKeyValuePair("TrustPKI", CString(GetTrustPKI()));
    config.AddKeyValuePair("FloodRate", CString(GetFloodRate()));
    config.AddKeyValuePair("FloodBurst", CString(GetFloodBurst()));
    config.AddKeyValuePair("JoinDelay", CString(GetJoinDelay()));
    config.AddKeyValuePair("Encoding", m_sEncoding);

    if (!m_sQuitMsg.empty()) {
        config.AddKeyValuePair("QuitMsg", m_sQuitMsg);
    }

    // Modules
    const CModules& Mods = GetModules();

    if (!Mods.empty()) {
        for (CModule* pMod : Mods) {
            CString sArgs = pMod->GetArgs();

            if (!sArgs.empty()) {
                sArgs = " " + sArgs;
            }

            config.AddKeyValuePair("LoadModule", pMod->GetModName() + sArgs);
        }
    }

    // Servers
    for (CServer* pServer : m_vServers) {
        config.AddKeyValuePair("Server", pServer->GetString());
    }

    for (const CString& sFP : m_ssTrustedFingerprints) {
        config.AddKeyValuePair("TrustedServerFingerprint", sFP);
    }

    // Chans
    for (CChan* pChan : m_vChans) {
        if (pChan->InConfig()) {
            config.AddSubConfig("Chan", pChan->GetName(), pChan->ToConfig());
        }
    }

    return config;
}

void CIRCNetwork::BounceAllClients() {
    for (CClient* pClient : m_vClients) {
        pClient->BouncedOff();
    }

    m_vClients.clear();
}

bool CIRCNetwork::IsUserOnline() const {
    for (CClient* pClient : m_vClients) {
        if (!pClient->IsAway()) {
            return true;
        }
    }

    return false;
}

void CIRCNetwork::ClientConnected(CClient* pClient) {
    if (!m_pUser->MultiClients()) {
        BounceAllClients();
    }

    m_vClients.push_back(pClient);

    size_t uIdx, uSize;

    pClient->SetPlaybackActive(true);

    if (m_RawBuffer.IsEmpty()) {
        pClient->PutClient(":irc.znc.in 001 " + pClient->GetNick() +
                           " :" + t_s("Welcome to ZNC"));
    } else {
        const CString& sClientNick = pClient->GetNick(false);
        MCString msParams;
        msParams["target"] = sClientNick;

        uSize = m_RawBuffer.Size();
        for (uIdx = 0; uIdx < uSize; uIdx++) {
            pClient->PutClient(m_RawBuffer.GetLine(uIdx, *pClient, msParams));
        }

        const CNick& Nick = GetIRCNick();
        if (sClientNick != Nick.GetNick()) {  // case-sensitive match
            pClient->PutClient(":" + sClientNick + "!" + Nick.GetIdent() + "@" +
                               Nick.GetHost() + " NICK :" + Nick.GetNick());
            pClient->SetNick(Nick.GetNick());
        }
    }

    MCString msParams;
    msParams["target"] = GetIRCNick().GetNick();

    // Send the cached MOTD
    uSize = m_MotdBuffer.Size();
    if (uSize > 0) {
        for (uIdx = 0; uIdx < uSize; uIdx++) {
            pClient->PutClient(m_MotdBuffer.GetLine(uIdx, *pClient, msParams));
        }
    }

    if (GetIRCSock() != nullptr) {
        CString sUserMode("");
        const set<char>& scUserModes = GetIRCSock()->GetUserModes();
        for (char cMode : scUserModes) {
            sUserMode += cMode;
        }
        if (!sUserMode.empty()) {
            pClient->PutClient(":" + GetIRCNick().GetNickMask() + " MODE " +
                               GetIRCNick().GetNick() + " :+" + sUserMode);
        }
    }

    if (m_bIRCAway) {
        // If they want to know their away reason they'll have to whois
        // themselves. At least we can tell them their away status...
        pClient->PutClient(":irc.znc.in 306 " + GetIRCNick().GetNick() +
                           " :You have been marked as being away");
    }

    const vector<CChan*>& vChans = GetChans();
    for (CChan* pChan : vChans) {
        if ((pChan->IsOn()) && (!pChan->IsDetached())) {
            pChan->AttachUser(pClient);
        }
    }

    bool bClearQuery = m_pUser->AutoClearQueryBuffer();
    for (CQuery* pQuery : m_vQueries) {
        pQuery->SendBuffer(pClient);
        if (bClearQuery) {
            delete pQuery;
        }
    }
    if (bClearQuery) {
        m_vQueries.clear();
    }

    uSize = m_NoticeBuffer.Size();
    for (uIdx = 0; uIdx < uSize; uIdx++) {
        const CBufLine& BufLine = m_NoticeBuffer.GetBufLine(uIdx);
        CMessage Message(BufLine.GetLine(*pClient, msParams));
        Message.SetNetwork(this);
        Message.SetClient(pClient);
        Message.SetTime(BufLine.GetTime());
        Message.SetTags(BufLine.GetTags());
        bool bContinue = false;
        NETWORKMODULECALL(OnPrivBufferPlayMessage(Message), m_pUser, this,
                          nullptr, &bContinue);
        if (bContinue) continue;
        pClient->PutClient(Message);
    }
    m_NoticeBuffer.Clear();

    pClient->SetPlaybackActive(false);

    // Tell them why they won't connect
    if (!GetIRCConnectEnabled())
        pClient->PutStatus(
            t_s("You are currently disconnected from IRC. Use 'connect' to "
                "reconnect."));
}

void CIRCNetwork::ClientDisconnected(CClient* pClient) {
    auto it = std::find(m_vClients.begin(), m_vClients.end(), pClient);
    if (it != m_vClients.end()) {
        m_vClients.erase(it);
    }
}

CUser* CIRCNetwork::GetUser() const { return m_pUser; }

const CString& CIRCNetwork::GetName() const { return m_sName; }

std::vector<CClient*> CIRCNetwork::FindClients(
    const CString& sIdentifier) const {
    std::vector<CClient*> vClients;
    for (CClient* pClient : m_vClients) {
        if (pClient->GetIdentifier().Equals(sIdentifier)) {
            vClients.push_back(pClient);
        }
    }

    return vClients;
}

void CIRCNetwork::SetUser(CUser* pUser) {
    for (CClient* pClient : m_vClients) {
        pClient->PutStatus(
            t_s("This network is being deleted or moved to another user."));
        pClient->SetNetwork(nullptr);
    }

    m_vClients.clear();

    if (m_pUser) {
        m_pUser->RemoveNetwork(this);
    }

    m_pUser = pUser;
    if (m_pUser) {
        m_pUser->AddNetwork(this);
    }
}

bool CIRCNetwork::SetName(const CString& sName) {
    if (IsValidNetwork(sName)) {
        m_sName = sName;
        return true;
    }

    return false;
}

bool CIRCNetwork::PutUser(const CString& sLine, CClient* pClient,
                          CClient* pSkipClient) {
    for (CClient* pEachClient : m_vClients) {
        if ((!pClient || pClient == pEachClient) &&
            pSkipClient != pEachClient) {
            pEachClient->PutClient(sLine);

            if (pClient) {
                return true;
            }
        }
    }

    return (pClient == nullptr);
}

bool CIRCNetwork::PutUser(const CMessage& Message, CClient* pClient,
                          CClient* pSkipClient) {
    for (CClient* pEachClient : m_vClients) {
        if ((!pClient || pClient == pEachClient) &&
            pSkipClient != pEachClient) {
            pEachClient->PutClient(Message);

            if (pClient) {
                return true;
            }
        }
    }

    return (pClient == nullptr);
}

bool CIRCNetwork::PutStatus(const CString& sLine, CClient* pClient,
                            CClient* pSkipClient) {
    for (CClient* pEachClient : m_vClients) {
        if ((!pClient || pClient == pEachClient) &&
            pSkipClient != pEachClient) {
            pEachClient->PutStatus(sLine);

            if (pClient) {
                return true;
            }
        }
    }

    return (pClient == nullptr);
}

bool CIRCNetwork::PutModule(const CString& sModule, const CString& sLine,
                            CClient* pClient, CClient* pSkipClient) {
    for (CClient* pEachClient : m_vClients) {
        if ((!pClient || pClient == pEachClient) &&
            pSkipClient != pEachClient) {
            pEachClient->PutModule(sModule, sLine);

            if (pClient) {
                return true;
            }
        }
    }

    return (pClient == nullptr);
}

// Channels

const vector<CChan*>& CIRCNetwork::GetChans() const { return m_vChans; }

CChan* CIRCNetwork::FindChan(CString sName) const {
    if (GetIRCSock()) {
        // See
        // https://tools.ietf.org/html/draft-brocklesby-irc-isupport-03#section-3.16
        sName.TrimLeft(GetIRCSock()->GetISupport("STATUSMSG", ""));
    }

    for (CChan* pChan : m_vChans) {
        if (sName.Equals(pChan->GetName())) {
            return pChan;
        }
    }

    return nullptr;
}

std::vector<CChan*> CIRCNetwork::FindChans(const CString& sWild) const {
    std::vector<CChan*> vChans;
    vChans.reserve(m_vChans.size());
    const CString sLower = sWild.AsLower();
    for (CChan* pChan : m_vChans) {
        if (pChan->GetName().AsLower().WildCmp(sLower)) vChans.push_back(pChan);
    }
    return vChans;
}

bool CIRCNetwork::AddChan(CChan* pChan) {
    if (!pChan) {
        return false;
    }

    for (CChan* pEachChan : m_vChans) {
        if (pEachChan->GetName().Equals(pChan->GetName())) {
            delete pChan;
            return false;
        }
    }

    m_vChans.push_back(pChan);
    return true;
}

bool CIRCNetwork::AddChan(const CString& sName, bool bInConfig) {
    if (sName.empty() || FindChan(sName)) {
        return false;
    }

    CChan* pChan = new CChan(sName, this, bInConfig);
    m_vChans.push_back(pChan);
    return true;
}

bool CIRCNetwork::DelChan(const CString& sName) {
    for (vector<CChan*>::iterator a = m_vChans.begin(); a != m_vChans.end();
         ++a) {
        if (sName.Equals((*a)->GetName())) {
            delete *a;
            m_vChans.erase(a);
            return true;
        }
    }

    return false;
}

bool CIRCNetwork::MoveChan(const CString& sChan, unsigned int uIndex,
                           CString& sError) {
    if (uIndex >= m_vChans.size()) {
        sError = t_s("Invalid index");
        return false;
    }

    auto it = m_vChans.begin();
    for (; it != m_vChans.end(); ++it)
        if ((*it)->GetName().Equals(sChan)) break;
    if (it == m_vChans.end()) {
        sError = t_f("You are not on {1}")(sChan);
        return false;
    }

    const auto pChan = *it;
    m_vChans.erase(it);
    m_vChans.insert(m_vChans.begin() + uIndex, pChan);
    return true;
}

bool CIRCNetwork::SwapChans(const CString& sChan1, const CString& sChan2,
                            CString& sError) {
    auto it1 = m_vChans.begin();
    for (; it1 != m_vChans.end(); ++it1)
        if ((*it1)->GetName().Equals(sChan1)) break;
    if (it1 == m_vChans.end()) {
        sError = t_f("You are not on {1}")(sChan1);
        return false;
    }

    auto it2 = m_vChans.begin();
    for (; it2 != m_vChans.end(); ++it2)
        if ((*it2)->GetName().Equals(sChan2)) break;
    if (it2 == m_vChans.end()) {
        sError = t_f("You are not on {1}")(sChan2);
        return false;
    }

    std::swap(*it1, *it2);
    return true;
}

void CIRCNetwork::JoinChans() {
    // Avoid divsion by zero, it's bad!
    if (m_vChans.empty()) return;

    // We start at a random offset into the channel list so that if your
    // first 3 channels are invite-only and you got MaxJoins == 3, ZNC will
    // still be able to join the rest of your channels.
    unsigned int start = rand() % m_vChans.size();
    unsigned int uJoins = m_pUser->MaxJoins();
    set<CChan*> sChans;
    for (unsigned int a = 0; a < m_vChans.size(); a++) {
        unsigned int idx = (start + a) % m_vChans.size();
        CChan* pChan = m_vChans[idx];
        if (!pChan->IsOn() && !pChan->IsDisabled()) {
            if (!JoinChan(pChan)) continue;

            sChans.insert(pChan);

            // Limit the number of joins
            if (uJoins != 0 && --uJoins == 0) {
                // Reset the timer.
                m_pJoinTimer->Reset();
                break;
            }
        }
    }

    while (!sChans.empty()) JoinChans(sChans);
}

void CIRCNetwork::JoinChans(set<CChan*>& sChans) {
    CString sKeys, sJoin;
    bool bHaveKey = false;
    size_t uiJoinLength = strlen("JOIN ");

    while (!sChans.empty()) {
        set<CChan*>::iterator it = sChans.begin();
        const CString& sName = (*it)->GetName();
        const CString& sKey = (*it)->GetKey();
        size_t len = sName.length() + sKey.length();
        len += 2;  // two comma

        if (!sKeys.empty() && uiJoinLength + len >= 512) break;

        if (!sJoin.empty()) {
            sJoin += ",";
            sKeys += ",";
        }
        uiJoinLength += len;
        sJoin += sName;
        if (!sKey.empty()) {
            sKeys += sKey;
            bHaveKey = true;
        }
        sChans.erase(it);
    }

    if (bHaveKey)
        PutIRC("JOIN " + sJoin + " " + sKeys);
    else
        PutIRC("JOIN " + sJoin);
}

bool CIRCNetwork::JoinChan(CChan* pChan) {
    bool bReturn = false;
    NETWORKMODULECALL(OnJoining(*pChan), m_pUser, this, nullptr, &bReturn);

    if (bReturn) return false;

    if (m_pUser->JoinTries() != 0 &&
        pChan->GetJoinTries() >= m_pUser->JoinTries()) {
        PutStatus(t_f("The channel {1} could not be joined, disabling it.")(
            pChan->GetName()));
        pChan->Disable();
    } else {
        pChan->IncJoinTries();
        bool bFailed = false;
        NETWORKMODULECALL(OnTimerAutoJoin(*pChan), m_pUser, this, nullptr,
                          &bFailed);
        if (bFailed) return false;
        return true;
    }
    return false;
}

bool CIRCNetwork::IsChan(const CString& sChan) const {
    if (sChan.empty()) return false;  // There is no way this is a chan
    if (GetChanPrefixes().empty())
        return true;  // We can't know, so we allow everything
    // Thanks to the above if (empty), we can do sChan[0]
    return GetChanPrefixes().find(sChan[0]) != CString::npos;
}

// Queries

const vector<CQuery*>& CIRCNetwork::GetQueries() const { return m_vQueries; }

CQuery* CIRCNetwork::FindQuery(const CString& sName) const {
    for (CQuery* pQuery : m_vQueries) {
        if (sName.Equals(pQuery->GetName())) {
            return pQuery;
        }
    }

    return nullptr;
}

std::vector<CQuery*> CIRCNetwork::FindQueries(const CString& sWild) const {
    std::vector<CQuery*> vQueries;
    vQueries.reserve(m_vQueries.size());
    const CString sLower = sWild.AsLower();
    for (CQuery* pQuery : m_vQueries) {
        if (pQuery->GetName().AsLower().WildCmp(sLower))
            vQueries.push_back(pQuery);
    }
    return vQueries;
}

CQuery* CIRCNetwork::AddQuery(const CString& sName) {
    if (sName.empty()) {
        return nullptr;
    }

    CQuery* pQuery = FindQuery(sName);
    if (!pQuery) {
        pQuery = new CQuery(sName, this);
        m_vQueries.push_back(pQuery);

        if (m_pUser->MaxQueryBuffers() > 0) {
            while (m_vQueries.size() > m_pUser->MaxQueryBuffers()) {
                delete *m_vQueries.begin();
                m_vQueries.erase(m_vQueries.begin());
            }
        }
    }

    return pQuery;
}

bool CIRCNetwork::DelQuery(const CString& sName) {
    for (vector<CQuery*>::iterator a = m_vQueries.begin();
         a != m_vQueries.end(); ++a) {
        if (sName.Equals((*a)->GetName())) {
            delete *a;
            m_vQueries.erase(a);
            return true;
        }
    }

    return false;
}

// Server list

const vector<CServer*>& CIRCNetwork::GetServers() const { return m_vServers; }

CServer* CIRCNetwork::FindServer(const CString& sName) const {
    for (CServer* pServer : m_vServers) {
        if (sName.Equals(pServer->GetName())) {
            return pServer;
        }
    }

    return nullptr;
}

bool CIRCNetwork::DelServer(const CString& sName, unsigned short uPort,
                            const CString& sPass) {
    if (sName.empty()) {
        return false;
    }

    CServer Server(sName, uPort, sPass);
    return DelServer(Server);
}

bool CIRCNetwork::DelServer(const CServer& Server) {
    unsigned int a = 0;
    bool bSawCurrentServer = false;
    CServer* pCurServer = GetCurrentServer();

    for (vector<CServer*>::iterator it = m_vServers.begin();
         it != m_vServers.end(); ++it, a++) {
        CServer* pServer = *it;

        if (pServer == pCurServer) bSawCurrentServer = true;

        // Unix sockets can be removed with "unix:" prefix and without, both
        // work - that's not part of GetName()
        if (!pServer->GetName().Equals(Server.GetName())) continue;

        // But it makes no sense to remove TCP server via "unix:hostname.com"
        if (!pServer->IsUnixSocket() && Server.IsUnixSocket()) continue;

        if (Server.GetPort() != 6667 && pServer->GetPort() != Server.GetPort()) continue;

        if (!Server.GetPass().empty() && pServer->GetPass() != Server.GetPass()) continue;

        m_vServers.erase(it);

        if (pServer == pCurServer) {
            CIRCSock* pIRCSock = GetIRCSock();

            // Make sure we don't skip the next server in the list!
            if (m_uServerIdx) {
                m_uServerIdx--;
            }

            if (pIRCSock) {
                pIRCSock->Quit();
                PutStatus(t_s("Your current server was removed, jumping..."));
            }
        } else if (!bSawCurrentServer) {
            // Our current server comes after the server which we
            // are removing. This means that it now got a different
            // index in m_vServers!
            m_uServerIdx--;
        }

        delete pServer;

        return true;
    }

    return false;
}

bool CIRCNetwork::AddServer(const CString& sName) {
    if (sName.empty()) {
        return false;
    }

    return AddServer(CServer::Parse(sName));
}

bool CIRCNetwork::AddServer(CServer Server) {
    if (Server.GetName().empty()) return false;
#ifndef HAVE_LIBSSL
    if (Server.IsSSL()) return false;
#endif

    // Check if server is already added
    for (CServer* pServer : m_vServers) {
        if (*pServer == Server) return false;
    }

    m_vServers.push_back(new CServer(std::move(Server)));
    CheckIRCConnect();
    return true;
}

bool CIRCNetwork::AddServer(const CString& sName, unsigned short uPort,
                            const CString& sPass, bool bSSL) {
#ifndef HAVE_LIBSSL
    if (bSSL) {
        return false;
    }
#endif

    if (sName.empty()) {
        return false;
    }

    return AddServer(CServer(sName, uPort, sPass, bSSL));
}

CServer* CIRCNetwork::GetNextServer(bool bAdvance) {
    if (m_vServers.empty()) {
        return nullptr;
    }

    if (m_uServerIdx >= m_vServers.size()) {
        m_uServerIdx = 0;
    }

    if (bAdvance) {
        return m_vServers[m_uServerIdx++];
    } else {
        return m_vServers[m_uServerIdx];
    }
}

CServer* CIRCNetwork::GetCurrentServer() const {
    size_t uIdx = (m_uServerIdx) ? m_uServerIdx - 1 : 0;

    if (uIdx >= m_vServers.size()) {
        return nullptr;
    }

    return m_vServers[uIdx];
}

void CIRCNetwork::SetIRCServer(const CString& s) { m_sIRCServer = s; }

bool CIRCNetwork::SetNextServer(const CServer* pServer) {
    for (unsigned int a = 0; a < m_vServers.size(); a++) {
        if (m_vServers[a] == pServer) {
            m_uServerIdx = a;
            return true;
        }
    }

    return false;
}

bool CIRCNetwork::IsLastServer() const {
    return (m_uServerIdx >= m_vServers.size());
}

const CString& CIRCNetwork::GetIRCServer() const { return m_sIRCServer; }
const CNick& CIRCNetwork::GetIRCNick() const { return m_IRCNick; }

void CIRCNetwork::SetIRCNick(const CNick& n) {
    m_IRCNick = n;

    for (CClient* pClient : m_vClients) {
        pClient->SetNick(n.GetNick());
    }
}

CString CIRCNetwork::GetCurNick() const {
    const CIRCSock* pIRCSock = GetIRCSock();

    if (pIRCSock) {
        return pIRCSock->GetNick();
    }

    if (!m_vClients.empty()) {
        return m_vClients[0]->GetNick();
    }

    return "";
}

bool CIRCNetwork::Connect() {
    if (!GetIRCConnectEnabled() || m_pIRCSock || !HasServers()) return false;

    CServer* pServer = GetNextServer();
    if (!pServer) return false;

    if (CZNC::Get().GetServerThrottle(pServer->GetName())) {
        // Can't connect right now, schedule retry later
        CZNC::Get().AddNetworkToQueue(this);
        return false;
    }

    CZNC::Get().AddServerThrottle(pServer->GetName());

    bool bSSL = pServer->IsSSL();
#ifndef HAVE_LIBSSL
    if (bSSL) {
        PutStatus(
            t_f("Cannot connect to {1}, because ZNC is not compiled with SSL "
                "support.")(pServer->GetString(false)));
        CZNC::Get().AddNetworkToQueue(this);
        return false;
    }
#endif

    CIRCSock* pIRCSock = new CIRCSock(this);
    pIRCSock->SetPass(pServer->GetPass());
    pIRCSock->SetSSLTrustedPeerFingerprints(m_ssTrustedFingerprints);
    pIRCSock->SetTrustAllCerts(GetTrustAllCerts());
    pIRCSock->SetTrustPKI(GetTrustPKI());

    DEBUG("Connecting user/network [" << m_pUser->GetUsername() << "/"
                                      << m_sName << "]");

    bool bAbort = false;
    NETWORKMODULECALL(OnIRCConnecting(pIRCSock), m_pUser, this, nullptr,
                      &bAbort);
    if (bAbort) {
        DEBUG("Some module aborted the connection attempt");
        PutStatus(t_s("Some module aborted the connection attempt"));
        delete pIRCSock;
        CZNC::Get().AddNetworkToQueue(this);
        return false;
    }

    CString sSockName = "IRC::" + m_pUser->GetUsername() + "::" + m_sName;

    if (pServer->IsUnixSocket()) {
        pIRCSock->SetSSL(bSSL);
        CZNC::Get().GetManager().ConnectUnix(sSockName, pServer->GetName(),
                                             pIRCSock);
    } else {
        CZNC::Get().GetManager().Connect(pServer->GetName(), pServer->GetPort(),
                                         sSockName, 120, bSSL, GetBindHost(),
                                         pIRCSock);
    }

    return true;
}

bool CIRCNetwork::IsIRCConnected() const {
    const CIRCSock* pSock = GetIRCSock();
    return (pSock && pSock->IsAuthed());
}

void CIRCNetwork::SetIRCSocket(CIRCSock* pIRCSock) { m_pIRCSock = pIRCSock; }

void CIRCNetwork::IRCConnected() {
    if (m_uJoinDelay > 0) {
        m_pJoinTimer->Delay(m_uJoinDelay);
    } else {
        JoinChans();
    }
}

void CIRCNetwork::IRCDisconnected() {
    m_pIRCSock = nullptr;

    SetIRCServer("");
    m_bIRCAway = false;

    // Get the reconnect going
    CheckIRCConnect();
}

void CIRCNetwork::NotifyClientsAboutServerDependentCap(const CString& sCap, bool bValue) {
    CString sValue = GetIRCSock() ? GetIRCSock()->GetCapLsValue(sCap) : "";
    for (CClient* pClient : m_vClients) {
        pClient->NotifyServerDependentCap(sCap, bValue, sValue);
    }
}

bool CIRCNetwork::IsServerCapAccepted(const CString& sCap) const {
    return m_pIRCSock && m_pIRCSock->IsCapAccepted(sCap);
}

void CIRCNetwork::SetIRCConnectEnabled(bool b) {
    m_bIRCConnectEnabled = b;

    if (m_bIRCConnectEnabled) {
        CheckIRCConnect();
    } else if (GetIRCSock()) {
        if (GetIRCSock()->IsConnected()) {
            GetIRCSock()->Quit();
        } else {
            GetIRCSock()->Close();
        }
    }
}

void CIRCNetwork::CheckIRCConnect() {
    // Do we want to connect?
    if (GetIRCConnectEnabled() && GetIRCSock() == nullptr)
        CZNC::Get().AddNetworkToQueue(this);
}

bool CIRCNetwork::PutIRC(const CString& sLine) {
    CIRCSock* pIRCSock = GetIRCSock();

    if (!pIRCSock) {
        return false;
    }

    pIRCSock->PutIRC(sLine);
    return true;
}

bool CIRCNetwork::PutIRC(const CMessage& Message) {
    CIRCSock* pIRCSock = GetIRCSock();

    if (!pIRCSock) {
        return false;
    }

    pIRCSock->PutIRC(Message);
    return true;
}

void CIRCNetwork::ClearQueryBuffer() {
    std::for_each(m_vQueries.begin(), m_vQueries.end(),
                  std::default_delete<CQuery>());
    m_vQueries.clear();
}

const CString& CIRCNetwork::GetNick(const bool bAllowDefault) const {
    if (m_sNick.empty()) {
        return m_pUser->GetNick(bAllowDefault);
    }

    return m_sNick;
}

const CString& CIRCNetwork::GetAltNick(const bool bAllowDefault) const {
    if (m_sAltNick.empty()) {
        return m_pUser->GetAltNick(bAllowDefault);
    }

    return m_sAltNick;
}

const CString& CIRCNetwork::GetIdent(const bool bAllowDefault) const {
    if (m_sIdent.empty()) {
        return m_pUser->GetIdent(bAllowDefault);
    }

    return m_sIdent;
}

CString CIRCNetwork::GetRealName() const {
    if (m_sRealName.empty()) {
        return m_pUser->GetRealName();
    }

    return m_sRealName;
}

const CString& CIRCNetwork::GetBindHost() const {
    if (m_sBindHost.empty()) {
        return m_pUser->GetBindHost();
    }

    return m_sBindHost;
}

const CString& CIRCNetwork::GetEncoding() const { return m_sEncoding; }

CString CIRCNetwork::GetQuitMsg() const {
    if (m_sQuitMsg.empty()) {
        return m_pUser->GetQuitMsg();
    }

    return m_sQuitMsg;
}

void CIRCNetwork::SetNick(const CString& s) {
    if (m_pUser->GetNick().Equals(s)) {
        m_sNick = "";
    } else {
        m_sNick = s;
    }
}

void CIRCNetwork::SetAltNick(const CString& s) {
    if (m_pUser->GetAltNick().Equals(s)) {
        m_sAltNick = "";
    } else {
        m_sAltNick = s;
    }
}

void CIRCNetwork::SetIdent(const CString& s) {
    if (m_pUser->GetIdent().Equals(s)) {
        m_sIdent = "";
    } else {
        m_sIdent = s;
    }
}

void CIRCNetwork::SetRealName(const CString& s) {
    if (m_pUser->GetRealName().Equals(s)) {
        m_sRealName = "";
    } else {
        m_sRealName = s;
    }
}

void CIRCNetwork::SetBindHost(const CString& s) {
    if (m_pUser->GetBindHost().Equals(s)) {
        m_sBindHost = "";
    } else {
        m_sBindHost = s;
    }
}

void CIRCNetwork::SetEncoding(const CString& s) {
    m_sEncoding = CZNC::Get().FixupEncoding(s);
    if (GetIRCSock()) {
        GetIRCSock()->SetEncoding(m_sEncoding);
    }
}

void CIRCNetwork::SetQuitMsg(const CString& s) {
    if (m_pUser->GetQuitMsg().Equals(s)) {
        m_sQuitMsg = "";
    } else {
        m_sQuitMsg = s;
    }
}

CString CIRCNetwork::ExpandString(const CString& sStr) const {
    CString sRet;
    return ExpandString(sStr, sRet);
}

CString& CIRCNetwork::ExpandString(const CString& sStr, CString& sRet) const {
    sRet = sStr;

    sRet.Replace("%altnick%", GetAltNick());
    sRet.Replace("%bindhost%", GetBindHost());
    sRet.Replace("%defnick%", GetNick());
    sRet.Replace("%ident%", GetIdent());
    sRet.Replace("%network%", GetName());
    sRet.Replace("%nick%", GetCurNick());
    sRet.Replace("%realname%", GetRealName());

    return m_pUser->ExpandString(sRet, sRet);
}

bool CIRCNetwork::LoadModule(const CString& sModName, const CString& sArgs,
                             const CString& sNotice, CString& sError) {
    CUtils::PrintAction(sNotice);
    CString sModRet;

    bool bModRet = GetModules().LoadModule(
        sModName, sArgs, CModInfo::NetworkModule, GetUser(), this, sModRet);

    CUtils::PrintStatus(bModRet, sModRet);
    if (!bModRet) {
        sError = sModRet;
    }
    return bModRet;
}
