#include "ActionDatabase.hpp"
#ifndef OGRE_STUB_H
#include <OgreLogManager.h>
#include <OgreResourceGroupManager.h>
#endif

#include <flecs.h>
#include <nlohmann/json.hpp>
#include <fstream>
#include <filesystem>

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

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

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

// ---------------------------------------------------------------------------
// Find methods
// ---------------------------------------------------------------------------

const GoapAction *ActionDatabase::findAction(const Ogre::String &name) const
{
	for (const auto &action : actions) {
		if (action.name == name)
			return &action;
	}
	return nullptr;
}

GoapAction *ActionDatabase::findAction(const Ogre::String &name)
{
	for (auto &action : actions) {
		if (action.name == name)
			return &action;
	}
	return nullptr;
}

const GoapGoal *ActionDatabase::findGoal(const Ogre::String &name) const
{
	for (const auto &goal : goals) {
		if (goal.name == name)
			return &goal;
	}
	return nullptr;
}

GoapGoal *ActionDatabase::findGoal(const Ogre::String &name)
{
	for (auto &goal : goals) {
		if (goal.name == name)
			return &goal;
	}
	return nullptr;
}

// ---------------------------------------------------------------------------
// Add or replace
// ---------------------------------------------------------------------------

void ActionDatabase::addOrReplaceAction(const GoapAction &action)
{
	for (auto &a : actions) {
		if (a.name == action.name) {
			a = action;
			return;
		}
	}
	actions.push_back(action);
}

void ActionDatabase::addOrReplaceGoal(const GoapGoal &goal)
{
	for (auto &g : goals) {
		if (g.name == goal.name) {
			g = goal;
			return;
		}
	}
	goals.push_back(goal);
}

// ---------------------------------------------------------------------------
// Remove methods
// ---------------------------------------------------------------------------

bool ActionDatabase::removeAction(const Ogre::String &name)
{
	for (auto it = actions.begin(); it != actions.end(); ++it) {
		if (it->name == name) {
			actions.erase(it);
			return true;
		}
	}
	return false;
}

bool ActionDatabase::removeGoal(const Ogre::String &name)
{
	for (auto it = goals.begin(); it != goals.end(); ++it) {
		if (it->name == name) {
			goals.erase(it);
			return true;
		}
	}
	return false;
}

// ---------------------------------------------------------------------------
// Selection / validation
// ---------------------------------------------------------------------------

const GoapGoal *
ActionDatabase::selectBestGoal(const GoapBlackboard &blackboard) const
{
	const GoapGoal *best = nullptr;
	int bestPriority = -1;

	for (const auto &goal : goals) {
		if (!goal.isValid(blackboard))
			continue;
		if (goal.priority > bestPriority) {
			bestPriority = goal.priority;
			best = &goal;
		}
	}
	return best;
}

std::vector<const GoapAction *>
ActionDatabase::getValidActions(const GoapBlackboard &blackboard) const
{
	std::vector<const GoapAction *> result;
	for (const auto &action : actions) {
		if (action.canRun(blackboard))
			result.push_back(&action);
	}
	return result;
}

// ---------------------------------------------------------------------------
// Clear
// ---------------------------------------------------------------------------

void ActionDatabase::clear()
{
	actions.clear();
	goals.clear();
}

// ---------------------------------------------------------------------------
// ActionDatabaseComponent
// ---------------------------------------------------------------------------

void ActionDatabaseComponent::syncToSingleton() const
{
	auto &db = ActionDatabase::getSingleton();
	db.clear();
	for (const auto &action : actions)
		db.addOrReplaceAction(action);
	for (const auto &goal : goals)
		db.addOrReplaceGoal(goal);
}

// ---------------------------------------------------------------------------
// JSON serialization helpers (local to this translation unit)
// ---------------------------------------------------------------------------

static nlohmann::json serializeGoapBlackboard(const GoapBlackboard &bb)
{
	nlohmann::json json;
	json["bits"] = (uint64_t)bb.bits;
	json["mask"] = (uint64_t)bb.mask;
	if (bb.bitmask != ~0ULL)
		json["bitmask"] = (uint64_t)bb.bitmask;
	if (!bb.values.empty()) {
		json["values"] = nlohmann::json::object();
		for (const auto &pair : bb.values)
			json["values"][pair.first] = pair.second;
	}
	if (!bb.floatValues.empty()) {
		json["floatValues"] = nlohmann::json::object();
		for (const auto &pair : bb.floatValues)
			json["floatValues"][pair.first] = pair.second;
	}
	if (!bb.vec3Values.empty()) {
		json["vec3Values"] = nlohmann::json::object();
		for (const auto &pair : bb.vec3Values) {
			nlohmann::json v;
			v.push_back(pair.second.x);
			v.push_back(pair.second.y);
			v.push_back(pair.second.z);
			json["vec3Values"][pair.first] = v;
		}
	}
	return json;
}

static void deserializeGoapBlackboard(GoapBlackboard &bb,
				      const nlohmann::json &json)
{
	bb.bits = json.value("bits", (uint64_t)0);
	bb.mask = json.value("mask", (uint64_t)0);
	bb.bitmask = json.value("bitmask", ~0ULL);
	bb.values.clear();
	if (json.contains("values") && json["values"].is_object()) {
		for (auto &[key, val] : json["values"].items())
			bb.values[key] = val.get<int>();
	}
	bb.floatValues.clear();
	if (json.contains("floatValues") && json["floatValues"].is_object()) {
		for (auto &[key, val] : json["floatValues"].items())
			bb.floatValues[key] = val.get<float>();
	}
	bb.vec3Values.clear();
	if (json.contains("vec3Values") && json["vec3Values"].is_object()) {
		for (auto &[key, val] : json["vec3Values"].items()) {
			if (val.is_array() && val.size() >= 3)
				bb.vec3Values[key] =
					Ogre::Vector3(val[0].get<float>(),
						      val[1].get<float>(),
						      val[2].get<float>());
		}
	}
}

static nlohmann::json serializeBehaviorTreeNode(const BehaviorTreeNode &node)
{
	nlohmann::json json;
	json["type"] = node.type;
	if (!node.name.empty())
		json["name"] = node.name;
	if (!node.params.empty())
		json["params"] = node.params;
	if (!node.children.empty()) {
		json["children"] = nlohmann::json::array();
		for (const auto &child : node.children)
			json["children"].push_back(
				serializeBehaviorTreeNode(child));
	}
	return json;
}

static void deserializeBehaviorTreeNode(BehaviorTreeNode &node,
					const nlohmann::json &json)
{
	node.type = json.value("type", "task");
	node.name = json.value("name", "");
	node.params = json.value("params", "");
	node.children.clear();
	if (json.contains("children") && json["children"].is_array()) {
		for (const auto &childJson : json["children"]) {
			BehaviorTreeNode child;
			deserializeBehaviorTreeNode(child, childJson);
			node.children.push_back(child);
		}
	}
}

static nlohmann::json serializeGoapAction(const GoapAction &action)
{
	nlohmann::json json;
	json["name"] = action.name;
	json["cost"] = action.cost;
	json["preconditions"] = serializeGoapBlackboard(action.preconditions);
	json["effects"] = serializeGoapBlackboard(action.effects);
	if (action.preconditionMask != ~0ULL)
		json["preconditionMask"] = action.preconditionMask;
	json["behaviorTree"] = serializeBehaviorTreeNode(action.behaviorTree);
	if (!action.behaviorTreeName.empty())
		json["behaviorTreeName"] = action.behaviorTreeName;
	return json;
}

static void deserializeGoapAction(GoapAction &action,
				  const nlohmann::json &json)
{
	action.name = json.value("name", "Unnamed");
	action.cost = json.value("cost", 1);
	if (json.contains("preconditions"))
		deserializeGoapBlackboard(action.preconditions,
					  json["preconditions"]);
	if (json.contains("effects"))
		deserializeGoapBlackboard(action.effects, json["effects"]);
	action.preconditionMask = json.value("preconditionMask", ~0ULL);
	if (json.contains("behaviorTree"))
		deserializeBehaviorTreeNode(action.behaviorTree,
					    json["behaviorTree"]);
	action.behaviorTreeName = json.value("behaviorTreeName", "");
}

static nlohmann::json serializeGoapGoal(const GoapGoal &goal)
{
	nlohmann::json json;
	json["name"] = goal.name;
	json["priority"] = goal.priority;
	json["target"] = serializeGoapBlackboard(goal.target);
	if (!goal.condition.empty())
		json["condition"] = goal.condition;
	return json;
}

static void deserializeGoapGoal(GoapGoal &goal, const nlohmann::json &json)
{
	goal.name = json.value("name", "Unnamed");
	goal.priority = json.value("priority", 1);
	if (json.contains("target"))
		deserializeGoapBlackboard(goal.target, json["target"]);
	goal.condition = json.value("condition", "");
}

// ---------------------------------------------------------------------------
// saveToJson / loadFromJson
// ---------------------------------------------------------------------------

bool ActionDatabase::saveToJson(const std::string &filename)
{
	try {
		// Resolve the filesystem path from the "General" resource group
		Ogre::ResourceGroupManager &rgm =
			Ogre::ResourceGroupManager::getSingleton();
		const Ogre::ResourceGroupManager::LocationList &locations =
			rgm.getResourceLocationList("General");
		if (locations.empty()) {
			Ogre::LogManager::getSingleton().logMessage(
				"ActionDatabase::saveToJson: "
				"no resource locations for group 'General'");
			return false;
		}
		// Use the first location's path
		std::string dir = locations.begin()->archive->getName();

		std::string filepath = dir + "/" + filename;

		// Backup existing file
		if (std::filesystem::exists(filepath)) {
			std::string backup = filepath + ".bak";
			try {
				std::filesystem::copy_file(
					filepath, backup,
					std::filesystem::copy_options::
						overwrite_existing);
			} catch (const std::exception &e) {
				Ogre::LogManager::getSingleton().logMessage(
					"ActionDatabase::saveToJson: "
					"backup failed: " +
					Ogre::String(e.what()));
			}
		}

		const ActionDatabase &db = getSingleton();
		nlohmann::json root;

		root["actions"] = nlohmann::json::array();
		for (const auto &action : db.actions)
			root["actions"].push_back(serializeGoapAction(action));

		root["goals"] = nlohmann::json::array();
		for (const auto &goal : db.goals)
			root["goals"].push_back(serializeGoapGoal(goal));

		// Save bit names
		nlohmann::json bitNames = nlohmann::json::array();
		for (int i = 0; i < 64; i++) {
			const char *name = GoapBlackboard::getBitName(i);
			if (name) {
				nlohmann::json entry;
				entry["index"] = i;
				entry["name"] = name;
				bitNames.push_back(entry);
			}
		}
		if (!bitNames.empty())
			root["bitNames"] = bitNames;

		std::ofstream file(filepath);
		if (!file.is_open()) {
			Ogre::LogManager::getSingleton().logMessage(
				"ActionDatabase::saveToJson: "
				"failed to open " +
				filepath);
			return false;
		}
		file << root.dump(4);
		file.close();

		Ogre::LogManager::getSingleton().logMessage(
			"ActionDatabase saved to " + filepath);
		return true;

	} catch (const std::exception &e) {
		Ogre::LogManager::getSingleton().logMessage(
			"ActionDatabase::saveToJson error: " +
			Ogre::String(e.what()));
		return false;
	}
}

bool ActionDatabase::loadFromJson(const std::string &filename)
{
	try {
		// Resolve the filesystem path from the "General" resource group
		Ogre::ResourceGroupManager &rgm =
			Ogre::ResourceGroupManager::getSingleton();
		const Ogre::ResourceGroupManager::LocationList &locations =
			rgm.getResourceLocationList("General");
		if (locations.empty()) {
			Ogre::LogManager::getSingleton().logMessage(
				"ActionDatabase::loadFromJson: "
				"no resource locations for group 'General'");
			return false;
		}
		std::string dir = locations.begin()->archive->getName();

		std::string filepath = dir + "/" + filename;

		if (!std::filesystem::exists(filepath)) {
			Ogre::LogManager::getSingleton().logMessage(
				"ActionDatabase::loadFromJson: "
				"file not found: " +
				filepath);
			return false;
		}

		std::ifstream file(filepath);
		if (!file.is_open()) {
			Ogre::LogManager::getSingleton().logMessage(
				"ActionDatabase::loadFromJson: "
				"failed to open " +
				filepath);
			return false;
		}

		nlohmann::json root;
		try {
			file >> root;
		} catch (const nlohmann::json::parse_error &e) {
			Ogre::LogManager::getSingleton().logMessage(
				"ActionDatabase::loadFromJson: "
				"JSON parse error in " +
				filepath + ": " + Ogre::String(e.what()));
			return false;
		}
		file.close();

		ActionDatabase &db = getSingleton();

		// Load actions (add/replace)
		if (root.contains("actions") && root["actions"].is_array()) {
			for (const auto &actionJson : root["actions"]) {
				GoapAction action;
				deserializeGoapAction(action, actionJson);
				db.addOrReplaceAction(action);
			}
		}

		// Load goals (add/replace)
		if (root.contains("goals") && root["goals"].is_array()) {
			for (const auto &goalJson : root["goals"]) {
				GoapGoal goal;
				deserializeGoapGoal(goal, goalJson);
				db.addOrReplaceGoal(goal);
			}
		}

		// Load bit names
		if (root.contains("bitNames") && root["bitNames"].is_array()) {
			for (const auto &entry : root["bitNames"]) {
				if (entry.contains("index") &&
				    entry.contains("name"))
					GoapBlackboard::setBitName(
						entry["index"].get<int>(),
						entry["name"]
							.get<std::string>());
			}
		}

		Ogre::LogManager::getSingleton().logMessage(
			"ActionDatabase loaded from " + filepath);
		return true;

	} catch (const std::exception &e) {
		Ogre::LogManager::getSingleton().logMessage(
			"ActionDatabase::loadFromJson error: " +
			Ogre::String(e.what()));
		return false;
	}
}

// ---------------------------------------------------------------------------
// reloadFromSceneComponents
// ---------------------------------------------------------------------------

void ActionDatabase::reloadFromSceneComponents(flecs::world &world)
{
	// First, load from file (if available) — this is done by the caller
	// before calling this function.  Here we just re-sync from scene
	// entities so that scene-defined actions are applied on top.

	// Iterate all entities with ActionDatabaseComponent
	world.each([](flecs::entity e, ActionDatabaseComponent &dbComp) {
		(void)e;
		dbComp.syncToSingleton();
	});
}
