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

namespace KfChatDotNetBot.Services;

public class KasinoMines
{
    private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
    private IDatabase? _redisDb;
    private static ChatBot _kfChatBot = null!;
    public Dictionary<int, KasinoMinesGame> ActiveGames = new();
    private decimal HOUSE_EDGE = (decimal)0.98; //used to rig win rate, payout is 100% fair. with shop i plan to implement a sort of kasino shop profile holding the investments and buffs and tracking the gamblers current house edge
    public class KasinoMinesGame
    {
        public GamblerDbModel Creator { get; set; }
        public DateTimeOffset LastInteracted = DateTimeOffset.UtcNow;
        
        public char[][] MinesBoard;
        public decimal Wager { get; set; }
        public int Size { get; set; }
        public int Mines { get; set; }
        public List<(int r, int c)> BetsPlaced;
        public string? LastMessageId;
        public string LastMessageReference = "";
        

        public KasinoMinesGame(GamblerDbModel creator, decimal wager, int size, int mines)
        {
            Creator = creator;
            Size = size;
            Mines = mines;
            Wager = wager;
            MinesBoard = CreateBoard();
            BetsPlaced = [];
        }

        public async Task ResetMessage(SentMessageTrackerModel msg)
        {
            _logger.Info("Resetting message");
            // 0 is the default for int
            if (LastMessageId != null)
            {
                await _kfChatBot.KfClient.DeleteMessageAsync(LastMessageId);
            }

            LastMessageId = msg.ChatMessageUuid ?? throw new InvalidOperationException($"ChatMessageId was null for {msg.Reference}");
            LastMessageReference = msg.Reference;
        }

        public async Task RigBoard((int r, int c) coord) //moves one of the mines to a specified coordinate for house edge rigging
        {
            //find the first mine
            (int r, int c) originalMine = (11, 11);
            bool brek = false;
            for (int r = 0; r < Size; r++)
            {
                for (int c = 0; c < Size; c++)
                {
                    if (MinesBoard[r][ c] == 'M')
                    {
                        originalMine = (r, c);
                        brek = true;
                        break;
                    }
                }
                if (brek) break;
            }

            MinesBoard[coord.r][ coord.c] = 'M';
            if (originalMine.r == 11)
            {
                _logger.Error("Rigboard failed to find a mine somehow?");
                return;
            }
            MinesBoard[originalMine.r][ originalMine.c] = 'G';

        }
        public async Task Explode((int r, int c) mineLocation, SentMessageTrackerModel msg)
        {
            if (LastMessageId == null || LastMessageId != msg.ChatMessageUuid)
            {
                await ResetMessage(msg);
            }

            int frames = 16;
            string str;
            bool revealedSpace;
            int yellowWave = 1;
            int orangeWave = 2;
            int redWave = 3;
            int whiteWave = 0;
            for (int f = 0; f < frames; f++)
            {
                str = "";
                for (int r = 0; r < Size; r++)
                {
                    await Task.Delay(100);
                    
                    for (int c = 0; c < Size; c++)
                    {
                        revealedSpace = false;
                        foreach (var bet in BetsPlaced)
                        {
                            if (bet.r == r && bet.c == c) revealedSpace = true;
                        }
                        
                        if (mineLocation.r == r && mineLocation.c == c)
                        {
                            str += "💣";
                        }
                        else if (revealedSpace)
                        {
                            str += "💎";
                        }
                        else if (DistanceFromMine((r, c)).vertical == yellowWave || DistanceFromMine((r, c)).horizontal == yellowWave)
                        {
                            str += "🟨";
                        }
                        else if (DistanceFromMine((r, c)).vertical == orangeWave ||
                                 DistanceFromMine((r, c)).horizontal == orangeWave)
                        {
                            str += "🟧";
                        }
                        else if (DistanceFromMine((r, c)).vertical == redWave ||
                                 DistanceFromMine((r, c)).horizontal == redWave)
                        {
                            str += "🟥";
                        }
                        else if (DistanceFromMine((r, c)).vertical == whiteWave ||
                                 DistanceFromMine((r, c)).horizontal == whiteWave)
                        {
                            str += "⬜";
                        }
                        else
                        {
                            str += "⬜";
                        }
                    }
                    str += "[br]";
                }

                yellowWave++;
                orangeWave++;
                redWave++;
                whiteWave++;
                await Task.Delay(100);
                await _kfChatBot.KfClient.EditMessageAsync(LastMessageId!, $"{str}[br]{Creator.User.FormatUsername()}");
            }

            await Task.Delay(TimeSpan.FromSeconds(10));
            await _kfChatBot.KfClient.DeleteMessageAsync(msg.ChatMessageUuid!);
            LastMessageId = null;

            (int vertical, int horizontal) DistanceFromMine((int r, int c) coord)
            {
                return (Math.Abs(coord.r - mineLocation.r), Math.Abs(coord.c - mineLocation.c));
            }
        }
        
        public new string ToString()
        {
            string value = "";
            bool revealedSpace;
            for (int r = 0; r < Size; r++)
            {
                for (int c = 0; c < Size; c++)
                {
                    revealedSpace = false;
                    foreach (var bet in BetsPlaced)
                    {
                        if (bet.r == r && bet.c == c) revealedSpace = true;
                    }

                    if (!revealedSpace)
                    {
                        value += "⬜";
                    }
                    else if (MinesBoard[r][ c] == 'M') value += "💣";
                    else value += "💎";
                }

                value += "[br]";
            }

            value += $"{Creator.User.FormatUsername()} [ditto]!mines 1[/ditto] [ditto]!mines cashout[/ditto]";
            return value;
        }
        
        public char[][] CreateBoard()
        {
            char[][] board = new char[Size][];
            for (int i = 0; i < Size; i++) board[i] = new char[Size];
            
            List<(int r, int c)> minesCoords = new List<(int r, int c)>();
            (int r, int c) coord;
            int counter = 0;
            bool gems = !(Mines < (Size * Size)/2); //if there are more mines than gems, generate list of gem locations instead since thats less generations
            int coordsCounter;
            if (gems) coordsCounter = Size * Size - Mines;
            else coordsCounter = Mines;
            while (minesCoords.Count != coordsCounter)
            {
                coord = (Money.GetRandomNumber(Creator, 0, Size, incrementMaxParam: false), Money.GetRandomNumber(Creator, 0, Size, incrementMaxParam: false));
                if (!minesCoords.Contains(coord)) minesCoords.Add(coord);
                else counter++;
                if (counter >= 100000) throw new Exception($"mines failed to generate mines coordinates. Mines: {Mines} | Board size: {Size} | Current count of mines list {minesCoords.Count}");
            }

            foreach (var coords in minesCoords)
            {
                if (gems) board[coords.r][ coords.c] = 'G';
                else board[coords.r][ coords.c] = 'M';
            }
            for (int r = 0; r < Size; r++)
            {
                for (int c = 0; c < Size; c++)
                {
                    if (gems)
                    {
                        if (!(board[r][c] == 'G'))  board[r][ c] = 'M';
                    }
                    else
                    {
                        if (!(board[r][c] == 'M'))  board[r][ c] = 'G';
                    }
                }
            }

            return board;
        }

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

        _redisDb = Redis.Multiplexer.GetDatabase();
        GetSavedGames(gamblerId).Wait();
    }

    public async Task RefreshGameMessage(int gamblerId)
    {
        await GetSavedGames(gamblerId);
        var game = ActiveGames[gamblerId];
        game.LastInteracted = DateTimeOffset.UtcNow;
        var msg = await _kfChatBot.SendChatMessageAsync($"{game.ToString()}", true);
        await _kfChatBot.WaitForChatMessageAsync(msg);
        await game.ResetMessage(msg);
        ActiveGames[gamblerId] = game;
        await SaveActiveGames(gamblerId);
    }
    
    public async Task GetSavedGames(int gamblerId)
    {
        if (_redisDb == null) throw new InvalidOperationException("Kasino mines service isn't initialized");
        var json = await _redisDb.StringGetAsync($"Mines.State.{gamblerId}");
        if (string.IsNullOrEmpty(json)) return;
        try
        {
            var options = new JsonSerializerOptions{IncludeFields = true};
            ActiveGames = JsonSerializer.Deserialize<Dictionary<int, KasinoMinesGame>>(json.ToString(), options) ??
                          throw new InvalidOperationException();
        }
        catch (Exception e)
        {
            _logger.Error(e);
            _logger.Error("Potentially failed to deserialize active mines games in GetSavedGames() in KasinoMines in Services");
            ActiveGames = new Dictionary<int, KasinoMinesGame>();
        }
    }
    public async Task SaveActiveGames(int gamblerId)
    {
        if (_redisDb == null) throw new InvalidOperationException("Kasino mines service isn't initialized");
        var options = new JsonSerializerOptions
        {
            IncludeFields = true,
            WriteIndented = false
        };
        var json = JsonSerializer.Serialize(ActiveGames, options);
        await _redisDb.StringSetAsync($"Mines.State.{gamblerId}", json, null, When.Always);
    }

    public async Task RemoveGame(int gamblerId)
    {
        await GetSavedGames(gamblerId);
        //attempt to delete the message if its there
        var lastMsgId = ActiveGames[gamblerId].LastMessageId;
        if (lastMsgId != null)
        {
            _kfChatBot.ScheduleMessageAutoDelete(lastMsgId, TimeSpan.FromSeconds(15));
        }
        ActiveGames.Remove(gamblerId);
        await SaveActiveGames(gamblerId);
    }

    public async Task Cashout(KasinoMinesGame game)
    {
        decimal payout = game.Wager;
        decimal mines = game.Mines;
        decimal size2 = game.Size * game.Size;
        decimal gems = size2 - mines;
        for (int i = 0; i < game.BetsPlaced.Count; i++)
        {
            payout *= ((size2 - i) / (gems-i));
        }
        //edit the message with the board to show all the mine locations if you won 💣💎⬜
        string str = "";
        for (int r = 0; r < game.Size; r++)
        {
            for (int c = 0; c < game.Size; c++)
            {
                bool revealedSpace = false;
                foreach (var bet in game.BetsPlaced)
                {
                    if (bet.r == r && bet.c == c) revealedSpace = true;
                }
                if (game.MinesBoard[r][ c] == 'M') str += "💣";
                else if (revealedSpace) str += "💎";
                else str += "⬜";
                
            }
            str += "[br]";
        }
        await _kfChatBot.KfClient.EditMessageAsync(game.LastMessageId!, str);
        var net = payout - game.Wager;
        var newBalance = await Money.NewWagerAsync(game.Creator.Id, game.Wager, net, WagerGame.Mines);
        
        await _kfChatBot.SendChatMessageAsync(
            $"{game.Creator.User.FormatUsername()}, you won {await payout.FormatKasinoCurrencyAsync()} from your {await game.Wager.FormatKasinoCurrencyAsync()} bet on mines, collecting {game.BetsPlaced.Count} gems while avoiding {game.Mines} mines. Net: {await net.FormatKasinoCurrencyAsync()}. Balance: {await newBalance.FormatKasinoCurrencyAsync()}", true, autoDeleteAfter: TimeSpan.FromSeconds(15));
        await RemoveGame(game.Creator.Id);
    }
        
    public async Task<bool> Bet(GamblerDbModel gambler, int count, SentMessageTrackerModel msg, bool cashOut) //returns false if you hit a bomb, true if you didn't
    {
        int gamblerId = gambler.Id;
        await GetSavedGames(gamblerId);
        var game = ActiveGames[gamblerId];
        game.LastInteracted = DateTimeOffset.UtcNow;
        if (game.LastMessageId == null || game.LastMessageId != msg.ChatMessageUuid)
        {
            
            await game.ResetMessage(msg);
        }
        List<(int r, int c)> betCoords = new();
        List<(int r, int c)> validBets = new();
        int numGems = 0;
        
        //first get a list of valid coordinates that could be bet on (any unrevealed tile - gem or mine)
        for (int r = 0; r < game.Size; r++)
        {
            for (int c = 0; c < game.Size; c++)
            {
                if (!game.BetsPlaced.Contains((r, c)))
                {
                    validBets.Add((r, c));
                    if (game.MinesBoard[r][c] == 'G') numGems++;
                }
            }
        }

        if (validBets.Count == 0)
        {
            await _kfChatBot.SendChatMessageAsync($"{game.Creator.User.FormatUsername()}, unable to generate valid betting positions, game closed.", true, autoDeleteAfter: TimeSpan.FromSeconds(5));
            return false;
        }

        if (count > numGems)
        {
            count = numGems;
            await _kfChatBot.SendChatMessageAsync(
                $"{game.Creator.User.FormatUsername()}, there are only {numGems} gems left, so you bet on {count} gems, and will automatically cash out if you win.",
                true, autoDeleteAfter: TimeSpan.FromSeconds(5));
            cashOut = true;
        }
        else if (count == numGems && cashOut == false)
        {
            await _kfChatBot.SendChatMessageAsync($"{game.Creator.User.FormatUsername()}, you bet on all gems, so you will automatically cash out if you win.", true, autoDeleteAfter: TimeSpan.FromSeconds(5));
            cashOut = true;
        }

        
        //randomly pull from that list to add coordinates to bet on
        for (int i = 0; i < count; i++)
        {
            int rand = Money.GetRandomNumber(game.Creator, 0, validBets.Count - 1);
            betCoords.Add(validBets[rand]);
            validBets.RemoveAt(rand);
        }

        return await Bet(gambler, betCoords, msg, cashOut, true);
    }

    public async Task<bool> Bet(GamblerDbModel gambler, List<(int r, int c)> coords, SentMessageTrackerModel msg, bool cashOut, bool calledFromBet = false)
    {
        //KasinoShop stuff -------------------------------------------------------------------------
        if (_kfChatBot.BotServices.KasinoShop != null)
        {
            await GlobalShopFunctions.CheckProfile(_kfChatBot, gambler.User, gambler);
            HOUSE_EDGE += _kfChatBot.BotServices.KasinoShop.Gambler_Profiles[gambler.User.KfId].HouseEdgeModifier;
        }
        //------------------------------------------------------------------------------------------
        int gamblerId = gambler.Id;
        await GetSavedGames(gamblerId);
        var game = ActiveGames[gamblerId];
        game.LastInteracted = DateTimeOffset.UtcNow;
        if (game.LastMessageId == null || game.LastMessageId != msg.ChatMessageUuid)
        {
            await game.ResetMessage(msg);
        }

        List<(int r, int c)> bets = new();
        if (!calledFromBet)
        {
            List<(int r, int c)> validBets = new();
            int numGems = 0;
        
            //first get a list of valid coordinates that could be bet on (any unrevealed cell for user-provided coords)
            for (int r = 0; r < game.Size; r++)
            {
                for (int c = 0; c < game.Size; c++)
                {
                    if (!game.BetsPlaced.Contains((r, c)))
                    {
                        validBets.Add((r, c));
                        if (game.MinesBoard[r][c] == 'G') numGems++;
                    }
                }
            }

            var invalidBetMsg = await _kfChatBot.SendChatMessageAsync($"{game.Creator.User.FormatUsername()}, checking bets...", true);
            await _kfChatBot.WaitForChatMessageAsync(invalidBetMsg);
            foreach (var bet in coords)
            {
                if (!validBets.Contains(bet) || game.BetsPlaced.Contains(bet) || bets.Contains(bet))
                {
                    await _kfChatBot.KfClient.EditMessageAsync(invalidBetMsg.ChatMessageUuid!,
                        $"{game.Creator.User.FormatUsername()}, invalid bet of {bet.r},{bet.c} removed (already placed, duplicate, or invalid coordinate)");
                    await Task.Delay(5);
                }
                else bets.Add(bet);
            }

            
            if (bets.Count > numGems)
            {

                await _kfChatBot.KfClient.EditMessageAsync(invalidBetMsg.ChatMessageUuid!,
                    $"{game.Creator.User.FormatUsername()}, you bet on {bets.Count} gems, but there are only {numGems} left. Your list of bets was automatically truncated, and the game will automatically cash out if you win.");
                bets.RemoveRange(numGems, bets.Count - numGems);
                cashOut = true;
            }
            else if (bets.Count == numGems)
            {
                await _kfChatBot.KfClient.EditMessageAsync(invalidBetMsg.ChatMessageUuid!,
                    $"{game.Creator.User.FormatUsername()}, you bet on all gems, so you will automatically cash out if you win.");
                cashOut = true;
            }

            await Task.Delay(50);
            _ = _kfChatBot.KfClient.DeleteMessageAsync(invalidBetMsg.ChatMessageUuid!);

        }
        else bets = coords;
        foreach (var coord in bets) //the main portion of the game
        {
            var r = Money.GetRandomNumber(game.Creator, 0, 100);
            await Task.Delay(100);
            if (game.MinesBoard[coord.r][ coord.c] == 'M')
            {
                game.BetsPlaced.Add(coord);
                await _kfChatBot.KfClient.EditMessageAsync(msg.ChatMessageUuid!, game.ToString());
                _ = game.Explode((coord.r, coord.c), msg);
                var newBalance = await Money.NewWagerAsync(game.Creator.Id, game.Wager, -game.Wager, WagerGame.Mines);
                var net = -game.Wager;
                await _kfChatBot.SendChatMessageAsync(
                    $"{game.Creator.User.FormatUsername()}, you lost your {await game.Wager.FormatKasinoCurrencyAsync()} bet on mines, collecting {game.BetsPlaced.Count} gems until you hit one of {game.Mines} mines. Net: {await net.FormatKasinoCurrencyAsync()}. Balance: {await newBalance.FormatKasinoCurrencyAsync()}",
                    true, autoDeleteAfter: TimeSpan.FromSeconds(15));
                await RemoveGame(gamblerId);
                return false;
            }
            
            if (r > 100 * HOUSE_EDGE && game.BetsPlaced.Count == 0)//if you didn't lose, check to see if the switch was flipped (only on the first round)
            {
                game.BetsPlaced.Add(coord);
                await _kfChatBot.KfClient.EditMessageAsync(msg.ChatMessageUuid!, game.ToString());   
                await game.RigBoard(coord);
                await Task.Delay(75);
                await _kfChatBot.KfClient.EditMessageAsync(msg.ChatMessageUuid!, game.ToString());
                _ = game.Explode(coord, msg);
                var newBalance = await Money.NewWagerAsync(game.Creator.Id, game.Wager, -game.Wager, WagerGame.Mines);
                var net = -game.Wager;
                await _kfChatBot.SendChatMessageAsync(
                    $"R! {game.Creator.User.FormatUsername()}, you lost your {await game.Wager.FormatKasinoCurrencyAsync()} bet on mines, collecting {game.BetsPlaced.Count} gems until you hit one of {game.Mines} mines. Net: {await net.FormatKasinoCurrencyAsync()}. Balance: {await newBalance.FormatKasinoCurrencyAsync()}",
                    true, autoDeleteAfter: TimeSpan.FromSeconds(15));
                await RemoveGame(gamblerId);
                return false;
            }
            else
            {
                game.BetsPlaced.Add(coord);
            }
            await _kfChatBot.KfClient.EditMessageAsync(msg.ChatMessageUuid!, game.ToString());
        }


        ActiveGames[gamblerId] = game;
        if (cashOut) await Cashout(game);
        else await SaveActiveGames(gamblerId);
        return true;
    }

    public async Task CreateGame(GamblerDbModel gambler, decimal bet, int size, int mines)
    {
        await GetSavedGames(gambler.Id);
        ActiveGames.Add(gambler.Id, new KasinoMinesGame(gambler, bet, size, mines));
        await SaveActiveGames(gambler.Id);
    }

}



