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

enum
{
	// Used by anope.checkCredentials, anope.identify, and anope.command.
	ERR_INVALID_ACCOUNT = RPC::ERR_CUSTOM_START,

	// Used by anope.checkCredentials
	ERR_INVALID_PASSWORD = RPC::ERR_CUSTOM_START + 1,
	ERR_ACCOUNT_SUSPENDED = RPC::ERR_CUSTOM_START + 2,

	// Used by anope.identify
	ERR_INVALID_USER = RPC::ERR_CUSTOM_START + 1,

	// Used by anope.listCommands, and anope.command
	ERR_INVALID_SERVICE = RPC::ERR_CUSTOM_START + 1,

	// Used by anope.command
	ERR_INVALID_COMMAND = RPC::ERR_CUSTOM_START + 2,
};


class AnopeCheckCredentialsRPCEvent final
	: public RPC::Event
{
private:
	class RPCIdentifyRequest final
		: public IdentifyRequest
	{
	private:
		RPC::Request request;
		Reference<HTTP::Client> client;
		Reference<RPC::ServiceInterface> rpcinterface;

	public:
		RPCIdentifyRequest(Module *m, RPC::Request &r, HTTP::Client *c, RPC::ServiceInterface *i, const Anope::string &a, const Anope::string &p)
			: IdentifyRequest(m, a, p, c->GetIP())
			, request(r)
			, client(c)
			, rpcinterface(i)
		{
		}

		void OnSuccess(NickAlias *na) override
		{
			if (!rpcinterface || !client)
				return;

			NickCore *nc = na->nc;
			if (nc->HasExt("NS_SUSPENDED"))
			{
				request.Error(ERR_ACCOUNT_SUSPENDED, "Account suspended");
				rpcinterface->Reply(request);
				client->SendReply(&request.reply);
				return;
			}

			auto &root = request.Root();
			root.Reply("account",nc->display)
				.Reply("confirmed", !nc->HasExt("UNCONFIRMED"))
				.Reply("uniqueid", nc->GetId());

			rpcinterface->Reply(request);
			client->SendReply(&request.reply);
		}

		void OnFail() override
		{
			if (!rpcinterface || !client)
				return;

			if (NickAlias::Find(GetAccount()))
				request.Error(ERR_INVALID_PASSWORD, "Invalid password");
			else
				request.Error(ERR_INVALID_ACCOUNT, "Invalid account");

			rpcinterface->Reply(request);
			client->SendReply(&request.reply);
		}
	};

public:
	AnopeCheckCredentialsRPCEvent(Module *o)
		: RPC::Event(o, "anope.checkCredentials", 2)
	{
	}

	bool Run(RPC::ServiceInterface *iface, HTTP::Client *client, RPC::Request &request) override
	{
		const auto &username = request.data[0];
		const auto &password = request.data[1];
		if (username.empty() || password.empty())
		{
			request.Error(RPC::ERR_INVALID_PARAMS, "Not enough parameters");
			return true;
		}

		auto *req = new RPCIdentifyRequest(this->owner, request, client, iface, username, password);
		FOREACH_MOD(OnCheckAuthentication, (nullptr, req));
		req->Dispatch();
		return false;
	}
};

class AnopeIdentifyRPCEvent final
	: public RPC::Event
{
public:
	AnopeIdentifyRPCEvent(Module *o)
		: RPC::Event(o, "anope.identify", 2)
	{
	}

	bool Run(RPC::ServiceInterface *iface, HTTP::Client *client, RPC::Request &request) override
	{
		auto *na = request.data[0].is_pos_number_only()
				? NickAlias::FindId(Anope::Convert(request.data[0], 0))
				: NickAlias::Find(request.data[0]);
		if (!na)
		{
			request.Error(ERR_INVALID_ACCOUNT, "No such account");
			return true;
		}

		auto *u = User::Find(request.data[1]);
		if (!u)
		{
			request.Error(ERR_INVALID_USER, "No such user");
			return true;
		}

		u->Identify(na);
		return true;
	}
};

class AnopeListCommandsRPCEvent final
	: public RPC::Event
{
public:
	AnopeListCommandsRPCEvent(Module *o)
		: RPC::Event(o, "anope.listCommands")
	{
	}

	bool Run(RPC::ServiceInterface *iface, HTTP::Client *client, RPC::Request &request) override
	{
		std::vector<BotInfo *> bots;
		if (request.data.empty())
		{
			for (const auto &[_, bi] : *BotListByNick)
				bots.push_back(bi);
		}
		else
		{
			for (const auto &bot : request.data)
			{
				auto *bi = BotInfo::Find(bot);
				if (!bi)
				{
					request.Error(ERR_INVALID_SERVICE, "No such service");
					return true;
				}
				bots.push_back(bi);
			}
		}

		auto &root = request.Root();
		for (const auto *bi : bots)
		{
			if (bi->commands.empty())
				continue;

			auto &commands = root.ReplyMap(bi->nick);
			for (const auto &[command, info] : bi->commands)
			{
				ServiceReference<Command> cmdref("Command", info.name);
				if (!cmdref)
					continue;

				auto &cmdinfo = commands.ReplyMap(command);
				cmdinfo.Reply("hidden", info.hide)
					.Reply("minparams", cmdref->min_params)
					.Reply("requiresaccount", !cmdref->AllowUnregistered())
					.Reply("requiresuser", cmdref->RequireUser());

				if (info.group.empty())
					cmdinfo.Reply("group", nullptr);
				else
					cmdinfo.Reply("group", info.group);

				if (cmdref->max_params)
					cmdinfo.Reply("maxparams", cmdref->max_params);
				else
					cmdinfo.Reply("maxparams", nullptr);

				if (info.permission.empty())
					cmdinfo.Reply("permission", nullptr);
				else
					cmdinfo.Reply("permission", info.permission);
			}
		}

		return true;
	}
};

class AnopeCommandRPCEvent final
	: public RPC::Event
{
private:
	class RPCCommandReply final
		: public CommandReply
	{
	private:
		RPC::Array &root;

	public:
		RPCCommandReply(RPC::Array &r)
			: root(r)
		{
		}

		void SendMessage(BotInfo *source, const Anope::string &msg) override
		{
			root.Reply(Anope::RemoveFormatting(msg.replace_all_cs("\x1A", "\x20")));
		};
	};

public:
	static bool pretenduser;

	AnopeCommandRPCEvent(Module *o)
		: RPC::Event(o, "anope.command", 3)
	{
	}

	bool Run(RPC::ServiceInterface *iface, HTTP::Client *client, RPC::Request &request) override
	{
		NickAlias *na = nullptr;
		if (!request.data[0].empty())
		{
			na = request.data[0].is_pos_number_only()
				? NickAlias::FindId(Anope::Convert(request.data[0], 0))
				: NickAlias::Find(request.data[0]);
			if (!na)
			{
				request.Error(ERR_INVALID_ACCOUNT, "No such account");
				return true;
			}
		}

		auto *bi = BotInfo::Find(request.data[1], true);
		if (!bi)
		{
			request.Error(ERR_INVALID_SERVICE, "No such service");
			return true;
		}

		Anope::string command;
		for (size_t i = 2; i < request.data.size(); ++i)
		{
			if (!command.empty())
				command.push_back(' ');
			command.append(request.data[i]);
		}

		User *u = nullptr;
		if (pretenduser && na && !na->nc->users.empty())
		{
			// Try and find the nick user first.
			for (auto *user : na->nc->users)
			{
				if (user->nick.equals_ci(na->nick))
				{
					u = user;
					break;
				}
			}

			// No nick user, fallback to the first.
			if (!u)
				u = na->nc->users.front();
		}

		RPCCommandReply reply(request.Root<RPC::Array>());
		CommandSource source(na ? na->nick : "RPC", u, na ? *na->nc : nullptr, &reply, bi, request.id);

		if (!Command::Run(source, command))
			request.Error(ERR_INVALID_COMMAND, "No such command");

		return true;
	}
};

bool AnopeCommandRPCEvent::pretenduser = false;

class ModuleRPCAccount final
	: public Module
{
private:
	AnopeCheckCredentialsRPCEvent anopecheckcredentialsrpcevent;
	AnopeIdentifyRPCEvent anopeidentifyrpcevent;
	AnopeListCommandsRPCEvent anopelistcommandsrpcevent;
	AnopeCommandRPCEvent anopecommandrpcevent;

public:
	ModuleRPCAccount(const Anope::string &modname, const Anope::string &creator)
		: Module(modname, creator, EXTRA | VENDOR)
		, anopecheckcredentialsrpcevent(this)
		, anopeidentifyrpcevent(this)
		, anopelistcommandsrpcevent(this)
		, anopecommandrpcevent(this)
	{
	}

	void OnReload(Configuration::Conf &conf) override
	{
		AnopeCommandRPCEvent::pretenduser = conf.GetModule(this).Get<bool>("pretenduser");
	}
};

MODULE_INIT(ModuleRPCAccount)
