// 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/info.h"

struct OperInfoImpl final
	: OperInfo
	, Serializable
{
	OperInfoImpl()
		: Serializable("OperInfo")
	{
	}

	OperInfoImpl(const Anope::string &t, const Anope::string &i, const Anope::string &a, time_t c)
		: OperInfo(t, i, a, c)
		, Serializable("OperInfo")
	{
	}

	~OperInfoImpl() override;
};

struct OperInfoTypeImpl
	: Serialize::Type
{
	OperInfoTypeImpl()
		: Serialize::Type("OperInfo")
	{
	}

	void Serialize(Serializable *obj, Serialize::Data &data) const override
	{
		const auto *oi = static_cast<const OperInfoImpl *>(obj);
		data.Store("target", oi->target);
		data.Store("info", oi->info);
		data.Store("adder", oi->adder);
		data.Store("created", oi->created);
	}

	Serializable *Unserialize(Serializable *obj, Serialize::Data &data) const override;
};

struct OperInfos final
	: OperInfoList
{
	OperInfos(Extensible *) { }

	OperInfo *Create() override
	{
		return new OperInfoImpl();
	}

	static Extensible *Find(const Anope::string &target)
	{
		NickAlias *na = NickAlias::Find(target);
		if (na)
			return na->nc;
		return ChannelInfo::Find(target);
	}
};

OperInfoImpl::~OperInfoImpl()
{
	Extensible *e = OperInfos::Find(target);
	if (e)
	{
		auto *op  = e->GetExt<OperInfos>("operinfo");
		if (op)
		{
			auto it = std::find((*op)->begin(), (*op)->end(), this);
			if (it != (*op)->end())
				(*op)->erase(it);
		}
	}
}

Serializable *OperInfoTypeImpl::Unserialize(Serializable *obj, Serialize::Data &data) const
{
	const auto starget = data.Load("target");

	Extensible *e = OperInfos::Find(starget);
	if (!e)
		return NULL;

	auto *oi = e->Require<OperInfos>("operinfo");
	OperInfoImpl *o;
	if (obj)
		o = anope_dynamic_static_cast<OperInfoImpl *>(obj);
	else
	{
		o = new OperInfoImpl();
		o->target = starget;
	}
	o->info = data.Load("info");
	o->adder = data.Load("adder");
	o->created = data.Load<time_t>("created");

	if (!obj)
		(*oi)->push_back(o);
	return o;
}

class CommandOSInfo final
	: public Command
{
public:
	CommandOSInfo(Module *creator) : Command(creator, "operserv/info", 2, 3)
	{
		this->SetDesc(_("Associate oper info with a nick or channel"));
		this->SetSyntax(_("ADD \037target\037 \037info\037"));
		this->SetSyntax(_("DEL \037target\037 \037info\037"));
		this->SetSyntax(_("CLEAR \037target\037"));
	}

	void Execute(CommandSource &source, const std::vector<Anope::string> &params) override
	{
		const Anope::string &cmd = params[0], target = params[1], info = params.size() > 2 ? params[2] : "";

		Extensible *e;
		if (IRCD->IsChannelValid(target))
		{
			ChannelInfo *ci = ChannelInfo::Find(target);
			if (!ci)
			{
				source.Reply(CHAN_X_NOT_REGISTERED, target.c_str());
				return;
			}

			e = ci;
		}
		else
		{
			NickAlias *na = NickAlias::Find(target);
			if (!na)
			{
				source.Reply(NICK_X_NOT_REGISTERED, target.c_str());
				return;
			}

			e = na->nc;
		}

		if (cmd.equals_ci("ADD"))
		{
			if (info.empty())
			{
				this->OnSyntaxError(source, cmd);
				return;
			}

			auto *oi = e->Require<OperInfos>("operinfo");

			if ((*oi)->size() >= Config->GetModule(this->module).Get<unsigned>("max", "10"))
			{
				source.Reply(_("The oper info list for \002%s\002 is full."), target.c_str());
				return;
			}

			for (auto *o : *(*oi))
			{
				if (o->info.equals_ci(info))
				{
					source.Reply(_("The oper info already exists on \002%s\002."), target.c_str());
					return;
				}
			}

			(*oi)->push_back(new OperInfoImpl(target, info, source.GetNick(), Anope::CurTime));

			source.Reply(_("Added info to \002%s\002."), target.c_str());
			Log(LOG_ADMIN, source, this) << "to add information to " << target;

			if (Anope::ReadOnly)
				source.Reply(READ_ONLY_MODE);
		}
		else if (cmd.equals_ci("DEL"))
		{
			if (info.empty())
			{
				this->OnSyntaxError(source, cmd);
				return;
			}

			auto *oi = e->GetExt<OperInfos>("operinfo");

			if (!oi)
			{
				source.Reply(_("Oper info list for \002%s\002 is empty."), target.c_str());
				return;
			}

			bool found = false;
			for (unsigned i = (*oi)->size(); i > 0; --i)
			{
				OperInfo *o = (*oi)->at(i - 1);

				if (o->info.equals_ci(info))
				{
					delete o;
					found = true;
					break;
				}
			}

			if (!found)
			{
				source.Reply(_("No such info \"%s\" on \002%s\002."), info.c_str(), target.c_str());
			}
			else
			{
				if ((*oi)->empty())
					e->Shrink<OperInfos>("operinfo");

				source.Reply(_("Deleted info from \002%s\002."), target.c_str());
				Log(LOG_ADMIN, source, this) << "to remove information from " << target;

				if (Anope::ReadOnly)
					source.Reply(READ_ONLY_MODE);
			}
		}
		else if (cmd.equals_ci("CLEAR"))
		{
			auto *oi = e->GetExt<OperInfos>("operinfo");

			if (!oi)
			{
				source.Reply(_("Oper info list for \002%s\002 is empty."), target.c_str());
				return;
			}

			e->Shrink<OperInfos>("operinfo");

			source.Reply(_("Cleared info from \002%s\002."), target.c_str());
			Log(LOG_ADMIN, source, this) << "to clear information for " << target;

			if (Anope::ReadOnly)
				source.Reply(READ_ONLY_MODE);
		}
		else
		{
			this->OnSyntaxError(source, cmd);
		}
	}

	bool OnHelp(CommandSource &source, const Anope::string &subcommand) override
	{
		this->SendSyntax(source);
		source.Reply(" ");
		source.Reply(_(
			"Add or delete oper information for a given nick or channel. "
			"This will show to opers in the respective info command for "
			"the nick or channel."
		));
		return true;
	}
};

class OSInfo final
	: public Module
{
	CommandOSInfo commandosinfo;
	ExtensibleItem<OperInfos> oinfo;
	OperInfoTypeImpl oinfo_type;

	void OnInfo(CommandSource &source, Extensible *e, InfoFormatter &info)
	{
		if (!source.IsOper())
			return;

		OperInfos *oi = oinfo.Get(e);
		if (!oi)
			return;

		for (auto *o : *(*oi))
		{
			info[_("Oper Info")] = Anope::Format(_("(by %s on %s) %s"), o->adder.c_str(), Anope::strftime(o->created, source.GetAccount(), true).c_str(), o->info.c_str());
		}
	}

public:
	OSInfo(const Anope::string &modname, const Anope::string &creator)
		: Module(modname, creator, VENDOR)
		, commandosinfo(this)
		, oinfo(this, "operinfo")
	{
	}

	void OnNickInfo(CommandSource &source, NickAlias *na, InfoFormatter &info, bool show_hidden) override
	{
		OnInfo(source, na->nc, info);
	}

	void OnChanInfo(CommandSource &source, ChannelInfo *ci, InfoFormatter &info, bool show_hidden) override
	{
		OnInfo(source, ci, info);
	}
};

MODULE_INIT(OSInfo)
