// Anope IRC Services <https://www.anope.org/>
//
// Copyright (C) 2003-2026 Anope Contributors
//
// Anope is free software. You can use, modify, and/or distribute it under the
// terms of version 2 of the GNU General Public License. See docs/LICENSE.txt
// for the complete terms of this license and docs/AUTHORS.txt for a list of
// contributors.
//
// Based on the original code of Epona by Lara
// Based on the original code of Services by Andy Church
//
// SPDX-License-Identifier: GPL-2.0-only

#include "module.h"
#include "modules/operserv/session.h"

struct Stats final
	: Serializable
{
	static Stats *me;

	Stats() : Serializable("Stats")
	{
		if (!me)
			me = this;
	}
};

struct StatsType final
	: Serialize::Type
{
	StatsType()
		: Serialize::Type("Stats")
	{
	}

	void Serialize(Serializable *obj, Serialize::Data &data) const override
	{
		data.Store("maxusercnt", MaxUserCount);
		data.Store("maxusertime", MaxUserTime);
	}

	Serializable *Unserialize(Serializable *obj, Serialize::Data &data) const override
	{
		MaxUserCount = data.Load<size_t>("maxusercnt");
		MaxUserTime = data.Load<time_t>("maxusertime");
		return Stats::me;
	}
};

Stats *Stats::me = nullptr;

class CommandOSStats final
	: public Command
{
private:
	ServiceReference<XLineManager> akills, snlines, sqlines;

	static void ReportXLineStats(CommandSource &source, XLineManager *xlm, const char *type, const char *config)
	{
		source.Reply(_("Current number of %ss: \002%zu\002"), type, xlm->GetCount());

		const auto timeout = Config->GetModule("operserv").Get<time_t>(config, "30d");
		if (timeout)
			source.Reply(_("Default %s expiry time: \002%s\002"), type, Anope::Duration(timeout, source.nc, true).c_str());
		else
			source.Reply(_("Default %s expiry time: \002No expiration\002"), type);
	}
private:
	void DoStatsAkill(CommandSource &source)
	{
		if (akills)
			ReportXLineStats(source, *akills, "AKILL", "autokillexpiry");

		if (snlines)
			ReportXLineStats(source, *snlines, "SNLINE", "snlineexpiry");

		if (sqlines)
			ReportXLineStats(source, *sqlines, "SQLINE", "sqlineexpiry");
	}

	static void DoStatsReset(CommandSource &source)
	{
		MaxUserCount = UserListByNick.size();
		MaxUserTime = Anope::CurTime;
		Stats::me->QueueUpdate();
		source.Reply(_("Statistics reset."));
	}

	static void DoStatsUptime(CommandSource &source)
	{
		time_t uptime = Anope::CurTime - Anope::StartTime;
		source.Reply(_("Current users: \002%zu\002 (\002%zu\002 ops)"), UserListByNick.size(), OperCount);
		source.Reply(_("Maximum users: \002%zu\002 (%s)"), MaxUserCount, Anope::strftime(MaxUserTime, source.GetAccount()).c_str());
		source.Reply(_("Services up %s."), Anope::Duration(uptime, source.GetAccount()).c_str());
	}

	static void DoStatsUplink(CommandSource &source)
	{
		Anope::string buf;
		for (const auto &capab : Servers::Capab)
			buf += " " + capab;
		if (!buf.empty())
			buf.erase(buf.begin());

		source.Reply(_("Uplink server: %s"), Me->GetLinks().front()->GetName().c_str());
		source.Reply(_("Uplink capab: %s"), buf.c_str());
		source.Reply(_("Servers found: %zu"), Servers::ByName.size() - 1);
	}

	template<typename T> void GetHashStats(const T &map, size_t &entries, size_t &buckets, size_t &max_chain)
	{
		entries = map.size(), buckets = map.bucket_count(), max_chain = 0;
		for (size_t i = 0; i < buckets; ++i)
			if (map.bucket_size(i) > max_chain)
				max_chain = map.bucket_size(i);
	}

	void DoStatsHash(CommandSource &source)
	{
		size_t entries, buckets, max_chain;

		GetHashStats(UserListByNick, entries, buckets, max_chain);
		source.Reply(_("Users (nick): %lu entries, %lu buckets, longest chain is %zu"), entries, buckets, max_chain);

		if (!UserListByUID.empty())
		{
			GetHashStats(UserListByUID, entries, buckets, max_chain);
			source.Reply(_("Users (uid): %lu entries, %lu buckets, longest chain is %zu"), entries, buckets, max_chain);
		}

		GetHashStats(ChannelList, entries, buckets, max_chain);
		source.Reply(_("Channels: %zu entries, %zu buckets, longest chain is %zu"), entries, buckets, max_chain);

		GetHashStats(*RegisteredChannelList, entries, buckets, max_chain);
		source.Reply(_("Registered channels: %zu entries, %zu buckets, longest chain is %zu"), entries, buckets, max_chain);

		GetHashStats(*NickAliasList, entries, buckets, max_chain);
		source.Reply(_("Registered nicknames: %zu entries, %zu buckets, longest chain is %zu"), entries, buckets, max_chain);

		GetHashStats(*NickCoreList, entries, buckets, max_chain);
		source.Reply(_("Registered accounts: %zu entries, %zu buckets, longest chain is %zu"), entries, buckets, max_chain);

		if (OperServ::session_service)
		{
			GetHashStats(OperServ::session_service->GetSessions(), entries, buckets, max_chain);
			source.Reply(_("Sessions: %zu entries, %zu buckets, longest chain is %zu"), entries, buckets, max_chain);
		}
	}

	void DoStatsPassword(CommandSource &source)
	{
		Anope::map<size_t> counts;
		size_t missing = 0;
		size_t unknown = 0;
		for (const auto &[_, nc] : *NickCoreList)
		{
			if (nc->pass.empty())
			{
				missing++;
				continue;
			}

			auto sep = nc->pass.find(':');
			if (sep == Anope::string::npos)
			{
				unknown++;
				continue;
			}

			counts[nc->pass.substr(0, sep)]++;
		}

		for (const auto &[algo, count] : counts)
			source.Reply(_("Passwords encrypted with %s: %zu"), algo.c_str(), count);
		if (missing)
			source.Reply(_("Missing passwords: %zu"), missing);
		if (unknown)
			source.Reply(_("Unknown passwords: %zu"), unknown);
	}

public:
	CommandOSStats(Module *creator) : Command(creator, "operserv/stats", 0, 1),
		akills("XLineManager", "xlinemanager/sgline"), snlines("XLineManager", "xlinemanager/snline"), sqlines("XLineManager", "xlinemanager/sqline")
	{
		this->SetDesc(_("Show status of services and network"));
		this->SetSyntax("[AKILL | HASH | PASSWORD | UPLINK | UPTIME | ALL | RESET]");
	}

	void Execute(CommandSource &source, const std::vector<Anope::string> &params) override
	{
		Anope::string extra = !params.empty() ? params[0] : "";

		Log(LOG_ADMIN, source, this) << extra;

		if (extra.equals_ci("RESET"))
			return this->DoStatsReset(source);

		bool handled = false;
		if (extra.equals_ci("ALL") || extra.equals_ci("AKILL"))
		{
			this->DoStatsAkill(source);
			handled = true;
		}

		if (extra.equals_ci("ALL") || extra.equals_ci("HASH"))
		{
			this->DoStatsHash(source);
			handled = true;
		}

		if (extra.equals_ci("ALL") || extra.equals_ci("PASSWORD"))
		{
			this->DoStatsPassword(source);
			handled = true;
		}

		if (extra.equals_ci("ALL") || extra.equals_ci("UPLINK"))
		{
			this->DoStatsUplink(source);
			handled = true;
		}

		if (extra.empty() || extra.equals_ci("ALL") || extra.equals_ci("UPTIME"))
		{
			this->DoStatsUptime(source);
			handled = true;
		}

		if (!handled)
			source.Reply(_("Unknown STATS option: \002%s\002"), extra.c_str());
	}

	bool OnHelp(CommandSource &source, const Anope::string &subcommand) override
	{
		this->SendSyntax(source);
		source.Reply(" ");
		source.Reply(_(
			"Without any option, shows the current number of users online, "
			"and the highest number of users online since services was "
			"started, and the length of time services has been running."
			"\n\n"
			"With the \002AKILL\002 option, displays the current size of the "
			"AKILL list and the current default expiry time."
			"\n\n"
			"The \002RESET\002 option currently resets the maximum user count "
			"to the number of users currently present on the network."
			"\n\n"
			"The \002PASSWORD\002 option displays the encryption algorithms used "
			"for user passwords. "
			"\n\n"
			"The \002UPLINK\002 option displays information about the current "
			"server Anope uses as an uplink to the network."
			"\n\n"
			"The \002HASH\002 option displays information about the hash maps."
			"\n\n"
			"The \002ALL\002 option displays all of the above statistics."
		));
		return true;
	}
};

class OSStats final
	: public Module
{
	CommandOSStats commandosstats;
	StatsType stats_type;
	Stats stats_saver;

public:
	OSStats(const Anope::string &modname, const Anope::string &creator)
		: Module(modname, creator, VENDOR)
		, commandosstats(this)
	{
	}

	void OnUserConnect(User *u, bool &exempt) override
	{
		if (UserListByNick.size() == MaxUserCount && Anope::CurTime == MaxUserTime)
			Stats::me->QueueUpdate();
	}
};

MODULE_INIT(OSStats)
