#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using EllieBot.Common.ModuleBehaviors;
using EllieBot.Db.Models;
using System.Collections.Frozen;

namespace EllieBot.Modules.Searches;

public sealed partial class FlagTranslateService : IReadyExecutor, IEService
{
    private readonly IBotCreds _creds;
    private readonly DiscordSocketClient _client;
    private readonly TranslateService _ts;
    private readonly IMessageSenderService _sender;
    private IReadOnlyDictionary<string, string> _supportedFlags;
    private readonly DbService _db;
    private ConcurrentHashSet<ulong> _enabledChannels;
    private readonly IBotCache _cache;

    // disallow same message being translated multiple times to the same language
    private readonly ConcurrentHashSet<(ulong, string)> _msgLangs = new();

    public FlagTranslateService(
        IBotCreds creds,
        DiscordSocketClient client,
        TranslateService ts,
        IMessageSenderService sender,
        DbService db,
        IBotCache cache)
    {
        _creds = creds;
        _client = client;
        _ts = ts;
        _sender = sender;
        _db = db;
        _cache = cache;
    }

    public async Task OnReadyAsync()
    {
        _supportedFlags = COUNTRIES
                          .Split('\n')
                          .Select(x => x.Split(' '))
                          .ToDictionary(x => x[0], x => x[1].TrimEnd())
                          .ToFrozenDictionary();

        await using (var uow = _db.GetDbContext())
        {
            _enabledChannels = (await uow.GetTable<FlagTranslateChannel>()
                                         .Where(x => Linq2DbExpressions.GuildOnShard(x.GuildId,
                                             _creds.TotalShards,
                                             _client.ShardId))
                                         .Select(x => new
                                         {
                                             x.ChannelId,
                                             x.GuildId
                                         })
                                         .ToListAsyncLinqToDB())
                               .Select(x => x.ChannelId)
                               .ToHashSet()
                               .ToConcurrentSet();
        }

        _client.ReactionAdded += OnReactionAdded;

        var periodicCleanup = new PeriodicTimer(TimeSpan.FromHours(24));

        while (await periodicCleanup.WaitForNextTickAsync())
        {
            _msgLangs.Clear();
        }
    }

    private const int FLAG_START = 127462;

    private static TypedKey<bool> CdKey(ulong userId)
        => new($"flagtranslate:{userId}");

    private Task OnReactionAdded(
        Cacheable<IUserMessage, ulong> arg1,
        Cacheable<IMessageChannel, ulong> arg2,
        SocketReaction reaction)
    {
        if (!_enabledChannels.Contains(reaction.Channel.Id))
            return Task.CompletedTask;

        var runes = reaction.Emote.Name.EnumerateRunes();
        if (!runes.MoveNext()
            || runes.Current is not { Value: >= 127462 and <= 127487 } l1
            || !runes.MoveNext()
            || runes.Current is not { Value: >= 127462 and <= 127487 } l2)
        {
            return Task.CompletedTask;
        }

        _ = Task.Run(async () =>
        {
            if (reaction.Channel is not SocketTextChannel tc)
                return;

            var user = await ((IGuild)tc.Guild).GetUserAsync(reaction.UserId);

            if (user is null)
                return;

            if (!user.GetPermissions(tc).SendMessages)
                return;

            if (!tc.Guild.CurrentUser.GetPermissions(tc).SendMessages
                || !tc.Guild.CurrentUser.GetPermissions(tc).EmbedLinks)
            {
                await Disable(tc.Guild.Id, tc.Id);
                return;
            }

            var c1 = (char)(l1.Value - FLAG_START + 65);
            var c2 = (char)(l2.Value - FLAG_START + 65);

            var code = $"{c1}{c2}".ToUpper();

            if (!_supportedFlags.TryGetValue(code, out var lang))
                return;

            if (!_msgLangs.Add((reaction.MessageId, lang)))
                return;

            var result = await _cache.GetAsync(CdKey(reaction.UserId));
            if (result.TryPickT0(out _, out _))
                return;

            await _cache.AddAsync(CdKey(reaction.UserId), true, TimeSpan.FromSeconds(5));

            var msg = await arg1.GetOrDownloadAsync();

            var response = await _ts.Translate("", lang, msg.Content).ConfigureAwait(false);

            await msg.ReplyAsync(embed: _sender.CreateEmbed(tc.Guild?.Id)
                                               .WithOkColor()
                                               .WithFooter(user.ToString() ?? reaction.UserId.ToString(),
                                                   user.RealAvatarUrl().ToString())
                                               .WithDescription(response)
                                               .WithAuthor(reaction.Emote.ToString())
                                               .Build(),
                allowedMentions: AllowedMentions.None
            );
        });

        return Task.CompletedTask;
    }

    public async Task Disable(ulong guildId, ulong tcId)
    {
        if (!_enabledChannels.TryRemove(tcId))
            return;

        await using var uow = _db.GetDbContext();
        await uow.GetTable<FlagTranslateChannel>()
                 .Where(x => x.GuildId == guildId
                             && x.ChannelId == tcId)
                 .DeleteAsync();
    }

    public async Task<bool> Toggle(ulong guildId, ulong tcId)
    {
        if (_enabledChannels.Contains(tcId))
        {
            await Disable(guildId, tcId);

            return false;
        }

        await Enable(guildId, tcId);

        return true;
    }

    public async Task Enable(ulong guildId, ulong tcId)
    {
        if (!_enabledChannels.Add(tcId))
            return;

        await using var uow = _db.GetDbContext();
        await uow.GetTable<FlagTranslateChannel>()
                 .InsertAsync(() => new FlagTranslateChannel
                 {
                     GuildId = guildId,
                     ChannelId = tcId
                 });
    }
}