// 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/nickserv/cert.h"
#include "modules/nickserv/service.h"

typedef std::map<Anope::string, ChannelStatus> NSRecoverInfo;

class NSRecoverSvsnick final
{
public:
	Reference<User> from;
	Anope::string to;
};

class NSRecoverRequest final
	: public IdentifyRequest
{
	CommandSource source;
	Command *cmd;
	Anope::string user;

public:
	NSRecoverRequest(Module *o, CommandSource &src, Command *c, const Anope::string &nick, const Anope::string &pass)
		: IdentifyRequest(o, nick, pass, src.ip)
		, source(src)
		, cmd(c)
		, user(nick)
	{
	}

	void OnSuccess(NickAlias *na) override
	{
		User *u = User::Find(user, true);
		if (!source.GetUser() || !source.service)
			return;

		Log(LOG_COMMAND, source, cmd) << "for " << na->nick;

		/* Nick is being held by us, release it */
		if (na->HasExt("HELD"))
		{
			NickServ::service->Release(na);
			source.Reply(_("Services' hold on \002%s\002 has been released."), na->nick.c_str());
		}
		else if (!u)
		{
			source.Reply(_("No one is using your nick, and services are not holding it."));
		}
		// If the user being recovered is identified for the account of the nick then the user is the
		// same person that is executing the command, so kill them off (old GHOST command).
		else if (u->Account() == na->nc)
		{
			if (!source.GetAccount())
			{
				source.GetUser()->Login(u->Account());
				Log(LOG_COMMAND, source, cmd) << "and was automatically identified to " << u->Account()->display;
			}

			if (Config->GetModule("ns_recover").Get<bool>("restoreonrecover"))
			{
				if (!u->chans.empty())
				{
					auto *ei = source.GetUser()->Extend<NSRecoverInfo>("recover");
					for (auto &[chan, cuc] : u->chans)
						(*ei)[chan->name] = cuc->status;
				}
			}

			u->SendMessage(source.service, _(
					"This nickname has been recovered by %s. If you did not do "
					"this then %s may have your password, and you should change it."
				),
				source.GetNick().c_str(),
				source.GetNick().c_str());

			Anope::string buf = source.command.upper() + " command used by " + source.GetNick();
			u->Kill(*source.service, buf);

			source.Reply(_("Ghost with your nick has been killed."));

			if (IRCD->CanSVSNick)
				IRCD->SendForceNickChange(source.GetUser(), GetAccount(), Anope::CurTime);
		}
		/* User is not identified or not identified to the same account as the person using this command */
		else
		{
			if (!source.GetAccount())
			{
				source.GetUser()->Login(na->nc); // Identify the user using the command if they arent identified
				Log(LOG_COMMAND, source, cmd) << "and was automatically identified to " << na->nick << " (" << na->nc->display << ")";
				source.Reply(_("You have been logged in as \002%s\002."), na->nc->display.c_str());
			}

			u->SendMessage(source.service, _("This nickname has been recovered by %s."), source.GetNick().c_str());

			if (IRCD->CanSVSNick)
			{
				auto *svs = u->Extend<NSRecoverSvsnick>("svsnick");
				svs->from = source.GetUser();
				svs->to = u->nick;
			}

			if (NickServ::service)
				NickServ::service->Collide(u, na);

			if (IRCD->CanSVSNick)
			{
				/* If we can svsnick then release our hold and svsnick the user using the command */
				if (NickServ::service)
					NickServ::service->Release(na);

				source.Reply(_("You have regained control of \002%s\002."), u->nick.c_str());
			}
			else
			{
				source.Reply(_("The user with your nick has been removed. Use this command again to release services's hold on your nick."));
			}
		}
	}

	void OnFail() override
	{
		if (NickAlias::Find(GetAccount()) != NULL)
		{
			source.Reply(ACCESS_DENIED);
			if (!GetPassword().empty())
			{
				Log(LOG_COMMAND, source, cmd) << "with an invalid password for " << GetAccount();
				if (source.GetUser())
					source.GetUser()->BadPassword();
			}
		}
		else
			source.Reply(NICK_X_NOT_REGISTERED, GetAccount().c_str());
	}
};

class CommandNSRecover final
	: public Command
{
public:
	CommandNSRecover(Module *creator) : Command(creator, "nickserv/recover", 1, 2)
	{
		this->SetDesc(_("Regains control of your nick"));
		this->SetSyntax(_("\037nickname\037 [\037password\037]"));
		this->AllowUnregistered(true);
	}

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

		User *user = User::Find(nick, true);

		if (user && source.GetUser() == user)
		{
			source.Reply(_("You can't %s yourself!"), source.command.lower().c_str());
			return;
		}

		auto *na = NickAlias::Find(nick);
		if (!na)
		{
			source.Reply(NICK_X_NOT_REGISTERED, nick.c_str());
			return;
		}
		else if (na->nc->HasExt("NS_SUSPENDED"))
		{
			source.Reply(NICK_X_SUSPENDED, na->nick.c_str());
			return;
		}

		bool ok = false;
		if (source.GetAccount() == na->nc)
			ok = true;

		auto *cl = na->nc->GetExt<NickServ::CertList>(NICKSERV_CERT_EXT);
		if (source.GetUser() && !source.GetUser()->fingerprint.empty() && cl && cl->FindCert(source.GetUser()->fingerprint))
			ok = true;

		if (source.HasPriv("nickserv/recover"))
			ok = true;

		if (!ok && !pass.empty())
		{
			auto *req = new NSRecoverRequest(owner, source, this, na->nick, pass);
			FOREACH_MOD(OnCheckAuthentication, (source.GetUser(), req));
			req->Dispatch();
		}
		else
		{
			NSRecoverRequest req(owner, source, this, na->nick, pass);

			if (ok)
				req.OnSuccess(na);
			else
				req.OnFail();
		}
	}

	bool OnHelp(CommandSource &source, const Anope::string &subcommand) override
	{
		this->SendSyntax(source);
		source.Reply(" ");
		source.Reply(_(
			"Recovers your nick from another user or from services. "
			"If services are currently holding your nick, the hold "
			"will be released. If another user is holding your nick "
			"and is identified they will be killed (similar to the old "
			"GHOST command). If they are not identified they will be "
			"forced off of the nick."
		));
		return true;
	}
};

class NSRecover final
	: public Module
{
	CommandNSRecover commandnsrecover;
	PrimitiveExtensibleItem<NSRecoverInfo> recover;
	PrimitiveExtensibleItem<NSRecoverSvsnick> svsnick;

public:
	NSRecover(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR),
		commandnsrecover(this), recover(this, "recover"), svsnick(this, "svsnick")
	{

		if (Config->GetModule("nickserv").Get<bool>("nonicknameownership"))
			throw ModuleException(modname + " can not be used with options:nonicknameownership enabled");

	}

	void OnUserNickChange(User *u, const Anope::string &oldnick) override
	{
		if (Config->GetModule(this).Get<bool>("restoreonrecover"))
		{
			NSRecoverInfo *ei = recover.Get(u);
			BotInfo *NickServ = Config->GetClient("NickServ");

			if (ei != NULL && NickServ != NULL)
				for (auto it = ei->begin(), it_end = ei->end(); it != it_end;)
				{
					Channel *c = Channel::Find(it->first);
					const Anope::string &cname = it->first;
					++it;

					/* User might already be on the channel */
					if (u->FindChannel(c))
						this->OnJoinChannel(u, c);
					else if (IRCD->CanSVSJoin)
						IRCD->SendSVSJoin(NickServ, u, cname, "");
				}
		}

		NSRecoverSvsnick *svs = svsnick.Get(u);
		if (svs)
		{
			if (svs->from)
			{
				// svsnick from to to
				IRCD->SendForceNickChange(svs->from, svs->to, Anope::CurTime);
			}

			svsnick.Unset(u);
		}
	}

	void OnJoinChannel(User *u, Channel *c) override
	{
		if (Config->GetModule(this).Get<bool>("restoreonrecover"))
		{
			NSRecoverInfo *ei = recover.Get(u);

			if (ei != NULL)
			{
				auto it = ei->find(c->name);
				if (it != ei->end())
				{
					for (auto mode : it->second.Modes())
						c->SetMode(c->WhoSends(), mode, u->GetUID());

					ei->erase(it);
					if (ei->empty())
						recover.Unset(u);
				}
			}
		}
	}
};

MODULE_INIT(NSRecover)
