/**
 * Copyright 2022-2025 Theai, Inc. dba Inworld AI
 */

#include "Graph/Nodes/InworldNode_Goals.h"

#include "Async/Async.h"
#include "Graph/InworldCharacterData.h"
#include "Graph/InworldGraph.h"
#include "Graph/RuntimeData/InworldGraphRuntimeData_Goals.h"
#include "Internationalization/Regex.h"
#include "InworldBlueprintFunctionLibrary.h"
#include "InworldCharacterBFL.h"
#include "InworldCharacterTypes.h"
#include "Misc/TVariant.h"

UInworldNode_Goals::UInworldNode_Goals()
{
	NodeName = "Goals";
}

FInworldCharacterAction UInworldNode_Goals::Process_Action(
	const FInworldCharacterAction& Action, const FInworldGoalsData& GoalsData) const
{
	return Action;
}

FInworldCharacterAction UInworldNode_Goals::Process_MatchedIntents(const UInworldProcessContext* ProcessContext,
	const FInworldData_MatchedIntents& MatchedIntents, const FInworldGoalsData& Goals) const
{
	if (MatchedIntents.IntentMatches.Num() > 0)
	{
		FInworldIntentMatch FirstIntentMatch = MatchedIntents.IntentMatches[0];
		for (const TTuple<FName, FInworldGoal>& GoalsMapEntry : Goals.GoalsMap)
		{
			FName GoalName = GoalsMapEntry.Key;
			const FInworldGoal& Goal = GoalsMapEntry.Value;
			if (Goal.Intents.Contains(FirstIntentMatch.IntentId))
			{
				TMap<FString, FString> Parameters;
				if (!IsInGameThread())
				{
					auto Event{FGenericPlatformProcess::GetSynchEventFromPool()};
					AsyncTask(ENamedThreads::GameThread,
						[Event, GoalName, Parameters = &Parameters, GoalParameterInterface = Goals.GoalParameterInterface]()
						{
							if (IsValid(GoalParameterInterface.GetObject()))
							{
								GoalParameterInterface->GetGoalParameters(GoalName, *Parameters);
							}
							Event->Trigger();
						});
					Event->Wait();
					FGenericPlatformProcess::ReturnSynchEventToPool(Event);
				}
				else
				{
					if (IsValid(Goals.GoalParameterInterface.GetObject()))
					{
						Goals.GoalParameterInterface->GetGoalParameters(GoalName, Parameters);
					}
				}
				// Default parameters
				if (FInworldPlayerProfile PlayerProfile;
					ProcessContext->GetRuntimeStruct(PlayerProfileRuntimeDataKey, PlayerProfile))
				{
					if (!Parameters.Contains("player"))
					{
						Parameters.Add("player", PlayerProfile.Name);
					}
				}
				if (FInworldCharacterProfile CharacterProfile;
					ProcessContext->GetRuntimeStruct(CharacterProfileRuntimeDataKey, CharacterProfile))
				{
					if (!Parameters.Contains("character"))
					{
						Parameters.Add("character", CharacterProfile.Name);
					}
				}

				return CreateActionData(GoalName, Goals.GoalsMap[GoalName], Parameters);
			}
		}
		UE_LOG(LogInworldGraph, Warning, TEXT("UInworldNode_Goals::Process_MatchedIntents - No goal for matched intent: %s"),
			*FirstIntentMatch.IntentId.ToString());

		FInworldCharacterAction EmptyAction;
		EmptyAction.ResponseType = EInworldGoalResponseType::NONE;
		return EmptyAction;
	}
	else
	{
		FInworldCharacterAction EmptyAction;
		EmptyAction.ResponseType = EInworldGoalResponseType::NONE;
		return EmptyAction;
	}
}

FInworldCharacterAction UInworldNode_Goals::Process_Trigger(
	const UInworldProcessContext* ProcessContext, const FInworldCharacterTrigger& Trigger, const FInworldGoalsData& Goals) const
{
	if (Goals.GoalsMap.Contains(Trigger.GoalName))
	{
		// Default parameters
		TMap<FString, FString> Parameters = {Trigger.Parameters};
		if (FInworldPlayerProfile PlayerProfile; ProcessContext->GetRuntimeStruct(PlayerProfileRuntimeDataKey, PlayerProfile))
		{
			if (!Parameters.Contains("player"))
			{
				Parameters.Add("player", PlayerProfile.Name);
			}
		}
		if (FInworldCharacterProfile CharacterProfile;
			ProcessContext->GetRuntimeStruct(CharacterProfileRuntimeDataKey, CharacterProfile))
		{
			if (!Parameters.Contains("character"))
			{
				Parameters.Add("character", CharacterProfile.Name);
			}
		}
		return CreateActionData(Trigger.GoalName, Goals.GoalsMap[Trigger.GoalName], Parameters);
	}
	else
	{
		UE_LOG(LogInworldGraph, Warning, TEXT("UInworldNode_Goals::Process_Trigger - No goal with the name: %s"),
			*Trigger.GoalName.ToString());

		FInworldCharacterAction EmptyAction;
		EmptyAction.ResponseType = EInworldGoalResponseType::NONE;
		return EmptyAction;
	}
}

FInworldCharacterAction UInworldNode_Goals::CreateActionData(
	FName GoalName, const FInworldGoal& Goal, const TMap<FString, FString>& Parameters) const
{
	FInworldCharacterAction Action;
	Action.GoalName = GoalName;
	Action.ResponseType = Goal.ResponseType;
	switch (Action.ResponseType)
	{
		case EInworldGoalResponseType::INSTRUCTION:
			Action.Action = FormatParameters(Goal.Instruction, Parameters);
			break;
		case EInworldGoalResponseType::VERBATIM:
			Action.Action = FormatParameters(Goal.Verbatim, Parameters);
			break;
	}
	return Action;
}

FString UInworldNode_Goals::FormatParameters(FString Action, const TMap<FString, FString>& Parameters)
{
	for (const TTuple<FString, FString>& Parameter : Parameters)
	{
		FString SearchToken =
			TEXT("(?i)\\{\\{\\s*") + Parameter.Key + TEXT("\\s*\\}\\}");	// Regex pattern with optional whitespace
		FString ReplaceValue = Parameter.Value;

		FRegexPattern Pattern(SearchToken);
		FRegexMatcher Matcher(Pattern, Action);

		TArray<int32> MatchBeginnings;
		TArray<int32> MatchLengths;

		// Collect all matches and their positions
		while (Matcher.FindNext())
		{
			MatchBeginnings.Add(Matcher.GetMatchBeginning());
			MatchLengths.Add(Matcher.GetMatchEnding() - Matcher.GetMatchBeginning());
		}

		// Perform replacements from end to start
		for (int32 i = MatchBeginnings.Num() - 1; i >= 0; --i)
		{
			int32 MatchBeginning = MatchBeginnings[i];
			int32 MatchLength = MatchLengths[i];
			Action = Action.Left(MatchBeginning) + ReplaceValue + Action.RightChop(MatchBeginning + MatchLength);
		}
	}

	// Find and remove any leftover tokens that were not replaced
	FRegexPattern LeftoverPattern(TEXT("\\{\\{.*?\\}\\}"));	   // Regex pattern to match anything between {{ and }}
	FRegexMatcher LeftoverMatcher(LeftoverPattern, Action);

	while (LeftoverMatcher.FindNext())
	{
		Action = Action.Left(LeftoverMatcher.GetMatchBeginning()) + Action.RightChop(LeftoverMatcher.GetMatchEnding());
		LeftoverMatcher = FRegexMatcher(LeftoverPattern, Action);
	}

	return Action;
}
