#include "HyperTwistTraining/HyperTwistTrainingPersistenceLibrary.h"

#include "HyperTwistBootstrap/HyperTwistContractLibrary.h"
#include "HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h"
#include "HAL/FileManager.h"
#include "JsonObjectConverter.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"

namespace HyperTwistTrainingPersistenceLibraryInternal
{
	template <typename TStruct>
	FString SerializeStructToJson(const TStruct& Value)
	{
		FString Json;
		FJsonObjectConverter::UStructToJsonObjectString(TStruct::StaticStruct(), &Value, Json, 0, 0);
		return Json;
	}

	void AppendVerificationNote(TArray<FString>& Notes, const FString& Note);

	template <typename TStruct>
	bool DoSerializedStructsMatch(const TStruct& Left, const TStruct& Right)
	{
		const FString LeftJson = SerializeStructToJson(Left);
		const FString RightJson = SerializeStructToJson(Right);
		return !LeftJson.IsEmpty() && !RightJson.IsEmpty() && LeftJson == RightJson;
	}

	template <typename TStruct>
	bool CheckDerivedStructParity(
		const TStruct& Expected,
		const TStruct& Loaded,
		TArray<FString>& DerivedNotes,
		const FString& Note
	)
	{
		if (DoSerializedStructsMatch(Expected, Loaded))
		{
			return true;
		}

		AppendVerificationNote(DerivedNotes, Note);
		return false;
	}

	int32 CompareUtcStrings(const FString& Left, const FString& Right)
	{
		if (Left == Right)
		{
			return 0;
		}
		if (Left.IsEmpty())
		{
			return -1;
		}
		if (Right.IsEmpty())
		{
			return 1;
		}

		FDateTime LeftTime;
		FDateTime RightTime;
		const bool bParsedLeft = FDateTime::ParseIso8601(*Left, LeftTime);
		const bool bParsedRight = FDateTime::ParseIso8601(*Right, RightTime);
		if (bParsedLeft && bParsedRight)
		{
			return LeftTime < RightTime ? -1 : 1;
		}

		return Left.Compare(Right);
	}

	void ConsiderLatestUtc(FString& LatestUtc, const FString& CandidateUtc)
	{
		if (!CandidateUtc.IsEmpty()
			&& (LatestUtc.IsEmpty() || CompareUtcStrings(CandidateUtc, LatestUtc) > 0))
		{
			LatestUtc = CandidateUtc;
		}
	}

	FString ResolveDerivedVerificationReferenceUtc(
		const FHyperTwistTrainingRepositoryState& RepositoryState
	)
	{
		FString LatestUtc;
		for (const FHyperTwistTrainingRunRecord& RunRecord : RepositoryState.RunRecords)
		{
			ConsiderLatestUtc(LatestUtc, RunRecord.LastUpdatedAtUtc);
			ConsiderLatestUtc(LatestUtc, RunRecord.Session.EndedAtUtc);
		}
		for (const FHyperTwistTrainingReviewPlanState& ReviewPlan : RepositoryState.ReviewPlans)
		{
			ConsiderLatestUtc(LatestUtc, ReviewPlan.CreatedAtUtc);
			ConsiderLatestUtc(LatestUtc, ReviewPlan.ReferenceUtc);
		}
		for (const FHyperTwistTrainingSessionTemplate& SessionTemplate : RepositoryState.StoredSessionTemplates)
		{
			ConsiderLatestUtc(LatestUtc, SessionTemplate.LastUpdatedAtUtc);
		}
		for (const FHyperTwistTrainingMethodDrillBatch& DrillBatch : RepositoryState.StoredMethodDrillBatches)
		{
			ConsiderLatestUtc(LatestUtc, DrillBatch.LastUpdatedAtUtc);
		}
		for (const FHyperTwistTrainingFriendLinkRecord& FriendLinkRecord : RepositoryState.StoredFriendLinks)
		{
			ConsiderLatestUtc(LatestUtc, FriendLinkRecord.RequestedAtUtc);
			ConsiderLatestUtc(LatestUtc, FriendLinkRecord.RespondedAtUtc);
			ConsiderLatestUtc(LatestUtc, FriendLinkRecord.LastUpdatedAtUtc);
			ConsiderLatestUtc(LatestUtc, FriendLinkRecord.FriendSnapshot.LastPublishedAtUtc);
		}
		for (const FHyperTwistTrainingNotificationRecord& NotificationRecord : RepositoryState.StoredNotifications)
		{
			ConsiderLatestUtc(LatestUtc, NotificationRecord.CreatedAtUtc);
			ConsiderLatestUtc(LatestUtc, NotificationRecord.ReadAtUtc);
			ConsiderLatestUtc(LatestUtc, NotificationRecord.AcknowledgedAtUtc);
			ConsiderLatestUtc(LatestUtc, NotificationRecord.ArchivedAtUtc);
		}
		for (const FHyperTwistTrainingChallengeRecord& ChallengeRecord : RepositoryState.StoredChallenges)
		{
			ConsiderLatestUtc(LatestUtc, ChallengeRecord.CreatedAtUtc);
			ConsiderLatestUtc(LatestUtc, ChallengeRecord.LastUpdatedAtUtc);
			ConsiderLatestUtc(LatestUtc, ChallengeRecord.RespondedAtUtc);
			ConsiderLatestUtc(LatestUtc, ChallengeRecord.ReadyAtUtc);
			ConsiderLatestUtc(LatestUtc, ChallengeRecord.CompletedAtUtc);
			ConsiderLatestUtc(LatestUtc, ChallengeRecord.ExpiresAtUtc);
			ConsiderLatestUtc(LatestUtc, ChallengeRecord.ChallengerSnapshot.ReferenceEntry.RecordedAtUtc);
			ConsiderLatestUtc(LatestUtc, ChallengeRecord.RecipientSnapshot.ReferenceEntry.RecordedAtUtc);
		}
		for (const FHyperTwistTrainingCoachActionPlan& ActionPlan : RepositoryState.StoredCoachActionPlans)
		{
			ConsiderLatestUtc(LatestUtc, ActionPlan.CompletedAtUtc);
			ConsiderLatestUtc(LatestUtc, ActionPlan.StartedAtUtc);
			ConsiderLatestUtc(LatestUtc, ActionPlan.CreatedAtUtc);
			ConsiderLatestUtc(LatestUtc, ActionPlan.ReferenceUtc);
		}
		for (const FHyperTwistTrainingCoachSessionQueueState& QueueState : RepositoryState.StoredCoachSessionQueues)
		{
			ConsiderLatestUtc(LatestUtc, QueueState.LastUpdatedAtUtc);
			ConsiderLatestUtc(LatestUtc, QueueState.ReferenceUtc);
		}
		for (const FHyperTwistTrainingCoachSessionQueueHistoryEntry& HistoryEntry : RepositoryState.StoredCoachSessionQueueHistory)
		{
			ConsiderLatestUtc(LatestUtc, HistoryEntry.EventAtUtc);
		}
		for (const FHyperTwistTrainingSmartDeviceSessionRecord& SessionRecord : RepositoryState.StoredSmartDeviceSessions)
		{
			ConsiderLatestUtc(LatestUtc, SessionRecord.StartedAtUtc);
			ConsiderLatestUtc(LatestUtc, SessionRecord.LastUpdatedAtUtc);
			ConsiderLatestUtc(LatestUtc, SessionRecord.EndedAtUtc);
		}
		for (const FHyperTwistTrainingSmartDeviceEventFrame& EventFrame : RepositoryState.StoredSmartDeviceEventFrames)
		{
			ConsiderLatestUtc(LatestUtc, EventFrame.OccurredAtUtc);
		}

		return LatestUtc.IsEmpty() ? FDateTime::UtcNow().ToIso8601() : LatestUtc;
	}

	void AppendRepositoryUserIds(
		const FHyperTwistTrainingRepositoryState& RepositoryState,
		TSet<FString>& OutUserIds
	)
	{
		for (const FHyperTwistTrainingRunRecord& RunRecord : RepositoryState.RunRecords)
		{
			if (!RunRecord.Session.UserId.IsEmpty())
			{
				OutUserIds.Add(RunRecord.Session.UserId);
			}
		}
		for (const FHyperTwistTrainingCaseLearnerState& LearnerState : RepositoryState.LearnerStates)
		{
			if (!LearnerState.UserId.IsEmpty())
			{
				OutUserIds.Add(LearnerState.UserId);
			}
		}
		for (const FHyperTwistTrainingReviewPlanState& ReviewPlan : RepositoryState.ReviewPlans)
		{
			if (!ReviewPlan.UserId.IsEmpty())
			{
				OutUserIds.Add(ReviewPlan.UserId);
			}
		}
		for (const FHyperTwistTrainingSessionTemplate& SessionTemplate : RepositoryState.StoredSessionTemplates)
		{
			if (!SessionTemplate.UserId.IsEmpty())
			{
				OutUserIds.Add(SessionTemplate.UserId);
			}
		}
		for (const FHyperTwistTrainingMethodDrillFavoriteEntry& FavoriteEntry : RepositoryState.StoredMethodDrillFavorites)
		{
			if (!FavoriteEntry.UserId.IsEmpty())
			{
				OutUserIds.Add(FavoriteEntry.UserId);
			}
		}
		for (const FHyperTwistTrainingMethodDrillBatch& DrillBatch : RepositoryState.StoredMethodDrillBatches)
		{
			if (!DrillBatch.UserId.IsEmpty())
			{
				OutUserIds.Add(DrillBatch.UserId);
			}
		}
		for (const FHyperTwistTrainingFriendLinkRecord& FriendLinkRecord : RepositoryState.StoredFriendLinks)
		{
			if (!FriendLinkRecord.UserId.IsEmpty())
			{
				OutUserIds.Add(FriendLinkRecord.UserId);
			}
		}
		for (const FHyperTwistTrainingNotificationRecord& NotificationRecord : RepositoryState.StoredNotifications)
		{
			if (!NotificationRecord.RecipientUserId.IsEmpty())
			{
				OutUserIds.Add(NotificationRecord.RecipientUserId);
			}
		}
		for (const FHyperTwistTrainingChallengeRecord& ChallengeRecord : RepositoryState.StoredChallenges)
		{
			if (!ChallengeRecord.ChallengerUserId.IsEmpty())
			{
				OutUserIds.Add(ChallengeRecord.ChallengerUserId);
			}
			if (!ChallengeRecord.RecipientUserId.IsEmpty())
			{
				OutUserIds.Add(ChallengeRecord.RecipientUserId);
			}
		}
		for (const FHyperTwistTrainingCoachActionPlan& ActionPlan : RepositoryState.StoredCoachActionPlans)
		{
			if (!ActionPlan.UserId.IsEmpty())
			{
				OutUserIds.Add(ActionPlan.UserId);
			}
		}
		for (const FHyperTwistTrainingCoachSessionQueueState& QueueState : RepositoryState.StoredCoachSessionQueues)
		{
			if (!QueueState.UserId.IsEmpty())
			{
				OutUserIds.Add(QueueState.UserId);
			}
		}
		for (const FHyperTwistTrainingCoachSessionQueueHistoryEntry& HistoryEntry : RepositoryState.StoredCoachSessionQueueHistory)
		{
			if (!HistoryEntry.UserId.IsEmpty())
			{
				OutUserIds.Add(HistoryEntry.UserId);
			}
		}
		for (const FHyperTwistTrainingSmartDeviceSessionRecord& SessionRecord : RepositoryState.StoredSmartDeviceSessions)
		{
			if (!SessionRecord.UserId.IsEmpty())
			{
				OutUserIds.Add(SessionRecord.UserId);
			}
		}
		for (const FHyperTwistTrainingSmartDeviceEventFrame& EventFrame : RepositoryState.StoredSmartDeviceEventFrames)
		{
			if (!EventFrame.UserId.IsEmpty())
			{
				OutUserIds.Add(EventFrame.UserId);
			}
		}
	}

	FString MakeQueueVerificationKey(const FString& UserId, const FString& QueueId)
	{
		return FString::Printf(TEXT("%s\t%s"), *UserId, *QueueId);
	}

	void ParseQueueVerificationKey(
		const FString& QueueKey,
		FString& OutUserId,
		FString& OutQueueId
	)
	{
		OutUserId.Reset();
		OutQueueId.Reset();

		int32 SeparatorIndex = INDEX_NONE;
		if (QueueKey.FindChar(TEXT('\t'), SeparatorIndex))
		{
			OutUserId = QueueKey.Left(SeparatorIndex);
			OutQueueId = QueueKey.Mid(SeparatorIndex + 1);
		}
	}

	void AppendRepositoryQueueKeys(
		const FHyperTwistTrainingRepositoryState& RepositoryState,
		TSet<FString>& OutQueueKeys
	)
	{
		for (const FHyperTwistTrainingCoachSessionQueueState& QueueState : RepositoryState.StoredCoachSessionQueues)
		{
			if (!QueueState.UserId.IsEmpty() && !QueueState.QueueId.IsEmpty())
			{
				OutQueueKeys.Add(MakeQueueVerificationKey(QueueState.UserId, QueueState.QueueId));
			}
		}
	}

	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& RepositoryPath)
	{
		if (RepositoryPath.IsEmpty())
		{
			return FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("HyperTwist"), TEXT("training-repository.json"));
		}

		return FPaths::IsRelative(RepositoryPath)
			? FPaths::Combine(FPaths::ProjectSavedDir(), RepositoryPath)
			: RepositoryPath;
	}

	FString ResolveVerificationPath(const FString& RepositoryPath)
	{
		if (RepositoryPath.IsEmpty())
		{
			return FPaths::Combine(
				FPaths::ProjectSavedDir(),
				TEXT("HyperTwist"),
				TEXT("training-repository-roundtrip.json")
			);
		}

		return FPaths::IsRelative(RepositoryPath)
			? FPaths::Combine(FPaths::ProjectSavedDir(), RepositoryPath)
			: RepositoryPath;
	}

	FString ResolveTimerExportPath(
		const FString& UserId,
		const FString& DeckId,
		const FString& ExportPath
	)
	{
		if (ExportPath.IsEmpty())
		{
			return FPaths::Combine(
				FPaths::ProjectSavedDir(),
				TEXT("HyperTwist"),
				TEXT("timer-exports"),
				FString::Printf(
					TEXT("%s__%s.json"),
					*SanitizePathToken(UserId),
					*SanitizePathToken(DeckId)
				)
			);
		}

		return FPaths::IsRelative(ExportPath)
			? FPaths::Combine(FPaths::ProjectSavedDir(), ExportPath)
			: ExportPath;
	}

	void AppendVerificationNote(TArray<FString>& Notes, const FString& Note)
	{
		if (!Note.IsEmpty())
		{
			Notes.Add(Note);
		}
	}
}

FString UHyperTwistTrainingPersistenceLibrary::GetDefaultTrainingRepositoryPath()
{
	return HyperTwistTrainingPersistenceLibraryInternal::ResolvePath(FString());
}

FString UHyperTwistTrainingPersistenceLibrary::ResolveTrainingRepositoryPath(const FString& RepositoryPath)
{
	return HyperTwistTrainingPersistenceLibraryInternal::ResolvePath(RepositoryPath);
}

FString UHyperTwistTrainingPersistenceLibrary::GetDefaultTrainingRepositoryVerificationPath()
{
	return HyperTwistTrainingPersistenceLibraryInternal::ResolveVerificationPath(FString());
}

FString UHyperTwistTrainingPersistenceLibrary::ResolveTrainingRepositoryVerificationPath(const FString& RepositoryPath)
{
	return HyperTwistTrainingPersistenceLibraryInternal::ResolveVerificationPath(RepositoryPath);
}

FString UHyperTwistTrainingPersistenceLibrary::GetDefaultTrainingTimerExportPath(const FString& UserId, const FString& DeckId)
{
	return HyperTwistTrainingPersistenceLibraryInternal::ResolveTimerExportPath(UserId, DeckId, FString());
}

FString UHyperTwistTrainingPersistenceLibrary::ResolveTrainingTimerExportPath(
	const FString& UserId,
	const FString& DeckId,
	const FString& ExportPath
)
{
	return HyperTwistTrainingPersistenceLibraryInternal::ResolveTimerExportPath(UserId, DeckId, ExportPath);
}

bool UHyperTwistTrainingPersistenceLibrary::SaveTrainingRepositoryStateToFile(
	const FHyperTwistTrainingRepositoryState& RepositoryState,
	const FString& RepositoryPath,
	FString& OutResolvedPath
)
{
	OutResolvedPath = ResolveTrainingRepositoryPath(RepositoryPath);
	const FString Directory = FPaths::GetPath(OutResolvedPath);
	if (!Directory.IsEmpty())
	{
		IFileManager::Get().MakeDirectory(*Directory, true);
	}

	const FHyperTwistTrainingRepositoryState RepairedRepositoryState =
		UHyperTwistTrainingRepositoryLibrary::RepairRepositoryState(RepositoryState);
	const FString Json = UHyperTwistContractLibrary::SerializeTrainingRepositoryStateToJson(
		RepairedRepositoryState
	);
	return !Json.IsEmpty() && FFileHelper::SaveStringToFile(Json, *OutResolvedPath);
}

bool UHyperTwistTrainingPersistenceLibrary::SaveTrainingTimerExportPacketToFile(
	const FHyperTwistTrainingTimerExportPacket& TimerExportPacket,
	const FString& ExportPath,
	FString& OutResolvedPath
)
{
	const FHyperTwistTrainingTimerExportPacket NormalizedPacket =
		UHyperTwistTrainingRepositoryLibrary::NormalizeTimerExportPacket(TimerExportPacket);
	OutResolvedPath = ResolveTrainingTimerExportPath(
		NormalizedPacket.UserId,
		NormalizedPacket.DeckId,
		ExportPath
	);
	const FString Directory = FPaths::GetPath(OutResolvedPath);
	if (!Directory.IsEmpty())
	{
		IFileManager::Get().MakeDirectory(*Directory, true);
	}

	const FString Json = UHyperTwistContractLibrary::SerializeTrainingTimerExportPacketToJson(NormalizedPacket);
	return !Json.IsEmpty() && FFileHelper::SaveStringToFile(Json, *OutResolvedPath);
}

bool UHyperTwistTrainingPersistenceLibrary::LoadTrainingRepositoryStateFromFile(
	const FString& RepositoryPath,
	FHyperTwistTrainingRepositoryState& OutRepositoryState,
	FString& OutResolvedPath
)
{
	OutRepositoryState = FHyperTwistTrainingRepositoryState();
	OutResolvedPath = ResolveTrainingRepositoryPath(RepositoryPath);

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

	if (!UHyperTwistContractLibrary::DeserializeTrainingRepositoryStateFromJson(Json, OutRepositoryState))
	{
		return false;
	}

	OutRepositoryState = UHyperTwistTrainingRepositoryLibrary::RepairRepositoryState(OutRepositoryState);
	return true;
}

FHyperTwistTrainingRepositoryRoundTripVerification
UHyperTwistTrainingPersistenceLibrary::VerifyTrainingRepositoryStateRoundTrip(
	const FHyperTwistTrainingRepositoryState& RepositoryState,
	const FString& RepositoryPath
)
{
	FHyperTwistTrainingRepositoryRoundTripVerification Verification;
	Verification.ResolvedPath = ResolveTrainingRepositoryVerificationPath(RepositoryPath);

	const FHyperTwistTrainingRepositoryState ExpectedRepositoryState =
		UHyperTwistTrainingRepositoryLibrary::RepairRepositoryState(RepositoryState);
	Verification.ExpectedRunRecordCount = ExpectedRepositoryState.RunRecords.Num();
	Verification.ExpectedLearnerStateCount = ExpectedRepositoryState.LearnerStates.Num();
	Verification.ExpectedReviewPlanCount = ExpectedRepositoryState.ReviewPlans.Num();
	Verification.ExpectedStoredSessionTemplateCount = ExpectedRepositoryState.StoredSessionTemplates.Num();
	Verification.ExpectedMethodDrillFavoriteCount = ExpectedRepositoryState.StoredMethodDrillFavorites.Num();
	Verification.ExpectedMethodDrillBatchCount = ExpectedRepositoryState.StoredMethodDrillBatches.Num();
	Verification.ExpectedFriendLinkCount = ExpectedRepositoryState.StoredFriendLinks.Num();
	Verification.ExpectedNotificationCount = ExpectedRepositoryState.StoredNotifications.Num();
	Verification.ExpectedChallengeCount = ExpectedRepositoryState.StoredChallenges.Num();
	Verification.ExpectedCoachActionPlanCount = ExpectedRepositoryState.StoredCoachActionPlans.Num();
	Verification.ExpectedCoachSessionQueueCount = ExpectedRepositoryState.StoredCoachSessionQueues.Num();
	Verification.ExpectedCoachSessionQueueHistoryCount =
		ExpectedRepositoryState.StoredCoachSessionQueueHistory.Num();
	Verification.ExpectedSmartDeviceSessionCount = ExpectedRepositoryState.StoredSmartDeviceSessions.Num();
	Verification.ExpectedSmartDeviceEventFrameCount =
		ExpectedRepositoryState.StoredSmartDeviceEventFrames.Num();
	Verification.ExpectedIntegrityReport =
		UHyperTwistTrainingRepositoryLibrary::DeriveRepositoryIntegrityReport(ExpectedRepositoryState);

	FString SavedPath;
	Verification.bSaveSucceeded = SaveTrainingRepositoryStateToFile(
		ExpectedRepositoryState,
		Verification.ResolvedPath,
		SavedPath
	);
	Verification.ResolvedPath = SavedPath;
	if (!Verification.bSaveSucceeded)
	{
		HyperTwistTrainingPersistenceLibraryInternal::AppendVerificationNote(
			Verification.VerificationNotes,
			TEXT("roundtrip-save-failed")
		);
		return Verification;
	}

	FHyperTwistTrainingRepositoryState LoadedRepositoryState;
	FString LoadedPath;
	Verification.bLoadSucceeded = LoadTrainingRepositoryStateFromFile(
		Verification.ResolvedPath,
		LoadedRepositoryState,
		LoadedPath
	);
	Verification.ResolvedPath = LoadedPath;
	if (!Verification.bLoadSucceeded)
	{
		HyperTwistTrainingPersistenceLibraryInternal::AppendVerificationNote(
			Verification.VerificationNotes,
			TEXT("roundtrip-load-failed")
		);
		return Verification;
	}

	Verification.LoadedRunRecordCount = LoadedRepositoryState.RunRecords.Num();
	Verification.LoadedLearnerStateCount = LoadedRepositoryState.LearnerStates.Num();
	Verification.LoadedReviewPlanCount = LoadedRepositoryState.ReviewPlans.Num();
	Verification.LoadedStoredSessionTemplateCount = LoadedRepositoryState.StoredSessionTemplates.Num();
	Verification.LoadedMethodDrillFavoriteCount = LoadedRepositoryState.StoredMethodDrillFavorites.Num();
	Verification.LoadedMethodDrillBatchCount = LoadedRepositoryState.StoredMethodDrillBatches.Num();
	Verification.LoadedFriendLinkCount = LoadedRepositoryState.StoredFriendLinks.Num();
	Verification.LoadedNotificationCount = LoadedRepositoryState.StoredNotifications.Num();
	Verification.LoadedChallengeCount = LoadedRepositoryState.StoredChallenges.Num();
	Verification.LoadedCoachActionPlanCount = LoadedRepositoryState.StoredCoachActionPlans.Num();
	Verification.LoadedCoachSessionQueueCount = LoadedRepositoryState.StoredCoachSessionQueues.Num();
	Verification.LoadedCoachSessionQueueHistoryCount =
		LoadedRepositoryState.StoredCoachSessionQueueHistory.Num();
	Verification.LoadedSmartDeviceSessionCount = LoadedRepositoryState.StoredSmartDeviceSessions.Num();
	Verification.LoadedSmartDeviceEventFrameCount =
		LoadedRepositoryState.StoredSmartDeviceEventFrames.Num();
	Verification.LoadedIntegrityReport =
		UHyperTwistTrainingRepositoryLibrary::DeriveRepositoryIntegrityReport(LoadedRepositoryState);

	Verification.bRepositoryCountsMatch =
		Verification.ExpectedRunRecordCount == Verification.LoadedRunRecordCount
		&& Verification.ExpectedLearnerStateCount == Verification.LoadedLearnerStateCount
		&& Verification.ExpectedReviewPlanCount == Verification.LoadedReviewPlanCount
		&& Verification.ExpectedStoredSessionTemplateCount == Verification.LoadedStoredSessionTemplateCount
		&& Verification.ExpectedMethodDrillFavoriteCount == Verification.LoadedMethodDrillFavoriteCount
		&& Verification.ExpectedMethodDrillBatchCount == Verification.LoadedMethodDrillBatchCount
		&& Verification.ExpectedFriendLinkCount == Verification.LoadedFriendLinkCount
		&& Verification.ExpectedNotificationCount == Verification.LoadedNotificationCount
		&& Verification.ExpectedChallengeCount == Verification.LoadedChallengeCount
		&& Verification.ExpectedCoachActionPlanCount == Verification.LoadedCoachActionPlanCount
		&& Verification.ExpectedCoachSessionQueueCount == Verification.LoadedCoachSessionQueueCount
		&& Verification.ExpectedCoachSessionQueueHistoryCount
			== Verification.LoadedCoachSessionQueueHistoryCount
		&& Verification.ExpectedSmartDeviceSessionCount == Verification.LoadedSmartDeviceSessionCount
		&& Verification.ExpectedSmartDeviceEventFrameCount
			== Verification.LoadedSmartDeviceEventFrameCount;

	if (!Verification.bRepositoryCountsMatch)
	{
		HyperTwistTrainingPersistenceLibraryInternal::AppendVerificationNote(
			Verification.VerificationNotes,
			TEXT("roundtrip-count-mismatch")
		);
	}

	const FString ExpectedRepositoryJson =
		UHyperTwistContractLibrary::SerializeTrainingRepositoryStateToJson(ExpectedRepositoryState);
	const FString LoadedRepositoryJson =
		UHyperTwistContractLibrary::SerializeTrainingRepositoryStateToJson(LoadedRepositoryState);
	Verification.bSerializedStateMatch =
		!ExpectedRepositoryJson.IsEmpty()
		&& !LoadedRepositoryJson.IsEmpty()
		&& ExpectedRepositoryJson == LoadedRepositoryJson;
	if (!Verification.bSerializedStateMatch)
	{
		HyperTwistTrainingPersistenceLibraryInternal::AppendVerificationNote(
			Verification.VerificationNotes,
			TEXT("roundtrip-serialized-state-mismatch")
		);
	}

	const FString ExpectedIntegrityJson =
		HyperTwistTrainingPersistenceLibraryInternal::SerializeStructToJson(Verification.ExpectedIntegrityReport);
	const FString LoadedIntegrityJson =
		HyperTwistTrainingPersistenceLibraryInternal::SerializeStructToJson(Verification.LoadedIntegrityReport);
	Verification.bIntegrityReportMatch =
		!ExpectedIntegrityJson.IsEmpty()
		&& !LoadedIntegrityJson.IsEmpty()
		&& ExpectedIntegrityJson == LoadedIntegrityJson;
	if (!Verification.bIntegrityReportMatch)
	{
		HyperTwistTrainingPersistenceLibraryInternal::AppendVerificationNote(
			Verification.VerificationNotes,
			TEXT("roundtrip-integrity-report-mismatch")
		);
	}

	Verification.bDerivedStateMatch = true;
	TArray<FString> DerivedVerificationNotes;
	const FString DerivedReferenceUtc =
		HyperTwistTrainingPersistenceLibraryInternal::ResolveDerivedVerificationReferenceUtc(
			ExpectedRepositoryState
		);

	TSet<FString> RepositoryUserIds;
	HyperTwistTrainingPersistenceLibraryInternal::AppendRepositoryUserIds(
		ExpectedRepositoryState,
		RepositoryUserIds
	);
	HyperTwistTrainingPersistenceLibraryInternal::AppendRepositoryUserIds(
		LoadedRepositoryState,
		RepositoryUserIds
	);
	Verification.VerifiedDerivedUserCount = RepositoryUserIds.Num();
	for (const FString& UserId : RepositoryUserIds)
	{
		if (UserId.IsEmpty())
		{
			continue;
		}

		Verification.bDerivedStateMatch =
			HyperTwistTrainingPersistenceLibraryInternal::CheckDerivedStructParity(
				UHyperTwistTrainingRepositoryLibrary::DeriveLearnerProfile(
					ExpectedRepositoryState,
					UserId,
					DerivedReferenceUtc
				),
				UHyperTwistTrainingRepositoryLibrary::DeriveLearnerProfile(
					LoadedRepositoryState,
					UserId,
					DerivedReferenceUtc
				),
				DerivedVerificationNotes,
				FString::Printf(TEXT("roundtrip-derived-learner-profile-mismatch:%s"), *UserId)
			)
			&& Verification.bDerivedStateMatch;
		Verification.bDerivedStateMatch =
			HyperTwistTrainingPersistenceLibraryInternal::CheckDerivedStructParity(
				UHyperTwistTrainingRepositoryLibrary::DeriveReviewProgramSummary(
					ExpectedRepositoryState,
					UserId,
					DerivedReferenceUtc
				),
				UHyperTwistTrainingRepositoryLibrary::DeriveReviewProgramSummary(
					LoadedRepositoryState,
					UserId,
					DerivedReferenceUtc
				),
				DerivedVerificationNotes,
				FString::Printf(TEXT("roundtrip-derived-review-program-mismatch:%s"), *UserId)
			)
			&& Verification.bDerivedStateMatch;
		Verification.bDerivedStateMatch =
			HyperTwistTrainingPersistenceLibraryInternal::CheckDerivedStructParity(
				UHyperTwistTrainingRepositoryLibrary::DeriveCoachMemorySnapshot(
					ExpectedRepositoryState,
					UserId,
					DerivedReferenceUtc
				),
				UHyperTwistTrainingRepositoryLibrary::DeriveCoachMemorySnapshot(
					LoadedRepositoryState,
					UserId,
					DerivedReferenceUtc
				),
				DerivedVerificationNotes,
				FString::Printf(TEXT("roundtrip-derived-coach-memory-mismatch:%s"), *UserId)
			)
			&& Verification.bDerivedStateMatch;
		Verification.bDerivedStateMatch =
			HyperTwistTrainingPersistenceLibraryInternal::CheckDerivedStructParity(
				UHyperTwistTrainingRepositoryLibrary::DeriveCoachActionPlanSummary(
					ExpectedRepositoryState,
					UserId,
					DerivedReferenceUtc
				),
				UHyperTwistTrainingRepositoryLibrary::DeriveCoachActionPlanSummary(
					LoadedRepositoryState,
					UserId,
					DerivedReferenceUtc
				),
				DerivedVerificationNotes,
				FString::Printf(TEXT("roundtrip-derived-coach-plan-summary-mismatch:%s"), *UserId)
			)
			&& Verification.bDerivedStateMatch;
		Verification.bDerivedStateMatch =
			HyperTwistTrainingPersistenceLibraryInternal::CheckDerivedStructParity(
				UHyperTwistTrainingRepositoryLibrary::DeriveCoachClosureMemorySummary(
					ExpectedRepositoryState,
					UserId,
					DerivedReferenceUtc
				),
				UHyperTwistTrainingRepositoryLibrary::DeriveCoachClosureMemorySummary(
					LoadedRepositoryState,
					UserId,
					DerivedReferenceUtc
				),
				DerivedVerificationNotes,
				FString::Printf(TEXT("roundtrip-derived-coach-closure-mismatch:%s"), *UserId)
			)
			&& Verification.bDerivedStateMatch;
		Verification.bDerivedStateMatch =
			HyperTwistTrainingPersistenceLibraryInternal::CheckDerivedStructParity(
				UHyperTwistTrainingRepositoryLibrary::DeriveCoachRecommendationHistorySummary(
					ExpectedRepositoryState,
					UserId,
					DerivedReferenceUtc
				),
				UHyperTwistTrainingRepositoryLibrary::DeriveCoachRecommendationHistorySummary(
					LoadedRepositoryState,
					UserId,
					DerivedReferenceUtc
				),
				DerivedVerificationNotes,
				FString::Printf(TEXT("roundtrip-derived-coach-recommendation-history-mismatch:%s"), *UserId)
			)
			&& Verification.bDerivedStateMatch;
		Verification.bDerivedStateMatch =
			HyperTwistTrainingPersistenceLibraryInternal::CheckDerivedStructParity(
				UHyperTwistTrainingRepositoryLibrary::DeriveCoachCarryForwardPolicySummary(
					ExpectedRepositoryState,
					UserId,
					DerivedReferenceUtc
				),
				UHyperTwistTrainingRepositoryLibrary::DeriveCoachCarryForwardPolicySummary(
					LoadedRepositoryState,
					UserId,
					DerivedReferenceUtc
				),
				DerivedVerificationNotes,
				FString::Printf(TEXT("roundtrip-derived-carry-forward-policy-mismatch:%s"), *UserId)
			)
			&& Verification.bDerivedStateMatch;
		Verification.bDerivedStateMatch =
			HyperTwistTrainingPersistenceLibraryInternal::CheckDerivedStructParity(
				UHyperTwistTrainingRepositoryLibrary::DeriveCoachArchivePolicySummary(
					ExpectedRepositoryState,
					UserId,
					DerivedReferenceUtc
				),
				UHyperTwistTrainingRepositoryLibrary::DeriveCoachArchivePolicySummary(
					LoadedRepositoryState,
					UserId,
					DerivedReferenceUtc
				),
				DerivedVerificationNotes,
				FString::Printf(TEXT("roundtrip-derived-archive-policy-mismatch:%s"), *UserId)
			)
			&& Verification.bDerivedStateMatch;
	}

	TSet<FString> QueueVerificationKeys;
	HyperTwistTrainingPersistenceLibraryInternal::AppendRepositoryQueueKeys(
		ExpectedRepositoryState,
		QueueVerificationKeys
	);
	HyperTwistTrainingPersistenceLibraryInternal::AppendRepositoryQueueKeys(
		LoadedRepositoryState,
		QueueVerificationKeys
	);
	Verification.VerifiedDerivedQueueCount = QueueVerificationKeys.Num();
	for (const FString& QueueKey : QueueVerificationKeys)
	{
		FString UserId;
		FString QueueId;
		HyperTwistTrainingPersistenceLibraryInternal::ParseQueueVerificationKey(
			QueueKey,
			UserId,
			QueueId
		);
		if (UserId.IsEmpty() || QueueId.IsEmpty())
		{
			continue;
		}

		Verification.bDerivedStateMatch =
			HyperTwistTrainingPersistenceLibraryInternal::CheckDerivedStructParity(
				UHyperTwistTrainingRepositoryLibrary::DeriveCoachSessionQueueExecutionSummary(
					ExpectedRepositoryState,
					UserId,
					QueueId,
					DerivedReferenceUtc
				),
				UHyperTwistTrainingRepositoryLibrary::DeriveCoachSessionQueueExecutionSummary(
					LoadedRepositoryState,
					UserId,
					QueueId,
					DerivedReferenceUtc
				),
				DerivedVerificationNotes,
				FString::Printf(
					TEXT("roundtrip-derived-queue-execution-mismatch:%s:%s"),
					*UserId,
					*QueueId
				)
			)
			&& Verification.bDerivedStateMatch;
	}

	if (!Verification.bDerivedStateMatch)
	{
		HyperTwistTrainingPersistenceLibraryInternal::AppendVerificationNote(
			Verification.VerificationNotes,
			TEXT("roundtrip-derived-state-mismatch")
		);
		for (const FString& Note : DerivedVerificationNotes)
		{
			HyperTwistTrainingPersistenceLibraryInternal::AppendVerificationNote(
				Verification.VerificationNotes,
				Note
			);
		}
	}

	if (Verification.LoadedIntegrityReport.HasIntegrityIssues())
	{
		HyperTwistTrainingPersistenceLibraryInternal::AppendVerificationNote(
			Verification.VerificationNotes,
			TEXT("roundtrip-loaded-state-still-has-integrity-issues")
		);
	}

	Verification.bDidRoundTripPass =
		Verification.bSaveSucceeded
		&& Verification.bLoadSucceeded
		&& Verification.bRepositoryCountsMatch
		&& Verification.bSerializedStateMatch
		&& Verification.bIntegrityReportMatch
		&& Verification.bDerivedStateMatch
		&& !Verification.LoadedIntegrityReport.HasIntegrityIssues();
	return Verification;
}

bool UHyperTwistTrainingPersistenceLibrary::LoadTrainingTimerExportPacketFromFile(
	const FString& UserId,
	const FString& DeckId,
	const FString& ExportPath,
	FHyperTwistTrainingTimerExportPacket& OutTimerExportPacket,
	FString& OutResolvedPath
)
{
	OutTimerExportPacket = FHyperTwistTrainingTimerExportPacket();
	OutResolvedPath = ResolveTrainingTimerExportPath(UserId, DeckId, ExportPath);

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

	if (!UHyperTwistContractLibrary::DeserializeTrainingTimerExportPacketFromJson(Json, OutTimerExportPacket))
	{
		return false;
	}

	OutTimerExportPacket = UHyperTwistTrainingRepositoryLibrary::NormalizeTimerExportPacket(
		OutTimerExportPacket
	);
	return true;
}
