#include "HyperTwistReplay/HyperTwistReplayPersistenceLibrary.h"

#include "HAL/FileManager.h"
#include "HyperTwistBootstrap/HyperTwistContractLibrary.h"
#include "HyperTwistReplay/HyperTwistReplayLibrary.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"

namespace HyperTwistReplayPersistenceLibraryInternal
{
	const TCHAR* ReplayPacketVersion = TEXT("ht-replay/v1");

	FString SanitizePathToken(const FString& Value)
	{
		FString Sanitized = Value;
		for (const TCHAR InvalidCharacter : {
				TEXT('\\'),
				TEXT('/'),
				TEXT(':'),
				TEXT('*'),
				TEXT('?'),
				TEXT('"'),
				TEXT('<'),
				TEXT('>'),
				TEXT('|'),
				TEXT(' ')
			})
		{
			Sanitized.ReplaceCharInline(InvalidCharacter, TEXT('_'));
		}

		return Sanitized.IsEmpty() ? TEXT("unknown") : Sanitized;
	}

	FString ResolvePath(const FString& Path)
	{
		return FPaths::ConvertRelativePathToFull(Path);
	}

	FString GetEventTypeToken(const EHyperTwistReplayEventType EventType)
	{
		switch (EventType)
		{
		case EHyperTwistReplayEventType::InspectionStart:
			return TEXT("inspection_start");
		case EHyperTwistReplayEventType::InspectionEnd:
			return TEXT("inspection_end");
		case EHyperTwistReplayEventType::TimerStart:
			return TEXT("timer_start");
		case EHyperTwistReplayEventType::Move:
			return TEXT("move");
		case EHyperTwistReplayEventType::Rotation:
			return TEXT("rotation");
		case EHyperTwistReplayEventType::StateSnapshot:
			return TEXT("state_snapshot");
		case EHyperTwistReplayEventType::PhaseMark:
			return TEXT("phase_mark");
		case EHyperTwistReplayEventType::DeviceSync:
			return TEXT("device_sync");
		case EHyperTwistReplayEventType::RecognitionPreview:
			return TEXT("recognition_preview");
		case EHyperTwistReplayEventType::RecognitionScan:
			return TEXT("recognition_scan");
		case EHyperTwistReplayEventType::RecognitionCommit:
			return TEXT("recognition_commit");
		case EHyperTwistReplayEventType::RecognitionCorrection:
			return TEXT("recognition_correction");
		case EHyperTwistReplayEventType::Pause:
			return TEXT("pause");
		case EHyperTwistReplayEventType::Resume:
			return TEXT("resume");
		case EHyperTwistReplayEventType::Annotation:
			return TEXT("annotation");
		case EHyperTwistReplayEventType::SolveEnd:
			return TEXT("solve_end");
		default:
			return TEXT("event");
		}
	}

	FString BuildFallbackEventId(
		const FHyperTwistReplayEvent& ReplayEvent,
		const int32 Sequence
	)
	{
		return FString::Printf(
			TEXT("%s_%03d"),
			*GetEventTypeToken(ReplayEvent.EventType),
			FMath::Max(Sequence, 1)
		);
	}

	FString BuildFallbackPacketIdentity(const FHyperTwistReplayPacket& ReplayPacket)
	{
		const FString PuzzleToken = SanitizePathToken(
			!ReplayPacket.PuzzleDefinition.PuzzleId.IsEmpty()
				? ReplayPacket.PuzzleDefinition.PuzzleId
				: TEXT("replay")
		);
		const FString IdentitySeed = !ReplayPacket.StartedAtUtc.IsEmpty()
			? ReplayPacket.StartedAtUtc
			: !ReplayPacket.EndedAtUtc.IsEmpty()
				? ReplayPacket.EndedAtUtc
				: FString::Printf(TEXT("event-count-%d"), ReplayPacket.Events.Num());
		return FString::Printf(
			TEXT("%s_%s"),
			*PuzzleToken,
			*SanitizePathToken(IdentitySeed)
		);
	}

	bool ShouldSortEventsBySequence(const TArray<FHyperTwistReplayEvent>& Events)
	{
		if (Events.Num() < 2)
		{
			return false;
		}

		for (const FHyperTwistReplayEvent& ReplayEvent : Events)
		{
			if (ReplayEvent.Sequence <= 0)
			{
				return false;
			}
		}

		return true;
	}

	void NormalizeEventOrder(TArray<FHyperTwistReplayEvent>& Events)
	{
		if (!ShouldSortEventsBySequence(Events))
		{
			return;
		}

		struct FIndexedReplayEvent
		{
			FHyperTwistReplayEvent Event;
			int32 OriginalIndex = 0;
		};

		TArray<FIndexedReplayEvent> IndexedEvents;
		IndexedEvents.Reserve(Events.Num());
		for (int32 EventIndex = 0; EventIndex < Events.Num(); ++EventIndex)
		{
			FIndexedReplayEvent& IndexedEvent = IndexedEvents.AddDefaulted_GetRef();
			IndexedEvent.Event = Events[EventIndex];
			IndexedEvent.OriginalIndex = EventIndex;
		}

		IndexedEvents.Sort([](const FIndexedReplayEvent& Left, const FIndexedReplayEvent& Right)
		{
			if (Left.Event.Sequence != Right.Event.Sequence)
			{
				return Left.Event.Sequence < Right.Event.Sequence;
			}

			if (Left.Event.TimeMs != Right.Event.TimeMs)
			{
				return Left.Event.TimeMs < Right.Event.TimeMs;
			}

			return Left.OriginalIndex < Right.OriginalIndex;
		});

		Events.Reset();
		Events.Reserve(IndexedEvents.Num());
		for (FIndexedReplayEvent& IndexedEvent : IndexedEvents)
		{
			Events.Add(MoveTemp(IndexedEvent.Event));
		}
	}

	FString BuildUniqueEventId(
		const FHyperTwistReplayEvent& ReplayEvent,
		const int32 Sequence,
		TSet<FString>& UsedEventIds
	)
	{
		const FString PreferredEventId = !ReplayEvent.EventId.IsEmpty()
			? ReplayEvent.EventId
			: BuildFallbackEventId(ReplayEvent, Sequence);
		if (!UsedEventIds.Contains(PreferredEventId))
		{
			UsedEventIds.Add(PreferredEventId);
			return PreferredEventId;
		}

		const FString BaseEventId = SanitizePathToken(PreferredEventId);
		FString CandidateEventId = FString::Printf(
			TEXT("%s_%03d"),
			*BaseEventId,
			FMath::Max(Sequence, 1)
		);
		int32 CollisionIndex = 1;
		while (UsedEventIds.Contains(CandidateEventId))
		{
			CandidateEventId = FString::Printf(
				TEXT("%s_%03d_%02d"),
				*BaseEventId,
				FMath::Max(Sequence, 1),
				CollisionIndex
			);
			++CollisionIndex;
		}

		UsedEventIds.Add(CandidateEventId);
		return CandidateEventId;
	}
}

FString UHyperTwistReplayPersistenceLibrary::GetDefaultReplayDirectory()
{
	return FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("HyperTwist"), TEXT("Replays"));
}

FHyperTwistReplayPacket UHyperTwistReplayPersistenceLibrary::NormalizeReplayPacket(
	const FHyperTwistReplayPacket& ReplayPacket
)
{
	FHyperTwistReplayPacket NormalizedPacket = ReplayPacket;
	if (NormalizedPacket.PacketVersion.IsEmpty())
	{
		NormalizedPacket.PacketVersion =
			HyperTwistReplayPersistenceLibraryInternal::ReplayPacketVersion;
	}

	if (NormalizedPacket.ReplayId.IsEmpty() && !NormalizedPacket.SessionId.IsEmpty())
	{
		NormalizedPacket.ReplayId = NormalizedPacket.SessionId;
	}

	if (NormalizedPacket.SessionId.IsEmpty() && !NormalizedPacket.ReplayId.IsEmpty())
	{
		NormalizedPacket.SessionId = NormalizedPacket.ReplayId;
	}

	if (NormalizedPacket.ReplayId.IsEmpty() && NormalizedPacket.SessionId.IsEmpty())
	{
		const FString FallbackIdentity =
			HyperTwistReplayPersistenceLibraryInternal::BuildFallbackPacketIdentity(
				NormalizedPacket
			);
		NormalizedPacket.ReplayId = FallbackIdentity;
		NormalizedPacket.SessionId = FallbackIdentity;
	}

	HyperTwistReplayPersistenceLibraryInternal::NormalizeEventOrder(NormalizedPacket.Events);

	int32 PreviousSequence = 0;
	int32 PreviousTimeMs = 0;
	TSet<FString> UsedEventIds;
	for (FHyperTwistReplayEvent& ReplayEvent : NormalizedPacket.Events)
	{
		ReplayEvent.TimeMs = FMath::Max(ReplayEvent.TimeMs, 0);
		if (ReplayEvent.Sequence <= PreviousSequence)
		{
			ReplayEvent.Sequence = PreviousSequence + 1;
		}

		if (ReplayEvent.TimeMs < PreviousTimeMs)
		{
			ReplayEvent.TimeMs = PreviousTimeMs;
		}

		ReplayEvent.EventId =
			HyperTwistReplayPersistenceLibraryInternal::BuildUniqueEventId(
				ReplayEvent,
				ReplayEvent.Sequence,
				UsedEventIds
			);

		PreviousSequence = ReplayEvent.Sequence;
		PreviousTimeMs = ReplayEvent.TimeMs;
	}

	NormalizedPacket.DerivedSummary = NormalizedPacket.Events.Num() > 0
		? UHyperTwistReplayLibrary::DeriveReplaySummary(NormalizedPacket)
		: FHyperTwistDerivedReplaySummary();
	return NormalizedPacket;
}

FString UHyperTwistReplayPersistenceLibrary::GetDefaultReplayPath(
	const FHyperTwistReplayPacket& ReplayPacket
)
{
	if (ReplayPacket.ReplayId.IsEmpty() && ReplayPacket.SessionId.IsEmpty())
	{
		return FString();
	}

	const FString SessionToken =
		HyperTwistReplayPersistenceLibraryInternal::SanitizePathToken(ReplayPacket.SessionId);
	const FString ReplayToken =
		HyperTwistReplayPersistenceLibraryInternal::SanitizePathToken(
			ReplayPacket.ReplayId.IsEmpty() ? ReplayPacket.SessionId : ReplayPacket.ReplayId
		);
	return FPaths::Combine(
		GetDefaultReplayDirectory(),
		FString::Printf(TEXT("%s_%s.json"), *SessionToken, *ReplayToken)
	);
}

FString UHyperTwistReplayPersistenceLibrary::ResolveReplayPacketPath(
	const FHyperTwistReplayPacket& ReplayPacket,
	const FString& ReplayPath
)
{
	const FString EffectivePath = ReplayPath.IsEmpty()
		? GetDefaultReplayPath(ReplayPacket)
		: ReplayPath;
	return EffectivePath.IsEmpty()
		? FString()
		: HyperTwistReplayPersistenceLibraryInternal::ResolvePath(EffectivePath);
}

bool UHyperTwistReplayPersistenceLibrary::SaveReplayPacketToFile(
	const FHyperTwistReplayPacket& ReplayPacket,
	const FString& ReplayPath,
	FString& OutResolvedPath
)
{
	const FHyperTwistReplayPacket NormalizedPacket = NormalizeReplayPacket(ReplayPacket);
	OutResolvedPath = ResolveReplayPacketPath(NormalizedPacket, ReplayPath);
	if (!NormalizedPacket.IsStructurallyValid() || OutResolvedPath.IsEmpty())
	{
		return false;
	}

	const FString Json =
		UHyperTwistContractLibrary::SerializeReplayPacketToJson(NormalizedPacket);
	if (Json.IsEmpty())
	{
		return false;
	}

	IFileManager::Get().MakeDirectory(*FPaths::GetPath(OutResolvedPath), true);
	return FFileHelper::SaveStringToFile(
		Json,
		*OutResolvedPath,
		FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM
	);
}

bool UHyperTwistReplayPersistenceLibrary::LoadReplayPacketFromFile(
	const FString& ReplayPath,
	FHyperTwistReplayPacket& OutReplayPacket,
	FString& OutResolvedPath
)
{
	OutReplayPacket = FHyperTwistReplayPacket();
	OutResolvedPath = ReplayPath.IsEmpty()
		? FString()
		: HyperTwistReplayPersistenceLibraryInternal::ResolvePath(ReplayPath);
	if (OutResolvedPath.IsEmpty() || !FPaths::FileExists(OutResolvedPath))
	{
		return false;
	}

	FString Json;
	if (!FFileHelper::LoadFileToString(Json, *OutResolvedPath))
	{
		return false;
	}

	FHyperTwistReplayPacket LoadedPacket;
	if (!UHyperTwistContractLibrary::DeserializeReplayPacketFromJson(Json, LoadedPacket))
	{
		return false;
	}

	OutReplayPacket = NormalizeReplayPacket(LoadedPacket);
	return OutReplayPacket.IsStructurallyValid();
}
