using System.IO;
using System.IO.Compression;
using CompatApiClient.Utils;
using CompatBot.EventHandlers;
using DSharpPlus.Commands.Processors.TextCommands;

namespace CompatBot.Commands;

[Command("audit"), RequiresBotModRole, LimitedToSpamChannel]
[Description("Commands to audit server things")]
internal static class Audit
{
    public static readonly SemaphoreSlim CheckLock = new(1, 1);

    [Command("spoofing"), TextAlias("impersonation"), RequiresDm]
    [Description("Checks every user on the server for name spoofing")]
    public static ValueTask Spoofing(TextCommandContext ctx)
    {
        SpoofingCheck(ctx);
        return ValueTask.CompletedTask;
    }

    [Command("members"), TextAlias("users"), RequiresDm]
    [Description("Dumps server member information, including usernames, nicknames, and roles")]
    public static async ValueTask Members(TextCommandContext ctx)
    {
        if (!await CheckLock.WaitAsync(0).ConfigureAwait(false))
        {
            await ctx.Channel.SendMessageAsync("Another check is already in progress").ConfigureAwait(false);
            return;
        }

        try
        {
            await ctx.ReactWithAsync(Config.Reactions.PleaseWait).ConfigureAwait(false);
            var members = GetMembers(ctx.Client);
            await using var compressedResult = Config.MemoryStreamManager.GetStream();
            await using var memoryStream = Config.MemoryStreamManager.GetStream();
            await using var writer = new StreamWriter(memoryStream, new UTF8Encoding(false), 4096, true);
            foreach (var member in members)
                await writer.WriteLineAsync($"{member.Username}\t{member.Nickname}\t{member.JoinedAt:O}\t{string.Join(',', member.Roles.Select(r => r.Name))}").ConfigureAwait(false);
            await writer.FlushAsync().ConfigureAwait(false);
            memoryStream.Seek(0, SeekOrigin.Begin);
            if (memoryStream.Length <= ctx.GetAttachmentSizeLimit())
            {
                await ctx.Channel.SendMessageAsync(new DiscordMessageBuilder().AddFile("names.txt", memoryStream)).ConfigureAwait(false);
                return;
            }

            await using var gzip = new GZipStream(compressedResult, CompressionLevel.Optimal, true);
            await memoryStream.CopyToAsync(gzip).ConfigureAwait(false);
            await gzip.FlushAsync().ConfigureAwait(false);
            compressedResult.Seek(0, SeekOrigin.Begin);
            if (compressedResult.Length <= ctx.GetAttachmentSizeLimit())
                await ctx.Channel.SendMessageAsync(new DiscordMessageBuilder().AddFile("names.txt.gz", compressedResult)).ConfigureAwait(false);
            else
                await ctx.Channel.SendMessageAsync($"Dump is too large: {compressedResult.Length} bytes").ConfigureAwait(false);
        }
        catch (Exception e)
        {
            Config.Log.Warn(e, "Failed to dump guild members");
            await ctx.ReactWithAsync(Config.Reactions.Failure, "Failed to dump guild members").ConfigureAwait(false);
        }
        finally
        {
            CheckLock.Release();
            await ctx.RemoveReactionAsync(Config.Reactions.PleaseWait).ConfigureAwait(false);
        }
    }

    [Command("raid")]
    [Description("Kick known raiders")]
    public static async ValueTask Raid(TextCommandContext ctx)
    {
        if (!await CheckLock.WaitAsync(0).ConfigureAwait(false))
        {
            await ctx.Channel.SendMessageAsync("Another check is already in progress").ConfigureAwait(false);
            return;
        }

        try
        {
            await ctx.ReactWithAsync(Config.Reactions.PleaseWait).ConfigureAwait(false);
            var result = new StringBuilder("List of users:").AppendLine();
            var headerLength = result.Length;
            var members = GetMembers(ctx.Client);
            foreach (var member in members)
                try
                {
                    var displayName = member.DisplayName;
                    if (!UsernameRaidMonitor.NeedsKick(displayName))
                        continue;

                    try
                    {
                        await member.RemoveAsync("Anti Raid").ConfigureAwait(false);
                        result.AppendLine($"{member.Username} have been automatically kicked");
                    }
                    catch (Exception e)
                    {
                        Config.Log.Warn(e, $"Failed to kick member {member.GetUsernameWithNickname()}");
                    }
                }
                catch (Exception e)
                {
                    Config.Log.Warn(e, $"Failed to audit username for {member.Id}");
                }
            if (result.Length == headerLength)
                result.AppendLine("No naughty users 🎉");
            await ctx.SendAutosplitMessageAsync(result, blockStart: "", blockEnd: "").ConfigureAwait(false);
        }
        catch (Exception e)
        {
            var msg = "Failed to check display names for raids for all guild members";
            Config.Log.Warn(e, msg);
            await ctx.ReactWithAsync(Config.Reactions.Failure, msg).ConfigureAwait(false);
        }
        finally
        {
            CheckLock.Release();
            await ctx.RemoveReactionAsync(Config.Reactions.PleaseWait).ConfigureAwait(false);
        }
    }

    [Command("zalgo"), TextAlias("diacritics")]
    [Description("Checks every member's display name for discord and rule #7 requirements")]
    public static async ValueTask Zalgo(TextCommandContext ctx)
    {
        if (!await CheckLock.WaitAsync(0).ConfigureAwait(false))
        {
            await ctx.Channel.SendMessageAsync("Another check is already in progress").ConfigureAwait(false);
            return;
        }

        try
        {
            await ctx.ReactWithAsync(Config.Reactions.PleaseWait).ConfigureAwait(false);
            var result = new StringBuilder("List of users who do not meet Rule #7 requirements:").AppendLine();
            var headerLength = result.Length;
            var members = GetMembers(ctx.Client);
            foreach (var member in members)
                try
                {
                    var displayName = member.DisplayName;
                    if (!await UsernameZalgoMonitor.NeedsRenameAsync(displayName).ConfigureAwait(false))
                        continue;

                    var nickname = await UsernameZalgoMonitor.StripZalgoAsync(displayName, member.Username, member.Id).ConfigureAwait(false);
                    nickname = nickname.Sanitize();
                    try
                    {
                        await member.ModifyAsync(m => m.Nickname = nickname).ConfigureAwait(false);
                        result.AppendLine($"{member.Mention} have been automatically renamed from {displayName} to {nickname} according Rule #7");
                    }
                    catch (Exception e)
                    {
                        Config.Log.Warn(e, $"Failed to rename member {member.GetUsernameWithNickname()}");
                        result.AppendLine($"{member.Mention} please change your nickname according to Rule #7 (suggestion: {nickname})");
                    }
                }
                catch (Exception e)
                {
                    Config.Log.Warn(e, $"Failed to audit username for {member.Id}");
                }
            if (result.Length == headerLength)
                result.AppendLine("No naughty users 🎉");
            await ctx.SendAutosplitMessageAsync(result, blockStart: "", blockEnd: "").ConfigureAwait(false);
        }
        catch (Exception e)
        {
            var msg = "Failed to check display names for zalgo for all guild members";
            Config.Log.Warn(e, msg);
            await ctx.ReactWithAsync(Config.Reactions.Failure, msg).ConfigureAwait(false);
        }
        finally
        {
            CheckLock.Release();
            await ctx.RemoveReactionAsync(Config.Reactions.PleaseWait).ConfigureAwait(false);
        }
    }

#if DEBUG
    [Command("locales"), TextAlias("locale", "languages", "language", "lang", "loc")]
    public static async ValueTask UserLocales(TextCommandContext ctx)
    {
#pragma warning disable VSTHRD103
        if (!CheckLock.Wait(0))
#pragma warning restore VSTHRD103
        {
            await ctx.Channel.SendMessageAsync("Another check is already in progress").ConfigureAwait(false);
            return;
        }

        try
        {
            await ctx.ReactWithAsync(Config.Reactions.PleaseWait).ConfigureAwait(false);
            var members = GetMembers(ctx.Client);
            var stats = new Dictionary<string, int>();
            foreach (var m in members)
            {
                var loc = m.Locale ?? "Unknown";
                if (stats.ContainsKey(loc))
                    stats[loc]++;
                else
                    stats[loc] = 1;
            }
            var table = new AsciiTable(
                new AsciiColumn("Locale"),
                new AsciiColumn("Count", alignToRight: true),
                new AsciiColumn("%", alignToRight: true)
            );
            var total = stats.Values.Sum();
            foreach (var lang in stats.OrderByDescending(l => l.Value).ThenBy(l => l.Key))
                table.Add(lang.Key, lang.Value.ToString(), $"{100.0 * lang.Value / total:0.00}%");
            await ctx.SendAutosplitMessageAsync(new StringBuilder().AppendLine("Member locale stats:").Append(table)).ConfigureAwait(false);
        }
        catch (Exception e)
        {
            Config.Log.Warn(e, "Failed to get locale stats");
            await ctx.ReactWithAsync(Config.Reactions.Failure, "Failed to get locale stats").ConfigureAwait(false);
        }
        finally
        {
            CheckLock.Release();
            await ctx.RemoveReactionAsync(Config.Reactions.PleaseWait).ConfigureAwait(false);
        }
    }
#endif

    private static List<DiscordMember> GetMembers(DiscordClient client)
    {
        //owner -> white name
        //newbs -> veterans
        return client.Guilds.Select(g => g.Value.GetAllMembersAsync())
            .SelectMany(l => l.ToList())
            .OrderByDescending(m => m.Hierarchy)
            .ThenByDescending(m => m.JoinedAt)
            .ToList();
    }

    private static async void SpoofingCheck(TextCommandContext ctx)
    {
        if (!CheckLock.Wait(0))
        {
            await ctx.Channel.SendMessageAsync("Another check is already in progress").ConfigureAwait(false);
            return;
        }

        try
        {
            await ctx.ReactWithAsync(Config.Reactions.PleaseWait).ConfigureAwait(false);
            var members = GetMembers(ctx.Client);
            if (members.Count < 2)
                return;

            var result = new StringBuilder("List of potential impersonators → victims:").AppendLine();
            var headerLength = result.Length;
            var checkedMembers = new List<DiscordMember>(members.Count) {members[0]};
            for (var i = 1; i < members.Count; i++)
            {
                var member = members[i];
                var victims = UsernameSpoofMonitor.GetPotentialVictims(ctx.Client, member, true, true, checkedMembers);
                if (victims.Any())
                    result.Append(member.GetMentionWithNickname()).Append(" → ").AppendLine(string.Join(", ", victims.Select(m => m.GetMentionWithNickname())));
                checkedMembers.Add(member);
            }

            await using var compressedStream = Config.MemoryStreamManager.GetStream();
            await using var uncompressedStream = Config.MemoryStreamManager.GetStream();
            await using (var writer = new StreamWriter(uncompressedStream, new UTF8Encoding(false), 4096, true))
            {
                await writer.WriteAsync(result.ToString()).ConfigureAwait(false);
                await writer.FlushAsync().ConfigureAwait(false);
            }
            uncompressedStream.Seek(0, SeekOrigin.Begin);
            if (result.Length <= headerLength)
            {
                await ctx.Channel.SendMessageAsync("No potential name spoofing was detected").ConfigureAwait(false);
                return;
            }

            if (uncompressedStream.Length <= ctx.GetAttachmentSizeLimit())
            {
                await ctx.Channel.SendMessageAsync(new DiscordMessageBuilder().AddFile("spoofing_check_results.txt", uncompressedStream)).ConfigureAwait(false);
                return;
            }

            await using (var gzip = new GZipStream(compressedStream, CompressionLevel.Optimal, true))
            {
                await uncompressedStream.CopyToAsync(gzip).ConfigureAwait(false);
                gzip.Flush();
            }
            compressedStream.Seek(0, SeekOrigin.Begin);
            if (compressedStream.Length <= ctx.GetAttachmentSizeLimit())
                await ctx.Channel.SendMessageAsync(new DiscordMessageBuilder().AddFile("spoofing_check_results.txt.gz", compressedStream)).ConfigureAwait(false);
            else
                await ctx.Channel.SendMessageAsync($"Dump is too large: {compressedStream.Length} bytes").ConfigureAwait(false);
        }
        catch (Exception e)
        {
            Config.Log.Error(e);
            //should be extra careful, as async void will run on a thread pull, and will terminate the whole application with an uncaught exception
            try { await ctx.ReactWithAsync(Config.Reactions.Failure, "(X_X)").ConfigureAwait(false); } catch { }
        }
        finally
        {
            CheckLock.Release();
            await ctx.RemoveReactionAsync(Config.Reactions.PleaseWait).ConfigureAwait(false);
        }
    }
}
