// 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/botserv/badwords.h"
#include "modules/botserv/kick.h"
#include "modules/chanserv/akick.h"
#include "modules/chanserv/entrymsg.h"
#include "modules/chanserv/mode.h"
#include "modules/hostserv/request.h"
#include "modules/info.h"
#include "modules/nickserv/cert.h"
#include "modules/operserv/forbid.h"
#include "modules/operserv/news.h"
#include "modules/operserv/oper.h"
#include "modules/operserv/session.h"
#include "modules/set_misc.h"
#include "modules/suspend.h"

// Handles reading from an Atheme database row.
class AthemeRow final
{
private:
	// The number of failed reads.
	unsigned error = 0;

	// The underlying token stream.
	spacesepstream stream;

public:
	AthemeRow(const Anope::string &str)
		: stream(str)
	{
	}

	operator bool() const
	{
		return !error;
	}

	// Retrieves the next parameter.
	Anope::string Get()
	{
		Anope::string token;
		if (!stream.GetToken(token))
			error++;
		return token;
	}

	// Retrieves the next parameter as a number.
	template<typename Numeric>
	std::enable_if_t<std::is_arithmetic_v<Numeric>, Numeric> GetNum()
	{
		return Anope::Convert<Numeric>(Get(), 0);
	}

	// Retrieves the entire row.
	Anope::string GetRow()
	{
		return stream.GetString();
	}

	// Retrieves the remaining data in the row.
	Anope::string GetRemaining()
	{
		auto remaining = stream.GetRemaining();
		if (remaining.empty())
			error++;
		return remaining;
	}

	bool LogError(Module *mod)
	{
		Log(mod) << "Malformed database line (expected " << error << " fields): " << GetRow();
		return false;
	}
};

struct ModeLockData final
{
	char letter;
	Anope::string name;
	Anope::string value;
	bool set;

	ModeLockData(const Anope::string &n, bool s, const Anope::string &v = "")
		: letter(0)
		, name(n)
		, value(v)
		, set(s)
	{
	}

	ModeLockData(char l, const Anope::string &v = "")
		: letter(l)
		, value(v)
		, set(true)
	{
	}

	Anope::string str() const
	{
		std::stringstream buf;
		buf << '+' << (name.empty() ? letter : name);
		if (value.empty())
			buf << ' ' << value;
		return buf.str();
	}
};

struct ChannelData final
{
	Anope::unordered_map<ChanServ::AutoKick *> akicks;
	Anope::string bot;
	Anope::string info_adder;
	Anope::string info_message;
	time_t info_ts = 0;
	std::vector<ModeLockData> mlocks;
	Anope::string suspend_by;
	Anope::string suspend_reason;
	time_t suspend_ts = 0;
};

struct UserData final
{
	Anope::string info_adder;
	Anope::string info_message;
	time_t info_ts = 0;
	Anope::string last_quit;
	Anope::string last_userhost;
	Anope::string last_userhost_real;
	bool noexpire = false;
	bool protect = false;
	std::optional<time_t> protectafter;
	Anope::string suspend_by;
	Anope::string suspend_reason;
	time_t suspend_ts = 0;
	Anope::string vhost;
	Anope::string vhost_creator;
	Anope::map<Anope::string> vhost_nick;
	time_t vhost_ts = 0;
};

namespace
{
	// Whether we can safely import clones.
	bool import_clones = true;
}

class DBAtheme final
	: public Module
{
private:
	ServiceReference<AccessProvider> accessprov;
	PrimitiveExtensibleItem<ChannelData> chandata;
	std::map<Anope::string, char> flags;
	PrimitiveExtensibleItem<UserData> userdata;
	ServiceReference<XLineManager> sglinemgr;
	ServiceReference<XLineManager> snlinemgr;
	ServiceReference<XLineManager> sqlinemgr;
	Anope::map<Anope::string> csmiscdata;
	Anope::map<Anope::string> nsmiscdata;

	Anope::map<std::function<bool(DBAtheme*,AthemeRow&)>> rowhandlers = {
		{ "AC",         &DBAtheme::HandleIgnore    },
		{ "AR",         &DBAtheme::HandleIgnore    },
		{ "BE",         &DBAtheme::HandleBE        },
		{ "BLE",        &DBAtheme::HandleIgnore    },
		{ "BOT",        &DBAtheme::HandleBOT       },
		{ "BOT-COUNT",  &DBAtheme::HandleIgnore    },
		{ "BW",         &DBAtheme::HandleBW        },
		{ "CA",         &DBAtheme::HandleCA        },
		{ "CF",         &DBAtheme::HandleIgnore    },
		{ "CFCHAN",     &DBAtheme::HandleIgnore    },
		{ "CFDBV",      &DBAtheme::HandleIgnore    },
		{ "CFMD",       &DBAtheme::HandleIgnore    },
		{ "CFOP",       &DBAtheme::HandleIgnore    },
		{ "CLONES-CD",  &DBAtheme::HandleIgnore    },
		{ "CLONES-CK",  &DBAtheme::HandleIgnore    },
		{ "CLONES-DBV", &DBAtheme::HandleCLONESDBV },
		{ "CLONES-EX",  &DBAtheme::HandleCLONESEX  },
		{ "CLONES-GR",  &DBAtheme::HandleIgnore    },
		{ "CSREQ",      &DBAtheme::HandleIgnore    },
		{ "CSREQ",      &DBAtheme::HandleIgnore    },
		{ "DBV",        &DBAtheme::HandleDBV       },
		{ "GACL",       &DBAtheme::HandleIgnore    },
		{ "GDBV",       &DBAtheme::HandleIgnore    },
		{ "GE",         &DBAtheme::HandleIgnore    },
		{ "GFA",        &DBAtheme::HandleIgnore    },
		{ "GRP",        &DBAtheme::HandleIgnore    },
		{ "GRVER",      &DBAtheme::HandleGRVER     },
		{ "HE",         &DBAtheme::HandleIgnore    },
		{ "HO",         &DBAtheme::HandleIgnore    },
		{ "HR",         &DBAtheme::HandleHR        },
		{ "JM",         &DBAtheme::HandleIgnore    },
		{ "KID",        &DBAtheme::HandleIgnore    },
		{ "KL",         &DBAtheme::HandleKL        },
		{ "LI",         &DBAtheme::HandleLI        },
		{ "LIO",        &DBAtheme::HandleLIO       },
		{ "LUID",       &DBAtheme::HandleIgnore    },
		{ "MC",         &DBAtheme::HandleMC        },
		{ "MCFP",       &DBAtheme::HandleMCFP      },
		{ "MDA",        &DBAtheme::HandleMDA       },
		{ "MDC",        &DBAtheme::HandleMDC       },
		{ "MDEP",       &DBAtheme::HandleIgnore    },
		{ "MDG",        &DBAtheme::HandleIgnore    },
		{ "MDN",        &DBAtheme::HandleMDN       },
		{ "MDU",        &DBAtheme::HandleMDU       },
		{ "ME",         &DBAtheme::HandleME        },
		{ "MI",         &DBAtheme::HandleMI        },
		{ "MM",         &DBAtheme::HandleMM        },
		{ "MN",         &DBAtheme::HandleMN        },
		{ "MU",         &DBAtheme::HandleMU        },
		{ "NAM",        &DBAtheme::HandleNAM       },
		{ "QID",        &DBAtheme::HandleIgnore    },
		{ "QL",         &DBAtheme::HandleQL        },
		{ "RM",         &DBAtheme::HandleIgnore    },
		{ "RR",         &DBAtheme::HandleIgnore    },
		{ "RW",         &DBAtheme::HandleIgnore    },
		{ "SI",         &DBAtheme::HandleIgnore    },
		{ "SO",         &DBAtheme::HandleSO        },
		{ "TS",         &DBAtheme::HandleIgnore    },
		{ "XID",        &DBAtheme::HandleIgnore    },
		{ "XL",         &DBAtheme::HandleXL        },
	};

	void ApplyAccess(Anope::string &in, char flag, Anope::string &out, std::initializer_list<const char*> privs)
	{
		for (const auto *priv : privs)
		{
			auto pos = in.find(flag);
			if (pos != Anope::string::npos)
			{
				auto privchar = flags.find(priv);
				if (privchar != flags.end())
				{
					out.push_back(privchar->second);
					in.erase(pos, 1);
				}
			}
		}
	}

	void ApplyFlags(Extensible *ext, Anope::string &flags, char flag, const char *extname, bool extend = true)
	{
		auto pos = flags.find(flag);
		auto has_flag = (pos != Anope::string::npos);

		if (has_flag == extend)
			ext->Extend<bool>(extname);
		else
			ext->Shrink<bool>(extname);

		if (has_flag)
			flags.erase(pos, 1);
	}

	void ApplyLocks(ChannelInfo *ci, unsigned locks, const Anope::string &limit, const Anope::string &key, bool set)
	{
		auto *data = chandata.Require(ci);

		// Start off with the standard mode values.
		if (locks & 0x1u)
			data->mlocks.emplace_back("INVITE", set);
		if (locks & 0x2u)
			data->mlocks.emplace_back("KEY", set, key);
		if (locks & 0x4u)
			data->mlocks.emplace_back("LIMIT", set, limit);
		if (locks & 0x8u)
			data->mlocks.emplace_back("MODERATED", set);
		if (locks & 0x10u)
			data->mlocks.emplace_back("NOEXTERNAL", set);
		if (locks & 0x40u)
			data->mlocks.emplace_back("PRIVATE", set);
		if (locks & 0x80u)
			data->mlocks.emplace_back("SECRET", set);
		if (locks & 0x100u)
			data->mlocks.emplace_back("TOPIC", set);
		if (locks & 0x200u)
			data->mlocks.emplace_back("REGISTERED", set);

		// Atheme also supports per-ircd values here (ew).
		if (IRCD->owner->name == "inspircd")
		{
			if (locks & 0x1000u)
				data->mlocks.emplace_back("BLOCKCOLOR", set);
			if (locks & 0x2000u)
				data->mlocks.emplace_back("REGMODERATED", set);
			if (locks & 0x4000u)
				data->mlocks.emplace_back("REGISTEREDONLY", set);
			if (locks & 0x8000u)
				data->mlocks.emplace_back("OPERONLY", set);
			if (locks & 0x10000u)
				data->mlocks.emplace_back("NONOTICE", set);
			if (locks & 0x20000u)
				data->mlocks.emplace_back("NOKICK", set);
			if (locks & 0x40000u)
				data->mlocks.emplace_back("STRIPCOLOR", set);
			if (locks & 0x80000u)
				data->mlocks.emplace_back("NOKNOCK", set);
			if (locks & 0x100000u)
				data->mlocks.emplace_back("ALLINVITE", set);
			if (locks & 0x200000u)
				data->mlocks.emplace_back("NOCTCP", set);
			if (locks & 0x400000u)
				data->mlocks.emplace_back("AUDITORIUM", set);
			if (locks & 0x800000u)
				data->mlocks.emplace_back("SSL", set);
			if (locks & 0x100000u)
				data->mlocks.emplace_back("NONICK", set);
			if (locks & 0x200000u)
				data->mlocks.emplace_back("CENSOR", set);
			if (locks & 0x400000u)
				data->mlocks.emplace_back("BLOCKCAPS", set);
			if (locks & 0x800000u)
				data->mlocks.emplace_back("PERM", set);
			if (locks & 0x2000000u)
				data->mlocks.emplace_back("DELAYEDJOIN", set);
		}
		else if (IRCD->owner->name == "ngircd")
		{
			if (locks & 0x1000u)
				data->mlocks.emplace_back("REGISTEREDONLY", set);
			if (locks & 0x2000u)
				data->mlocks.emplace_back("OPERONLY", set);
			if (locks & 0x4000u)
				data->mlocks.emplace_back("PERM", set);
			if (locks & 0x8000u)
				data->mlocks.emplace_back("SSL", set);
		}
		else if (IRCD->owner->name == "solanum")
		{
			if (locks & 0x1000u)
				data->mlocks.emplace_back("BLOCKCOLOR", set);
			if (locks & 0x2000u)
				data->mlocks.emplace_back("REGISTEREDONLY", set);
			if (locks & 0x4000u)
				data->mlocks.emplace_back("OPMODERATED", set);
			if (locks & 0x8000u)
				data->mlocks.emplace_back("ALLINVITE", set);
			if (locks & 0x10000u)
				data->mlocks.emplace_back("LBAN", set);
			if (locks & 0x20000u)
				data->mlocks.emplace_back("PERM", set);
			if (locks & 0x40000u)
				data->mlocks.emplace_back("ALLOWFORWARD", set);
			if (locks & 0x80000u)
				data->mlocks.emplace_back("NOFORWARD", set);
			if (locks & 0x100000u)
				data->mlocks.emplace_back("NOCTCP", set);
			if (locks & 0x400000u)
				data->mlocks.emplace_back("SSL", set);
			if (locks & 0x800000u)
				data->mlocks.emplace_back("OPERONLY", set);
			if (locks & 0x1000000u)
				data->mlocks.emplace_back("ADMINONLY", set);
			if (locks & 0x2000000u)
				data->mlocks.emplace_back("NONOTICE", set);
			if (locks & 0x4000000u)
				data->mlocks.emplace_back("PROTECTED", set);
			if (locks & 0x8000000)
				data->mlocks.emplace_back("NOFILTER", set);
			if (locks & 0x10000000U)
				data->mlocks.emplace_back("REGMODERATED", set);
		}
		else if (IRCD->owner->name == "unrealircd")
		{
			if (locks & 0x1000u)
				data->mlocks.emplace_back("BLOCKCOLOR", set);
			if (locks & 0x2000u)
				data->mlocks.emplace_back("REGMODERATED", set);
			if (locks & 0x4000u)
				data->mlocks.emplace_back("REGISTEREDONLY", set);
			if (locks & 0x8000u)
				data->mlocks.emplace_back("OPERONLY", set);
			if (locks & 0x20000u)
				data->mlocks.emplace_back("NOKICK", set);
			if (locks & 0x40000u)
				data->mlocks.emplace_back("STRIPCOLOR", set);
			if (locks & 0x80000u)
				data->mlocks.emplace_back("NOKNOCK", set);
			if (locks & 0x100000u)
				data->mlocks.emplace_back("NOINVITE", set);
			if (locks & 0x200000u)
				data->mlocks.emplace_back("NOCTCP", set);
			if (locks & 0x800000u)
				data->mlocks.emplace_back("SSL", set);
			if (locks & 0x1000000u)
				data->mlocks.emplace_back("NONICK", set);
			if (locks & 0x4000000u)
				data->mlocks.emplace_back("CENSOR", set);
			if (locks & 0x8000000u)
				data->mlocks.emplace_back("PERM", set);
			if (locks & 0x1000000u)
				data->mlocks.emplace_back("NONOTICE", set);
			if (locks & 0x2000000u)
				data->mlocks.emplace_back("DELAYJOIN", set);
		}
		else if (IRCD->owner->name != "ratbox")
		{
			Log(this) << "Unable to import mode locks for " << IRCD->GetProtocolName();
		}
	}

	void ApplyPassword(NickCore *nc, Anope::string &flags, const Anope::string &pass)
	{
		auto pos = flags.find('C');
		if (pos == Anope::string::npos)
		{
			// Password is unencrypted so we can use it.
			Anope::Encrypt(pass, nc->pass);
			return;
		}

		// We are processing an encrypted password.
		flags.erase(pos, 1);

		// Atheme supports several password hashing methods. We can only import
		// some of them currently.
		//
		// anope-enc-sha256  Converted to enc_sha256
		// argon2            Converted to enc_argon2
		// base64            Converted to the first encryption algorithm
		// bcrypt            Converted to enc_bcrypt
		// crypt3-des        NO
		// crypt3-md5        Converted to enc_posix
		// crypt3-sha2-256   Converted to enc_posix
		// crypt3-sha2-512   Converted to enc_posix
		// ircservices       NO
		// pbkdf2            NO
		// pbkdf2v2          NO
		// rawmd5            Converted to enc_md5
		// rawsha1           Converted to enc_sha1
		// rawsha2-256       Converted to enc_sha2
		// rawsha2-512       Converted to enc_sha2
		// scrypt            NO
		if (pass.compare(0, 18, "$anope$enc_sha256$", 18) == 0)
		{
			auto sep = pass.find('$', 18);
			auto iv = Anope::B64Decode(pass.substr(18, sep - 18));
			auto pw = Anope::B64Decode(pass.substr(sep + 1));
			nc->pass = "sha256:" + Anope::Hex(pw) + ":" + Anope::Hex(iv);
		}

		else if (pass.compare(0, 9, "$argon2d$", 9) == 0)
			nc->pass = "argon2d:" + pass;

		else if (pass.compare(0, 9, "$argon2i$", 9) == 0)
			nc->pass = "argon2i:" + pass;

		else if (pass.compare(0, 10, "$argon2id$", 10) == 0)
			nc->pass = "argon2id:" + pass;

		else if (pass.compare(0, 8, "$base64$", 8) == 0)
		{
			auto rawpass = Anope::B64Decode(pass.substr(8));
			Anope::Encrypt(rawpass, nc->pass);
		}

		else if (pass.compare(0, 8, "$rawmd5$", 8) == 0)
			nc->pass = "md5:" + pass.substr(8);

		else if (pass.compare(0, 9, "$rawsha1$", 9) == 0)
			nc->pass = "sha1:" + pass.substr(9);

		else if (pass.compare(0, 11, "$rawsha256$", 11) == 0)
			nc->pass = "raw-sha256:" + pass.substr(11);

		else if (pass.compare(0, 11, "$rawsha512$", 11) == 0)
			nc->pass = "raw-sha512:" + pass.substr(11);

		else if (pass.compare(0, 3, "$1$", 3) == 0 || pass.compare(0, 3, "$5", 3) == 0 || pass.compare(0, 3, "$6", 3) == 0)
			nc->pass = "posix:" + pass;

		else if (pass.compare(0, 4, "$2a$", 4) == 0 || pass.compare(0, 4, "$2b$", 4) == 0)
			nc->pass = "bcrypt:" + pass;

		else
		{
			// Generate a new password as we can't use the old one.
			auto maxpasslen = Config->GetModule("nickserv").Get<unsigned>("maxpasslen", "50");
			Anope::Encrypt(Anope::Random(maxpasslen), nc->pass);

			// If the password is set to * then an external service is being used for authentication.
			if (pass != "*")
				Log(this) << "Unable to convert the password for " << nc->display << " as Anope does not support the format!";
		}

	}

	bool HandleBE(AthemeRow &row)
	{
		// BE <email> <created> <creator> <reason>
		auto email = row.Get();
		auto created = row.GetNum<time_t>();
		auto creator = row.Get();
		auto reason = row.GetRemaining();

		if (!row)
			return row.LogError(this);

		if (!OperServ::forbid_service)
		{
			Log(this) << "Unable to convert forbidden email address " << email << " as os_forbid is not loaded";
			return true;
		}

		auto *forbid = OperServ::forbid_service->CreateForbid();
		forbid->created = created;
		forbid->creator = creator;
		forbid->mask = email;
		forbid->reason = reason;
		forbid->type = OperServ::FT_EMAIL;
		OperServ::forbid_service->AddForbid(forbid);
		return true;
	}

	bool HandleBOT(AthemeRow &row)
	{
		// BOT <nick> <user> <host> <operonly> <created> <real>
		auto nick = row.Get();
		auto user = row.Get();
		auto host = row.Get();
		auto operonly = row.GetNum<unsigned>();
		auto created = row.GetNum<time_t>();
		auto real = row.GetRemaining();

		if (!row)
			return row.LogError(this);

		auto *bi = BotInfo::Find(nick);
		if (bi)
		{
			Log(this) << "Refusing to import duplicate bot: " << nick;
			return true;
		}

		bi = new BotInfo(nick, user, host, real);
		bi->oper_only = operonly;
		bi->created = created;
		return true;
	}

	bool HandleBW(AthemeRow &row)
	{
		// BW <badword> <added> <creator> <channel> <action>
		auto badword = row.Get();
		/* auto added = */ row.GetNum<time_t>();
		/* auto creator = */ row.Get();
		auto channel = row.Get();
		/* auto action = */ row.Get();

		if (!row)
			return row.LogError(this);

		auto *ci = ChannelInfo::Find(channel);
		if (!ci)
		{
			Log(this) << "Missing ChannelInfo for BW: " << channel;
			return false;
		}

		auto *bw = ci->Require<BotServ::BadWords>(BOTSERV_BAD_WORDS_EXT);
		if (!bw)
		{
			Log(this) << "Unable to import badwords for " << ci->name << " as bs_kick is not loaded";
			return true;
		}

		auto *kd = ci->Require<BotServ::KickerData>(BOTSERV_KICKER_DATA_EXT);
		if (kd)
		{
			kd->badwords = true;
			kd->ttb[BotServ::TTB_BADWORDS] = 0;
		}

		bw->AddBadWord(badword, BotServ::BW_ANY);
		return true;
	}

	bool HandleCA(AthemeRow &row)
	{
		// CA <channel> <account/mask> <flags> <modifiedtime> <setter>
		auto channel = row.Get();
		auto mask = row.Get();
		auto flags = row.Get();
		auto modifiedtime = row.GetNum<time_t>();
		auto setter = row.Get();

		if (!row)
			return row.LogError(this);

		auto *ci = ChannelInfo::Find(channel);
		if (!ci)
		{
			Log(this) << "Missing ChannelInfo for CA: " << channel;
			return false;
		}

		auto *nc = NickCore::Find(mask);
		if (flags.find('b') != Anope::string::npos)
		{
			if (ChanServ::akick_service)
			{
				Log(this) << "Unable to import channel akick for " << ci->name << " as cs_akick is not loaded";
				return true;
			}

			auto *data = chandata.Require(ci);
			if (nc)
				data->akicks[mask] = ChanServ::akick_service->AddAKick(ci, setter, nc, "", modifiedtime, modifiedtime);
			else
				data->akicks[mask] = ChanServ::akick_service->AddAKick(ci, setter, mask, "", modifiedtime, modifiedtime);
			return true;
		}

		if (!accessprov)
		{
			Log(this) << "Unable to import channel access for " << ci->name << " as cs_flags is not loaded";
			return true;
		}

		auto originalflags = flags;
		Anope::string accessflags;
		ApplyAccess(flags, 'A', accessflags, { "ACCESS_LIST" });
		ApplyAccess(flags, 'a', accessflags, { "AUTOPROTECT", "PROTECT", "PROTECTME" });
		ApplyAccess(flags, 'e', accessflags, { "GETKEY", "NOKICK", "UNBANME" });
		ApplyAccess(flags, 'f', accessflags, { "ACCESS_CHANGE" });
		ApplyAccess(flags, 'F', accessflags, { "FOUNDER" });
		ApplyAccess(flags, 'H', accessflags, { "AUTOHALFOP" });
		ApplyAccess(flags, 'h', accessflags, { "HALFOP", "HALFOPME" });
		ApplyAccess(flags, 'i', accessflags, { "INVITE" });
		ApplyAccess(flags, 'O', accessflags, { "AUTOOP" });
		ApplyAccess(flags, 'o', accessflags, { "OP", "OPME" });
		ApplyAccess(flags, 'q', accessflags, { "AUTOOWNER", "OWNER", "OWNERME" });
		ApplyAccess(flags, 'r', accessflags, { "KICK" });
		ApplyAccess(flags, 's', accessflags, { "SET" });
		ApplyAccess(flags, 't', accessflags, { "TOPIC" });
		ApplyAccess(flags, 'V', accessflags, { "AUTOVOICE" });
		ApplyAccess(flags, 'v', accessflags, { "VOICE", "VOICEME" });
		if (!accessflags.empty())
		{
			auto *access = accessprov->Create();
			access->SetMask(mask, ci);
			access->creator = setter;
			access->description = "Imported from Atheme: " + originalflags;
			access->last_seen = modifiedtime;
			access->created = modifiedtime;
			access->AccessUnserialize(accessflags);
			ci->AddAccess(access);
		}

		if (flags != "+")
			Log(this) << "Unable to convert channel access flags " << flags << " for " << mask << " on " << ci->name;

		return true;
	}

	bool HandleCLONESDBV(AthemeRow &row)
	{
		// CLONES-DBV <version>
		auto version = row.GetNum<unsigned>();
		if (version != 3)
		{
			Log(this) << "Clones database is version " << version << " which is not supported!";
			import_clones = false;
		}
		return true;
	}

	bool HandleCLONESEX(AthemeRow &row)
	{
		if (!import_clones)
			return HandleIgnore(row);

		// CLONES-EX <ip> <allowed> <warn> <expires> <reason>
		auto ip = row.Get();
		auto allowed = row.GetNum<unsigned>();
		/* auto warn = */ row.GetNum<unsigned>();
		auto expires = row.GetNum<time_t>();
		auto reason = row.GetRemaining();

		if (!row)
			return row.LogError(this);

		if (!OperServ::session_service)
		{
			Log(this) << "Unable to import session limit for " << ip << " as os_session is not loaded";
			return true;
		}

		auto *exception = OperServ::session_service->CreateException();
		exception->mask = ip;
		exception->limit = allowed;
		exception->who = "Unknown";
		exception->time = Anope::CurTime;
		exception->expires = expires;
		exception->reason = reason;
		OperServ::session_service->AddException(exception);
		return true;
	}

	bool HandleDBV(AthemeRow &row)
	{
		// DBV <version>
		auto version = row.GetNum<unsigned>();
		if (version != 12)
		{
			Log(this) << "Database is version " << version << " which is not supported!";
			return false;
		}
		return true;
	}

	bool HandleGRVER(AthemeRow &row)
	{
		// GRVER <version>
		auto version = row.GetNum<unsigned>();
		if (version != 1)
		{
			Log(this) << "Database grammar is version " << version << " which is not supported!";
			return false;
		}
		return true;
	}

	bool HandleHR(AthemeRow &row)
	{
		// HR <nick> <host> <reqts> <creator>
		auto nick = row.Get();
		auto host = row.Get();
		auto reqts = row.GetNum<time_t>();
		/* auto creator = */ row.Get();

		if (!row)
			return row.LogError(this);

		auto *na = NickAlias::Find(nick);
		if (!na)
		{
			Log(this) << "Missing NickAlias for HR: " << nick;
			return false;
		}

		auto *hr = na->Require<HostServ::HostRequest>(HOSTSERV_HOST_REQUEST_EXT);
		if (!hr)
		{
			Log(this) << "Unable to convert host request for " << na->nick << " as hs_request is not loaded";
			return true;
		}

		hr->nick = na->nick;
		hr->ident.clear();
		hr->host = host;
		hr->time = reqts;
		return true;
	}

	bool HandleKL(AthemeRow &row)
	{
		// KL <id> <user> <host> <duration> <settime> <setby> <reason>
		auto id = row.Get();
		auto user = row.Get();
		auto host = row.Get();
		auto duration = row.GetNum<unsigned>();
		auto settime = row.GetNum<time_t>();
		auto setby = row.Get();
		auto reason = row.GetRemaining();

		if (!row)
			return row.LogError(this);

		if (!sglinemgr)
		{
			Log(this) << "Unable to import K-line on " << user << "@" << host << " as operserv is not loaded";
			return true;
		}

		auto *xl = new XLine(user + "@" + host, setby, settime + duration, reason);
		xl->id = id;
		sglinemgr->AddXLine(xl);
		return true;
	}

	bool HandleLI(AthemeRow &row)
	{
		// LI <setter> <subject> <ts> <body>
		auto setter = row.Get();
		auto subject = row.Get();
		auto ts = row.GetNum<time_t>();
		auto body = row.GetRemaining();

		if (!row)
			return row.LogError(this);

		if (!OperServ::news_service)
		{
			Log(this) << "Unable to convert logon news as os_news is not loaded";
			return true;
		}

		auto *ni = OperServ::news_service->CreateNewsItem();
		ni->type = OperServ::NEWS_LOGON;
		ni->text = Anope::Format("[%s] %s", subject.c_str(), body.c_str());
		ni->who = setter;
		ni->time = ts;
		OperServ::news_service->AddNewsItem(ni);
		return true;
	}

	bool HandleLIO(AthemeRow &row)
	{
		// LIO <setter> <subject> <ts> <body>
		auto setter = row.Get();
		auto subject = row.Get();
		auto ts = row.GetNum<time_t>();
		auto body = row.GetRemaining();

		if (!row)
			return row.LogError(this);

		if (!OperServ::news_service)
		{
			Log(this) << "Unable to convert oper news as os_news is not loaded";
			return true;
		}

		auto *ni = OperServ::news_service->CreateNewsItem();
		ni->type = OperServ::NEWS_OPER;
		ni->text = Anope::Format("[%s] %s", subject.c_str(), body.c_str());
		ni->who = setter;
		ni->time = ts;
		OperServ::news_service->AddNewsItem(ni);
		return true;
	}

	bool HandleIgnore(AthemeRow &row)
	{
		Log(LOG_DEBUG_3) << "Intentionally ignoring Atheme database row: " << row.GetRow();
		return true;
	}

	bool HandleIgnoreMetadata(const Anope::string &target, const Anope::string &key, const Anope::string &value)
	{
		Log(LOG_DEBUG_3) << "Intentionally ignoring Atheme database metadata for " << target << ": " << key << " = " << value;
		return true;
	}

	bool HandleMC(AthemeRow &row)
	{
		// MC <channel> <regtime> <used> <flags> <mlock-on> <mlock-off> <mlock-limit> [<mlock-key>]
		auto channel = row.Get();
		auto regtime = row.GetNum<time_t>();
		auto used = row.GetNum<time_t>();
		auto flags = row.Get();
		auto mlock_on = row.GetNum<unsigned>();
		auto mlock_off = row.GetNum<unsigned>();
		auto mlock_limit = row.Get();

		if (!row)
			return row.LogError(this);

		auto mlock_key = row.Get(); // May not exist.

		auto *ci = new ChannelInfo(channel);
		ci->registered = regtime;
		ci->last_used = used;

		// No equivalent: elnv
		ApplyFlags(ci, flags, 'h', "CS_NO_EXPIRE");
		ApplyFlags(ci, flags, 'k', "KEEPTOPIC");
		ApplyFlags(ci, flags, 'o', "NOAUTOOP");
		ApplyFlags(ci, flags, 'p', "CS_PRIVATE");
		ApplyFlags(ci, flags, 'r', "RESTRICTED");
		ApplyFlags(ci, flags, 't', "TOPICLOCK");
		ApplyFlags(ci, flags, 'z', "SECUREOPS");

		auto pos = flags.find('a');
		if (pos != Anope::string::npos)
		{
			ci->SetLevel("ACCESS_CHANGE", 0);
			flags.erase(pos, 1);
		}

		pos = flags.find('f');
		if (pos != Anope::string::npos)
		{
			auto *kd = ci->Require<BotServ::KickerData>(BOTSERV_KICKER_DATA_EXT);
			if (kd)
			{
				kd->flood = true;
				kd->floodlines = 10;
				kd->floodsecs = 60;
				kd->ttb[BotServ::TTB_FLOOD] = 0;
				flags.erase(pos, 1);
			}
			else
			{
				Log(this) << "Unable to convert the 'f' flag for " << ci->name << " as bs_kick is not loaded";
			}
		}

		pos = flags.find('g');
		if (pos != Anope::string::npos)
		{
			auto *bi = Config->GetClient("ChanServ");
			if (bi)
			{
				bi->Assign(nullptr, ci);
				flags.erase(pos, 1);
			}
			else
				Log(this) << "Unable to convert the 'g' flag for " << ci->name << " as chanserv is not loaded";
		}

		if (flags != "+")
			Log(this) << "Unable to convert channel flags " << flags << " for " << ci->name;

		ApplyLocks(ci, mlock_on, mlock_limit, mlock_key, true);
		ApplyLocks(ci, mlock_off, mlock_limit, mlock_key, false);
		return true;
	}

	bool HandleMCFP(AthemeRow &row)
	{
		// MCFP <display> <fingerprint>
		auto display = row.Get();
		auto fingerprint = row.Get();

		if (!row)
			return row.LogError(this);

		auto *nc = NickCore::Find(display);
		if (!nc)
		{
			Log(this) << "Missing NickCore for MCFP: " << display;
			return false;
		}

		auto *cl = nc->Require<NickServ::CertList>(NICKSERV_CERT_EXT);
		if (!cl)
		{
			Log(this) << "Unable to convert certificate for " << nc->display << " as ns_cert is not loaded";
			return true;
		}

		cl->AddCert(fingerprint);
		return true;
	}

	bool HandleMDA(AthemeRow &row)
	{
		// MDA <channel> <account/mask> <key> <value>
		auto channel = row.Get();
		auto mask = row.Get();
		auto key = row.Get();
		auto value = row.GetRemaining();

		if (!row)
			return row.LogError(this);

		auto *ci = ChannelInfo::Find(channel);
		if (!ci)
		{
			Log(this) << "Missing ChannelInfo for MDA: " << channel;
			return false;
		}

		if (key == "reason")
		{
			auto *data = chandata.Require(ci);
			auto akick = data->akicks.find(mask);
			if (akick != data->akicks.end())
				akick->second->reason = value;
		}
		else
			Log(this) << "Unknown channel access metadata for " << mask << " on " << ci->name << ": " << key << " = " << value;

		return true;
	}

	bool HandleMDC(AthemeRow &row)
	{
		// MDC <channel> <key> <value>
		auto channel = row.Get();
		auto key = row.Get();
		auto value = row.GetRemaining();

		if (!row)
			return row.LogError(this);

		auto *ci = ChannelInfo::Find(channel);
		if (!ci)
		{
			Log(this) << "Missing ChannelInfo for MDC: " << channel;
			return false;
		}

		auto *data = chandata.Require(ci);
		if (key == "private:botserv:bot-assigned")
			data->bot = value;
		else if (key == "private:botserv:bot-handle-fantasy")
			ci->Extend<bool>("BS_FANTASY");
		else if (key == "private:botserv:no-bot")
			ci->Extend<bool>("BS_NOBOT");
		else if (key == "private:channelts")
			return HandleIgnoreMetadata(ci->name, key, value);
		else if (key == "private:close:closer")
			data->suspend_by = value;
		else if (key == "private:close:reason")
			data->suspend_reason = value;
		else if (key == "private:close:timestamp")
			data->suspend_ts = Anope::Convert<time_t>(value, 0);
		else if (key == "private:entrymsg")
		{
			auto *eml = ci->Require<ChanServ::EntryMessageList>(CHANSERV_ENTRY_MESSAGE_EXT);
			if (!eml)
			{
				Log(this) << "Unable to convert entry message for " << ci->name << " as cs_mode is not loaded";
				return true;
			}

			auto *msg = eml->Create();
			msg->chan = ci->name;
			msg->creator = "Unknown";
			msg->message = value;
			msg->when = Anope::CurTime;
			(*eml)->push_back(msg);
		}
		else if (key == "private:klinechan:closer")
			data->suspend_by = value;
		else if (key == "private:klinechan:reason")
			data->suspend_reason = value;
		else if (key == "private:klinechan:timestamp")
			data->suspend_ts = Anope::Convert<time_t>(value, 0);
		else if (key == "private:mark:reason")
			data->info_message = value;
		else if (key == "private:mark:setter")
			data->info_adder = value;
		else if (key == "private:mark:timestamp")
			data->info_ts = Anope::Convert<time_t>(value, 0);
		else if (key == "private:mlockext")
		{
			spacesepstream mlocks(value);
			for (Anope::string mlock; mlocks.GetToken(mlock); )
				data->mlocks.emplace_back(mlock[0], mlock.substr(1));
		}
		else if (key == "private:templates")
			return HandleIgnoreMetadata(ci->name, key, value);
		else if (key == "private:topic:setter")
			ci->last_topic_setter = value;
		else if (key == "private:topic:text")
			ci->last_topic = value;
		else if (key == "private:topic:ts")
			ci->last_topic_time = Anope::Convert<time_t>(value, 0);
		else if (key.compare(0, 14, "private:stats:", 14) == 0)
			return HandleIgnoreMetadata(ci->name, key, value);
		else if (key.find(':') == Anope::string::npos)
		{
			ExtensibleRef<MiscData> extref("cs_set_misc:" + key.upper());
			if (!extref)
			{
				Log(this) << "Unknown public channel metadata for " << ci->name << ": " << key << " = " << value;
				return true;
			}

			auto *data = extref->Set(ci);
			data->object = ci->name;
			data->name = key;
			data->data = value;
		}
		else
		{
			auto it = csmiscdata.find(key);
			if (it == csmiscdata.end())
			{
				Log(this) << "Unknown channel metadata for " << ci->name << ": " << key << " = " << value;
				return true;
			}

			ExtensibleRef<MiscData> extref("cs_set_misc:" + it->second);
			if (!extref)
			{
				Log(this) << "Unknown imported channel metadata for " << ci->name << ": " << it->second << " = " << value;
				return true;
			}

			auto *data = extref->Set(ci);
			data->object = ci->name;
			data->name = it->second;
			data->data = value;
		}

		return true;
	}

	bool HandleMDN(AthemeRow &row)
	{
		// MDN <nick> <key> <value>
		auto nick = row.Get();
		auto key = row.Get();
		auto value = row.GetRemaining();

		if (!row)
			return row.LogError(this);

		if (!OperServ::forbid_service)
		{
			Log(this) << "Unable to convert forbidden nick " << nick << " metadata as os_forbid is not loaded";
			return true;
		}

		auto *forbid = OperServ::forbid_service->FindForbidExact(nick, OperServ::FT_NICK);
		if (!forbid)
		{
			Log(this) << "Missing forbid for MDN: " << nick;
			return false;
		}

		if (key == "private:mark:reason")
			forbid->reason = value;
		else if (key == "private:mark:setter")
			forbid->creator = value;
		else if (key == "private:mark:timestamp")
			forbid->created = Anope::Convert<time_t>(value, 0);
		else
			Log(this) << "Unknown forbidden nick metadata for " << forbid->mask << ": " << key << " = " << value;

		return true;
	}

	bool HandleMDU(AthemeRow &row)
	{
		// MDU <display> <key> <value>
		auto display = row.Get();
		auto key = row.Get();
		auto value = row.GetRemaining();

		if (!row)
			return row.LogError(this);

		auto *nc = NickCore::Find(display);
		if (!nc)
		{
			Log(this) << "Missing NickCore for MDU: " << display;
			return false;
		}

		auto *data = userdata.Require(nc);
		if (key == "private:autojoin")
			return true; // TODO
		else if (key == "private:doenforce")
			data->protect = true;
		else if (key == "private:enforcetime")
			data->protectafter = Anope::TryConvert<time_t>(value);
		else if (key == "private:freeze:freezer")
			data->suspend_by = value;
		else if (key == "private:freeze:reason")
			data->suspend_reason = value;
		else if (key == "private:freeze:timestamp")
			data->suspend_ts = Anope::Convert<time_t>(value, 0);
		else if (key == "private:host:actual")
			data->last_userhost_real = value;
		else if (key == "private:host:vhost")
			data->last_userhost = value;
		else if (key == "private:lastquit:message")
			data->last_quit = value;
		else if (key == "private:loginfail:failnum")
			return HandleIgnoreMetadata(nc->display, key, value);
		else if (key == "private:loginfail:lastfailaddr")
			return HandleIgnoreMetadata(nc->display, key, value);
		else if (key == "private:loginfail:lastfailtime")
			return HandleIgnoreMetadata(nc->display, key, value);
		else if (key == "private:mark:reason")
			data->info_message = value;
		else if (key == "private:mark:setter")
			data->info_adder = value;
		else if (key == "private:mark:timestamp")
			data->info_ts = Anope::Convert<time_t>(value, 0);
		else if (key == "private:swhois")
			return HandleIgnoreMetadata(nc->display, key, value);
		else if (key == "private:usercloak")
			data->vhost = value;
		else if (key == "private:usercloak-assigner")
			data->vhost_creator = value;
		else if (key == "private:usercloak-timestamp")
			data->vhost_ts = Anope::Convert<time_t>(value, 0);
		else if (key.compare(0, 18, "private:usercloak:", 18) == 0)
			data->vhost_nick[key.substr(18)] = value;
		else if (key.find(':') == Anope::string::npos)
		{
			ExtensibleRef<MiscData> extref("ns_set_misc:" + key.upper());
			if (!extref)
			{
				Log(this) << "Unknown public account metadata for " << nc->display << ": " << key << " = " << value;
				return true;
			}

			auto *data = extref->Set(nc);
			data->object = nc->display;
			data->name = key;
			data->data = value;
		}
		else
		{
			auto it = nsmiscdata.find(key);
			if (it == nsmiscdata.end())
			{
				Log(this) << "Unknown public account metadata for " << nc->display << ": " << key << " = " << value;
				return true;
			}

			ExtensibleRef<MiscData> extref("ns_set_misc:" + it->second);
			if (!extref)
			{
				Log(this) << "Unknown imported account metadata for " << nc->display << ": " << key << " = " << value;
				return true;
			}

			auto *data = extref->Set(nc);
			data->object = nc->display;
			data->name = it->second;
			data->data = value;
		}

		return true;
	}

	bool HandleME(AthemeRow &row)
	{
		// ME <target> <source> <sent> <flags> <text>
		auto target = row.Get();
		auto source = row.Get();
		auto sent = row.GetNum<time_t>();
		auto flags = row.GetNum<unsigned>();
		auto text = row.GetRemaining();

		if (!row)
			return row.LogError(this);

		auto *nc = NickCore::Find(target);
		if (!nc)
		{
			Log(this) << "Missing NickCore for ME: " << source;
			return false;
		}

		auto *m = new Memo();
		m->mi = &nc->memos;
		m->owner = nc->display;
		m->sender = source;
		m->time = sent;
		m->text = text;
		m->unread = flags & 0x1;
		nc->memos.memos->push_back(m);
		return true;
	}

	bool HandleMI(AthemeRow &row)
	{
		// MI <display> <ignored>
		auto display = row.Get();
		auto ignored = row.Get();

		if (!row)
			return row.LogError(this);

		auto *nc = NickCore::Find(display);
		if (!nc)
		{
			Log(this) << "Missing NickCore for MI: " << display;
			return false;
		}

		nc->memos.ignores.insert(ignored);
		return true;
	}

	bool HandleMM(AthemeRow &row)
	{
		// MM <id> <setterid> <setteraccount> <markedid> <markedaccount> <setts> <num> <message>
		/* auto id = */ row.Get();
		/* auto setterid = */ row.Get();
		auto setteraccount = row.Get();
		/* auto markedid = */ row.Get();
		auto markedaccount = row.Get();
		auto setts = row.GetNum<time_t>();
		/* auto num = */ row.Get();
		auto message = row.GetRemaining();

		if (!row)
			return row.LogError(this);

		auto *nc = NickCore::Find(markedaccount);
		if (!nc)
		{
			Log(this) << "Missing NickCore for MM: " << markedaccount;
			return false;
		}

		auto *oil = nc->Require<OperInfoList>("operinfo");
		if (oil)
		{
			auto *info = oil->Create();
			info->target = nc->display;
			info->info = message;
			info->adder = setteraccount;
			info->created = setts;
			(*oil)->push_back(info);
		}
		else
		{
			Log(this) << "Unable to convert oper info for " << nc->display << " as os_info is not loaded";
		}
		return true;
	}

	bool HandleMN(AthemeRow &row)
	{
		// MN <display> <nick> <regtime> <lastseen>
		auto display = row.Get();
		auto nick = row.Get();
		auto regtime = row.GetNum<time_t>();
		auto lastseen = row.GetNum<time_t>();

		if (!row)
			return row.LogError(this);

		auto *nc = NickCore::Find(display);
		if (!nc)
		{
			Log(this) << "Missing NickCore for MN: " << display;
			return false;
		}

		auto *na = NickAlias::Find(nick);
		if (na)
		{
			Log(this) << "Refusing to import duplicate nick: " << nick;
			return true;
		}

		na = new NickAlias(nick, nc);
		na->registered = regtime;
		na->last_seen = lastseen ? lastseen : regtime;

		auto *data = userdata.Get(nc);
		if (data)
		{
			if (!data->last_userhost.empty())
				na->last_userhost = data->last_userhost;

			if (!data->last_quit.empty())
				na->last_quit = data->last_quit;

			if (!data->last_userhost_real.empty())
				na->last_userhost_real = data->last_userhost_real;

			if (data->noexpire)
				na->Extend<bool>("NS_NO_EXPIRE");

			auto vhost = data->vhost;
			auto nick_vhost = data->vhost_nick.find(nick);
			if (nick_vhost != data->vhost_nick.end())
				vhost = nick_vhost->second;
			if (!vhost.empty())
				na->SetVHost("", vhost, data->vhost_creator, data->vhost_ts);
		}

		return true;
	}

	bool HandleMU(AthemeRow &row)
	{
		// MU <id> <display> <pass> <email> <regtime> <lastlogin> <flags> <language>
		/* auto id = */ row.Get();
		auto display = row.Get();
		auto pass = row.Get();
		auto email = row.Get();
		auto regtime = row.GetNum<time_t>();
		/* auto lastlogin = */ row.Get();
		auto flags = row.Get();
		auto language = row.Get();

		if (!row)
			return row.LogError(this);

		auto *nc = NickCore::Find(display);
		if (nc)
		{
			Log(this) << "Refusing to import duplicate account: " << display;
			return true;
		}

		nc = new NickCore(display);
		nc->email = email;
		nc->registered = regtime;
		ApplyPassword(nc, flags, pass);

		// No equivalent: bglmNQrS
		ApplyFlags(nc, flags, 'E', "PROTECT");
		ApplyFlags(nc, flags, 'e', "MEMO_MAIL");
		ApplyFlags(nc, flags, 'n', "NEVEROP");
		ApplyFlags(nc, flags, 'o', "AUTOOP", false);
		ApplyFlags(nc, flags, 'P', "MSG");
		ApplyFlags(nc, flags, 'p', "NS_PRIVATE");
		ApplyFlags(nc, flags, 's', "HIDE_EMAIL");
		ApplyFlags(nc, flags, 'W', "UNCONFIRMED");

		// If an Atheme account was awaiting confirmation but Anope is not
		// configured to use confirmation then autoconfirm it.
		const auto &nsregister = Config->GetModule("ns_register").Get<const Anope::string>("registration");
		if (nsregister.equals_ci("none"))
			nc->Shrink<bool>("UNCONFIRMED");

		auto pos = flags.find('h');
		if (pos != Anope::string::npos)
		{
			userdata.Require(nc)->noexpire = true;
			flags.erase(pos, 1);
		}


		if (flags != "+")
			Log(this) << "Unable to convert account flags " << flags << " for " << nc->display;

		// No translations yet: bg, cy, da.
		if (language == "de")
			nc->language = "de_DE.UTF-8";
		else if (language == "en")
			nc->language = "en_US.UTF-8";
		else if (language == "es")
			nc->language = "es_ES.UTF-8";
		else if (language == "fr")
			nc->language = "fr_FR.UTF-8";
		else if (language == "ru")
			nc->language = "ru_RU.UTF-8";
		else if (language == "tr")
			nc->language = "tr_TR.UTF-8";
		else if (language != "default")
		{
			Log(this) << "Unable to convert language " << language << " for " << nc->display;
		}

		return true;
	}

	bool HandleNAM(AthemeRow &row)
	{
		// NAM <nick>
		auto nick = row.Get();

		if (!row)
			return row.LogError(this);

		if (!OperServ::forbid_service)
		{
			Log(this) << "Unable to convert forbidden nick " << nick << " as os_forbid is not loaded";
			return true;
		}

		auto *forbid = OperServ::forbid_service->CreateForbid();
		forbid->creator = "Unknown";
		forbid->mask = nick;
		forbid->reason = "Unknown";
		forbid->type = OperServ::FT_NICK;
		OperServ::forbid_service->AddForbid(forbid);
		return true;
	}

	bool HandleQL(AthemeRow &row)
	{
		// QL <nick> <host> <duration> <settime> <setby> <reason>
		auto id = row.Get();
		auto nick = row.Get();
		auto duration = row.GetNum<unsigned>();
		auto settime = row.GetNum<time_t>();
		auto setby = row.Get();
		auto reason = row.GetRemaining();

		if (!row)
			return row.LogError(this);

		if (!sglinemgr)
		{
			Log(this) << "Unable to import Q-line on " << nick << " as operserv is not loaded";
			return true;
		}

		auto *xl = new XLine(nick, setby, settime + duration, reason);
		xl->id = id;
		sqlinemgr->AddXLine(xl);
		return true;
	}

	bool HandleSO(AthemeRow &row)
	{
		// SO <display> <type> <flags>
		auto display = row.Get();
		auto type = row.Get();
		auto flags = row.Get();

		if (!row)
			return row.LogError(this);

		auto *nc = NickCore::Find(display);
		if (!nc)
		{
			Log(this) << "Missing NickCore for SO: " << display;
			return false;
		}

		auto *ot = OperType::Find(type);
		if (!ot)
		{
			// Attempt to convert oper types.
			if (type == "sra")
				ot = OperType::Find("Services Root");
			else if (type == "ircop")
				ot = OperType::Find("Services Operator");
		}

		if (!ot)
		{
			Log(this) << "Unable to convert operator status for " << nc->display << " as there is no equivalent oper type: " << type;
			return true;
		}

		nc->o = new OperServ::Oper(nc->display, ot);
		return true;
	}

	bool HandleXL(AthemeRow &row)
	{
		// XL <id> <real> <duration> <settime> <setby> <reason>
		auto id = row.Get();
		auto real = row.Get();
		auto duration = row.GetNum<unsigned>();
		auto settime = row.GetNum<time_t>();
		auto setby = row.Get();
		auto reason = row.GetRemaining();

		if (!row)
			return row.LogError(this);

		if (!sglinemgr)
		{
			Log(this) << "Unable to import X-line on " << real << " as operserv is not loaded";
			return true;
		}

		auto *xl = new XLine(real, setby, settime + duration, reason);
		xl->id = id;
		snlinemgr->AddXLine(xl);
		return true;
	}

public:
	DBAtheme(const Anope::string &modname, const Anope::string &creator)
		: Module(modname, creator, DATABASE | VENDOR)
		, accessprov("AccessProvider", "access/flags")
		, chandata(this, "ATHEME_CHANDATA")
		, userdata(this, "ATHEME_USERDATA")
		, sglinemgr("XLineManager","xlinemanager/sgline")
		, snlinemgr("XLineManager","xlinemanager/snline")
		, sqlinemgr("XLineManager","xlinemanager/sqline")
	{
	}

	void OnReload(Configuration::Conf &conf) override
	{
		const auto &modconf = conf.GetModule(this);

		csmiscdata.clear();
		for (auto idx = 0; idx < modconf.CountBlock("cs_set_misc"); ++idx)
		{
			const auto &data = modconf.GetBlock("cs_set_misc", idx);
			const auto &anope = data.Get<const Anope::string>("anope");
			const auto &atheme = data.Get<const Anope::string>("atheme");
			if (!anope.empty() && !atheme.empty())
				csmiscdata[atheme] = anope.upper();
		}

		nsmiscdata.clear();
		for (auto idx = 0; idx < modconf.CountBlock("ns_set_misc"); ++idx)
		{
			const auto &data = modconf.GetBlock("ns_set_misc", idx);
			const auto &anope = data.Get<const Anope::string>("anope");
			const auto &atheme = data.Get<const Anope::string>("atheme");
			if (!anope.empty() && !atheme.empty())
				nsmiscdata[atheme] = anope.upper();
		}

		flags.clear();
		for (int i = 0; i < Config->CountBlock("privilege"); ++i)
		{
			const auto &priv = Config->GetBlock("privilege", i);
			const Anope::string &name = priv.Get<const Anope::string>("name");
			const Anope::string &value = priv.Get<const Anope::string>("flag");
			if (!name.empty() && !value.empty())
				flags[name] = value[0];
		}
	}

	EventReturn OnLoadDatabase() override
	{
		const auto dbname = Anope::ExpandData(Config->GetModule(this).Get<const Anope::string>("database", "atheme.db"));
		std::ifstream fd(dbname.str());
		if (!fd.is_open())
		{
			Log(this) << "Unable to open " << dbname << " for reading!";
			return EVENT_STOP;
		}

		for (Anope::string buf; std::getline(fd, buf.str()); )
		{
			AthemeRow row(buf);

			auto rowtype = row.Get();
			if (!row)
				continue; // Empty row.

			auto rowhandler = rowhandlers.find(rowtype);
			if (rowhandler == rowhandlers.end())
			{
				Log(this) << "Unknown row type: " << row.GetRow();
				continue;
			}

			if (!rowhandler->second(this, row))
				break;
		}

		for (const auto &[_, ci] : *RegisteredChannelList)
		{
			auto *data = chandata.Get(ci);
			if (!data)
				continue;

			if (!data->bot.empty())
			{
				auto *bi = BotInfo::Find(data->bot);
				if (bi)
					bi->Assign(nullptr, ci);
			}

			if (!data->info_message.empty())
			{
				auto *oil = ci->Require<OperInfoList>("operinfo");
				if (oil)
				{
					auto *info = oil->Create();
					info->target = ci->name;
					info->info = data->info_message;
					info->adder = data->info_adder.empty() ? "Unknown" : data->info_adder;
					info->created = data->info_ts;
					(*oil)->push_back(info);
				}
				else
				{
					Log(this) << "Unable to convert oper info for " << ci->name << " as os_info is not loaded";
				}
			}

			if (!data->suspend_reason.empty())
			{
				SuspendInfo si;
				si.by = data->suspend_by.empty() ? "Unknown" : data->suspend_by;
				si.expires = 0;
				si.reason = data->suspend_reason;
				si.what = ci->name;
				si.when = data->suspend_ts;
				ci->Extend("CS_SUSPENDED", si);
			}
		}

		for (const auto &[_, nc] : *NickCoreList)
		{
			auto *data = userdata.Get(nc);
			if (!data)
				continue;

			if (!data->info_message.empty())
			{
				auto *oil = nc->Require<OperInfoList>("operinfo");
				if (oil)
				{
					auto *info = oil->Create();
					info->target = nc->display;
					info->info = data->info_message;
					info->adder = data->info_adder.empty() ? "Unknown" : data->info_adder;
					info->created = data->info_ts;
					(*oil)->push_back(info);
				}
				else
				{
					Log(this) << "Unable to convert oper info for " << nc->display << " as os_info is not loaded";
				}
			}

			if (data->protect)
			{
				nc->Extend<bool>("PROTECT");
				if (data->protectafter)
					nc->Extend("PROTECT_AFTER", data->protectafter.value());
			}

			if (!data->suspend_reason.empty())
			{
				SuspendInfo si;
				si.by = data->suspend_by.empty() ? "Unknown" : data->suspend_by;
				si.expires = 0;
				si.reason = data->suspend_reason;
				si.what = nc->display;
				si.when = data->suspend_ts;
				nc->Extend("NS_SUSPENDED", si);
			}
		}

		return EVENT_STOP;
	}

	void OnUplinkSync(Server *s) override
	{
		for (auto &[_, ci] : *RegisteredChannelList)
		{
			auto *data = chandata.Get(ci);
			if (!data)
				continue;

			auto *ml = ci->Require<ChanServ::ModeLocks>(CHANSERV_MODE_LOCK_EXT);
			if (!ml)
			{
				Log(this) << "Unable to convert mode locks for " << ci->name << " as cs_mode is not loaded";
				continue;
			}

			for (const auto &mlock : data->mlocks)
			{
				ChannelMode *mh;
				if (mlock.name.empty())
					mh = ModeManager::FindChannelModeByChar(mlock.letter);
				else
					mh = ModeManager::FindChannelModeByName(mlock.name);
				if (!mh)
				{
					Log(this) << "Unable to find mode while importing mode lock on " << ci->name << ": " << mlock.str();
					continue;
				}

				ml->SetMLock(mh, mlock.set, mlock.value, "Unknown");
			}
		}
		Anope::SaveDatabases();
	}
};

MODULE_INIT(DBAtheme)
