using System.Text.RegularExpressions;
using KfChatDotNetBot.Extensions;
using KfChatDotNetBot.Models;
using KfChatDotNetBot.Models.DbModels;
using KfChatDotNetBot.Services;
using KfChatDotNetBot.Settings;
using KfChatDotNetWsClient.Models.Events;

namespace KfChatDotNetBot.Commands.Kasino;

[KasinoCommand]
[WagerCommand]
public class LambchopCommand : ICommand
{
    public List<Regex> Patterns =>
    [
        new Regex(@"lambchop (?<amount>\d+)$", RegexOptions.IgnoreCase),
        new Regex(@"lambchop (?<amount>\d+\.\d+)$", RegexOptions.IgnoreCase),
        new Regex(@"lambchop (?<amount>\d+) (?<targetTile>\d+)$", RegexOptions.IgnoreCase),
        new Regex(@"lambchop (?<amount>\d+\.\d+) (?<targetTile>\d+)$", RegexOptions.IgnoreCase)
    ];

    public string? HelpText =>
        "Tread treacherous terrain towards terrific treasures. Play using !lambchop bet, amount of tiles you want to move";
    public UserRight RequiredRight => UserRight.Loser;
    public TimeSpan Timeout => TimeSpan.FromSeconds(12);
    public RateLimitOptionsModel? RateLimitOptions => new()
    {
        MaxInvocations = 3,
        Window = TimeSpan.FromSeconds(15)
    };
    public bool WhisperCanInvoke => false;
    private static double _houseEdge = 0.015; // house edge hack?
    
    // game assets
    private const string HAIRSPACE = " ";
    private const string SHEEP = "🐑";
    private const string YELLOW_TILE = "🟡";
    private const string PURPLE_TILE = "🟣";
    private const string GREEN_TILE = "🟢";
    private const string RED_TILE = "🔴";
    private const string FORREST_TILE = "🌳";
    private const string DESERT_TILE = "🏜️";
    private const string WOLF = "🐺";
    private const string ALIEN = "🛸";
    private const string LIGHTNING = "⚡";
    private const string BLOOD = HAIRSPACE + "🩸" + HAIRSPACE;
    private const string SKULL = "☠";
    private const string MEDAL = "🏅";
    private const string MONEYBAG = "💰";
    private const string CELEBRATION = "🏆🪩✨";
    private const string CASTLE = "🏯";
    private const string WOOSH = "💨";
    private const string FIST = HAIRSPACE + "✊" + HAIRSPACE;
    private const string TILE_SPACING = "[color=#36393f]......[/color]";
    private const string HAZARD_SPACING = "[color=#36393f].......[/color]";
    // game settings
    private const int FRAME_DELAY = 200;    // time between lambchop frames in milliseconds
    private const int FIELD_LENGTH = 16;    // indicates how many tiles the lamb can cross. default is 16
                                            // WARNING: do NOT change without first implementing dynamic payout logic in LambchopPayoutMultiplier() 
                                            // has to be an EVEN number > 1
    public async Task RunCommand(ChatBot botInstance, BotCommandMessageModel message, UserDbModel user, GroupCollection arguments,
        CancellationToken ctx)
    {
        var settings = await SettingsProvider.GetMultipleValuesAsync([
            BuiltIn.Keys.KasinoGameDisabledMessageCleanupDelay, BuiltIn.Keys.KasinoLambchopCleanupDelay,
            BuiltIn.Keys.KasinoLambchopEnabled
        ]);
        if (message is { IsWhisper: false, MessageUuid: not null })
        {
            await botInstance.KfClient.DeleteMessageAsync(message.MessageUuid);
        }
        // Check if lambchop is enabled
        var lambchopEnabled = (settings[BuiltIn.Keys.KasinoLambchopEnabled]).ToBoolean();
        if (!lambchopEnabled)
        {
            var gameDisabledCleanupDelay= TimeSpan.FromMilliseconds(settings[BuiltIn.Keys.KasinoGameDisabledMessageCleanupDelay].ToType<int>());
            await botInstance.SendChatMessageAsync(
                $"{user.FormatUsername()}, lambchop is currently disabled.", 
                true, autoDeleteAfter: gameDisabledCleanupDelay);
            return;
        }

        // await botInstance.SendChatMessageAsync($"{user.FormatUsername()}, fuck you", true);
        // return;
        
        var cleanupDelay = TimeSpan.FromMilliseconds(settings[BuiltIn.Keys.KasinoLambchopCleanupDelay].ToType<int>());
        
        if (!arguments.TryGetValue("amount", out var amount))
        {
            await botInstance.SendChatMessageAsync($"{user.FormatUsername()}, not enough arguments. !lambchop <wager> <number between 1 and {FIELD_LENGTH}>", true, autoDeleteAfter: cleanupDelay);
            RateLimitService.RemoveMostRecentEntry(user, this);
            return;
        }
        var targetTile = arguments["targetTile"].Success ? Convert.ToInt32(arguments["targetTile"].Value) : FIELD_LENGTH;
        if (targetTile is < 1 or > FIELD_LENGTH)
        {
            await botInstance.SendChatMessageAsync($"{user.FormatUsername()},  Please choose a target tile between 1 and {FIELD_LENGTH}", true, autoDeleteAfter: cleanupDelay);
            RateLimitService.RemoveMostRecentEntry(user, this);
            return;
        }
        var wager = Convert.ToDecimal(amount.Value);
        var gambler = await Money.GetGamblerEntityAsync(user.Id, ct: ctx);
        if (gambler == null)
            throw new InvalidOperationException($"Caught a null when retrieving gambler for {user.KfUsername}");
        if (gambler.Balance < wager)
        {
            await botInstance.SendChatMessageAsync(
                $"{user.FormatUsername()}, your balance of {await gambler.Balance.FormatKasinoCurrencyAsync()} isn't enough for this wager.",
                true, autoDeleteAfter: cleanupDelay);
            RateLimitService.RemoveMostRecentEntry(user, this);
            return;
        }
        
        if (wager == 0)
        {
            await botInstance.SendChatMessageAsync(
                $"{user.FormatUsername()}, you have to wager more than {await wager.FormatKasinoCurrencyAsync()}", true,
                autoDeleteAfter: cleanupDelay);
            RateLimitService.RemoveMostRecentEntry(user, this);
            return;
        }
        
        var colors =
            await SettingsProvider.GetMultipleValuesAsync([
                BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor
            ]);
       
        List<string> tiles = Enumerable.Repeat(YELLOW_TILE, FIELD_LENGTH / 2).ToList();
        tiles.AddRange(Enumerable.Repeat(PURPLE_TILE, FIELD_LENGTH / 2));
        List<string> hazards = Enumerable.Repeat(FORREST_TILE, FIELD_LENGTH / 2).ToList();
        hazards.AddRange(Enumerable.Repeat(DESERT_TILE, FIELD_LENGTH / 2));
        
        // calculate death tile, death tile = -1 means no death tile
        int deathTile = CalculateDeathTile(targetTile, gambler);
        bool win;
        int steps;
        if (deathTile == -1) // no death tile on field
        {
            win = true; // if there's is no deathTile then automatic win!
            steps = targetTile - 1;
        }
        else
        {
            win = (targetTile - 1) < deathTile; // if your targetTile is less then the death tile then you win!
            steps = win ? targetTile - 1 : deathTile;
        }
        // first game state
        var lambChopDisplayMessage =
            await botInstance.SendChatMessageAsync(ConvertLambchopFieldToString(tiles, hazards, true), true,
                autoDeleteAfter: cleanupDelay);
        while (lambChopDisplayMessage.ChatMessageUuid == null)
        {
            await Task.Delay(50, ctx); // wait until first message is fully sent
            if (lambChopDisplayMessage.Status is SentMessageTrackerStatus.Lost or SentMessageTrackerStatus.NotSending)
                return;
        }

        for (int i = -1; i <= steps;)       // main game loop, if/else "state machine"
        {
            if (i == -1)
            {
                // first state, print empty tileset and sheep placeholder
                await Task.Delay(TimeSpan.FromMilliseconds(FRAME_DELAY), ctx);
                tiles = MoveSheep(tiles); // move the sheep onto the first tile
                i++; // increase step counter by 1
                continue;
            }
            // boundary check for sheep movement
            if (i >= tiles.Count)
            {
                break; // exit if we've gone past the field
            }
            // normal "move" state

            // let alien follow player
            if (i > FIELD_LENGTH / 2 - 1)
            {
                hazards[i] = ALIEN; // alien follows you in later part of the map
                if (i > 0 && hazards[i - 1] == ALIEN)
                {
                    // update previous hazard tile back to desert
                    hazards[i - 1] = DESERT_TILE;
                }
               
            }
            if (i == deathTile) // trigger hazard death?
            {
                // player dies on this step
                if (i > FIELD_LENGTH / 2 - 1)
                {
                    // death by alien
                    await UpdateGameAsync();
                    tiles[i] = LIGHTNING;   // strike player with lightning
                    await UpdateGameAsync();
                    tiles[i] = SKULL;   // skull
                    await UpdateGameAsync();
                    break;
                    // i++;
                    //continue;
                }

                // death by wolf
                await UpdateGameAsync();
                hazards[i] = WOLF;  // add wolf
                await UpdateGameAsync();
                tiles[i] = BLOOD;   // blood
                await UpdateGameAsync();
                tiles[i] = SKULL;   // skull
                await UpdateGameAsync();
                break;
                //i++;
                //continue;
            }
            if (i == (targetTile - 1) && win) // trigger win animation
            {
                await UpdateGameAsync();                //arrive at targetTile
                if (targetTile == FIELD_LENGTH)
                {
                    // mega win, end of the line
                    string lambChopFieldEndState = ConvertLambchopFieldToString(tiles, hazards, false);
                    lambChopFieldEndState = lambChopFieldEndState.Replace(SHEEP, GREEN_TILE);
                    lambChopFieldEndState += SHEEP;
                    await UpdateGameAsync(lambChopFieldEndState);
                    lambChopFieldEndState += CELEBRATION;
                    await UpdateGameAsync(lambChopFieldEndState);
                    break;
                    //i++;
                    //continue;
                }
                if (i > FIELD_LENGTH / 2 - 1)
                {
                    // win in the tundra, moneybags
                    hazards[i] = MONEYBAG; // add moneybag
                    if (deathTile >= 0 && deathTile < tiles.Count)
                    {
                        tiles[deathTile] = RED_TILE; // add deathTile indicator
                    }
                    await UpdateGameAsync();
                    break;
                    //i++;
                    //continue;
                }

                // win in the forrest, medal
                hazards[i] = MEDAL; // add medal
                if (deathTile != -1 && deathTile < tiles.Count)
                {
                    tiles[deathTile] = RED_TILE; // add deathTile indicator
                }
                await UpdateGameAsync();
                break;
                //i++;
                //continue;
            }
            if (Money.GetRandomDouble(gambler) <= 0.15)
            {
                //fakeouts
                // forrest or desert
                if (i > FIELD_LENGTH / 2 - 1)
                {
                    // desert fakeout
                    await UpdateGameAsync();
                    tiles[i] = LIGHTNING;   // strike player with lightning
                    string leftTile = tiles[i - 1];
                    tiles[i - 1] = WOOSH;   // add woosh fakeout
                    await UpdateGameAsync();
                    tiles[i - 1] = leftTile; // return left tile to normal
                    tiles[i] = SHEEP;   // change back to sheep
                }
                else
                {
                    // forrest fakeout
                    await UpdateGameAsync();
                    string forrestTile = hazards[i];
                    hazards[i] = WOLF;  // add wolf
                    await UpdateGameAsync();
                    tiles[i] = FIST;    // add fist
                    await UpdateGameAsync();
                    hazards[i] = forrestTile;
                    tiles[i] = SHEEP;   // change back to sheep
                }
            }
            await UpdateGameAsync();
            tiles = MoveSheep(tiles);
            i++;
        }
        
        // payout logic
        string lambchopResultMessage;
        decimal newBalance;
        if (win)
        {
            var multi = LambchopPayoutMultiplier(targetTile);
            var lambchopPayout = Math.Round(wager * multi - wager, 2);
            newBalance = await Money.NewWagerAsync(gambler.Id, wager, lambchopPayout, WagerGame.LambChop, ct: ctx);
            lambchopResultMessage = $"{user.FormatUsername()}, you [B][COLOR={colors[BuiltIn.Keys.KiwiFarmsGreenColor].Value}]WON[/COLOR][/B]" +
                                    $" | Multi {multi} | Balance {await newBalance.FormatKasinoCurrencyAsync()}";
        }
        else
        {
            newBalance = await Money.NewWagerAsync(gambler.Id, wager, -wager, WagerGame.LambChop, ct: ctx);
            lambchopResultMessage = $"{user.FormatUsername()}, you [B][COLOR={colors[BuiltIn.Keys.KiwiFarmsRedColor].Value}]LOST[/COLOR][/B]" +
                                    $", better luck next time | Balance {await newBalance.FormatKasinoCurrencyAsync()}";
            
        }
        await botInstance.SendChatMessageAsync(lambchopResultMessage, true, autoDeleteAfter: cleanupDelay);
        return;
        
        // hacky local helper function to quickly print the current state of the game field and trigger the frame delay
        async Task UpdateGameAsync(string? updateText = null)
        {
            updateText ??= ConvertLambchopFieldToString(tiles, hazards, false);
            await botInstance.KfClient.EditMessageAsync(lambChopDisplayMessage.ChatMessageUuid, updateText);
            await Task.Delay(TimeSpan.FromMilliseconds(FRAME_DELAY), ctx);
        }
    }

    // return -1 if player can proceed trough entire field
    private static int CalculateDeathTile(int targetTile, GamblerDbModel gambler)
    {
        // CHECK: does player want to move all tiles?
        if (targetTile == FIELD_LENGTH)
        {
            // PLAYER WANTS TO MOVE ALL TILES
            // normal success chance
            double successChance = 1.0 / (FIELD_LENGTH + 1); // +1 because "winning" means you dont die on the last tile
            if (_houseEdge > 0)
            {
                // Decrease success chance based on houseEdge (linearly)
                successChance *= (1.0 - _houseEdge);
            }
            // Determine if player can walk all tiles
            if (Money.GetRandomDouble(gambler) <= successChance)
            {
                return -1; // No death tile (player succeeds)
            }
            
            // Player fails - calculate where the death tile appears
            double riggingFactor = Money.GetRandomDouble(gambler);
            if (_houseEdge > 0 && riggingFactor < _houseEdge * 2) // shitty hack because I made the decision to clamp houseEdge to max 50%
            {
                // More rigging means death tile is more likely near the end
                int minDeathTile = Math.Max(0, FIELD_LENGTH - 3);
                return Money.GetRandomNumber(gambler, minDeathTile, FIELD_LENGTH, incrementMaxParam:false); // return 15 means dying on the last tile xd
            }
            else
            {
                // Player fail, random tile in the path becomes death tile
                return Money.GetRandomNumber(gambler,0, FIELD_LENGTH, incrementMaxParam:false);
            }
        }

        // Tiles 1 - 15
        if (_houseEdge < 0.015)
        {
            int deathTile = Money.GetRandomNumber(gambler,-1, FIELD_LENGTH, incrementMaxParam:false); // can be any tile, including no tile! (result -1 to FIELD_LENGTH (-1 - 15))
            return deathTile;
        }

        // game is rigged, manipulate tile placement
        int fairDeathTile = Money.GetRandomNumber(gambler,-1, FIELD_LENGTH, incrementMaxParam:false);
        fairDeathTile = fairDeathTile == -1 ? FIELD_LENGTH + 1 : fairDeathTile; // shit hack, -1 means no death tile, change it to FIELD_LENGTH + 1 to compensate for next check.
        bool wouldSucceedFairly = fairDeathTile > targetTile;
        fairDeathTile = fairDeathTile == FIELD_LENGTH + 1 ? -1 : fairDeathTile;
        if (wouldSucceedFairly)
        {
            // are we gonna rig it
            double riggedFailChance = _houseEdge * 2;
            if (Money.GetRandomDouble(gambler) <= riggedFailChance)
            {
                double cruelnessLevel = Money.GetRandomDouble(gambler);
                if (cruelnessLevel < _houseEdge * 2)
                {
                    // extra rigged fail, choose tile just before target tile
                    return targetTile > 1 ? targetTile - 1 : 1;
                }
                else
                {
                    // rigging failed, normal tile return
                    return Money.GetRandomNumber(gambler,-1, targetTile, incrementMaxParam:false);
                }

            }
            return fairDeathTile;
        }

        {
            // Player would fail in fair game
            double riggingFactor = Money.GetRandomDouble(gambler);
            if (riggingFactor < _houseEdge)
            {
                // Place death tile closer to target
                // higher house edge = more likely to place closer
                int minTile = Math.Max(0, targetTile - 3);
                return Money.GetRandomNumber(gambler,minTile, targetTile, incrementMaxParam:false);
            }
            return fairDeathTile;
        }
    }

    private static string ConvertLambchopFieldToString(List<string> tiles, List<string> hazards, bool first)
    {
        // This function takes the current state of the lambchop field and transforms it into a print ready string.
        // Its very hacky as it uses weird hairspaces to evenly space out some of the game elements for aesthetic reasons.
        // The game is optimized to display best on windows machines running a mostly default webbrowser.
        // This comes at the aesthetic expense of other platforms using different sets of emoji.
        // In case this is the first game state (bool first) print the sheep emoji in front of the tiles as to indicate
        // that the game is about to start, this prevents the game starting on a fail state on tile 0 which would look silly.
        string lambchopFieldState = "";
        int hazardSplitIndex = hazards.Count / 2; // first half of the field uses forrest emoji which need to be alternated with hairspaces for good spacing.
        string forrestHazards = string.Join(HAIRSPACE, hazards.GetRange(0, hazardSplitIndex)); // alternate forrest emoji and hairspaces
        string desertHazards = string.Concat(hazards.GetRange(hazardSplitIndex, hazards.Count - hazardSplitIndex)); // add desert emojis without spacing
        lambchopFieldState += HAZARD_SPACING + forrestHazards + desertHazards + "\n"; // glue it all together with the tiles
        lambchopFieldState += first ? SHEEP : TILE_SPACING; // first state uses sheep in front of tiles, every other state uses custom spacer string.
        lambchopFieldState += string.Join("", tiles);
        lambchopFieldState += CASTLE;
        return lambchopFieldState;
    }

    private static List<string> MoveSheep(List<string> tiles)
    {
        int index = tiles.IndexOf(SHEEP);
        if (index == -1)
        {
            // no sheep on tiles? Second game state, move sheep to first tile.
            tiles.RemoveAt(0);
            tiles.Insert(0, SHEEP);
        }
        else
        {
            if (index < tiles.Count - 1)
            {
                //tiles[index] = index < tiles.Count / 2 ? yellow_tile : purple_tile;
                tiles[index] = GREEN_TILE;
                tiles[index + 1] = SHEEP;
            }
            // sheep is already at end position
        }
        return tiles;
    }

    private static decimal LambchopPayoutMultiplier(int targetTile)
    {
        targetTile -= 1; // make it 0 indexed xd
        List<double> lambChopMultis =
        [
            1.062, 1.138, 1.228, 1.318, 1.426, 1.561, 1.714, 1.912, 2.142,
            2.442, 2.871, 3.425, 4.272, 5.702, 8.539, 16.861
        ];
        if (FIELD_LENGTH != lambChopMultis.Count)
        {
            throw new InvalidOperationException("FIELD_LENGTH doesn't match lambChopMultis array size. " +
                                                "Update the multees for the new field length");
        }
        return (decimal)lambChopMultis[targetTile];
    }

}
