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


#include "TimeOfDaySubsystem.h"
#include "Constants.h"
#include "Kismet/GameplayStatics.h"
#include "Misc/App.h"
#include "Misc/Timecode.h"
#include "NakedDesire/Player/NakedDesireCharacter.h"
#include "NakedDesire/SaveGame/GlobalSaveGameData.h"
#include "NakedDesire/SaveGame/SaveSubsystem.h"
#include "NakedDesire/Stats/StatsManager.h"

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

	if (const UGlobalSaveGameData* Save = GetSave())
	{
		CurrentPhase = ComputePhase(Save->MinuteOfDay);
	}

	bBegunPlay = true;
	PushTimeToSky(/*bForce=*/true); // sync the sky to the loaded time immediately

	// Sanity-log the clock speed: how long a full 24h in-game day takes in real time.
	const double RealMinutesPerDay = MINUTES_PER_DAY / (INGAME_MINUTES_PER_REAL_SECOND * 60.0);
	UE_LOG(LogTemp, Warning, TEXT("UTimeOfDaySubsystem: a full in-game day takes %.2f real minutes (%.1f in-game minutes/real second)."),
		RealMinutesPerDay, INGAME_MINUTES_PER_REAL_SECOND);
}

void UTimeOfDaySubsystem::Tick(float DeltaTime)
{
	if (!bBegunPlay)
		return;

	// The sleep sweep owns the clock while it runs — it drives AdvanceClock itself, so
	// skip the normal real-time advancement (and ignore pause, which sleep doesn't honor).
	if (bSleeping)
	{
		TickSleep(DeltaTime);
		return;
	}

	if (IsPaused())
		return;

	AdvanceClock(static_cast<double>(DeltaTime) * INGAME_MINUTES_PER_REAL_SECOND);
}

TStatId UTimeOfDaySubsystem::GetStatId() const
{
	RETURN_QUICK_DECLARE_CYCLE_STAT(UTimeOfDaySubsystem, STATGROUP_Tickables);
}

int32 UTimeOfDaySubsystem::GetDay() const
{
	const UGlobalSaveGameData* Save = GetSave();
	return Save ? Save->DaysPassed : 0;
}

float UTimeOfDaySubsystem::GetMinuteOfDay() const
{
	const UGlobalSaveGameData* Save = GetSave();
	return Save ? Save->MinuteOfDay : 0.0f;
}

int32 UTimeOfDaySubsystem::GetHour() const
{
	return FMath::FloorToInt(GetMinuteOfDay() / MINUTES_PER_HOUR);
}

int32 UTimeOfDaySubsystem::GetMinute() const
{
	return FMath::FloorToInt(GetMinuteOfDay()) % MINUTES_PER_HOUR;
}

EDayPhase UTimeOfDaySubsystem::GetPhase() const
{
	return ComputePhase(GetMinuteOfDay());
}

void UTimeOfDaySubsystem::SkipTime(float Minutes)
{
	if (Minutes > 0.0f)
	{
		// Discrete jump — snap the sky to the new time, don't wait for the throttle.
		AdvanceClock(static_cast<double>(Minutes), /*bForceSkyPush=*/true);
	}
}

void UTimeOfDaySubsystem::SkipToNextMorning()
{
	const double Target = DAY_START_HOUR * MINUTES_PER_HOUR; // 08:00
	double Delta = Target - GetMinuteOfDay();
	if (Delta <= 0.0)
	{
		Delta += MINUTES_PER_DAY;
	}
	AdvanceClock(Delta, /*bForceSkyPush=*/true);
}

void UTimeOfDaySubsystem::Sleep()
{
	if (bSleeping)
		return; // already sweeping; ignore re-entrant interacts

	// Kick off the animated sweep — Tick advances the clock a slice at a time so the sky
	// sun/lighting interpolate across the 8 hours instead of snapping. Energy restore and
	// autosave are deferred to FinishSleep() so they land on the final time, not the start.
	const double TotalMinutes = SLEEP_DURATION_HOURS * MINUTES_PER_HOUR;
	SleepMinutesRemaining = TotalMinutes;
	SleepMinutesPerRealSecond = TotalMinutes / FMath::Max(SLEEP_TRANSITION_SECONDS, KINDA_SMALL_NUMBER);
	bSleeping = true;
}

void UTimeOfDaySubsystem::TickSleep(float DeltaTime)
{
	// Clamp the final slice so we land exactly on +8h rather than overshooting.
	double Step = SleepMinutesPerRealSecond * static_cast<double>(DeltaTime);
	if (Step >= SleepMinutesRemaining)
		Step = SleepMinutesRemaining;

	SleepMinutesRemaining -= Step;
	// Throttled push (bForceSkyPush=false) keeps the sky at the smooth 30fps cadence; the
	// per-hour boundaries (phase flip, day-roll, rent) still fire as each hour is crossed.
	AdvanceClock(Step);

	if (SleepMinutesRemaining <= 0.0)
		FinishSleep();
}

void UTimeOfDaySubsystem::FinishSleep()
{
	bSleeping = false;
	SleepMinutesRemaining = 0.0;
	SleepMinutesPerRealSecond = 0.0;

	PushTimeToSky(/*bForce=*/true); // snap the sky to the exact final time
	RestorePlayerEnergy();
	// TODO(§9.8 / Phase 9): charge the equipped phone to 100% as part of the sleep cycle.
	// TODO(§7.3): sleep does NOT reset hunger / effective-max decay — only eating does.
	Autosave();
}

void UTimeOfDaySubsystem::PushPause(FName Reason)
{
	PauseReasons.Add(Reason);
}

void UTimeOfDaySubsystem::PopPause(FName Reason)
{
	PauseReasons.Remove(Reason);
}

void UTimeOfDaySubsystem::AdvanceClock(double DeltaMinutes, bool bForceSkyPush)
{
	UGlobalSaveGameData* Save = GetSave();
	if (!Save || DeltaMinutes <= 0.0)
		return;

	const double Prev = Save->MinuteOfDay;
	double Next = Prev + DeltaMinutes;

	// Fire an hour boundary for every integer hour crossed (handles midnight wrap and
	// multi-hour skips). Each index is folded to 0–23; boundary 4 rolls the calendar.
	const int32 PrevHourIdx = FMath::FloorToInt(Prev / MINUTES_PER_HOUR);
	const int32 NextHourIdx = FMath::FloorToInt(Next / MINUTES_PER_HOUR);
	for (int32 HourIdx = PrevHourIdx + 1; HourIdx <= NextHourIdx; ++HourIdx)
	{
		HandleHourBoundary(((HourIdx % 24) + 24) % 24);
	}

	while (Next >= MINUTES_PER_DAY)
	{
		Next -= MINUTES_PER_DAY;
	}
	Save->MinuteOfDay = static_cast<float>(Next);

	PushTimeToSky(bForceSkyPush);
}

void UTimeOfDaySubsystem::HandleHourBoundary(int32 HourOfDay)
{
	OnHourChanged.Broadcast(HourOfDay);

	if (HourOfDay == DAY_ROLL_HOUR)
	{
		AdvanceCalendarDay();
	}

	if (HourOfDay == FMath::FloorToInt(DAY_START_HOUR))
	{
		SetPhase(EDayPhase::Day);
	}
	else if (HourOfDay == FMath::FloorToInt(NIGHT_START_HOUR))
	{
		SetPhase(EDayPhase::Night);
	}
}

void UTimeOfDaySubsystem::SetPhase(EDayPhase NewPhase)
{
	if (NewPhase == CurrentPhase)
		return;

	CurrentPhase = NewPhase;
	OnPhaseChanged.Broadcast(NewPhase);
}

void UTimeOfDaySubsystem::AdvanceCalendarDay()
{
	UGlobalSaveGameData* Save = GetSave();
	if (!Save)
		return;

	Save->DaysPassed++;
	OnDayChanged.Broadcast(Save->DaysPassed);

	DepositDailyFollowerIncome();

	if (Save->DaysPassed > 0 && (Save->DaysPassed % WEEK_LENGTH_DAYS) == 0)
	{
		ChargeWeeklyRent();
	}

	// §3.3: surviving to day 90 ends the campaign (win). Days advance one at a time, so
	// an exact-match fires this once. Endless mode never ends here.
	if (!Save->bEndlessMode && Save->DaysPassed == CAMPAIGN_LENGTH_DAYS)
	{
		OnCampaignEnded.Broadcast(ECampaignEndReason::CampaignComplete);
	}
}

void UTimeOfDaySubsystem::ChargeWeeklyRent()
{
	UGlobalSaveGameData* Save = GetSave();
	if (!Save || Save->bEndlessMode)
		return;

	if (Save->Money >= WEEKLY_RENT)
	{
		Save->Money -= WEEKLY_RENT;
		Save->LastRentChargeDay = Save->DaysPassed;
		Autosave();
	}
	else
	{
		// §3.3 / §22 #8: can't make rent → eviction → run over. No grace period.
		OnCampaignEnded.Broadcast(ECampaignEndReason::Evicted);
	}
}

void UTimeOfDaySubsystem::DepositDailyFollowerIncome()
{
	// §7.9 / §20 #25: passive follower income auto-deposits to the bank each day-roll.
	// TODO(Phase 8): once a follower-count attribute exists, deposit
	// FollowerCount * <daily per-follower rate> into Save->Money here. There is no
	// follower attribute yet, so this is intentionally a no-op (payout reads 0).
}

void UTimeOfDaySubsystem::PushTimeToSky(bool bForce)
{
	const UGlobalSaveGameData* Save = GetSave();
	if (!Save)
		return;

	// Throttle continuous updates to 30fps of real time. Discrete jumps (load / skips)
	// force a push so the sky snaps to the new time without waiting for the next frame.
	const double Now = FApp::GetCurrentTime();
	if (!bForce && LastSkyPushRealTime >= 0.0 && (Now - LastSkyPushRealTime) < SKY_PUSH_INTERVAL_SECONDS)
		return;
	LastSkyPushRealTime = Now;

	// Carry sub-minute precision so the sun moves smoothly between minute marks. The
	// fractional minute becomes seconds + frames (at the same 30fps cadence).
	const double MinuteOfDay = Save->MinuteOfDay;
	const int32 Hours = FMath::FloorToInt(MinuteOfDay / MINUTES_PER_HOUR);
	const int32 Minutes = FMath::FloorToInt(MinuteOfDay) % MINUTES_PER_HOUR;

	const double FracMinute = MinuteOfDay - FMath::FloorToDouble(MinuteOfDay);
	const double FracSeconds = FracMinute * 60.0;
	const int32 Seconds = FMath::FloorToInt(FracSeconds);
	const int32 Frames = FMath::FloorToInt((FracSeconds - Seconds) * 30.0);

	OnPushTimeToSky.Broadcast(FTimecode(Hours, Minutes, Seconds, Frames, false));
}

EDayPhase UTimeOfDaySubsystem::ComputePhase(float InMinuteOfDay)
{
	const float Hour = InMinuteOfDay / MINUTES_PER_HOUR;
	return (Hour >= DAY_START_HOUR && Hour < NIGHT_START_HOUR) ? EDayPhase::Day : EDayPhase::Night;
}

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

void UTimeOfDaySubsystem::RestorePlayerEnergy() const
{
	if (ANakedDesireCharacter* Player = Cast<ANakedDesireCharacter>(UGameplayStatics::GetPlayerCharacter(this, SLOT_PLAYER)))
	{
		if (Player->StatsManager)
		{
			Player->StatsManager->RestoreEnergy();
		}
	}
}

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