#include "HyperTwistTraining/HyperTwistTrainingCatalogLibrary.h"

#include "HyperTwistTraining/HyperTwistTrainingLibrary.h"
#include "JsonObjectConverter.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"

namespace HyperTwistTrainingCatalogLibraryInternal
{
	FHyperTwistRetainedHyperPuzzleCatalogEntry MakeRetainedHyperCatalogEntry(
		const FString& PuzzleId,
		const FString& DisplayName,
		const TArray<int32>& SizeVector,
		const FString& Variant,
		const TArray<FString>& Tags,
		const TArray<FString>& AliasPuzzleIds
	)
	{
		FHyperTwistRetainedHyperPuzzleCatalogEntry Entry;
		Entry.PuzzleId = PuzzleId;
		Entry.DisplayName = DisplayName;
		Entry.Dimension = SizeVector.Num();
		Entry.SizeVector = SizeVector;
		Entry.Variant = Variant;
		Entry.Tags = Tags;
		Entry.AliasPuzzleIds = AliasPuzzleIds;
		return Entry;
	}

	template <typename TStruct>
	bool LoadStructFromJsonFile(
		const FString& JsonPath,
		TStruct& OutValue,
		TArray<FString>& InOutNotes,
		const FString& Label
	)
	{
		FString Json;
		if (!FPaths::FileExists(JsonPath))
		{
			InOutNotes.Add(FString::Printf(TEXT("%s missing: %s"), *Label, *JsonPath));
			return false;
		}

		if (!FFileHelper::LoadFileToString(Json, *JsonPath))
		{
			InOutNotes.Add(FString::Printf(TEXT("%s could not be read: %s"), *Label, *JsonPath));
			return false;
		}

		if (!FJsonObjectConverter::JsonObjectStringToUStruct(Json, &OutValue, 0, 0))
		{
			InOutNotes.Add(FString::Printf(TEXT("%s JSON could not be parsed into the expected import struct: %s"), *Label, *JsonPath));
			return false;
		}

		return true;
	}

	bool TryConvertImportedSourceMode(
		const FString& ImportedSourceMode,
		EHyperTwistTrainingSourceMode& OutSourceMode
	)
	{
		if (ImportedSourceMode.Equals(TEXT("FirstParty"), ESearchCase::IgnoreCase))
		{
			OutSourceMode = EHyperTwistTrainingSourceMode::FirstParty;
			return true;
		}
		if (ImportedSourceMode.Equals(TEXT("DirectDonor"), ESearchCase::IgnoreCase))
		{
			OutSourceMode = EHyperTwistTrainingSourceMode::DirectDonor;
			return true;
		}
		if (ImportedSourceMode.Equals(TEXT("CleanRoom"), ESearchCase::IgnoreCase))
		{
			OutSourceMode = EHyperTwistTrainingSourceMode::CleanRoom;
			return true;
		}
		if (ImportedSourceMode.Equals(TEXT("Benchmark"), ESearchCase::IgnoreCase))
		{
			OutSourceMode = EHyperTwistTrainingSourceMode::Benchmark;
			return true;
		}

		return false;
	}

	bool TryConvertImportedDeliveryMode(
		const FString& ImportedDeliveryMode,
		EHyperTwistTrainingDeliveryMode& OutDeliveryMode
	)
	{
		if (ImportedDeliveryMode.Equals(TEXT("Timer"), ESearchCase::IgnoreCase))
		{
			OutDeliveryMode = EHyperTwistTrainingDeliveryMode::Timer;
			return true;
		}
		if (ImportedDeliveryMode.Equals(TEXT("VirtualCube"), ESearchCase::IgnoreCase))
		{
			OutDeliveryMode = EHyperTwistTrainingDeliveryMode::VirtualCube;
			return true;
		}
		if (ImportedDeliveryMode.Equals(TEXT("SmartDevice"), ESearchCase::IgnoreCase))
		{
			OutDeliveryMode = EHyperTwistTrainingDeliveryMode::SmartDevice;
			return true;
		}
		if (ImportedDeliveryMode.Equals(TEXT("RecognitionAssisted"), ESearchCase::IgnoreCase))
		{
			OutDeliveryMode = EHyperTwistTrainingDeliveryMode::RecognitionAssisted;
			return true;
		}
		if (ImportedDeliveryMode.Equals(TEXT("CoachReviewed"), ESearchCase::IgnoreCase))
		{
			OutDeliveryMode = EHyperTwistTrainingDeliveryMode::CoachReviewed;
			return true;
		}

		return false;
	}

	int32 CountTrackManifests(const FHyperTwistTrainingCatalogContentPackManifestDocument& Document)
	{
		int32 Count = 0;
		for (const FHyperTwistTrainingCatalogImportedContentPackManifest& ContentPack : Document.content_packs)
		{
			Count += ContentPack.TrackManifests.Num();
		}
		return Count;
	}

	bool HasTag(const TArray<FString>& Tags, const FString& Tag)
	{
		return Tags.ContainsByPredicate([&Tag](const FString& Candidate)
		{
			return Candidate.Equals(Tag, ESearchCase::IgnoreCase);
		});
	}

	FString ToTitleLabel(const FString& Value)
	{
		if (Value.IsEmpty())
		{
			return TEXT("Method");
		}

		TArray<FString> Parts;
		Value.Replace(TEXT("-"), TEXT(" ")).Replace(TEXT("_"), TEXT(" ")).ParseIntoArrayWS(Parts);
		FString Label;
		for (int32 Index = 0; Index < Parts.Num(); ++Index)
		{
			FString Part = Parts[Index].ToLower();
			if (Part.Len() <= 4)
			{
				Part = Part.ToUpper();
			}
			else
			{
				Part[0] = FChar::ToUpper(Part[0]);
			}

			if (!Label.IsEmpty())
			{
				Label += TEXT(" ");
			}
			Label += Part;
		}

		return Label.IsEmpty() ? Value : Label;
	}

	FString ResolveMethodKey(const FHyperTwistTrainingDeck& Deck)
	{
		TArray<FString> CandidateTags = Deck.Tags;
		for (const FHyperTwistTrainingCase& TrainingCase : Deck.Cases)
		{
			for (const FString& Tag : TrainingCase.Tags)
			{
				CandidateTags.AddUnique(Tag);
			}
		}

		const TArray<FString> PriorityTags =
		{
			TEXT("5style"),
			TEXT("oll"),
			TEXT("pll"),
			TEXT("f2l"),
			TEXT("cross"),
			TEXT("roux"),
			TEXT("cfop"),
			TEXT("zbll"),
			TEXT("block-building"),
			TEXT("blind"),
			TEXT("lookahead"),
			TEXT("inspection")
		};

		for (const FString& PriorityTag : PriorityTags)
		{
			if (HasTag(CandidateTags, PriorityTag))
			{
				return PriorityTag;
			}
		}

		if (!Deck.SourceAttribution.SourceTrackId.IsEmpty())
		{
			return Deck.SourceAttribution.SourceTrackId.ToLower();
		}

		if (!Deck.SubsetId.IsEmpty())
		{
			return Deck.SubsetId.ToLower();
		}

		if (CandidateTags.Num() > 0)
		{
			for (const FString& Tag : CandidateTags)
			{
				const FString LowerTag = Tag.ToLower();
				if (!LowerTag.StartsWith(TEXT("subset:"))
					&& LowerTag != TEXT("classic")
					&& LowerTag != TEXT("hypercube")
					&& LowerTag != TEXT("seed")
					&& LowerTag != TEXT("template")
					&& !LowerTag.StartsWith(TEXT("template:"))
					&& LowerTag != TEXT("review")
					&& LowerTag != TEXT("recommendation-shaped")
					&& LowerTag != TEXT("alg-trainer"))
				{
					return LowerTag;
				}
			}
		}

		return Deck.DeckId.ToLower();
	}

	FString ResolveMethodLabel(const FString& MethodKey)
	{
		if (MethodKey.Equals(TEXT("oll"), ESearchCase::IgnoreCase))
		{
			return TEXT("OLL");
		}
		if (MethodKey.Equals(TEXT("pll"), ESearchCase::IgnoreCase))
		{
			return TEXT("PLL");
		}
		if (MethodKey.Equals(TEXT("f2l"), ESearchCase::IgnoreCase))
		{
			return TEXT("F2L");
		}
		if (MethodKey.Equals(TEXT("cfop"), ESearchCase::IgnoreCase))
		{
			return TEXT("CFOP");
		}
		if (MethodKey.Equals(TEXT("zbll"), ESearchCase::IgnoreCase))
		{
			return TEXT("ZBLL");
		}
		if (MethodKey.Equals(TEXT("roux"), ESearchCase::IgnoreCase))
		{
			return TEXT("Roux");
		}
		if (MethodKey.Equals(TEXT("cross"), ESearchCase::IgnoreCase))
		{
			return TEXT("Cross");
		}
		if (MethodKey.Equals(TEXT("5style"), ESearchCase::IgnoreCase))
		{
			return TEXT("5-Style");
		}

		return ToTitleLabel(MethodKey);
	}

	bool DoesSelectorOptionMatchCase(
		const FHyperTwistTrainingImportedRuntimeSelectorOption& Option,
		const FHyperTwistTrainingCase& TrainingCase
	)
	{
		if (!Option.IsStructurallyValid() || !TrainingCase.IsStructurallyValid())
		{
			return false;
		}

		if (TrainingCase.SourceAttribution.SourceCaseId.Equals(Option.OptionValue, ESearchCase::IgnoreCase)
			|| TrainingCase.SourceAttribution.SourceCaseId.Equals(Option.OptionId, ESearchCase::IgnoreCase))
		{
			return true;
		}

		const FString ExpectedCaseSuffixA = FString::Printf(TEXT("/%s"), *Option.OptionValue);
		const FString ExpectedCaseSuffixB = FString::Printf(TEXT("/%s"), *Option.OptionId);
		return TrainingCase.CaseId.EndsWith(ExpectedCaseSuffixA, ESearchCase::IgnoreCase)
			|| TrainingCase.CaseId.EndsWith(ExpectedCaseSuffixB, ESearchCase::IgnoreCase);
	}

	FString TemplateKindToTag(EHyperTwistTrainingSessionTemplateKind TemplateKind)
	{
		switch (TemplateKind)
		{
		case EHyperTwistTrainingSessionTemplateKind::ReviewPlan:
			return TEXT("template:review-plan");
		case EHyperTwistTrainingSessionTemplateKind::Recovery:
			return TEXT("template:recovery");
		case EHyperTwistTrainingSessionTemplateKind::Accuracy:
			return TEXT("template:accuracy");
		case EHyperTwistTrainingSessionTemplateKind::Speed:
			return TEXT("template:speed");
		case EHyperTwistTrainingSessionTemplateKind::Coach:
			return TEXT("template:coach");
		default:
			return TEXT("template:unknown");
		}
	}

	FHyperTwistTrainingSourceAttribution MakeAlgTrainerAttribution(
		const FString& TrackId,
		const FString& DeckId,
		const FString& CaseId,
		const FString& Notes
	)
	{
		FHyperTwistTrainingSourceAttribution Attribution;
		Attribution.SourceMode = EHyperTwistTrainingSourceMode::DirectDonor;
		Attribution.SourceRepo = TEXT("tao-yu/Alg-Trainer");
		Attribution.SourceLicense = TEXT("MIT");
		Attribution.SourceTrackId = TrackId;
		Attribution.SourceDeckId = DeckId;
		Attribution.SourceCaseId = CaseId;
		Attribution.SourceNotes = Notes;
		return Attribution;
	}

	FHyperTwistTrainingSourceAttribution MakeCrossTrainerAttribution(
		const FString& DeckId,
		const FString& CaseId,
		const FString& Notes
	)
	{
		FHyperTwistTrainingSourceAttribution Attribution;
		Attribution.SourceMode = EHyperTwistTrainingSourceMode::DirectDonor;
		Attribution.SourceRepo = TEXT("newyork-anthonyng/rubiks-cross-trainer");
		Attribution.SourceLicense = TEXT("MIT");
		Attribution.SourceTrackId = TEXT("cross");
		Attribution.SourceDeckId = DeckId;
		Attribution.SourceCaseId = CaseId;
		Attribution.SourceNotes = Notes;
		return Attribution;
	}

	FHyperTwistTrainingSourceAttribution MakeFiveStyleAttribution(
		const FString& DeckId,
		const FString& CaseId,
		const FString& Notes
	)
	{
		FHyperTwistTrainingSourceAttribution Attribution;
		Attribution.SourceMode = EHyperTwistTrainingSourceMode::DirectDonor;
		Attribution.SourceRepo = TEXT("abunickabhi/5style-Trainer");
		Attribution.SourceLicense = TEXT("MIT");
		Attribution.SourceTrackId = TEXT("5-style");
		Attribution.SourceDeckId = DeckId;
		Attribution.SourceCaseId = CaseId;
		Attribution.SourceNotes = Notes;
		return Attribution;
	}

	FString DecodeCrossTrainerScramble(const FString& EncodedScramble)
	{
		const TArray<FString> MoveNamesWCA =
		{
			TEXT("R"),
			TEXT("R2"),
			TEXT("R'"),
			TEXT("F"),
			TEXT("F2"),
			TEXT("F'"),
			TEXT("L"),
			TEXT("L2"),
			TEXT("L'"),
			TEXT("B"),
			TEXT("B2"),
			TEXT("B'"),
			TEXT("U"),
			TEXT("U2"),
			TEXT("U'"),
			TEXT("D"),
			TEXT("D2"),
			TEXT("D'")
		};

		TArray<FString> Moves;
		for (int32 Index = 0; Index < EncodedScramble.Len(); ++Index)
		{
			const int32 MoveIndex = EncodedScramble[Index] - TEXT('A');
			if (MoveNamesWCA.IsValidIndex(MoveIndex))
			{
				Moves.Add(MoveNamesWCA[MoveIndex]);
			}
		}

		return FString::Join(Moves, TEXT(" "));
	}

	FString NormalizeFiveStyleAlgorithm(const FString& Algorithm)
	{
		FString Normalized = Algorithm;
		Normalized.TrimStartAndEndInline();
		return Normalized;
	}

	FHyperTwistTrainingCase MakeAlgTrainerCase(
		const FString& CaseId,
		const FString& PromptLabel,
		const FString& SubsetId,
		const FString& CanonicalNotation,
		const FString& AlternateNotation,
		const FString& ScrambleNotation,
		float Difficulty,
		int32 TimeTargetMs
	)
	{
		FHyperTwistTrainingCase TrainingCase;
		TrainingCase.CaseId = CaseId;
		TrainingCase.PuzzleId = TEXT("cube/3x3x3");
		TrainingCase.PromptKind = EHyperTwistTrainingPromptKind::Algorithm;
		TrainingCase.PromptLabel = PromptLabel;
		TrainingCase.CanonicalNotation = CanonicalNotation;
		if (!AlternateNotation.IsEmpty())
		{
			TrainingCase.AlternateNotations = {AlternateNotation};
		}
		TrainingCase.ScrambleNotation = ScrambleNotation;
		TrainingCase.SubsetId = SubsetId;
		TrainingCase.Difficulty = Difficulty;
		TrainingCase.Tags = {TEXT("classic"), TEXT("oll"), TEXT("alg-trainer"), FString::Printf(TEXT("subset:%s"), *SubsetId.ToLower())};
		TrainingCase.AllowedDeliveryModes =
		{
			EHyperTwistTrainingDeliveryMode::Timer,
			EHyperTwistTrainingDeliveryMode::VirtualCube,
			EHyperTwistTrainingDeliveryMode::SmartDevice
		};
		TrainingCase.TimeTargetMs = TimeTargetMs;
		TrainingCase.SourceAttribution = MakeAlgTrainerAttribution(
			TEXT("oll"),
			FString::Printf(TEXT("oll-%s"), *SubsetId.ToLower()),
			CaseId,
			TEXT("Starter case shaped from Alg-Trainer subset-based algorithm practice flow.")
		);
		return TrainingCase;
	}

	FHyperTwistTrainingTimingPolicy MakeStandardAlgorithmTimingPolicy()
	{
		FHyperTwistTrainingTimingPolicy TimingPolicy;
		TimingPolicy.bInspectionEnabled = true;
		TimingPolicy.InspectionDurationMs = 15000;
		TimingPolicy.bInspectionPenaltiesEnabled = true;
		TimingPolicy.bAllowManualPenalty = true;
		TimingPolicy.bHideRunningTime = false;

		FHyperTwistTrainingSplitPhaseDefinition RecognitionPhase;
		RecognitionPhase.PhaseId = TEXT("recognition");
		RecognitionPhase.Label = TEXT("Recognition");
		RecognitionPhase.Order = 0;

		FHyperTwistTrainingSplitPhaseDefinition ExecutionPhase;
		ExecutionPhase.PhaseId = TEXT("execution");
		ExecutionPhase.Label = TEXT("Execution");
		ExecutionPhase.Order = 1;

		TimingPolicy.SplitPhases = {RecognitionPhase, ExecutionPhase};
		return TimingPolicy;
	}

	FHyperTwistTrainingTimingPolicy MakeStandardCrossTimingPolicy()
	{
		FHyperTwistTrainingTimingPolicy TimingPolicy;
		TimingPolicy.bInspectionEnabled = true;
		TimingPolicy.InspectionDurationMs = 15000;
		TimingPolicy.bInspectionPenaltiesEnabled = true;
		TimingPolicy.bAllowManualPenalty = true;
		TimingPolicy.bHideRunningTime = false;

		FHyperTwistTrainingSplitPhaseDefinition PlanningPhase;
		PlanningPhase.PhaseId = TEXT("planning");
		PlanningPhase.Label = TEXT("Cross Planning");
		PlanningPhase.Order = 0;

		FHyperTwistTrainingSplitPhaseDefinition ExecutionPhase;
		ExecutionPhase.PhaseId = TEXT("execution");
		ExecutionPhase.Label = TEXT("Cross Execution");
		ExecutionPhase.Order = 1;

		TimingPolicy.SplitPhases = {PlanningPhase, ExecutionPhase};
		return TimingPolicy;
	}

	FHyperTwistTrainingCase MakeCrossTrainerCase(
		const int32 MoveCount,
		const int32 CaseOrdinal,
		const FString& EncodedScramble
	)
	{
		FHyperTwistTrainingCase TrainingCase;
		TrainingCase.CaseId = FString::Printf(TEXT("cross-%dmove-%02d"), MoveCount, CaseOrdinal);
		TrainingCase.PuzzleId = TEXT("cube/3x3x3");
		TrainingCase.PromptKind = EHyperTwistTrainingPromptKind::State;
		TrainingCase.PromptLabel = FString::Printf(TEXT("Cross %d-Move %02d"), MoveCount, CaseOrdinal);
		TrainingCase.CanonicalStateRef = TEXT("cfop/cross");
		TrainingCase.ScrambleNotation = DecodeCrossTrainerScramble(EncodedScramble);
		TrainingCase.SubsetId = FString::Printf(TEXT("%dmove"), MoveCount);
		TrainingCase.Difficulty = static_cast<float>(MoveCount) / 8.0f;
		TrainingCase.Tags =
		{
			TEXT("classic"),
			TEXT("cfop"),
			TEXT("cross"),
			TEXT("rubiks-cross-trainer"),
			FString::Printf(TEXT("subset:%dmove"), MoveCount),
			FString::Printf(TEXT("difficulty:%dmove"), MoveCount)
		};
		TrainingCase.AllowedDeliveryModes =
		{
			EHyperTwistTrainingDeliveryMode::Timer,
			EHyperTwistTrainingDeliveryMode::VirtualCube,
			EHyperTwistTrainingDeliveryMode::RecognitionAssisted
		};
		TrainingCase.TimeTargetMs = 1200 + MoveCount * 450;
		TrainingCase.SourceAttribution = MakeCrossTrainerAttribution(
			FString::Printf(TEXT("cross-%dmove"), MoveCount),
			TrainingCase.CaseId,
			FString::Printf(
				TEXT("Curated CFOP cross scramble from the %d-move difficulty bucket in rubiks-cross-trainer."),
				MoveCount
			)
		);
		return TrainingCase;
	}

	FString ResolveCrossDifficultyBand(const int32 MoveCount)
	{
		if (MoveCount <= 2)
		{
			return TEXT("beginner");
		}
		if (MoveCount <= 5)
		{
			return TEXT("intermediate");
		}
		return TEXT("advanced");
	}

	FHyperTwistTrainingDeck MakeCrossTrainerDeck(
		const int32 MoveCount,
		const TArray<FString>& EncodedScrambles
	)
	{
		FHyperTwistTrainingDeck Deck;
		Deck.DeckId = FString::Printf(TEXT("cross-%dmove"), MoveCount);
		Deck.Title = FString::Printf(TEXT("Cross %d-Move Cases"), MoveCount);
		Deck.DeliveryModes =
		{
			EHyperTwistTrainingDeliveryMode::Timer,
			EHyperTwistTrainingDeliveryMode::VirtualCube,
			EHyperTwistTrainingDeliveryMode::RecognitionAssisted
		};
		Deck.SelectionPolicy = EHyperTwistTrainingSelectionPolicy::Weighted;
		Deck.bUsesRealScrambles = true;
		Deck.bSupportsAlgorithmReveal = false;
		Deck.bSupportsVirtualCube = true;
		Deck.bSupportsSmartCube = false;
		Deck.TimingPolicy = MakeStandardCrossTimingPolicy();
		Deck.DifficultyBand = ResolveCrossDifficultyBand(MoveCount);
		Deck.Tags =
		{
			TEXT("classic"),
			TEXT("cfop"),
			TEXT("cross"),
			TEXT("rubiks-cross-trainer"),
			FString::Printf(TEXT("subset:%dmove"), MoveCount),
			FString::Printf(TEXT("difficulty:%dmove"), MoveCount)
		};
		Deck.SubsetId = FString::Printf(TEXT("%dmove"), MoveCount);
		Deck.SourceAttribution = MakeCrossTrainerAttribution(
			Deck.DeckId,
			TEXT(""),
			FString::Printf(
				TEXT("Curated CFOP cross drill deck shaped from the %d-move scramble bucket in rubiks-cross-trainer."),
				MoveCount
			)
		);

		for (int32 Index = 0; Index < EncodedScrambles.Num(); ++Index)
		{
			Deck.Cases.Add(MakeCrossTrainerCase(MoveCount, Index + 1, EncodedScrambles[Index]));
		}

		return Deck;
	}

	FString ResolveFiveStyleDifficultyBand(const int32 MoveCount)
	{
		return MoveCount <= 5 ? TEXT("advanced") : TEXT("expert");
	}

	FHyperTwistTrainingCase MakeFiveStyleCase(
		const int32 MoveCount,
		const int32 CaseOrdinal,
		const FString& Algorithm
	)
	{
		const FString NormalizedAlgorithm = NormalizeFiveStyleAlgorithm(Algorithm);

		FHyperTwistTrainingCase TrainingCase;
		TrainingCase.CaseId = FString::Printf(TEXT("5style-%dmover-%02d"), MoveCount, CaseOrdinal);
		TrainingCase.PuzzleId = TEXT("cube/3x3x3");
		TrainingCase.PromptKind = EHyperTwistTrainingPromptKind::Algorithm;
		TrainingCase.PromptLabel = FString::Printf(TEXT("5-Style %d-Mover %02d"), MoveCount, CaseOrdinal);
		TrainingCase.CanonicalNotation = NormalizedAlgorithm;
		TrainingCase.SubsetId = FString::Printf(TEXT("%dmover"), MoveCount);
		TrainingCase.Difficulty = static_cast<float>(MoveCount) / 10.0f;
		TrainingCase.Tags =
		{
			TEXT("classic"),
			TEXT("blind"),
			TEXT("5style"),
			TEXT("edge-cycles"),
			TEXT("rubiks-5style-trainer"),
			FString::Printf(TEXT("subset:%dmover"), MoveCount)
		};
		TrainingCase.AllowedDeliveryModes =
		{
			EHyperTwistTrainingDeliveryMode::Timer,
			EHyperTwistTrainingDeliveryMode::VirtualCube,
			EHyperTwistTrainingDeliveryMode::SmartDevice
		};
		TrainingCase.TimeTargetMs = 2600 + (MoveCount - 4) * 550;
		TrainingCase.SourceAttribution = MakeFiveStyleAttribution(
			FString::Printf(TEXT("5style-%dmover"), MoveCount),
			TrainingCase.CaseId,
			FString::Printf(
				TEXT("Curated 5-style edge-cycle algorithm from the %d-mover subset in 5style-Trainer."),
				MoveCount
			)
		);
		return TrainingCase;
	}

	FHyperTwistTrainingDeck MakeFiveStyleDeck(
		const int32 MoveCount,
		const TArray<FString>& Algorithms
	)
	{
		FHyperTwistTrainingDeck Deck;
		Deck.DeckId = FString::Printf(TEXT("5style-%dmover"), MoveCount);
		Deck.Title = FString::Printf(TEXT("5-Style %d-Mover Cases"), MoveCount);
		Deck.DeliveryModes =
		{
			EHyperTwistTrainingDeliveryMode::Timer,
			EHyperTwistTrainingDeliveryMode::VirtualCube,
			EHyperTwistTrainingDeliveryMode::SmartDevice
		};
		Deck.SelectionPolicy = EHyperTwistTrainingSelectionPolicy::Weighted;
		Deck.bUsesRealScrambles = false;
		Deck.bSupportsAlgorithmReveal = true;
		Deck.bSupportsVirtualCube = true;
		Deck.bSupportsSmartCube = true;
		Deck.TimingPolicy = MakeStandardAlgorithmTimingPolicy();
		Deck.DifficultyBand = ResolveFiveStyleDifficultyBand(MoveCount);
		Deck.Tags =
		{
			TEXT("classic"),
			TEXT("blind"),
			TEXT("5style"),
			TEXT("edge-cycles"),
			TEXT("rubiks-5style-trainer"),
			FString::Printf(TEXT("subset:%dmover"), MoveCount)
		};
		Deck.SubsetId = FString::Printf(TEXT("%dmover"), MoveCount);
		Deck.SourceAttribution = MakeFiveStyleAttribution(
			Deck.DeckId,
			TEXT(""),
			FString::Printf(
				TEXT("Curated 5-style drill deck shaped from the %d-mover subset in 5style-Trainer."),
				MoveCount
			)
		);

		for (int32 Index = 0; Index < Algorithms.Num(); ++Index)
		{
			Deck.Cases.Add(MakeFiveStyleCase(MoveCount, Index + 1, Algorithms[Index]));
		}

		return Deck;
	}
}

FHyperTwistRetainedHyperPuzzleCatalog UHyperTwistTrainingCatalogLibrary::MakeRetainedHyperPuzzleCatalog()
{
	FHyperTwistRetainedHyperPuzzleCatalog Catalog;
	Catalog.Entries =
	{
		HyperTwistTrainingCatalogLibraryInternal::MakeRetainedHyperCatalogEntry(
			TEXT("hypercube/3x3x3x3"),
			TEXT("3x3x3x3"),
			{3, 3, 3, 3},
			FString(),
			{TEXT("hypercube"), TEXT("virtual"), TEXT("entry"), TEXT("retained/hyperspeedcube")},
			{TEXT("3x3x3x3")}
		),
		HyperTwistTrainingCatalogLibraryInternal::MakeRetainedHyperCatalogEntry(
			TEXT("hypercube/2x2x2x2"),
			TEXT("2x2x2x2"),
			{2, 2, 2, 2},
			FString(),
			{TEXT("hypercube"), TEXT("virtual"), TEXT("compact"), TEXT("retained/hyperspeedcube")},
			{TEXT("2x2x2x2")}
		),
		HyperTwistTrainingCatalogLibraryInternal::MakeRetainedHyperCatalogEntry(
			TEXT("hypercube/4x4x4x4"),
			TEXT("4x4x4x4"),
			{4, 4, 4, 4},
			FString(),
			{TEXT("hypercube"), TEXT("virtual"), TEXT("extended"), TEXT("retained/hyperspeedcube")},
			{TEXT("4x4x4x4")}
		),
		HyperTwistTrainingCatalogLibraryInternal::MakeRetainedHyperCatalogEntry(
			TEXT("hypercube/3x3x3x3x3"),
			TEXT("3x3x3x3x3"),
			{3, 3, 3, 3, 3},
			FString(),
			{TEXT("hypercube"), TEXT("virtual"), TEXT("higher-dimensional"), TEXT("retained/hyperspeedcube")},
			{TEXT("3x3x3x3x3")}
		),
		HyperTwistTrainingCatalogLibraryInternal::MakeRetainedHyperCatalogEntry(
			TEXT("hypercube/2x2x2x2/physical"),
			TEXT("Physical 2x2x2x2"),
			{2, 2, 2, 2},
			TEXT("physical"),
			{TEXT("hypercube"), TEXT("physical"), TEXT("retained/hyperspeedcube")},
			{TEXT("phys_2x2x2x2")}
		)
	};
	return Catalog;
}

bool UHyperTwistTrainingCatalogLibrary::TryGetRetainedHyperPuzzleCatalogEntry(
	const FString& PuzzleId,
	FHyperTwistRetainedHyperPuzzleCatalogEntry& OutEntry
)
{
	if (PuzzleId.IsEmpty())
	{
		return false;
	}

	const FHyperTwistRetainedHyperPuzzleCatalog Catalog = MakeRetainedHyperPuzzleCatalog();
	for (const FHyperTwistRetainedHyperPuzzleCatalogEntry& Entry : Catalog.Entries)
	{
		if (Entry.MatchesPuzzleId(PuzzleId))
		{
			OutEntry = Entry;
			return true;
		}
	}

	return false;
}

bool UHyperTwistTrainingCatalogLibrary::TryResolveRetainedHyperPuzzleDefinition(
	const FString& PuzzleId,
	FHyperTwistPuzzleDefinitionRef& OutDefinition
)
{
	FHyperTwistRetainedHyperPuzzleCatalogEntry Entry;
	if (!TryGetRetainedHyperPuzzleCatalogEntry(PuzzleId, Entry))
	{
		return false;
	}

	OutDefinition = Entry.ToDefinitionRef();
	return true;
}

FHyperTwistContentPack UHyperTwistTrainingCatalogLibrary::MakeAlgTrainerStarterContentPack()
{
	const FHyperTwistTrainingCase OLLT01 = HyperTwistTrainingCatalogLibraryInternal::MakeAlgTrainerCase(
		TEXT("alg-trainer-oll-t-01"),
		TEXT("OLL T 01"),
		TEXT("T"),
		TEXT("R U R' U' R' F R F'"),
		TEXT("F R U R' U' F'"),
		TEXT("F R U R' U' F' U2 R U R'"),
		0.48f,
		2800
	);

	const FHyperTwistTrainingCase OLLT02 = HyperTwistTrainingCatalogLibraryInternal::MakeAlgTrainerCase(
		TEXT("alg-trainer-oll-t-02"),
		TEXT("OLL T 02"),
		TEXT("T"),
		TEXT("r U R' U' r' F R F'"),
		TEXT("R U R' U' R' F R F'"),
		TEXT("R U2 R' U' R U' R' U2 F R U R'"),
		0.55f,
		3200
	);

	FHyperTwistTrainingDeck TDeck;
	TDeck.DeckId = TEXT("oll-t");
	TDeck.Title = TEXT("OLL T Cases");
	TDeck.DeliveryModes =
	{
		EHyperTwistTrainingDeliveryMode::Timer,
		EHyperTwistTrainingDeliveryMode::VirtualCube,
		EHyperTwistTrainingDeliveryMode::SmartDevice
	};
	TDeck.SelectionPolicy = EHyperTwistTrainingSelectionPolicy::Weighted;
	TDeck.bUsesRealScrambles = true;
	TDeck.bSupportsAlgorithmReveal = true;
	TDeck.bSupportsVirtualCube = true;
	TDeck.bSupportsSmartCube = true;
	TDeck.TimingPolicy = HyperTwistTrainingCatalogLibraryInternal::MakeStandardAlgorithmTimingPolicy();
	TDeck.Cases = {OLLT01, OLLT02};
	TDeck.DifficultyBand = TEXT("intermediate");
	TDeck.Tags = {TEXT("classic"), TEXT("oll"), TEXT("alg-trainer"), TEXT("subset:t")};
	TDeck.SubsetId = TEXT("T");
	TDeck.SourceAttribution = HyperTwistTrainingCatalogLibraryInternal::MakeAlgTrainerAttribution(
		TEXT("oll"),
		TEXT("oll-t"),
		TEXT(""),
		TEXT("Subset deck reflecting Alg-Trainer's set/subset organization, timer flow, and real-scramble practice posture.")
	);

	const FHyperTwistTrainingCase OLLU01 = HyperTwistTrainingCatalogLibraryInternal::MakeAlgTrainerCase(
		TEXT("alg-trainer-oll-u-01"),
		TEXT("OLL U 01"),
		TEXT("U"),
		TEXT("R U2 R2 F R F' R U2 R'"),
		TEXT("R2 D R' U2 R D' R' U2 R'"),
		TEXT("R U R' U R U2 R' F R U R' U' F'"),
		0.62f,
		3600
	);

	const FHyperTwistTrainingCase OLLU02 = HyperTwistTrainingCatalogLibraryInternal::MakeAlgTrainerCase(
		TEXT("alg-trainer-oll-u-02"),
		TEXT("OLL U 02"),
		TEXT("U"),
		TEXT("r U R' U' r' F R F'"),
		TEXT("R U2 R' U' R U R' U' R U' R'"),
		TEXT("F R U R' U' F' U R U2 R'"),
		0.66f,
		3800
	);

	FHyperTwistTrainingDeck UDeck;
	UDeck.DeckId = TEXT("oll-u");
	UDeck.Title = TEXT("OLL U Cases");
	UDeck.DeliveryModes = TDeck.DeliveryModes;
	UDeck.SelectionPolicy = EHyperTwistTrainingSelectionPolicy::Weighted;
	UDeck.bUsesRealScrambles = true;
	UDeck.bSupportsAlgorithmReveal = true;
	UDeck.bSupportsVirtualCube = true;
	UDeck.bSupportsSmartCube = true;
	UDeck.TimingPolicy = HyperTwistTrainingCatalogLibraryInternal::MakeStandardAlgorithmTimingPolicy();
	UDeck.Cases = {OLLU01, OLLU02};
	UDeck.DifficultyBand = TEXT("intermediate");
	UDeck.Tags = {TEXT("classic"), TEXT("oll"), TEXT("alg-trainer"), TEXT("subset:u")};
	UDeck.SubsetId = TEXT("U");
	UDeck.SourceAttribution = HyperTwistTrainingCatalogLibraryInternal::MakeAlgTrainerAttribution(
		TEXT("oll"),
		TEXT("oll-u"),
		TEXT(""),
		TEXT("Subset deck reflecting Alg-Trainer's set/subset organization, timer flow, and real-scramble practice posture.")
	);

	FHyperTwistTrainingTrack OLLTrack;
	OLLTrack.TrackId = TEXT("oll");
	OLLTrack.Title = TEXT("OLL");
	OLLTrack.Goal = TEXT("Memorize and execute OLL algorithms through subset-organized timed drilling.");
	OLLTrack.Decks = {TDeck, UDeck};
	OLLTrack.SourceAttribution = HyperTwistTrainingCatalogLibraryInternal::MakeAlgTrainerAttribution(
		TEXT("oll"),
		TEXT(""),
		TEXT(""),
		TEXT("Track structure shaped from Alg-Trainer's algorithm-set and subset training model.")
	);

	FHyperTwistContentPack ContentPack;
	ContentPack.ContentPackId = TEXT("alg-trainer/classic-oll-starter");
	ContentPack.Title = TEXT("Alg-Trainer Classic OLL Starter");
	ContentPack.Version = TEXT("2026.04.alg-trainer.01");
	ContentPack.PuzzleFamily = EHyperTwistPuzzleFamily::ClassicCube;
	ContentPack.NotationProfile = TEXT("classic-wca");
	ContentPack.Tracks = {OLLTrack};
	ContentPack.SourceAttribution = HyperTwistTrainingCatalogLibraryInternal::MakeAlgTrainerAttribution(
		TEXT("oll"),
		TEXT(""),
		TEXT(""),
		TEXT("First direct donor-backed content pack wired into HyperTwist. Real-scramble, timer, virtual-cube, and subset semantics come from Alg-Trainer's practice model.")
	);
	return ContentPack;
}

FHyperTwistContentPack UHyperTwistTrainingCatalogLibrary::MakeCrossTrainerStarterContentPack()
{
	using namespace HyperTwistTrainingCatalogLibraryInternal;

	const TArray<FString> OneMove =
	{
		TEXT("ERENIOBFBRLBNJNLHQJNL"),
		TEXT("RHEMKHQHBKHLBPENFOHFN"),
		TEXT("BKBDHJQJQDHCRFPMEAKNL")
	};
	const TArray<FString> TwoMove =
	{
		TEXT("BNHBLQBQBLDAKOCLMJFC"),
		TEXT("BJNFQHJNDHQCJEOJNCEM"),
		TEXT("BOKHRBOKHEMJPOJEGFMEQ")
	};
	const TArray<FString> ThreeMove =
	{
		TEXT("AHKMCDBDMHMEHPKPEBH"),
		TEXT("GQKQKINIQADIDHLOICRC"),
		TEXT("OGNJGODCLNENKNHFBEN")
	};
	const TArray<FString> FourMove =
	{
		TEXT("QKBLNDHENFIQJOKAREMK"),
		TEXT("HNAEGENCNKCJAPBNCPN"),
		TEXT("NBQEHEBREIBLILMKBDO")
	};
	const TArray<FString> FiveMove =
	{
		TEXT("NBJBJQNBFHJCRECPAKQIK"),
		TEXT("BOHEQBEQOHMIKMGBPCKOJ"),
		TEXT("QKQLENLBDNFAJPHEGQGJM")
	};
	const TArray<FString> SixMove =
	{
		TEXT("DNIPLCRKPEBNQJQDQBHK"),
		TEXT("QKNBQBKFHQEMGBFCRGMEB"),
		TEXT("HKCKAQIQBQEMHLAOEIJFH")
	};
	const TArray<FString> SevenMove =
	{
		TEXT("HEGQCQHECQBFGJBQCOFO"),
		TEXT("DINLOBQEMGEBHLHJHNDQH"),
		TEXT("BQHMBKOKBOKFGMJCFBOHK")
	};
	const TArray<FString> EightMove =
	{
		TEXT("JQNFQEQBQLFPJIPDAJOB"),
		TEXT("HRBEMHNEHMEINKDGLCFRI"),
		TEXT("CQNCKECKIBKPIMIFOBJQ")
	};

	FHyperTwistTrainingTrack CrossTrack;
	CrossTrack.TrackId = TEXT("cross");
	CrossTrack.Title = TEXT("CFOP Cross");
	CrossTrack.Goal = TEXT("Practice CFOP cross planning and execution through a curated 1-move to 8-move scramble ladder.");
	CrossTrack.Decks =
	{
		MakeCrossTrainerDeck(1, OneMove),
		MakeCrossTrainerDeck(2, TwoMove),
		MakeCrossTrainerDeck(3, ThreeMove),
		MakeCrossTrainerDeck(4, FourMove),
		MakeCrossTrainerDeck(5, FiveMove),
		MakeCrossTrainerDeck(6, SixMove),
		MakeCrossTrainerDeck(7, SevenMove),
		MakeCrossTrainerDeck(8, EightMove)
	};
	CrossTrack.SourceAttribution = MakeCrossTrainerAttribution(
		TEXT("cross"),
		TEXT(""),
		TEXT("Track structure shaped from rubiks-cross-trainer's explicit 1-move to 8-move cross difficulty ladder.")
	);

	FHyperTwistContentPack ContentPack;
	ContentPack.ContentPackId = TEXT("rubiks-cross-trainer/classic-cross-starter");
	ContentPack.Title = TEXT("Cross Trainer CFOP Starter");
	ContentPack.Version = TEXT("2026.04.cross-trainer.01");
	ContentPack.PuzzleFamily = EHyperTwistPuzzleFamily::ClassicCube;
	ContentPack.NotationProfile = TEXT("classic-wca");
	ContentPack.Tracks = {CrossTrack};
	ContentPack.SourceAttribution = MakeCrossTrainerAttribution(
		TEXT("cross"),
		TEXT(""),
		TEXT("Direct donor-backed CFOP cross difficulty ladder shaped from rubiks-cross-trainer's curated scramble corpus.")
	);
	return ContentPack;
}

FHyperTwistContentPack UHyperTwistTrainingCatalogLibrary::MakeFiveStyleStarterContentPack()
{
	using namespace HyperTwistTrainingCatalogLibraryInternal;

	const TArray<FString> FourMover =
	{
		TEXT("E F E' F'"),
		TEXT("E F' E' F"),
		TEXT("E' F E F'")
	};
	const TArray<FString> FiveMover =
	{
		TEXT("U M U M' U2"),
		TEXT("M' U M' U' M2"),
		TEXT("M' U' M' U M2")
	};
	const TArray<FString> SixMover =
	{
		TEXT("M f L' S' L F' M'"),
		TEXT("M R U' M' U R'"),
		TEXT("M U M2 U M U2")
	};

	FHyperTwistTrainingTrack FiveStyleTrack;
	FiveStyleTrack.TrackId = TEXT("5-style");
	FiveStyleTrack.Title = TEXT("5-Style Edge Cycles");
	FiveStyleTrack.Goal = TEXT("Practice 5-style edge-cycle algorithms through mover-subset drilling, timed execution, and reveal-driven memorization flow.");
	FiveStyleTrack.Decks =
	{
		MakeFiveStyleDeck(4, FourMover),
		MakeFiveStyleDeck(5, FiveMover),
		MakeFiveStyleDeck(6, SixMover)
	};
	FiveStyleTrack.SourceAttribution = MakeFiveStyleAttribution(
		TEXT("5-style"),
		TEXT(""),
		TEXT("Track structure shaped from 5style-Trainer's mover-subset drilling and timed reveal practice for 5-style edge cycles.")
	);

	FHyperTwistContentPack ContentPack;
	ContentPack.ContentPackId = TEXT("5style-trainer/classic-edge-cycle-starter");
	ContentPack.Title = TEXT("5-Style Edge Cycle Starter");
	ContentPack.Version = TEXT("2026.04.5style.01");
	ContentPack.PuzzleFamily = EHyperTwistPuzzleFamily::ClassicCube;
	ContentPack.NotationProfile = TEXT("classic-wca");
	ContentPack.Tracks = {FiveStyleTrack};
	ContentPack.SourceAttribution = MakeFiveStyleAttribution(
		TEXT("5-style"),
		TEXT(""),
		TEXT("Direct donor-backed 5-style edge-cycle starter pack shaped from 5style-Trainer's advanced subset drilling flow.")
	);
	return ContentPack;
}

namespace HyperTwistTrainingCatalogMaterializationInternal
{
	FString ResolveBundledContentSourcePath(
		const FString& CatalogDirectory,
		const FString& ContentSourceFile
	)
	{
		if (ContentSourceFile.IsEmpty())
		{
			return FString();
		}
		if (FPaths::IsRelative(ContentSourceFile))
		{
			FString RelativePath = ContentSourceFile;
			if (RelativePath.StartsWith(TEXT("./")))
			{
				RelativePath = RelativePath.RightChop(2);
			}

			return FPaths::ConvertRelativePathToFull(CatalogDirectory / RelativePath);
		}

		return FPaths::ConvertRelativePathToFull(ContentSourceFile);
	}

	bool LoadRawExerciseRecordsFromJsonLinesFile(
		const FString& JsonLinesPath,
		TArray<FHyperTwistTrainingCatalogRawExerciseRecord>& OutRecords,
		TArray<FString>& InOutNotes,
		const FString& Label
	)
	{
		OutRecords.Reset();
		if (!FPaths::FileExists(JsonLinesPath))
		{
			InOutNotes.Add(FString::Printf(TEXT("%s missing: %s"), *Label, *JsonLinesPath));
			return false;
		}

		TArray<FString> Lines;
		if (!FFileHelper::LoadFileToStringArray(Lines, *JsonLinesPath))
		{
			InOutNotes.Add(FString::Printf(TEXT("%s could not be read: %s"), *Label, *JsonLinesPath));
			return false;
		}

		for (int32 LineIndex = 0; LineIndex < Lines.Num(); ++LineIndex)
		{
			FString Line = Lines[LineIndex];
			Line.TrimStartAndEndInline();
			if (Line.IsEmpty())
			{
				continue;
			}

			FHyperTwistTrainingCatalogRawExerciseRecord Record;
			if (!FJsonObjectConverter::JsonObjectStringToUStruct(Line, &Record, 0, 0))
			{
				InOutNotes.Add(
					FString::Printf(TEXT("%s JSONL line %d could not be parsed into a raw exercise record."), *Label, LineIndex + 1)
				);
				OutRecords.Reset();
				return false;
			}
			if (!Record.IsStructurallyValid())
			{
				InOutNotes.Add(
					FString::Printf(TEXT("%s JSONL line %d parsed but was structurally invalid."), *Label, LineIndex + 1)
				);
				OutRecords.Reset();
				return false;
			}

			OutRecords.Add(MoveTemp(Record));
		}

		if (OutRecords.Num() == 0)
		{
			InOutNotes.Add(FString::Printf(TEXT("%s contained no usable raw exercise records."), *Label));
			return false;
		}

		return true;
	}

	bool LoadGenerativeSurfaceRecordsFromJsonLinesFile(
		const FString& JsonLinesPath,
		TArray<FHyperTwistTrainingCatalogGenerativeSurfaceRecord>& OutRecords,
		TArray<FString>& InOutNotes,
		const FString& Label
	)
	{
		OutRecords.Reset();
		if (!FPaths::FileExists(JsonLinesPath))
		{
			InOutNotes.Add(FString::Printf(TEXT("%s missing: %s"), *Label, *JsonLinesPath));
			return false;
		}

		TArray<FString> Lines;
		if (!FFileHelper::LoadFileToStringArray(Lines, *JsonLinesPath))
		{
			InOutNotes.Add(FString::Printf(TEXT("%s could not be read: %s"), *Label, *JsonLinesPath));
			return false;
		}

		for (int32 LineIndex = 0; LineIndex < Lines.Num(); ++LineIndex)
		{
			FString Line = Lines[LineIndex];
			Line.TrimStartAndEndInline();
			if (Line.IsEmpty())
			{
				continue;
			}

			FHyperTwistTrainingCatalogGenerativeSurfaceRecord Record;
			if (!FJsonObjectConverter::JsonObjectStringToUStruct(Line, &Record, 0, 0))
			{
				InOutNotes.Add(
					FString::Printf(TEXT("%s JSONL line %d could not be parsed into a generative surface record."), *Label, LineIndex + 1)
				);
				OutRecords.Reset();
				return false;
			}
			if (!Record.IsStructurallyValid())
			{
				InOutNotes.Add(
					FString::Printf(TEXT("%s JSONL line %d parsed but was structurally invalid."), *Label, LineIndex + 1)
				);
				OutRecords.Reset();
				return false;
			}

			OutRecords.Add(MoveTemp(Record));
		}

		if (OutRecords.Num() == 0)
		{
			InOutNotes.Add(FString::Printf(TEXT("%s contained no usable generative surface records."), *Label));
			return false;
		}

		return true;
	}

	FString ResolveRawStructureTemplate(const FHyperTwistTrainingCatalogRawExerciseRecord& RawRecord)
	{
		if (RawRecord.exercise_type.Equals(TEXT("algorithm-case"), ESearchCase::IgnoreCase))
		{
			return TEXT("algorithm-case-deck");
		}
		if (RawRecord.exercise_type.Equals(TEXT("scramble-case"), ESearchCase::IgnoreCase))
		{
			return TEXT("scramble-drill-deck");
		}
		if (RawRecord.exercise_type.Equals(TEXT("selector-option"), ESearchCase::IgnoreCase))
		{
			return TEXT("selector-catalog");
		}

		return FString();
	}

	bool MatchesRuntimeBundleFilter(
		const FHyperTwistTrainingCatalogRuntimeContentBundle& RuntimeBundle,
		const FHyperTwistTrainingCatalogRawExerciseRecord& RawRecord
	)
	{
		if (!RuntimeBundle.MatchFilter.source.Equals(RawRecord.source, ESearchCase::IgnoreCase))
		{
			return false;
		}
		if (!RuntimeBundle.MatchFilter.source_scope.Equals(RawRecord.source_scope, ESearchCase::IgnoreCase))
		{
			return false;
		}

		const FString RawStructureTemplate = ResolveRawStructureTemplate(RawRecord);
		if (!RuntimeBundle.MatchFilter.structure_template.Equals(RawStructureTemplate, ESearchCase::IgnoreCase))
		{
			return false;
		}
		if (!RuntimeBundle.MatchFilter.family.IsEmpty()
			&& !RuntimeBundle.MatchFilter.family.Equals(RawRecord.family, ESearchCase::IgnoreCase))
		{
			return false;
		}
		if (!RuntimeBundle.MatchFilter.subset.IsEmpty()
			&& !RuntimeBundle.MatchFilter.subset.Equals(RawRecord.subset, ESearchCase::IgnoreCase))
		{
			return false;
		}

		return true;
	}

	bool MatchesRuntimeBundleFilter(
		const FHyperTwistTrainingCatalogRuntimeContentBundle& RuntimeBundle,
		const FHyperTwistTrainingCatalogGenerativeSurfaceRecord& GenerativeRecord
	)
	{
		if (!RuntimeBundle.MatchFilter.source.Equals(GenerativeRecord.source, ESearchCase::IgnoreCase))
		{
			return false;
		}
		if (!RuntimeBundle.MatchFilter.source_scope.Equals(GenerativeRecord.source_scope, ESearchCase::IgnoreCase))
		{
			return false;
		}
		if (!RuntimeBundle.MatchFilter.structure_template.Equals(TEXT("generative-trainer-surface"), ESearchCase::IgnoreCase))
		{
			return false;
		}
		if (!RuntimeBundle.MatchFilter.family.IsEmpty()
			&& !RuntimeBundle.MatchFilter.family.Equals(GenerativeRecord.family, ESearchCase::IgnoreCase))
		{
			return false;
		}
		if (!RuntimeBundle.MatchFilter.subset.IsEmpty()
			&& !RuntimeBundle.MatchFilter.subset.Equals(GenerativeRecord.subset, ESearchCase::IgnoreCase))
		{
			return false;
		}
		if (!RuntimeBundle.MatchFilter.mode_id.IsEmpty()
			&& !RuntimeBundle.MatchFilter.mode_id.Equals(GenerativeRecord.mode_id, ESearchCase::IgnoreCase))
		{
			return false;
		}

		return true;
	}

	FString SlugifyValue(const FString& Value)
	{
		FString Slug;
		bool bLastWasSeparator = false;
		for (TCHAR Character : Value)
		{
			if (FChar::IsAlnum(Character))
			{
				Slug.AppendChar(FChar::ToLower(Character));
				bLastWasSeparator = false;
			}
			else if (!Slug.IsEmpty() && !bLastWasSeparator)
			{
				Slug.AppendChar(TEXT('-'));
				bLastWasSeparator = true;
			}
		}

		while (Slug.EndsWith(TEXT("-")))
		{
			Slug.LeftChopInline(1, EAllowShrinking::No);
		}

		return Slug;
	}

	FString NormalizeCaseLabel(
		const FString& CandidateLabel,
		const FString& FallbackCaseId,
		const int32 CaseOrdinal
	)
	{
		FString Label = CandidateLabel.IsEmpty() ? FallbackCaseId : CandidateLabel;
		Label.TrimStartAndEndInline();
		if (Label.IsEmpty())
		{
			return FString::Printf(TEXT("Case %d"), CaseOrdinal);
		}

		bool bAllDigits = true;
		for (const TCHAR Character : Label)
		{
			if (!FChar::IsDigit(Character))
			{
				bAllDigits = false;
				break;
			}
		}
		if (bAllDigits)
		{
			return FString::Printf(TEXT("Case %s"), *Label);
		}

		return Label;
	}

	float ResolveDifficultyValue(const FString& DifficultyBand)
	{
		if (DifficultyBand.Equals(TEXT("beginner"), ESearchCase::IgnoreCase))
		{
			return 0.25f;
		}
		if (DifficultyBand.Equals(TEXT("intermediate"), ESearchCase::IgnoreCase))
		{
			return 0.5f;
		}
		if (DifficultyBand.Equals(TEXT("advanced"), ESearchCase::IgnoreCase))
		{
			return 0.75f;
		}
		if (DifficultyBand.Equals(TEXT("expert"), ESearchCase::IgnoreCase))
		{
			return 1.0f;
		}

		return 0.5f;
	}

	int32 ResolveTimeTargetMs(
		const FHyperTwistTrainingCatalogDeckImportSpec& DeckImportSpec,
		const EHyperTwistTrainingPromptKind PromptKind
	)
	{
		if (PromptKind == EHyperTwistTrainingPromptKind::State)
		{
			return DeckImportSpec.bUsesRealScrambles ? 3200 : 2500;
		}
		if (PromptKind == EHyperTwistTrainingPromptKind::Memo)
		{
			return 12000;
		}
		if (PromptKind == EHyperTwistTrainingPromptKind::Recognition)
		{
			return 4500;
		}
		if (DeckImportSpec.DifficultyBand.Equals(TEXT("expert"), ESearchCase::IgnoreCase))
		{
			return 6000;
		}
		if (DeckImportSpec.DifficultyBand.Equals(TEXT("advanced"), ESearchCase::IgnoreCase))
		{
			return 5000;
		}

		return 4000;
	}

	FString ResolveImportedPuzzleId(const FHyperTwistTrainingCatalogDeckImportSpec& DeckImportSpec)
	{
		const FString Combined = FString::Printf(
			TEXT("%s %s %s"),
			*DeckImportSpec.ContentPackId.ToLower(),
			*DeckImportSpec.TrackId.ToLower(),
			*DeckImportSpec.DeckId.ToLower()
		);
		if (Combined.Contains(TEXT("2x2")))
		{
			return TEXT("cube/2x2x2");
		}
		if (Combined.Contains(TEXT("square-1")) || Combined.Contains(TEXT("square1")))
		{
			return TEXT("square-1");
		}
		if (Combined.Contains(TEXT("skewb")))
		{
			return TEXT("skewb");
		}
		if (Combined.Contains(TEXT("pyraminx")))
		{
			return TEXT("pyraminx");
		}
		if (Combined.Contains(TEXT("megaminx")))
		{
			return TEXT("megaminx");
		}
		if (Combined.Contains(TEXT("big-cube")) || Combined.Contains(TEXT("4x4")))
		{
			return TEXT("cube/4x4x4");
		}

		return TEXT("cube/3x3x3");
	}

	FHyperTwistTrainingTimingPolicy MakeStandardMemoRecallTimingPolicy()
	{
		FHyperTwistTrainingTimingPolicy TimingPolicy;
		TimingPolicy.bInspectionEnabled = false;
		TimingPolicy.bInspectionPenaltiesEnabled = false;
		TimingPolicy.bAllowManualPenalty = false;
		TimingPolicy.bHideRunningTime = false;

		FHyperTwistTrainingSplitPhaseDefinition MemorizationPhase;
		MemorizationPhase.PhaseId = TEXT("memorization");
		MemorizationPhase.Label = TEXT("Memorization");
		MemorizationPhase.Order = 0;

		FHyperTwistTrainingSplitPhaseDefinition RecallPhase;
		RecallPhase.PhaseId = TEXT("recall");
		RecallPhase.Label = TEXT("Recall");
		RecallPhase.Order = 1;

		TimingPolicy.SplitPhases = {MemorizationPhase, RecallPhase};
		return TimingPolicy;
	}

	FHyperTwistTrainingTimingPolicy MakeStandardRecognitionReviewTimingPolicy()
	{
		FHyperTwistTrainingTimingPolicy TimingPolicy;
		TimingPolicy.bInspectionEnabled = false;
		TimingPolicy.bInspectionPenaltiesEnabled = false;
		TimingPolicy.bAllowManualPenalty = false;
		TimingPolicy.bHideRunningTime = false;

		FHyperTwistTrainingSplitPhaseDefinition RecognitionPhase;
		RecognitionPhase.PhaseId = TEXT("recognition");
		RecognitionPhase.Label = TEXT("Recognition");
		RecognitionPhase.Order = 0;

		FHyperTwistTrainingSplitPhaseDefinition ReviewPhase;
		ReviewPhase.PhaseId = TEXT("review");
		ReviewPhase.Label = TEXT("Review");
		ReviewPhase.Order = 1;

		TimingPolicy.SplitPhases = {RecognitionPhase, ReviewPhase};
		return TimingPolicy;
	}

	FHyperTwistTrainingTimingPolicy MakeGeneratedRandomStateTimingPolicy()
	{
		FHyperTwistTrainingTimingPolicy TimingPolicy;
		TimingPolicy.bInspectionEnabled = true;
		TimingPolicy.InspectionDurationMs = 15000;
		TimingPolicy.bInspectionPenaltiesEnabled = true;
		TimingPolicy.bAllowManualPenalty = true;
		TimingPolicy.bHideRunningTime = false;

		FHyperTwistTrainingSplitPhaseDefinition RecognitionPhase;
		RecognitionPhase.PhaseId = TEXT("recognition");
		RecognitionPhase.Label = TEXT("Recognition");
		RecognitionPhase.Order = 0;

		FHyperTwistTrainingSplitPhaseDefinition ExecutionPhase;
		ExecutionPhase.PhaseId = TEXT("execution");
		ExecutionPhase.Label = TEXT("Execution");
		ExecutionPhase.Order = 1;

		TimingPolicy.SplitPhases = {RecognitionPhase, ExecutionPhase};
		return TimingPolicy;
	}

	bool TryResolveTimingPolicyTemplate(
		const FString& TimingPolicyTemplateId,
		FHyperTwistTrainingTimingPolicy& OutTimingPolicy
	)
	{
		if (TimingPolicyTemplateId.Equals(TEXT("algorithm-standard"), ESearchCase::IgnoreCase))
		{
			OutTimingPolicy = HyperTwistTrainingCatalogLibraryInternal::MakeStandardAlgorithmTimingPolicy();
			return true;
		}
		if (TimingPolicyTemplateId.Equals(TEXT("cross-planning-standard"), ESearchCase::IgnoreCase))
		{
			OutTimingPolicy = HyperTwistTrainingCatalogLibraryInternal::MakeStandardCrossTimingPolicy();
			return true;
		}
		if (TimingPolicyTemplateId.Equals(TEXT("memo-recall-standard"), ESearchCase::IgnoreCase))
		{
			OutTimingPolicy = MakeStandardMemoRecallTimingPolicy();
			return true;
		}
		if (TimingPolicyTemplateId.Equals(TEXT("recognition-review-standard"), ESearchCase::IgnoreCase))
		{
			OutTimingPolicy = MakeStandardRecognitionReviewTimingPolicy();
			return true;
		}
		if (TimingPolicyTemplateId.Equals(TEXT("generated-random-state-standard"), ESearchCase::IgnoreCase))
		{
			OutTimingPolicy = MakeGeneratedRandomStateTimingPolicy();
			return true;
		}

		return false;
	}

	void PopulateDeckCoreFields(
		const FHyperTwistTrainingCatalogDeckImportSpec& DeckImportSpec,
		const TArray<EHyperTwistTrainingDeliveryMode>& DeliveryModes,
		const EHyperTwistTrainingSelectionPolicy SelectionPolicy,
		const FHyperTwistTrainingTimingPolicy& TimingPolicy,
		const FHyperTwistTrainingSourceAttribution& DeckSourceAttribution,
		TArray<FHyperTwistTrainingCase>&& Cases,
		FHyperTwistTrainingDeck& OutDeck
	)
	{
		OutDeck.DeckId = DeckImportSpec.DeckId;
		OutDeck.Title = DeckImportSpec.Title;
		OutDeck.DeliveryModes = DeliveryModes;
		OutDeck.SelectionPolicy = SelectionPolicy;
		OutDeck.bUsesRealScrambles = DeckImportSpec.bUsesRealScrambles;
		OutDeck.bSupportsAlgorithmReveal = DeckImportSpec.bSupportsAlgorithmReveal;
		OutDeck.bSupportsVirtualCube = DeckImportSpec.bSupportsVirtualCube;
		OutDeck.bSupportsSmartCube = DeckImportSpec.bSupportsSmartCube;
		OutDeck.TimingPolicy = TimingPolicy;
		OutDeck.Cases = MoveTemp(Cases);
		OutDeck.DifficultyBand = DeckImportSpec.DifficultyBand;
		OutDeck.Tags = DeckImportSpec.Tags;
		OutDeck.Tags.AddUnique(TEXT("imported-materialized"));
		OutDeck.Tags.AddUnique(TEXT("catalog-materialized"));
		OutDeck.SubsetId = DeckImportSpec.SubsetId;
		OutDeck.SourceAttribution = DeckSourceAttribution;
	}

	void PopulateGeneratedModeRuntimeSurface(
		const FHyperTwistTrainingCatalogDeckImportSpec& DeckImportSpec,
		const FHyperTwistTrainingCatalogRuntimeContentBundle& RuntimeBundle,
		const FHyperTwistTrainingCatalogGenerativeSurfaceRecord& GenerativeRecord,
		FHyperTwistTrainingImportedRuntimeSurface& OutRuntimeSurface
	)
	{
		OutRuntimeSurface = FHyperTwistTrainingImportedRuntimeSurface();
		OutRuntimeSurface.SurfaceKind = TEXT("generated-mode");
		OutRuntimeSurface.SurfaceId = RuntimeBundle.CaseBundleId;
		OutRuntimeSurface.SurfaceLabel = GenerativeRecord.mode_name;
		OutRuntimeSurface.RuntimeModeId = GenerativeRecord.runtime_mode;
		OutRuntimeSurface.GeneratorType = GenerativeRecord.generator_type;
		OutRuntimeSurface.OriginPaths = GenerativeRecord.origin_paths;
		OutRuntimeSurface.Notes = GenerativeRecord.notes;

		for (int32 SelectorIndex = 0; SelectorIndex < GenerativeRecord.selectors.Num(); ++SelectorIndex)
		{
			const FHyperTwistTrainingCatalogGenerativeSelectorRecord& SelectorRecord = GenerativeRecord.selectors[SelectorIndex];
			if (!SelectorRecord.IsStructurallyValid())
			{
				continue;
			}

			FHyperTwistTrainingImportedRuntimeSelector Selector;
			Selector.SelectorId = FString::Printf(
				TEXT("%s/selector-%d-%s"),
				*DeckImportSpec.DeckId,
				SelectorIndex + 1,
				*SlugifyValue(SelectorRecord.label)
			);
			Selector.Label = SelectorRecord.label;
			Selector.SelectorBinding = SelectorRecord.options;
			OutRuntimeSurface.Selectors.Add(MoveTemp(Selector));
		}
	}

	void PopulateSelectorCatalogRuntimeSurface(
		const FHyperTwistTrainingCatalogDeckImportSpec& DeckImportSpec,
		const FHyperTwistTrainingCatalogRuntimeContentBundle& RuntimeBundle,
		const TArray<FHyperTwistTrainingCatalogRawExerciseRecord>& MatchingRecords,
		FHyperTwistTrainingImportedRuntimeSurface& OutRuntimeSurface
	)
	{
		OutRuntimeSurface = FHyperTwistTrainingImportedRuntimeSurface();
		OutRuntimeSurface.SurfaceKind = TEXT("selector-catalog");
		OutRuntimeSurface.SurfaceId = RuntimeBundle.CaseBundleId;
		OutRuntimeSurface.SurfaceLabel = DeckImportSpec.Title;
		OutRuntimeSurface.Notes = FString::Printf(
			TEXT("Imported selector catalog materialized from bundled case bundle %s."),
			*RuntimeBundle.CaseBundleId
		);

		FHyperTwistTrainingImportedRuntimeSelector Selector;
		Selector.SelectorId = FString::Printf(TEXT("%s/selector-catalog"), *DeckImportSpec.DeckId);
		Selector.Label = DeckImportSpec.Title;

		for (const FHyperTwistTrainingCatalogRawExerciseRecord& RawRecord : MatchingRecords)
		{
			if (!RawRecord.IsStructurallyValid())
			{
				continue;
			}

			FHyperTwistTrainingImportedRuntimeSelectorOption Option;
			Option.OptionId = RawRecord.case_id;
			Option.Label = NormalizeCaseLabel(RawRecord.case_name, RawRecord.case_id, Selector.Options.Num() + 1);
			Option.OptionValue = RawRecord.case_id;
			Selector.Options.Add(MoveTemp(Option));

			for (const FString& OriginPath : RawRecord.origin_paths)
			{
				OutRuntimeSurface.OriginPaths.AddUnique(OriginPath);
			}
		}

		if (Selector.IsStructurallyValid())
		{
			OutRuntimeSurface.Selectors.Add(MoveTemp(Selector));
		}
	}

	bool IsSupportedFiniteImportedExerciseBundle(const FHyperTwistTrainingCatalogRuntimeContentBundle& RuntimeBundle)
	{
		const bool bIsSupportedFinitePolicy =
			RuntimeBundle.RuntimeMaterializationPolicy.Equals(TEXT("instantiate-algorithm-cases"), ESearchCase::IgnoreCase)
			|| RuntimeBundle.RuntimeMaterializationPolicy.Equals(TEXT("instantiate-scramble-drill-cases"), ESearchCase::IgnoreCase)
			|| RuntimeBundle.RuntimeMaterializationPolicy.Equals(TEXT("instantiate-selector-catalog"), ESearchCase::IgnoreCase);
		if (!bIsSupportedFinitePolicy)
		{
			return false;
		}

		return RuntimeBundle.ContentSourceFile.EndsWith(TEXT("repo-backed-exercises.jsonl"), ESearchCase::IgnoreCase)
			|| RuntimeBundle.ContentSourceFile.EndsWith(TEXT("httrack-retained-exercises.jsonl"), ESearchCase::IgnoreCase)
			|| RuntimeBundle.ContentSourceFile.EndsWith(TEXT("restrictive-static-exercises.jsonl"), ESearchCase::IgnoreCase);
	}

	bool TryBuildTrainingCaseFromRawRecord(
		const FHyperTwistTrainingCatalogDeckImportSpec& DeckImportSpec,
		const FHyperTwistTrainingCatalogRuntimeContentBundle& RuntimeBundle,
		const FHyperTwistTrainingCatalogRawExerciseRecord& RawRecord,
		const EHyperTwistTrainingPromptKind PromptKind,
		const TArray<EHyperTwistTrainingDeliveryMode>& DeliveryModes,
		const FString& PuzzleId,
		const FHyperTwistTrainingSourceAttribution& DeckSourceAttribution,
		const int32 CaseOrdinal,
		FHyperTwistTrainingCase& OutCase
	)
	{
		OutCase = FHyperTwistTrainingCase();
		if (!RawRecord.IsStructurallyValid())
		{
			return false;
		}

		OutCase.CaseId = FString::Printf(TEXT("%s/%s"), *DeckImportSpec.DeckId, *RawRecord.case_id);
		OutCase.PuzzleId = PuzzleId;
		OutCase.PromptKind = PromptKind;
		OutCase.PromptLabel = NormalizeCaseLabel(RawRecord.case_name, RawRecord.case_id, CaseOrdinal);
		OutCase.CanonicalStateRef = FString::Printf(
			TEXT("%s/%s"),
			*DeckImportSpec.TrackId.ToLower(),
			*SlugifyValue(RawRecord.family)
		);

		if (RawRecord.exercise_type.Equals(TEXT("algorithm-case"), ESearchCase::IgnoreCase))
		{
			for (const FHyperTwistTrainingCatalogRawAlgorithmVariant& Variant : RawRecord.algorithm_variants)
			{
				if (!Variant.algorithm.IsEmpty())
				{
					if (OutCase.CanonicalNotation.IsEmpty())
					{
						OutCase.CanonicalNotation = Variant.algorithm;
					}
					else
					{
						OutCase.AlternateNotations.AddUnique(Variant.algorithm);
					}
				}
			}

			if (OutCase.CanonicalNotation.IsEmpty())
			{
				return false;
			}
		}
		else if (RawRecord.exercise_type.Equals(TEXT("scramble-case"), ESearchCase::IgnoreCase))
		{
			OutCase.ScrambleNotation =
				HyperTwistTrainingCatalogLibraryInternal::DecodeCrossTrainerScramble(RawRecord.scramble_code);
			if (OutCase.ScrambleNotation.IsEmpty())
			{
				return false;
			}
		}
		else if (RawRecord.exercise_type.Equals(TEXT("selector-option"), ESearchCase::IgnoreCase))
		{
			OutCase.CanonicalStateRef = FString::Printf(
				TEXT("%s/%s/%s"),
				*DeckImportSpec.TrackId.ToLower(),
				*SlugifyValue(RawRecord.family),
				*SlugifyValue(RawRecord.case_id)
			);
		}
		else
		{
			return false;
		}

		OutCase.SubsetId = !RawRecord.subset.IsEmpty()
			? RawRecord.subset
			: (!DeckImportSpec.SubsetId.IsEmpty() ? DeckImportSpec.SubsetId : RawRecord.family);
		OutCase.Difficulty = ResolveDifficultyValue(DeckImportSpec.DifficultyBand);
		OutCase.Tags = DeckImportSpec.Tags;
		OutCase.Tags.AddUnique(TEXT("imported-materialized"));
		OutCase.Tags.AddUnique(FString::Printf(TEXT("source:%s"), *SlugifyValue(RawRecord.source)));
		OutCase.Tags.AddUnique(FString::Printf(TEXT("family:%s"), *SlugifyValue(RawRecord.family)));
		if (!OutCase.SubsetId.IsEmpty())
		{
			OutCase.Tags.AddUnique(FString::Printf(TEXT("subset:%s"), *SlugifyValue(OutCase.SubsetId)));
		}
		OutCase.Tags.AddUnique(FString::Printf(TEXT("runtime-bundle:%s"), *SlugifyValue(RuntimeBundle.CaseBundleId)));
		OutCase.AllowedDeliveryModes = DeliveryModes;
		OutCase.TimeTargetMs = ResolveTimeTargetMs(DeckImportSpec, PromptKind);
		OutCase.SourceAttribution = DeckSourceAttribution;
		OutCase.SourceAttribution.SourceCaseId = RawRecord.case_id;
		OutCase.SourceAttribution.SourceNotes = FString::Printf(
			TEXT("Imported runtime case materialized from bundle %s."),
			*RuntimeBundle.CaseBundleId
		);
		return OutCase.IsStructurallyValid();
	}

	bool TryMaterializeFiniteImportedDeck(
		const FString& CatalogDirectory,
		const FHyperTwistTrainingCatalogDeckImportSpec& DeckImportSpec,
		const FHyperTwistTrainingCatalogRuntimeContentBundle& RuntimeBundle,
		TMap<FString, TArray<FHyperTwistTrainingCatalogRawExerciseRecord>>& RawExerciseRecordCache,
		FHyperTwistTrainingDeck& OutDeck,
		TArray<FString>& InOutNotes
	)
	{
		OutDeck = FHyperTwistTrainingDeck();
		if (!DeckImportSpec.IsStructurallyValid() || !RuntimeBundle.IsStructurallyValid())
		{
			InOutNotes.Add(TEXT("Imported deck materialization failed because the deck spec or runtime bundle was invalid."));
			return false;
		}
		if (!IsSupportedFiniteImportedExerciseBundle(RuntimeBundle))
		{
			InOutNotes.Add(FString::Printf(
				TEXT("Imported deck %s remains deferred because runtime policy %s from %s is outside the current finite imported-exercise slice."),
				*DeckImportSpec.DeckId,
				*RuntimeBundle.RuntimeMaterializationPolicy,
				*RuntimeBundle.ContentSourceFile
			));
			return false;
		}

		TArray<EHyperTwistTrainingDeliveryMode> DeliveryModes;
		if (!UHyperTwistTrainingCatalogLibrary::TryConvertImportedDeliveryModes(DeckImportSpec.DeliveryModes, DeliveryModes))
		{
			InOutNotes.Add(FString::Printf(TEXT("Imported deck %s had delivery modes that could not be converted."), *DeckImportSpec.DeckId));
			return false;
		}

		EHyperTwistTrainingPromptKind PromptKind = EHyperTwistTrainingPromptKind::Algorithm;
		if (!UHyperTwistTrainingCatalogLibrary::TryConvertImportedPromptKind(DeckImportSpec.PromptKind, PromptKind))
		{
			InOutNotes.Add(FString::Printf(TEXT("Imported deck %s had a prompt kind that could not be converted."), *DeckImportSpec.DeckId));
			return false;
		}

		EHyperTwistTrainingSelectionPolicy SelectionPolicy = EHyperTwistTrainingSelectionPolicy::Weighted;
		if (!UHyperTwistTrainingCatalogLibrary::TryConvertImportedSelectionPolicy(DeckImportSpec.SelectionPolicy, SelectionPolicy))
		{
			InOutNotes.Add(FString::Printf(TEXT("Imported deck %s had a selection policy that could not be converted."), *DeckImportSpec.DeckId));
			return false;
		}

		FHyperTwistTrainingTimingPolicy TimingPolicy;
		if (!TryResolveTimingPolicyTemplate(DeckImportSpec.TimingPolicyTemplateId, TimingPolicy))
		{
			InOutNotes.Add(FString::Printf(
				TEXT("Imported deck %s referenced unsupported timing policy template %s."),
				*DeckImportSpec.DeckId,
				*DeckImportSpec.TimingPolicyTemplateId
			));
			return false;
		}

		FHyperTwistTrainingSourceAttribution DeckSourceAttribution;
		if (!UHyperTwistTrainingCatalogLibrary::TryConvertImportedSourceAttribution(
				DeckImportSpec.SourceAttribution,
				DeckSourceAttribution))
		{
			InOutNotes.Add(FString::Printf(TEXT("Imported deck %s had source attribution that could not be converted."), *DeckImportSpec.DeckId));
			return false;
		}

		const FString SourcePath = ResolveBundledContentSourcePath(CatalogDirectory, RuntimeBundle.ContentSourceFile);
		TArray<FHyperTwistTrainingCatalogRawExerciseRecord>* RawRecords = RawExerciseRecordCache.Find(SourcePath);
		if (RawRecords == nullptr)
		{
			TArray<FHyperTwistTrainingCatalogRawExerciseRecord> LoadedRawRecords;
			if (!LoadRawExerciseRecordsFromJsonLinesFile(
					SourcePath,
					LoadedRawRecords,
					InOutNotes,
					FString::Printf(TEXT("Imported raw exercise source for %s"), *DeckImportSpec.DeckId)))
			{
				return false;
			}

			RawRecords = &RawExerciseRecordCache.Add(SourcePath, MoveTemp(LoadedRawRecords));
		}

		const FString PuzzleId = ResolveImportedPuzzleId(DeckImportSpec);
		TArray<FHyperTwistTrainingCase> Cases;
        TArray<FHyperTwistTrainingCatalogRawExerciseRecord> MatchingRecords;
		for (const FHyperTwistTrainingCatalogRawExerciseRecord& RawRecord : *RawRecords)
		{
			if (!MatchesRuntimeBundleFilter(RuntimeBundle, RawRecord))
			{
				continue;
			}

			FHyperTwistTrainingCase TrainingCase;
			if (!TryBuildTrainingCaseFromRawRecord(
					DeckImportSpec,
					RuntimeBundle,
					RawRecord,
					PromptKind,
					DeliveryModes,
					PuzzleId,
					DeckSourceAttribution,
					Cases.Num() + 1,
					TrainingCase))
			{
				InOutNotes.Add(FString::Printf(
					TEXT("Imported deck %s skipped a filtered raw record because case materialization failed for case %s."),
					*DeckImportSpec.DeckId,
					*RawRecord.case_id
				));
				continue;
			}

			MatchingRecords.Add(RawRecord);
			Cases.Add(MoveTemp(TrainingCase));
		}

		if (Cases.Num() == 0)
		{
			InOutNotes.Add(FString::Printf(
				TEXT("Imported deck %s matched no finite raw exercise records in %s."),
				*DeckImportSpec.DeckId,
				*SourcePath
			));
			return false;
		}
		if (RuntimeBundle.RetainedCounts.case_count > 0 && Cases.Num() != RuntimeBundle.RetainedCounts.case_count)
		{
			InOutNotes.Add(FString::Printf(
				TEXT("Imported deck %s materialized %d cases while bundle metadata expected %d."),
				*DeckImportSpec.DeckId,
				Cases.Num(),
				RuntimeBundle.RetainedCounts.case_count
			));
		}

		PopulateDeckCoreFields(
			DeckImportSpec,
			DeliveryModes,
			SelectionPolicy,
			TimingPolicy,
			DeckSourceAttribution,
			MoveTemp(Cases),
			OutDeck
		);
		OutDeck.Tags.AddUnique(FString::Printf(TEXT("runtime-bundle:%s"), *SlugifyValue(RuntimeBundle.CaseBundleId)));
		OutDeck.SourceAttribution.SourceNotes = FString::Printf(
			TEXT("Imported runtime deck materialized from bundled case bundle %s."),
			*RuntimeBundle.CaseBundleId
		);
		if (RuntimeBundle.RuntimeMaterializationPolicy.Equals(TEXT("instantiate-selector-catalog"), ESearchCase::IgnoreCase))
		{
			PopulateSelectorCatalogRuntimeSurface(DeckImportSpec, RuntimeBundle, MatchingRecords, OutDeck.ImportedRuntimeSurface);
			OutDeck.Tags.AddUnique(TEXT("runtime-surface:selector-catalog"));
		}
		return OutDeck.IsStructurallyValid();
	}

	bool TryMaterializeGeneratedModeImportedDeck(
		const FString& CatalogDirectory,
		const FHyperTwistTrainingCatalogDeckImportSpec& DeckImportSpec,
		const FHyperTwistTrainingCatalogRuntimeContentBundle& RuntimeBundle,
		TMap<FString, TArray<FHyperTwistTrainingCatalogGenerativeSurfaceRecord>>& GenerativeSurfaceCache,
		FHyperTwistTrainingDeck& OutDeck,
		TArray<FString>& InOutNotes
	)
	{
		OutDeck = FHyperTwistTrainingDeck();
		if (!DeckImportSpec.IsStructurallyValid() || !RuntimeBundle.IsStructurallyValid())
		{
			InOutNotes.Add(TEXT("Imported generated-mode deck materialization failed because the deck spec or runtime bundle was invalid."));
			return false;
		}
		if (!RuntimeBundle.RuntimeMaterializationPolicy.Equals(TEXT("instantiate-generated-mode"), ESearchCase::IgnoreCase))
		{
			return false;
		}

		TArray<EHyperTwistTrainingDeliveryMode> DeliveryModes;
		if (!UHyperTwistTrainingCatalogLibrary::TryConvertImportedDeliveryModes(DeckImportSpec.DeliveryModes, DeliveryModes))
		{
			InOutNotes.Add(FString::Printf(TEXT("Imported generated-mode deck %s had delivery modes that could not be converted."), *DeckImportSpec.DeckId));
			return false;
		}

		EHyperTwistTrainingPromptKind PromptKind = EHyperTwistTrainingPromptKind::State;
		if (!UHyperTwistTrainingCatalogLibrary::TryConvertImportedPromptKind(DeckImportSpec.PromptKind, PromptKind))
		{
			InOutNotes.Add(FString::Printf(TEXT("Imported generated-mode deck %s had a prompt kind that could not be converted."), *DeckImportSpec.DeckId));
			return false;
		}

		EHyperTwistTrainingSelectionPolicy SelectionPolicy = EHyperTwistTrainingSelectionPolicy::Weighted;
		if (!UHyperTwistTrainingCatalogLibrary::TryConvertImportedSelectionPolicy(DeckImportSpec.SelectionPolicy, SelectionPolicy))
		{
			InOutNotes.Add(FString::Printf(TEXT("Imported generated-mode deck %s had a selection policy that could not be converted."), *DeckImportSpec.DeckId));
			return false;
		}

		FHyperTwistTrainingTimingPolicy TimingPolicy;
		if (!TryResolveTimingPolicyTemplate(DeckImportSpec.TimingPolicyTemplateId, TimingPolicy))
		{
			InOutNotes.Add(FString::Printf(
				TEXT("Imported generated-mode deck %s referenced unsupported timing policy template %s."),
				*DeckImportSpec.DeckId,
				*DeckImportSpec.TimingPolicyTemplateId
			));
			return false;
		}

		FHyperTwistTrainingSourceAttribution DeckSourceAttribution;
		if (!UHyperTwistTrainingCatalogLibrary::TryConvertImportedSourceAttribution(
				DeckImportSpec.SourceAttribution,
				DeckSourceAttribution))
		{
			InOutNotes.Add(FString::Printf(TEXT("Imported generated-mode deck %s had source attribution that could not be converted."), *DeckImportSpec.DeckId));
			return false;
		}

		const FString SourcePath = ResolveBundledContentSourcePath(CatalogDirectory, RuntimeBundle.ContentSourceFile);
		TArray<FHyperTwistTrainingCatalogGenerativeSurfaceRecord>* GenerativeRecords = GenerativeSurfaceCache.Find(SourcePath);
		if (GenerativeRecords == nullptr)
		{
			TArray<FHyperTwistTrainingCatalogGenerativeSurfaceRecord> LoadedRecords;
			if (!LoadGenerativeSurfaceRecordsFromJsonLinesFile(
					SourcePath,
					LoadedRecords,
					InOutNotes,
					FString::Printf(TEXT("Imported generative surface source for %s"), *DeckImportSpec.DeckId)))
			{
				return false;
			}

			GenerativeRecords = &GenerativeSurfaceCache.Add(SourcePath, MoveTemp(LoadedRecords));
		}

		const FHyperTwistTrainingCatalogGenerativeSurfaceRecord* MatchingSurface = GenerativeRecords->FindByPredicate(
			[&RuntimeBundle](const FHyperTwistTrainingCatalogGenerativeSurfaceRecord& Candidate)
			{
				return MatchesRuntimeBundleFilter(RuntimeBundle, Candidate);
			}
		);
		if (MatchingSurface == nullptr)
		{
			InOutNotes.Add(FString::Printf(
				TEXT("Imported generated-mode deck %s matched no bundled generative surface records in %s."),
				*DeckImportSpec.DeckId,
				*SourcePath
			));
			return false;
		}

		FHyperTwistTrainingCase LaunchCase;
		LaunchCase.CaseId = FString::Printf(TEXT("%s/runtime-surface"), *DeckImportSpec.DeckId);
		LaunchCase.PuzzleId = ResolveImportedPuzzleId(DeckImportSpec);
		LaunchCase.PromptKind = PromptKind;
		LaunchCase.PromptLabel = MatchingSurface->mode_name;
		LaunchCase.CanonicalStateRef = FString::Printf(
			TEXT("%s/generated/%s"),
			*DeckImportSpec.TrackId.ToLower(),
			*SlugifyValue(MatchingSurface->mode_id)
		);
		LaunchCase.SubsetId = !MatchingSurface->subset.IsEmpty() ? MatchingSurface->subset : DeckImportSpec.SubsetId;
		LaunchCase.Difficulty = ResolveDifficultyValue(DeckImportSpec.DifficultyBand);
		LaunchCase.Tags = DeckImportSpec.Tags;
		LaunchCase.Tags.AddUnique(TEXT("imported-materialized"));
		LaunchCase.Tags.AddUnique(TEXT("runtime-surface:generated-mode"));
		LaunchCase.Tags.AddUnique(FString::Printf(TEXT("runtime-bundle:%s"), *SlugifyValue(RuntimeBundle.CaseBundleId)));
		LaunchCase.Tags.AddUnique(FString::Printf(TEXT("mode:%s"), *SlugifyValue(MatchingSurface->mode_id)));
		LaunchCase.AllowedDeliveryModes = DeliveryModes;
		LaunchCase.TimeTargetMs = ResolveTimeTargetMs(DeckImportSpec, PromptKind);
		LaunchCase.SourceAttribution = DeckSourceAttribution;
		LaunchCase.SourceAttribution.SourceCaseId = MatchingSurface->mode_id;
		LaunchCase.SourceAttribution.SourceNotes = FString::Printf(
			TEXT("Imported generated-mode runtime surface materialized from bundle %s."),
			*RuntimeBundle.CaseBundleId
		);

		TArray<FHyperTwistTrainingCase> Cases;
		Cases.Add(MoveTemp(LaunchCase));
		PopulateDeckCoreFields(
			DeckImportSpec,
			DeliveryModes,
			SelectionPolicy,
			TimingPolicy,
			DeckSourceAttribution,
			MoveTemp(Cases),
			OutDeck
		);
		OutDeck.Tags.AddUnique(TEXT("runtime-surface:generated-mode"));
		OutDeck.Tags.AddUnique(FString::Printf(TEXT("runtime-bundle:%s"), *SlugifyValue(RuntimeBundle.CaseBundleId)));
		PopulateGeneratedModeRuntimeSurface(DeckImportSpec, RuntimeBundle, *MatchingSurface, OutDeck.ImportedRuntimeSurface);
		OutDeck.SourceAttribution.SourceNotes = FString::Printf(
			TEXT("Imported generated-mode deck materialized from bundled runtime surface %s."),
			*RuntimeBundle.CaseBundleId
		);
		return OutDeck.IsStructurallyValid() && OutDeck.ImportedRuntimeSurface.IsStructurallyValid();
	}

	bool TryMaterializeImportedDeck(
		const FString& CatalogDirectory,
		const FHyperTwistTrainingCatalogDeckImportSpec& DeckImportSpec,
		const FHyperTwistTrainingCatalogRuntimeContentBundle& RuntimeBundle,
		TMap<FString, TArray<FHyperTwistTrainingCatalogRawExerciseRecord>>& RawExerciseRecordCache,
		TMap<FString, TArray<FHyperTwistTrainingCatalogGenerativeSurfaceRecord>>& GenerativeSurfaceCache,
		FHyperTwistTrainingDeck& OutDeck,
		TArray<FString>& InOutNotes
	)
	{
		if (RuntimeBundle.RuntimeMaterializationPolicy.Equals(TEXT("instantiate-generated-mode"), ESearchCase::IgnoreCase))
		{
			return TryMaterializeGeneratedModeImportedDeck(
				CatalogDirectory,
				DeckImportSpec,
				RuntimeBundle,
				GenerativeSurfaceCache,
				OutDeck,
				InOutNotes
			);
		}

		return TryMaterializeFiniteImportedDeck(
			CatalogDirectory,
			DeckImportSpec,
			RuntimeBundle,
			RawExerciseRecordCache,
			OutDeck,
			InOutNotes
		);
	}

	bool TryFindDeckImportSpecByImportSpecId(
		const FHyperTwistTrainingCatalogMaterializedBundle& MaterializedBundle,
		const FString& DeckImportSpecId,
		FHyperTwistTrainingCatalogDeckImportSpec& OutDeckImportSpec
	)
	{
		OutDeckImportSpec = FHyperTwistTrainingCatalogDeckImportSpec();
		if (!MaterializedBundle.IsStructurallyValid() || DeckImportSpecId.IsEmpty())
		{
			return false;
		}

		for (const FHyperTwistTrainingCatalogDeckImportSpec& DeckImportSpec
			: MaterializedBundle.DeckImportSpecs.deck_import_specs)
		{
			if (DeckImportSpec.DeckImportSpecId == DeckImportSpecId)
			{
				OutDeckImportSpec = DeckImportSpec;
				return OutDeckImportSpec.IsStructurallyValid();
			}
		}

		return false;
	}

	bool TryBuildImportedTrackFromManifest(
		const FString& CatalogDirectory,
		const FHyperTwistTrainingCatalogMaterializedBundle& MaterializedBundle,
		const FHyperTwistTrainingCatalogImportedTrackManifest& TrackManifest,
		TMap<FString, TArray<FHyperTwistTrainingCatalogRawExerciseRecord>>& RawExerciseRecordCache,
		TMap<FString, TArray<FHyperTwistTrainingCatalogGenerativeSurfaceRecord>>& GenerativeSurfaceCache,
		FHyperTwistTrainingTrack& OutTrack
	)
	{
		OutTrack = FHyperTwistTrainingTrack();
		if (!MaterializedBundle.IsStructurallyValid() || !TrackManifest.IsStructurallyValid())
		{
			return false;
		}

		FHyperTwistTrainingSourceAttribution TrackSourceAttribution;
		if (!UHyperTwistTrainingCatalogLibrary::TryConvertImportedSourceAttribution(
				TrackManifest.SourceAttribution,
				TrackSourceAttribution))
		{
			return false;
		}

		TArray<FHyperTwistTrainingDeck> Decks;
		for (const FString& DeckImportSpecId : TrackManifest.DeckImportSpecIds)
		{
			FHyperTwistTrainingCatalogDeckImportSpec DeckImportSpec;
			if (!TryFindDeckImportSpecByImportSpecId(MaterializedBundle, DeckImportSpecId, DeckImportSpec))
			{
				continue;
			}

			FHyperTwistTrainingCatalogRuntimeContentBundle RuntimeBundle;
			if (!UHyperTwistTrainingCatalogLibrary::TryFindRuntimeContentBundleForDeckImportSpec(
					MaterializedBundle,
					DeckImportSpec,
					RuntimeBundle))
			{
				continue;
			}

			FHyperTwistTrainingDeck Deck;
			TArray<FString> MaterializationNotes;
			if (!TryMaterializeImportedDeck(
					CatalogDirectory,
					DeckImportSpec,
					RuntimeBundle,
					RawExerciseRecordCache,
					GenerativeSurfaceCache,
					Deck,
					MaterializationNotes))
			{
				continue;
			}

			Decks.Add(MoveTemp(Deck));
		}

		if (Decks.Num() == 0)
		{
			return false;
		}

		OutTrack.TrackId = TrackManifest.TrackId;
		OutTrack.Title = TrackManifest.Title;
		OutTrack.Goal = TrackManifest.Goal;
		OutTrack.Decks = MoveTemp(Decks);
		OutTrack.SourceAttribution = TrackSourceAttribution;
		return OutTrack.IsStructurallyValid();
	}

	bool TryBuildImportedContentPackFromManifest(
		const FString& CatalogDirectory,
		const FHyperTwistTrainingCatalogMaterializedBundle& MaterializedBundle,
		const FHyperTwistTrainingCatalogImportedContentPackManifest& ContentPackManifest,
		TMap<FString, TArray<FHyperTwistTrainingCatalogRawExerciseRecord>>& RawExerciseRecordCache,
		TMap<FString, TArray<FHyperTwistTrainingCatalogGenerativeSurfaceRecord>>& GenerativeSurfaceCache,
		FHyperTwistContentPack& OutContentPack
	)
	{
		OutContentPack = FHyperTwistContentPack();
		if (!MaterializedBundle.IsStructurallyValid() || !ContentPackManifest.IsStructurallyValid())
		{
			return false;
		}

		FHyperTwistTrainingSourceAttribution ContentPackSourceAttribution;
		if (!UHyperTwistTrainingCatalogLibrary::TryConvertImportedSourceAttribution(
				ContentPackManifest.SourceAttribution,
				ContentPackSourceAttribution))
		{
			return false;
		}

		EHyperTwistPuzzleFamily PuzzleFamily = EHyperTwistPuzzleFamily::ClassicCube;
		if (!UHyperTwistTrainingCatalogLibrary::TryConvertImportedPuzzleFamily(
				ContentPackManifest.PuzzleFamily,
				PuzzleFamily))
		{
			return false;
		}

		TArray<FHyperTwistTrainingTrack> Tracks;
		for (const FHyperTwistTrainingCatalogImportedTrackManifest& TrackManifest : ContentPackManifest.TrackManifests)
		{
			FHyperTwistTrainingTrack Track;
			if (TryBuildImportedTrackFromManifest(
					CatalogDirectory,
					MaterializedBundle,
					TrackManifest,
					RawExerciseRecordCache,
					GenerativeSurfaceCache,
					Track))
			{
				Tracks.Add(MoveTemp(Track));
			}
		}

		if (Tracks.Num() == 0)
		{
			return false;
		}

		OutContentPack.ContentPackId = ContentPackManifest.ContentPackId;
		OutContentPack.Title = ContentPackManifest.Title;
		OutContentPack.Version = ContentPackManifest.Version.IsEmpty() ? OutContentPack.Version : ContentPackManifest.Version;
		OutContentPack.PuzzleFamily = PuzzleFamily;
		OutContentPack.NotationProfile =
			ContentPackManifest.NotationProfile.IsEmpty() ? OutContentPack.NotationProfile : ContentPackManifest.NotationProfile;
		OutContentPack.Tracks = MoveTemp(Tracks);
		OutContentPack.SourceAttribution = ContentPackSourceAttribution;
		return OutContentPack.IsStructurallyValid();
	}
}

TArray<FHyperTwistContentPack> UHyperTwistTrainingCatalogLibrary::MakePhase3TrainingCatalog()
{
	TArray<FHyperTwistContentPack> Catalog =
	{
		MakeAlgTrainerStarterContentPack(),
		MakeCrossTrainerStarterContentPack(),
		MakeFiveStyleStarterContentPack(),
		UHyperTwistTrainingLibrary::MakeSampleClassicContentPack()
	};

	FHyperTwistTrainingCatalogMaterializedBundle MaterializedBundle;
	FHyperTwistTrainingCatalogImportLoadReport LoadReport;
	if (!LoadBundledCatalogMaterializationBundle(MaterializedBundle, LoadReport))
	{
		return Catalog;
	}

	TMap<FString, TArray<FHyperTwistTrainingCatalogRawExerciseRecord>> RawExerciseRecordCache;
	TMap<FString, TArray<FHyperTwistTrainingCatalogGenerativeSurfaceRecord>> GenerativeSurfaceCache;
	const FString CatalogDirectory = GetBundledCatalogMaterializationDirectory();
	for (const FHyperTwistTrainingCatalogImportedContentPackManifest& ContentPackManifest
		: MaterializedBundle.ContentPackManifests.content_packs)
	{
		FHyperTwistContentPack ImportedContentPack;
		if (!HyperTwistTrainingCatalogMaterializationInternal::TryBuildImportedContentPackFromManifest(
				CatalogDirectory,
				MaterializedBundle,
				ContentPackManifest,
				RawExerciseRecordCache,
				GenerativeSurfaceCache,
				ImportedContentPack))
		{
			continue;
		}

		Catalog.Add(MoveTemp(ImportedContentPack));
	}

	return Catalog;
}

FString UHyperTwistTrainingCatalogLibrary::GetBundledCatalogMaterializationDirectory()
{
	return FPaths::ConvertRelativePathToFull(
		FPaths::ProjectContentDir() / TEXT("HyperTwistTraining/MaterializedCatalog")
	);
}

bool UHyperTwistTrainingCatalogLibrary::LoadCatalogMaterializationBundleFromDirectory(
	const FString& CatalogDirectory,
	FHyperTwistTrainingCatalogMaterializedBundle& OutBundle,
	FHyperTwistTrainingCatalogImportLoadReport& OutLoadReport
)
{
	OutBundle = FHyperTwistTrainingCatalogMaterializedBundle();
	OutLoadReport = FHyperTwistTrainingCatalogImportLoadReport();

	const FString NormalizedDirectory = FPaths::ConvertRelativePathToFull(CatalogDirectory);
	OutLoadReport.CatalogDirectory = NormalizedDirectory;
	OutLoadReport.SummaryPath =
		FPaths::Combine(NormalizedDirectory, TEXT("hypertwist-catalog-materialization-summary.json"));
	OutLoadReport.ContentPackManifestPath =
		FPaths::Combine(NormalizedDirectory, TEXT("hypertwist-content-pack-manifests.json"));
	OutLoadReport.DeckImportSpecPath =
		FPaths::Combine(NormalizedDirectory, TEXT("hypertwist-deck-import-specs.json"));
	OutLoadReport.RuntimeContentBundlePath =
		FPaths::Combine(NormalizedDirectory, TEXT("hypertwist-runtime-content-bundles.json"));
	OutLoadReport.ReferenceTargetPath =
		FPaths::Combine(NormalizedDirectory, TEXT("hypertwist-reference-incorporation-targets.json"));

	OutLoadReport.bLoadedSummary = HyperTwistTrainingCatalogLibraryInternal::LoadStructFromJsonFile(
		OutLoadReport.SummaryPath,
		OutBundle.Summary,
		OutLoadReport.Notes,
		TEXT("Materialized catalog summary")
	);
	OutLoadReport.bLoadedContentPackManifests = HyperTwistTrainingCatalogLibraryInternal::LoadStructFromJsonFile(
		OutLoadReport.ContentPackManifestPath,
		OutBundle.ContentPackManifests,
		OutLoadReport.Notes,
		TEXT("Content pack manifests")
	);
	OutLoadReport.bLoadedDeckImportSpecs = HyperTwistTrainingCatalogLibraryInternal::LoadStructFromJsonFile(
		OutLoadReport.DeckImportSpecPath,
		OutBundle.DeckImportSpecs,
		OutLoadReport.Notes,
		TEXT("Deck import specs")
	);
	OutLoadReport.bLoadedRuntimeBundles = HyperTwistTrainingCatalogLibraryInternal::LoadStructFromJsonFile(
		OutLoadReport.RuntimeContentBundlePath,
		OutBundle.RuntimeContentBundles,
		OutLoadReport.Notes,
		TEXT("Runtime content bundles")
	);
	OutLoadReport.bLoadedReferenceTargets = HyperTwistTrainingCatalogLibraryInternal::LoadStructFromJsonFile(
		OutLoadReport.ReferenceTargetPath,
		OutBundle.ReferenceTargets,
		OutLoadReport.Notes,
		TEXT("Reference incorporation targets")
	);

	if (OutLoadReport.bLoadedContentPackManifests)
	{
		OutLoadReport.LoadedContentPackCount = OutBundle.ContentPackManifests.content_packs.Num();
		OutLoadReport.LoadedTrackManifestCount =
			HyperTwistTrainingCatalogLibraryInternal::CountTrackManifests(OutBundle.ContentPackManifests);
	}
	if (OutLoadReport.bLoadedDeckImportSpecs)
	{
		OutLoadReport.LoadedDeckImportSpecCount = OutBundle.DeckImportSpecs.deck_import_specs.Num();
	}
	if (OutLoadReport.bLoadedRuntimeBundles)
	{
		OutLoadReport.LoadedRuntimeBundleCount = OutBundle.RuntimeContentBundles.runtime_content_bundles.Num();
	}
	if (OutLoadReport.bLoadedReferenceTargets)
	{
		OutLoadReport.LoadedReferenceTargetCount = OutBundle.ReferenceTargets.reference_targets.Num();
	}

	if (OutLoadReport.bLoadedSummary)
	{
		OutLoadReport.bCountsMatchSummary =
			OutLoadReport.LoadedContentPackCount == OutBundle.Summary.counts.content_packs
			&& OutLoadReport.LoadedTrackManifestCount == OutBundle.Summary.counts.track_manifests
			&& OutLoadReport.LoadedDeckImportSpecCount == OutBundle.Summary.counts.deck_import_specs
			&& OutLoadReport.LoadedRuntimeBundleCount == OutBundle.Summary.counts.runtime_content_bundles
			&& OutLoadReport.LoadedReferenceTargetCount == OutBundle.Summary.counts.reference_targets;
		if (!OutLoadReport.bCountsMatchSummary)
		{
			OutLoadReport.Notes.Add(
				TEXT("Materialized catalog counts do not match the authored summary counts. Rebuild or resync the generated assets.")
			);
		}
	}

	return OutLoadReport.IsStructurallyValid() && OutBundle.IsStructurallyValid();
}

bool UHyperTwistTrainingCatalogLibrary::LoadBundledCatalogMaterializationBundle(
	FHyperTwistTrainingCatalogMaterializedBundle& OutBundle,
	FHyperTwistTrainingCatalogImportLoadReport& OutLoadReport
)
{
	return LoadCatalogMaterializationBundleFromDirectory(
		GetBundledCatalogMaterializationDirectory(),
		OutBundle,
		OutLoadReport
	);
}

bool UHyperTwistTrainingCatalogLibrary::TryFindDeckImportSpecInMaterializedBundle(
	const FHyperTwistTrainingCatalogMaterializedBundle& MaterializedBundle,
	const FString& DeckId,
	FHyperTwistTrainingCatalogDeckImportSpec& OutDeckImportSpec
)
{
	OutDeckImportSpec = FHyperTwistTrainingCatalogDeckImportSpec();
	if (!MaterializedBundle.IsStructurallyValid() || DeckId.IsEmpty())
	{
		return false;
	}

	for (const FHyperTwistTrainingCatalogDeckImportSpec& DeckImportSpec : MaterializedBundle.DeckImportSpecs.deck_import_specs)
	{
		if (DeckImportSpec.DeckId == DeckId || DeckImportSpec.DeckImportSpecId == DeckId)
		{
			OutDeckImportSpec = DeckImportSpec;
			return OutDeckImportSpec.IsStructurallyValid();
		}
	}

	return false;
}

bool UHyperTwistTrainingCatalogLibrary::TryFindRuntimeContentBundleForDeckImportSpec(
	const FHyperTwistTrainingCatalogMaterializedBundle& MaterializedBundle,
	const FHyperTwistTrainingCatalogDeckImportSpec& DeckImportSpec,
	FHyperTwistTrainingCatalogRuntimeContentBundle& OutRuntimeBundle
)
{
	OutRuntimeBundle = FHyperTwistTrainingCatalogRuntimeContentBundle();
	if (!MaterializedBundle.IsStructurallyValid() || !DeckImportSpec.IsStructurallyValid())
	{
		return false;
	}

	for (const FHyperTwistTrainingCatalogRuntimeContentBundle& RuntimeBundle
		: MaterializedBundle.RuntimeContentBundles.runtime_content_bundles)
	{
		if (RuntimeBundle.CaseBundleId == DeckImportSpec.CaseBundleId)
		{
			OutRuntimeBundle = RuntimeBundle;
			return OutRuntimeBundle.IsStructurallyValid();
		}
	}

	return false;
}

bool UHyperTwistTrainingCatalogLibrary::TryFindReferenceTargetInMaterializedBundle(
	const FHyperTwistTrainingCatalogMaterializedBundle& MaterializedBundle,
	const FString& ReferenceTargetId,
	FHyperTwistTrainingCatalogReferenceTarget& OutReferenceTarget
)
{
	OutReferenceTarget = FHyperTwistTrainingCatalogReferenceTarget();
	if (!MaterializedBundle.IsStructurallyValid() || ReferenceTargetId.IsEmpty())
	{
		return false;
	}

	for (const FHyperTwistTrainingCatalogReferenceTarget& ReferenceTarget
		: MaterializedBundle.ReferenceTargets.reference_targets)
	{
		if (ReferenceTarget.ReferenceTargetId == ReferenceTargetId)
		{
			OutReferenceTarget = ReferenceTarget;
			return OutReferenceTarget.IsStructurallyValid();
		}
	}

	return false;
}

bool UHyperTwistTrainingCatalogLibrary::TryMaterializeImportedDeckFromMaterializedBundle(
	const FHyperTwistTrainingCatalogMaterializedBundle& MaterializedBundle,
	const FString& DeckId,
	FHyperTwistTrainingDeck& OutDeck,
	FHyperTwistTrainingCatalogImportLoadReport& OutLoadReport
)
{
	OutDeck = FHyperTwistTrainingDeck();
	if (!MaterializedBundle.IsStructurallyValid() || DeckId.IsEmpty())
	{
		OutLoadReport.Notes.Add(TEXT("Imported deck materialization requested an invalid materialized bundle or empty deck id."));
		return false;
	}

	FHyperTwistTrainingCatalogDeckImportSpec DeckImportSpec;
	if (!TryFindDeckImportSpecInMaterializedBundle(MaterializedBundle, DeckId, DeckImportSpec))
	{
		OutLoadReport.Notes.Add(FString::Printf(TEXT("Imported deck %s was not present in the bundled deck import specs."), *DeckId));
		return false;
	}

	FHyperTwistTrainingCatalogRuntimeContentBundle RuntimeBundle;
	if (!TryFindRuntimeContentBundleForDeckImportSpec(MaterializedBundle, DeckImportSpec, RuntimeBundle))
	{
		OutLoadReport.Notes.Add(FString::Printf(
			TEXT("Imported deck %s could not resolve its runtime content bundle %s."),
			*DeckId,
			*DeckImportSpec.CaseBundleId
		));
		return false;
	}

  TMap<FString, TArray<FHyperTwistTrainingCatalogRawExerciseRecord>> RawExerciseRecordCache;
	TMap<FString, TArray<FHyperTwistTrainingCatalogGenerativeSurfaceRecord>> GenerativeSurfaceCache;
	const bool bMaterialized = HyperTwistTrainingCatalogMaterializationInternal::TryMaterializeImportedDeck(
		GetBundledCatalogMaterializationDirectory(),
		DeckImportSpec,
		RuntimeBundle,
		RawExerciseRecordCache,
		GenerativeSurfaceCache,
		OutDeck,
		OutLoadReport.Notes
	);
	return bMaterialized && OutDeck.IsStructurallyValid();
}

bool UHyperTwistTrainingCatalogLibrary::TryMaterializeImportedDeckFromBundledCatalog(
	const FString& DeckId,
	FHyperTwistTrainingDeck& OutDeck,
	FHyperTwistTrainingCatalogImportLoadReport& OutLoadReport
)
{
	OutDeck = FHyperTwistTrainingDeck();
	FHyperTwistTrainingCatalogMaterializedBundle MaterializedBundle;
	if (!LoadBundledCatalogMaterializationBundle(MaterializedBundle, OutLoadReport))
	{
		return false;
	}

	return TryMaterializeImportedDeckFromMaterializedBundle(
		MaterializedBundle,
		DeckId,
		OutDeck,
		OutLoadReport
	);
}

bool UHyperTwistTrainingCatalogLibrary::TryConvertImportedSourceAttribution(
	const FHyperTwistTrainingCatalogImportedSourceAttribution& ImportedAttribution,
	FHyperTwistTrainingSourceAttribution& OutSourceAttribution
)
{
	OutSourceAttribution = FHyperTwistTrainingSourceAttribution();
	if (!ImportedAttribution.IsStructurallyValid())
	{
		return false;
	}

	if (!HyperTwistTrainingCatalogLibraryInternal::TryConvertImportedSourceMode(
			ImportedAttribution.SourceMode,
			OutSourceAttribution.SourceMode))
	{
		return false;
	}

	OutSourceAttribution.SourceRepo = ImportedAttribution.SourceRepo;
	OutSourceAttribution.SourceLicense = ImportedAttribution.SourceLicense;
	OutSourceAttribution.SourceTrackId = ImportedAttribution.SourceTrackId;
	OutSourceAttribution.SourceDeckId = ImportedAttribution.SourceDeckId;
	OutSourceAttribution.SourceCaseId = ImportedAttribution.SourceCaseId;
	OutSourceAttribution.SourceNotes = ImportedAttribution.SourceNotes;
	return OutSourceAttribution.IsStructurallyValid();
}

bool UHyperTwistTrainingCatalogLibrary::TryConvertImportedPromptKind(
	const FString& ImportedPromptKind,
	EHyperTwistTrainingPromptKind& OutPromptKind
)
{
	if (ImportedPromptKind.Equals(TEXT("Algorithm"), ESearchCase::IgnoreCase))
	{
		OutPromptKind = EHyperTwistTrainingPromptKind::Algorithm;
		return true;
	}
	if (ImportedPromptKind.Equals(TEXT("State"), ESearchCase::IgnoreCase))
	{
		OutPromptKind = EHyperTwistTrainingPromptKind::State;
		return true;
	}
	if (ImportedPromptKind.Equals(TEXT("Recognition"), ESearchCase::IgnoreCase))
	{
		OutPromptKind = EHyperTwistTrainingPromptKind::Recognition;
		return true;
	}
	if (ImportedPromptKind.Equals(TEXT("Memo"), ESearchCase::IgnoreCase))
	{
		OutPromptKind = EHyperTwistTrainingPromptKind::Memo;
		return true;
	}
	if (ImportedPromptKind.Equals(TEXT("Sequence"), ESearchCase::IgnoreCase))
	{
		OutPromptKind = EHyperTwistTrainingPromptKind::Sequence;
		return true;
	}

	return false;
}

bool UHyperTwistTrainingCatalogLibrary::TryConvertImportedSelectionPolicy(
	const FString& ImportedSelectionPolicy,
	EHyperTwistTrainingSelectionPolicy& OutSelectionPolicy
)
{
	if (ImportedSelectionPolicy.Equals(TEXT("Uniform"), ESearchCase::IgnoreCase))
	{
		OutSelectionPolicy = EHyperTwistTrainingSelectionPolicy::Uniform;
		return true;
	}
	if (ImportedSelectionPolicy.Equals(TEXT("Weighted"), ESearchCase::IgnoreCase))
	{
		OutSelectionPolicy = EHyperTwistTrainingSelectionPolicy::Weighted;
		return true;
	}
	if (ImportedSelectionPolicy.Equals(TEXT("Spaced"), ESearchCase::IgnoreCase))
	{
		OutSelectionPolicy = EHyperTwistTrainingSelectionPolicy::Spaced;
		return true;
	}
	if (ImportedSelectionPolicy.Equals(TEXT("Scripted"), ESearchCase::IgnoreCase))
	{
		OutSelectionPolicy = EHyperTwistTrainingSelectionPolicy::Scripted;
		return true;
	}

	return false;
}

bool UHyperTwistTrainingCatalogLibrary::TryConvertImportedDeliveryModes(
	const TArray<FString>& ImportedDeliveryModes,
	TArray<EHyperTwistTrainingDeliveryMode>& OutDeliveryModes
)
{
	OutDeliveryModes.Reset();
	if (ImportedDeliveryModes.Num() == 0)
	{
		return false;
	}

	for (const FString& ImportedDeliveryMode : ImportedDeliveryModes)
	{
		EHyperTwistTrainingDeliveryMode ConvertedMode = EHyperTwistTrainingDeliveryMode::Timer;
		if (!HyperTwistTrainingCatalogLibraryInternal::TryConvertImportedDeliveryMode(
				ImportedDeliveryMode,
				ConvertedMode))
		{
			OutDeliveryModes.Reset();
			return false;
		}

		OutDeliveryModes.AddUnique(ConvertedMode);
	}

	return OutDeliveryModes.Num() > 0;
}

bool UHyperTwistTrainingCatalogLibrary::TryConvertImportedPuzzleFamily(
	const FString& ImportedPuzzleFamily,
	EHyperTwistPuzzleFamily& OutPuzzleFamily
)
{
	if (ImportedPuzzleFamily.Equals(TEXT("ClassicCube"), ESearchCase::IgnoreCase))
	{
		OutPuzzleFamily = EHyperTwistPuzzleFamily::ClassicCube;
		return true;
	}
	if (ImportedPuzzleFamily.Equals(TEXT("Hypercube"), ESearchCase::IgnoreCase))
	{
		OutPuzzleFamily = EHyperTwistPuzzleFamily::Hypercube;
		return true;
	}
	if (ImportedPuzzleFamily.Equals(TEXT("Tiling"), ESearchCase::IgnoreCase))
	{
		OutPuzzleFamily = EHyperTwistPuzzleFamily::Tiling;
		return true;
	}
	if (ImportedPuzzleFamily.Equals(TEXT("Other"), ESearchCase::IgnoreCase))
	{
		OutPuzzleFamily = EHyperTwistPuzzleFamily::Other;
		return true;
	}

	return false;
}

bool UHyperTwistTrainingCatalogLibrary::TryFindDeckInCatalog(
	const TArray<FHyperTwistContentPack>& TrainingCatalog,
	const FString& DeckId,
	FHyperTwistTrainingDeck& OutDeck
)
{
	OutDeck = FHyperTwistTrainingDeck();
	if (DeckId.IsEmpty())
	{
		return false;
	}

	for (const FHyperTwistContentPack& ContentPack : TrainingCatalog)
	{
		if (!ContentPack.IsStructurallyValid())
		{
			continue;
		}

		for (const FHyperTwistTrainingTrack& Track : ContentPack.Tracks)
		{
			if (!Track.IsStructurallyValid())
			{
				continue;
			}

			for (const FHyperTwistTrainingDeck& Deck : Track.Decks)
			{
				if (Deck.DeckId == DeckId)
				{
					OutDeck = Deck;
					return OutDeck.IsStructurallyValid();
				}
			}
		}
	}

	FHyperTwistTrainingCatalogImportLoadReport ImportedDeckLoadReport;
	if (TryMaterializeImportedDeckFromBundledCatalog(DeckId, OutDeck, ImportedDeckLoadReport))
	{
		return OutDeck.IsStructurallyValid();
	}

	OutDeck = FHyperTwistTrainingDeck();
	return false;
}

bool UHyperTwistTrainingCatalogLibrary::TryGetTrackFromContentPack(
	const FHyperTwistContentPack& ContentPack,
	const FString& TrackId,
	FHyperTwistTrainingTrack& OutTrack
)
{
	OutTrack = FHyperTwistTrainingTrack();

	if (!ContentPack.IsStructurallyValid() || TrackId.IsEmpty())
	{
		return false;
	}

	for (const FHyperTwistTrainingTrack& Track : ContentPack.Tracks)
	{
		if (Track.TrackId == TrackId)
		{
			OutTrack = Track;
			return OutTrack.IsStructurallyValid();
		}
	}

	return false;
}

bool UHyperTwistTrainingCatalogLibrary::TryGetDeckFromContentPack(
	const FHyperTwistContentPack& ContentPack,
	const FString& TrackId,
	const FString& DeckId,
	FHyperTwistTrainingDeck& OutDeck
)
{
	OutDeck = FHyperTwistTrainingDeck();

	FHyperTwistTrainingTrack Track;
	if (!TryGetTrackFromContentPack(ContentPack, TrackId, Track) || DeckId.IsEmpty())
	{
		return false;
	}

	for (const FHyperTwistTrainingDeck& Deck : Track.Decks)
	{
		if (Deck.DeckId == DeckId)
		{
			OutDeck = Deck;
			return OutDeck.IsStructurallyValid();
		}
	}

	return false;
}

bool UHyperTwistTrainingCatalogLibrary::TryGetDefaultDeckFromContentPack(
	const FHyperTwistContentPack& ContentPack,
	FHyperTwistTrainingDeck& OutDeck
)
{
	OutDeck = FHyperTwistTrainingDeck();

	if (!ContentPack.IsStructurallyValid() || ContentPack.Tracks.Num() == 0 || ContentPack.Tracks[0].Decks.Num() == 0)
	{
		return false;
	}

	OutDeck = ContentPack.Tracks[0].Decks[0];
	return OutDeck.IsStructurallyValid();
}

bool UHyperTwistTrainingCatalogLibrary::TryGetImportedRuntimeSurfaceFromDeck(
	const FHyperTwistTrainingDeck& Deck,
	FHyperTwistTrainingImportedRuntimeSurface& OutRuntimeSurface
)
{
	OutRuntimeSurface = FHyperTwistTrainingImportedRuntimeSurface();
	if (!Deck.IsStructurallyValid() || !Deck.ImportedRuntimeSurface.IsStructurallyValid())
	{
		return false;
	}

	OutRuntimeSurface = Deck.ImportedRuntimeSurface;
	return OutRuntimeSurface.IsStructurallyValid();
}

bool UHyperTwistTrainingCatalogLibrary::TryGetImportedRuntimeSelectorFromDeck(
	const FHyperTwistTrainingDeck& Deck,
	const FString& SelectorId,
	FHyperTwistTrainingImportedRuntimeSelector& OutSelector
)
{
	OutSelector = FHyperTwistTrainingImportedRuntimeSelector();
	FHyperTwistTrainingImportedRuntimeSurface RuntimeSurface;
	if (!TryGetImportedRuntimeSurfaceFromDeck(Deck, RuntimeSurface) || SelectorId.IsEmpty())
	{
		return false;
	}

	for (const FHyperTwistTrainingImportedRuntimeSelector& Selector : RuntimeSurface.Selectors)
	{
		if (Selector.SelectorId == SelectorId)
		{
			OutSelector = Selector;
			return OutSelector.IsStructurallyValid();
		}
	}

	return false;
}

bool UHyperTwistTrainingCatalogLibrary::TryGetImportedRuntimeSelectorOptionCasesFromDeck(
	const FHyperTwistTrainingDeck& Deck,
	const FString& SelectorId,
	TArray<FHyperTwistTrainingCase>& OutCases
)
{
	OutCases.Reset();

	FHyperTwistTrainingImportedRuntimeSelector Selector;
	if (!TryGetImportedRuntimeSelectorFromDeck(Deck, SelectorId, Selector) || Selector.Options.Num() == 0)
	{
		return false;
	}

	for (const FHyperTwistTrainingImportedRuntimeSelectorOption& Option : Selector.Options)
	{
		const FHyperTwistTrainingCase* MatchingCase = Deck.Cases.FindByPredicate(
			[&Option](const FHyperTwistTrainingCase& Candidate)
			{
				return HyperTwistTrainingCatalogLibraryInternal::DoesSelectorOptionMatchCase(Option, Candidate);
			}
		);
		if (MatchingCase != nullptr)
		{
			OutCases.Add(*MatchingCase);
		}
	}

	return OutCases.Num() > 0;
}

bool UHyperTwistTrainingCatalogLibrary::TryGetImportedRuntimeSelectorOptionCaseFromDeck(
	const FHyperTwistTrainingDeck& Deck,
	const FString& SelectorId,
	const FString& OptionId,
	FHyperTwistTrainingCase& OutCase
)
{
	OutCase = FHyperTwistTrainingCase();

	FHyperTwistTrainingImportedRuntimeSelector Selector;
	if (!TryGetImportedRuntimeSelectorFromDeck(Deck, SelectorId, Selector) || OptionId.IsEmpty())
	{
		return false;
	}

	const FHyperTwistTrainingImportedRuntimeSelectorOption* MatchingOption = Selector.Options.FindByPredicate(
		[&OptionId](const FHyperTwistTrainingImportedRuntimeSelectorOption& Candidate)
		{
			return Candidate.OptionId == OptionId || Candidate.OptionValue == OptionId;
		}
	);
	if (MatchingOption == nullptr)
	{
		return false;
	}

	const FHyperTwistTrainingCase* MatchingCase = Deck.Cases.FindByPredicate(
		[&MatchingOption](const FHyperTwistTrainingCase& Candidate)
		{
			return HyperTwistTrainingCatalogLibraryInternal::DoesSelectorOptionMatchCase(*MatchingOption, Candidate);
		}
	);
	if (MatchingCase == nullptr)
	{
		return false;
	}

	OutCase = *MatchingCase;
	return OutCase.IsStructurallyValid();
}

bool UHyperTwistTrainingCatalogLibrary::TryBuildImportedGeneratedModeLaunchConfig(
	const FHyperTwistTrainingDeck& Deck,
	const FHyperTwistTrainingImportedRuntimeSelectionState& ImportedRuntimeSelectionState,
	const FHyperTwistTrainingSession& Session,
	FHyperTwistTrainingImportedGeneratedModeLaunchConfig& OutLaunchConfig
)
{
	OutLaunchConfig = FHyperTwistTrainingImportedGeneratedModeLaunchConfig();
	if (!Deck.IsStructurallyValid()
		|| !Session.IsStructurallyValid()
		|| !Deck.ImportedRuntimeSurface.IsStructurallyValid()
		|| !ImportedRuntimeSelectionState.IsStructurallyValid()
		|| !Deck.ImportedRuntimeSurface.SurfaceKind.Equals(TEXT("generated-mode"), ESearchCase::IgnoreCase)
		|| ImportedRuntimeSelectionState.SurfaceId != Deck.ImportedRuntimeSurface.SurfaceId)
	{
		return false;
	}

	for (const FHyperTwistTrainingImportedRuntimeSelector& Selector : Deck.ImportedRuntimeSurface.Selectors)
	{
		const FHyperTwistTrainingImportedRuntimeSelectorChoice* MatchingChoice =
			ImportedRuntimeSelectionState.SelectorChoices.FindByPredicate(
				[&Selector](const FHyperTwistTrainingImportedRuntimeSelectorChoice& Candidate)
				{
					return Candidate.SelectorId == Selector.SelectorId && Candidate.IsStructurallyValid();
				}
			);
		if (MatchingChoice == nullptr)
		{
			return false;
		}
	}

	OutLaunchConfig.DeckId = Deck.DeckId;
	OutLaunchConfig.UserId = Session.UserId;
	OutLaunchConfig.TrainingSessionId = Session.TrainingSessionId;
	OutLaunchConfig.DeliveryMode = Session.Mode;
	OutLaunchConfig.SurfaceId = Deck.ImportedRuntimeSurface.SurfaceId;
	OutLaunchConfig.SurfaceLabel = Deck.ImportedRuntimeSurface.SurfaceLabel;
	OutLaunchConfig.RuntimeModeId = Deck.ImportedRuntimeSurface.RuntimeModeId;
	OutLaunchConfig.GeneratorType = Deck.ImportedRuntimeSurface.GeneratorType;
	OutLaunchConfig.OriginPaths = Deck.ImportedRuntimeSurface.OriginPaths;
	OutLaunchConfig.Notes = Deck.ImportedRuntimeSurface.Notes;
	OutLaunchConfig.SelectorChoices = ImportedRuntimeSelectionState.SelectorChoices;

	const FHyperTwistTrainingCase* SeedCase = Deck.Cases.FindByPredicate(
		[](const FHyperTwistTrainingCase& Candidate)
		{
			return Candidate.IsStructurallyValid();
		}
	);
	if (SeedCase != nullptr)
	{
		OutLaunchConfig.PuzzleId = SeedCase->PuzzleId;
	}

	return OutLaunchConfig.IsStructurallyValid();
}

FHyperTwistTrainingMethodDescriptor UHyperTwistTrainingCatalogLibrary::DescribeTrainingDeckMethod(
	const FHyperTwistTrainingDeck& Deck
)
{
	FHyperTwistTrainingMethodDescriptor Descriptor;
	if (!Deck.IsStructurallyValid())
	{
		return Descriptor;
	}

	Descriptor.PuzzleId = TEXT("unknown");
	for (const FHyperTwistTrainingCase& TrainingCase : Deck.Cases)
	{
		if (TrainingCase.IsStructurallyValid())
		{
			Descriptor.PuzzleId = TrainingCase.PuzzleId;
			break;
		}
	}

	Descriptor.SubsetId = Deck.SubsetId;
	Descriptor.Tags = Deck.Tags;
	Descriptor.MethodKey = HyperTwistTrainingCatalogLibraryInternal::ResolveMethodKey(Deck);
	Descriptor.MethodLabel = HyperTwistTrainingCatalogLibraryInternal::ResolveMethodLabel(Descriptor.MethodKey);
	Descriptor.SegmentId = FString::Printf(TEXT("%s|%s"), *Descriptor.PuzzleId.ToLower(), *Descriptor.MethodKey.ToLower());
	return Descriptor;
}

bool UHyperTwistTrainingCatalogLibrary::TryDescribeDeckMethodFromCatalog(
	const TArray<FHyperTwistContentPack>& TrainingCatalog,
	const FString& DeckId,
	FHyperTwistTrainingMethodDescriptor& OutDescriptor
)
{
	OutDescriptor = FHyperTwistTrainingMethodDescriptor();

	FHyperTwistTrainingDeck Deck;
	if (!TryFindDeckInCatalog(TrainingCatalog, DeckId, Deck))
	{
		return false;
	}

	OutDescriptor = DescribeTrainingDeckMethod(Deck);
	return OutDescriptor.IsStructurallyValid();
}

FHyperTwistTrainingDeck UHyperTwistTrainingCatalogLibrary::MaterializeTrainingSessionTemplateDeck(
	const FHyperTwistTrainingSessionTemplate& SessionTemplate,
	const TArray<FHyperTwistContentPack>& TrainingCatalog
)
{
	FHyperTwistTrainingDeck SourceDeck;
	if (!SessionTemplate.IsStructurallyValid()
		|| !TryFindDeckInCatalog(TrainingCatalog, SessionTemplate.FocusDeckId, SourceDeck))
	{
		return FHyperTwistTrainingDeck();
	}

	FHyperTwistTrainingDeck MaterializedDeck = SourceDeck;
	MaterializedDeck.DeckId = FString::Printf(TEXT("%s/template/%s"), *SourceDeck.DeckId, *SessionTemplate.TemplateId);
	MaterializedDeck.Title = FString::Printf(TEXT("%s - %s"), *SourceDeck.Title, *SessionTemplate.Title);
	MaterializedDeck.SelectionPolicy = SessionTemplate.SuggestedSelectionPolicy;
	MaterializedDeck.Tags.AddUnique(TEXT("template"));
	MaterializedDeck.Tags.AddUnique(HyperTwistTrainingCatalogLibraryInternal::TemplateKindToTag(SessionTemplate.TemplateKind));
	if (SessionTemplate.bPreferReviewRun)
	{
		MaterializedDeck.Tags.AddUnique(TEXT("review-template"));
	}
	if (SessionTemplate.bCoachReview)
	{
		MaterializedDeck.Tags.AddUnique(TEXT("coach-template"));
	}
	if (SessionTemplate.bUserCreated)
	{
		MaterializedDeck.Tags.AddUnique(TEXT("user-template"));
	}
	if (SessionTemplate.bUserCustomized)
	{
		MaterializedDeck.Tags.AddUnique(TEXT("customized-template"));
	}
	if (!SessionTemplate.MethodSegmentId.IsEmpty())
	{
		MaterializedDeck.Tags.AddUnique(FString::Printf(TEXT("method-segment:%s"), *SessionTemplate.MethodSegmentId.ToLower()));
	}
	MaterializedDeck.DeliveryModes.AddUnique(SessionTemplate.SuggestedMode);

	TArray<FHyperTwistTrainingCase> MaterializedCases;
	if (SessionTemplate.FocusCaseIds.Num() > 0)
	{
		for (const FString& CaseId : SessionTemplate.FocusCaseIds)
		{
			const FHyperTwistTrainingCase* MatchingCase = SourceDeck.Cases.FindByPredicate(
				[&CaseId](const FHyperTwistTrainingCase& Candidate)
				{
					return Candidate.CaseId == CaseId;
				}
			);
			if (MatchingCase != nullptr)
			{
				MaterializedCases.Add(*MatchingCase);
			}
		}
	}
	else
	{
		MaterializedCases = SourceDeck.Cases;
	}

	const int32 RecommendedCaseCount = FMath::Max(1, SessionTemplate.RecommendedCaseCount);
	if (MaterializedCases.Num() > RecommendedCaseCount)
	{
		MaterializedCases.SetNum(RecommendedCaseCount, EAllowShrinking::No);
	}

	MaterializedDeck.Cases = MoveTemp(MaterializedCases);
	if (MaterializedDeck.Cases.Num() == 0)
	{
		return FHyperTwistTrainingDeck();
	}

	MaterializedDeck.SourceAttribution.SourceNotes = SessionTemplate.GuidanceLabel;
	return MaterializedDeck;
}
