#include "CharacterClassDatabase.hpp"
#include <OgreLogManager.h>
#include <OgreResourceGroupManager.h>
#include <nlohmann/json.hpp>
#include <fstream>
#include <filesystem>

// ---------------------------------------------------------------------------
// Singleton
// ---------------------------------------------------------------------------

CharacterClassDatabase &CharacterClassDatabase::getSingleton()
{
	static CharacterClassDatabase instance;
	return instance;
}

CharacterClassDatabase *CharacterClassDatabase::getSingletonPtr()
{
	return &getSingleton();
}

// ---------------------------------------------------------------------------
// Lookup
// ---------------------------------------------------------------------------

const CharacterClassDatabase::StatDef *
CharacterClassDatabase::findStat(const Ogre::String &name) const
{
	auto it = m_stats.find(name);
	if (it != m_stats.end())
		return &it->second;
	return nullptr;
}

const CharacterClassDatabase::SkillDef *
CharacterClassDatabase::findSkill(const Ogre::String &name) const
{
	auto it = m_skills.find(name);
	if (it != m_skills.end())
		return &it->second;
	return nullptr;
}

const CharacterClassDatabase::NeedDef *
CharacterClassDatabase::findNeed(const Ogre::String &name) const
{
	auto it = m_needs.find(name);
	if (it != m_needs.end())
		return &it->second;
	return nullptr;
}

const CharacterClassDatabase::ClassDef *
CharacterClassDatabase::findClass(const Ogre::String &name) const
{
	auto it = m_classes.find(name);
	if (it != m_classes.end())
		return &it->second;
	return nullptr;
}

CharacterClassDatabase::StatDef *
CharacterClassDatabase::findStat(const Ogre::String &name)
{
	auto it = m_stats.find(name);
	if (it != m_stats.end())
		return &it->second;
	return nullptr;
}

CharacterClassDatabase::SkillDef *
CharacterClassDatabase::findSkill(const Ogre::String &name)
{
	auto it = m_skills.find(name);
	if (it != m_skills.end())
		return &it->second;
	return nullptr;
}

CharacterClassDatabase::NeedDef *
CharacterClassDatabase::findNeed(const Ogre::String &name)
{
	auto it = m_needs.find(name);
	if (it != m_needs.end())
		return &it->second;
	return nullptr;
}

CharacterClassDatabase::ClassDef *
CharacterClassDatabase::findClass(const Ogre::String &name)
{
	auto it = m_classes.find(name);
	if (it != m_classes.end())
		return &it->second;
	return nullptr;
}

// ---------------------------------------------------------------------------
// Mutators
// ---------------------------------------------------------------------------

void CharacterClassDatabase::addOrReplaceStat(const StatDef &def)
{
	bool wasNew = m_stats.find(def.name) == m_stats.end();
	m_stats[def.name] = def;
	if (wasNew)
		m_statNames.push_back(def.name);
}

void CharacterClassDatabase::addOrReplaceSkill(const SkillDef &def)
{
	bool wasNew = m_skills.find(def.name) == m_skills.end();
	m_skills[def.name] = def;
	if (wasNew)
		m_skillNames.push_back(def.name);
}

void CharacterClassDatabase::addOrReplaceNeed(const NeedDef &def)
{
	bool wasNew = m_needs.find(def.name) == m_needs.end();
	m_needs[def.name] = def;
	if (wasNew)
		m_needNames.push_back(def.name);
}

void CharacterClassDatabase::addOrReplaceClass(const ClassDef &def)
{
	bool wasNew = m_classes.find(def.name) == m_classes.end();
	m_classes[def.name] = def;
	if (wasNew)
		m_classNames.push_back(def.name);
}

bool CharacterClassDatabase::removeStat(const Ogre::String &name)
{
	if (m_stats.erase(name) == 0)
		return false;
	auto it = std::remove(m_statNames.begin(), m_statNames.end(),
			      name);
	m_statNames.erase(it, m_statNames.end());
	return true;
}

bool CharacterClassDatabase::removeSkill(const Ogre::String &name)
{
	if (m_skills.erase(name) == 0)
		return false;
	auto it = std::remove(m_skillNames.begin(), m_skillNames.end(),
			      name);
	m_skillNames.erase(it, m_skillNames.end());
	return true;
}

bool CharacterClassDatabase::removeNeed(const Ogre::String &name)
{
	if (m_needs.erase(name) == 0)
		return false;
	auto it = std::remove(m_needNames.begin(), m_needNames.end(),
			      name);
	m_needNames.erase(it, m_needNames.end());
	return true;
}

bool CharacterClassDatabase::removeClass(const Ogre::String &name)
{
	if (m_classes.erase(name) == 0)
		return false;
	auto it = std::remove(m_classNames.begin(), m_classNames.end(),
			      name);
	m_classNames.erase(it, m_classNames.end());
	return true;
}

void CharacterClassDatabase::clear()
{
	m_stats.clear();
	m_skills.clear();
	m_needs.clear();
	m_classes.clear();
	m_statNames.clear();
	m_skillNames.clear();
	m_needNames.clear();
	m_classNames.clear();
}

// ---------------------------------------------------------------------------
// Runtime helpers
// ---------------------------------------------------------------------------

int64_t CharacterClassDatabase::computeXPForLevel(int level,
						  const ClassDef &cls) const
{
	return static_cast<int64_t>(cls.xpForLevel.evaluate(level));
}

int CharacterClassDatabase::computePointsForLevel(int level,
						  const ClassDef &cls) const
{
	return static_cast<int>(cls.pointsPerLevel.evaluate(level));
}

int CharacterClassDatabase::computeStatGrowth(const Ogre::String &stat,
						      int level,
						      const ClassDef &cls) const
{
	auto it = cls.statGrowth.find(stat);
	if (it == cls.statGrowth.end())
		return 0;
	return static_cast<int>(it->second.evaluate(level));
}

int CharacterClassDatabase::computeSkillGrowth(const Ogre::String &skill,
						       int level,
						       const ClassDef &cls) const
{
	auto it = cls.skillGrowth.find(skill);
	if (it == cls.skillGrowth.end())
		return 0;
	return static_cast<int>(it->second.evaluate(level));
}

int CharacterClassDatabase::computeStatCost(int currentValue,
					    const ClassDef &cls) const
{
	return static_cast<int>(cls.statCost.evaluate(0, static_cast<double>(currentValue)));
}

// ---------------------------------------------------------------------------
// JSON serialization
// ---------------------------------------------------------------------------

bool CharacterClassDatabase::saveToJson(const std::string &filename)
{
	auto &db = getSingleton();
	nlohmann::json json;
	json["version"] = 1;

	// Stats
	json["stat_definitions"] = nlohmann::json::object();
	for (const auto &name : db.m_statNames) {
		const auto &def = db.m_stats.at(name);
		nlohmann::json j;
		j["display_name"] = def.displayName;
		j["kind"] = (def.kind == StatKind::Attribute) ? "attribute" :
					   "resource_pool";
		j["min"] = def.minValue;
		j["max"] = def.maxValue;
		json["stat_definitions"][name] = j;
	}

	// Skills
	json["skill_definitions"] = nlohmann::json::object();
	for (const auto &name : db.m_skillNames) {
		const auto &def = db.m_skills.at(name);
		nlohmann::json j;
		j["display_name"] = def.displayName;
		j["max"] = def.maxValue;
		json["skill_definitions"][name] = j;
	}

	// Needs
	json["need_definitions"] = nlohmann::json::object();
	for (const auto &name : db.m_needNames) {
		const auto &def = db.m_needs.at(name);
		nlohmann::json j;
		j["display_name"] = def.displayName;
		j["max"] = def.maxValue;
		j["accumulation_rate"] = def.accumulationRate;
		j["low_threshold"] = def.lowThreshold;
		j["high_threshold"] = def.highThreshold;
		j["bit_name"] = def.bitName;
		json["need_definitions"][name] = j;
	}

	// Classes
	json["classes"] = nlohmann::json::object();
	for (const auto &name : db.m_classNames) {
		const auto &cls = db.m_classes.at(name);
		nlohmann::json j;
		j["description"] = cls.description;
		j["primary_stats"] = cls.primaryStats;
		j["base_stats"] = cls.baseStats;
		j["base_skills"] = cls.baseSkills;
		j["base_needs"] = cls.baseNeeds;
		j["xp_for_level"] = cls.xpForLevel.getExpression();
		j["points_per_level"] = cls.pointsPerLevel.getExpression();
		j["stat_cost"] = cls.statCost.getExpression();
		j["stat_growth"] = nlohmann::json::object();
		for (const auto &pair : cls.statGrowth)
			j["stat_growth"][pair.first] =
				pair.second.getExpression();
		j["skill_growth"] = nlohmann::json::object();
		for (const auto &pair : cls.skillGrowth)
			j["skill_growth"][pair.first] =
				pair.second.getExpression();
		json["classes"][name] = j;
	}

	try {
		std::filesystem::path outPath =
			std::filesystem::current_path() / filename;
		std::ofstream f(outPath);
		if (!f.is_open()) {
			Ogre::LogManager::getSingleton().logMessage(
				"CharacterClassDatabase: Failed to open " +
				filename + " for writing");
			return false;
		}
		f << json.dump(4);
		return true;
	} catch (...) {
		Ogre::LogManager::getSingleton().logMessage(
			"CharacterClassDatabase: Exception saving " + filename);
		return false;
	}
}

bool CharacterClassDatabase::loadFromJson(const std::string &filename)
{
	auto &db = getSingleton();
	db.clear();

	std::filesystem::path inPath =
		std::filesystem::current_path() / filename;
	std::ifstream f(inPath);
	if (!f.is_open()) {
		Ogre::LogManager::getSingleton().logMessage(
			"CharacterClassDatabase: Could not load " + filename +
			", using defaults.");
		return false;
	}

	nlohmann::json json;
	try {
		f >> json;
	} catch (...) {
		Ogre::LogManager::getSingleton().logMessage(
			"CharacterClassDatabase: JSON parse error in " +
			filename);
		return false;
	}

	// Stats
	if (json.contains("stat_definitions") &&
	    json["stat_definitions"].is_object()) {
		for (auto &[key, val] : json["stat_definitions"].items()) {
			StatDef def;
			def.name = key;
			def.displayName = val.value("display_name", key);
			Ogre::String kindStr = val.value("kind", "attribute");
			def.kind = (kindStr == "resource_pool") ? StatKind::ResourcePool :
						    StatKind::Attribute;
			def.minValue = val.value("min", 1);
			def.maxValue = val.value("max", 999);
			db.addOrReplaceStat(def);
		}
	}

	// Skills
	if (json.contains("skill_definitions") &&
	    json["skill_definitions"].is_object()) {
		for (auto &[key, val] : json["skill_definitions"].items()) {
			SkillDef def;
			def.name = key;
			def.displayName = val.value("display_name", key);
			def.maxValue = val.value("max", 100);
			db.addOrReplaceSkill(def);
		}
	}

	// Needs
	if (json.contains("need_definitions") &&
	    json["need_definitions"].is_object()) {
		for (auto &[key, val] : json["need_definitions"].items()) {
			NeedDef def;
			def.name = key;
			def.displayName = val.value("display_name", key);
			def.maxValue = val.value("max", 1000);
			def.accumulationRate = val.value("accumulation_rate", 0.0f);
			def.lowThreshold = val.value("low_threshold", 0);
			def.highThreshold = val.value("high_threshold", 1000);
			def.bitName = val.value("bit_name", "");
			// Backward compat: old files used low_bit_name / high_bit_name
			if (def.bitName.empty()) {
				def.bitName = val.value("high_bit_name", "");
				if (def.bitName.empty())
					def.bitName = val.value("low_bit_name", "");
			}
			db.addOrReplaceNeed(def);
		}
	}

	// Classes
	if (json.contains("classes") && json["classes"].is_object()) {
		for (auto &[key, val] : json["classes"].items()) {
			ClassDef cls;
			cls.name = key;
			cls.description = val.value("description", "");
			if (val.contains("primary_stats") &&
			    val["primary_stats"].is_array()) {
				for (const auto &item : val["primary_stats"])
					cls.primaryStats.push_back(
						item.get<std::string>());
			}
			if (val.contains("base_stats") &&
			    val["base_stats"].is_object()) {
				for (auto &[k, v] : val["base_stats"].items())
					cls.baseStats[k] = v.get<int>();
			}
			if (val.contains("base_skills") &&
			    val["base_skills"].is_object()) {
				for (auto &[k, v] : val["base_skills"].items())
					cls.baseSkills[k] = v.get<int>();
			}
			if (val.contains("base_needs") &&
			    val["base_needs"].is_object()) {
				for (auto &[k, v] : val["base_needs"].items())
					cls.baseNeeds[k] = v.get<int>();
			}
			cls.xpForLevel = Formula(val.value("xp_for_level", "0"));
			cls.pointsPerLevel =
				Formula(val.value("points_per_level", "0"));
			cls.statCost = Formula(val.value("stat_cost", "1"));
			if (val.contains("stat_growth") &&
			    val["stat_growth"].is_object()) {
				for (auto &[k, v] : val["stat_growth"].items())
					cls.statGrowth[k] =
						Formula(v.get<std::string>());
			}
			if (val.contains("skill_growth") &&
			    val["skill_growth"].is_object()) {
				for (auto &[k, v] : val["skill_growth"].items())
					cls.skillGrowth[k] =
						Formula(v.get<std::string>());
			}
			db.addOrReplaceClass(cls);
		}
	}

	return true;
}
