using Discord;
using Discord.WebSocket;
using NadekoBot.Common;
using NadekoBot.Common.Collections;
using NadekoBot.Db.Models;
using NadekoBot.Extensions;
using Newtonsoft.Json;
using SixLabors.Fonts;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Services;
using NadekoBot.Services.Database.Models;
using NadekoBot.Db;
using Serilog;
using StackExchange.Redis;
using Image = SixLabors.ImageSharp.Image;

namespace NadekoBot.Modules.Xp.Services
{
    // todo improve xp with linqtodb
    public class XpService : INService
    {
        private enum NotifOf
        {
            Server,
            Global
        } // is it a server level-up or global level-up notification

        private readonly DbService _db;
        private readonly CommandHandler _cmd;
        private readonly IImageCache _images;
        private readonly IBotStrings _strings;
        private readonly IDataCache _cache;
        private readonly FontProvider _fonts;
        private readonly IBotCredentials _creds;
        private readonly ICurrencyService _cs;
        private readonly Task updateXpTask;
        private readonly IHttpClientFactory _httpFactory;
        private readonly XpConfigService _xpConfig;
        private readonly IPubSub _pubSub;
        private readonly IEmbedBuilderService _eb;

        public const int XP_REQUIRED_LVL_1 = 36;

        private readonly ConcurrentDictionary<ulong, ConcurrentHashSet<ulong>> _excludedRoles;

        private readonly ConcurrentDictionary<ulong, ConcurrentHashSet<ulong>> _excludedChannels;

        private readonly ConcurrentHashSet<ulong> _excludedServers;

        private readonly ConcurrentQueue<UserCacheItem> _addMessageXp
            = new ConcurrentQueue<UserCacheItem>();
        private XpTemplate _template;
        private readonly DiscordSocketClient _client;

        private readonly TypedKey<bool> _xpTemplateReloadKey;

        public XpService(
            DiscordSocketClient client,
            CommandHandler cmd,
            Bot bot,
            DbService db,
            IBotStrings strings,
            IDataCache cache,
            FontProvider fonts,
            IBotCredentials creds,
            ICurrencyService cs,
            IHttpClientFactory http,
            XpConfigService xpConfig,
            IPubSub pubSub,
            IEmbedBuilderService eb)
        {
            _db = db;
            _cmd = cmd;
            _images = cache.LocalImages;
            _strings = strings;
            _cache = cache;
            _fonts = fonts;
            _creds = creds;
            _cs = cs;
            _httpFactory = http;
            _xpConfig = xpConfig;
            _pubSub = pubSub;
            _eb = eb;
            _excludedServers = new ConcurrentHashSet<ulong>();
            _excludedChannels = new ConcurrentDictionary<ulong, ConcurrentHashSet<ulong>>();
            _client = client;
            _xpTemplateReloadKey = new("xp.template.reload");

            InternalReloadXpTemplate();

            if (client.ShardId == 0)
            {
                _pubSub.Sub(_xpTemplateReloadKey, _ =>
                {
                    InternalReloadXpTemplate();
                    return default;
                });
            }

            //load settings
            var allGuildConfigs = bot.AllGuildConfigs
                .Where(x => x.XpSettings != null)
                .ToList();
            
            _excludedChannels = allGuildConfigs
                .ToDictionary(
                    x => x.GuildId,
                    x => new ConcurrentHashSet<ulong>(x.XpSettings
                        .ExclusionList
                        .Where(ex => ex.ItemType == ExcludedItemType.Channel)
                        .Select(ex => ex.ItemId)
                        .Distinct()))
                .ToConcurrent();

            _excludedRoles = allGuildConfigs
                .ToDictionary(
                    x => x.GuildId,
                    x => new ConcurrentHashSet<ulong>(x.XpSettings
                        .ExclusionList
                        .Where(ex => ex.ItemType == ExcludedItemType.Role)
                        .Select(ex => ex.ItemId)
                        .Distinct()))
                .ToConcurrent();

            _excludedServers = new ConcurrentHashSet<ulong>(
                allGuildConfigs.Where(x => x.XpSettings.ServerExcluded)
                    .Select(x => x.GuildId));

            _cmd.OnMessageNoTrigger += _cmd_OnMessageNoTrigger;
            
#if !GLOBAL_NADEKO
            _client.UserVoiceStateUpdated += _client_OnUserVoiceStateUpdated;
            
            // Scan guilds on startup.
            _client.GuildAvailable += _client_OnGuildAvailable;
            foreach (var guild in _client.Guilds)
            {
                _client_OnGuildAvailable(guild);
            }
#endif
            updateXpTask = Task.Run(UpdateLoop);
        }

        private async Task UpdateLoop()
        {
            while (true)
            {
                await Task.Delay(TimeSpan.FromSeconds(5));
                try
                {
                    var toNotify =
                        new List<(IGuild Guild, IMessageChannel MessageChannel, IUser User, int Level,
                            XpNotificationLocation NotifyType, NotifOf NotifOf)>();
                    var roleRewards = new Dictionary<ulong, List<XpRoleReward>>();
                    var curRewards = new Dictionary<ulong, List<XpCurrencyReward>>();

                    var toAddTo = new List<UserCacheItem>();
                    while (_addMessageXp.TryDequeue(out var usr)) toAddTo.Add(usr);

                    var group = toAddTo.GroupBy(x => (GuildId: x.Guild.Id, x.User));
                    if (toAddTo.Count == 0) continue;

                    using (var uow = _db.GetDbContext())
                    {
                        foreach (var item in group)
                        {
                            var xp = item.Sum(x => x.XpAmount);

                            //1. Mass query discord users and userxpstats and get them from local dict
                            //2. (better but much harder) Move everything to the database, and get old and new xp
                            // amounts for every user (in order to give rewards)

                            var usr = uow.GetOrCreateUserXpStats(item.Key.GuildId, item.Key.User.Id);
                            var du = uow.GetOrCreateUser(item.Key.User);

                            var globalXp = du.TotalXp;
                            var oldGlobalLevelData = new LevelStats(globalXp);
                            var newGlobalLevelData = new LevelStats(globalXp + xp);

                            var oldGuildLevelData = new LevelStats(usr.Xp + usr.AwardedXp);
                            usr.Xp += xp;
                            du.TotalXp += xp;
                            if (du.Club != null) du.Club.Xp += xp;
                            var newGuildLevelData = new LevelStats(usr.Xp + usr.AwardedXp);

                            if (oldGlobalLevelData.Level < newGlobalLevelData.Level)
                            {
                                du.LastLevelUp = DateTime.UtcNow;
                                var first = item.First();
                                if (du.NotifyOnLevelUp != XpNotificationLocation.None)
                                    toNotify.Add((first.Guild, first.Channel, first.User, newGlobalLevelData.Level,
                                        du.NotifyOnLevelUp, NotifOf.Global));
                            }

                            if (oldGuildLevelData.Level < newGuildLevelData.Level)
                            {
                                usr.LastLevelUp = DateTime.UtcNow;
                                //send level up notification
                                var first = item.First();
                                if (usr.NotifyOnLevelUp != XpNotificationLocation.None)
                                    toNotify.Add((first.Guild, first.Channel, first.User, newGuildLevelData.Level,
                                        usr.NotifyOnLevelUp, NotifOf.Server));

                                //give role
                                if (!roleRewards.TryGetValue(usr.GuildId, out var rrews))
                                {
                                    rrews = uow.XpSettingsFor(usr.GuildId).RoleRewards.ToList();
                                    roleRewards.Add(usr.GuildId, rrews);
                                }

                                if (!curRewards.TryGetValue(usr.GuildId, out var crews))
                                {
                                    crews = uow.XpSettingsFor(usr.GuildId).CurrencyRewards.ToList();
                                    curRewards.Add(usr.GuildId, crews);
                                }

                                //loop through levels since last level up, so if a high amount of xp is gained, reward are still applied.
                                for (var i = oldGuildLevelData.Level + 1; i <= newGuildLevelData.Level; i++)
                                {
                                    var rrew = rrews.FirstOrDefault(x => x.Level == i);
                                    if (rrew != null)
                                    {
                                        var role = first.User.Guild.GetRole(rrew.RoleId);
                                        if (role is not null)
                                        {
                                            if (rrew.Remove)
                                            {
                                                _ = first.User.RemoveRoleAsync(role);
                                            }
                                            else
                                            {
                                                _ = first.User.AddRoleAsync(role);
                                            }
                                        }
                                    }

                                    //get currency reward for this level
                                    var crew = crews.FirstOrDefault(x => x.Level == i);
                                    if (crew != null)
                                    {
                                        //give the user the reward if it exists
                                        await _cs.AddAsync(item.Key.User.Id, "Level-up Reward", crew.Amount);
                                    }
                                }
                            }
                        }

                        uow.SaveChanges();
                    }

                    await Task.WhenAll(toNotify.Select(async x =>
                    {
                        if (x.NotifOf == NotifOf.Server)
                        {
                            if (x.NotifyType == XpNotificationLocation.Dm)
                            {
                                var chan = await x.User.GetOrCreateDMChannelAsync();
                                if (chan != null)
                                    await chan.SendConfirmAsync(_eb,
                                        _strings.GetText(strs.level_up_dm(
                                                x.User.Mention, Format.Bold(x.Level.ToString()),
                                                Format.Bold(x.Guild.ToString() ?? "-")),
                                            x.Guild.Id));
                            }
                            else if (x.MessageChannel != null) // channel
                            {
                                await x.MessageChannel.SendConfirmAsync(_eb,
                                    _strings.GetText(strs.level_up_channel(
                                            x.User.Mention,
                                            Format.Bold(x.Level.ToString())),
                                        x.Guild.Id));
                            }
                        }
                        else
                        {
                            IMessageChannel chan;
                            if (x.NotifyType == XpNotificationLocation.Dm)
                            {
                                chan = await x.User.GetOrCreateDMChannelAsync();
                            }
                            else // channel
                            {
                                chan = x.MessageChannel;
                            }

                            await chan.SendConfirmAsync(_eb,
                                _strings.GetText(strs.level_up_global(
                                        x.User.Mention,
                                        Format.Bold(x.Level.ToString())),
                                    x.Guild.Id));
                        }
                    }));
                }
                catch (Exception ex)
                {
                    Log.Error(ex, "Error In the XP update loop");
                }
            }
        }
        

        private void InternalReloadXpTemplate()
        {
            try
            {
                var settings = new JsonSerializerSettings
                {
                    ContractResolver = new RequireObjectPropertiesContractResolver()
                };
                _template = JsonConvert.DeserializeObject<XpTemplate>(
                    File.ReadAllText("./data/xp_template.json"), settings);
            }
            catch (Exception ex)
            {
                Log.Error(ex, "Xp template is invalid. Loaded default values");
                _template = new XpTemplate();
                File.WriteAllText("./data/xp_template_backup.json",
                    JsonConvert.SerializeObject(_template, Formatting.Indented));
            }
        }

        public void ReloadXpTemplate()
        {
           _pubSub.Pub(_xpTemplateReloadKey, true);
        }

        public void SetCurrencyReward(ulong guildId, int level, int amount)
        {
            using (var uow = _db.GetDbContext())
            {
                var settings = uow.XpSettingsFor(guildId);

                if (amount <= 0)
                {
                    var toRemove = settings.CurrencyRewards.FirstOrDefault(x => x.Level == level);
                    if (toRemove != null)
                    {
                        uow.Remove(toRemove);
                        settings.CurrencyRewards.Remove(toRemove);
                    }
                }
                else
                {
                    var rew = settings.CurrencyRewards.FirstOrDefault(x => x.Level == level);

                    if (rew != null)
                        rew.Amount = amount;
                    else
                        settings.CurrencyRewards.Add(new XpCurrencyReward()
                        {
                            Level = level,
                            Amount = amount,
                        });
                }

                uow.SaveChanges();
            }
        }

        public IEnumerable<XpCurrencyReward> GetCurrencyRewards(ulong id)
        {
            using (var uow = _db.GetDbContext())
            {
                return uow.XpSettingsFor(id)
                    .CurrencyRewards
                    .ToArray();
            }
        }

        public IEnumerable<XpRoleReward> GetRoleRewards(ulong id)
        {
            using var uow = _db.GetDbContext();
            return uow.XpSettingsFor(id)
                .RoleRewards
                .ToArray();
        }

        public void ResetRoleReward(ulong guildId, int level)
        {
            using var uow = _db.GetDbContext();
            var settings = uow.XpSettingsFor(guildId);
            
            var toRemove = settings.RoleRewards.FirstOrDefault(x => x.Level == level);
            if (toRemove != null)
            {
                uow.Remove(toRemove);
                settings.RoleRewards.Remove(toRemove);
            }

            uow.SaveChanges();
        }
        
        public void SetRoleReward(ulong guildId, int level, ulong roleId, bool remove)
        {
            using var uow = _db.GetDbContext();
            var settings = uow.XpSettingsFor(guildId);

            
            var rew = settings.RoleRewards.FirstOrDefault(x => x.Level == level);

            if (rew != null)
            {
                rew.RoleId = roleId;
                rew.Remove = remove;
            }
            else
            {
                settings.RoleRewards.Add(new XpRoleReward()
                {
                    Level = level,
                    RoleId = roleId,
                    Remove = remove,
                });
            }

            uow.SaveChanges();
        }

        public List<UserXpStats> GetUserXps(ulong guildId, int page)
        {
            using (var uow = _db.GetDbContext())
            {
                return uow.UserXpStats.GetUsersFor(guildId, page);
            }
        }

        public List<UserXpStats> GetTopUserXps(ulong guildId, int count)
        {
            using (var uow = _db.GetDbContext())
            {
                return uow.UserXpStats.GetTopUserXps(guildId, count);
            }
        }

        public DiscordUser[] GetUserXps(int page)
        {
            using (var uow = _db.GetDbContext())
            {
                return uow.DiscordUser.GetUsersXpLeaderboardFor(page);
            }
        }

        public async Task ChangeNotificationType(ulong userId, ulong guildId, XpNotificationLocation type)
        {
            using (var uow = _db.GetDbContext())
            {
                var user = uow.GetOrCreateUserXpStats(guildId, userId);
                user.NotifyOnLevelUp = type;
                await uow.SaveChangesAsync();
            }
        }
        
        public XpNotificationLocation GetNotificationType(ulong userId, ulong guildId)
        {
            using (var uow = _db.GetDbContext())
            {
                var user = uow.GetOrCreateUserXpStats(guildId, userId);
                return user.NotifyOnLevelUp;
            }
        }

        public XpNotificationLocation GetNotificationType(IUser user)
        {
            using (var uow = _db.GetDbContext())
            {
                return uow.GetOrCreateUser(user).NotifyOnLevelUp;
            }
        }

        public async Task ChangeNotificationType(IUser user, XpNotificationLocation type)
        {
            using (var uow = _db.GetDbContext())
            {
                var du = uow.GetOrCreateUser(user);
                du.NotifyOnLevelUp = type;
                await uow.SaveChangesAsync();
            }
        }

        private Task _client_OnGuildAvailable(SocketGuild guild)
        {
            Task.Run(() =>
            {
                foreach (var channel in guild.VoiceChannels)
                {
                    ScanChannelForVoiceXp(channel);
                }
            });
            
            return Task.CompletedTask;
        }
        
        private Task _client_OnUserVoiceStateUpdated(SocketUser socketUser, SocketVoiceState before, SocketVoiceState after)
        {
            if (!(socketUser is SocketGuildUser user) || user.IsBot)
                return Task.CompletedTask;

            var _ = Task.Run(() =>
            {
                if (before.VoiceChannel != null)
                {
                    ScanChannelForVoiceXp(before.VoiceChannel);
                }

                if (after.VoiceChannel != null && after.VoiceChannel != before.VoiceChannel)
                {
                    ScanChannelForVoiceXp(after.VoiceChannel);
                }
                else if (after.VoiceChannel is null)
                {
                    // In this case, the user left the channel and the previous for loops didn't catch
                    // it because it wasn't in any new channel. So we need to get rid of it.
                    UserLeftVoiceChannel(user, before.VoiceChannel);
                }
            });
            
            return Task.CompletedTask;
        }

        private void ScanChannelForVoiceXp(SocketVoiceChannel channel)
        {
            if (ShouldTrackVoiceChannel(channel))
            {
                foreach (var user in channel.Users)
                {
                    ScanUserForVoiceXp(user, channel);
                }
            }
            else
            {
                foreach (var user in channel.Users)
                {
                    UserLeftVoiceChannel(user, channel);
                }
            }
        }

        /// <summary>
        /// Assumes that the channel itself is valid and adding xp.
        /// </summary>
        /// <param name="user"></param>
        /// <param name="channel"></param>
        private void ScanUserForVoiceXp(SocketGuildUser user, SocketVoiceChannel channel)
        {
            if (UserParticipatingInVoiceChannel(user) && ShouldTrackXp(user, channel.Id))
            {
                UserJoinedVoiceChannel(user);
            }
            else
            {
                UserLeftVoiceChannel(user, channel);
            }
        }

        private bool ShouldTrackVoiceChannel(SocketVoiceChannel channel)
        {
            return channel.Users.Where(UserParticipatingInVoiceChannel).Take(2).Count() >= 2;
        }

        private bool UserParticipatingInVoiceChannel(SocketGuildUser user)
        {
            return !user.IsDeafened && !user.IsMuted && !user.IsSelfDeafened && !user.IsSelfMuted;
        }

        private void UserJoinedVoiceChannel(SocketGuildUser user)
        {
            var key = $"{_creds.RedisKey()}_user_xp_vc_join_{user.Id}";
            var value = DateTimeOffset.UtcNow.ToUnixTimeSeconds();

            _cache.Redis.GetDatabase().StringSet(key, value, TimeSpan.FromMinutes(_xpConfig.Data.VoiceMaxMinutes), When.NotExists);
        }

        private void UserLeftVoiceChannel(SocketGuildUser user, SocketVoiceChannel channel)
        {
            var key = $"{_creds.RedisKey()}_user_xp_vc_join_{user.Id}";
            var value = _cache.Redis.GetDatabase().StringGet(key);
            _cache.Redis.GetDatabase().KeyDelete(key);
            
            // Allow for if this function gets called multiple times when a user leaves a channel.
            if (value.IsNull) return;
            
            if (!value.TryParse(out long startUnixTime))
                return;
            
            var dateStart = DateTimeOffset.FromUnixTimeSeconds(startUnixTime);
            var dateEnd = DateTimeOffset.UtcNow;
            var minutes = (dateEnd - dateStart).TotalMinutes;
            var xp = _xpConfig.Data.VoiceXpPerMinute * minutes;
            var actualXp = (int) Math.Floor(xp);

            if (actualXp > 0)
            {
                _addMessageXp.Enqueue(new UserCacheItem
                {
                    Guild = channel.Guild,
                    User = user,
                    XpAmount = actualXp
                });
            }
        }

        private bool ShouldTrackXp(SocketGuildUser user, ulong channelId)
        {
            if (_excludedChannels.TryGetValue(user.Guild.Id, out var chans) &&
                chans.Contains(channelId)) return false;
            
            if (_excludedServers.Contains(user.Guild.Id)) return false;
            
            if (_excludedRoles.TryGetValue(user.Guild.Id, out var roles) &&
                user.Roles.Any(x => roles.Contains(x.Id)))
                return false;

            return true;
        }
        
        private Task _cmd_OnMessageNoTrigger(IUserMessage arg)
        {
            if (!(arg.Author is SocketGuildUser user) || user.IsBot)
                return Task.CompletedTask;

            var _ = Task.Run(() =>
            {
                if (!ShouldTrackXp(user, arg.Channel.Id))
                    return;

                var xpConf = _xpConfig.Data;
                var xp = 0;
                if (arg.Attachments.Any(a => a.Height >= 128 && a.Width >= 128))
                {
                    xp = xpConf.XpFromImage;
                }
                
                if (arg.Content.Contains(' ') || arg.Content.Length >= 5)
                {
                    xp = Math.Max(xp, xpConf.XpPerMessage);
                }

                if (xp <= 0)
                    return;

                if (!SetUserRewarded(user.Id))
                    return;

                _addMessageXp.Enqueue(new UserCacheItem
                {
                    Guild = user.Guild,
                    Channel = arg.Channel,
                    User = user,
                    XpAmount = xp 
                });
            });
            return Task.CompletedTask;
        }

        public void AddXpDirectly(IGuildUser user, IMessageChannel channel, int amount)
        {
            if (amount <= 0) throw new ArgumentOutOfRangeException(nameof(amount));
            
            _addMessageXp.Enqueue(new UserCacheItem
            {
                Guild = user.Guild,
                Channel = channel,
                User = user,
                XpAmount = amount
            });
        }
        
        public void AddXp(ulong userId, ulong guildId, int amount)
        {
            using (var uow = _db.GetDbContext())
            {
                var usr = uow.GetOrCreateUserXpStats(guildId, userId);

                usr.AwardedXp += amount;

                uow.SaveChanges();
            }
        }

        public bool IsServerExcluded(ulong id)
        {
            return _excludedServers.Contains(id);
        }

        public IEnumerable<ulong> GetExcludedRoles(ulong id)
        {
            if (_excludedRoles.TryGetValue(id, out var val))
                return val.ToArray();

            return Enumerable.Empty<ulong>();
        }

        public IEnumerable<ulong> GetExcludedChannels(ulong id)
        {
            if (_excludedChannels.TryGetValue(id, out var val))
                return val.ToArray();

            return Enumerable.Empty<ulong>();
        }

        private bool SetUserRewarded(ulong userId)
        {
            var r = _cache.Redis.GetDatabase();
            var key = $"{_creds.RedisKey()}_user_xp_gain_{userId}";

            return r.StringSet(key,
                true,
                TimeSpan.FromMinutes(_xpConfig.Data.MessageXpCooldown),
                StackExchange.Redis.When.NotExists);
        }

        public async Task<FullUserStats> GetUserStatsAsync(IGuildUser user)
        {
            DiscordUser du;
            UserXpStats stats = null;
            int totalXp;
            int globalRank;
            int guildRank;
            using (var uow = _db.GetDbContext())
            {
                du = uow.GetOrCreateUser(user);
                totalXp = du.TotalXp;
                globalRank = uow.DiscordUser.GetUserGlobalRank(user.Id);
                guildRank = uow.UserXpStats.GetUserGuildRanking(user.Id, user.GuildId);
                stats = uow.GetOrCreateUserXpStats(user.GuildId, user.Id);
                await uow.SaveChangesAsync();
            }

            return new FullUserStats(du,
                stats,
                new LevelStats(totalXp),
                new LevelStats(stats.Xp + stats.AwardedXp),
                globalRank,
                guildRank);
        }

        public static (int Level, int LevelXp, int LevelRequiredXp) GetLevelData(UserXpStats stats)
        {
            var baseXp = XpService.XP_REQUIRED_LVL_1;

            var required = baseXp;
            var totalXp = 0;
            var lvl = 1;
            while (true)
            {
                required = (int) (baseXp + baseXp / 4.0 * (lvl - 1));

                if (required + totalXp > stats.Xp)
                    break;

                totalXp += required;
                lvl++;
            }

            return (lvl - 1, stats.Xp - totalXp, required);
        }

        public bool ToggleExcludeServer(ulong id)
        {
            using (var uow = _db.GetDbContext())
            {
                var xpSetting = uow.XpSettingsFor(id);
                if (_excludedServers.Add(id))
                {
                    xpSetting.ServerExcluded = true;
                    uow.SaveChanges();
                    return true;
                }

                _excludedServers.TryRemove(id);
                xpSetting.ServerExcluded = false;
                uow.SaveChanges();
                return false;
            }
        }

        public bool ToggleExcludeRole(ulong guildId, ulong rId)
        {
            var roles = _excludedRoles.GetOrAdd(guildId, _ => new ConcurrentHashSet<ulong>());
            using (var uow = _db.GetDbContext())
            {
                var xpSetting = uow.XpSettingsFor(guildId);
                var excludeObj = new ExcludedItem
                {
                    ItemId = rId,
                    ItemType = ExcludedItemType.Role,
                };

                if (roles.Add(rId))
                {
                    if (xpSetting.ExclusionList.Add(excludeObj))
                    {
                        uow.SaveChanges();
                    }

                    return true;
                }
                else
                {
                    roles.TryRemove(rId);

                    var toDelete = xpSetting.ExclusionList.FirstOrDefault(x => x.Equals(excludeObj));
                    if (toDelete != null)
                    {
                        uow.Remove(toDelete);
                        uow.SaveChanges();
                    }

                    return false;
                }
            }
        }

        public bool ToggleExcludeChannel(ulong guildId, ulong chId)
        {
            var channels = _excludedChannels.GetOrAdd(guildId, _ => new ConcurrentHashSet<ulong>());
            using (var uow = _db.GetDbContext())
            {
                var xpSetting = uow.XpSettingsFor(guildId);
                var excludeObj = new ExcludedItem
                {
                    ItemId = chId,
                    ItemType = ExcludedItemType.Channel,
                };

                if (channels.Add(chId))
                {
                    if (xpSetting.ExclusionList.Add(excludeObj))
                    {
                        uow.SaveChanges();
                    }

                    return true;
                }
                else
                {
                    channels.TryRemove(chId);

                    if (xpSetting.ExclusionList.Remove(excludeObj))
                    {
                        uow.SaveChanges();
                    }

                    return false;
                }
            }
        }

        public async Task<(Stream Image, IImageFormat Format)> GenerateXpImageAsync(IGuildUser user)
        {
            var stats = await GetUserStatsAsync(user);
            return await GenerateXpImageAsync(stats);
        }


        public Task<(Stream Image, IImageFormat Format)> GenerateXpImageAsync(FullUserStats stats) => Task.Run(
            async () =>
            {
                var usernameTextOptions = new TextGraphicsOptions()
                {
                    TextOptions = new TextOptions()
                    {
                        HorizontalAlignment = HorizontalAlignment.Left,
                        VerticalAlignment = VerticalAlignment.Center,
                    }
                }.WithFallbackFonts(_fonts.FallBackFonts);
                
                var clubTextOptions = new TextGraphicsOptions()
                {
                    TextOptions = new TextOptions()
                    {
                        HorizontalAlignment = HorizontalAlignment.Right,
                        VerticalAlignment = VerticalAlignment.Top,
                    }
                }.WithFallbackFonts(_fonts.FallBackFonts);
                
                using (var img = Image.Load<Rgba32>(_images.XpBackground, out var imageFormat))
                {
                    if (_template.User.Name.Show)
                    {
                        var fontSize = (int)(_template.User.Name.FontSize * 0.9);
                        var username = stats.User.ToString();
                        var usernameFont = _fonts.NotoSans
                            .CreateFont(fontSize, FontStyle.Bold);

                        var size = TextMeasurer.Measure($"@{username}", new RendererOptions(usernameFont));
                        var scale = 400f / size.Width;
                        if (scale < 1)
                        {
                            usernameFont = _fonts.NotoSans
                                .CreateFont(_template.User.Name.FontSize * scale, FontStyle.Bold);
                        }

                        img.Mutate(x =>
                        {
                            x.DrawText(usernameTextOptions,
                                "@" + username,
                                usernameFont,
                                _template.User.Name.Color,
                                new PointF(_template.User.Name.Pos.X, _template.User.Name.Pos.Y + 8));
                        });
                    }

                    //club name

                    if (_template.Club.Name.Show)
                    {
                        var clubName = stats.User.Club?.ToString() ?? "-";

                        var clubFont = _fonts.NotoSans
                            .CreateFont(_template.Club.Name.FontSize, FontStyle.Regular);
                        
                        img.Mutate(x => x.DrawText(clubTextOptions,
                            clubName,
                            clubFont,
                            _template.Club.Name.Color,
                            new PointF(_template.Club.Name.Pos.X + 50, _template.Club.Name.Pos.Y - 8))
                        );
                    }

                    if (_template.User.GlobalLevel.Show)
                    {
                        img.Mutate(x =>
                        {
                            x.DrawText(
                                stats.Global.Level.ToString(),
                                _fonts.NotoSans.CreateFont(_template.User.GlobalLevel.FontSize, FontStyle.Bold),
                                _template.User.GlobalLevel.Color,
                                new PointF(_template.User.GlobalLevel.Pos.X, _template.User.GlobalLevel.Pos.Y)
                            ); //level
                        });
                    }

                    if (_template.User.GuildLevel.Show)
                    {
                        img.Mutate(x =>
                        {
                            x.DrawText(
                                stats.Guild.Level.ToString(),
                                _fonts.NotoSans.CreateFont(_template.User.GuildLevel.FontSize, FontStyle.Bold),
                                _template.User.GuildLevel.Color,
                                new PointF(_template.User.GuildLevel.Pos.X, _template.User.GuildLevel.Pos.Y)
                            );
                        });
                    }


                    var pen = new Pen(SixLabors.ImageSharp.Color.Black, 1);

                    var global = stats.Global;
                    var guild = stats.Guild;

                    //xp bar
                    if (_template.User.Xp.Bar.Show)
                    {
                        var xpPercent = (global.LevelXp / (float) global.RequiredXp);
                        DrawXpBar(xpPercent, _template.User.Xp.Bar.Global, img);
                        xpPercent = (guild.LevelXp / (float) guild.RequiredXp);
                        DrawXpBar(xpPercent, _template.User.Xp.Bar.Guild, img);
                    }

                    if (_template.User.Xp.Global.Show)
                    {
                        img.Mutate(x => x.DrawText($"{global.LevelXp}/{global.RequiredXp}",
                            _fonts.NotoSans.CreateFont(_template.User.Xp.Global.FontSize, FontStyle.Bold),
                            Brushes.Solid(_template.User.Xp.Global.Color),
                            pen,
                            new PointF(_template.User.Xp.Global.Pos.X, _template.User.Xp.Global.Pos.Y)));
                    }

                    if (_template.User.Xp.Guild.Show)
                    {
                        img.Mutate(x => x.DrawText($"{guild.LevelXp}/{guild.RequiredXp}",
                            _fonts.NotoSans.CreateFont(_template.User.Xp.Guild.FontSize, FontStyle.Bold),
                            Brushes.Solid(_template.User.Xp.Guild.Color),
                            pen,
                            new PointF(_template.User.Xp.Guild.Pos.X, _template.User.Xp.Guild.Pos.Y)));
                    }

                    if (stats.FullGuildStats.AwardedXp != 0 && _template.User.Xp.Awarded.Show)
                    {
                        var sign = stats.FullGuildStats.AwardedXp > 0
                            ? "+ "
                            : "";
                        var awX = _template.User.Xp.Awarded.Pos.X -
                                  (Math.Max(0, (stats.FullGuildStats.AwardedXp.ToString().Length - 2)) * 5);
                        var awY = _template.User.Xp.Awarded.Pos.Y;
                        img.Mutate(x => x.DrawText($"({sign}{stats.FullGuildStats.AwardedXp})",
                            _fonts.NotoSans.CreateFont(_template.User.Xp.Awarded.FontSize, FontStyle.Bold),
                            Brushes.Solid(_template.User.Xp.Awarded.Color),
                            pen,
                            new PointF(awX, awY)));
                    }

                    //ranking
                    if (_template.User.GlobalRank.Show)
                    {
                        img.Mutate(x => x.DrawText(stats.GlobalRanking.ToString(),
                            _fonts.UniSans.CreateFont(_template.User.GlobalRank.FontSize, FontStyle.Bold),
                            _template.User.GlobalRank.Color,
                            new PointF(_template.User.GlobalRank.Pos.X, _template.User.GlobalRank.Pos.Y)));
                    }

                    if (_template.User.GuildRank.Show)
                    {
                        img.Mutate(x => x.DrawText(stats.GuildRanking.ToString(),
                            _fonts.UniSans.CreateFont(_template.User.GuildRank.FontSize, FontStyle.Bold),
                            _template.User.GuildRank.Color,
                            new PointF(_template.User.GuildRank.Pos.X, _template.User.GuildRank.Pos.Y)));
                    }

                    //time on this level

                    string GetTimeSpent(DateTime time, string format)
                    {
                        var offset = DateTime.UtcNow - time;
                        return string.Format(format, offset.Days, offset.Hours, offset.Minutes);
                    }

                    if (_template.User.TimeOnLevel.Global.Show)
                    {
                        img.Mutate(x =>
                            x.DrawText(GetTimeSpent(stats.User.LastLevelUp, _template.User.TimeOnLevel.Format),
                                _fonts.NotoSans.CreateFont(_template.User.TimeOnLevel.Global.FontSize, FontStyle.Bold),
                                _template.User.TimeOnLevel.Global.Color,
                                new PointF(_template.User.TimeOnLevel.Global.Pos.X,
                                    _template.User.TimeOnLevel.Global.Pos.Y)));
                    }

                    if (_template.User.TimeOnLevel.Guild.Show)
                    {
                        img.Mutate(x =>
                            x.DrawText(
                                GetTimeSpent(stats.FullGuildStats.LastLevelUp, _template.User.TimeOnLevel.Format),
                                _fonts.NotoSans.CreateFont(_template.User.TimeOnLevel.Guild.FontSize, FontStyle.Bold),
                                _template.User.TimeOnLevel.Guild.Color,
                                new PointF(_template.User.TimeOnLevel.Guild.Pos.X,
                                    _template.User.TimeOnLevel.Guild.Pos.Y)));
                    }
                    //avatar

                    if (stats.User.AvatarId != null && _template.User.Icon.Show)
                    {
                        try
                        {
                            var avatarUrl = stats.User.RealAvatarUrl();

                            var (succ, data) = await _cache.TryGetImageDataAsync(avatarUrl);
                            if (!succ)
                            {
                                using (var http = _httpFactory.CreateClient())
                                {
                                    var avatarData = await http.GetByteArrayAsync(avatarUrl);
                                    using (var tempDraw = Image.Load(avatarData))
                                    {
                                        tempDraw.Mutate(x => x
                                            .Resize(_template.User.Icon.Size.X, _template.User.Icon.Size.Y)
                                            .ApplyRoundedCorners(
                                                Math.Max(_template.User.Icon.Size.X, _template.User.Icon.Size.Y) / 2));
                                        using (var stream = tempDraw.ToStream())
                                        {
                                            data = stream.ToArray();
                                        }
                                    }
                                }

                                await _cache.SetImageDataAsync(avatarUrl, data);
                            }

                            using (var toDraw = Image.Load(data))
                            {
                                if (toDraw.Size() != new Size(_template.User.Icon.Size.X, _template.User.Icon.Size.Y))
                                {
                                    toDraw.Mutate(x =>
                                        x.Resize(_template.User.Icon.Size.X, _template.User.Icon.Size.Y));
                                }

                                img.Mutate(x => x.DrawImage(toDraw,
                                    new Point(_template.User.Icon.Pos.X, _template.User.Icon.Pos.Y), 1));
                            }
                        }
                        catch (Exception ex)
                        {
                            Log.Warning(ex, "Error drawing avatar image");
                        }
                    }

                    //club image
                    if (_template.Club.Icon.Show)
                    {
                        await DrawClubImage(img, stats);
                    }

                    img.Mutate(x => x.Resize(_template.OutputSize.X, _template.OutputSize.Y));
                    return ((Stream) img.ToStream(imageFormat), imageFormat);
                }
            });

        void DrawXpBar(float percent, XpBar info, Image<Rgba32> img)
        {
            var x1 = info.PointA.X;
            var y1 = info.PointA.Y;

            var x2 = info.PointB.X;
            var y2 = info.PointB.Y;

            var length = info.Length * percent;

            float x3 = 0, x4 = 0, y3 = 0, y4 = 0;

            if (info.Direction == XpTemplateDirection.Down)
            {
                x3 = x1;
                x4 = x2;
                y3 = y1 + length;
                y4 = y2 + length;
            }
            else if (info.Direction == XpTemplateDirection.Up)
            {
                x3 = x1;
                x4 = x2;
                y3 = y1 - length;
                y4 = y2 - length;
            }
            else if (info.Direction == XpTemplateDirection.Left)
            {
                x3 = x1 - length;
                x4 = x2 - length;
                y3 = y1;
                y4 = y2;
            }
            else
            {
                x3 = x1 + length;
                x4 = x2 + length;
                y3 = y1;
                y4 = y2;
            }

            img.Mutate(x => x.FillPolygon(info.Color,
                new[]
                {
                    new PointF(x1, y1),
                    new PointF(x3, y3),
                    new PointF(x4, y4),
                    new PointF(x2, y2),
                }));
        }

        private async Task DrawClubImage(Image<Rgba32> img, FullUserStats stats)
        {
            if (!string.IsNullOrWhiteSpace(stats.User.Club?.ImageUrl))
            {
                try
                {
                    var imgUrl = new Uri(stats.User.Club.ImageUrl);
                    var (succ, data) = await _cache.TryGetImageDataAsync(imgUrl);
                    if (!succ)
                    {
                        using (var http = _httpFactory.CreateClient())
                        using (var temp = await http.GetAsync(imgUrl, HttpCompletionOption.ResponseHeadersRead))
                        {
                            if (!temp.IsImage() || temp.GetImageSize() > 11)
                                return;
                            var imgData = await temp.Content.ReadAsByteArrayAsync();
                            using (var tempDraw = Image.Load(imgData))
                            {
                                tempDraw.Mutate(x => x
                                    .Resize(_template.Club.Icon.Size.X, _template.Club.Icon.Size.Y)
                                    .ApplyRoundedCorners(
                                        Math.Max(_template.Club.Icon.Size.X, _template.Club.Icon.Size.Y) / 2.0f));
                                ;
                                using (var tds = tempDraw.ToStream())
                                {
                                    data = tds.ToArray();
                                }
                            }
                        }

                        await _cache.SetImageDataAsync(imgUrl, data);
                    }

                    using (var toDraw = Image.Load(data))
                    {
                        if (toDraw.Size() != new Size(_template.Club.Icon.Size.X, _template.Club.Icon.Size.Y))
                        {
                            toDraw.Mutate(x => x.Resize(_template.Club.Icon.Size.X, _template.Club.Icon.Size.Y));
                        }

                        img.Mutate(x => x.DrawImage(
                            toDraw,
                            new Point(_template.Club.Icon.Pos.X, _template.Club.Icon.Pos.Y),
                            1));
                    }
                }
                catch (Exception ex)
                {
                    Log.Warning(ex, "Error drawing club image");
                }
            }
        }

        public void XpReset(ulong guildId, ulong userId)
        {
            using (var uow = _db.GetDbContext())
            {
                uow.UserXpStats.ResetGuildUserXp(userId, guildId);
                uow.SaveChanges();
            }
        }

        public void XpReset(ulong guildId)
        {
            using (var uow = _db.GetDbContext())
            {
                uow.UserXpStats.ResetGuildXp(guildId);
                uow.SaveChanges();
            }
        }

        public async Task ResetXpRewards(ulong guildId)
        {
            using var uow = _db.GetDbContext();
            var guildConfig = uow.GuildConfigsForId(guildId, 
                set => set
                    .Include(x => x.XpSettings)
                    .ThenInclude(x => x.CurrencyRewards)
                    .Include(x => x.XpSettings)
                    .ThenInclude(x => x.RoleRewards));
            
            uow.RemoveRange(guildConfig.XpSettings.RoleRewards);
            uow.RemoveRange(guildConfig.XpSettings.CurrencyRewards);
            await uow.SaveChangesAsync();
        }
    }
}