// 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/encryption.h"
#include "modules/sql.h"

namespace
{
	Module *me;

	ServiceReference<Encryption::Provider> encryption("Encryption::Provider");
	Anope::string password_hash, password_field;
}
class SQLAuthenticationResult final
	: public SQL::Interface
{
	Reference<User> user;
	IdentifyRequest *req;

	void OnFail(const Anope::string &reason)
	{
		Log(LOG_DEBUG) << "sql_authentication: Unsuccessful authentication for " << req->GetAccount() << ": " << reason;
		delete this;
	}

public:
	SQLAuthenticationResult(User *u, IdentifyRequest *r) : SQL::Interface(me), user(u), req(r)
	{
		req->Hold(me);
	}

	~SQLAuthenticationResult()
	{
		req->Release(me);
	}

	void OnResult(const SQL::Result &r) override
	{
		if (r.Rows() == 0)
			return OnFail("no rows");

		if (!password_hash.empty())
		{
			if (!encryption)
				return OnFail("encryption provider not loaded");

			auto success = false;
			for (auto i = 0; i < r.Rows(); ++i)
			{
				if (encryption->Compare(r.Get(i, password_field), req->GetPassword()))
				{
					success = true;
					break;
				}
			}

			if (!success)
				return OnFail("no matching passwords");
		}

		Log(LOG_DEBUG) << "sql_authentication: Successful authentication for " << req->GetAccount();

		Anope::string email;
		try
		{
			email = r.Get(0, "email");
		}
		catch (const SQL::Exception &) { }

		NickAlias *na = NickAlias::Find(req->GetAccount());
		BotInfo *NickServ = Config->GetClient("NickServ");
		if (na == NULL)
		{
			na = new NickAlias(req->GetAccount(), new NickCore(req->GetAccount()));
			FOREACH_MOD(OnNickRegister, (user, na, ""));
			if (user && NickServ)
				user->SendMessage(NickServ, _("Your account \002%s\002 has been successfully created."), na->nick.c_str());
		}

		if (!email.empty() && email != na->nc->email)
		{
			na->nc->email = email;
			if (user && NickServ)
				user->SendMessage(NickServ, _("Your email address has been updated to \002%s\002."), email.c_str());
		}

		req->Success(me, na);
		delete this;
	}

	void OnError(const SQL::Result &r) override
	{
		Log(this->owner) << "sql_authentication: Error executing query " << r.GetQuery().query << ": " << r.GetError();
		delete this;
	}
};

class ModuleSQLAuthentication final
	: public Module
{
	Anope::string query;
	Anope::string disable_reason, disable_email_reason;

	ServiceReference<SQL::Provider> SQL;

public:
	ModuleSQLAuthentication(const Anope::string &modname, const Anope::string &creator)
		: Module(modname, creator, EXTRA | VENDOR)
		, SQL("SQL::Provider")
	{
		me = this;
	}

	void OnReload(Configuration::Conf &conf) override
	{
		const auto &config = conf.GetModule(this);
		this->query =  config.Get<const Anope::string>("query");
		this->disable_reason = config.Get<const Anope::string>("disable_reason");
		this->disable_email_reason = config.Get<Anope::string>("disable_email_reason");
		this->SQL.SetServiceName(config.Get<const Anope::string>("engine"));

		password_hash = config.Get<const Anope::string>("password_hash");
		if (!password_hash.empty())
		{
			password_field = config.Get<const Anope::string>("password_field", "password");
			encryption.SetServiceName(password_hash);
		}
	}

	EventReturn OnPreCommand(CommandSource &source, Command *command, std::vector<Anope::string> &params) override
	{
		if (!this->disable_reason.empty() && (command->name == "nickserv/register" || command->name == "nickserv/group"))
		{
			source.Reply(this->disable_reason);
			return EVENT_STOP;
		}

		if (!this->disable_email_reason.empty() && command->name == "nickserv/set/email")
		{
			source.Reply(this->disable_email_reason);
			return EVENT_STOP;
		}

		return EVENT_CONTINUE;
	}

	void OnCheckAuthentication(User *u, IdentifyRequest *req) override
	{
		if (!this->SQL)
		{
			Log(this) << "Unable to find SQL engine";
			return;
		}

		SQL::Query q(this->query);
		q.SetValue("a", req->GetAccount());
		q.SetValue("i", req->GetAddress().str());
		q.SetValue("n", u ? u->nick : "");
		q.SetValue("p", req->GetPassword());

		this->SQL->Run(new SQLAuthenticationResult(u, req), q);

		Log(LOG_DEBUG) << "sql_authentication: Checking authentication for " << req->GetAccount();
	}

	void OnPreNickExpire(NickAlias *na, bool &expire) override
	{
		// We can't let nicks expire if they still have a group or
		// there will be a zombie account left over that can't be
		// authenticated to.
		if (na->nick == na->nc->display && na->nc->aliases->size() > 1)
			expire = false;
	}
};

MODULE_INIT(ModuleSQLAuthentication)
