// Copyright HyperTwist, Inc. All Rights Reserved.

#include "HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h"

#include "HyperTwistTrainingRepositoryLibraryInternal.h"

FHyperTwistTrainingReviewPolicy UHyperTwistTrainingRepositoryLibrary::GetDefaultReviewPolicy(
	EHyperTwistTrainingDeliveryMode DeliveryMode
)
{
	FHyperTwistTrainingReviewPolicy ReviewPolicy;
	ReviewPolicy.DeliveryMode = DeliveryMode;
	ReviewPolicy.MaxCases = 5;
	ReviewPolicy.NewCaseBias = 0.0f;
	ReviewPolicy.DueCaseBias = 0.0f;
	ReviewPolicy.RecognitionBias = 0.0f;
	ReviewPolicy.AccuracyBias = 0.0f;
	ReviewPolicy.bRepeatFailuresImmediately = true;
	ReviewPolicy.bPreferShortReviewSets = false;

	switch (DeliveryMode)
	{
	case EHyperTwistTrainingDeliveryMode::VirtualCube:
		ReviewPolicy.PolicyKind = EHyperTwistTrainingReviewPolicyKind::VirtualCubePractice;
		ReviewPolicy.PolicyLabel = TEXT("virtual-cube-practice");
		ReviewPolicy.MaxCases = 6;
		ReviewPolicy.NewCaseBias = 80.0f;
		ReviewPolicy.DueCaseBias = 95.0f;
		ReviewPolicy.AccuracyBias = 20.0f;
		break;

	case EHyperTwistTrainingDeliveryMode::SmartDevice:
		ReviewPolicy.PolicyKind = EHyperTwistTrainingReviewPolicyKind::SmartDeviceAccuracy;
		ReviewPolicy.PolicyLabel = TEXT("smart-device-accuracy");
		ReviewPolicy.MaxCases = 5;
		ReviewPolicy.DueCaseBias = 115.0f;
		ReviewPolicy.AccuracyBias = 90.0f;
		ReviewPolicy.bPreferShortReviewSets = true;
		break;

	case EHyperTwistTrainingDeliveryMode::RecognitionAssisted:
		ReviewPolicy.PolicyKind = EHyperTwistTrainingReviewPolicyKind::RecognitionAssistedScan;
		ReviewPolicy.PolicyLabel = TEXT("recognition-assisted-scan");
		ReviewPolicy.MaxCases = 4;
		ReviewPolicy.NewCaseBias = -20.0f;
		ReviewPolicy.DueCaseBias = 105.0f;
		ReviewPolicy.RecognitionBias = 130.0f;
		ReviewPolicy.AccuracyBias = 45.0f;
		ReviewPolicy.bPreferShortReviewSets = true;
		break;

	case EHyperTwistTrainingDeliveryMode::CoachReviewed:
		ReviewPolicy.PolicyKind = EHyperTwistTrainingReviewPolicyKind::CoachReview;
		ReviewPolicy.PolicyLabel = TEXT("coach-review");
		ReviewPolicy.MaxCases = 4;
		ReviewPolicy.NewCaseBias = 35.0f;
		ReviewPolicy.DueCaseBias = 90.0f;
		ReviewPolicy.RecognitionBias = 30.0f;
		ReviewPolicy.AccuracyBias = 110.0f;
		ReviewPolicy.bPreferShortReviewSets = true;
		break;

	case EHyperTwistTrainingDeliveryMode::Timer:
	default:
		ReviewPolicy.PolicyKind = EHyperTwistTrainingReviewPolicyKind::TimerBalanced;
		ReviewPolicy.PolicyLabel = TEXT("timer-balanced");
		ReviewPolicy.MaxCases = 5;
		ReviewPolicy.NewCaseBias = -10.0f;
		ReviewPolicy.DueCaseBias = 120.0f;
		ReviewPolicy.AccuracyBias = 35.0f;
		break;
	}

	return ReviewPolicy;
}

TArray<FHyperTwistTrainingCaseRecommendation> UHyperTwistTrainingRepositoryLibrary::ListCaseRecommendationsForDeck(
	const FHyperTwistTrainingRepositoryState& RepositoryState,
	const FHyperTwistTrainingDeck& Deck,
	const FString& UserId,
	const FString& ReferenceUtc,
	int32 MaxRecommendations
)
{
	TArray<FHyperTwistTrainingCaseRecommendation> Recommendations;
	if (!Deck.IsStructurallyValid())
	{
		return Recommendations;
	}

	for (const FHyperTwistTrainingCase& TrainingCase : Deck.Cases)
	{
		if (!TrainingCase.IsStructurallyValid())
		{
			continue;
		}

		FHyperTwistTrainingCaseLearnerState LearnerState;
		TryGetLearnerState(RepositoryState, UserId, Deck.DeckId, TrainingCase.CaseId, LearnerState);
		FHyperTwistTrainingCaseRecommendation Recommendation = MakeCaseRecommendation(
			TrainingCase,
			LearnerState,
			ReferenceUtc
		);
		if (Recommendation.IsStructurallyValid())
		{
			Recommendations.Add(Recommendation);
		}
	}

	Recommendations.Sort([](const FHyperTwistTrainingCaseRecommendation& Left, const FHyperTwistTrainingCaseRecommendation& Right)
	{
		if (!FMath::IsNearlyEqual(Left.RecommendationScore, Right.RecommendationScore))
		{
			return Left.RecommendationScore > Right.RecommendationScore;
		}

		if (!FMath::IsNearlyEqual(Left.DaysUntilDue, Right.DaysUntilDue))
		{
			return Left.DaysUntilDue < Right.DaysUntilDue;
		}

		return Left.TrainingCase.CaseId < Right.TrainingCase.CaseId;
	});

	if (MaxRecommendations > 0 && Recommendations.Num() > MaxRecommendations)
	{
		Recommendations.SetNum(MaxRecommendations, EAllowShrinking::No);
	}

	return Recommendations;
}

TArray<FHyperTwistTrainingCaseRecommendation> UHyperTwistTrainingRepositoryLibrary::ApplyReviewPolicyToRecommendations(
	const TArray<FHyperTwistTrainingCaseRecommendation>& Recommendations,
	const FHyperTwistTrainingReviewPolicy& ReviewPolicy,
	int32 MaxRecommendations
)
{
	TArray<FHyperTwistTrainingCaseRecommendation> FilteredRecommendations;
	FilteredRecommendations.Reserve(Recommendations.Num());

	for (const FHyperTwistTrainingCaseRecommendation& Recommendation : Recommendations)
	{
		if (!Recommendation.IsStructurallyValid())
		{
			continue;
		}

		FHyperTwistTrainingCaseRecommendation FilteredRecommendation = Recommendation;
		float AdjustedScore = FilteredRecommendation.RecommendationScore;

		if (FilteredRecommendation.bIsNewCase)
		{
			AdjustedScore += ReviewPolicy.NewCaseBias;
		}
		if (FilteredRecommendation.bIsDue)
		{
			AdjustedScore += ReviewPolicy.DueCaseBias;
		}
		if (FilteredRecommendation.TrainingCase.PromptKind == EHyperTwistTrainingPromptKind::Recognition
			|| ReviewPolicy.DeliveryMode == EHyperTwistTrainingDeliveryMode::RecognitionAssisted)
		{
			AdjustedScore += ReviewPolicy.RecognitionBias;
		}
		if (FilteredRecommendation.bRequiresReviewFocus
			|| FilteredRecommendation.LearnerState.LastGrade == EHyperTwistTrainingReviewGrade::Again
			|| FilteredRecommendation.LearnerState.LastGrade == EHyperTwistTrainingReviewGrade::Hard
			|| FilteredRecommendation.LearnerState.LapseCount > 0)
		{
			AdjustedScore += ReviewPolicy.AccuracyBias;
		}

		if (ReviewPolicy.bPreferShortReviewSets
			&& !FilteredRecommendation.bIsDue
			&& !FilteredRecommendation.bRequiresReviewFocus
			&& !FilteredRecommendation.bIsNewCase)
		{
			AdjustedScore -= 55.0f;
		}

		FilteredRecommendation.RecommendationScore = AdjustedScore;
		FilteredRecommendations.Add(FilteredRecommendation);
	}

	FilteredRecommendations.Sort([](const FHyperTwistTrainingCaseRecommendation& Left, const FHyperTwistTrainingCaseRecommendation& Right)
	{
		if (!FMath::IsNearlyEqual(Left.RecommendationScore, Right.RecommendationScore))
		{
			return Left.RecommendationScore > Right.RecommendationScore;
		}

		if (!FMath::IsNearlyEqual(Left.DaysUntilDue, Right.DaysUntilDue))
		{
			return Left.DaysUntilDue < Right.DaysUntilDue;
		}

		return Left.TrainingCase.CaseId < Right.TrainingCase.CaseId;
	});

	const int32 EffectiveMaxRecommendations = HyperTwistTrainingRepositoryLibraryInternal::ResolvePolicyCaseLimit(
		ReviewPolicy,
		MaxRecommendations
	);
	if (EffectiveMaxRecommendations > 0 && FilteredRecommendations.Num() > EffectiveMaxRecommendations)
	{
		FilteredRecommendations.SetNum(EffectiveMaxRecommendations, EAllowShrinking::No);
	}

	return FilteredRecommendations;
}

FHyperTwistTrainingDeck UHyperTwistTrainingRepositoryLibrary::BuildReviewDeckFromRecommendations(
	const FHyperTwistTrainingDeck& SourceDeck,
	const TArray<FHyperTwistTrainingCaseRecommendation>& Recommendations,
	int32 MaxCases
)
{
	FHyperTwistTrainingDeck ReviewDeck = SourceDeck;
	if (!SourceDeck.IsStructurallyValid())
	{
		return FHyperTwistTrainingDeck();
	}

	ReviewDeck.DeckId = SourceDeck.DeckId + TEXT("/review");
	ReviewDeck.Title = SourceDeck.Title + TEXT(" Review");
	ReviewDeck.SelectionPolicy = EHyperTwistTrainingSelectionPolicy::Spaced;
	ReviewDeck.Tags = SourceDeck.Tags;
	ReviewDeck.Tags.AddUnique(TEXT("review"));
	ReviewDeck.Tags.AddUnique(TEXT("recommendation-shaped"));
	ReviewDeck.Cases.Reset();

	const int32 EffectiveMaxCases = MaxCases > 0 ? MaxCases : Recommendations.Num();
	for (const FHyperTwistTrainingCaseRecommendation& Recommendation : Recommendations)
	{
		if (!Recommendation.IsStructurallyValid())
		{
			continue;
		}

		ReviewDeck.Cases.Add(Recommendation.TrainingCase);
		if (ReviewDeck.Cases.Num() >= EffectiveMaxCases)
		{
			break;
		}
	}

	if (ReviewDeck.Cases.Num() == 0)
	{
		return FHyperTwistTrainingDeck();
	}

	return ReviewDeck;
}

FHyperTwistTrainingReviewPlanState UHyperTwistTrainingRepositoryLibrary::BuildReviewPlan(
	const FHyperTwistTrainingDeck& SourceDeck,
	const FHyperTwistTrainingDeck& ReviewDeck,
	const FString& UserId,
	const TArray<FHyperTwistTrainingCaseRecommendation>& Recommendations,
	const FHyperTwistTrainingReviewPolicy& ReviewPolicy,
	const FString& PlanId,
	const FString& ReferenceUtc,
	EHyperTwistTrainingDeliveryMode DeliveryMode
)
{
	FHyperTwistTrainingReviewPlanState ReviewPlan;
	if (!SourceDeck.IsStructurallyValid() || !ReviewDeck.IsStructurallyValid() || UserId.IsEmpty())
	{
		return ReviewPlan;
	}

	ReviewPlan.PlanId = PlanId.IsEmpty() ? FString::Printf(TEXT("review_plan_%s"), *ReviewDeck.DeckId) : PlanId;
	ReviewPlan.UserId = UserId;
	ReviewPlan.SourceDeckId = SourceDeck.DeckId;
	ReviewPlan.ReviewDeckId = ReviewDeck.DeckId;
	ReviewPlan.ReferenceUtc = ReferenceUtc.IsEmpty() ? FDateTime::UtcNow().ToIso8601() : ReferenceUtc;
	ReviewPlan.DeliveryMode = DeliveryMode;
	ReviewPlan.Policy = ReviewPolicy;
	ReviewPlan.CreatedAtUtc = ReviewPlan.ReferenceUtc;
	ReviewPlan.StartedAtUtc = ReviewPlan.ReferenceUtc;

	for (const FHyperTwistTrainingCase& TrainingCase : ReviewDeck.Cases)
	{
		if (!TrainingCase.IsStructurallyValid())
		{
			continue;
		}

		const FHyperTwistTrainingCaseRecommendation* MatchingRecommendation = Recommendations.FindByPredicate(
			[&TrainingCase](const FHyperTwistTrainingCaseRecommendation& Candidate)
			{
				return Candidate.TrainingCase.CaseId == TrainingCase.CaseId;
			}
		);

		FHyperTwistTrainingReviewPlanEntry Entry;
		Entry.CaseId = TrainingCase.CaseId;
		Entry.PromptLabel = TrainingCase.PromptLabel;
		Entry.RecommendationReason = MatchingRecommendation != nullptr ? MatchingRecommendation->RecommendationReason : TEXT("review-case");
		Entry.RecommendationScore = MatchingRecommendation != nullptr ? MatchingRecommendation->RecommendationScore : 0.0f;
		Entry.EntryState = EHyperTwistTrainingReviewPlanEntryState::Pending;
		ReviewPlan.Entries.Add(Entry);
	}

	if (ReviewPlan.Entries.Num() > 0)
	{
		ReviewPlan.CurrentEntryIndex = 0;
		ReviewPlan.Entries[0].EntryState = EHyperTwistTrainingReviewPlanEntryState::Current;
	}

	return HyperTwistTrainingRepositoryLibraryInternal::NormalizeReviewPlanState(ReviewPlan);
}

FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::UpsertReviewPlan(
	const FHyperTwistTrainingRepositoryState& RepositoryState,
	const FHyperTwistTrainingReviewPlanState& ReviewPlan
)
{
	FHyperTwistTrainingRepositoryState UpdatedState = RepositoryState;
	const FHyperTwistTrainingReviewPlanState NormalizedReviewPlan =
		HyperTwistTrainingRepositoryLibraryInternal::NormalizeReviewPlanState(ReviewPlan);
	if (!NormalizedReviewPlan.IsStructurallyValid())
	{
		return UpdatedState;
	}

	const int32 ExistingIndex = UpdatedState.ReviewPlans.IndexOfByPredicate(
		[&NormalizedReviewPlan](const FHyperTwistTrainingReviewPlanState& ExistingPlan)
		{
			return ExistingPlan.PlanId == NormalizedReviewPlan.PlanId;
		}
	);

	if (ExistingIndex >= 0)
	{
		UpdatedState.ReviewPlans[ExistingIndex] = NormalizedReviewPlan;
	}
	else
	{
		UpdatedState.ReviewPlans.Add(NormalizedReviewPlan);
	}

	return UpdatedState;
}

bool UHyperTwistTrainingRepositoryLibrary::TryGetReviewPlan(
	const FHyperTwistTrainingRepositoryState& RepositoryState,
	const FString& PlanId,
	FHyperTwistTrainingReviewPlanState& OutReviewPlan
)
{
	OutReviewPlan = FHyperTwistTrainingReviewPlanState();
	if (PlanId.IsEmpty())
	{
		return false;
	}

	const FHyperTwistTrainingReviewPlanState* ExistingPlan = RepositoryState.ReviewPlans.FindByPredicate(
		[&PlanId](const FHyperTwistTrainingReviewPlanState& Candidate)
		{
			return Candidate.PlanId == PlanId;
		}
	);

	if (ExistingPlan == nullptr)
	{
		return false;
	}

	OutReviewPlan = HyperTwistTrainingRepositoryLibraryInternal::NormalizeReviewPlanState(*ExistingPlan);
	return OutReviewPlan.IsStructurallyValid();
}

FHyperTwistTrainingReviewPlanState UHyperTwistTrainingRepositoryLibrary::UpdateReviewPlanAfterAttempt(
	const FHyperTwistTrainingReviewPlanState& ReviewPlan,
	const FHyperTwistTrainingRunStepResult& StepResult,
	const FHyperTwistTrainingAttempt& Attempt
)
{
	FHyperTwistTrainingReviewPlanState UpdatedPlan = ReviewPlan;
	if (!UpdatedPlan.IsStructurallyValid() || !StepResult.IsStructurallyValid() || !Attempt.IsStructurallyValid())
	{
		return UpdatedPlan;
	}

	const int32 EntryIndex = UpdatedPlan.Entries.IndexOfByPredicate(
		[&Attempt](const FHyperTwistTrainingReviewPlanEntry& Entry)
		{
			return Entry.CaseId == Attempt.CaseId;
		}
	);
	if (EntryIndex < 0)
	{
		return UpdatedPlan;
	}

	FHyperTwistTrainingReviewPlanEntry& Entry = UpdatedPlan.Entries[EntryIndex];
	Entry.AttemptCount += 1;
	Entry.LastAttemptId = Attempt.AttemptId;
	Entry.LastOutcome = Attempt.Result;
	Entry.LastCompletedAtUtc = Attempt.CompletedAtUtc;

	if (Attempt.Result == EHyperTwistTrainingAttemptResult::Success)
	{
		Entry.SuccessCount += 1;
		Entry.EntryState = EHyperTwistTrainingReviewPlanEntryState::Completed;
	}
	else if (Attempt.Result == EHyperTwistTrainingAttemptResult::Aborted)
	{
		Entry.EntryState = EHyperTwistTrainingReviewPlanEntryState::Skipped;
	}
	else
	{
		Entry.EntryState = EHyperTwistTrainingReviewPlanEntryState::NeedsRepeat;
	}

	for (int32 Index = 0; Index < UpdatedPlan.Entries.Num(); ++Index)
	{
		if (Index == EntryIndex)
		{
			continue;
		}

		if (UpdatedPlan.Entries[Index].EntryState == EHyperTwistTrainingReviewPlanEntryState::Current)
		{
			UpdatedPlan.Entries[Index].EntryState = EHyperTwistTrainingReviewPlanEntryState::Pending;
		}
	}

	const EHyperTwistTrainingSessionState SessionState = StepResult.UpdatedRunState.Session.SessionState;
	if (SessionState == EHyperTwistTrainingSessionState::Completed
		|| SessionState == EHyperTwistTrainingSessionState::Aborted)
	{
		UpdatedPlan.bCompleted = true;
		UpdatedPlan.CurrentEntryIndex = INDEX_NONE;
		UpdatedPlan.CompletedAtUtc = !StepResult.UpdatedRunState.Session.EndedAtUtc.IsEmpty()
			? StepResult.UpdatedRunState.Session.EndedAtUtc
			: Attempt.CompletedAtUtc;
		return HyperTwistTrainingRepositoryLibraryInternal::NormalizeReviewPlanState(UpdatedPlan);
	}

	FString NextCaseId;
	if (StepResult.UpdatedRunState.CurrentSelection.IsStructurallyValid())
	{
		NextCaseId = StepResult.UpdatedRunState.CurrentSelection.TrainingCase.CaseId;
	}
	if (NextCaseId.IsEmpty())
	{
		NextCaseId = StepResult.UpdatedRunState.Session.CurrentCaseId;
	}

	int32 NextEntryIndex = INDEX_NONE;
	if (!NextCaseId.IsEmpty())
	{
		NextEntryIndex = UpdatedPlan.Entries.IndexOfByPredicate(
			[&NextCaseId](const FHyperTwistTrainingReviewPlanEntry& Candidate)
			{
				return Candidate.CaseId == NextCaseId
					&& Candidate.EntryState != EHyperTwistTrainingReviewPlanEntryState::Completed
					&& Candidate.EntryState != EHyperTwistTrainingReviewPlanEntryState::Skipped;
			}
		);
	}

	if (NextEntryIndex < 0)
	{
		NextEntryIndex = UpdatedPlan.Entries.IndexOfByPredicate(
			[](const FHyperTwistTrainingReviewPlanEntry& Candidate)
			{
				return Candidate.EntryState == EHyperTwistTrainingReviewPlanEntryState::NeedsRepeat
					|| Candidate.EntryState == EHyperTwistTrainingReviewPlanEntryState::Pending;
			}
		);
	}

	UpdatedPlan.CurrentEntryIndex = NextEntryIndex;
	if (NextEntryIndex >= 0)
	{
		UpdatedPlan.Entries[NextEntryIndex].EntryState = EHyperTwistTrainingReviewPlanEntryState::Current;
	}

	return HyperTwistTrainingRepositoryLibraryInternal::NormalizeReviewPlanState(UpdatedPlan);
}

FHyperTwistTrainingReviewFlowStatus UHyperTwistTrainingRepositoryLibrary::DeriveReviewFlowStatus(
	const FHyperTwistTrainingReviewPlanState& ReviewPlan
)
{
	FHyperTwistTrainingReviewFlowStatus FlowStatus;
	if (!ReviewPlan.IsStructurallyValid())
	{
		return FlowStatus;
	}

	FlowStatus.PlanId = ReviewPlan.PlanId;
	FlowStatus.bPlanComplete = ReviewPlan.bCompleted;

	int32 CurrentEntryIndex = ReviewPlan.CurrentEntryIndex;
	if (!ReviewPlan.Entries.IsValidIndex(CurrentEntryIndex))
	{
		CurrentEntryIndex = ReviewPlan.Entries.IndexOfByPredicate(
			[](const FHyperTwistTrainingReviewPlanEntry& Entry)
			{
				return Entry.EntryState == EHyperTwistTrainingReviewPlanEntryState::Current;
			}
		);
	}

	for (const FHyperTwistTrainingReviewPlanEntry& Entry : ReviewPlan.Entries)
	{
		switch (Entry.EntryState)
		{
		case EHyperTwistTrainingReviewPlanEntryState::Pending:
			FlowStatus.PendingCount += 1;
			break;
		case EHyperTwistTrainingReviewPlanEntryState::NeedsRepeat:
			FlowStatus.RepeatCount += 1;
			break;
		case EHyperTwistTrainingReviewPlanEntryState::Completed:
			FlowStatus.CompletedCount += 1;
			break;
		case EHyperTwistTrainingReviewPlanEntryState::Skipped:
			FlowStatus.SkippedCount += 1;
			break;
		default:
			break;
		}

		if (Entry.EntryState == EHyperTwistTrainingReviewPlanEntryState::NeedsRepeat && Entry.AttemptCount >= 2)
		{
			FlowStatus.bNeedsCoachIntervention = true;
		}
	}

	const bool bCoachFacingRepeatPressure =
		FlowStatus.RepeatCount > 0
		&& (
			ReviewPlan.Policy.PolicyKind == EHyperTwistTrainingReviewPolicyKind::CoachReview
			|| ReviewPlan.Policy.bPreferRecognitionReplayReview
		);
	if (bCoachFacingRepeatPressure)
	{
		FlowStatus.bNeedsCoachIntervention = true;
	}

	const int32 TotalTerminalCount = FlowStatus.CompletedCount + FlowStatus.SkippedCount;
	if (ReviewPlan.Entries.Num() > 0)
	{
		FlowStatus.ProgressRatio = static_cast<float>(TotalTerminalCount) / static_cast<float>(ReviewPlan.Entries.Num());
	}

	if (ReviewPlan.Entries.IsValidIndex(CurrentEntryIndex))
	{
		const FHyperTwistTrainingReviewPlanEntry& CurrentEntry = ReviewPlan.Entries[CurrentEntryIndex];
		FlowStatus.CurrentCaseId = CurrentEntry.CaseId;
		FlowStatus.CurrentPromptLabel = CurrentEntry.PromptLabel;
		FlowStatus.bHasCurrentEntry = true;
	}

	if (FlowStatus.bPlanComplete || TotalTerminalCount >= ReviewPlan.Entries.Num())
	{
		FlowStatus.bPlanComplete = true;
		FlowStatus.NextActionLabel = TEXT("review-complete");
	}
	else if (FlowStatus.bNeedsCoachIntervention)
	{
		FlowStatus.NextActionLabel = TEXT("coach-review-needed");
	}
	else if (FlowStatus.bHasCurrentEntry)
	{
		FlowStatus.NextActionLabel = TEXT("continue-review");
	}
	else if (FlowStatus.RepeatCount > 0)
	{
		FlowStatus.NextActionLabel = TEXT("repeat-failed-cases");
	}
	else if (FlowStatus.PendingCount > 0)
	{
		FlowStatus.NextActionLabel = TEXT("advance-to-next-case");
	}
	else
	{
		FlowStatus.NextActionLabel = TEXT("finalize-review");
	}

	return FlowStatus;
}

FHyperTwistTrainingReviewProgramSummary UHyperTwistTrainingRepositoryLibrary::DeriveReviewProgramSummary(
	const FHyperTwistTrainingRepositoryState& RepositoryState,
	const FString& UserId,
	const FString& ReferenceUtc
)
{
	FHyperTwistTrainingReviewProgramSummary Summary;
	if (UserId.IsEmpty())
	{
		return Summary;
	}

	Summary.UserId = UserId;
	Summary.ReferenceUtc = HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(ReferenceUtc);

	const TArray<FHyperTwistTrainingMethodSegment> MethodSegments = DeriveLearnerMethodSegments(
		RepositoryState,
		UserId,
		Summary.ReferenceUtc
	);
	const auto ResolveMethodSegmentIdForDeck = [&MethodSegments](const FString& DeckId) -> FString
	{
		for (const FHyperTwistTrainingMethodSegment& MethodSegment : MethodSegments)
		{
			if (MethodSegment.DeckIds.Contains(DeckId))
			{
				return MethodSegment.MethodDescriptor.SegmentId;
			}
		}

		return FString();
	};

	int32 BestCarryoverScore = -1;
	FString BestPlanCreatedAtUtc;
	FString LatestCompletedAtUtc;
	int32 DeliveryModeCounts[5] = {0, 0, 0, 0, 0};
	TMap<EHyperTwistTrainingReviewPolicyKind, int32> PolicyCounts;
	int64 CompletedPlanDurationAccumulator = 0;
	int32 CompletedPlanDurationCount = 0;
	float ProgressRatioAccumulator = 0.0f;

	for (const FHyperTwistTrainingReviewPlanState& ReviewPlan : RepositoryState.ReviewPlans)
	{
		if (!ReviewPlan.IsStructurallyValid() || ReviewPlan.UserId != UserId)
		{
			continue;
		}

		const FHyperTwistTrainingReviewFlowStatus FlowStatus = DeriveReviewFlowStatus(ReviewPlan);
		const int32 CarryoverCount = FlowStatus.PendingCount
			+ FlowStatus.RepeatCount
			+ (FlowStatus.bHasCurrentEntry ? 1 : 0);
		const int32 DeliveryModeIndex = FMath::Clamp(static_cast<int32>(ReviewPlan.DeliveryMode), 0, 4);
		DeliveryModeCounts[DeliveryModeIndex] += 1;
		PolicyCounts.FindOrAdd(ReviewPlan.Policy.PolicyKind) += 1;
		Summary.TotalPlanCount += 1;
		Summary.TotalEntryCount += ReviewPlan.Entries.Num();
		Summary.CompletedEntryCount += FlowStatus.CompletedCount;
		Summary.SkippedEntryCount += FlowStatus.SkippedCount;
		ProgressRatioAccumulator += FlowStatus.ProgressRatio;

		if (Summary.LastPlanCreatedAtUtc.IsEmpty()
			|| HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(
				ReviewPlan.CreatedAtUtc,
				Summary.LastPlanCreatedAtUtc
			) > 0)
		{
			Summary.LastPlanCreatedAtUtc = ReviewPlan.CreatedAtUtc;
		}

		if (ReviewPlan.bCompleted || FlowStatus.bPlanComplete)
		{
			Summary.CompletedPlanCount += 1;
			const FString CompletedAtUtc = !ReviewPlan.CompletedAtUtc.IsEmpty()
				? ReviewPlan.CompletedAtUtc
				: ReviewPlan.CreatedAtUtc;
			if (LatestCompletedAtUtc.IsEmpty()
				|| HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(CompletedAtUtc, LatestCompletedAtUtc) > 0)
			{
				LatestCompletedAtUtc = CompletedAtUtc;
				Summary.LastCompletedPlanId = ReviewPlan.PlanId;
			}
			const int32 PlanDurationMs = HyperTwistTrainingRepositoryLibraryInternal::ComputePlanDurationMs(
				ReviewPlan,
				Summary.ReferenceUtc
			);
			if (PlanDurationMs > 0)
			{
				CompletedPlanDurationAccumulator += PlanDurationMs;
				CompletedPlanDurationCount += 1;
				if (Summary.BestCompletedPlanDurationMs == 0 || PlanDurationMs < Summary.BestCompletedPlanDurationMs)
				{
					Summary.BestCompletedPlanDurationMs = PlanDurationMs;
				}
			}
			continue;
		}

		Summary.PendingPlanIds.Add(ReviewPlan.PlanId);
		Summary.PendingPlanCount += 1;
		Summary.PendingEntryCount += FlowStatus.PendingCount;
		Summary.RepeatEntryCount += FlowStatus.RepeatCount;
		Summary.CarryoverCaseCount += CarryoverCount;
		if (FlowStatus.bNeedsCoachIntervention)
		{
			Summary.CoachInterventionPlanCount += 1;
			Summary.bNeedsCoachIntervention = true;
		}

		const int32 CarryoverScore = CarryoverCount * 100
			+ (FlowStatus.bNeedsCoachIntervention ? 25 : 0)
			+ (FlowStatus.bHasCurrentEntry ? 10 : 0);
		if (CarryoverScore > BestCarryoverScore
			|| (CarryoverScore == BestCarryoverScore
				&& (BestPlanCreatedAtUtc.IsEmpty()
					|| HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings(ReviewPlan.CreatedAtUtc, BestPlanCreatedAtUtc) > 0)))
		{
			BestCarryoverScore = CarryoverScore;
			BestPlanCreatedAtUtc = ReviewPlan.CreatedAtUtc;
			Summary.FocusDeckId = ReviewPlan.SourceDeckId;
			Summary.FocusMethodSegmentId = ResolveMethodSegmentIdForDeck(ReviewPlan.SourceDeckId);
			Summary.NextPlanId = ReviewPlan.PlanId;
			if (FlowStatus.bHasCurrentEntry)
			{
				Summary.CurrentPlanId = ReviewPlan.PlanId;
			}
		}

		if (Summary.CurrentPlanId.IsEmpty() && FlowStatus.bHasCurrentEntry)
		{
			Summary.CurrentPlanId = ReviewPlan.PlanId;
		}
	}

	if (Summary.CurrentPlanId.IsEmpty())
	{
		Summary.CurrentPlanId = Summary.NextPlanId;
	}
	if (Summary.FocusMethodSegmentId.IsEmpty() && !Summary.FocusDeckId.IsEmpty())
	{
		Summary.FocusMethodSegmentId = ResolveMethodSegmentIdForDeck(Summary.FocusDeckId);
	}

	if (Summary.TotalPlanCount > 0)
	{
		Summary.AveragePlanProgressRatio = ProgressRatioAccumulator / static_cast<float>(Summary.TotalPlanCount);
	}
	if (Summary.PendingPlanCount > 0)
	{
		Summary.AveragePendingEntriesPerOpenPlan =
			static_cast<float>(Summary.PendingEntryCount) / static_cast<float>(Summary.PendingPlanCount);
		Summary.AverageCarryoverCasesPerOpenPlan =
			static_cast<float>(Summary.CarryoverCaseCount) / static_cast<float>(Summary.PendingPlanCount);
	}
	if (CompletedPlanDurationCount > 0)
	{
		Summary.AverageCompletedPlanDurationMs = static_cast<int32>(
			CompletedPlanDurationAccumulator / CompletedPlanDurationCount
		);
	}

	int32 BestModeCount = -1;
	for (int32 ModeIndex = 0; ModeIndex < UE_ARRAY_COUNT(DeliveryModeCounts); ++ModeIndex)
	{
		if (DeliveryModeCounts[ModeIndex] > BestModeCount)
		{
			BestModeCount = DeliveryModeCounts[ModeIndex];
			Summary.DominantDeliveryMode = static_cast<EHyperTwistTrainingDeliveryMode>(ModeIndex);
		}
	}

	int32 BestPolicyCount = -1;
	for (const TPair<EHyperTwistTrainingReviewPolicyKind, int32>& Pair : PolicyCounts)
	{
		if (Pair.Value > BestPolicyCount)
		{
			BestPolicyCount = Pair.Value;
			Summary.DominantPolicyKind = Pair.Key;
		}
	}

	Summary.ProgramPressureScore =
		static_cast<float>(Summary.CarryoverCaseCount * 1.0f)
		+ static_cast<float>(Summary.RepeatEntryCount * 1.5f)
		+ static_cast<float>(Summary.CoachInterventionPlanCount * 3.0f)
		+ (Summary.CurrentPlanId.IsEmpty() ? 0.0f : 1.0f);
	Summary.bHasCarryover = Summary.CarryoverCaseCount > 0 || Summary.PendingPlanCount > 0;
	return Summary;
}

FHyperTwistTrainingReviewPlanState UHyperTwistTrainingRepositoryLibrary::FinalizeReviewPlan(
	const FHyperTwistTrainingReviewPlanState& ReviewPlan,
	const FHyperTwistTrainingSession& Session
)
{
	FHyperTwistTrainingReviewPlanState UpdatedPlan = ReviewPlan;
	if (!UpdatedPlan.IsStructurallyValid())
	{
		return UpdatedPlan;
	}

	UpdatedPlan.bCompleted = false;
	UpdatedPlan.CurrentEntryIndex = INDEX_NONE;
	UpdatedPlan.CompletedAtUtc = !Session.EndedAtUtc.IsEmpty()
		? Session.EndedAtUtc
		: FDateTime::UtcNow().ToIso8601();

	for (FHyperTwistTrainingReviewPlanEntry& Entry : UpdatedPlan.Entries)
	{
		if (Entry.EntryState == EHyperTwistTrainingReviewPlanEntryState::Current)
		{
			Entry.EntryState = Session.SessionState == EHyperTwistTrainingSessionState::Aborted
				? EHyperTwistTrainingReviewPlanEntryState::Skipped
				: EHyperTwistTrainingReviewPlanEntryState::Pending;
		}
	}

	return HyperTwistTrainingRepositoryLibraryInternal::NormalizeReviewPlanState(UpdatedPlan);
}
