#nullable disable
using EllieBot.Modules.Gambling.Common.AnimalRacing.Exceptions;
using EllieBot.Modules.Games.Common;

namespace EllieBot.Modules.Gambling.Common.AnimalRacing;

public sealed class AnimalRace : IDisposable
{
    public const double BASE_MULTIPLIER = 0.87;
    public const double MAX_MULTIPLIER = 0.94;
    public const double MULTI_PER_USER = 0.005;

    public enum Phase
    {
        WaitingForPlayers,
        Running,
        Ended
    }

    public event Func<AnimalRace, Task> OnStarted = delegate { return Task.CompletedTask; };
    public event Func<AnimalRace, Task> OnStartingFailed = delegate { return Task.CompletedTask; };
    public event Func<AnimalRace, Task> OnStateUpdate = delegate { return Task.CompletedTask; };
    public event Func<AnimalRace, Task> OnEnded = delegate { return Task.CompletedTask; };

    public Phase CurrentPhase { get; private set; } = Phase.WaitingForPlayers;

    public IReadOnlyCollection<AnimalRacingUser> Users
        => _users.ToList();

    public List<AnimalRacingUser> FinishedUsers { get; } = new();
    public int MaxUsers { get; }

    private readonly SemaphoreSlim _locker = new(1, 1);
    private readonly HashSet<AnimalRacingUser> _users = new();
    private readonly ICurrencyService _currency;
    private readonly RaceOptions _options;
    private readonly Queue<RaceAnimal> _animalsQueue;

    public AnimalRace(RaceOptions options, ICurrencyService currency, IEnumerable<RaceAnimal> availableAnimals)
    {
        _currency = currency;
        _options = options;
        _animalsQueue = new(availableAnimals);
        MaxUsers = _animalsQueue.Count;

        if (_animalsQueue.Count == 0)
            CurrentPhase = Phase.Ended;
    }

    public void Initialize() //lame name
        => _ = Task.Run(async () =>
        {
            await Task.Delay(_options.StartTime * 1000);

            await _locker.WaitAsync();
            try
            {
                if (CurrentPhase != Phase.WaitingForPlayers)
                    return;

                await Start();
            }
            finally { _locker.Release(); }
        });

    public async Task<AnimalRacingUser> JoinRace(ulong userId, string userName, long bet = 0)
    {
        ArgumentOutOfRangeException.ThrowIfNegative(bet);

        var user = new AnimalRacingUser(userName, userId, bet);

        await _locker.WaitAsync();
        try
        {
            if (_users.Count == MaxUsers)
                throw new AnimalRaceFullException();

            if (CurrentPhase != Phase.WaitingForPlayers)
                throw new AlreadyStartedException();

            if (!await _currency.RemoveAsync(userId, bet, new("animalrace", "bet")))
                throw new NotEnoughFundsException();

            if (_users.Contains(user))
                throw new AlreadyJoinedException();

            var animal = _animalsQueue.Dequeue();
            user.Animal = animal;
            _users.Add(user);

            if (_animalsQueue.Count == 0) //start if no more spots left
                await Start();

            return user;
        }
        finally { _locker.Release(); }
    }

    private async Task Start()
    {
        CurrentPhase = Phase.Running;
        if (_users.Count <= 1)
        {
            foreach (var user in _users)
            {
                if (user.Bet > 0)
                    await _currency.AddAsync(user.UserId, (long)(user.Bet + BASE_MULTIPLIER), new("animalrace", "refund"));
            }

            _ = OnStartingFailed?.Invoke(this);
            CurrentPhase = Phase.Ended;
            return;
        }

        _ = OnStarted?.Invoke(this);
        _ = Task.Run(async () =>
        {
            var rng = new EllieRandom();
            while (!_users.All(x => x.Progress >= 60))
            {
                foreach (var user in _users)
                {
                    user.Progress += rng.Next(1, 10);
                    if (user.Progress >= 60)
                        user.Progress = 60;
                }

                var finished = _users.Where(x => x.Progress >= 60 && !FinishedUsers.Contains(x)).Shuffle();

                FinishedUsers.AddRange(finished);

                _ = OnStateUpdate?.Invoke(this);
                await Task.Delay(1750);
            }

            if (FinishedUsers[0].Bet > 0)
            {
                Multi = FinishedUsers.Count
                        * Math.Min(MAX_MULTIPLIER, BASE_MULTIPLIER + (MULTI_PER_USER * FinishedUsers.Count));
                await _currency.AddAsync(FinishedUsers[0].UserId,
                    (long)(FinishedUsers[0].Bet * Multi),
                    new("animalrace", "win"));
            }

            _ = OnEnded?.Invoke(this);
        });
    }

    public double Multi { get; set; } = BASE_MULTIPLIER;

    public void Dispose()
    {
        CurrentPhase = Phase.Ended;
        OnStarted = null;
        OnEnded = null;
        OnStartingFailed = null;
        OnStateUpdate = null;
        _locker.Dispose();
        _users.Clear();
    }
}