/*	EQEMu: Everquest Server Emulator
	Copyright (C) 2001-2016 EQEMu Development Team (http://eqemulator.org)

	This program is free software; you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation; version 2 of the License.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY except by those people which sell it, which
	are required to give you total support for your newly bought product;
	without even the implied warranty of MERCHANTABILITY or FITNESS FOR
	A PARTICULAR PURPOSE. See the GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program; if not, write to the Free Software
	Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/

#include "../common/data_verification.h"
#include "../common/global_define.h"
#include "../common/rulesys.h"
#include "../common/strings.h"
#include "../common/eqemu_logsys.h"

#include "../common/repositories/bot_blocked_buffs_repository.h"
#include "../common/repositories/bot_buffs_repository.h"
#include "../common/repositories/bot_create_combinations_repository.h"
#include "../common/repositories/bot_data_repository.h"
#include "../common/repositories/bot_heal_rotations_repository.h"
#include "../common/repositories/bot_heal_rotation_members_repository.h"
#include "../common/repositories/bot_heal_rotation_targets_repository.h"
#include "../common/repositories/bot_inspect_messages_repository.h"
#include "../common/repositories/bot_inventories_repository.h"
#include "../common/repositories/bot_owner_options_repository.h"
#include "../common/repositories/bot_pets_repository.h"
#include "../common/repositories/bot_pet_buffs_repository.h"
#include "../common/repositories/bot_pet_inventories_repository.h"
#include "../common/repositories/bot_spell_casting_chances_repository.h"
#include "../common/repositories/bot_spells_entries_repository.h"
#include "../common/repositories/bot_settings_repository.h"
#include "../common/repositories/bot_stances_repository.h"
#include "../common/repositories/bot_timers_repository.h"
#include "../common/repositories/character_data_repository.h"
#include "../common/repositories/group_id_repository.h"

#include "zonedb.h"
#include "bot.h"
#include "client.h"

#include <fmt/format.h>


bool BotDatabase::LoadBotCommandSettings(std::map<std::string, std::pair<uint8, std::vector<std::string>>> &bot_command_settings)
{
	bot_command_settings.clear();

	query = "SELECT `bot_command`, `access`, `aliases` FROM `bot_command_settings`";
	auto results = database.QueryDatabase(query);
	if (!results.Success())
		return false;

	for (auto row = results.begin(); row != results.end(); ++row) {
		bot_command_settings[row[0]].first = Strings::ToInt(row[1]);
		if (row[2][0] == 0)
			continue;

		auto aliases = Strings::Split(row[2], '|');
		for (auto iter : aliases) {
			if (!iter.empty())
				bot_command_settings[row[0]].second.push_back(iter);
		}
	}

	return true;
}

template<typename T1, typename T2>
inline std::vector<std::string> join_pair(
	const std::string &glue,
	const std::pair<char, char> &encapsulation,
	const std::vector<std::pair<T1, T2>> &src
)
{
	if (src.empty()) {
		return {};
	}

	std::vector<std::string> output;

	for (const std::pair<T1, T2> &src_iter: src) {
		output.emplace_back(

			fmt::format(
				"{}{}{}{}{}{}{}",
				encapsulation.first,
				src_iter.first,
				encapsulation.second,
				glue,
				encapsulation.first,
				src_iter.second,
				encapsulation.second
			)
		);
	}

	return output;
}

template<typename T>
inline std::string
ImplodePair(const std::string &glue, const std::pair<char, char> &encapsulation, const std::vector<T> &src)
{
	if (src.empty()) {
		return {};
	}
	std::ostringstream oss;
	for (const T &src_iter: src) {
		oss << encapsulation.first << src_iter << encapsulation.second << glue;
	}
	std::string output(oss.str());
	output.resize(output.size() - glue.size());
	return output;
}

bool BotDatabase::UpdateInjectedBotCommandSettings(const std::vector<std::pair<std::string, uint8>> &injected)
{
	if (injected.size()) {

		query = fmt::format(
			"REPLACE INTO `bot_command_settings`(`bot_command`, `access`) VALUES {}",
			ImplodePair(
				",",
				std::pair<char, char>('(', ')'),
				join_pair(",", std::pair<char, char>('\'', '\''), injected)
			)
		);

		if (!database.QueryDatabase(query).Success()) {
			return false;
		}

		LogInfo(
			"[{}] New Bot Command{} Added",
			injected.size(),
			(injected.size() == 1 ? "" : "s")
		);
	}

	return true;
}

bool BotDatabase::UpdateOrphanedBotCommandSettings(const std::vector<std::string> &orphaned)
{
	if (orphaned.size()) {

		query = fmt::format(
			"DELETE FROM `bot_command_settings` WHERE `bot_command` IN ({})",
			ImplodePair(",", std::pair<char, char>('\'', '\''), orphaned)
		);

		if (!database.QueryDatabase(query).Success()) {
			return false;
		}

		LogInfo(
			"[{}] Orphaned Bot Command{} Deleted",
			orphaned.size(),
			(orphaned.size() == 1 ? "" : "s")
		);
	}

	return true;
}

bool BotDatabase::LoadBotSpellCastingChances()
{
	const auto& l = BotSpellCastingChancesRepository::All(database);

	if (l.empty()) {
		return false;
	}

	std::vector<uint8> conditions = { };

	for (const auto& e : l) {
		if (
			e.spell_type_index >= Bot::SPELL_TYPE_COUNT ||
			!IsPlayerClass(e.class_id) ||
			e.stance_index >= Stance::AEBurn
		) {
			continue;
		}

		const uint8 class_index = (e.class_id - 1);

		conditions = {
			e.nHSND_value,
			e.pH_value,
			e.pS_value,
			e.pHS_value,
			e.pN_value,
			e.pHN_value,
			e.pSN_value,
			e.pHSN_value,
			e.pD_value,
			e.pHD_value,
			e.pSD_value,
			e.pHSD_value,
			e.pND_value,
			e.pHND_value,
			e.pSND_value,
			e.pHSN_value
		};

		for (uint8 index = 0; index < conditions.size(); index++) {
			uint8 value = conditions[index];

			if (!value) {
				continue;
			}

			if (value > 100) {
				value = 100;
			}

			Bot::spell_casting_chances[e.spell_type_index][class_index][e.stance_index][index] = value;
		}
	}

	return true;
}

bool BotDatabase::QueryNameAvailability(const std::string& bot_name, bool& available_flag)
{
	if (
		bot_name.empty() ||
		bot_name.size() > 60 ||
		!database.CheckNameFilter(bot_name) ||
		database.IsNameUsed(bot_name)
	) {
		return false;
	}

	available_flag = true;

	return true;
}

bool BotDatabase::QueryBotCount(const uint32 owner_id, int class_id, uint32& bot_count, uint32& bot_class_count)
{
	if (!owner_id) {
		return false;
	}

	bot_count = BotDataRepository::Count(
		database,
		fmt::format(
			"`owner_id` = {} AND `name` NOT LIKE '%-deleted-%'",
			owner_id
		)
	);

	if (IsPlayerClass(class_id)) {
		bot_class_count = BotDataRepository::Count(
			database,
			fmt::format(
				"`owner_id` = {} AND `class` = {} AND `name` NOT LIKE '%-deleted-%'",
				owner_id,
				class_id
			)
		);
	}

	return true;
}

bool BotDatabase::LoadBotsList(const uint32 owner_id, std::list<BotsAvailableList>& bots_list, bool by_account)
{
	if (!owner_id) {
		return false;
	}

	BotsAvailableList ble;

	if (by_account) {
		const std::string& owner_name = database.GetCharNameByID(owner_id);

		const auto& l = BotDataRepository::GetWhere(
			database,
			fmt::format(
				SQL(
					`owner_id` IN
					(
						SELECT `id` FROM `character_data` WHERE `account_id` =
						(
							SELECT `account_id` FROM `character_data` WHERE `id` = {}
						)
					)
					AND
					`name` NOT LIKE '%-deleted-%'
				),
				owner_id
			)
		);

		if (l.empty()) {
			return true;
		}

		for (const auto& e : l) {
			ble.bot_id   = e.bot_id;
			ble.class_   = e.class_;
			ble.level    = e.level;
			ble.race     = e.race;
			ble.gender   = e.gender;
			ble.owner_id = e.owner_id;

			strn0cpy(ble.bot_name, e.name.c_str(), sizeof(ble.bot_name));
			strn0cpy(ble.owner_name, owner_name.c_str(), sizeof(ble.owner_name));

			bots_list.emplace_back(ble);
		}
	} else {
		const auto& l = BotDataRepository::GetWhere(
			database,
			fmt::format(
				"`owner_id` = {} AND `name` NOT LIKE '%-deleted-%'",
				owner_id
			)
		);

		if (l.empty()) {
			return true;
		}

		for (const auto& e : l) {
			ble.bot_id   = e.bot_id;
			ble.class_   = e.class_;
			ble.level    = e.level;
			ble.race     = e.race;
			ble.gender   = e.gender;
			ble.owner_id = e.owner_id;

			strn0cpy(ble.bot_name, e.name.c_str(), sizeof(ble.bot_name));
			strn0cpy(ble.owner_name, "You", sizeof(ble.owner_name));

			bots_list.emplace_back(ble);
		}
	}

	return true;
}

uint32 BotDatabase::GetOwnerID(const uint32 bot_id)
{
	if (!bot_id) {
		return 0;
	}

	const auto& l = BotDataRepository::FindOne(database, bot_id);

	return l.bot_id ? l.owner_id : 0;
}

bool BotDatabase::LoadBotID(const std::string& bot_name, uint32& bot_id, uint8& bot_class_id)
{
	if (bot_name.empty()) {
		return false;
	}

	const auto& l = BotDataRepository::GetWhere(
		database,
		fmt::format(
			"`name` = '{}' AND `name` NOT LIKE '%-deleted-%' LIMIT 1",
			Strings::Escape(bot_name)
		)
	);

	if (l.empty()) {
		return true;
	}

	auto e = l.front();

	bot_id       = e.bot_id;
	bot_class_id = e.class_;

	return true;
}

bool BotDatabase::LoadBot(const uint32 bot_id, Bot*& loaded_bot)
{
	if (!bot_id || loaded_bot) {
		return false;
	}

	const auto& e = BotDataRepository::FindOne(database, bot_id);
	if (!e.bot_id) {
		return false;
	}

	auto d = Bot::CreateDefaultNPCTypeStructForBot(
		e.name,
		e.last_name,
		e.level,
		e.race,
		e.class_,
		e.gender
	);

	auto t = Bot::FillNPCTypeStruct(
		e.spells_id,
		e.name,
		e.last_name,
		e.level,
		e.race,
		e.class_,
		e.gender,
		e.size,
		e.face,
		e.hair_style,
		e.hair_color,
		e.eye_color_1,
		e.eye_color_2,
		e.beard,
		e.beard_color,
		e.drakkin_heritage,
		e.drakkin_tattoo,
		e.drakkin_details,
		e.hp,
		e.mana,
		d->MR,
		d->CR,
		d->DR,
		d->FR,
		d->PR,
		d->Corrup,
		d->AC,
		d->STR,
		d->STA,
		d->DEX,
		d->AGI,
		d->INT,
		d->WIS,
		d->CHA,
		d->ATK
	);

	safe_delete(d);

	loaded_bot = new Bot(
		bot_id,
		e.owner_id,
		e.spells_id,
		e.time_spawned,
		e.zone_id,
		t
	);

	if (loaded_bot) {
		loaded_bot->SetSurname(e.last_name);
		loaded_bot->SetTitle(e.title);
		loaded_bot->SetSuffix(e.suffix);
	}

	return true;
}

bool BotDatabase::SaveNewBot(Bot* b, uint32& bot_id)
{
	if (!b) {
		return false;
	}

	auto e = BotDataRepository::NewEntity();

	e.owner_id               = b->GetBotOwnerCharacterID();
	e.spells_id              = b->GetBotSpellID();
	e.name                   = b->GetCleanName();
	e.last_name              = b->GetLastName();
	e.title                  = b->GetTitle();
	e.suffix                 = b->GetSuffix();
	e.zone_id                = b->GetLastZoneID();
	e.gender                 = b->GetGender();
	e.race                   = b->GetBaseRace();
	e.class_                 = b->GetClass();
	e.level                  = b->GetLevel();
	e.creation_day           = std::time(nullptr);
	e.last_spawn             = std::time(nullptr);
	e.size                   = b->GetSize();
	e.face                   = b->GetLuclinFace();
	e.hair_color             = b->GetHairColor();
	e.hair_style             = b->GetHairStyle();
	e.beard                  = b->GetBeard();
	e.beard_color            = b->GetBeardColor();
	e.eye_color_1            = b->GetEyeColor1();
	e.eye_color_2            = b->GetEyeColor2();
	e.drakkin_heritage       = b->GetDrakkinHeritage();
	e.drakkin_tattoo         = b->GetDrakkinTattoo();
	e.drakkin_details        = b->GetDrakkinDetails();
	e.ac                     = b->GetBaseAC();
	e.atk                    = b->GetBaseATK();
	e.hp                     = b->GetHP();
	e.mana                   = b->GetMana();
	e.str                    = b->GetBaseSTR();
	e.sta                    = b->GetBaseSTA();
	e.cha                    = b->GetBaseCHA();
	e.dex                    = b->GetBaseDEX();
	e.int_                   = b->GetBaseINT();
	e.agi                    = b->GetBaseAGI();
	e.wis                    = b->GetBaseWIS();
	e.fire                   = b->GetBaseFR();
	e.cold                   = b->GetBaseCR();
	e.magic                  = b->GetBaseMR();
	e.poison                 = b->GetBasePR();
	e.disease                = b->GetBaseDR();
	e.corruption             = b->GetBaseCorrup();

	e = BotDataRepository::InsertOne(database, e);

	if (!e.bot_id) {
		return false;
	}

	bot_id = e.bot_id;

	return true;
}

bool BotDatabase::SaveBot(Bot* b)
{
	if (!b) {
		return false;
	}

	auto e = BotDataRepository::FindOne(database, b->GetBotID());
	if (!e.bot_id) {
		return false;
	}

	e.owner_id               = b->GetBotOwnerCharacterID();
	e.spells_id              = b->GetBotSpellID();
	e.name                   = b->GetCleanName();
	e.last_name              = b->GetLastName();
	e.title                  = b->GetTitle();
	e.suffix                 = b->GetSuffix();
	e.zone_id                = b->GetLastZoneID();
	e.gender                 = b->GetBaseGender();
	e.race                   = b->GetBaseRace();
	e.class_                 = b->GetClass();
	e.level                  = b->GetLevel();
	e.last_spawn             = std::time(nullptr);
	e.time_spawned           = b->GetTotalPlayTime();
	e.size                   = b->GetSize();
	e.face                   = b->GetLuclinFace();
	e.hair_color             = b->GetHairColor();
	e.hair_style             = b->GetHairStyle();
	e.beard                  = b->GetBeard();
	e.beard_color            = b->GetBeardColor();
	e.eye_color_1            = b->GetEyeColor1();
	e.eye_color_2            = b->GetEyeColor2();
	e.drakkin_heritage       = b->GetDrakkinHeritage();
	e.drakkin_tattoo         = b->GetDrakkinTattoo();
	e.drakkin_details        = b->GetDrakkinDetails();
	e.ac                     = b->GetBaseAC();
	e.atk                    = b->GetBaseATK();
	e.hp                     = b->GetHP();
	e.mana                   = b->GetMana();
	e.str                    = b->GetBaseSTR();
	e.sta                    = b->GetBaseSTA();
	e.cha                    = b->GetBaseCHA();
	e.dex                    = b->GetBaseDEX();
	e.int_                   = b->GetBaseINT();
	e.agi                    = b->GetBaseAGI();
	e.wis                    = b->GetBaseWIS();
	e.fire                   = b->GetBaseFR();
	e.cold                   = b->GetBaseCR();
	e.magic                  = b->GetBaseMR();
	e.poison                 = b->GetBasePR();
	e.disease                = b->GetBaseDR();
	e.corruption             = b->GetBaseCorrup();

	return BotDataRepository::UpdateOne(database, e);
}

bool BotDatabase::DeleteBot(const uint32 bot_id)
{
	if (!bot_id) {
		return false;
	}

	BotDataRepository::DeleteOne(database, bot_id);

	return true;
}

bool BotDatabase::LoadBuffs(Bot* b)
{
	if (!b) {
		return false;
	}

	const auto& l = BotBuffsRepository::GetWhere(
		database,
		fmt::format(
			"`bot_id` = {}",
			b->GetBotID()
		)
	);

	if (l.empty()) {
		return true;
	}

	auto buffs = b->GetBuffs();

	if (!buffs) {
		return false;
	}

	uint32 max_slots = b->GetMaxBuffSlots();
	for (int index = 0; index < max_slots; index++) {
		buffs[index].spellid = SPELL_UNKNOWN;
	}

	uint32 buff_count = 0;

	for (const auto& e : l) {
		if (buff_count >= BUFF_COUNT) {
			continue;
		}

		buffs[buff_count].spellid       = e.spell_id;
		buffs[buff_count].casterlevel   = e.caster_level;
		buffs[buff_count].ticsremaining = e.tics_remaining;
		buffs[buff_count].counters      = 0;

		if (CalculatePoisonCounters(buffs[buff_count].spellid) > 0) {
			buffs[buff_count].counters = e.poison_counters;
		} else if (CalculateDiseaseCounters(buffs[buff_count].spellid) > 0) {
			buffs[buff_count].counters = e.disease_counters;
		} else if (CalculateCurseCounters(buffs[buff_count].spellid) > 0) {
			buffs[buff_count].counters = e.curse_counters;
		} else if (CalculateCorruptionCounters(buffs[buff_count].spellid) > 0) {
			buffs[buff_count].counters = e.corruption_counters;
		}

		buffs[buff_count].hit_number      = e.numhits;
		buffs[buff_count].melee_rune      = e.melee_rune;
		buffs[buff_count].magic_rune      = e.magic_rune;
		buffs[buff_count].dot_rune        = e.dot_rune;
		buffs[buff_count].persistant_buff = e.persistent;
		buffs[buff_count].caston_x        = e.caston_x;
		buffs[buff_count].caston_y        = e.caston_y;
		buffs[buff_count].caston_z        = e.caston_z;
		buffs[buff_count].ExtraDIChance   = e.extra_di_chance;
		buffs[buff_count].instrument_mod  = e.instrument_mod;
		buffs[buff_count].casterid        = 0;

		++buff_count;
	}

	return true;
}

bool BotDatabase::SaveBuffs(Bot* b)
{
	if (!b) {
		return false;
	}

	if (!DeleteBuffs(b->GetBotID())) {
		return false;
	}

	auto buffs = b->GetBuffs();

	if (!buffs) {
		return false;
	}

	auto e = BotBuffsRepository::NewEntity();

	e.bot_id = b->GetBotID();

	std::vector<BotBuffsRepository::BotBuffs> v = { };

	for (int buff_index = 0; buff_index < BUFF_COUNT; ++buff_index) {
		if (!IsValidSpell(buffs[buff_index].spellid)) {
			continue;
		}

		e.spell_id            = buffs[buff_index].spellid;
		e.caster_level        = buffs[buff_index].casterlevel;
		e.duration_formula    = spells[buffs[buff_index].spellid].buff_duration_formula;
		e.tics_remaining      = buffs[buff_index].ticsremaining;
		e.poison_counters     = CalculatePoisonCounters(buffs[buff_index].spellid) > 0 ? buffs[buff_index].counters : 0;
		e.disease_counters    = CalculateDiseaseCounters(buffs[buff_index].spellid) > 0 ? buffs[buff_index].counters : 0;
		e.curse_counters      = CalculateCurseCounters(buffs[buff_index].spellid) > 0 ? buffs[buff_index].counters : 0;
		e.corruption_counters = CalculateCorruptionCounters(buffs[buff_index].spellid) > 0 ? buffs[buff_index].counters : 0;
		e.numhits             = buffs[buff_index].hit_number;
		e.melee_rune          = buffs[buff_index].melee_rune;
		e.magic_rune          = buffs[buff_index].magic_rune;
		e.dot_rune            = buffs[buff_index].dot_rune;
		e.persistent          = buffs[buff_index].persistant_buff ? 1 : 0;
		e.caston_x            = buffs[buff_index].caston_x;
		e.caston_y            = buffs[buff_index].caston_y;
		e.caston_z            = buffs[buff_index].caston_z;
		e.extra_di_chance     = buffs[buff_index].ExtraDIChance;

		v.emplace_back(e);
	}

	if (!v.empty()) {
		const int inserted = BotBuffsRepository::InsertMany(database, v);

		if (!inserted) {
			DeleteBuffs(b->GetBotID());
			return false;
		}
	}

	return true;
}

bool BotDatabase::DeleteBuffs(const uint32 bot_id)
{
	if (!bot_id) {
		return false;
	}

	BotBuffsRepository::DeleteWhere(
		database,
		fmt::format(
			"`bot_id` = {}",
			bot_id
		)
	);

	return true;
}

bool BotDatabase::LoadStance(const uint32 bot_id, int& bot_stance)
{
	if (!bot_id) {
		return false;
	}

	const auto& l = BotStancesRepository::GetWhere(
		database,
		fmt::format(
			"`bot_id` = {} LIMIT 1",
			bot_id
		)
	);

	if (l.empty()) {
		return true;
	}

	auto e = l.front();

	bot_stance = e.stance_id;

	return true;
}

bool BotDatabase::LoadStance(Bot* b, bool& stance_flag)
{
	if (!b) {
		return false;
	}

	b->SetDefaultBotStance();

	const auto& l = BotStancesRepository::GetWhere(
		database,
		fmt::format(
			"`bot_id` = {} LIMIT 1",
			b->GetBotID()
		)
	);

	if (l.empty()) {
		return true;
	}

	auto e = l.front();

	b->SetBotStance(e.stance_id);

	stance_flag = true;

	return true;
}

bool BotDatabase::SaveStance(const uint32 bot_id, const int bot_stance)
{
	if (!bot_id) {
		return false;
	}

	return BotStancesRepository::ReplaceOne(
		database,
		BotStancesRepository::BotStances{
			.bot_id = bot_id,
			.stance_id = static_cast<uint8_t>(bot_stance)
		}
	);
}

bool BotDatabase::SaveStance(Bot* b)
{
	if (!b) {
		return false;
	}

	return BotStancesRepository::ReplaceOne(
		database,
		BotStancesRepository::BotStances{
			.bot_id = b->GetBotID(),
			.stance_id = b->GetBotStance()
		}
	);
}

bool BotDatabase::DeleteStance(const uint32 bot_id)
{
	if (!bot_id) {
		return false;
	}

	BotStancesRepository::DeleteOne(database, bot_id);

	return true;
}

bool BotDatabase::LoadTimers(Bot* b)
{
	if (!b) {
		return false;
	}

	const auto& l = BotTimersRepository::GetWhere(
		database,
		fmt::format(
			"`bot_id` = {}",
			b->GetBotID()
		)
	);

	std::vector<BotTimer> v;

	BotTimer t{ };

	for (const auto& e : l) {
		if (e.timer_value < (Timer::GetCurrentTime() + e.recast_time)) {
			t.timer_id    = e.timer_id;
			t.timer_value = e.timer_value;
			t.recast_time = e.recast_time;
			t.is_spell    = e.is_spell;
			t.is_disc     = e.is_disc;
			t.spell_id    = e.spell_id;
			t.is_item     = e.is_item;
			t.item_id     = e.item_id;

			v.push_back(t);
		}
	}

	if (!v.empty()) {
		b->SetBotTimers(v);
	}

	return true;
}

bool BotDatabase::SaveTimers(Bot* b)
{
	if (!b) {
		return false;
	}

	if (!DeleteTimers(b->GetBotID())) {
		return false;
	}

	std::vector<BotTimer> v = b->GetBotTimers();

	if (v.empty()) {
		return true;
	}

	std::vector<BotTimersRepository::BotTimers> l;

	if (!v.empty()) {
		for (auto& bot_timer : v) {
			if (bot_timer.timer_value <= Timer::GetCurrentTime()) {
				continue;
			}

			auto e = BotTimersRepository::BotTimers{
				.bot_id = b->GetBotID(),
				.timer_id = bot_timer.timer_id,
				.timer_value = bot_timer.timer_value,
				.recast_time = bot_timer.recast_time,
				.is_spell = bot_timer.is_spell,
				.is_disc = bot_timer.is_disc,
				.spell_id = bot_timer.spell_id,
				.is_item = bot_timer.is_item,
				.item_id = bot_timer.item_id
			};

			l.push_back(e);
		}

		if (l.empty()) {
			return true;
		}

		BotTimersRepository::DeleteWhere(
			database,
			fmt::format(
				"`bot_id` = {}",
				b->GetBotID()
			)
		);

		const int inserted = BotTimersRepository::InsertMany(database, l);

		if (!inserted) {
			DeleteTimers(b->GetBotID());
			return false;
		}
	}

	return true;
}

bool BotDatabase::DeleteTimers(const uint32 bot_id)
{
	if (!bot_id) {
		return false;
	}

	BotTimersRepository::DeleteWhere(
		database,
		fmt::format(
			"`bot_id` = {}",
			bot_id
		)
	);

	return true;
}

bool BotDatabase::QueryInventoryCount(const uint32 bot_id, uint32& item_count)
{
	if (!bot_id) {
		return false;
	}

	item_count = BotInventoriesRepository::Count(
		database,
		fmt::format(
			"`bot_id` = {}",
			bot_id
		)
	);

	return true;
}

bool BotDatabase::LoadItems(const uint32 bot_id, EQ::InventoryProfile& inventory_inst)
{
	if (!bot_id) {
		return false;
	}

	const auto& l = BotInventoriesRepository::GetWhere(
		database,
		fmt::format(
			"`bot_id` = {} ORDER BY `slot_id`",
			bot_id
		)
	);

	if (l.empty()) {
		return true;
	}

	for (const auto& e : l) {
		if (!EQ::ValueWithin(e.slot_id, EQ::invslot::EQUIPMENT_BEGIN, EQ::invslot::EQUIPMENT_END)) {
			continue;
		}

		auto inst = database.CreateItem(
			e.item_id,
			e.inst_charges,
			e.augment_1,
			e.augment_2,
			e.augment_3,
			e.augment_4,
			e.augment_5,
			e.augment_6
		);

		if (!inst) {
			LogError(
				"Warning: bot_id [{}] has an invalid item_id [{}] in slot_id [{}]",
				bot_id,
				e.item_id,
				e.slot_id
			);

			continue;
		}

		if (e.inst_charges == INT16_MAX) {
			inst->SetCharges(-1);
		} else if (
			e.inst_charges == 0 &&
			inst->IsStackable()
		) { // Stackable items need a minimum charge of 1 remain moveable.
			inst->SetCharges(1);
		} else {
			inst->SetCharges(e.inst_charges);
		}

		if (e.inst_color) {
			inst->SetColor(e.inst_color);
		}

		if (inst->GetItem()->Attuneable) {
			if (e.inst_no_drop) {
				inst->SetAttuned(true);
			} else if (EQ::ValueWithin(e.slot_id, EQ::invslot::EQUIPMENT_BEGIN, EQ::invslot::EQUIPMENT_END)) {
				inst->SetAttuned(true);
			}
		}

		if (!e.inst_custom_data.empty()) {
			inst->SetCustomDataString(e.inst_custom_data);
		}

		inst->SetOrnamentIcon(e.ornament_icon);
		inst->SetOrnamentationIDFile(e.ornament_id_file);
		inst->SetOrnamentHeroModel(e.ornament_hero_model);

		if (inventory_inst.PutItem(e.slot_id, *inst) == INVALID_INDEX) {
			LogError(
				"Warning: Invalid slot_id for item in inventory: bot_id [{}] item_id [{}] slot_id [{}]",
				bot_id,
				e.item_id,
				e.slot_id
			);
		}

		safe_delete(inst);
	}

	return true;
}

bool BotDatabase::DeleteItems(const uint32 bot_id)
{
	if (!bot_id) {
		return false;
	}

	BotInventoriesRepository::DeleteOne(database, bot_id);

	return true;
}

bool BotDatabase::LoadItemBySlot(const uint32 bot_id, const uint32 slot_id, uint32& item_id)
{
	if (!bot_id || slot_id > EQ::invslot::EQUIPMENT_END) {
		return false;
	}

	const auto& l = BotInventoriesRepository::GetWhere(
		database,
		fmt::format(
			"`bot_id` = {} AND `slot_id` = {} LIMIT 1",
			bot_id,
			slot_id
		)
	);

	if (l.empty()) {
		return true;
	}

	auto e = l.front();

	item_id = e.item_id;

	return true;
}

bool BotDatabase::LoadItemSlots(const uint32 bot_id, std::map<uint16, uint32>& m)
{
	if (!bot_id) {
		return false;
	}

	const auto& l = BotInventoriesRepository::GetWhere(
		database,
		fmt::format(
			"`bot_id` = {}",
			bot_id
		)
	);

	if (l.empty()) {
		return true;
	}

	for (const auto& e : l) {
		m.emplace(std::pair<uint16, uint32>(e.slot_id, e.item_id));
	}

	return true;
}

bool BotDatabase::SaveItemBySlot(Bot* b, const uint32 slot_id, const EQ::ItemInstance* inst)
{
	if (
		!b ||
		!b->GetBotID() ||
		slot_id > EQ::invslot::EQUIPMENT_END
	) {
		return false;
	}

	if (!inst || !inst->GetID()) {
		return true;
	}

	DeleteItemBySlot(b->GetBotID(), slot_id);

	uint32 augment_id[EQ::invaug::SOCKET_COUNT] = { 0, 0, 0, 0, 0, 0 };

	for (uint16 slot_id = EQ::invaug::SOCKET_BEGIN; slot_id <= EQ::invaug::SOCKET_END; ++slot_id) {
		augment_id[slot_id] = inst->GetAugmentItemID(slot_id);
	}

	uint16 item_charges = 0;

	if (inst->GetCharges() >= 0) {
		item_charges = inst->GetCharges();
	} else {
		item_charges = INT16_MAX;
	}

	auto e = BotInventoriesRepository::NewEntity();

	e.bot_id              = b->GetBotID();
	e.slot_id             = slot_id;
	e.item_id             = inst->GetID();
	e.inst_charges        = item_charges;
	e.inst_color          = inst->GetColor();
	e.inst_no_drop        = inst->IsAttuned() ? 1 : 0;
	e.inst_custom_data    = inst->GetCustomDataString();
	e.ornament_icon       = inst->GetOrnamentationIcon();
	e.ornament_id_file    = inst->GetOrnamentationIDFile();
	e.ornament_hero_model = inst->GetOrnamentHeroModel();
	e.augment_1           = augment_id[0];
	e.augment_2           = augment_id[1];
	e.augment_3           = augment_id[2];
	e.augment_4           = augment_id[3];
	e.augment_5           = augment_id[4];
	e.augment_6           = augment_id[5];

	return BotInventoriesRepository::InsertOne(database, e).inventories_index;
}

bool BotDatabase::DeleteItemBySlot(const uint32 bot_id, const uint32 slot_id)
{
	if (!bot_id || slot_id > EQ::invslot::EQUIPMENT_END) {
		return false;
	}

	BotInventoriesRepository::DeleteWhere(
		database,
		fmt::format(
			"`bot_id` = {} AND `slot_id` = {}",
			bot_id,
			slot_id
		)
	);

	return true;
}

bool BotDatabase::SaveEquipmentColor(const uint32 bot_id, const int16 slot_id, const uint32 color)
{
	if (!bot_id) {
		return false;
	}

	const bool all_flag = (slot_id == -2);

	if (!EQ::ValueWithin(slot_id, EQ::invslot::EQUIPMENT_BEGIN, EQ::invslot::EQUIPMENT_END) && !all_flag) {
		return false;
	}

	std::string where_clause;
	if (all_flag) {
		where_clause = fmt::format(
			"IN ({}, {}, {}, {}, {}, {}, {})",
			EQ::invslot::slotHead,
			EQ::invslot::slotArms,
			EQ::invslot::slotWrist1,
			EQ::invslot::slotHands,
			EQ::invslot::slotChest,
			EQ::invslot::slotLegs,
			EQ::invslot::slotFeet
		);
	} else {
		where_clause = fmt::format(
			"= {}",
			slot_id
		);
	}

	return BotInventoriesRepository::UpdateItemColors(database, bot_id, color, where_clause);
}

bool BotDatabase::LoadPetIndex(const uint32 bot_id, uint32& pet_index)
{
	if (!bot_id) {
		return false;
	}

	const auto& l = BotPetsRepository::GetWhere(
		database,
		fmt::format(
			"`bot_id` = {} LIMIT 1",
			bot_id
		)
	);

	if (l.empty()) {
		return true;
	}

	auto e = l.front();

	pet_index = e.pets_index;

	return true;
}

bool BotDatabase::LoadPetSpellID(const uint32 bot_id, uint32& pet_spell_id)
{
	if (!bot_id) {
		return false;
	}

	const auto& l = BotPetsRepository::GetWhere(
		database,
		fmt::format(
			"`bot_id` = {} LIMIT 1",
			bot_id
		)
	);

	if (l.empty()) {
		return true;
	}

	auto e = l.front();

	pet_spell_id = e.spell_id;

	return true;
}

bool BotDatabase::LoadPetStats(const uint32 bot_id, std::string& pet_name, uint32& pet_mana, uint32& pet_hp, uint32& pet_spell_id)
{
	if (!bot_id) {
		return false;
	}

	uint32 saved_pet_index = 0;

	if (!LoadPetIndex(bot_id, saved_pet_index)) {
		return false;
	}

	if (!saved_pet_index) {
		return true;
	}

	const auto& l = BotPetsRepository::GetWhere(
		database,
		fmt::format(
			"`pets_index` = {} LIMIT 1",
			saved_pet_index
		)
	);

	if (l.empty()) {
		return true;
	}

	auto e = l.front();

	pet_spell_id = e.spell_id;
	pet_name     = e.name;
	pet_mana     = e.mana;
	pet_hp       = e.hp;

	return true;
}

bool BotDatabase::SavePetStats(const uint32 bot_id, const std::string& pet_name, const uint32 pet_mana, const uint32 pet_hp, const uint32 pet_spell_id)
{
	if (!bot_id || pet_name.empty() || !pet_spell_id || pet_spell_id > SPDAT_RECORDS) {
		return false;
	}

	if (
		!DeletePetItems(bot_id) ||
		!DeletePetBuffs(bot_id) ||
		!DeletePetStats(bot_id)
	) {
		return false;
	}

	return BotPetsRepository::InsertOne(
		database,
		BotPetsRepository::BotPets{
			.spell_id = pet_spell_id,
			.bot_id = bot_id,
			.name = pet_name,
			.mana = static_cast<int32_t>(pet_mana),
			.hp = static_cast<int32_t>(pet_hp)
		}
	).pets_index;
}

bool BotDatabase::DeletePetStats(const uint32 bot_id)
{
	if (!bot_id) {
		return false;
	}

	uint32 saved_pet_index = 0;

	if (!LoadPetIndex(bot_id, saved_pet_index)) {
		return false;
	}

	if (!saved_pet_index) {
		return true;
	}

	return BotPetsRepository::DeleteOne(database, saved_pet_index) == 1;
}

bool BotDatabase::LoadPetBuffs(const uint32 bot_id, SpellBuff_Struct* pet_buffs)
{
	if (!bot_id) {
		return false;
	}

	uint32 saved_pet_index = 0;

	if (!LoadPetIndex(bot_id, saved_pet_index)) {
		return false;
	}

	if (!saved_pet_index) {
		return true;
	}

	const auto& l = BotPetBuffsRepository::GetWhere(
		database,
		fmt::format(
			"`pets_index` = {}",
			saved_pet_index
		)
	);

	if (l.empty()) {
		return true;
	}

	uint16 buff_index = 0;

	for (const auto& e : l) {
		if (buff_index >= PET_BUFF_COUNT) {
			break;
		}

		pet_buffs[buff_index].spellid  = e.spell_id;
		pet_buffs[buff_index].level    = e.caster_level;
		pet_buffs[buff_index].duration = e.duration;

		if (CalculatePoisonCounters(pet_buffs[buff_index].spellid) > 0) {
			pet_buffs[buff_index].counters = CalculatePoisonCounters(pet_buffs[buff_index].spellid);
		} else if (CalculateDiseaseCounters(pet_buffs[buff_index].spellid) > 0) {
			pet_buffs[buff_index].counters = CalculateDiseaseCounters(pet_buffs[buff_index].spellid);
		} else if (CalculateCurseCounters(pet_buffs[buff_index].spellid) > 0) {
			pet_buffs[buff_index].counters = CalculateCurseCounters(pet_buffs[buff_index].spellid);
		} else if (CalculateCorruptionCounters(pet_buffs[buff_index].spellid) > 0) {
			pet_buffs[buff_index].counters = CalculateCorruptionCounters(pet_buffs[buff_index].spellid);
		}

		++buff_index;
	}

	return true;
}

bool BotDatabase::SavePetBuffs(const uint32 bot_id, const SpellBuff_Struct* pet_buffs, bool delete_flag)
{
	if (
		!bot_id ||
		!pet_buffs ||
		(delete_flag && !DeletePetBuffs(bot_id))
	) {
		return false;
	}

	uint32 saved_pet_index = 0;

	if (!LoadPetIndex(bot_id, saved_pet_index)) {
		return false;
	}

	if (!saved_pet_index) {
		return true;
	}

	auto e = BotPetBuffsRepository::NewEntity();

	e.pets_index = saved_pet_index;

	std::vector<BotPetBuffsRepository::BotPetBuffs> v;

	for (uint16 buff_index = 0; buff_index < PET_BUFF_COUNT; ++buff_index) {
		if (!IsValidSpell(pet_buffs[buff_index].spellid)) {
			continue;
		}

		e.spell_id     = pet_buffs[buff_index].spellid;
		e.caster_level = pet_buffs[buff_index].level;
		e.duration     = pet_buffs[buff_index].duration;

		v.emplace_back(e);
	}

	if (!v.empty()) {
		BotPetBuffsRepository::InsertMany(database, v);
	}

	return true;
}

bool BotDatabase::DeletePetBuffs(const uint32 bot_id)
{
	if (!bot_id) {
		return false;
	}

	uint32 saved_pet_index = 0;

	if (!LoadPetIndex(bot_id, saved_pet_index)) {
		return false;
	}

	if (!saved_pet_index) {
		return true;
	}

	BotPetBuffsRepository::DeleteWhere(database, fmt::format("pets_index = {}", saved_pet_index));

	return true;
}

bool BotDatabase::LoadPetItems(const uint32 bot_id, uint32* pet_items)
{
	if (!bot_id || !pet_items) {
		return false;
	}

	uint32 saved_pet_index = 0;

	if (!LoadPetIndex(bot_id, saved_pet_index)) {
		return false;
	}

	if (!saved_pet_index) {
		return true;
	}

	const auto& l = BotPetInventoriesRepository::GetWhere(
		database,
		fmt::format(
			"`pets_index` = {}",
			saved_pet_index
		)
	);

	if (l.empty()) {
		return true;
	}

	int16 slot_id = EQ::invslot::EQUIPMENT_BEGIN;

	for (const auto& e : l) {
		if (!EQ::ValueWithin(slot_id, EQ::invslot::EQUIPMENT_BEGIN, EQ::invslot::EQUIPMENT_END)) {
			break;
		}

		pet_items[slot_id] = e.item_id;
		++slot_id;
	}

	return true;
}

bool BotDatabase::SavePetItems(const uint32 bot_id, const uint32* pet_items, bool delete_flag)
{
	// Only use 'delete_flag' if not invoked after a botdb.SavePetStats() call

	if (
		!bot_id ||
		!pet_items ||
			(delete_flag && !DeletePetItems(bot_id))
	) {
		return false;
	}

	uint32 saved_pet_index = 0;

	if (!LoadPetIndex(bot_id, saved_pet_index)) {
		return false;
	}

	if (!saved_pet_index) {
		return true;
	}

	auto e = BotPetInventoriesRepository::NewEntity();

	e.pets_index = saved_pet_index;

	std::vector<BotPetInventoriesRepository::BotPetInventories> v;

	for (int slot_id = EQ::invslot::EQUIPMENT_BEGIN; slot_id <= EQ::invslot::EQUIPMENT_END; ++slot_id) {
		if (!pet_items[slot_id]) {
			continue;
		}

		e.item_id = pet_items[slot_id];

		v.emplace_back(e);
	}

	if (!v.empty()) {
		BotPetInventoriesRepository::InsertMany(database, v);
	}

	return true;
}

bool BotDatabase::DeletePetItems(const uint32 bot_id)
{
	if (!bot_id) {
		return false;
	}

	uint32 saved_pet_index = 0;

	if (!LoadPetIndex(bot_id, saved_pet_index)) {
		return false;
	}

	if (!saved_pet_index) {
		return true;
	}

	BotPetInventoriesRepository::DeleteOne(database, saved_pet_index);

	return true;
}

bool BotDatabase::LoadInspectMessage(const uint32 bot_id, InspectMessage_Struct& inspect_message)
{
	if (!bot_id) {
		return false;
	}

	const auto& e = BotInspectMessagesRepository::FindOne(database, bot_id);

	if (!e.bot_id) {
		return false;
	}

	if (e.inspect_message.empty()) {
		return true;
	}

	std::string bot_message = e.inspect_message;

	if (bot_message.size() > UINT8_MAX) {
		bot_message = bot_message.substr(0, UINT8_MAX);
	}

	strn0cpy(inspect_message.text, bot_message.c_str(), sizeof(inspect_message.text));

	return true;
}

bool BotDatabase::SaveInspectMessage(const uint32 bot_id, const InspectMessage_Struct& inspect_message)
{
	if (!bot_id || !DeleteInspectMessage(bot_id)) {
		return false;
	}

	std::string bot_message = inspect_message.text;

	if (bot_message.empty()) {
		return true;
	}

	if (bot_message.size() > UINT8_MAX) {
		bot_message = bot_message.substr(0, UINT8_MAX);
	}
	return true;
}

bool BotDatabase::DeleteInspectMessage(const uint32 bot_id)
{
	if (!bot_id) {
		return false;
	}

	BotInspectMessagesRepository::DeleteOne(database, bot_id);

	return true;
}

bool BotDatabase::SaveAllInspectMessages(const uint32 owner_id, const InspectMessage_Struct& inspect_message)
{
	if (!owner_id || !DeleteAllInspectMessages(owner_id)) {
		return false;
	}

	std::string bot_message = inspect_message.text;

	if (bot_message.empty()) {
		return true;
	}

	if (bot_message.size() > UINT8_MAX) {
		bot_message = bot_message.substr(0, UINT8_MAX);
	}

	return BotInspectMessagesRepository::SaveAllInspectMessages(database, owner_id, bot_message);
}

bool BotDatabase::DeleteAllInspectMessages(const uint32 owner_id)
{
	if (!owner_id) {
		return false;
	}

	BotInspectMessagesRepository::DeleteAllInspectMessages(database, owner_id);

	return true;
}

bool BotDatabase::SaveAllArmorColorBySlot(const uint32 owner_id, const int16 slot_id, const uint32 rgb_value)
{
	if (!owner_id) {
		return false;
	}

	BotInventoriesRepository::SaveAllArmorColorsBySlot(database, owner_id, slot_id, rgb_value);

	return true;
}

bool BotDatabase::SaveAllArmorColors(const uint32 owner_id, const uint32 rgb_value)
{
	if (!owner_id) {
		return false;
	}

	return BotInventoriesRepository::SaveAllArmorColors(database, owner_id, rgb_value);
}

bool BotDatabase::CreateCloneBot(const uint32 bot_id, const std::string& clone_name, uint32& clone_id)
{
	if (!bot_id || clone_name.empty()) {
		return false;
	}

	auto e = BotDataRepository::FindOne(database, bot_id);

	e.bot_id = 0;
	e.name   = clone_name;

	e = BotDataRepository::InsertOne(database, e);

	if (!e.bot_id) {
		return false;
	}

	clone_id = e.bot_id;

	return true;
}

bool BotDatabase::CreateCloneBotInventory(const uint32 bot_id, const uint32 clone_id)
{
	if (!bot_id || !clone_id) {
		return false;
	}

	auto l = BotInventoriesRepository::GetWhere(
		database,
		fmt::format(
			"`bot_id` = {}",
			bot_id
		)
	);

	if (l.empty()) {
		return true;
	}

	for (auto& e : l) {
		e.inventories_index = 0;
		e.bot_id            = clone_id;
	}

	return BotInventoriesRepository::InsertMany(database, l);
}

bool BotDatabase::LoadOwnerOptions(Client* c)
{
	if (!c || !c->CharacterID()) {
		return false;
	}

	const auto& l = BotOwnerOptionsRepository::GetWhere(
		database,
		fmt::format(
			"`owner_id` = {}",
			c->CharacterID()
		)
	);

	if (l.empty()) {
		return true;
	}

	for (const auto& e : l) {
		c->SetBotOption(static_cast<Client::BotOwnerOption>(e.option_type), e.option_value);
	}

	return true;
}

bool BotDatabase::SaveOwnerOption(const uint32 owner_id, size_t type, const bool flag)
{
	if (!owner_id) {
		return false;
	}

	std::vector<Client::BotOwnerOption> l = {
		Client::booDeathMarquee,
		Client::booStatsUpdate,
		Client::booSpawnMessageClassSpecific,
		Client::booUnused,
		Client::booAutoDefend,
		Client::booBuffCounter,
		Client::booMonkWuMessage
	};

	if (
		std::find(
			l.begin(),
			l.end(),
			static_cast<Client::BotOwnerOption>(type)
		) != l.end()
	) {
		return BotOwnerOptionsRepository::ReplaceOne(
			database,
			BotOwnerOptionsRepository::BotOwnerOptions{
				.owner_id = owner_id,
				.option_type = static_cast<uint16_t>(type),
				.option_value = static_cast<uint16_t>(flag ? 1 : 0)
			}
		);
	}

	return false;
}

bool BotDatabase::SaveOwnerOption(const uint32 owner_id, const std::pair<size_t, size_t> type, const std::pair<bool, bool> flag)
{
	if (!owner_id) {
		return false;
	}

	std::vector<Client::BotOwnerOption> l = {
		Client::booSpawnMessageSay,
		Client::booSpawnMessageTell
	};

	auto e = BotOwnerOptionsRepository::NewEntity();

	std::vector<BotOwnerOptionsRepository::BotOwnerOptions> v;

	if (
		std::find(
			l.begin(),
			l.end(),
			static_cast<Client::BotOwnerOption>(type.first)
		) != l.end() &&
		std::find(
			l.begin(),
			l.end(),
			static_cast<Client::BotOwnerOption>(type.second)
		) != l.end()
	) {
		e.owner_id     = owner_id;
		e.option_type  = static_cast<uint16_t>(type.first);
		e.option_value = static_cast<uint16_t>(flag.first ? 1 : 0);

		v.emplace_back(e);

		e.option_type  = static_cast<uint16_t>(type.second);
		e.option_value = static_cast<uint16_t>(flag.second ? 1 : 0);

		v.emplace_back(e);

		return BotOwnerOptionsRepository::ReplaceMany(database, v);
	}

	return false;
}

bool BotDatabase::LoadGroupedBotsByGroupID(const uint32 owner_id, const uint32 group_id, std::list<uint32>& group_list)
{
	if (!group_id || !owner_id) {
		return false;
	}

	const auto& l = GroupIdRepository::GetWhere(
		database,
		fmt::format(
			"`group_id` = {} AND `bot_id` != 0 AND `name` IN (SELECT `name` FROM `bot_data` WHERE `owner_id` = {})",
			group_id,
			owner_id
		)
	);

	for (const auto& e : l) {
		group_list.emplace_back(e.bot_id);
	}

	return true;
}

bool BotDatabase::LoadHealRotationIDByBotID(const uint32 bot_id, uint32& hr_index)
{
	if (!bot_id) {
		return false;
	}

	const auto& l = BotHealRotationsRepository::GetWhere(
		database,
		fmt::format(
			"`bot_id` = {} LIMIT 1",
			bot_id
		)
	);

	if (l.empty()) {
		return true;
	}

	auto e = l.front();

	hr_index = e.heal_rotation_index;

	return true;
}

bool BotDatabase::LoadHealRotation(Bot* hr_member, std::list<uint32>& member_list, std::list<std::string>& target_list, bool& load_flag, bool& member_fail, bool& target_fail)
{
	if (!hr_member) {
		return false;
	}

	uint32 hr_index = 0;

	if (!LoadHealRotationIDByBotID(hr_member->GetBotID(), hr_index)) {
		return false;
	}

	if (!hr_index) {
		return true;
	}

	if (!hr_member->IsHealRotationMember()) {
		return false;
	}

	const auto& e = BotHealRotationsRepository::FindOne(database, hr_index);

	if (!e.heal_rotation_index) {
		return false;
	}

	auto m = (*hr_member->MemberOfHealRotation());

	m->SetIntervalS(e.interval_);
	m->SetFastHeals(e.fast_heals);
	m->SetAdaptiveTargeting(e.adaptive_targeting);
	m->SetCastingOverride(e.casting_override);

	m->SetArmorTypeSafeHPRatio(ARMOR_TYPE_UNKNOWN, e.safe_hp_base);
	m->SetArmorTypeSafeHPRatio(ARMOR_TYPE_CLOTH, e.safe_hp_cloth);
	m->SetArmorTypeSafeHPRatio(ARMOR_TYPE_LEATHER, e.safe_hp_leather);
	m->SetArmorTypeSafeHPRatio(ARMOR_TYPE_CHAIN, e.safe_hp_chain);
	m->SetArmorTypeSafeHPRatio(ARMOR_TYPE_PLATE, e.safe_hp_plate);

	m->SetArmorTypeCriticalHPRatio(ARMOR_TYPE_UNKNOWN, e.critical_hp_base);
	m->SetArmorTypeCriticalHPRatio(ARMOR_TYPE_CLOTH, e.critical_hp_cloth);
	m->SetArmorTypeCriticalHPRatio(ARMOR_TYPE_LEATHER, e.critical_hp_leather);
	m->SetArmorTypeCriticalHPRatio(ARMOR_TYPE_CHAIN, e.critical_hp_chain);
	m->SetArmorTypeCriticalHPRatio(ARMOR_TYPE_PLATE, e.critical_hp_plate);

	load_flag = true;

	if (!LoadHealRotationMembers(hr_index, member_list)) {
		member_fail = true;
	}

	if (!LoadHealRotationTargets(hr_index, target_list)) {
		target_fail = true;
	}

	return true;
}

bool BotDatabase::LoadHealRotationMembers(const uint32 hr_index, std::list<uint32>& member_list)
{
	if (!hr_index) {
		return false;
	}

	const auto& l = BotHealRotationMembersRepository::GetWhere(
		database,
		fmt::format(
			"`heal_rotation_index` = {}",
			hr_index
		)
	);

	if (l.empty()) {
		return true;
	}

	for (const auto& e : l) {
		member_list.push_back(e.bot_id);
	}

	return true;
}

bool BotDatabase::LoadHealRotationTargets(const uint32 hr_index, std::list<std::string>& target_list)
{
	if (!hr_index) {
		return false;
	}

	const auto& l = BotHealRotationTargetsRepository::GetWhere(
		database,
		fmt::format(
			"`heal_rotation_index` = {}",
			hr_index
		)
	);

	if (l.empty()) {
		return true;
	}

	for (const auto& e : l) {
		target_list.push_back(e.target_name);
	}

	return true;
}

bool BotDatabase::SaveHealRotation(Bot* hr_member, bool& member_fail, bool& target_fail)
{
	if (
		!hr_member ||
		!DeleteHealRotation(hr_member->GetBotID()) ||
		!hr_member->IsHealRotationMember()
	) {
		return false;
	}

	auto m = (*hr_member->MemberOfHealRotation());

	auto e = BotHealRotationsRepository::NewEntity();

	e.bot_id             = hr_member->GetBotID();
	e.interval_          = m->IntervalS();
	e.fast_heals         = m->FastHeals();
	e.adaptive_targeting = m->AdaptiveTargeting();
	e.casting_override   = m->CastingOverride();

	e.safe_hp_base    = m->ArmorTypeSafeHPRatio(ARMOR_TYPE_UNKNOWN);
	e.safe_hp_cloth   = m->ArmorTypeSafeHPRatio(ARMOR_TYPE_CLOTH);
	e.safe_hp_leather = m->ArmorTypeSafeHPRatio(ARMOR_TYPE_LEATHER);
	e.safe_hp_chain   = m->ArmorTypeSafeHPRatio(ARMOR_TYPE_CHAIN);
	e.safe_hp_plate   = m->ArmorTypeSafeHPRatio(ARMOR_TYPE_PLATE);

	e.critical_hp_base    = m->ArmorTypeCriticalHPRatio(ARMOR_TYPE_UNKNOWN);
	e.critical_hp_cloth   = m->ArmorTypeCriticalHPRatio(ARMOR_TYPE_CLOTH);
	e.critical_hp_leather = m->ArmorTypeCriticalHPRatio(ARMOR_TYPE_LEATHER);
	e.critical_hp_chain   = m->ArmorTypeCriticalHPRatio(ARMOR_TYPE_CHAIN);
	e.critical_hp_plate   = m->ArmorTypeCriticalHPRatio(ARMOR_TYPE_PLATE);

	e = BotHealRotationsRepository::InsertOne(database, e);

	if (!e.heal_rotation_index) {
		return false;
	}

	std::list<Bot*>* ml = m->MemberList();

	auto re = BotHealRotationMembersRepository::NewEntity();

	re.heal_rotation_index = e.heal_rotation_index;

	std::vector<BotHealRotationMembersRepository::BotHealRotationMembers> rv;

	for (auto m : *ml) {
		if (!m) {
			continue;
		}

		re.bot_id = m->GetBotID();

		rv.emplace_back(re);
	}

	const int inserted_members = BotHealRotationMembersRepository::InsertMany(database, rv);

	if (!inserted_members) {
		member_fail = true;
	}

	std::list<Mob*>* tl = m->TargetList();

	auto te = BotHealRotationTargetsRepository::NewEntity();

	te.heal_rotation_index = e.heal_rotation_index;

	std::vector<BotHealRotationTargetsRepository::BotHealRotationTargets> tv;

	for (auto m : *tl) {
		if (!m) {
			continue;
		}

		te.target_name = m->GetCleanName();

		tv.emplace_back(te);
	}

	const int inserted_targets = BotHealRotationTargetsRepository::InsertMany(database, tv);

	if (!inserted_targets) {
		target_fail = true;
	}

	return true;
}

bool BotDatabase::DeleteHealRotation(const uint32 creator_id)
{
	if (!creator_id) {
		return false;
	}

	uint32 hr_index = 0;

	if (!LoadHealRotationIDByBotID(creator_id, hr_index)) {
		return false;
	}

	if (!hr_index) {
		return true;
	}

	BotHealRotationTargetsRepository::DeleteWhere(
		database,
		fmt::format(
			"`heal_rotation_index` = {}",
			hr_index
		)
	);

	BotHealRotationMembersRepository::DeleteWhere(
		database,
		fmt::format(
			"`heal_rotation_index` = {}",
			hr_index
		)
	);

	BotHealRotationsRepository::DeleteWhere(
		database,
		fmt::format(
			"`heal_rotation_index` = {}",
			hr_index
		)
	);

	return true;
}

bool BotDatabase::DeleteAllHealRotations(const uint32 owner_id)
{
	if (!owner_id) {
		return false;
	}

	const auto& l = BotHealRotationsRepository::GetWhere(
		database,
		fmt::format(
			"`bot_id` IN (SELECT `bot_id` FROM `bot_data` WHERE `owner_id` = {})",
			owner_id
		)
	);

	if (l.empty()) {
		return true;
	}

	for (const auto& e : l) {
		DeleteHealRotation(e.bot_id);
	}

	return true;
}


/* Bot miscellaneous functions   */
uint8 BotDatabase::GetSpellCastingChance(uint8 spell_type_index, uint8 class_index, uint8 stance_index, uint8 conditional_index) // class_index is 0-based
{
	if (
		spell_type_index >= Bot::SPELL_TYPE_COUNT ||
		class_index >= Class::PLAYER_CLASS_COUNT ||
		stance_index >= Stance::AEBurn ||
		conditional_index >= cntHSND
	) {
		return 0;
	}

	return Bot::spell_casting_chances[spell_type_index][class_index][stance_index][conditional_index];
}

uint32 BotDatabase::GetRaceClassBitmask(uint32 bot_race)
{
	const auto& e = BotCreateCombinationsRepository::FindOne(database, bot_race);

	return e.race ? e.classes : 0;
}

const uint8 BotDatabase::GetBotClassByID(const uint32 bot_id)
{
	const auto& e = BotDataRepository::FindOne(database, bot_id);

	return e.bot_id ? e.class_ : Class::None;
}

const uint8 BotDatabase::GetBotGenderByID(const uint32 bot_id)
{
	const auto& e = BotDataRepository::FindOne(database, bot_id);

	return e.bot_id ? e.gender : Gender::Neuter;
}

std::vector<uint32> BotDatabase::GetBotIDsByCharacterID(const uint32 character_id, uint8 class_id)
{
	std::vector<uint32> v;

	const auto& l = BotDataRepository::GetWhere(
		database,
		fmt::format(
			"`owner_id` = {}{}",
			character_id,
			(
				class_id ?
				fmt::format(
					" AND `class` = {}",
					class_id
				) :
				""
				)
		)
	);

	for (const auto& e : l) {
		v.push_back(e.bot_id);
	}

	return v;
}

const uint8 BotDatabase::GetBotLevelByID(const uint32 bot_id)
{
	const auto& e = BotDataRepository::FindOne(database, bot_id);

	return e.bot_id ? e.level : 0;
}

const std::string BotDatabase::GetBotNameByID(const uint32 bot_id)
{
	const auto& e = BotDataRepository::FindOne(database, bot_id);

	return e.bot_id ? e.name : std::string();
}

const uint16 BotDatabase::GetBotRaceByID(const uint32 bot_id)
{
	const auto& e = BotDataRepository::FindOne(database, bot_id);

	return e.bot_id ? e.race : Race::Doug;
}

const int BotDatabase::GetBotExtraHasteByID(const uint32 bot_id)
{
	const auto& e = BotDataRepository::FindOne(database, bot_id);

	return e.bot_id ? e.extra_haste : 0;
}

bool BotDatabase::LoadBotSettings(Mob* m)
{
	if (!m) {
		return false;
	}

	if (!m->IsOfClientBot()) {
		return false;
	}

	uint32 mob_id = (m->IsClient() ? m->CastToClient()->CharacterID() : m->CastToBot()->GetBotID());
	uint8 stance_id = (m->IsBot() ? m->CastToBot()->GetBotStance() : 0);

	std::string query = "";

	if (m->IsClient()) {
		query = fmt::format("`character_id` = {} AND `stance` = {}", mob_id, stance_id);
	}
	else {
		query = fmt::format("`bot_id` = {} AND `stance` = {}", mob_id, stance_id);
	}

	if (stance_id == Stance::Passive) {
		LogBotSettings("{} is currently set to {} [#{}]. No saving or loading required.", m->GetCleanName(), Stance::GetName(Stance::Passive), Stance::Passive);
		return true;
	}

	const auto& l = BotSettingsRepository::GetWhere(database, query);

	if (l.empty()) {
		return true;
	}

	for (const auto& e : l) {
		if (e.setting_type == BotSettingCategories::BaseSetting) {
			LogBotSettings("[{}] says, 'Loading {} [{}] - setting to [{}].",
				m->GetCleanName(),
				Bot::GetBotSettingCategoryName(e.setting_type),
				e.setting_type,
				e.value
			);
		}
		else {
			LogBotSettings("[{}] says, 'Loading {} [{}], {} [{}] - setting to [{}].",
				m->GetCleanName(),
				Bot::GetBotSpellCategoryShortName(e.setting_type),
				e.setting_type,
				Bot::GetSpellTypeNameByID(e.setting_id),
				e.setting_id,
				e.value
			);
		}

		if (m->IsClient()) {
			m->CastToClient()->SetBotSetting(e.setting_type, e.setting_id, e.value);
		}
		else {
			m->CastToBot()->SetBotSetting(e.setting_type, e.setting_id, e.value);
		}
	}

	return true;
}

bool BotDatabase::SaveBotSettings(Mob* m)
{
	if (!m) {
		return false;
	}

	if (!m->IsOfClientBot()) {
		return false;
	}

	uint32 bot_id = (m->IsBot() ? m->CastToBot()->GetBotID() : 0);
	uint32 character_id = (m->IsClient() ? m->CastToClient()->CharacterID() : 0);
	uint8 stance_id = (m->IsBot() ? m->CastToBot()->GetBotStance() : 0);

	if (stance_id == Stance::Passive) {
		LogBotSettings("{} is currently set to {} [#{}]. No saving or loading required.", m->GetCleanName(), Stance::GetName(Stance::Passive), Stance::Passive);
		return true;
	}

	std::string query = "";

	if (m->IsClient()) {
		query = fmt::format("`character_id` = {} AND `stance` = {}", character_id, stance_id);
	}
	else {
		query = fmt::format("`bot_id` = {} AND `stance` = {}", bot_id, stance_id);
	}

	BotSettingsRepository::DeleteWhere(database, query);

	std::vector<BotSettingsRepository::BotSettings> v;

	if (m->IsBot()) {
		uint8 bot_stance = m->CastToBot()->GetBotStance();

		for (uint16 i = BotBaseSettings::START_ALL; i <= BotBaseSettings::END; ++i) {
			if (m->CastToBot()->GetBotBaseSetting(i) != m->CastToBot()->GetDefaultBotBaseSetting(i, bot_stance)) {
				auto e = BotSettingsRepository::BotSettings{
					.character_id				= character_id,
					.bot_id						= bot_id,
					.stance						= stance_id,
					.setting_id					= static_cast<uint16_t>(i),
					.setting_type				= static_cast<uint8_t>(BotSettingCategories::BaseSetting),
					.value						= static_cast<int32_t>(m->CastToBot()->GetBotBaseSetting(i)),
					.category_name				= Bot::GetBotSpellCategoryShortName(BotSettingCategories::BaseSetting),
					.setting_name				= Bot::GetBotSettingCategoryName(i)
				};

				v.emplace_back(e);

				LogBotSettings("{} says, 'Saving {} [{}] - set to [{}] default [{}].'", m->GetCleanName(), Bot::GetBotSettingCategoryName(i), i, e.value, m->CastToBot()->GetDefaultBotBaseSetting(i));
			}
		}

		for (uint16 i = BotSettingCategories::START_NO_BASE; i <= BotSettingCategories::END; ++i) {
			for (uint16 x = BotSpellTypes::START; x <= BotSpellTypes::END; ++x) {
				if (m->CastToBot()->GetSetting(i, x) != m->CastToBot()->GetDefaultSetting(i, x, bot_stance)) {
					auto e = BotSettingsRepository::BotSettings{
						.character_id			= character_id,
						.bot_id					= bot_id,
						.stance					= stance_id,
						.setting_id				= static_cast<uint16_t>(x),
						.setting_type			= static_cast<uint8_t>(i),
						.value					= m->CastToBot()->GetSetting(i, x),
						.category_name			= Bot::GetBotSpellCategoryShortName(i),
						.setting_name			= Bot::GetSpellTypeNameByID(x)
					};

					v.emplace_back(e);

					LogBotSettings("{} says, 'Saving {} {} [{}] - set to [{}] default [{}].'", m->GetCleanName(), Bot::GetBotSpellCategoryShortName(i), Bot::GetSpellTypeNameByID(x), x, e.value, m->CastToBot()->GetDefaultSetting(i, x, bot_stance));
				}
			}
		}
	}

	if (m->IsClient()) {
		/* Currently unused
		if (m->CastToClient()->GetDefaultBotSettings(BotSettingCategories::BaseSetting, BotBaseSettings::IllusionBlock) != m->CastToClient()->GetIllusionBlock()) { // Only illusion block supported
			auto e = BotSettingsRepository::BotSettings{
						.character_id			= character_id,
						.bot_id					= bot_id,
						.stance					= stance_id,
						.setting_id				= static_cast<uint16_t>(BotBaseSettings::IllusionBlock),
						.setting_type			= static_cast<uint8_t>(BotSettingCategories::BaseSetting),
						.value 					= m->CastToClient()->GetIllusionBlock(),
						.category_name			= Bot::GetBotSpellCategoryShortName(BotSettingCategories::BaseSetting),
						.setting_name			= Bot::GetBotSettingCategoryName(BotBaseSettings::IllusionBlock)
			};

			v.emplace_back(e);

			LogBotSettings("{} says, 'Saving {} [{}] - set to [{}] default [{}].'", m->GetCleanName(), Bot::GetBotSettingCategoryName(BotBaseSettings::IllusionBlock), BotBaseSettings::IllusionBlock, e.value, m->CastToClient()->GetIllusionBlock());
		}
		*/

		for (uint16 i = BotSettingCategories::START_CLIENT; i <= BotSettingCategories::END_CLIENT; ++i) {
			for (uint16 x = BotSpellTypes::START; x <= BotSpellTypes::END; ++x) {
				LogBotSettings("{} says, 'Checking {} {} [{}] - set to [{}] default [{}].'", m->GetCleanName(), Bot::GetBotSpellCategoryShortName(i), Bot::GetSpellTypeNameByID(x), x, m->CastToClient()->GetBotSetting(i, x), m->CastToClient()->GetDefaultBotSettings(i, x));
				if (m->CastToClient()->GetBotSetting(i, x) != m->CastToClient()->GetDefaultBotSettings(i, x)) {
					auto e = BotSettingsRepository::BotSettings{
						.character_id			= character_id,
						.bot_id					= bot_id,
						.stance					= stance_id,
						.setting_id				= static_cast<uint16_t>(x),
						.setting_type			= static_cast<uint8_t>(i),
						.value					= m->CastToClient()->GetBotSetting(i, x),
						.category_name			= Bot::GetBotSpellCategoryShortName(i),
						.setting_name			= Bot::GetSpellTypeNameByID(x)
					};

					v.emplace_back(e);

					LogBotSettings("{} says, 'Saving {} {} [{}] - set to [{}] default [{}].'", m->GetCleanName(), Bot::GetBotSpellCategoryShortName(i), Bot::GetSpellTypeNameByID(x), x, e.value, m->CastToClient()->GetDefaultBotSettings(i, x));
				}
			}
		}
	}

	if (!v.empty()) {
		const int inserted = BotSettingsRepository::ReplaceMany(database, v);

		if (!inserted) {
			return false;
		}
	}

	return true;
}

bool BotDatabase::DeleteBotSettings(const uint32 bot_id)
{
	if (!bot_id) {
		return false;
	}

	BotSettingsRepository::DeleteWhere(
		database,
		fmt::format(
			"`bot_id` = {}",
			bot_id
		)
	);

	return true;
}

bool BotDatabase::LoadBotBlockedBuffs(Bot* b)
{
	if (!b) {
		return false;
	}

	const auto& l = BotBlockedBuffsRepository::GetWhere(
		database,
		fmt::format(
			"`bot_id` = {}",
			b->GetBotID()
		)
	);

	std::vector<BotBlockedBuffs> v;

	BotBlockedBuffs t{ };

	for (const auto& e : l) {
		t.spell_id				= e.spell_id;
		t.blocked				= e.blocked;
		t.blocked_pet			= e.blocked_pet;

		v.push_back(t);
	}

	if (!v.empty()) {
		b->SetBotBlockedBuffs(v);
	}

	return true;
}

bool BotDatabase::SaveBotBlockedBuffs(Bot* b)
{
	if (!b) {
		return false;
	}

	if (!DeleteBotBlockedBuffs(b->GetBotID())) {
		return false;
	}

	std::vector<BotBlockedBuffs> v = b->GetBotBlockedBuffs();

	if (v.empty()) {
		return true;
	}

	std::vector<BotBlockedBuffsRepository::BotBlockedBuffs> l;

	if (!v.empty()) {
		for (auto& blocked_buff : v) {
			if (blocked_buff.blocked == 0 && blocked_buff.blocked_pet == 0) {
				continue;
			}

			auto e = BotBlockedBuffsRepository::BotBlockedBuffs{
				.bot_id			= b->GetBotID(),
				.spell_id		= blocked_buff.spell_id,
				.blocked		= blocked_buff.blocked,
				.blocked_pet	= blocked_buff.blocked_pet
			};

			l.push_back(e);
		}

		if (l.empty()) {
			return true;
		}

		BotBlockedBuffsRepository::DeleteWhere(
			database,
			fmt::format(
				"`bot_id` = {}",
				b->GetBotID()
			)
		);

		const int inserted = BotBlockedBuffsRepository::InsertMany(database, l);

		if (!inserted) {
			DeleteBotBlockedBuffs(b->GetBotID());
			return false;
		}
	}

	return true;
}

bool BotDatabase::DeleteBotBlockedBuffs(const uint32 bot_id)
{
	if (!bot_id) {
		return false;
	}

	BotBlockedBuffsRepository::DeleteWhere(
		database,
		fmt::format(
			"`bot_id` = {}",
			bot_id
		)
	);

	return true;
}

void BotDatabase::CheckBotSpells() {
	auto spell_list = BotSpellsEntriesRepository::All(content_db);
	uint16 spell_id;
	SPDat_Spell_Struct spell;

	for (const auto& s : spell_list) {
		if (!IsValidSpell(s.spell_id)) {
			LogBotSpellTypeChecks("{} is an invalid spell", s.spell_id);
			continue;
		}

		spell = spells[s.spell_id];
		spell_id = spell.id;

		if (spell.classes[s.npc_spells_id - (BOT_CLASS_BASE_ID_PREFIX + 1)] >= 255) {
			LogBotSpellTypeChecks("{} [#{}] is not usable by a {} [#{}].", GetSpellName(spell_id), spell_id, GetClassIDName(s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX), s.npc_spells_id);
		}
		else {
			if (spell.classes[s.npc_spells_id - (BOT_CLASS_BASE_ID_PREFIX + 1)] > s.minlevel) {
				LogBotSpellTypeChecks("{} [#{}] is not usable until level {} for a {} [#{}] and the min level is currently set to {}.",
					GetSpellName(spell_id),
					spell_id,
					spell.classes[s.npc_spells_id - (BOT_CLASS_BASE_ID_PREFIX + 1)],
					GetClassIDName(s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX),
					s.npc_spells_id,
					s.minlevel
				);

				LogBotSpellTypeChecksDetail("UPDATE bot_spells_entries SET `minlevel` = {} WHERE `spellid` = {} AND `npc_spells_id` = {}; -- {} [#{}] from minlevel {} to {} for {} [#{}]",
					spell.classes[s.npc_spells_id - (BOT_CLASS_BASE_ID_PREFIX + 1)],
					spell_id,
					s.npc_spells_id,
					GetSpellName(spell_id),
					spell_id,
					s.minlevel,
					spell.classes[s.npc_spells_id - (BOT_CLASS_BASE_ID_PREFIX + 1)],
					GetClassIDName(s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX),
					s.npc_spells_id
				);
			}

			if (spell.classes[s.npc_spells_id - (BOT_CLASS_BASE_ID_PREFIX + 1)] < s.minlevel) {
				LogBotSpellTypeChecks("{} [#{}] could be used starting at level {} for a {} [#{}] instead of the current min level of {}.",
					GetSpellName(spell_id),
					spell_id,
					spell.classes[s.npc_spells_id - (BOT_CLASS_BASE_ID_PREFIX + 1)],
					GetClassIDName(s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX),
					s.npc_spells_id,
					s.minlevel
				);

				LogBotSpellTypeChecksDetail("UPDATE bot_spells_entries SET `minlevel` = {} WHERE `spellid` = {} AND `npc_spells_id` = {}; -- {} [#{}] from minlevel {} to {} for {} [#{}]",
					spell.classes[s.npc_spells_id - (BOT_CLASS_BASE_ID_PREFIX + 1)],
					spell_id,
					s.npc_spells_id,
					GetSpellName(spell_id),
					spell_id,
					s.minlevel,
					spell.classes[s.npc_spells_id - (BOT_CLASS_BASE_ID_PREFIX + 1)],
					GetClassIDName(s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX),
					s.npc_spells_id
				);
			}


			if (spell.classes[s.npc_spells_id - (BOT_CLASS_BASE_ID_PREFIX + 1)] > s.maxlevel) {
				LogBotSpellTypeChecks("{} [#{}] is not usable until level {} for a {} [#{}] and the max level is currently set to {}.",
					GetSpellName(spell_id),
					spell_id,
					spell.classes[s.npc_spells_id - (BOT_CLASS_BASE_ID_PREFIX + 1)],
					GetClassIDName(s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX),
					s.npc_spells_id,
					s.maxlevel
				);
			}
		}

		uint16 correct_type = GetCorrectBotSpellType(s.type, spell_id);

		if (RuleB(Bots, UseParentSpellTypeForChecks)) {
			uint16 parent_type = Bot::GetParentSpellType(correct_type);

			if (s.type == parent_type || s.type == correct_type) {
				continue;
			}

			if (correct_type != parent_type) {
				correct_type = parent_type;
			}
		}
		else {
			if (IsPetBotSpellType(s.type)) {
				correct_type = GetPetBotSpellType(correct_type);
			}
		}

		if (IsPetBotSpellType(correct_type) && (spell.target_type != ST_Pet && spell.target_type != ST_SummonedPet)) {
			correct_type = Bot::GetParentSpellType(correct_type);
		}

		if (correct_type == s.type) {
			continue;
		}

		if (correct_type == UINT16_MAX) {
				LogBotSpellTypeChecks(
					"{} [#{}] is incorrect. It is currently set as {} [#{}] but the correct type is unknown.",
					GetSpellName(spell_id),
					spell_id,
					Bot::GetSpellTypeNameByID(s.type),
					s.type
				);
		}
		else {
			LogBotSpellTypeChecks("{} [#{}] is incorrect. It is currently set as {} [#{}] and should be {} [#{}]",
				GetSpellName(spell_id),
				spell_id,
				Bot::GetSpellTypeNameByID(s.type),
				s.type,
				Bot::GetSpellTypeNameByID(correct_type),
				correct_type
			);
			LogBotSpellTypeChecksDetail("UPDATE bot_spells_entries SET `type` = {} WHERE `spell_id` = {}; -- {} [#{}] from {} [#{}] to {} [#{}]",
				correct_type,
				spell_id,
				GetSpellName(spell_id),
				spell_id,
				Bot::GetSpellTypeNameByID(s.type),
				s.type,
				Bot::GetSpellTypeNameByID(correct_type),
				correct_type
			);
		}
	}
}

void BotDatabase::MapCommandedSpellTypeMinLevels() {
	commanded_spell_type_min_levels.clear();

	auto start = std::min({ BotSpellTypes::START, BotSpellTypes::COMMANDED_START, BotSpellTypes::DISCIPLINE_START });
	auto end = std::max({ BotSpellTypes::END, BotSpellTypes::COMMANDED_END, BotSpellTypes::DISCIPLINE_END });

	for (int i = start; i <= end; ++i) {
		if (!Bot::IsValidBotSpellType(i)) {
			continue;
		}

		for (int x = Class::Warrior; x <= Class::Berserker; ++x) {
			commanded_spell_type_min_levels[i][x] = {UINT8_MAX, "" };
		}
	}

	auto spell_list = BotSpellsEntriesRepository::All(content_db);

	for (const auto& s : spell_list) {
		if (!IsValidSpell(s.spell_id)) {
			LogBotSpellTypeChecks("{} is an invalid spell", s.spell_id);
			continue;
		}

		auto spell = spells[s.spell_id];

		if (spell.target_type == ST_Self) {
			continue;
		}

		int32_t bot_class = s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX;

		if (
			!EQ::ValueWithin(bot_class, Class::Warrior, Class::Berserker) ||
			!Bot::IsValidBotSpellType(s.type)
		) {
			continue;
		}

		for (int i = start; i <= end; ++i) {
			if (s.minlevel > commanded_spell_type_min_levels[i][bot_class].min_level) {
				continue;
			}

			if (
				i > BotSpellTypes::PARENT_TYPE_END &&
				i != s.type &&
				Bot::GetParentSpellType(i) != s.type
			) {
				continue;
			}

			if (!Bot::IsValidSpellTypeBySpellID(i, s.spell_id)) {
				continue;
			}

			if (s.minlevel < commanded_spell_type_min_levels[i][bot_class].min_level) {
				commanded_spell_type_min_levels[i][bot_class].min_level   = s.minlevel;
				commanded_spell_type_min_levels[i][bot_class].description = StringFormat(
					"%s [#%u] - Level %u",
					GetClassIDName(bot_class),
					bot_class,
					s.minlevel
				);
			}
		}
	}
}
