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

#pragma once

#include "CoreMinimal.h"
#include "Subsystems/WorldSubsystem.h"
#include "TimeOfDaySubsystem.generated.h"

class UGlobalSaveGameData;

/**
 * Day vs. night phase (GDD §10.1). Drives NPC density and embarrassment rate.
 * Day is 08:00–20:00; everything else is night.
 */
UENUM(BlueprintType)
enum class EDayPhase : uint8
{
	Day,
	Night
};

/**
 * Why the 90-day campaign ended (GDD §3.3). Evicted is the rent-failure loss;
 * CampaignComplete fires when the player survives to day 90. The ending screen /
 * win-threshold logic (§21 open) lives in BP and reacts to OnCampaignEnded.
 */
UENUM(BlueprintType)
enum class ECampaignEndReason : uint8
{
	Evicted,
	CampaignComplete
};

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnHourChangedSignature, int32, Hour);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnDayChangedSignature, int32, NewDay);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnPhaseChangedSignature, EDayPhase, NewPhase);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCampaignEndedSignature, ECampaignEndReason, Reason);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnPushTimeToSkySignature, const FTimecode&, Timecode);

/**
 * The single authoritative clock (GDD §2.4, §10.1). Owns time-of-day and the
 * calendar in C++ and pushes the current time to the UltraDynamicSky actor at 30fps
 * (SKY_PUSH_INTERVAL_SECONDS) via ANakedDesireGameMode::SetCurrentTime — inverting the
 * old BP-drives-time flow. Persists to UGlobalSaveGameData (MinuteOfDay / DaysPassed).
 *
 * The calendar rolls at 04:00 (DAY_ROLL_HOUR); the day phase flips at 08:00 / 20:00.
 * Weekly rent is charged every WEEK_LENGTH_DAYS-th roll; follower income deposits
 * each roll. Sleep / time-skips funnel through AdvanceClock so boundaries always fire.
 */
UCLASS()
class NAKEDDESIRE_API UTimeOfDaySubsystem : public UTickableWorldSubsystem
{
	GENERATED_BODY()

public:
	virtual void OnWorldBeginPlay(UWorld& InWorld) override;

	// FTickableGameObject
	virtual void Tick(float DeltaTime) override;
	virtual TStatId GetStatId() const override;
	virtual bool IsTickable() const override { return bBegunPlay && !IsTemplate(); }

	// --- Queries ---
	UFUNCTION(BlueprintPure, Category = "Time")
	int32 GetDay() const;

	UFUNCTION(BlueprintPure, Category = "Time")
	float GetMinuteOfDay() const;

	UFUNCTION(BlueprintPure, Category = "Time")
	int32 GetHour() const;

	UFUNCTION(BlueprintPure, Category = "Time")
	int32 GetMinute() const;

	UFUNCTION(BlueprintPure, Category = "Time")
	EDayPhase GetPhase() const;

	UFUNCTION(BlueprintPure, Category = "Time")
	bool IsDay() const { return GetPhase() == EDayPhase::Day; }

	// --- Time control ---
	// Advance the clock by a number of in-game minutes, firing every boundary crossed.
	UFUNCTION(BlueprintCallable, Category = "Time")
	void SkipTime(float Minutes);

	// Fast-forward to the next 08:00 (used by the §4.4 holding-cell cutscene).
	UFUNCTION(BlueprintCallable, Category = "Time")
	void SkipToNextMorning();

	// §2.4 sleep: fast-forward 8 hours, restore energy, autosave. (The apartment bed
	// also routes outside-clothing loss through USessionLossResolver — see §4.4.)
	// The skip is animated over SLEEP_TRANSITION_SECONDS — the clock advances a little
	// each frame so the sky sweeps smoothly; energy/autosave land when the sweep finishes.
	UFUNCTION(BlueprintCallable, Category = "Time")
	void Sleep();

	// True while the sleep time-lapse is running (BP can hold a dim overlay / lock input).
	UFUNCTION(BlueprintPure, Category = "Time")
	bool IsSleeping() const { return bSleeping; }

	// --- Pause (reason-keyed; the clock runs only while the reason set is empty).
	// Note §11.17: the holding-cell cutscene deliberately does NOT pause the clock. ---
	UFUNCTION(BlueprintCallable, Category = "Time")
	void PushPause(FName Reason);

	UFUNCTION(BlueprintCallable, Category = "Time")
	void PopPause(FName Reason);

	UFUNCTION(BlueprintPure, Category = "Time")
	bool IsPaused() const { return PauseReasons.Num() > 0; }

	// --- Delegates ---
	UPROPERTY(BlueprintAssignable, Category = "Time")
	FOnHourChangedSignature OnHourChanged;

	UPROPERTY(BlueprintAssignable, Category = "Time")
	FOnDayChangedSignature OnDayChanged;

	UPROPERTY(BlueprintAssignable, Category = "Time")
	FOnPhaseChangedSignature OnPhaseChanged;

	UPROPERTY(BlueprintAssignable, Category = "Time")
	FOnCampaignEndedSignature OnCampaignEnded;
	
	UPROPERTY(BlueprintAssignable, Category = "Time")
	FOnPushTimeToSkySignature OnPushTimeToSky;

private:
	// bForceSkyPush bypasses the 30fps throttle for discrete jumps (skips / load),
	// so the sky snaps to the new time immediately instead of waiting a frame.
	void AdvanceClock(double DeltaMinutes, bool bForceSkyPush = false);
	// Per-frame driver for the animated sleep sweep; finalizes when the budget is spent.
	void TickSleep(float DeltaTime);
	void FinishSleep();
	void HandleHourBoundary(int32 HourOfDay); // 0–23
	void SetPhase(EDayPhase NewPhase);
	void AdvanceCalendarDay();
	void ChargeWeeklyRent();
	void DepositDailyFollowerIncome();
	void PushTimeToSky(bool bForce = false);

	static EDayPhase ComputePhase(float InMinuteOfDay);

	UGlobalSaveGameData* GetSave() const;
	void RestorePlayerEnergy() const;
	void Autosave() const;

	// Real-time stamp (seconds, FApp clock) of the last sky push, to throttle to 30fps.
	double LastSkyPushRealTime = -1.0;
	EDayPhase CurrentPhase = EDayPhase::Day;
	TSet<FName> PauseReasons;
	bool bBegunPlay = false;

	// Animated sleep state. While bSleeping, Tick drives the clock at SleepMinutesPerRealSecond
	// instead of the normal rate, advancing SleepMinutesRemaining in-game minutes total.
	bool bSleeping = false;
	double SleepMinutesRemaining = 0.0;
	double SleepMinutesPerRealSecond = 0.0;
};