#include "MoALiveLinkSource.h"
#include "MoALiveLinkSettings.h"
#include "ILiveLinkClient.h"
#include "Roles/LiveLinkAnimationTypes.h"
#include "Roles/LiveLinkAnimationRole.h"
#include "Roles/LiveLinkTransformTypes.h"
#include "Roles/LiveLinkTransformRole.h"
#include "Misc/ByteSwap.h"

#include "Common/UdpSocketBuilder.h"

#define LOCTEXT_NAMESPACE "MoALiveLinkSource"
#define RECV_BUFFER_SIZE 1024 * 1024

void ParseBlobDataToFloats(const uint8* DataPtr, int32 BlobSize, TArray<float>& OutFloatArray);

MoALiveLinkSource::MoALiveLinkSource(FIPv4Endpoint Endpoint)
	:Socket(nullptr)
	, Stopping(false)
	, Thread(nullptr)
	, WaitTime(FTimespan::FromMilliseconds(100))
{
	DeviceEndpoint = Endpoint;

	UE_LOG(LogTemp, Log, TEXT("[MoA Init] Initializing MoA LiveLink Source on Port: %d"), DeviceEndpoint.Port);

	SourceStatus = LOCTEXT("SourceStatus_DeviceNotFound", "Device Not Found");
	SourceType = LOCTEXT("MoALiveLinkSourceType", "MoA LiveLink");
	SourceMachineName = FText::FromString(DeviceEndpoint.ToString());

	LastCleanupCheck = FDateTime::UtcNow();

	Socket = FUdpSocketBuilder(TEXT("MoASocket"))
		.AsNonBlocking()
		.AsReusable()
		.BoundToAddress(FIPv4Address::Any)
		.BoundToPort(DeviceEndpoint.Port) 
		.WithReceiveBufferSize(RECV_BUFFER_SIZE);

	RecvBuffer.SetNumUninitialized(RECV_BUFFER_SIZE);
	if ((Socket != nullptr) && (Socket->GetSocketType() == SOCKTYPE_Datagram))
	{
		UE_LOG(LogTemp, Log, TEXT("[MoA Init] Socket created successfully"));
		SocketSubsystem = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM);
		Start();
		SourceStatus = LOCTEXT("SourceStatus_Receiving", "Receiving");
	}
	else
	{
		UE_LOG(LogTemp, Error, TEXT("[MoA Init] Failed to create socket! Socket: %s"), 
			Socket ? TEXT("Valid") : TEXT("Null"));
	}
}

MoALiveLinkSource::~MoALiveLinkSource()
{
	Stopping = true;

	if (Thread != nullptr)
	{
		Thread->WaitForCompletion();
		delete Thread;
		Thread = nullptr;
	}

	if (Socket != nullptr)
	{
		Socket->Close();
		ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->DestroySocket(Socket);
		Socket = nullptr;
	}
}

void MoALiveLinkSource::ReceiveClient(ILiveLinkClient* InClient, FGuid InSourceGuid)
{
	Client = InClient; 
	SourceGuid = InSourceGuid;
}

bool MoALiveLinkSource::IsSourceStillValid() const
{
	bool bIsSourceValid = !Stopping && Thread != nullptr && Socket != nullptr;
	return bIsSourceValid;
}

bool MoALiveLinkSource::RequestSourceShutdown()
{
	Stopping = true;
	
	// Only remove from saved connections if editor is NOT shutting down
	// This prevents losing saved connections when editor closes
	if (!IsEngineExitRequested())
	{
		UMoALiveLinkSettings* Settings = UMoALiveLinkSettings::Get();
		if (Settings)
		{
			Settings->RemoveConnection(DeviceEndpoint);
			UE_LOG(LogTemp, Log, TEXT("[MoA LiveLink] Source manually deleted, removed from settings: %s"), *DeviceEndpoint.ToString());
		}
	}
	else
	{
		UE_LOG(LogTemp, Log, TEXT("[MoA LiveLink] Editor shutting down, keeping saved connection: %s"), *DeviceEndpoint.ToString());
	}
	
	if (Thread != nullptr)
	{
		Thread->WaitForCompletion();
	}
	
	return true;
}

uint32 MoALiveLinkSource::Run()
{
	TSharedRef<FInternetAddr> Sender = SocketSubsystem->CreateInternetAddr();
	
	UE_LOG(LogTemp, Log, TEXT("[MoA Socket] UDP Receiver thread started on port %d"), DeviceEndpoint.Port);

	while (!Stopping)
		{
			FDateTime Now = FDateTime::UtcNow();
			if ((Now - LastCleanupCheck).GetTotalSeconds() >= 1.0)
			{
				CleanupInactiveSubjects();
				LastCleanupCheck = Now;
			}

			if (Socket->Wait(ESocketWaitConditions::WaitForRead, WaitTime))
			{
				uint32 Size;

			while (Socket->HasPendingData(Size))
			{
				int32 Read = 0;
				if (Socket->RecvFrom(RecvBuffer.GetData(), RecvBuffer.Num(), Read, *Sender))
				{
					if (Read > 0)
					{
						TSharedPtr<TArray<uint8>, ESPMode::ThreadSafe> ReceivedData = MakeShareable(new TArray<uint8>());
						ReceivedData->SetNumUninitialized(Read);
						memcpy(ReceivedData->GetData(), RecvBuffer.GetData(), Read);

						ILiveLinkClient* CapturedClient = Client;
						FGuid CapturedGuid = SourceGuid;

						AsyncTask(ENamedThreads::GameThread, [this, ReceivedData, CapturedClient, CapturedGuid]()
						{
							if (!Stopping && CapturedClient)
							{
								ProcessReceivedData(ReceivedData);
							}
						});
					}
				}
				else
				{
					UE_LOG(LogTemp, Error, TEXT("[MoA Socket] RecvFrom failed"));
				}
			}
		}
		}
	
	UE_LOG(LogTemp, Log, TEXT("[MoA Socket] UDP Receiver thread stopped"));

	return 0;
}

void MoALiveLinkSource::Start()
{
	ThreadName = "MoA OSC Receiver";
	ThreadName.AppendInt(FAsyncThreadIndex::GetNext());
	Thread = FRunnableThread::Create(this, *ThreadName, 128 * 1024, TPri_AboveNormal, FPlatformAffinity::GetPoolThreadMask());
}

void MoALiveLinkSource::Stop()
{
	Stopping = true;
}

void MoALiveLinkSource::ProcessReceivedData(TSharedPtr<TArray<uint8>, ESPMode::ThreadSafe> ReceivedData)
{
	const uint8* DataPtr = ReceivedData->GetData();
	int32 DataSize = ReceivedData->Num();
	int32 Offset = 0;

	FString AddressPattern = ParseString(DataPtr, Offset, DataSize);
	if (AddressPattern.IsEmpty())
	{
		return;
	}
	
	if (AddressPattern == "/MOA/Stream/Motion/Unreal/Local")
	{
		ProcessCharacterData(DataPtr, DataSize, Offset);
	}
	else if (AddressPattern == "/MOA/Stream/Motion/Unreal/Props")
	{
		ProcessPropData(DataPtr, DataSize, Offset);
	}
}

void MoALiveLinkSource::ProcessCharacterData(const uint8* DataPtr, int32 DataSize, int32 Offset)
{
	FString TypeTags = ParseString(DataPtr, Offset, DataSize);
	if (TypeTags.IsEmpty() || TypeTags[0] != ',')
	{
		return;
	}

	FString ActorName;
	TArray<float> BoneDatas;
	float UpperLegLength = 0.0f;
	float LowerLegLength = 0.0f;
	float HipToUpperLegLength = 0.0f;
	float PositionOffsetX = 0.0f;
	float PositionOffsetY = 0.0f;
	float PositionOffsetZ = 0.0f;
	float HeightOffset = 0.0f;
	int32 FloatIndex = 0;
	
	for(int32 i = 0; i < TypeTags.Len(); i++)
	{
		if (TypeTags[i] == 'i')
		{
			if (Offset + sizeof(int32) > DataSize)
			{
				UE_LOG(LogTemp, Warning, TEXT("Data out of bounds for int32"));
				break;
			}
			int32 IntValue = *reinterpret_cast<const int32*>(DataPtr + Offset);
			Offset += sizeof(int32);
		}
		else if (TypeTags[i] == 's')
		{
			ActorName = ParseString(DataPtr, Offset, DataSize);
			if (ActorName.IsEmpty())
			{
				UE_LOG(LogTemp, Warning, TEXT("Failed to parse actor name"));
				return;
			}
		}
		else if (TypeTags[i] == 'b')
		{
			if (Offset + sizeof(float) > DataSize)
			{
				UE_LOG(LogTemp, Error, TEXT("Offset exceeds data size for float. Offset: %d, DataSize: %d"), Offset, DataSize);
				return;
			}
			int32 BlobSize = *reinterpret_cast<const int32*>(DataPtr + Offset);
			Offset += sizeof(int32);

			BlobSize = ByteSwap(BlobSize);
			ParseBlobDataToFloats(DataPtr + Offset, BlobSize, BoneDatas);
			Offset += BlobSize;
		}
		else if (TypeTags[i] == 'f')
		{
			if (Offset + sizeof(float) > DataSize)
			{
				UE_LOG(LogTemp, Warning, TEXT("Data out of bounds for float"));
				break;
			}
			uint32 IntValue = *reinterpret_cast<const uint32*>(DataPtr + Offset);
			IntValue = ByteSwap(IntValue);
			float FloatValue = *reinterpret_cast<float*>(&IntValue);
			Offset += sizeof(float);
			
			if (FloatIndex == 0) UpperLegLength = FloatValue;
			else if (FloatIndex == 1) LowerLegLength = FloatValue;
			else if (FloatIndex == 2) HipToUpperLegLength = FloatValue;
			else if (FloatIndex == 3) PositionOffsetX = FloatValue;
			else if (FloatIndex == 4) PositionOffsetY = FloatValue;
			else if (FloatIndex == 5) PositionOffsetZ = FloatValue;
			else if (FloatIndex == 6) HeightOffset = FloatValue;
			FloatIndex++;
		}
		else
		{
			continue;
		}
	}

	if (BoneDatas.Num() != 211)
	{
		UE_LOG(LogTemp, Error, TEXT("Insufficient Bone Data received : %d (expected 211)"), BoneDatas.Num());
		return;
	}

	const int32 NumBones = 52;  

	FName SubjectName = FName(*ActorName);
	bool bIsEncounter = !EncounterdSubjects.Contains(SubjectName);
	if (bIsEncounter)
	{
		InitStaticData(NumBones, SubjectName);
		EncounterdSubjects.Add(SubjectName);
	}

	// Update last received time for this subject
	LastReceivedTimes.Add(SubjectName, FDateTime::UtcNow());

	FLiveLinkFrameDataStruct FrameDataStruct = FLiveLinkFrameDataStruct(FLiveLinkAnimationFrameData::StaticStruct());
	FLiveLinkAnimationFrameData& FrameData = *FrameDataStruct.Cast<FLiveLinkAnimationFrameData>(); 
	
	FrameData.Transforms.SetNumZeroed(NumBones);
	
	FrameData.PropertyValues.SetNum(7);
	FrameData.PropertyValues[0] = UpperLegLength;
	FrameData.PropertyValues[1] = LowerLegLength;
	FrameData.PropertyValues[2] = HipToUpperLegLength;
	FrameData.PropertyValues[3] = PositionOffsetX;
	FrameData.PropertyValues[4] = PositionOffsetY;
	FrameData.PropertyValues[5] = PositionOffsetZ;
	FrameData.PropertyValues[6] = HeightOffset;
	
	int32 boneOffset = 0;
	
	FVector BonePosition(BoneDatas[boneOffset], BoneDatas[boneOffset + 1], BoneDatas[boneOffset + 2]);
	FQuat BoneRotation(BoneDatas[boneOffset + 4], BoneDatas[boneOffset + 5], BoneDatas[boneOffset + 6], BoneDatas[boneOffset + 3]);
	FrameData.Transforms[0] = FTransform(BoneRotation, BonePosition, FVector(1.0f));
	boneOffset += 7;
	
	for (int32 i = 1; i < NumBones; i++)
	{
		FQuat Rotation(BoneDatas[boneOffset + 1], BoneDatas[boneOffset + 2], BoneDatas[boneOffset + 3], BoneDatas[boneOffset]);
		FrameData.Transforms[i] = FTransform(Rotation, FVector::ZeroVector, FVector(1.0f));
		boneOffset += 4;
	}

	Client->PushSubjectFrameData_AnyThread({ SourceGuid, SubjectName }, MoveTemp(FrameDataStruct));
}

void MoALiveLinkSource::ProcessPropData(const uint8* DataPtr, int32 DataSize, int32 Offset)
{
	FString TypeTags = ParseString(DataPtr, Offset, DataSize);
	if (TypeTags.IsEmpty() || TypeTags[0] != ',')
	{
		UE_LOG(LogTemp, Error, TEXT("[MoA Prop] Invalid TypeTags: %s"), *TypeTags);
		return;
	}
	
	int32 PropType = 0;
	int32 PropNum = 0;
	FString PropName;
	FVector Position = FVector::ZeroVector;
	FQuat Rotation = FQuat::Identity;
	FVector Scale = FVector::OneVector;

	int32 FloatIndex = 0; // Position(3) + Rotation(4) + Scale(3)
	int32 IntIndex = 0;
	
	for(int32 i = 1; i < TypeTags.Len(); i++)
	{
		if (TypeTags[i] == 'i')
		{
			if (Offset + sizeof(int32) > DataSize)
			{
				UE_LOG(LogTemp, Error, TEXT("[MoA Prop] Data out of bounds for int32 at index %d"), i);
				break;
			}
			int32 IntValue = *reinterpret_cast<const int32*>(DataPtr + Offset);
			IntValue = ByteSwap(IntValue); 
			Offset += sizeof(int32);
			
			if (IntIndex == 0)
			{
				PropType = IntValue;
			}
			else if (IntIndex == 1)
			{
				PropNum = IntValue;
			}
			IntIndex++;
		}
		else if (TypeTags[i] == 's')
		{
			PropName = ParseString(DataPtr, Offset, DataSize);
			if (PropName.IsEmpty())
			{
				UE_LOG(LogTemp, Error, TEXT("[MoA Prop] Failed to parse prop name"));
				return;
			}
		}
		else if (TypeTags[i] == 'f')
		{
			if (Offset + sizeof(float) > DataSize)
			{
				UE_LOG(LogTemp, Error, TEXT("[MoA Prop] Data out of bounds for float at index %d"), i);
				break;
			}
			uint32 IntValue = *reinterpret_cast<const uint32*>(DataPtr + Offset);
			IntValue = ByteSwap(IntValue);
			float FloatValue = *reinterpret_cast<float*>(&IntValue);
			Offset += sizeof(float);
			
			if (FloatIndex == 0) Position.X = FloatValue;
			else if (FloatIndex == 1) Position.Y = FloatValue;
			else if (FloatIndex == 2) Position.Z = FloatValue;
			else if (FloatIndex == 3) Rotation.X = FloatValue;
			else if (FloatIndex == 4) Rotation.Y = FloatValue;
			else if (FloatIndex == 5) Rotation.Z = FloatValue;
			else if (FloatIndex == 6) Rotation.W = FloatValue;
			else if (FloatIndex == 7) Scale.X = FloatValue;
			else if (FloatIndex == 8) Scale.Y = FloatValue;
			else if (FloatIndex == 9) Scale.Z = FloatValue;
			FloatIndex++;
		}
		else
		{
			continue;
		}
	}

	FString SubjectNameStr = FString::Printf(TEXT("%s_%d"), *PropName, PropNum);
	FName SubjectName = FName(*SubjectNameStr);

	bool bIsEncounter = !EncounterdSubjects.Contains(SubjectName);
	if (bIsEncounter)
	{
		InitPropStaticData(SubjectName, PropType);
		EncounterdSubjects.Add(SubjectName);
	}

	// Update last received time for this subject
	LastReceivedTimes.Add(SubjectName, FDateTime::UtcNow());

	FLiveLinkFrameDataStruct FrameDataStruct = FLiveLinkFrameDataStruct(FLiveLinkTransformFrameData::StaticStruct());
	FLiveLinkTransformFrameData& FrameData = *FrameDataStruct.Cast<FLiveLinkTransformFrameData>(); 
	
	FrameData.Transform = FTransform(Rotation, Position, Scale);
	
	FrameData.PropertyValues.SetNum(1);
	FrameData.PropertyValues[0] = static_cast<float>(PropType);

	Client->PushSubjectFrameData_AnyThread({ SourceGuid, SubjectName }, MoveTemp(FrameDataStruct));
}

void MoALiveLinkSource::InitStaticData(int32 NumBones, FName SubjectName)
{
	FLiveLinkStaticDataStruct StaticDataStruct = FLiveLinkStaticDataStruct(FLiveLinkSkeletonStaticData::StaticStruct());
	FLiveLinkSkeletonStaticData& StaticData = *StaticDataStruct.Cast<FLiveLinkSkeletonStaticData>();

	StaticData.BoneNames.SetNumUninitialized(NumBones); 
	StaticData.BoneParents.SetNumUninitialized(NumBones); 
	
	StaticData.PropertyNames.SetNum(7);
	StaticData.PropertyNames[0] = FName(TEXT("UpperLegLength"));
	StaticData.PropertyNames[1] = FName(TEXT("LowerLegLength"));
	StaticData.PropertyNames[2] = FName(TEXT("HipToUpperLegLength"));
	StaticData.PropertyNames[3] = FName(TEXT("PositionOffsetX"));
	StaticData.PropertyNames[4] = FName(TEXT("PositionOffsetY"));
	StaticData.PropertyNames[5] = FName(TEXT("PositionOffsetZ"));
	StaticData.PropertyNames[6] = FName(TEXT("HeightOffset"));

	for (int id = 0; id < NumBones; id++)
	{
		FString BoneName = SkeletalBoneNames[id];
		StaticData.BoneNames[id] = FName(BoneName);
		StaticData.BoneParents[id] = -1;
	}


	StaticData.BoneParents[1] = 0;    // thigh_l -> pelvis
	StaticData.BoneParents[2] = 1;    // calf_l -> thigh_l
	StaticData.BoneParents[3] = 2;    // foot_l -> calf_l
	StaticData.BoneParents[4] = 3;    // toes_l -> foot_l

	StaticData.BoneParents[5] = 0;    // thigh_r -> pelvis
	StaticData.BoneParents[6] = 5;    // calf_r -> thigh_r
	StaticData.BoneParents[7] = 6;    // foot_r -> calf_r
	StaticData.BoneParents[8] = 7;    // toes_r -> foot_r

	// Spine
	StaticData.BoneParents[9] = 0;    // spine_01 -> pelvis
	StaticData.BoneParents[10] = 9;   // spine_02 -> spine_01
	StaticData.BoneParents[11] = 10;  // spine_03 -> spine_02

	// Left Arm
	StaticData.BoneParents[12] = 11;  // clavicle_l -> spine_03
	StaticData.BoneParents[13] = 12;  // upperarm_l -> clavicle_l
	StaticData.BoneParents[14] = 13;  // lowerarm_l -> upperarm_l
	StaticData.BoneParents[15] = 14;  // hand_l -> lowerarm_l

	// Head
	StaticData.BoneParents[16] = 11;  // neck_01 -> spine_03
	StaticData.BoneParents[17] = 16;  // head -> neck_01

	// Right Arm
	StaticData.BoneParents[18] = 11;  // clavicle_r -> spine_03
	StaticData.BoneParents[19] = 18;  // upperarm_r -> clavicle_r
	StaticData.BoneParents[20] = 19;  // lowerarm_r -> upperarm_r
	StaticData.BoneParents[21] = 20;  // hand_r -> lowerarm_r

	// Left Hand Fingers
	StaticData.BoneParents[22] = 15;  // thumb_01_l -> hand_l
	StaticData.BoneParents[23] = 22;  // thumb_02_l -> thumb_01_l
	StaticData.BoneParents[24] = 23;  // thumb_03_l -> thumb_02_l

	StaticData.BoneParents[25] = 15;  // index_01_l -> hand_l
	StaticData.BoneParents[26] = 25;  // index_02_l -> index_01_l
	StaticData.BoneParents[27] = 26;  // index_03_l -> index_02_l

	StaticData.BoneParents[28] = 15;  // middle_01_l -> hand_l
	StaticData.BoneParents[29] = 28;  // middle_02_l -> middle_01_l
	StaticData.BoneParents[30] = 29;  // middle_03_l -> middle_02_l

	StaticData.BoneParents[31] = 15;  // ring_01_l -> hand_l
	StaticData.BoneParents[32] = 31;  // ring_02_l -> ring_01_l
	StaticData.BoneParents[33] = 32;  // ring_03_l -> ring_02_l

	StaticData.BoneParents[34] = 15;  // pinky_01_l -> hand_l
	StaticData.BoneParents[35] = 34;  // pinky_02_l -> pinky_01_l
	StaticData.BoneParents[36] = 35;  // pinky_03_l -> pinky_02_l

	// Right Hand Fingers
	StaticData.BoneParents[37] = 21;  // thumb_01_r -> hand_r
	StaticData.BoneParents[38] = 37;  // thumb_02_r -> thumb_01_r
	StaticData.BoneParents[39] = 38;  // thumb_03_r -> thumb_02_r

	StaticData.BoneParents[40] = 21;  // index_01_r -> hand_r
	StaticData.BoneParents[41] = 40;  // index_02_r -> index_01_r
	StaticData.BoneParents[42] = 41;  // index_03_r -> index_02_r

	StaticData.BoneParents[43] = 21;  // middle_01_r -> hand_r
	StaticData.BoneParents[44] = 43;  // middle_02_r -> middle_01_r
	StaticData.BoneParents[45] = 44;  // middle_03_r -> middle_02_r

	StaticData.BoneParents[46] = 21;  // ring_01_r -> hand_r
	StaticData.BoneParents[47] = 46;  // ring_02_r -> ring_01_r
	StaticData.BoneParents[48] = 47;  // ring_03_r -> ring_02_r

	StaticData.BoneParents[49] = 21;  // pinky_01_r -> hand_r
	StaticData.BoneParents[50] = 49;  // pinky_02_r -> pinky_01_r
	StaticData.BoneParents[51] = 50;

	Client->PushSubjectStaticData_AnyThread({ SourceGuid, SubjectName }, ULiveLinkAnimationRole::StaticClass(), MoveTemp(StaticDataStruct));
}

void MoALiveLinkSource::InitPropStaticData(FName SubjectName, int32 PropType)
{
	FLiveLinkStaticDataStruct StaticDataStruct = FLiveLinkStaticDataStruct(FLiveLinkTransformStaticData::StaticStruct());
	FLiveLinkTransformStaticData& StaticData = *StaticDataStruct.Cast<FLiveLinkTransformStaticData>();

	StaticData.PropertyNames.SetNum(1);
	StaticData.PropertyNames[0] = FName(TEXT("PropType"));

	Client->PushSubjectStaticData_AnyThread({ SourceGuid, SubjectName }, ULiveLinkTransformRole::StaticClass(), MoveTemp(StaticDataStruct));
}

FString MoALiveLinkSource::ParseString(const uint8* DataPtr, int32& offset, int32 DataSize)
{
	if (offset >= DataSize)
	{
		return FString();
	}

	const char* StringStart = reinterpret_cast<const char*>(DataPtr + offset);

	FString result = FString(ANSI_TO_TCHAR(StringStart));

	int32 StringLength = result.Len() + 1;
	int32 PaddedLength = (StringLength + 3) & ~3;

	offset += PaddedLength;

	return result;
}

void ParseBlobDataToFloats(const uint8* DataPtr, int32 BlobSize, TArray<float>& OutFloatArray)
{
	if (BlobSize % sizeof(float) != 0)
	{
		UE_LOG(LogTemp, Warning, TEXT("Invalid Blob Size: %d. Must be a multiple of 4."), BlobSize);
		return;
	}

	const int32 NumFloats = BlobSize / sizeof(float);
	OutFloatArray.SetNumUninitialized(NumFloats);

	for (int32 i = 0; i < NumFloats; ++i)
	{
		float Value = *reinterpret_cast<const float*>(DataPtr + i * sizeof(float));
		OutFloatArray[i] = Value;
	}
}

void MoALiveLinkSource::CleanupInactiveSubjects()
{
	const FTimespan InactiveThreshold = FTimespan::FromSeconds(5.0);
	const FDateTime Now = FDateTime::UtcNow();

	TArray<FName> SubjectsToRemove;

	for (const TPair<FName, FDateTime>& Pair : LastReceivedTimes)
	{
		FTimespan TimeSinceLastData = Now - Pair.Value;
		if (TimeSinceLastData > InactiveThreshold)
		{
			SubjectsToRemove.Add(Pair.Key);
			UE_LOG(LogTemp, Log, TEXT("[MoA Cleanup] Subject '%s' inactive for %.2f seconds, marking for removal"),
				*Pair.Key.ToString(), TimeSinceLastData.GetTotalSeconds());
		}
	}

	for (const FName& SubjectName : SubjectsToRemove)
	{
		LastReceivedTimes.Remove(SubjectName);
		EncounterdSubjects.Remove(SubjectName);

		if (Client)
		{
			Client->RemoveSubject_AnyThread({ SourceGuid, SubjectName });
			UE_LOG(LogTemp, Log, TEXT("[MoA Cleanup] Removed inactive subject: %s"), *SubjectName.ToString());
		}
	}
}

#undef LOCTEXT_NAMESPACE