#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using EllieBot.Common.ModuleBehaviors;
using EllieBot.Db.Models;
using EllieBot.Modules.Games.Quests;
using SixLabors.Fonts;
using SixLabors.Fonts.Unicode;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using Color = SixLabors.ImageSharp.Color;
using Image = SixLabors.ImageSharp.Image;

namespace EllieBot.Modules.Gambling.Services;

public class PlantPickService(
    DbService db,
    IBotStrings strings,
    IImageCache images,
    FontProvider fonts,
    ICurrencyService cs,
    CommandHandler cmdHandler,
    DiscordSocketClient client,
    GamblingConfigService gss,
    GamblingService gs,
    QuestService quests) : IEService, IExecNoCommand, IReadyExecutor
{
    //channelId/last generation
    public ConcurrentDictionary<ulong, long> LastGenerations { get; } = new();
    private ConcurrentHashSet<ulong> _generationChannels = [];

    public Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg)
        => PotentialFlowerGeneration(msg);

    private string GetText(ulong gid, LocStr str)
        => strings.GetText(str, gid);

    public async Task<bool> ToggleCurrencyGeneration(ulong gid, ulong cid)
    {
        bool enabled;
        await using var uow = db.GetDbContext();

        if (_generationChannels.Add(cid))
        {
            await uow.GetTable<GCChannelId>()
                .InsertOrUpdateAsync(() => new()
                    {
                        ChannelId = cid,
                        GuildId = gid
                    },
                    (x) => new()
                    {
                        ChannelId = cid,
                        GuildId = gid
                    },
                    () => new()
                    {
                        ChannelId = cid,
                        GuildId = gid
                    });

            _generationChannels.Add(cid);
            enabled = true;
        }
        else
        {
            await uow.GetTable<GCChannelId>()
                .Where(x => x.ChannelId == cid && x.GuildId == gid)
                .DeleteAsync();

            _generationChannels.TryRemove(cid);
            enabled = false;
        }

        return enabled;
    }

    public async Task<IReadOnlyCollection<GCChannelId>> GetAllGeneratingChannels()
    {
        await using var uow = db.GetDbContext();
        return await uow.GetTable<GCChannelId>()
            .ToListAsyncLinqToDB();
    }

    /// <summary>
    ///     Get a random currency image stream, with an optional password sticked onto it.
    /// </summary>
    /// <param name="pass">Optional password to add to top left corner.</param>
    /// <returns>Stream of the currency image</returns>
    public async Task<(Stream, string)> GetRandomCurrencyImageAsync(string pass)
    {
        var curImg = await images.GetCurrencyImageAsync();

        if (curImg is null)
            return (new MemoryStream(), null);

        if (string.IsNullOrWhiteSpace(pass))
        {
            // determine the extension
            using var load = Image.Load(curImg);

            var format = load.Metadata.DecodedImageFormat;
            // return the image
            return (curImg.ToStream(), format?.FileExtensions.FirstOrDefault() ?? "png");
        }

        // get the image stream and extension
        return AddPassword(curImg, pass);
    }

    /// <summary>
    ///     Add a password to the image.
    /// </summary>
    /// <param name="curImg">Image to add password to.</param>
    /// <param name="pass">Password to add to top left corner.</param>
    /// <returns>Image with the password in the top left corner.</returns>
    private (Stream, string) AddPassword(byte[] curImg, string pass)
    {
        // draw lower, it looks better
        pass = pass.TrimTo(10, true).ToLowerInvariant();
        using var img = Image.Load<Rgba32>(curImg);
        // choose font size based on the image height, so that it's visible
        var font = fonts.NotoSans.CreateFont(img.Height / 11.0f, FontStyle.Bold);
        img.Mutate(x =>
        {
            // measure the size of the text to be drawing
            var size = TextMeasurer.MeasureSize(pass,
                new RichTextOptions(font)
                {
                    Origin = new PointF(0, 0)
                });

            // fill the background with black, add 5 pixels on each side to make it look better
            x.FillPolygon(Color.ParseHex("00000080"),
                new PointF(1, 1),
                new PointF(size.Width + 5, 0),
                new PointF(size.Width + 5, size.Height + 10),
                new PointF(0, size.Height + 10));

            var strikeoutRun = new RichTextRun
            {
                Start = 0,
                End = pass.GetGraphemeCount(),
                Font = font,
                StrikeoutPen = new SolidPen(Color.White, 2),
                TextDecorations = TextDecorations.Strikeout
            };

            // draw the password over the background
            x.DrawText(new RichTextOptions(font)
                {
                    Origin = new(0, 0),
                    TextRuns =
                    [
                        strikeoutRun
                    ]
                },
                pass,
                new SolidBrush(Color.White));
        });
        // return image as a stream for easy sending
        var format = img.Metadata.DecodedImageFormat;
        return (img.ToStream(format), format?.FileExtensions.FirstOrDefault() ?? "png");
    }

    private Task PotentialFlowerGeneration(IUserMessage imsg)
    {
        if (imsg is not SocketUserMessage msg || msg.Author.IsBot)
            return Task.CompletedTask;

        if (imsg.Channel is not ITextChannel channel)
            return Task.CompletedTask;

        if (!_generationChannels.Contains(channel.Id))
            return Task.CompletedTask;

        _ = Task.Run(async () =>
        {
            try
            {
                var config = gss.Data;
                var lastGeneration = LastGenerations.GetOrAdd(channel.Id, DateTime.MinValue.ToBinary());
                var rng = new EllieRandom();

                if (DateTime.UtcNow - TimeSpan.FromSeconds(config.Generation.GenCooldown)
                    < DateTime.FromBinary(lastGeneration)) //recently generated in this channel, don't generate again
                    return;

                var num = rng.Next(1, 101) + (config.Generation.Chance * 100);
                if (num > 100 && LastGenerations.TryUpdate(channel.Id, DateTime.UtcNow.ToBinary(), lastGeneration))
                {
                    var dropAmount = config.Generation.MinAmount;
                    var dropAmountMax = config.Generation.MaxAmount;

                    if (dropAmountMax > dropAmount)
                        dropAmount = new EllieRandom().Next(dropAmount, dropAmountMax + 1);

                    if (dropAmount > 0)
                    {
                        var prefix = cmdHandler.GetPrefix(channel.Guild.Id);
                        var toSend = dropAmount == 1
                            ? GetText(channel.GuildId, strs.curgen_sn(config.Currency.Sign))
                              + " "
                              + GetText(channel.GuildId, strs.pick_sn(prefix))
                            : GetText(channel.GuildId, strs.curgen_pl(dropAmount, config.Currency.Sign))
                              + " "
                              + GetText(channel.GuildId, strs.pick_pl(prefix));

                        var pw = config.Generation.HasPassword ? gs.GeneratePassword().ToUpperInvariant() : null;

                        IUserMessage sent;
                        var (stream, ext) = await GetRandomCurrencyImageAsync(pw);

                        await using (stream)
                            sent = await channel.SendFileAsync(stream, $"currency_image.{ext}", toSend);

                        var res = await AddPlantToDatabase(channel.GuildId,
                            channel.Id,
                            client.CurrentUser.Id,
                            sent.Id,
                            dropAmount,
                            pw,
                            true);

                        if (res.toDelete.Length > 0)
                        {
                            await channel.DeleteMessagesAsync(res.toDelete);
                        }
                    }
                }
            }
            catch
            {
            }
        });
        return Task.CompletedTask;
    }

    public async Task<long> PickAsync(
        ulong gid,
        ITextChannel ch,
        ulong userId,
        string pass)
    {
        long amount;
        ulong[] ids;
        await using (var uow = db.GetDbContext())
        {
            // this method will sum all plants with that password,
            // remove them, and get messageids of the removed plants

            pass = pass?.Trim().TrimTo(10, true)?.ToUpperInvariant();
            // gets all plants in this channel with the same password
            var entries = await uow.GetTable<PlantedCurrency>()
                .Where(x => x.ChannelId == ch.Id && pass == x.Password)
                .DeleteWithOutputAsync();

            if (!entries.Any())
                return 0;

            amount = entries.Sum(x => x.Amount);
            ids = entries.Select(x => x.MessageId).ToArray();
        }

        if (amount > 0)
        {
            await cs.AddAsync(userId, amount, new("currency", "collect"));
            await quests.ReportActionAsync(userId,
                QuestEventType.PlantOrPick,
                new()
                {
                    { "type", "pick" },
                });
        }


        try
        {
            _ = ch.DeleteMessagesAsync(ids);
        }
        catch
        {
        }

        // return the amount of currency the user picked
        return amount;
    }

    public async Task<ulong?> SendPlantMessageAsync(
        ulong gid,
        IMessageChannel ch,
        string user,
        long amount,
        string pass)
    {
        try
        {
            // get the text
            var prefix = cmdHandler.GetPrefix(gid);
            var msgToSend = GetText(gid, strs.planted(Format.Bold(user), amount + gss.Data.Currency.Sign));

            if (amount > 1)
                msgToSend += " " + GetText(gid, strs.pick_pl(prefix));
            else
                msgToSend += " " + GetText(gid, strs.pick_sn(prefix));

            //get the image
            var (stream, ext) = await GetRandomCurrencyImageAsync(pass);
            // send it
            await using (stream)
            {
                var msg = await ch.SendFileAsync(stream, $"img.{ext}", msgToSend);
                // return sent message's id (in order to be able to delete it when it's picked)
                return msg.Id;
            }
        }
        catch (Exception ex)
        {
            // if sending fails, return null as message id
            Log.Warning(ex, "Sending plant message failed: {Message}", ex.Message);
            return null;
        }
    }

    public async Task<bool> PlantAsync(
        ulong gid,
        ITextChannel ch,
        ulong userId,
        string user,
        long amount,
        string pass)
    {
        // normalize it - no more than 10 chars, uppercase
        pass = pass?.Trim().TrimTo(10, true).ToUpperInvariant();
        // has to be either null or alphanumeric
        if (!string.IsNullOrWhiteSpace(pass) && !pass.IsAlphaNumeric())
            return false;

        // remove currency from the user who's planting
        if (await cs.RemoveAsync(userId, amount, new("put/collect", "put")))
        {
            // try to send the message with the currency image
            var msgId = await SendPlantMessageAsync(gid, ch, user, amount, pass);
            if (msgId is null)
            {
                // if it fails it will return null, if it returns null, refund
                await cs.AddAsync(userId, amount, new("put/collect", "refund"));
                return false;
            }

            // if it doesn't fail, put the plant in the database for other people to pick
            await AddPlantToDatabase(gid, ch.Id, userId, msgId.Value, amount, pass);
            await quests.ReportActionAsync(userId, QuestEventType.PlantOrPick, new() { { "type", "plant" } });

            return true;
        }

        // if user doesn't have enough currency, fail
        return false;
    }

    private async Task<(long totalAmount, ulong[] toDelete)> AddPlantToDatabase(
        ulong gid,
        ulong cid,
        ulong uid,
        ulong mid,
        long amount,
        string pass,
        bool auto = false)
    {
        await using var uow = db.GetDbContext();

        PlantedCurrency[] deleted = [];
        if (!string.IsNullOrWhiteSpace(pass) && auto)
        {
            deleted = await uow.GetTable<PlantedCurrency>()
                .Where(x => x.GuildId == gid
                            && x.ChannelId == cid
                            && x.Password != null
                            && x.Password.Length == pass.Length)
                .DeleteWithOutputAsync();
        }

        var totalDeletedAmount = deleted.Length == 0 ? 0 : deleted.Sum(x => x.Amount);

        await uow.GetTable<PlantedCurrency>()
            .InsertAsync(() => new()
            {
                Amount = totalDeletedAmount + amount,
                GuildId = gid,
                ChannelId = cid,
                Password = pass,
                UserId = uid,
                MessageId = mid,
            });

        return (totalDeletedAmount + amount, deleted.Select(x => x.MessageId).ToArray());
    }

    public async Task OnReadyAsync()
    {
        await using var uow = db.GetDbContext();
        _generationChannels = (await uow.GetTable<GCChannelId>()
                .Select(x => x.ChannelId)
                .ToListAsyncLinqToDB())
            .ToHashSet()
            .ToConcurrentSet();
    }
}