using System;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.Packets;
using NitroxServer.Helper;
using NitroxServer.GameLogic.Unlockables;
using NitroxModel.Helper;
using NitroxModel;

namespace NitroxServer.GameLogic;

/// <summary>
/// Keeps track of time and Aurora-related events.
/// </summary>
public class StoryManager : IDisposable
{
    private readonly PlayerManager playerManager;
    private readonly PDAStateData pdaStateData;
    private readonly StoryGoalData storyGoalData;
    private readonly TimeKeeper timeKeeper;
    private readonly string seed;

    /// <summary>
    /// Time at which the Aurora explosion countdown will start (last warning is sent).
    /// </summary>
    /// <remarks>
    /// It is required to calculate the time at which the Aurora warnings will be sent (along with <see cref="AuroraWarningTimeMs"/>, look into AuroraWarnings.cs and CrashedShipExploder.cs for more information).
    /// </remarks>
    public double AuroraCountdownTimeMs;
    /// <summary>
    /// Time at which the Aurora Events start (you start receiving warnings).
    /// </summary>
    public double AuroraWarningTimeMs;

    /// <summary>
    /// In seconds
    /// </summary>
    public double AuroraRealExplosionTime;

    private double ElapsedMilliseconds => timeKeeper.ElapsedMilliseconds;
    private double ElapsedSeconds => timeKeeper.ElapsedSeconds;

    public StoryManager(PlayerManager playerManager, PDAStateData pdaStateData, StoryGoalData storyGoalData, TimeKeeper timeKeeper, string seed, double? auroraExplosionTime, double? auroraWarningTime, double? auroraRealExplosionTime)
    {
        this.playerManager = playerManager;
        this.pdaStateData = pdaStateData;
        this.storyGoalData = storyGoalData;
        this.timeKeeper = timeKeeper;
        this.seed = seed;
        
        AuroraCountdownTimeMs = auroraExplosionTime ?? GenerateDeterministicAuroraTime(seed);
        AuroraWarningTimeMs = auroraWarningTime ?? ElapsedMilliseconds;
        // +27 is from CrashedShipExploder.IsExploded, -480 is from the default time (see TimeKeeper)
        AuroraRealExplosionTime = auroraRealExplosionTime ?? AuroraCountdownTimeMs * 0.001 + 27 - TimeKeeper.DEFAULT_TIME;

        timeKeeper.TimeSkipped += ReadjustAuroraRealExplosionTime;
    }

    public void ReadjustAuroraRealExplosionTime(double skipAmount)
    {
        // Readjust the aurora real explosion time when time skipping because it's based on in-game time
        if (AuroraRealExplosionTime > timeKeeper.RealTimeElapsed)
        {
            double newTime = timeKeeper.RealTimeElapsed + skipAmount;
            if (newTime > AuroraRealExplosionTime)
            {
                AuroraRealExplosionTime = timeKeeper.RealTimeElapsed;
            }
            else
            {
                AuroraRealExplosionTime -= skipAmount;
            }
        }
    }

    /// <param name="instantaneous">Whether we should make Aurora explode instantly or after a short countdown</param>
    public void BroadcastExplodeAurora(bool instantaneous)
    {
        // Calculations from CrashedShipExploder.OnConsoleCommand_countdownship()
        // We add 3 seconds to the cooldown (Subnautica adds only 1) so that players have enough time to receive the packet and process it
        AuroraCountdownTimeMs = ElapsedMilliseconds + 3000;
        AuroraWarningTimeMs = AuroraCountdownTimeMs;
        AuroraRealExplosionTime = timeKeeper.RealTimeElapsed + 30; // 27 + 3

        if (instantaneous)
        {
            // Calculations from CrashedShipExploder.OnConsoleCommand_explodeship()
            // Removes 25 seconds to the countdown time, jumping to the exact moment of the explosion
            AuroraCountdownTimeMs -= 25000;
            // Is 1 second less than countdown time to have the game understand that we only want the explosion.
            AuroraWarningTimeMs = AuroraCountdownTimeMs - 1000;
            AuroraRealExplosionTime -= 25;
            Log.Info("Aurora's explosion initiated");
        }
        else
        {
            Log.Info("Aurora's explosion countdown will start in 3 seconds");
        }

        playerManager.SendPacketToAllPlayers(new AuroraAndTimeUpdate(GetTimeData(), false));
    }

    public void BroadcastRestoreAurora()
    {
        AuroraWarningTimeMs = ElapsedMilliseconds;
        AuroraCountdownTimeMs = GenerateDeterministicAuroraTime(seed);
        // Current time + deltaTime before countdown + 27 seconds before explosion
        AuroraRealExplosionTime = timeKeeper.RealTimeElapsed + (AuroraCountdownTimeMs - timeKeeper.ElapsedMilliseconds) * 0.001 + 27;

        // We need to clear these entries from PdaLog and CompletedGoals to make sure that the client, when reconnecting, doesn't have false information
        foreach (string eventKey in AuroraEventData.GoalNames)
        {
            pdaStateData.PdaLog.RemoveAll(entry => entry.Key == eventKey);
            storyGoalData.CompletedGoals.Remove(eventKey);
        }

        playerManager.SendPacketToAllPlayers(new AuroraAndTimeUpdate(GetTimeData(), true));
        Log.Info($"Restored Aurora, will explode again in {GetMinutesBeforeAuroraExplosion()} minutes");
    }

    /// <summary>
    /// Calculate the time at Aurora's explosion countdown will begin.
    /// </summary>
    /// <remarks>
    /// Takes the current time into account.
    /// </remarks>
    private double GenerateDeterministicAuroraTime(string seed)
    {
        // Copied from CrashedShipExploder.SetExplodeTime() and changed from seconds to ms
        DeterministicGenerator generator = new(seed, nameof(StoryManager));
        return ElapsedMilliseconds + generator.NextDouble(2.3d, 4d) * 1200d * 1000d;
    }

    /// <summary>
    /// Clears the already completed sunbeam events to come and broadcasts it to all players along with the rescheduling of the specified sunbeam event.
    /// </summary>
    public void StartSunbeamEvent(string sunbeamEventKey)
    {
        int beginIndex = PlaySunbeamEvent.SunbeamGoals.GetIndex(sunbeamEventKey);
        if (beginIndex == -1)
        {
            Log.Error($"Couldn't find the corresponding sunbeam event in {nameof(PlaySunbeamEvent.SunbeamGoals)} for key {sunbeamEventKey}");
            return;
        }
        for (int i = beginIndex; i < PlaySunbeamEvent.SunbeamGoals.Length; i++)
        {
            storyGoalData.CompletedGoals.Remove(PlaySunbeamEvent.SunbeamGoals[i]);
        }
        playerManager.SendPacketToAllPlayers(new PlaySunbeamEvent(sunbeamEventKey));
    }

    /// <returns>Either the time in before Aurora explodes or -1 if it has already exploded.</returns>
    private double GetMinutesBeforeAuroraExplosion()
    {
        return AuroraCountdownTimeMs > ElapsedMilliseconds ? Math.Round((AuroraCountdownTimeMs - ElapsedMilliseconds) / 60000) : -1;
    }

    /// <summary>
    /// Makes a nice status of the Aurora events progress for the summary command.
    /// </summary>
    public string GetAuroraStateSummary()
    {
        double minutesBeforeExplosion = GetMinutesBeforeAuroraExplosion();
        if (minutesBeforeExplosion < 0)
        {
            return "already exploded";
        }
        // Based on AuroraWarnings.Update calculations
        // auroraWarningNumber is the amount of received Aurora warnings (there are 4 in total)
        int auroraWarningNumber = 0;
        if (ElapsedMilliseconds >= AuroraCountdownTimeMs)
        {
            auroraWarningNumber = 4;
        }
        else if (ElapsedMilliseconds >= Mathf.Lerp((float)AuroraWarningTimeMs, (float)AuroraCountdownTimeMs, 0.8f))
        {
            auroraWarningNumber = 3;
        }
        else if (ElapsedMilliseconds >= Mathf.Lerp((float)AuroraWarningTimeMs, (float)AuroraCountdownTimeMs, 0.5f))
        {
            auroraWarningNumber = 2;
        }
        else if (ElapsedMilliseconds >= Mathf.Lerp((float)AuroraWarningTimeMs, (float)AuroraCountdownTimeMs, 0.2f))
        {
            auroraWarningNumber = 1;
        }
        
        return $"explodes in {minutesBeforeExplosion} minutes [{auroraWarningNumber}/4]";
    }

    public AuroraEventData MakeAuroraData()
    {
        return new((float)AuroraCountdownTimeMs * 0.001f, (float)AuroraWarningTimeMs * 0.001f, (float)AuroraRealExplosionTime);
    }

    public TimeData GetTimeData()
    {
        return new(timeKeeper.MakeTimePacket(), MakeAuroraData());
    }

    public void Dispose()
    {
        timeKeeper.TimeSkipped -= ReadjustAuroraRealExplosionTime;
        GC.SuppressFinalize(this);
    }

    public enum TimeModification
    {
        DAY, NIGHT, SKIP
    }
}
