// © 2025 Naked People Team. All Rights Reserved.


#include "SessionLossResolver.h"

#include "DefaultLevelSequenceInstanceData.h"
#include "LevelSequence.h"
#include "LevelSequenceActor.h"
#include "LevelSequencePlayer.h"
#include "LossPresentationConfig.h"
#include "MovieSceneSequencePlaybackSettings.h"
#include "NakedDesireGameInstance.h"
#include "TimeOfDaySubsystem.h"
#include "GameFramework/Character.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/PlayerStart.h"
#include "Kismet/GameplayStatics.h"
#include "NakedDesire/Clothing/ClothingItemInstance.h"
#include "NakedDesire/Interactables/ItemPickup.h"
#include "NakedDesire/Player/NakedDesireCharacter.h"
#include "NakedDesire/Stats/StatsManager.h"
#include "NakedDesire/SaveGame/GlobalSaveGameData.h"
#include "NakedDesire/SaveGame/SaveSubsystem.h"

const FName USessionLossResolver::HomePlayerStartTag = FName(TEXT("Home"));

void USessionLossResolver::OnWorldBeginPlay(UWorld& InWorld)
{
	Super::OnWorldBeginPlay(InWorld);

	if (USessionManagerSubsystem* SessionManager = InWorld.GetSubsystem<USessionManagerSubsystem>())
	{
		SessionManager->OnSessionEnd.AddDynamic(this, &USessionLossResolver::ResolveLoss);
	}
}

void USessionLossResolver::ResolveLoss(ESessionLossCause Cause)
{
	// §4.4 precedence: if a chase is in progress, the loss always resolves as
	// police capture — the chase wins regardless of what actually fired.
	if (const USessionManagerSubsystem* SessionManager = GetWorld()->GetSubsystem<USessionManagerSubsystem>())
	{
		if (SessionManager->IsPoliceChaseActive())
		{
			Cause = ESessionLossCause::PoliceCapture;
		}
	}

	// §4.4: equipped clothing is never forcibly removed — the resolver simply never
	// touches equipped items.
	// TODO(§6.4): when bags exist, a bag placed in the world is lost with its
	// contents — mark its world record for deletion here.

	bool bWentToHoldingCell = false;

	switch (Cause)
	{
	case ESessionLossCause::SafeReturn:
		// Normal session end; nothing is lost. Autosave below.
		break;

	case ESessionLossCause::EmbarrassmentMax:
		// Fade to apartment with no extra cost — no time skip, money, or rep hit.
		LoseAllWorldClothing();
		break;

	case ESessionLossCause::EnergyZero:
		// Forced sleep cycle: everything left outside the apartment is guaranteed lost.
		if (UTimeOfDaySubsystem* Time = GetWorld()->GetSubsystem<UTimeOfDaySubsystem>())
		{
			Time->Sleep();
		}
		ResolveSleepLoss();
		break;

	case ESessionLossCause::PoliceCapture:
		{
			LoseAllWorldClothing();
			UGlobalSaveGameData* Save = GetSave();
			if (Save && Save->Money >= PoliceCaptureMoneyPenalty)
			{
				// Can pay: deduct the penalty and go home.
				Save->Money -= PoliceCaptureMoneyPenalty;
			}
			else
			{
				// Can't pay: a night in the holding cell settles the debt; no money owed.
				bWentToHoldingCell = true;
			}
			ClearWanted();
		}
		break;
	}

	Autosave();

	OnSessionLossResolved.Broadcast(Cause, bWentToHoldingCell);

	// Presentation: play the cause's cutscene, then teleport the player home on finish.
	BeginLossPresentation(Cause);
}

void USessionLossResolver::ResolveSleepLoss()
{
	// §4.4: sleeping is the deterministic side of the theft model — everything left
	// outside the apartment is lost, regardless of the in-session grace / roll state.
	// No autosave here: the sleep flow persists once via UTimeOfDaySubsystem::Sleep(),
	// which runs immediately after this. Callers must ensure a save follows.
	LoseAllWorldClothing();
}

void USessionLossResolver::LoseAllWorldClothing()
{
	UGlobalSaveGameData* Save = GetSave();

	// Loose clothing in the world is represented by AItemPickup actors. Dropping only
	// happens outside, and the apartment stores items in the wardrobe (not as pickups),
	// so every pickup currently counts as "outside the apartment".
	// TODO: once an apartment volume query exists, spare pickups that sit inside it.
	TArray<AActor*> Pickups;
	UGameplayStatics::GetAllActorsOfClass(this, AItemPickup::StaticClass(), Pickups);
	for (AActor* Actor : Pickups)
	{
		AItemPickup* Pickup = Cast<AItemPickup>(Actor);
		if (!Pickup)
			continue;

		if (Save)
		{
			if (UClothingItemInstance* Item = Pickup->GetItem())
			{
				Save->RemoveWorldItem(Item);
			}
		}
		Pickup->Destroy();
	}
}

void USessionLossResolver::ClearWanted()
{
	// TODO(§7.7 / Phase 6): clear the `wanted` tag once the Wanted attribute exists.
}

void USessionLossResolver::BeginLossPresentation(ESessionLossCause Cause)
{
	// SafeReturn = the player walked home under their own power; nothing to present.
	if (Cause == ESessionLossCause::SafeReturn)
		return;

	ULevelSequence* Sequence = nullptr;
	if (const ULossPresentationConfig* Config = GetPresentationConfig())
	{
		if (const TSoftObjectPtr<ULevelSequence>* Found = Config->LossCutscenes.Find(Cause))
		{
			// Synchronous load is acceptable here: the loss has already resolved and the
			// player is stationary, so the brief hitch is hidden by the transition.
			Sequence = Found->LoadSynchronous();
		}
	}

	if (!Sequence)
	{
		// No cutscene authored for this cause — go straight home.
		TeleportPlayerHome();
		return;
	}

	// Lock the player out of movement / look while the cutscene owns the view; state is
	// restored when the sequence finishes (just before we teleport home).
	FMovieSceneSequencePlaybackSettings Settings;
	Settings.bDisableMovementInput = true;
	Settings.bDisableLookAtInput = true;
	// Restore track state on finish so the view/player return to normal before we teleport.
	Settings.FinishCompletionStateOverride = EMovieSceneCompletionModeOverride::ForceRestoreState;

	ALevelSequenceActor* SequenceActor = nullptr;
	ActiveLossSequencePlayer = ULevelSequencePlayer::CreateLevelSequencePlayer(this, Sequence, Settings, SequenceActor);
	ActiveLossSequenceActor = SequenceActor;
	ActiveLossSequenceActor->bOverrideInstanceData = true;
	UDefaultLevelSequenceInstanceData* InstanceData = Cast<UDefaultLevelSequenceInstanceData>(ActiveLossSequenceActor->DefaultInstanceData.Get());
	InstanceData->TransformOriginActor = UGameplayStatics::GetPlayerPawn(GetWorld(), 0);

	if (!ActiveLossSequencePlayer)
	{
		// Playback couldn't be created — don't strand the player outside.
		TeleportPlayerHome();
		return;
	}

	ActiveLossSequencePlayer->OnFinished.AddDynamic(this, &USessionLossResolver::HandleLossCutsceneFinished);
	ActiveLossSequencePlayer->Play();
}

void USessionLossResolver::HandleLossCutsceneFinished()
{
	if (ActiveLossSequencePlayer)
	{
		ActiveLossSequencePlayer->OnFinished.RemoveDynamic(this, &USessionLossResolver::HandleLossCutsceneFinished);
	}

	TeleportPlayerHome();

	// Tear down the spawned sequence player/actor now that the cutscene is done.
	if (ActiveLossSequenceActor)
	{
		ActiveLossSequenceActor->Destroy();
	}
	ActiveLossSequenceActor = nullptr;
	ActiveLossSequencePlayer = nullptr;
}

void USessionLossResolver::TeleportPlayerHome()
{
	UWorld* World = GetWorld();
	if (!World)
		return;

	APawn* PlayerPawn = UGameplayStatics::GetPlayerPawn(World, 0);
	if (!PlayerPawn)
		return;

	// Prefer a PlayerStart tagged "Home"; fall back to the first one in the level.
	TArray<AActor*> Starts;
	UGameplayStatics::GetAllActorsOfClass(World, APlayerStart::StaticClass(), Starts);

	APlayerStart* Home = nullptr;
	for (AActor* Actor : Starts)
	{
		APlayerStart* Start = Cast<APlayerStart>(Actor);
		if (!Start)
			continue;

		if (!Home)
			Home = Start;
		if (Start->PlayerStartTag == HomePlayerStartTag)
		{
			Home = Start;
			break;
		}
	}

	if (!Home)
	{
		UE_LOG(LogTemp, Warning, TEXT("USessionLossResolver: no APlayerStart found; cannot teleport player home."));
		return;
	}

	PlayerPawn->TeleportTo(Home->GetActorLocation(), Home->GetActorRotation());

	// Kill residual velocity so the character doesn't slide on arrival.
	if (const ACharacter* Character = Cast<ACharacter>(PlayerPawn))
	{
		if (UCharacterMovementComponent* Movement = Character->GetCharacterMovement())
		{
			Movement->StopMovementImmediately();
		}
	}

	// A new day starts at home: clear embarrassment and refill energy / stamina (§4.4).
	if (const ANakedDesireCharacter* Character = Cast<ANakedDesireCharacter>(PlayerPawn))
	{
		if (UStatsManager* Stats = Character->StatsManager)
		{
			Stats->ResetStats();
		}
	}
}

void USessionLossResolver::Autosave() const
{
	if (const UGameInstance* GameInstance = GetWorld()->GetGameInstance())
	{
		if (USaveSubsystem* SaveSubsystem = GameInstance->GetSubsystem<USaveSubsystem>())
		{
			SaveSubsystem->SaveGame();
		}
	}
}

UGlobalSaveGameData* USessionLossResolver::GetSave() const
{
	if (const UGameInstance* GameInstance = GetWorld()->GetGameInstance())
	{
		if (USaveSubsystem* SaveSubsystem = GameInstance->GetSubsystem<USaveSubsystem>())
		{
			return SaveSubsystem->GetCurrentSave();
		}
	}
	return nullptr;
}

ULossPresentationConfig* USessionLossResolver::GetPresentationConfig() const
{
	const UNakedDesireGameInstance* GameInstance =
		Cast<UNakedDesireGameInstance>(GetWorld() ? GetWorld()->GetGameInstance() : nullptr);
	return GameInstance ? GameInstance->LossPresentation : nullptr;
}