/*
 * InspIRCd -- Internet Relay Chat Daemon
 *
 *   Copyright (C) 2019 iwalkalone <iwalkalone69@gmail.com>
 *   Copyright (C) 2013-2016, 2018 Attila Molnar <attilamolnar@hush.com>
 *   Copyright (C) 2013, 2018-2023 Sadie Powell <sadie@witchery.services>
 *   Copyright (C) 2013, 2015 Adam <Adam@anope.org>
 *   Copyright (C) 2012 Robby <robby@chatbelgie.be>
 *   Copyright (C) 2009-2010 Daniel De Graaf <danieldg@inspircd.org>
 *   Copyright (C) 2008-2009 Craig Edwards <brain@inspircd.org>
 *   Copyright (C) 2008 Robin Burchell <robin+git@viroteck.net>
 *
 * This file is part of InspIRCd.  InspIRCd is free software: you can
 * redistribute it and/or modify it under the terms of the GNU General Public
 * License as published by the Free Software Foundation, version 2.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */


#include "inspircd.h"
#include "clientprotocolmsg.h"
#include "iohook.h"
#include "xline.h"

namespace
{
	class WriteCommonQuit final
		: public User::ForEachNeighborHandler
	{
		ClientProtocol::Messages::Quit quitmsg;
		ClientProtocol::Event quitevent;
		ClientProtocol::Messages::Quit operquitmsg;
		ClientProtocol::Event operquitevent;

		void Execute(LocalUser* user) override
		{
			user->Send(user->IsOper() ? operquitevent : quitevent);
		}

	public:
		WriteCommonQuit(User* user, const std::string& msg, const std::string& opermsg)
			: quitmsg(user, msg)
			, quitevent(ServerInstance->GetRFCEvents().quit, quitmsg)
			, operquitmsg(user, opermsg)
			, operquitevent(ServerInstance->GetRFCEvents().quit, operquitmsg)
		{
			user->ForEachNeighbor(*this, false);
		}
	};

	void CheckPingTimeout(LocalUser* user)
	{
		// Check if it is time to ping the user yet.
		if (ServerInstance->Time() < user->nextping)
			return;

		// This user didn't answer the last ping, remove them.
		if (!user->lastping)
		{
			time_t secs = ServerInstance->Time() - (user->nextping - user->GetClass()->pingtime);
			const std::string message = "Ping timeout: " + ConvToStr(secs) + (secs != 1 ? " seconds" : " second");
			ServerInstance->Users.QuitUser(user, message);
			return;
		}

		user->lastping = 0;
		user->nextping = ServerInstance->Time() + user->GetClass()->pingtime;

		// If the user has an I/O hook that can handle pinging use that instead.
		IOHook* hook = user->eh.GetIOHook();
		while (hook)
		{
			if (hook->Ping())
				return; // Client has been pinged.

			IOHookMiddle* middlehook = IOHookMiddle::ToMiddleHook(hook);
			hook = middlehook ? middlehook->GetNextHook() : nullptr;
		}


		// Send a ping to the client using an IRC message.
		ClientProtocol::Messages::Ping ping;
		user->Send(ServerInstance->GetRFCEvents().ping, ping);
	}

	void CheckConnectionTimeout(LocalUser* user)
	{
		if (user->GetClass() && (ServerInstance->Time() > static_cast<time_t>(user->signon + user->GetClass()->connection_timeout)))
		{
			// Either the user did not send NICK/USER or a module blocked connection in
			// OnCheckReady until the client timed out.
			ServerInstance->Users.QuitUser(user, "Connection timeout");
		}
	}

	void CheckModulesReady(LocalUser* user)
	{
		ModResult res;
		FIRST_MOD_RESULT(OnCheckReady, res, (user));
		if (res == MOD_RES_PASSTHRU)
		{
			// User has sent NICK/USER and modules are ready.
			user->FullConnect();
			return;
		}

		// If the user has been quit in OnCheckReady then we shouldn't quit
		// them again for having a registration timeout.
		if (!user->quitting)
			CheckConnectionTimeout(user);
	}
}

UserManager::UserManager()
{
	// We need to define a constructor here to work around a Clang bug.
}

UserManager::~UserManager()
{
	for (const auto& [_, client] : clientlist)
		delete client;
}

void UserManager::AddUser(int socket, ListenSocket* via, const irc::sockets::sockaddrs& client, const irc::sockets::sockaddrs& server)
{
	// User constructor allocates a new UUID for the user and inserts it into the uuidlist
	LocalUser* const New = new LocalUser(socket, client, server);
	UserIOHandler* eh = &New->eh;

	ServerInstance->Logs.Debug("USERS", "New user fd: {}", socket);

	this->unknown_count++;
	this->clientlist[New->nick] = New;
	this->AddClone(New);
	this->local_users.push_front(New);
	FOREACH_MOD(OnUserInit, (New));

	if (!SocketEngine::AddFd(eh, FD_WANT_FAST_READ | FD_WANT_EDGE_WRITE))
	{
		ServerInstance->Logs.Debug("USERS", "Internal error on new connection");
		this->QuitUser(New, "Internal error handling connection");
		return;
	}

	// If this listener has an IO hook provider set then tell it about the connection
	for (ListenSocket::IOHookProvList::iterator i = via->iohookprovs.begin(); i != via->iohookprovs.end(); ++i)
	{
		ListenSocket::IOHookProvRef& iohookprovref = *i;
		if (!iohookprovref)
		{
			if (iohookprovref.GetProvider().empty())
				continue;

			const char* hooktype = i == via->iohookprovs.begin() ? "hook" : "sslprofile";
			ServerInstance->Logs.Warning("USERS", "Non-existent I/O hook '{}' in <bind:{}> tag at {}",
				iohookprovref.GetProvider(), hooktype, via->bind_tag->source.str());
			this->QuitUser(New, INSP_FORMAT("Internal error handling connection (misconfigured {})", hooktype));
			return;
		}

		iohookprovref->OnAccept(eh, client, server);

		// IOHook could have encountered a fatal error, e.g. if the TLS ClientHello
		// was already in the queue and there was no common TLS version.
		if (!eh->GetError().empty())
		{
			QuitUser(New, eh->GetError());
			return;
		}
	}

	if (this->local_users.size() > ServerInstance->Config->SoftLimit)
	{
		ServerInstance->SNO.WriteToSnoMask('a', "Warning: softlimit value has been reached: {} clients", ServerInstance->Config->SoftLimit);
		this->QuitUser(New, "No more connections allowed");
		return;
	}

	if (!New->FindConnectClass())
		return; // User does not match any connect classes.

	/*
	 * even with bancache, we still have to keep User::exempt current.
	 * besides that, if we get a positive bancache hit, we still won't fuck
	 * them over if they are exempt. -- w00t
	 */
	New->exempt = (ServerInstance->XLines->MatchesLine("E", New) != nullptr);

	BanCacheHit* const b = ServerInstance->BanCache.GetHit(New->GetAddress());
	if (b)
	{
		if (!b->Type.empty() && !New->exempt)
		{
			/* user banned */
			ServerInstance->Logs.Debug("BANCACHE", "Positive hit for " + New->GetAddress());

			if (!ServerInstance->Config->XLineMessage.empty())
				New->WriteNumeric(ERR_YOUREBANNEDCREEP, ServerInstance->Config->XLineMessage);

			// IMPORTANT: we don't check XLineQuitPublic here because the only
			// person who might see the ban at this point is the affected user.
			this->QuitUser(New, b->Reason);
			return;
		}
		else
		{
			ServerInstance->Logs.Debug("BANCACHE", "Negative hit for " + New->GetAddress());
		}
	}
	else
	{
		if (!New->exempt)
		{
			XLine* r = ServerInstance->XLines->MatchesLine("Z", New);

			if (r)
			{
				r->Apply(New);
				return;
			}
		}
	}

	if (ServerInstance->Config->RawLog)
		Log::NotifyRawIO(New, MessageType::NOTICE);

	FOREACH_MOD(OnChangeRemoteAddress, (New));
	if (!New->quitting)
		FOREACH_MOD(OnUserPostInit, (New));
}

void UserManager::QuitUser(User* user, const std::string& quitmessage, const std::string* operquitmessage)
{
	if (user->quitting)
	{
		ServerInstance->Logs.Debug("USERS", "BUG: Tried to quit quitting user: " + user->nick);
		return;
	}

	if (IS_SERVER(user))
	{
		ServerInstance->Logs.Debug("USERS", "BUG: Tried to quit server user: " + user->nick);
		return;
	}

	std::string quitmsg(quitmessage);
	std::string operquitmsg;
	if (operquitmessage)
		operquitmsg.assign(*operquitmessage);

	LocalUser* const localuser = IS_LOCAL(user);
	if (localuser)
	{
		ModResult MOD_RESULT;
		FIRST_MOD_RESULT(OnUserPreQuit, MOD_RESULT, (localuser, quitmsg, operquitmsg));
		if (MOD_RESULT == MOD_RES_DENY)
			return;
	}

	if (quitmsg.length() > ServerInstance->Config->Limits.MaxQuit)
		quitmsg.erase(ServerInstance->Config->Limits.MaxQuit + 1);

	if (operquitmsg.empty())
		operquitmsg.assign(quitmsg);
	else if (operquitmsg.length() > ServerInstance->Config->Limits.MaxQuit)
		operquitmsg.erase(ServerInstance->Config->Limits.MaxQuit + 1);

	user->quitting = true;
	ServerInstance->Logs.Debug("USERS", "QuitUser: {}={} '{}'", user->uuid, user->nick, quitmessage);
	if (localuser)
	{
		ClientProtocol::Messages::Error errormsg(INSP_FORMAT("Closing link: ({}) [{}]", user->GetRealUserHost(), operquitmsg));
		localuser->Send(ServerInstance->GetRFCEvents().error, errormsg);
	}

	ServerInstance->GlobalCulls.AddItem(user);

	if (user->IsFullyConnected())
	{
		FOREACH_MOD(OnUserQuit, (user, quitmsg, operquitmsg));
		WriteCommonQuit(user, quitmsg, operquitmsg);
	}
	else
		unknown_count--;

	if (IS_LOCAL(user))
	{
		LocalUser* lu = IS_LOCAL(user);
		FOREACH_MOD(OnUserDisconnect, (lu));
		lu->eh.Close();

		if (lu->IsFullyConnected())
		{
			ServerInstance->SNO.WriteToSnoMask('q', "Client exiting: {} ({}) [{}]", user->GetRealMask(),
				user->GetAddress(), operquitmsg);
		}
		local_users.erase(lu);
		if (lu->GetClass())
			lu->GetClass()->use_count--;
	}

	if (!clientlist.erase(user->nick))
		ServerInstance->Logs.Debug("USERS", "BUG: Nick not found in clientlist, cannot remove: " + user->nick);

	uuidlist.erase(user->uuid);
	user->PurgeEmptyChannels();
	user->OperLogout();
}

void UserManager::AddClone(User* user)
{
	CloneCounts& counts = clonemap[user->GetCIDRMask()];
	counts.global++;
	if (IS_LOCAL(user))
		counts.local++;
}

void UserManager::RemoveCloneCounts(User* user)
{
	CloneMap::iterator it = clonemap.find(user->GetCIDRMask());
	if (it != clonemap.end())
	{
		CloneCounts& counts = it->second;
		counts.global--;
		if (counts.global == 0)
		{
			// No more users from this IP, remove entry from the map
			clonemap.erase(it);
			return;
		}

		if (IS_LOCAL(user))
			counts.local--;
	}
}

void UserManager::RehashCloneCounts()
{
	clonemap.clear();

	for (const auto& [_, u] : ServerInstance->Users.GetUsers())
		AddClone(u);
}

const UserManager::CloneCounts& UserManager::GetCloneCounts(User* user) const
{
	CloneMap::const_iterator it = clonemap.find(user->GetCIDRMask());
	if (it != clonemap.end())
		return it->second;
	else
		return zeroclonecounts;
}

/**
 * This function is called once a second from the mainloop.
 * It is intended to do background checking on all the users, e.g. do
 * ping checks, connection timeouts, etc.
 */
void UserManager::DoBackgroundUserStuff()
{
	for (LocalList::iterator i = local_users.begin(); i != local_users.end(); )
	{
		// It's possible that we quit the user below due to ping timeout etc. and QuitUser() removes it from the list
		LocalUser* curr = *i;
		++i;

		if (curr->CommandFloodPenalty || curr->eh.GetSendQSize())
		{
			unsigned long rate = curr->GetClass()->commandrate;
			if (curr->CommandFloodPenalty > rate)
				curr->CommandFloodPenalty -= rate;
			else
				curr->CommandFloodPenalty = 0;
			curr->eh.OnDataReady();
		}

		switch (curr->connected)
		{
			case User::CONN_FULL:
				CheckPingTimeout(curr);
				break;

			case User::CONN_NICKUSER:
				CheckModulesReady(curr);
				break;

			default:
				CheckConnectionTimeout(curr);
				break;
		}
	}
}

uint64_t UserManager::NextAlreadySentId()
{
	if (++already_sent_id == 0)
	{
		// Wrapped around, reset the already_sent ids of all users
		already_sent_id = 1;
		for (auto* user : GetLocalUsers())
			user->already_sent = 0;
	}
	return already_sent_id;
}

User* UserManager::Find(const std::string& nickuuid, bool fullyconnected)
{
	if (nickuuid.empty())
		return nullptr;

	if (isdigit(nickuuid[0]))
		return FindUUID(nickuuid, fullyconnected);

	return FindNick(nickuuid, fullyconnected);
}

User* UserManager::FindNick(const std::string& nick, bool fullyconnected)
{
	if (nick.empty())
		return nullptr;

	UserMap::iterator uiter = this->clientlist.find(nick);
	if (uiter == this->clientlist.end())
		return nullptr;

	User* user = uiter->second;
	if (fullyconnected && !user->IsFullyConnected())
		return nullptr;

	return user;
}

User* UserManager::FindUUID(const std::string& uuid, bool fullyconnected)
{
	if (uuid.empty())
		return nullptr;

	UserMap::iterator uiter = this->uuidlist.find(uuid);
	if (uiter == this->uuidlist.end())
		return nullptr;

	User* user = uiter->second;
	if (fullyconnected && !user->IsFullyConnected())
		return nullptr;

	return user;
}
