using System.Text.Json;
using KfChatDotNetBot.Extensions;
using KfChatDotNetBot.Models.DbModels;
using KfChatDotNetBot.Settings;
using NLog;
using StackExchange.Redis;
using RandN;
using RandN.Compat;

namespace KfChatDotNetBot.Services;

public class KasinoKrash : IDisposable
{
    private readonly Logger _logger = LogManager.GetCurrentClassLogger();
    private Task? _krashTimerTask;
    private IDatabase? _redisDb;
    private ChatBot _kfChatBot;
    private CancellationToken _ct;
    public KasinoKrashModel? TheGame;
    public static decimal HOUSE_EDGE = (decimal)0.98;
    
    public KasinoKrash(ChatBot kfChatBot, CancellationToken ct = default) //the service itself
    {
        _kfChatBot = kfChatBot;
        _ct = ct;
        if (!Redis.IsAvailable)
        {
            _logger.Error($"Can't initialize the Kasino Krash service as Redis isn't configured in {BuiltIn.Keys.BotRedisConnectionString} " +
                          $"or the Redis service failed to connect");
            return;
        }

        _redisDb = Redis.Multiplexer.GetDatabase();
        //attempt to pull a game from the db in case the bot crashed while a game was ongoing. if so it will restart the run 
        TheGame = GetKrashState().Result;
        if (TheGame != null) _ = RunGame();
    }
    public bool IsInitialized()
    {
        return _redisDb != null;
    }
    public async Task<KasinoKrashModel?> GetKrashState()
    {
        if (_redisDb == null) throw new InvalidOperationException("Kasino Krash service isn't initialized");
        var json = await _redisDb.StringGetAsync("Krash.State");
        if (string.IsNullOrEmpty(json)) return null;
        var data = JsonSerializer.Deserialize<KasinoKrashModel>(json.ToString());
        return data;
    }
    public async Task RemoveKrashState()
    {
        if (_redisDb == null) throw new InvalidOperationException("Kasino krash service isn't initialized");
        await _redisDb.KeyDeleteAsync("Krash.State");
        TheGame = null;
    }
    public async Task SaveKrashState(KasinoKrashModel krash)
    {
        if (_redisDb == null) throw new InvalidOperationException("Kasino Krash service isn't initialized");
        var json = JsonSerializer.Serialize(krash);
        await _redisDb.StringSetAsync("Krash.State", json, null, When.Always);
    }

    public async Task AttemptKrash(GamblerDbModel gambler)
    {
        if (TheGame == null)
        {
            throw new InvalidOperationException("Failed to retrieve state or no krash is in progress");
        }
        if (TheGame.Bets.All(x => x.Gambler.User.KfId != gambler.User.KfId)) return;
        if (!TheGame.KrashAccepted) return;
        
        //find which bet is yours
        var index = TheGame.Bets.TakeWhile(bet => bet.Gambler.User.KfId != gambler.User.KfId).Count();
        
        var krashBet = TheGame.Bets[index];
        TheGame.Bets.RemoveAt(index);
        var multi = TheGame.CurrentMulti;
        var payout = multi * krashBet.Wager - krashBet.Wager;
        var newBalance = await Money.NewWagerAsync(krashBet.Gambler.Id, krashBet.Wager, payout, WagerGame.Krash, ct: _ct);
        var green = await SettingsProvider.GetValueAsync(BuiltIn.Keys.KiwiFarmsGreenColor);
        await _kfChatBot.SendChatMessageAsync(
            $"{krashBet.Gambler.User.FormatUsername()}, you [color={green.Value}][b]won[/b][/color] {await payout.FormatKasinoCurrencyAsync()} by cashing out the krash at {multi}x!",
            true, autoDeleteAfter: TimeSpan.FromSeconds(10));
        if (_kfChatBot.BotServices.KasinoShop != null)
        {
            await _kfChatBot.BotServices.KasinoShop.ProcessWagerTracking(krashBet.Gambler, WagerGame.Krash, krashBet.Wager,
                payout, newBalance);
        }
        await SaveKrashState(TheGame);
    }
    
    public async Task AddParticipant(GamblerDbModel gambler, decimal wager, decimal multi = -1)
    {
        if (TheGame == null)
        {
            TheGame = await GetKrashState();
            if (TheGame == null) throw new InvalidOperationException("Failed to retrieve state or no krash is in progress");
            _ = RunGame();
        }
        if (!TheGame.BetsAccepted) return;
        var bet = new KrashBet { Gambler = gambler, Wager = wager, Multi = multi };
        TheGame.Bets.Add(bet);
        if (_kfChatBot.BotServices.KasinoShop != null)
        {
            HOUSE_EDGE = _kfChatBot.BotServices.KasinoShop.DefaultHouseEdgeModifier - _kfChatBot.BotServices.KasinoShop.Gambler_Profiles[gambler.User.KfId].HouseEdgeModifier;
        }
        await SaveKrashState(TheGame);
    }

    public async Task StartGame(GamblerDbModel creator, decimal wager, decimal multi = -1)
    {
        TheGame = new KasinoKrashModel(creator);
        TheGame.Bets.Add(new KrashBet{Gambler = creator, Wager = wager, Multi = multi});
        await SaveKrashState(TheGame);
        if (_kfChatBot.BotServices.KasinoShop != null)
        {
            HOUSE_EDGE = (HOUSE_EDGE + _kfChatBot.BotServices.KasinoShop.DefaultHouseEdgeModifier - _kfChatBot.BotServices.KasinoShop.Gambler_Profiles[creator.User.KfId].HouseEdgeModifier)/2;
        }
        _ = RunGame();
        
    }
    public async Task RunGame() //running the actual game
    {
        if (TheGame == null)
        {
            await RemoveKrashState();
            await _kfChatBot.SendChatMessageAsync("Krash error 1", true);
            return;
        }
        var msg = await _kfChatBot.SendChatMessageAsync(
            $"{TheGame.Creator.User.FormatUsername()} started a Krash! You have 30 seconds to place your bets. [ditto]!krash[/ditto] <amount> <optional multi>", true);
        var preGameTimer = TimeSpan.FromSeconds(30);
        var interval = TimeSpan.FromSeconds(1);
        var timer = new PeriodicTimer(interval);
        await _kfChatBot.WaitForChatMessageAsync(msg, ct: _ct);
        
        while (await timer.WaitForNextTickAsync(_ct)) //timer before starting the game
        {
            var bets = "";
            foreach (var bet in TheGame.Bets)
            {
                bets += $"{bet.Gambler.User.FormatUsername()} is betting {await bet.Wager.FormatKasinoCurrencyAsync()}";
                if (bet.Multi != -1) bets += $" on {bet.Multi}x!";
                else bets += " on freehand!";
                bets += "[br]";
            }
            await _kfChatBot.KfClient.EditMessageAsync(msg.ChatMessageUuid!,
                $"{TheGame.Creator.User.FormatUsername()} started a Krash! You have [b]{preGameTimer}[/b] to place your bets. [ditto]!krash[/ditto] <amount> <optional multi>[br]{bets}");
            preGameTimer -= interval;
            if (preGameTimer <= TimeSpan.Zero)
            {
                break;
            }
        }
        
        //any bets placed after this point will be cancelled, must wait until the last game finishes to start a new one.
        TheGame.BetsAccepted = false;
        TheGame.KrashAccepted = true;
        await SaveKrashState(TheGame);
        
        //start the display of the game
        
        //change these to change the speed of the game
        var growthRate = 1.02m;
        var growthAcceleration = 1.00185m;
        await _kfChatBot.KfClient.DeleteMessageAsync(msg.ChatMessageUuid!);
        var green = (await SettingsProvider.GetValueAsync(BuiltIn.Keys.KiwiFarmsGreenColor)).Value;
        var red = (await SettingsProvider.GetValueAsync(BuiltIn.Keys.KiwiFarmsRedColor)).Value;
        msg = await _kfChatBot.SendChatMessageAsync($"[center][b][size=200][color=limegreen]{Math.Truncate(TheGame.CurrentMulti*100)/100.0m}x");
        var defaultGrowth = 0.01m;
        interval = TimeSpan.FromSeconds(0.1);
        timer = new PeriodicTimer(interval);
        while (await timer.WaitForNextTickAsync(_ct))
        {
            TheGame.KrashAccepted = true;
            await _kfChatBot.KfClient.EditMessageAsync(msg.ChatMessageUuid!, $"[center][b][size=200][color={green}]{Math.Truncate(TheGame.CurrentMulti*100)/100}x");
            TheGame.CurrentMulti += defaultGrowth;
            defaultGrowth *= growthRate;
            growthRate *= growthAcceleration;
            if (TheGame.CurrentMulti >= TheGame.FinalMulti) break;
        }
        //at this point the game crashes and everybody who did not cash out or pre bet on a multi will have balance subtracted, winners will be paid out.
        await _kfChatBot.KfClient.EditMessageAsync(msg.ChatMessageUuid!, $"[center][b][size=200][color={red}]{TheGame.FinalMulti}x");
        TheGame.KrashAccepted = false;
        while (TheGame.Bets.Count >= 1)
        {
            var bet = TheGame.Bets[0];
            var freshBalance = (await Money.GetGamblerEntityAsync(bet.Gambler.User.Id, ct: _ct))!.Balance;
            if (freshBalance < bet.Wager)
            {
                if (freshBalance < 1)
                {
                    await _kfChatBot.SendChatMessageAsync(
                        $"{bet.Gambler.User.FormatUsername()}, bet nullified due to lack of balance.",
                        true, autoDeleteAfter: TimeSpan.FromSeconds(10));
                }
                else
                {
                    //scale the bet
                    var ratio = freshBalance / bet.Wager;
                    bet.Wager *= ratio;
                    await _kfChatBot.SendChatMessageAsync(
                        $"{bet.Gambler.User.FormatUsername()}, due to your poor gambling skills, your bet was scaled down to {await bet.Wager.FormatKasinoCurrencyAsync()} to match your remaining balance.",
                        true, autoDeleteAfter: TimeSpan.FromSeconds(10));
                }
            }
            else if (bet.Multi <= TheGame.FinalMulti && bet.Multi != -1)
            {
                //you win
                var payout = bet.Multi * bet.Wager - bet.Wager;
                var newBalance = await Money.NewWagerAsync(bet.Gambler.Id, bet.Wager, payout, WagerGame.Krash, ct: _ct);
                await _kfChatBot.SendChatMessageAsync(
                    $"{bet.Gambler.User.FormatUsername()}, you [color={green}][b]won[/b][/color] {await payout.FormatKasinoCurrencyAsync()}!",
                    true, autoDeleteAfter: TimeSpan.FromSeconds(10));
                if (_kfChatBot.BotServices.KasinoShop != null)
                {
                    await _kfChatBot.BotServices.KasinoShop.ProcessWagerTracking(bet.Gambler, WagerGame.Krash, bet.Wager,
                        payout, newBalance);
                }
            }
            else
            {
                //automatically lose, no pre entered multi or it was greater than the final multi and failed to cash out
                var newBalance = await Money.NewWagerAsync(bet.Gambler.Id, bet.Wager, -bet.Wager, WagerGame.Krash, ct: _ct);
                await _kfChatBot.SendChatMessageAsync(
                    $"{bet.Gambler.User.FormatUsername()}, you [color={red}][b]lost[/b][/color] {await bet.Wager.FormatKasinoCurrencyAsync()}!",
                    true, autoDeleteAfter: TimeSpan.FromSeconds(10));
                if (_kfChatBot.BotServices.KasinoShop != null)
                {
                    await _kfChatBot.BotServices.KasinoShop.ProcessWagerTracking(bet.Gambler, WagerGame.Krash, bet.Wager,
                        -bet.Wager, newBalance);
                }
            }
            TheGame.Bets.RemoveAt(0);
        }

        HOUSE_EDGE = 0.98m;
        //now close the game
        await Task.Delay(10000, _ct);
        await _kfChatBot.KfClient.DeleteMessageAsync(msg.ChatMessageUuid!);
        await RemoveKrashState();
    }

    
    
    public class KasinoKrashModel
    {
        public GamblerDbModel Creator;
        public decimal FinalMulti = 0;
        public decimal CurrentMulti = 1.01m;
        public List<KrashBet> Bets = new();
        public decimal HouseEdge = 0.98m;
        public bool BetsAccepted = true;
        public bool KrashAccepted = false;
        public KasinoKrashModel(GamblerDbModel creator)
        {
            this.Creator = creator;
            FinalMulti = GetLinearWeightedRandom(1.01, 25000);
        }
        
        private decimal GetLinearWeightedRandom(double minValue, double maxValue)
        {
            var random = RandomShim.Create(StandardRng.Create());
            var r = random.NextDouble(); // Returns 0.0 to 1.0

            // The core 1/x logic
            var result = 1.0 / (1.0 - r);
            result *= (double)HOUSE_EDGE;
            // Clamp the result to your specific range
            if (result < minValue) result = minValue;
            if (result > maxValue) result = maxValue;

            return (decimal)result;
        }
    }

    public class KrashBet
    {
        public required GamblerDbModel Gambler{ get; set;}
        public required decimal Wager { get; set; }
        public required decimal Multi { get; set; }
        
    }
    public void Dispose()
    {
        _krashTimerTask?.Dispose();
        GC.SuppressFinalize(this);
    }
}

