/**
 * Canary - A free and open-source MMORPG server emulator
 * Copyright (©) 2019–present OpenTibiaBR <opentibiabr@outlook.com>
 * Repository: https://github.com/opentibiabr/canary
 * License: https://github.com/opentibiabr/canary/blob/main/LICENSE
 * Contributors: https://github.com/opentibiabr/canary/graphs/contributors
 * Website: https://docs.opentibiabr.com/
 */

#include "creatures/players/imbuements/imbuements.hpp"

#include "config/configmanager.hpp"
#include "creatures/players/player.hpp"
#include "items/item.hpp"
#include "items/tile.hpp"
#include "game/scheduling/dispatcher.hpp"
#include "lib/di/container.hpp"
#include "utils/pugicast.hpp"
#include "utils/const.hpp"
#include "utils/tools.hpp"

Imbuements &Imbuements::getInstance() {
	return inject<Imbuements>();
}

Imbuement* Imbuements::getImbuement(uint16_t id) {
	if (id == 0) {
		return nullptr;
	}

	auto it = imbuementMap.find(id);
	if (it == imbuementMap.end()) {
		g_logger().warn("Imbuement {} not found", id);
		return nullptr;
	}
	return &it->second;
}

Imbuement* Imbuements::getImbuementByScrollID(uint16_t scrollId) {
	if (auto it = scrollIdMap.find(scrollId); it != scrollIdMap.end()) {
		return it->second;
	}

	return nullptr;
}

bool Imbuements::loadFromXml(bool /* reloading */) {
	pugi::xml_document doc;
	auto folder = g_configManager().getString(CORE_DIRECTORY) + "/XML/imbuements.xml";
	pugi::xml_parse_result result = doc.load_file(folder.c_str());
	if (!result) {
		printXMLError(__FUNCTION__, folder, result);
		return false;
	}

	loaded = true;
	for (const auto &baseNode : doc.child("imbuements").children()) {
		pugi::xml_attribute attr;
		// Base for imbue
		if (strcasecmp(baseNode.name(), "base") == 0) {
			pugi::xml_attribute id = baseNode.attribute("id");
			if (!id) {
				g_logger().warn("Missing id for base entry");
				continue;
			}
			[[maybe_unused]] const auto &unusedBase = basesImbuement.emplace_back(
				pugi::cast<uint16_t>(id.value()),
				baseNode.attribute("name").as_string(),
				pugi::cast<uint32_t>(baseNode.attribute("price").value()),
				pugi::cast<uint32_t>(baseNode.attribute("protectionPrice").value()),
				pugi::cast<uint32_t>(baseNode.attribute("removecost").value()),
				pugi::cast<uint32_t>(baseNode.attribute("duration").value()),
				pugi::cast<uint16_t>(baseNode.attribute("percent").value())
			);

			// Category/Group
		} else if (strcasecmp(baseNode.name(), "category") == 0) {
			pugi::xml_attribute id = baseNode.attribute("id");
			if (!id) {
				g_logger().warn("Missing id for category entry");
				continue;
			}
			[[maybe_unused]] const auto &unusedCategory = categoriesImbuement.emplace_back(
				pugi::cast<uint16_t>(id.value()),
				baseNode.attribute("name").as_string(),
				baseNode.attribute("agressive").as_bool(true)
			);

			// Imbuements
		} else if (strcasecmp(baseNode.name(), "imbuement") == 0) {
			++runningid;
			pugi::xml_attribute base = baseNode.attribute("base");
			if (!base) {
				g_logger().warn("Missing imbuement base id");
				continue;
			}

			uint16_t baseid = pugi::cast<uint32_t>(base.value());
			auto groupBase = getBaseByID(baseid);
			if (groupBase == nullptr) {
				g_logger().warn("Group base '{}' not exist", baseid);
				continue;
			}

			auto [imbuementInfo, inserted] = imbuementMap.emplace(std::piecewise_construct, std::forward_as_tuple(runningid), std::forward_as_tuple(runningid, baseid));

			if (!inserted) {
				g_logger().warn("Duplicate imbuement of Base ID: '{}' ignored", baseid);
				continue;
			}

			Imbuement &imbuement = imbuementInfo->second;

			pugi::xml_attribute iconBase = baseNode.attribute("iconid");
			if (!iconBase) {
				g_logger().warn("Missing 'iconid' for imbuement entry");
				continue;
			}
			imbuement.icon = pugi::cast<uint16_t>(iconBase.value());

			if (pugi::xml_attribute soundBase = baseNode.attribute("sound")) {
				imbuement.soundEffect = static_cast<SoundEffect_t>(pugi::cast<uint16_t>(soundBase.value()));
			}

			pugi::xml_attribute premiumBase = baseNode.attribute("premium");
			if (premiumBase) {
				imbuement.premium = premiumBase.as_bool();
			}

			if (pugi::xml_attribute storageBase = baseNode.attribute("storage")) {
				imbuement.storage = pugi::cast<uint32_t>(storageBase.value());
			}

			pugi::xml_attribute subgroupBase = baseNode.attribute("subgroup");
			if (subgroupBase) {
				imbuement.subgroup = subgroupBase.as_string();
			}

			pugi::xml_attribute categorybase = baseNode.attribute("category");
			if (!categorybase) {
				g_logger().warn("Missing imbuement category");
				continue;
			}

			auto category = pugi::cast<uint16_t>(categorybase.value());
			auto category_p = getCategoryByID(category);
			if (category_p == nullptr) {
				g_logger().warn("Category imbuement {} not exist", category);
				continue;
			}

			imbuement.category = category;

			pugi::xml_attribute nameBase = baseNode.attribute("name");
			if (!nameBase) {
				g_logger().warn("Missing imbuement name");
				continue;
			}
			imbuement.name = nameBase.value();

			for (const auto &childNode : baseNode.children()) {
				if (!((attr = childNode.attribute("key")))) {
					g_logger().warn("Missing key attribute in imbuement id: {}", runningid);
					continue;
				}

				std::string type = attr.as_string();
				if (strcasecmp(type.c_str(), "scroll") == 0) {
					if ((attr = childNode.attribute("value"))) {
						imbuement.scrollId = pugi::cast<uint16_t>(attr.value());
					} else {
						g_logger().warn("Missing scroll ID for imbuement name '{}'", imbuement.name);
					}
				} else if (strcasecmp(type.c_str(), "item") == 0) {
					if (!((attr = childNode.attribute("value")))) {
						g_logger().warn("Missing item ID for imbuement name '{}'", imbuement.name);
						continue;
					}
					auto sourceId = pugi::cast<uint16_t>(attr.value());

					uint16_t count = 1;
					if ((attr = childNode.attribute("count"))) {
						count = pugi::cast<uint16_t>(childNode.attribute("count").value());
					}

					const auto &it2 = std::ranges::find_if(imbuement.items, [sourceId](const std::pair<uint16_t, uint16_t> &source) -> bool {
						return source.first == sourceId;
					});

					if (it2 != imbuement.items.end()) {
						g_logger().warn("Duplicate item: {}, imbument name: {} ignored", childNode.attribute("value").value(), imbuement.name);
						continue;
					}

					[[maybe_unused]] const auto &unusedItem = imbuement.items.emplace_back(sourceId, count);

				} else if (strcasecmp(type.c_str(), "description") == 0) {
					std::string description = imbuement.name;
					if ((attr = childNode.attribute("value"))) {
						description = attr.as_string();
					}

					imbuement.description = description;
				} else if (strcasecmp(type.c_str(), "effect") == 0) {
					// Effects
					if (!((attr = childNode.attribute("type")))) {
						g_logger().warn("Missing effect type for imbuement name: {}", imbuement.name);
						continue;
					}

					std::string effecttype = attr.as_string();

					if (strcasecmp(effecttype.c_str(), "skill") == 0) {
						if (!((attr = childNode.attribute("value")))) {
							g_logger().warn("Missing effect value for imbuement name {}", imbuement.name);
							continue;
						}

						uint8_t usenormalskill = 1; // 1 = skill normal, 2 = magiclevel, 3 = leechs/crit

						uint8_t skillId;
						std::string tmpStrValue = asLowerCaseString(attr.as_string());
						if (tmpStrValue == "sword") {
							skillId = SKILL_SWORD;
						} else if (tmpStrValue == "axe") {
							skillId = SKILL_AXE;
						} else if (tmpStrValue == "club") {
							skillId = SKILL_CLUB;
						} else if ((tmpStrValue == "dist") || (tmpStrValue == "distance")) {
							skillId = SKILL_DISTANCE;
						} else if (tmpStrValue == "fish") {
							skillId = SKILL_FISHING;
						} else if (tmpStrValue == "shield") {
							skillId = SKILL_SHIELD;
						} else if (tmpStrValue == "fist") {
							skillId = SKILL_FIST;
						} else if (tmpStrValue == "magicpoints") {
							skillId = STAT_MAGICPOINTS;
							usenormalskill = 2;
						} else if (tmpStrValue == "critical") {
							usenormalskill = 3;
							skillId = SKILL_CRITICAL_HIT_DAMAGE;
						} else if (tmpStrValue == "lifeleech") {
							usenormalskill = 3;
							skillId = SKILL_LIFE_LEECH_AMOUNT;
						} else if (tmpStrValue == "manaleech") {
							usenormalskill = 3;
							skillId = SKILL_MANA_LEECH_AMOUNT;
						} else {
							g_logger().warn("Unknow skill name {} in imbuement name {}", tmpStrValue, imbuement.name);
							continue;
						}

						if (!((attr = childNode.attribute("bonus")))) {
							g_logger().warn("Missing skill bonus for imbuement name {}", imbuement.name);
							continue;
						}
						auto bonus = pugi::cast<int32_t>(attr.value());

						if (usenormalskill == 1) {
							imbuement.skills[skillId] = bonus;
						} else if (usenormalskill == 2) {
							imbuement.stats[skillId] = bonus;
						} else if (usenormalskill == 3) {
							imbuement.skills[skillId] = bonus;
							int32_t chance = 100;
							if ((attr = childNode.attribute("chance"))) {
								chance = std::min<uint32_t>(10000, pugi::cast<int32_t>(attr.value()));
							}

							imbuement.skills[skillId - 1] = chance;
						}
					} else if (strcasecmp(effecttype.c_str(), "damage") == 0) {
						if (!((attr = childNode.attribute("combat")))) {
							g_logger().warn("Missing combat for imbuement name {}", imbuement.name);
							continue;
						}

						CombatType_t combatType = getCombatTypeByName(attr.as_string());
						if (combatType == COMBAT_NONE) {
							g_logger().warn("Unknown combat type for element {}", attr.as_string());
							continue;
						}

						if (!((attr = childNode.attribute("value")))) {
							g_logger().warn("Missing damage reduction percentage for imbuement name {}", imbuement.name);
							continue;
						}

						uint32_t percent = std::min<uint32_t>(100, pugi::cast<uint32_t>(attr.value()));

						imbuement.combatType = combatType;
						imbuement.elementDamage = std::min<int16_t>(100, percent);
					} else if (strcasecmp(effecttype.c_str(), "reduction") == 0) {
						if (!((attr = childNode.attribute("combat")))) {
							g_logger().warn("Missing combat for imbuement name {}", imbuement.name);
							continue;
						}

						CombatType_t combatType = getCombatTypeByName(attr.as_string());
						if (combatType == COMBAT_NONE) {
							g_logger().warn("Unknown combat type for element {}", attr.as_string());
							continue;
						}

						if (!((attr = childNode.attribute("value")))) {
							g_logger().warn("Missing damage reduction percentage for imbuement name {}", imbuement.name);
							continue;
						}

						uint32_t percent = std::min<uint32_t>(100, pugi::cast<uint32_t>(attr.value()));

						imbuement.absorbPercent[combatTypeToIndex(combatType)] = percent;
					} else if (strcasecmp(effecttype.c_str(), "speed") == 0) {
						if (!((attr = childNode.attribute("value")))) {
							g_logger().warn("Missing speed value for imbuement name {}", imbuement.name);
							continue;
						}

						imbuement.speed = pugi::cast<uint32_t>(attr.value());
					} else if (strcasecmp(effecttype.c_str(), "capacity") == 0) {
						if (!((attr = childNode.attribute("value")))) {
							g_logger().warn("Missing cap value for imbuement name {}", imbuement.name);
							continue;
						}

						imbuement.capacity = pugi::cast<uint32_t>(attr.value());

					} else if (strcasecmp(effecttype.c_str(), "paralysis") == 0 || strcasecmp(effecttype.c_str(), "vibrancy") == 0) {
						/////////Imbuement Vibrancy/////////
						// Accept both 'chance' and 'value' as percent (0-100)
						uint32_t chancePercent = 0;
						if ((attr = childNode.attribute("chance"))) {
							chancePercent = std::min<uint32_t>(100, pugi::cast<uint32_t>(attr.value()));
						} else if ((attr = childNode.attribute("value"))) {
							chancePercent = std::min<uint32_t>(100, pugi::cast<uint32_t>(attr.value()));
						} else {
							g_logger().warn("Missing paralysis chance/value for imbuement name {}", imbuement.name);
							continue;
						}

						imbuement.paralysisRemoveChance = static_cast<uint8_t>(chancePercent);
						// Optional: deflect additional PvP paralyse attacks
						uint32_t pvpDeflect = 0;
						if ((attr = childNode.attribute("pvpDeflect"))) {
							pvpDeflect = pugi::cast<uint32_t>(attr.value());
						}
						imbuement.pvpParalysisDeflect = (pvpDeflect != 0);
					}
				}
			}

			if (imbuement.scrollId != 0) {
				auto [it, scrollInserted] = scrollIdMap.emplace(imbuement.scrollId, &imbuement);
				if (!scrollInserted) {
					g_logger().warn("Duplicate scroll ID {} for imbuement '{}', already mapped to '{}'", imbuement.scrollId, imbuement.name, it->second->getName());
					it->second = &imbuement;
				}
			}
		}
	}

	return true;
}

bool Imbuements::reload() {
	imbuementMap.clear();
	scrollIdMap.clear();
	basesImbuement.clear();
	categoriesImbuement.clear();

	runningid = 0;
	loaded = false;

	return loadFromXml(true);
}

BaseImbuement* Imbuements::getBaseByID(uint16_t id) {
	const auto &baseImbuements = std::ranges::find_if(basesImbuement, [id](const BaseImbuement &groupImbuement) {
		return groupImbuement.id == id;
	});

	return baseImbuements != basesImbuement.end() ? &*baseImbuements : nullptr;
}

CategoryImbuement* Imbuements::getCategoryByID(uint16_t id) {
	const auto &categoryImbuements = std::ranges::find_if(categoriesImbuement, [id](const CategoryImbuement &categoryImbuement) {
		return categoryImbuement.id == id;
	});

	return categoryImbuements != categoriesImbuement.end() ? &*categoryImbuements : nullptr;
}

std::vector<Imbuement*> Imbuements::getImbuements(const std::shared_ptr<Player> &player, const std::shared_ptr<Item> &item /* = nullptr */, bool scroll /* = false */) {
	std::vector<Imbuement*> imbuements;

	for (auto &[key, value] : imbuementMap) {
		Imbuement* imbuement = &value;

		if (scroll && imbuement->getScrollItemID() == 0) {
			continue;
		}

		// Parse the storages for each imbuement in imbuements.xml and config.lua (enable/disable storage)
		if (g_configManager().getBoolean(TOGGLE_IMBUEMENT_SHRINE_STORAGE)
		    && imbuement->getStorage() != 0
		    && player->getStorageValue(imbuement->getStorage() == -1)
		    && imbuement->getBaseID() >= 1 && imbuement->getBaseID() <= 3) {
			continue;
		}

		// Send only the imbuements registered on item (in items.xml) to the imbuement window
		const CategoryImbuement* categoryImbuement = getCategoryByID(imbuement->getCategory());
		if (item && !item->hasImbuementType(static_cast<ImbuementTypes_t>(categoryImbuement->id), imbuement->getBaseID())) {
			continue;
		}

		// If the item is already imbued with an imbuement, remove the imbuement from the next free slot
		if (item && item->hasImbuementCategoryId(categoryImbuement->id)) {
			continue;
		}

		[[maybe_unused]] auto &unusedImbuement = imbuements.emplace_back(imbuement);
	}

	return imbuements;
}

uint16_t Imbuement::getID() const {
	return id;
}

uint16_t Imbuement::getBaseID() const {
	return baseid;
}

uint32_t Imbuement::getStorage() const {
	return storage;
}

bool Imbuement::isPremium() const {
	return premium;
}

std::string Imbuement::getName() const {
	return name;
}

std::string Imbuement::getDescription() const {
	return description;
}

std::string Imbuement::getSubGroup() const {
	return subgroup;
}

uint16_t Imbuement::getCategory() const {
	return category;
}

const std::vector<std::pair<uint16_t, uint16_t>> &Imbuement::getItems() const {
	return items;
}

uint16_t Imbuement::getIconID() const {
	return icon + (baseid - 1);
}

ImbuementDecay &ImbuementDecay::getInstance() {
	return inject<ImbuementDecay>();
}

bool ImbuementDecay::canDecayImbuement(const std::shared_ptr<Item> &item, const ImbuementInfo &imbuementInfo) const {
	const auto &player = item->getHoldingPlayer();
	if (!player) {
		return false;
	}

	const auto &playerTile = player->getTile();
	const bool isInProtectionZone = playerTile && playerTile->hasFlag(TILESTATE_PROTECTIONZONE);
	const bool isInFightMode = player->hasCondition(CONDITION_INFIGHT);
	const bool nonAggressiveFightOnly = g_configManager().getBoolean(TOGGLE_IMBUEMENT_NON_AGGRESSIVE_FIGHT_ONLY);
	const auto imbuement = imbuementInfo.imbuement;
	const CategoryImbuement* categoryImbuement = g_imbuements().getCategoryByID(imbuement->getCategory());
	const auto &parent = item->getParent();
	const bool isInBackpack = parent && parent->getContainer();

	if (categoryImbuement && (categoryImbuement->agressive || nonAggressiveFightOnly) && (isInProtectionZone || !isInFightMode || isInBackpack)) {
		return false;
	}

	if (categoryImbuement && !categoryImbuement->agressive && parent && parent != player) {
		return false;
	}

	return true;
}

void ImbuementDecay::startImbuementDecay(const std::shared_ptr<Item> &item) {
	if (!item) {
		g_logger().error("[{}] item is nullptr", __FUNCTION__);
		return;
	}

	if (!item->hasImbuements()) {
		return;
	}

	const auto key = item.get();
	const auto existing = m_itemsToDecay.find(key);
	if (existing != m_itemsToDecay.end()) {
		if (!existing->second.item.expired()) {
			return;
		}
		m_itemsToDecay.erase(existing);
	}

	g_logger().debug("Starting imbuement decay for item {}", item->getName());

	const int64_t now = OTSYS_TIME();

	m_itemsToDecay.emplace(key, TrackedImbuementItem { item, now });

	if (m_eventId == 0) {
		m_eventId = g_dispatcher().scheduleEvent(
			1000, [this] { checkImbuementDecay(); }, "ImbuementDecay::checkImbuementDecay", DispatcherLane::Maintenance
		);
		g_logger().trace("Scheduled imbuement decay check every 1 second.");
	}
}

void ImbuementDecay::stopImbuementDecay(const std::shared_ptr<Item> &item) {
	if (!item) {
		g_logger().error("[{}] item is nullptr", __FUNCTION__);
		return;
	}

	const auto key = item.get();
	const auto it = m_itemsToDecay.find(key);
	if (it == m_itemsToDecay.end()) {
		return;
	}

	g_logger().debug("Stopping imbuement decay for item {}", item->getName());

	m_itemsToDecay.erase(it);

	if (m_itemsToDecay.empty() && m_eventId != 0) {
		g_dispatcher().stopEvent(m_eventId);
		m_eventId = 0;
		g_logger().trace("No more items to decay. Stopped imbuement decay scheduler.");
	}
}

void ImbuementDecay::checkImbuementDecay() {
	const int64_t currentTime = OTSYS_TIME();

	g_logger().trace("Checking imbuement decay for {} items.", m_itemsToDecay.size());

	for (auto it = m_itemsToDecay.begin(); it != m_itemsToDecay.end();) {
		auto item = it->second.item.lock();
		if (!item) {
			g_logger().trace("[{}] tracked item expired; removing from decay queue.", __FUNCTION__);
			it = m_itemsToDecay.erase(it);
			continue;
		}

		// Get the player holding the item (if any)
		auto player = item->getHoldingPlayer();
		if (!player) {
			g_logger().debug("Item {} is not held by any player. Skipping decay.", item->getName());
			it = m_itemsToDecay.erase(it);
			continue;
		}

		bool hasImbuements = false;

		const int64_t lastUpdate = it->second.lastUpdate == 0 ? currentTime : it->second.lastUpdate;
		const int64_t elapsedTime = std::max<int64_t>(0, currentTime - lastUpdate);
		const uint32_t elapsedSeconds = static_cast<uint32_t>(elapsedTime / 1000);

		for (uint8_t slotid = 0; slotid < item->getImbuementSlot(); ++slotid) {
			ImbuementInfo imbuementInfo;
			if (!item->getImbuementInfo(slotid, &imbuementInfo)) {
				g_logger().debug("No imbuement info found for slot {} in item {}", slotid, item->getName());
				continue;
			}

			hasImbuements = true;

			if (!canDecayImbuement(item, imbuementInfo)) {
				continue;
			}

			const auto imbuement = imbuementInfo.imbuement;

			// If the imbuement's duration is 0, remove its stats and continue to the next slot
			if (imbuementInfo.duration == 0) {
				player->removeItemImbuementStats(imbuement);
				player->updateImbuementTrackerStats();
				continue;
			}

			g_logger().debug("Decaying imbuement {} from item {} of player {}", imbuement->getName(), item->getName(), player->getName());
			// Calculate the new duration of the imbuement, making sure it doesn't go below 0
			uint32_t duration = imbuementInfo.duration > elapsedSeconds ? imbuementInfo.duration - elapsedSeconds : 0;
			item->decayImbuementTime(slotid, imbuement->getID(), duration);

			if (duration == 0) {
				player->removeItemImbuementStats(imbuement);
				player->updateImbuementTrackerStats();
			}
		}

		if (!hasImbuements) {
			it = m_itemsToDecay.erase(it);
			continue;
		}

		it->second.lastUpdate = currentTime;
		++it;
	}

	// Reschedule the event if there are still items
	if (!m_itemsToDecay.empty()) {
		m_eventId = g_dispatcher().scheduleEvent(
			1000, [this] { checkImbuementDecay(); }, "ImbuementDecay::checkImbuementDecay", DispatcherLane::Maintenance
		);
		g_logger().trace("Re-scheduled imbuement decay check.");
	} else {
		m_eventId = 0;
		g_logger().trace("No more items to decay. Stopped imbuement decay scheduler.");
	}
}

uint16_t Imbuement::getScrollItemID() const {
	return scrollId;
}
