#include "GoapPlannerSystem.hpp"
#include "../components/GoapPlanner.hpp"
#include "../components/GoapBlackboard.hpp"
#include "../components/ActionDatabase.hpp"
#include "../components/ActionDebug.hpp"
#include "../components/GoapAction.hpp"
#include "../components/GoapGoal.hpp"
#include "../EditorApp.hpp"
#include <OgreLogManager.h>
#include <queue>
#include <functional>
#include <unordered_map>

namespace {

struct PlannerNode {
	GoapBlackboard state;
	std::vector<Ogre::String> actions;
	int cost = 0;

	int heuristic(const GoapBlackboard &goal) const
	{
		(void)goal;
		return 0;
	}

	int f(const GoapBlackboard &goal) const
	{
		return cost + heuristic(goal);
	}
};

struct NodeCompare {
	const GoapBlackboard *goal;
	bool operator()(const PlannerNode &a, const PlannerNode &b) const
	{
		return a.f(*goal) > b.f(*goal);
	}
};

} // namespace

GoapPlannerSystem::GoapPlannerSystem(flecs::world &world)
	: m_world(world)
{
}

GoapPlannerSystem::~GoapPlannerSystem() = default;


size_t GoapPlannerSystem::hashState(const GoapBlackboard &bb)
{
	std::hash<uint64_t> hasher;
	size_t h = hasher(bb.bits);
	h ^= hasher(bb.mask) + 0x9e3779b9 + (h << 6) + (h >> 2);
	h ^= hasher(bb.bitmask) + 0x9e3779b9 + (h << 6) + (h >> 2);
	for (const auto &pair : bb.values) {
		h ^= std::hash<std::string>{}(pair.first) + 0x9e3779b9 +
		    (h << 6) + (h >> 2);
		h ^= std::hash<int>{}(pair.second) + 0x9e3779b9 + (h << 6) +
		    (h >> 2);
	}
	return h;
}

bool GoapPlannerSystem::statesEqual(const GoapBlackboard &a,
				    const GoapBlackboard &b)
{
	return a.bits == b.bits && a.mask == b.mask &&
	       a.bitmask == b.bitmask && a.values == b.values;
}

std::vector<const GoapAction *> GoapPlannerSystem::resolveActions(
	const GoapPlannerComponent &planner, const ActionDatabase *db)
{
	std::vector<const GoapAction *> result;
	if (!db)
		return result;

	for (const auto &name : planner.actionNames) {
		const GoapAction *action = db->findAction(name);
		if (action)
			result.push_back(action);
	}
	return result;
}

const GoapGoal *GoapPlannerSystem::selectGoal(
	const GoapPlannerComponent &planner, const ActionDatabase *db,
	const GoapBlackboard &blackboard)
{
	if (!db)
		return nullptr;

	const GoapGoal *best = nullptr;
	int bestPriority = -1;

	for (const auto &name : planner.goalNames) {
		const GoapGoal *goal = db->findGoal(name);
		if (!goal)
			continue;
		if (!goal->isValid(blackboard))
			continue;
		if (goal->priority > bestPriority) {
			bestPriority = goal->priority;
			best = goal;
		}
	}
	return best;
}

void GoapPlannerSystem::planForEntity(flecs::entity e,
				      GoapPlannerComponent &planner,
				      const GoapBlackboard &blackboard)
{
	(void)e;

	// Find ActionDatabase singleton
	const ActionDatabase *db = ActionDatabase::getSingletonPtr();

	// Select best valid goal
	const GoapGoal *goal = selectGoal(planner, db, blackboard);
	if (!goal) {
		planner.status = GoapPlannerComponent::Status::NoPlanFound;
		planner.planDirty = false;
		planner.currentGoalName.clear();
		Ogre::LogManager::getSingleton().logMessage(
			"[GoapPlanner] No valid goal found.");
		return;
	}

	planner.currentGoalName = goal->name;

	// Resolve actions
	std::vector<const GoapAction *> actions =
		resolveActions(planner, db);

	if (actions.empty()) {
		planner.status = GoapPlannerComponent::Status::NoPlanFound;
		planner.planDirty = false;
		Ogre::LogManager::getSingleton().logMessage(
			"[GoapPlanner] No actions available for planning.");
		return;
	}

	// A* search
	NodeCompare cmp{&goal->target};
	std::priority_queue<PlannerNode, std::vector<PlannerNode>,
			    NodeCompare>
		openSet(cmp);

	// Closed set: map from state hash -> best cost seen for that state
	std::unordered_map<size_t, int> closedSet;

	// Track found plans to avoid duplicates
	std::unordered_set<std::string> foundPlanSignatures;

	PlannerNode start;
	start.state = blackboard;
	start.cost = 0;
	openSet.push(start);

	planner.nodesExplored = 0;
	planner.plansGenerated = 0;
	int maxNodes = 10000; // safety limit

	while (!openSet.empty() && planner.plansGenerated < planner.maxPlans &&
	       planner.nodesExplored < maxNodes) {
		PlannerNode current = openSet.top();
		openSet.pop();
		planner.nodesExplored++;

		// Check if goal is satisfied
		if (current.state.satisfies(goal->target)) {
			// Build plan signature for deduplication
			std::string sig;
			for (const auto &name : current.actions)
				sig += name + ",";
			if (foundPlanSignatures.insert(sig).second) {
				GoapPlannerComponent::Plan plan;
				plan.actions = current.actions;
				plan.totalCost = current.cost;
				plan.goalName = goal->name;
				planner.planQueue.push_back(std::move(plan));
				planner.plansGenerated++;
			}
			continue;
		}

		// Check closed set
		size_t stateHash = hashState(current.state);
		auto it = closedSet.find(stateHash);
		if (it != closedSet.end() && it->second <= current.cost)
			continue;
		closedSet[stateHash] = current.cost;

		// Expand: try each action
		for (const GoapAction *action : actions) {
			if (!action)
				continue;
			if (!action->canRun(current.state))
				continue;

			PlannerNode next = current;
			next.state.apply(action->effects);
			next.actions.push_back(action->name);
			next.cost += action->cost;

			openSet.push(std::move(next));
		}
	}

	if (planner.plansGenerated > 0) {
		planner.status =
			GoapPlannerComponent::Status::PlansAvailable;
		Ogre::LogManager::getSingleton().logMessage(
			"[GoapPlanner] Generated " +
			Ogre::StringConverter::toString(
				planner.plansGenerated) +
			" plan(s) for goal: " + goal->name);
	} else {
		planner.status =
			GoapPlannerComponent::Status::NoPlanFound;
		Ogre::LogManager::getSingleton().logMessage(
			"[GoapPlanner] No plan found for goal: " +
			goal->name);
	}

	planner.planDirty = false;
}

void GoapPlannerSystem::update(float deltaTime)
{
	(void)deltaTime;

	bool shouldRun = true;
	if (m_editorApp) {
		if (m_editorApp->getGameMode() == EditorApp::GameMode::Game &&
		    m_editorApp->getGamePlayState() !=
			    EditorApp::GamePlayState::Playing) {
			shouldRun = false;
		}
	}
	if (!shouldRun)
		return;

	m_world
		.query<GoapPlannerComponent, GoapBlackboard>()
		.each([&](flecs::entity e, GoapPlannerComponent &planner,
			  GoapBlackboard &blackboard) {
			(void)e;
			if (!planner.planDirty)
				return;

			// Clear old plans before replanning
			planner.clearPlans();
			planner.status =
				GoapPlannerComponent::Status::Planning;
			planForEntity(e, planner, blackboard);
		});
}
