#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using EllieBot.Common.ModuleBehaviors;
using EllieBot.Db.Models;
using System.Net;

namespace EllieBot.Modules.Searches;

public sealed class TranslateService : ITranslateService, IExecNoCommand, IReadyExecutor, IEService
{
    private readonly IGoogleApiService _google;
    private readonly DbService _db;
    private readonly IMessageSenderService _sender;
    private readonly IBot _bot;

    private readonly ConcurrentDictionary<ulong, bool> _atcs = new();
    private readonly ConcurrentDictionary<ulong, ConcurrentDictionary<ulong, (string From, string To)>> _users = new();

    public TranslateService(
        IGoogleApiService google,
        DbService db,
        IMessageSenderService sender,
        IBot bot)
    {
        _google = google;
        _db = db;
        _sender = sender;
        _bot = bot;
    }

    public async Task OnReadyAsync()
    {
        List<AutoTranslateChannel> cs;
        await using (var ctx = _db.GetDbContext())
        {
            var guilds = _bot.AllGuildConfigs.Select(x => x.GuildId).ToList();
            cs = await ctx.Set<AutoTranslateChannel>().Include(x => x.Users)
                          .Where(x => guilds.Contains(x.GuildId))
                          .ToListAsyncEF();
        }

        foreach (var c in cs)
        {
            _atcs[c.ChannelId] = c.AutoDelete;
            _users[c.ChannelId] = new(c.Users.ToDictionary(x => x.UserId, x => (x.Source.ToLower(), x.Target.ToLower())));
        }
    }

    public async Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg)
    {
        if (string.IsNullOrWhiteSpace(msg.Content))
            return;

        if (msg is { Channel: ITextChannel tch } um)
        {
            if (!_atcs.TryGetValue(tch.Id, out var autoDelete))
                return;

            if (!_users.TryGetValue(tch.Id, out var users) || !users.TryGetValue(um.Author.Id, out var langs))
                return;

            var output = await _google.Translate(msg.Content, langs.From, langs.To);

            if (string.IsNullOrWhiteSpace(output)
                || msg.Content.Equals(output, StringComparison.InvariantCultureIgnoreCase))
                return;

            var embed = _sender.CreateEmbed(guild?.Id).WithOkColor();

            if (autoDelete)
            {
                embed.WithAuthor(um.Author.ToString(), um.Author.GetAvatarUrl())
                     .AddField(langs.From, um.Content)
                     .AddField(langs.To, output);

                await _sender.Response(tch).Embed(embed).SendAsync();

                try
                {
                    await um.DeleteAsync();
                }
                catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.Forbidden)
                {
                    _atcs.TryUpdate(tch.Id, false, true);
                }

                return;
            }

            await um.ReplyAsync(embed: embed.AddField(langs.To, output).Build(), allowedMentions: AllowedMentions.None);
        }
    }

    public async Task<string> Translate(string source, string target, string text)
    {
        if (string.IsNullOrWhiteSpace(text))
            throw new ArgumentException("Text is empty or null", nameof(text));

        var res = await _google.Translate(text, source.ToLowerInvariant(), target.ToLowerInvariant());
        return res.SanitizeMentions(true);
    }

    public async Task<bool> ToggleAtl(ulong guildId, ulong channelId, bool autoDelete)
    {
        await using var ctx = _db.GetDbContext();

        var old = await ctx.Set<AutoTranslateChannel>().ToLinqToDBTable()
                           .FirstOrDefaultAsyncLinqToDB(x => x.ChannelId == channelId);

        if (old is null)
        {
            ctx.Set<AutoTranslateChannel>().Add(new()
            {
                GuildId = guildId,
                ChannelId = channelId,
                AutoDelete = autoDelete
            });

            await ctx.SaveChangesAsync();

            _atcs[channelId] = autoDelete;
            _users[channelId] = new();

            return true;
        }

        // if autodelete value is different, update the autodelete value
        // instead of disabling
        if (old.AutoDelete != autoDelete)
        {
            old.AutoDelete = autoDelete;
            await ctx.SaveChangesAsync();
            _atcs[channelId] = autoDelete;
            return true;
        }

        await ctx.Set<AutoTranslateChannel>().ToLinqToDBTable().DeleteAsync(x => x.ChannelId == channelId);

        await ctx.SaveChangesAsync();
        _atcs.TryRemove(channelId, out _);
        _users.TryRemove(channelId, out _);

        return false;
    }


    private void UpdateUser(
        ulong channelId,
        ulong userId,
        string from,
        string to)
    {
        var dict = _users.GetOrAdd(channelId, new ConcurrentDictionary<ulong, (string, string)>());
        dict[userId] = (from, to);
    }

    public async Task<bool?> RegisterUserAsync(
        ulong userId,
        ulong channelId,
        string from,
        string to)
    {
        if (!_google.Languages.ContainsKey(from) || !_google.Languages.ContainsKey(to))
            return null;

        await using var ctx = _db.GetDbContext();
        var ch = await ctx.Set<AutoTranslateChannel>().GetByChannelId(channelId);

        if (ch is null)
            return null;

        var user = ch.Users.FirstOrDefault(x => x.UserId == userId);

        if (user is null)
        {
            ch.Users.Add(user = new()
            {
                Source = from,
                Target = to,
                UserId = userId
            });

            await ctx.SaveChangesAsync();

            UpdateUser(channelId, userId, from, to);

            return true;
        }

        // if it's different from old settings, update
        if (user.Source != from || user.Target != to)
        {
            user.Source = from;
            user.Target = to;

            await ctx.SaveChangesAsync();

            UpdateUser(channelId, userId, from, to);

            return true;
        }

        return await UnregisterUser(channelId, userId);
    }

    public async Task<bool> UnregisterUser(ulong channelId, ulong userId)
    {
        await using var ctx = _db.GetDbContext();
        var rows = await ctx.Set<AutoTranslateUser>().ToLinqToDBTable()
                            .DeleteAsync(x => x.UserId == userId && x.Channel.ChannelId == channelId);

        if (_users.TryGetValue(channelId, out var inner))
            inner.TryRemove(userId, out _);
        
        return rows > 0;
    }

    public IEnumerable<string> GetLanguages()
        => _google.Languages.GroupBy(x => x.Value).Select(x => $"{x.AsEnumerable().Select(y => y.Key).Join(", ")}");
}