#include "CharacterClassSystem.hpp"
#include "../EditorApp.hpp"
#include "CharacterRegistry.hpp"
#include "ItemRegistry.hpp"
#include "../components/CharacterClassDatabase.hpp"
#include "../components/CharacterIdentity.hpp"
#include "../components/GoapBlackboard.hpp"
#include "../components/PlayerController.hpp"
#include "../components/Inventory.hpp"
#include <OgreLogManager.h>
#include <OgreFontManager.h>
#include <OgreImGuiOverlay.h>
#include <imgui.h>
#include <algorithm>
#include <fstream>

/* ---------------------------------------------------------------------------
 * Helpers
 * --------------------------------------------------------------------------- */

static CharacterRegistry::CharacterRecord *getRecord(flecs::entity entity)
{
	if (!entity.is_alive() || !entity.has<CharacterIdentityComponent>())
		return nullptr;
	auto &ci = entity.get<CharacterIdentityComponent>();
	return CharacterRegistry::getSingleton().findCharacter(ci.registryId);
}

static int getPoolMax(const CharacterRegistry::CharacterRecord *rec,
		      const Ogre::String &name)
{
	if (!rec)
		return 0;
	const auto *def = CharacterClassDatabase::getSingleton().findStat(name);
	if (!def || def->kind != CharacterClassDatabase::StatKind::ResourcePool)
		return 0;
	auto it = rec->stats.find(name.c_str());
	if (it == rec->stats.end())
		return 0;
	if (it->second < def->minValue)
		return def->minValue;
	if (it->second > def->maxValue)
		return def->maxValue;
	return it->second;
}

static int getPoolCurrent(const CharacterRegistry::CharacterRecord *rec,
			  const Ogre::String &name)
{
	int maxVal = getPoolMax(rec, name);
	if (maxVal <= 0)
		return 0;
	auto it = rec->currentPools.find(name.c_str());
	if (it == rec->currentPools.end())
		return maxVal;
	if (it->second < 0)
		return 0;
	if (it->second > maxVal)
		return maxVal;
	return it->second;
}

static void setPoolCurrent(CharacterRegistry::CharacterRecord *rec,
			   const Ogre::String &name, int value)
{
	int maxVal = getPoolMax(rec, name);
	if (maxVal <= 0)
		return;
	if (value < 0)
		value = 0;
	if (value > maxVal)
		value = maxVal;
	rec->currentPools[name.c_str()] = value;
}

/* ---------------------------------------------------------------------------
 * Construction / Destruction
 * --------------------------------------------------------------------------- */

CharacterClassSystem::CharacterClassSystem(flecs::world &world,
					   EditorApp *editorApp)
	: m_world(world)
	, m_editorApp(editorApp)
{
	// Load inventory dialog configuration
	m_config.loadFromFile("inventory_config.json");
}

CharacterClassSystem::~CharacterClassSystem()
{
	// Save inventory dialog configuration only in editor mode
	if (m_editorApp &&
	    m_editorApp->getGameMode() == EditorApp::GameMode::Editor) {
		m_config.saveToFile("inventory_config.json");
	}
}

/* ---------------------------------------------------------------------------
 * Public API
 * --------------------------------------------------------------------------- */

bool CharacterClassSystem::addXP(flecs::entity entity, int64_t amount)
{
	auto *rec = getRecord(entity);
	if (!rec)
		return false;

	rec->currentXP += amount;

	const auto *cls = CharacterClassDatabase::getSingleton().findClass(
		rec->className);
	if (!cls)
		return false;

	int64_t needed =
		CharacterClassDatabase::getSingleton().computeXPForLevel(
			rec->level, *cls);
	if (rec->currentXP >= needed) {
		applyLevelUp(entity);
		return true;
	}
	return false;
}

bool CharacterClassSystem::distributePoint(flecs::entity entity,
					   const Ogre::String &statName)
{
	auto *rec = getRecord(entity);
	if (!rec)
		return false;

	if (rec->availablePoints <= 0)
		return false;

	const auto *cls = CharacterClassDatabase::getSingleton().findClass(
		rec->className);
	if (!cls)
		return false;

	const auto *statDef =
		CharacterClassDatabase::getSingleton().findStat(statName);
	if (!statDef)
		return false;

	int current = rec->stats.count(statName.c_str()) ?
			      rec->stats[statName.c_str()] :
			      0;
	int cost = CharacterClassDatabase::getSingleton().computeStatCost(
		current, *cls);
	if (rec->availablePoints < cost)
		return false;

	rec->availablePoints -= cost;
	rec->stats[statName.c_str()] = current + 1;
	return true;
}

/* ---------------------------------------------------------------------------
 * Update
 * --------------------------------------------------------------------------- */

void CharacterClassSystem::toggleCharacterSheet(flecs::entity entity)
{
	if (!entity.is_alive()) {
		// Null or dead entity: close all sheets
		m_sheets.clear();
		return;
	}
	flecs::entity_t id = entity.id();
	if (m_sheets.count(id)) {
		m_sheets.erase(id);
	} else {
		m_sheets.insert(id);
	}
}

void CharacterClassSystem::update(float deltaTime)
{
	accumulateNeeds(deltaTime);
	checkLevelUps();

	// "I" key handling is now done in EditorApp::frameRenderingQueued
	// where iPressed is valid (set by keyPressed between frames).
}

void CharacterClassSystem::accumulateNeeds(float deltaTime)
{
	auto &db = CharacterClassDatabase::getSingleton();

	m_world.query<CharacterIdentityComponent>().each(
		[&](flecs::entity e, CharacterIdentityComponent &ci) {
			auto *rec =
				CharacterRegistry::getSingleton().findCharacter(
					ci.registryId);
			if (!rec || rec->className.empty())
				return;
			const auto *cls = db.findClass(rec->className);
			if (!cls)
				return;

			for (auto &pair : rec->needs) {
				const auto *needDef = db.findNeed(pair.first);
				if (!needDef)
					continue;

				pair.second += static_cast<int>(
					needDef->accumulationRate * deltaTime);
				if (pair.second > needDef->maxValue)
					pair.second = needDef->maxValue;
				if (pair.second < 0)
					pair.second = 0;
			}

			updateNeedBits(e);
		});
}

void CharacterClassSystem::updateNeedBits(flecs::entity entity)
{
	auto *rec = getRecord(entity);
	if (!rec)
		return;

	auto &db = CharacterClassDatabase::getSingleton();

	if (!entity.has<GoapBlackboard>())
		entity.set<GoapBlackboard>({});

	auto &bb = entity.get_mut<GoapBlackboard>();

	for (const auto &pair : rec->needs) {
		const auto *needDef = db.findNeed(pair.first);
		if (!needDef || needDef->bitName.empty())
			continue;

		int bitIdx = GoapBlackboard::findBitByName(needDef->bitName);
		if (bitIdx < 0)
			continue;

		int value = pair.second;
		bool currentlySet = bb.getBit(bitIdx);

		// Hysteresis: set at high threshold, clear at low threshold
		if (value >= needDef->highThreshold)
			bb.setBit(bitIdx, true);
		else if (value <= needDef->lowThreshold)
			bb.setBit(bitIdx, false);
		// else: keep current state (dead zone between thresholds)
	}
}

void CharacterClassSystem::checkLevelUps()
{
	m_world.query<CharacterIdentityComponent>().each(
		[&](flecs::entity e, CharacterIdentityComponent &ci) {
			auto *rec =
				CharacterRegistry::getSingleton().findCharacter(
					ci.registryId);
			if (!rec || rec->levelUpPending ||
			    rec->className.empty())
				return;

			const auto *cls = CharacterClassDatabase::getSingleton()
						  .findClass(rec->className);
			if (!cls)
				return;

			int64_t needed =
				CharacterClassDatabase::getSingleton()
					.computeXPForLevel(rec->level, *cls);
			if (rec->currentXP >= needed) {
				applyLevelUp(e);
			}
		});
}

void CharacterClassSystem::applyLevelUp(flecs::entity entity)
{
	auto *rec = getRecord(entity);
	if (!rec || rec->className.empty())
		return;

	const auto *cls = CharacterClassDatabase::getSingleton().findClass(
		rec->className);
	if (!cls)
		return;

	int64_t needed =
		CharacterClassDatabase::getSingleton().computeXPForLevel(
			rec->level, *cls);
	rec->currentXP -= needed;
	rec->level++;

	applyLevelUpGrowthAndPoints(entity);

	// Check if player
	bool isPlayer = entity.has<PlayerControllerComponent>();
	if (isPlayer && m_editorApp &&
	    m_editorApp->getGameMode() == EditorApp::GameMode::Game &&
	    m_editorApp->getGamePlayState() ==
		    EditorApp::GamePlayState::Playing) {
		rec->levelUpPending = true;
		m_levelUpDialogs.insert(entity.id());
	} else {
		// AI: auto-distribute immediately
		distributePointsAI(entity);
	}

	Ogre::LogManager::getSingleton().logMessage(
		Ogre::String("CharacterClassSystem: ") +
		Ogre::String(entity.name()) + " reached level " +
		std::to_string(rec->level) + "!");
}

void CharacterClassSystem::applyLevelUpGrowthAndPoints(flecs::entity entity)
{
	auto *rec = getRecord(entity);
	if (!rec || rec->className.empty())
		return;

	const auto *cls = CharacterClassDatabase::getSingleton().findClass(
		rec->className);
	if (!cls)
		return;

	auto &db = CharacterClassDatabase::getSingleton();

	// Auto-grow stats
	for (const auto &pair : cls->statGrowth) {
		int growth = db.computeStatGrowth(pair.first, rec->level, *cls);
		rec->stats[pair.first.c_str()] += growth;
	}

	// Auto-grow skills
	for (const auto &pair : cls->skillGrowth) {
		int growth =
			db.computeSkillGrowth(pair.first, rec->level, *cls);
		rec->skills[pair.first.c_str()] += growth;
		const auto *skillDef = db.findSkill(pair.first);
		if (skillDef &&
		    rec->skills[pair.first.c_str()] > skillDef->maxValue)
			rec->skills[pair.first.c_str()] = skillDef->maxValue;
	}

	// Grant points
	rec->availablePoints += db.computePointsForLevel(rec->level, *cls);
}

void CharacterClassSystem::distributePointsAI(flecs::entity entity)
{
	auto *rec = getRecord(entity);
	if (!rec || rec->className.empty())
		return;

	const auto *cls = CharacterClassDatabase::getSingleton().findClass(
		rec->className);
	if (!cls)
		return;

	int points = rec->availablePoints;

	// Round-robin primary stats
	int idx = 0;
	int safety = 0;
	while (points > 0 && !cls->primaryStats.empty() && safety < 1000) {
		safety++;
		const auto &statName =
			cls->primaryStats[idx % cls->primaryStats.size()];
		int current = rec->stats.count(statName.c_str()) ?
				      rec->stats[statName.c_str()] :
				      0;
		int cost =
			CharacterClassDatabase::getSingleton().computeStatCost(
				current, *cls);
		if (points >= cost) {
			rec->stats[statName.c_str()] = current + 1;
			points -= cost;
			idx++;
		} else {
			idx++;
			if (idx >= (int)cls->primaryStats.size() * 2)
				break;
		}
	}

	// Random distribution for remainder
	if (points > 0 && !rec->stats.empty()) {
		std::vector<std::string> statNames;
		for (const auto &pair : rec->stats)
			statNames.push_back(pair.first);

		int randomSafety = 0;
		while (points > 0 && !statNames.empty() &&
		       randomSafety < 1000) {
			randomSafety++;
			size_t r = rand() % statNames.size();
			const auto &statName = statNames[r];
			int current = rec->stats.count(statName) ?
					      rec->stats[statName] :
					      0;
			int cost = CharacterClassDatabase::getSingleton()
					   .computeStatCost(current, *cls);
			if (points >= cost) {
				rec->stats[statName] = current + 1;
				points -= cost;
			} else {
				// Can't afford this stat, remove from pool
				statNames.erase(statNames.begin() + r);
			}
		}
	}

	rec->availablePoints = points;
}

// ---------------------------------------------------------------------------
// Dialog rendering
// ---------------------------------------------------------------------------

void CharacterClassSystem::renderDialogs()
{
	// Level-up dialogs
	std::vector<flecs::entity_t> closedDialogs;
	for (flecs::entity_t id : m_levelUpDialogs) {
		flecs::entity e = m_world.entity(id);
		if (!e.is_alive() || !e.has<CharacterIdentityComponent>()) {
			closedDialogs.push_back(id);
			continue;
		}
		renderLevelUpDialog(e);
	}
	for (flecs::entity_t id : closedDialogs)
		m_levelUpDialogs.erase(id);

	// Character sheets
	std::vector<flecs::entity_t> closedSheets;
	for (flecs::entity_t id : m_sheets) {
		flecs::entity e = m_world.entity(id);
		if (!e.is_alive()) {
			closedSheets.push_back(id);
			continue;
		}
		renderCharacterSheet(e);
	}
	for (flecs::entity_t id : closedSheets)
		m_sheets.erase(id);
}

void CharacterClassSystem::renderLevelUpDialog(flecs::entity entity)
{
	auto *rec = getRecord(entity);
	if (!rec || rec->className.empty())
		return;

	const auto *cls = CharacterClassDatabase::getSingleton().findClass(
		rec->className);
	if (!cls)
		return;

	ImGui::SetNextWindowPos(ImVec2(200, 200), ImGuiCond_FirstUseEver);
	ImGui::SetNextWindowSize(ImVec2(450, 500), ImGuiCond_FirstUseEver);

	Ogre::String title =
		"Level Up! (Level " + std::to_string(rec->level) + ")";
	bool open = true;
	if (!ImGui::Begin(title.c_str(), &open)) {
		ImGui::End();
		return;
	}

	ImGui::Text("Available Points: %d", rec->availablePoints);
	ImGui::Separator();

	auto &db = CharacterClassDatabase::getSingleton();

	// Stats
	if (!rec->stats.empty()) {
		ImGui::Text("Stats");
		for (auto &pair : rec->stats) {
			const auto *statDef = db.findStat(pair.first);
			int current = pair.second;
			int cost = db.computeStatCost(current, *cls);
			bool canAfford = rec->availablePoints >= cost;

			ImGui::PushID(pair.first.c_str());
			ImGui::Text("%s: %d", pair.first.c_str(), current);
			ImGui::SameLine(150);
			ImGui::Text("Cost: %d", cost);
			ImGui::SameLine(220);
			if (ImGui::Button("+", ImVec2(30, 0)) && canAfford) {
				rec->availablePoints -= cost;
				pair.second = current + 1;
			}
			ImGui::PopID();
		}
	}

	ImGui::Separator();

	if (ImGui::Button("Confirm", ImVec2(100, 0))) {
		rec->levelUpPending = false;
		open = false;
	}
	ImGui::SameLine();
	if (ImGui::Button("Postpone", ImVec2(100, 0))) {
		open = false;
	}

	ImGui::End();

	if (!open) {
		m_levelUpDialogs.erase(entity.id());
		rec->levelUpPending = false;
	}
}

void CharacterClassSystem::renderCharacterSheet(flecs::entity entity)
{
	auto *rec = getRecord(entity);

	const CharacterClassDatabase::ClassDef *cls = nullptr;
	if (rec)
		cls = CharacterClassDatabase::getSingleton().findClass(
			rec->className);

	Ogre::String title = "Character Sheet";
	bool open = true;

	// Full-screen window: use the entire display area
	ImGuiIO &io = ImGui::GetIO();
	ImGui::SetNextWindowPos(ImVec2(0, 0));
	ImGui::SetNextWindowSize(io.DisplaySize);

	ImGuiWindowFlags flags =
		ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize |
		ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar;

	if (!ImGui::Begin(title.c_str(), &open, flags)) {
		ImGui::End();
		return;
	}

	if (rec) {
		ImGui::Text("Class: %s",
			    cls ? cls->name.c_str() : rec->className.c_str());
		ImGui::Text("Level: %d", rec->level);
		ImGui::Text("XP: %ld", (long)rec->currentXP);
		ImGui::Text("Available Points: %d", rec->availablePoints);
		ImGui::Separator();

		// Stats
		if (!rec->stats.empty()) {
			ImGui::Text("Stats");
			for (const auto &pair : rec->stats) {
				const auto *def =
					CharacterClassDatabase::getSingleton()
						.findStat(pair.first);
				if (def &&
				    def->kind ==
					    CharacterClassDatabase::StatKind::
						    ResourcePool) {
					int cur =
						getPoolCurrent(rec, pair.first);
					ImGui::Text("  %s: %d / %d",
						    pair.first.c_str(), cur,
						    pair.second);
				} else {
					ImGui::Text("  %s: %d",
						    pair.first.c_str(),
						    pair.second);
				}
			}
		}

		// Skills
		if (!rec->skills.empty()) {
			ImGui::Text("Skills");
			for (const auto &pair : rec->skills) {
				ImGui::Text("  %s: %d", pair.first.c_str(),
					    pair.second);
			}
		}

		// Needs
		if (!rec->needs.empty()) {
			ImGui::Text("Needs");
			for (const auto &pair : rec->needs) {
				ImGui::Text("  %s: %d", pair.first.c_str(),
					    pair.second);
			}
		}
	} else {
		ImGui::Text("(No character data available)");
	}

	ImGui::Separator();

	// Inventory
	if (entity.has<InventoryComponent>()) {
		auto &inv = entity.get<InventoryComponent>();
		ImGui::Text("Inventory (%.1f / %.1f kg)", inv.totalWeight,
			    inv.maxWeight);
		ImGui::Separator();
		if (inv.slots.empty()) {
			ImGui::Text("  (empty)");
		} else {
			for (size_t i = 0; i < inv.slots.size(); i++) {
				const auto &slot = inv.slots[i];
				if (slot.isEmpty())
					continue;
				Ogre::String itemName =
					ItemRegistry::getSingleton().getItemName(
						slot.itemId);
				if (itemName.empty())
					itemName = slot.itemId;
				ImGui::Text("  %s x%d", itemName.c_str(),
					    slot.stackSize);
			}
		}
	} else {
		ImGui::Text("No inventory");
	}

	ImGui::End();

	if (!open) {
		m_sheets.erase(entity.id());
		// Unpause the game when the sheet is closed via the X button
		if (m_editorApp &&
		    m_editorApp->getGameMode() == EditorApp::GameMode::Game &&
		    m_editorApp->getGamePlayState() ==
			    EditorApp::GamePlayState::Paused) {
			m_editorApp->setGamePlayState(
				EditorApp::GamePlayState::Playing);
		}
	}
}

// ---------------------------------------------------------------------------
// InventoryDialogConfig implementation
// ---------------------------------------------------------------------------

void InventoryDialogConfig::prepareFont(EditorApp *editorApp)
{
	if (!editorApp)
		return;

	Ogre::ImGuiOverlay *overlay = editorApp->getImGuiOverlay();
	if (!overlay)
		return;

	if (fontPath.empty())
		return;

	try {
		Ogre::String fontName = "InventoryDialogFont";
		if (Ogre::FontManager::getSingleton().resourceExists(
			    fontName, "General")) {
			Ogre::FontManager::getSingleton().remove(fontName,
								 "General");
		}
		Ogre::FontPtr font = Ogre::FontManager::getSingleton().create(
			fontName, "General");
		font->setType(Ogre::FontType::FT_TRUETYPE);
		font->setSource(fontPath);
		font->setTrueTypeSize(fontSize);
		font->setTrueTypeResolution(75);
		font->addCodePointRange(Ogre::Font::CodePointRange(32, 255));
		font->load();
		overlay->addFont(fontName, "General");
	} catch (...) {
		Ogre::LogManager::getSingleton().logMessage(
			"InventoryDialogConfig: Failed to load font " +
			fontPath);
	}
}

bool InventoryDialogConfig::loadFromFile(const std::string &filepath)
{
	std::ifstream f(filepath);
	if (!f.is_open())
		return false;
	try {
		nlohmann::json j;
		f >> j;
		deserialize(j);
		return true;
	} catch (...) {
		return false;
	}
}

bool InventoryDialogConfig::saveToFile(const std::string &filepath)
{
	try {
		std::ofstream f(filepath);
		if (!f.is_open())
			return false;
		f << serialize().dump(2);
		return true;
	} catch (...) {
		return false;
	}
}
