#include "HyperTwistSimulation/HyperTwistClassicCubeLeaderboardLibrary.h"

#include "HAL/FileManager.h"
#include "JsonObjectConverter.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"

namespace HyperTwistClassicCubeLeaderboardLibraryInternal
{
	const TCHAR* SchemaVersion = TEXT("ht-classic-cube-leaderboard/v1");
	const TCHAR* DefaultPuzzleId = TEXT("cube/3x3x3");
	const TCHAR* FallbackRecordedAtUtc = TEXT("9999-12-31T23:59:59Z");

	template <typename TStruct>
	bool SerializeStruct(const TStruct& Value, FString& OutJson)
	{
		return FJsonObjectConverter::UStructToJsonObjectString(
			TStruct::StaticStruct(),
			&Value,
			OutJson,
			0,
			0
		);
	}

	template <typename TStruct>
	bool DeserializeStruct(const FString& Json, TStruct& OutValue)
	{
		return !Json.IsEmpty()
			&& FJsonObjectConverter::JsonObjectStringToUStruct(Json, &OutValue, 0, 0);
	}

	FString MakeKey(const FString& PuzzleId, const int32 ScrambleLength)
	{
		return FString::Printf(TEXT("%s|%d"), *PuzzleId, ScrambleLength);
	}

	FString FormatMilliseconds(const int32 Milliseconds)
	{
		const int32 SafeMilliseconds = FMath::Max(Milliseconds, 0);
		const int32 Minutes = SafeMilliseconds / 60000;
		const int32 Seconds = (SafeMilliseconds / 1000) % 60;
		const int32 MillisRemainder = SafeMilliseconds % 1000;
		return FString::Printf(TEXT("%02d:%02d.%03d"), Minutes, Seconds, MillisRemainder);
	}

	FHyperTwistClassicCubeLeaderboardEntry NormalizeEntry(
		const FHyperTwistClassicCubeLeaderboardEntry& Entry
	)
	{
		FHyperTwistClassicCubeLeaderboardEntry NormalizedEntry = Entry;
		if (NormalizedEntry.PuzzleId.IsEmpty())
		{
			NormalizedEntry.PuzzleId = DefaultPuzzleId;
		}

		NormalizedEntry.ScrambleLength = FMath::Max(NormalizedEntry.ScrambleLength, 0);
		if (NormalizedEntry.RecordedAtUtc.IsEmpty())
		{
			NormalizedEntry.RecordedAtUtc = FallbackRecordedAtUtc;
		}

		return NormalizedEntry;
	}

	bool ShouldPreferEntry(
		const FHyperTwistClassicCubeLeaderboardEntry& Candidate,
		const FHyperTwistClassicCubeLeaderboardEntry& Existing
	)
	{
		if (!Candidate.IsStructurallyValid())
		{
			return false;
		}

		if (!Existing.IsStructurallyValid())
		{
			return true;
		}

		if (Candidate.FinalTimeMs != Existing.FinalTimeMs)
		{
			return Candidate.FinalTimeMs < Existing.FinalTimeMs;
		}

		if (!Candidate.RecordedAtUtc.IsEmpty())
		{
			if (Existing.RecordedAtUtc.IsEmpty())
			{
				return true;
			}

			if (Candidate.RecordedAtUtc != Existing.RecordedAtUtc)
			{
				return Candidate.RecordedAtUtc < Existing.RecordedAtUtc;
			}
		}

		if (Candidate.ReplayId != Existing.ReplayId)
		{
			return Candidate.ReplayId < Existing.ReplayId;
		}

		if (Candidate.SessionId != Existing.SessionId)
		{
			return Candidate.SessionId < Existing.SessionId;
		}

		return Candidate.ScrambleNotation < Existing.ScrambleNotation;
	}

	void SortEntries(TArray<FHyperTwistClassicCubeLeaderboardEntry>& Entries)
	{
		Entries.Sort([](
			const FHyperTwistClassicCubeLeaderboardEntry& Left,
			const FHyperTwistClassicCubeLeaderboardEntry& Right)
		{
			if (Left.PuzzleId != Right.PuzzleId)
			{
				return Left.PuzzleId < Right.PuzzleId;
			}

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

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

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

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

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

FString UHyperTwistClassicCubeLeaderboardLibrary::GetDefaultLeaderboardPath()
{
	return FPaths::Combine(
		FPaths::ProjectSavedDir(),
		TEXT("HyperTwist"),
		TEXT("ClassicCube"),
		TEXT("leaderboard.json")
	);
}

FString UHyperTwistClassicCubeLeaderboardLibrary::FormatBestTimeMs(const int32 FinalTimeMs)
{
	return HyperTwistClassicCubeLeaderboardLibraryInternal::FormatMilliseconds(
		FinalTimeMs
	);
}

FString UHyperTwistClassicCubeLeaderboardLibrary::ResolveLeaderboardPath(
	const FString& LeaderboardPath
)
{
	const FString EffectivePath = LeaderboardPath.IsEmpty()
		? GetDefaultLeaderboardPath()
		: LeaderboardPath;
	return FPaths::ConvertRelativePathToFull(EffectivePath);
}

FHyperTwistClassicCubeLeaderboardState UHyperTwistClassicCubeLeaderboardLibrary::NormalizeLeaderboardState(
	const FHyperTwistClassicCubeLeaderboardState& LeaderboardState
)
{
	FHyperTwistClassicCubeLeaderboardState NormalizedState = LeaderboardState;
	if (NormalizedState.SchemaVersion.IsEmpty())
	{
		NormalizedState.SchemaVersion = HyperTwistClassicCubeLeaderboardLibraryInternal::SchemaVersion;
	}

	TMap<FString, FHyperTwistClassicCubeLeaderboardEntry> BestEntryByKey;
	for (const FHyperTwistClassicCubeLeaderboardEntry& Entry : LeaderboardState.Entries)
	{
		const FHyperTwistClassicCubeLeaderboardEntry NormalizedEntry =
			HyperTwistClassicCubeLeaderboardLibraryInternal::NormalizeEntry(Entry);
		if (!NormalizedEntry.IsStructurallyValid())
		{
			continue;
		}

		const FString Key = HyperTwistClassicCubeLeaderboardLibraryInternal::MakeKey(
			NormalizedEntry.PuzzleId,
			NormalizedEntry.ScrambleLength
		);
		if (FHyperTwistClassicCubeLeaderboardEntry* ExistingEntry = BestEntryByKey.Find(Key))
		{
			if (HyperTwistClassicCubeLeaderboardLibraryInternal::ShouldPreferEntry(
					NormalizedEntry,
					*ExistingEntry))
			{
				*ExistingEntry = NormalizedEntry;
			}
		}
		else
		{
			BestEntryByKey.Add(Key, NormalizedEntry);
		}
	}

	NormalizedState.Entries.Reset();
	BestEntryByKey.GenerateValueArray(NormalizedState.Entries);
	HyperTwistClassicCubeLeaderboardLibraryInternal::SortEntries(NormalizedState.Entries);
	return NormalizedState;
}

bool UHyperTwistClassicCubeLeaderboardLibrary::SaveLeaderboardStateToFile(
	const FHyperTwistClassicCubeLeaderboardState& LeaderboardState,
	const FString& LeaderboardPath,
	FString& OutResolvedPath
)
{
	OutResolvedPath = ResolveLeaderboardPath(LeaderboardPath);
	const FHyperTwistClassicCubeLeaderboardState NormalizedState =
		NormalizeLeaderboardState(LeaderboardState);
	if (!NormalizedState.IsStructurallyValid() || OutResolvedPath.IsEmpty())
	{
		return false;
	}

	FString Json;
	if (!HyperTwistClassicCubeLeaderboardLibraryInternal::SerializeStruct(
			NormalizedState,
			Json))
	{
		return false;
	}

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

bool UHyperTwistClassicCubeLeaderboardLibrary::LoadLeaderboardStateFromFile(
	const FString& LeaderboardPath,
	FHyperTwistClassicCubeLeaderboardState& OutLeaderboardState,
	FString& OutResolvedPath
)
{
	OutLeaderboardState = FHyperTwistClassicCubeLeaderboardState();
	OutResolvedPath = ResolveLeaderboardPath(LeaderboardPath);
	if (OutResolvedPath.IsEmpty() || !FPaths::FileExists(OutResolvedPath))
	{
		return false;
	}

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

	FHyperTwistClassicCubeLeaderboardState LoadedState;
	if (!HyperTwistClassicCubeLeaderboardLibraryInternal::DeserializeStruct(
			Json,
			LoadedState))
	{
		return false;
	}

	OutLeaderboardState = NormalizeLeaderboardState(LoadedState);
	return OutLeaderboardState.IsStructurallyValid();
}

FHyperTwistClassicCubeLeaderboardState UHyperTwistClassicCubeLeaderboardLibrary::RecordBestTime(
	const FHyperTwistClassicCubeLeaderboardState& LeaderboardState,
	const FHyperTwistClassicCubeLeaderboardEntry& Entry
)
{
	FHyperTwistClassicCubeLeaderboardState UpdatedState =
		NormalizeLeaderboardState(LeaderboardState);
	if (!UpdatedState.IsStructurallyValid())
	{
		UpdatedState = FHyperTwistClassicCubeLeaderboardState();
		UpdatedState.SchemaVersion = HyperTwistClassicCubeLeaderboardLibraryInternal::SchemaVersion;
	}
	const FHyperTwistClassicCubeLeaderboardEntry NormalizedEntry =
		HyperTwistClassicCubeLeaderboardLibraryInternal::NormalizeEntry(Entry);
	if (!NormalizedEntry.IsStructurallyValid())
	{
		return UpdatedState;
	}

	const FString TargetKey = HyperTwistClassicCubeLeaderboardLibraryInternal::MakeKey(
		NormalizedEntry.PuzzleId,
		NormalizedEntry.ScrambleLength
	);
	const int32 ExistingIndex = UpdatedState.Entries.IndexOfByPredicate(
		[&TargetKey](const FHyperTwistClassicCubeLeaderboardEntry& Candidate)
		{
			return HyperTwistClassicCubeLeaderboardLibraryInternal::MakeKey(
				Candidate.PuzzleId,
				Candidate.ScrambleLength
			) == TargetKey;
		}
	);

	if (ExistingIndex == INDEX_NONE)
	{
		UpdatedState.Entries.Add(NormalizedEntry);
	}
	else
	{
		const FHyperTwistClassicCubeLeaderboardEntry& ExistingEntry = UpdatedState.Entries[ExistingIndex];
		if (HyperTwistClassicCubeLeaderboardLibraryInternal::ShouldPreferEntry(
				NormalizedEntry,
				ExistingEntry))
		{
			UpdatedState.Entries[ExistingIndex] = NormalizedEntry;
		}
	}

	HyperTwistClassicCubeLeaderboardLibraryInternal::SortEntries(UpdatedState.Entries);
	return UpdatedState;
}

bool UHyperTwistClassicCubeLeaderboardLibrary::FindBestEntry(
	const FHyperTwistClassicCubeLeaderboardState& LeaderboardState,
	const FString& PuzzleId,
	const int32 ScrambleLength,
	FHyperTwistClassicCubeLeaderboardEntry& OutEntry
)
{
	OutEntry = FHyperTwistClassicCubeLeaderboardEntry();
	if (PuzzleId.IsEmpty() || ScrambleLength < 0)
	{
		return false;
	}

	const FHyperTwistClassicCubeLeaderboardState NormalizedState =
		NormalizeLeaderboardState(LeaderboardState);
	if (!NormalizedState.IsStructurallyValid())
	{
		return false;
	}

	bool bFoundBestEntry = false;
	for (const FHyperTwistClassicCubeLeaderboardEntry& Entry : NormalizedState.Entries)
	{
		if (Entry.PuzzleId == PuzzleId
			&& Entry.ScrambleLength == ScrambleLength
			&& (!bFoundBestEntry
				|| HyperTwistClassicCubeLeaderboardLibraryInternal::ShouldPreferEntry(
					Entry,
					OutEntry)))
		{
			OutEntry = Entry;
			bFoundBestEntry = true;
		}
	}

	return bFoundBestEntry && OutEntry.IsStructurallyValid();
}

FString UHyperTwistClassicCubeLeaderboardLibrary::BuildLeaderboardStatusLine(
	const FHyperTwistClassicCubeLeaderboardState& LeaderboardState,
	const FString& PuzzleId,
	const int32 ScrambleLength
)
{
	const int32 SafeScrambleLength = FMath::Max(ScrambleLength, 0);
	FHyperTwistClassicCubeLeaderboardEntry BestEntry;
	if (!PuzzleId.IsEmpty()
		&& FindBestEntry(LeaderboardState, PuzzleId, SafeScrambleLength, BestEntry))
	{
		return FString::Printf(
			TEXT("leaderboard: best %s for %d-move scramble"),
			*FormatBestTimeMs(BestEntry.FinalTimeMs),
			BestEntry.ScrambleLength
		);
	}

	return FString::Printf(
		TEXT("leaderboard: no local best yet for %d-move scramble"),
		SafeScrambleLength
	);
}
