using System.Text.Json;
using KfChatDotNetBot.Commands.Kasino;
using KfChatDotNetBot.Extensions;
using KfChatDotNetBot.Models.DbModels;
using KfChatDotNetBot.Settings;
using Microsoft.EntityFrameworkCore;
using NLog;
using StackExchange.Redis;

namespace KfChatDotNetBot.Services;

public class KasinoRain : IDisposable
{
    private readonly Logger _logger = LogManager.GetCurrentClassLogger();
    private Task? _rainTimerTask;
    private IDatabase? _redisDb;
    private ChatBot _kfChatBot;
    private CancellationToken _ct;
    private CancellationTokenSource _rainCts = new();

    public KasinoRain(ChatBot kfChatBot, CancellationToken ct = default)
    {
        _kfChatBot = kfChatBot;
        _ct = ct;
        if (!Redis.IsAvailable)
        {
            _logger.Error($"Can't initialize the Kasino Rain service as Redis isn't configured in {BuiltIn.Keys.BotRedisConnectionString} " +
                          $"or the Redis service failed to connect");
            return;
        }

        _redisDb = Redis.Multiplexer.GetDatabase();
        _rainTimerTask = Task.Run(RainTimerTask, ct);
    }

    public bool IsInitialized()
    {
        return _redisDb != null;
    }

    public async Task AddParticipant(int userId)
    {
        var data = await GetRainState();
        if (data == null) throw new InvalidOperationException("Failed to retrieve state or no rain is in progress");
        if (data.Participants.Contains(userId)) return;
        data.Participants.Add(userId);
        await SaveRainState(data);
    }

    public async Task RemoveRainState()
    {
        if (_redisDb == null) throw new InvalidOperationException("Kasino Rain service isn't initialized");
        await _redisDb.KeyDeleteAsync("Rain.State");
    }

    public async Task<KasinoRainModel?> GetRainState()
    {
        if (_redisDb == null) throw new InvalidOperationException("Kasino Rain service isn't initialized");
        var json = await _redisDb.StringGetAsync("Rain.State");
        if (string.IsNullOrEmpty(json)) return null;
        var data = JsonSerializer.Deserialize<KasinoRainModel>(json.ToString());
        return data;
    }

    public async Task SaveRainState(KasinoRainModel rain)
    {
        if (_redisDb == null) throw new InvalidOperationException("Kasino Rain service isn't initialized");
        var json = JsonSerializer.Serialize(rain);
        await _redisDb.StringSetAsync("Rain.State", json, null, When.Always);
    }

    private async Task RainTimerTask()
    {
        var interval = TimeSpan.FromSeconds(1);
        using var timer = new PeriodicTimer(interval);
        while (await timer.WaitForNextTickAsync(_ct))
        {
            var rain = await GetRainState();
            if (rain == null) continue;
            if (DateTimeOffset.UtcNow < rain.PayoutWhen) continue;
            var creator = await Money.GetGamblerEntityAsync(rain.Creator, ct: _ct);
            if (creator == null)
            {
                _logger.Error($"Somehow this rain was created by a non-existent (or banned) gambler with user ID {rain.Creator}? Wiping this fucked up state");
                await RemoveRainState();
                continue;
            }
            
            if (rain.Participants.Count == 0)
            {
                await _kfChatBot.SendChatMessageAsync(
                    $"Nobody participated in {creator.User.FormatUsername()}'s rain!",
                    true, autoDeleteAfter: TimeSpan.FromSeconds(30));
                await RemoveRainState();
                continue;
            }

            if (rain.RainAmount > creator.Balance)
            {
                await _kfChatBot.SendChatMessageAsync(
                    $"{creator.User.FormatUsername()} lost it all before he could bless everyone! The giveaway is canceled! :lossmanjack:",
                    true, autoDeleteAfter: TimeSpan.FromSeconds(30));
                await RemoveRainState();
                continue;
            }
            
            List<string> participantNames = [];
            var payout = rain.RainAmount / rain.Participants.Count;
            decimal failedPayoutAmount = 0;
            foreach (var participant in rain.Participants)
            {
                var gambler = await Money.GetGamblerEntityAsync(participant, ct: _ct);
                if (gambler == null)
                {
                    _logger.Error($"Somehow this participant ({participant}) does not have a gambler entity or has been banned");
                    failedPayoutAmount += payout;
                    continue;
                }
                participantNames.Add(gambler.User.FormatUsername());
                await Money.ModifyBalanceAsync(gambler.Id, payout, TransactionSourceEventType.Rain,
                    "Payout from rain event", creator.Id, _ct);
                //KasinoShop stuff -------------------------------------------------------------------------
                if (_kfChatBot.BotServices.KasinoShop != null)
                {
                    await GlobalShopFunctions.CheckProfile(_kfChatBot, gambler.User, gambler);
                    await using var db = new ApplicationDbContext();
                    var creatorGambler = await db.Gamblers.FirstOrDefaultAsync(x => x.Id == creator.Id, _ct);
                    await _kfChatBot.BotServices.KasinoShop.ProcessJuicerOrRainTracking(creatorGambler!, gambler,
                        payout);
                }
                //-----------------------------------------------------------------------------------------
            }

            await Money.ModifyBalanceAsync(creator.Id, -rain.RainAmount + failedPayoutAmount, TransactionSourceEventType.Rain,
                $"Rained on {participantNames.Count} people", ct: _ct);
            await _kfChatBot.SendChatMessageAsync(
                $"{creator.User.FormatUsername()} made it rain {await payout.FormatKasinoCurrencyAsync()} on {string.Join(' ', participantNames)}",
                true, autoDeleteAfter: TimeSpan.FromSeconds(30));
            await RemoveRainState();
        }
    }
    public void Dispose()
    {
        _rainTimerTask?.Dispose();
        GC.SuppressFinalize(this);
    }
}

public class KasinoRainModel
{
    public required List<int> Participants { get; set; } = [];
    public required int Creator { get; set; }
    public required DateTimeOffset Started { get; set; }
    public required decimal RainAmount { get; set; }
    public required DateTimeOffset PayoutWhen { get; set; }
}
