// Copyright (C) 2021-present sam (starshines.gay)
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

using Catalogger.Backend.Cache.InMemoryCache;
using Catalogger.Backend.Database.Repositories;
using Catalogger.Backend.Extensions;
using Catalogger.Backend.Services;
using Humanizer;
using Remora.Discord.API.Abstractions.Gateway.Events;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.Extensions.Embeds;
using Remora.Discord.Gateway.Responders;
using Remora.Rest.Core;
using Remora.Results;

namespace Catalogger.Backend.Bot.Responders.Channels;

public class ChannelUpdateResponder(
    ILogger logger,
    GuildRepository guildRepository,
    ChannelCache channelCache,
    RoleCache roleCache,
    UserCache userCache,
    WebhookExecutorService webhookExecutor
) : IResponder<IChannelUpdate>
{
    private readonly ILogger _logger = logger.ForContext<ChannelUpdateResponder>();

    public async Task<Result> RespondAsync(IChannelUpdate evt, CancellationToken ct = default)
    {
        try
        {
            if (!channelCache.TryGet(evt.ID, out var oldChannel))
            {
                _logger.Debug("Updated channel {ChannelId} wasn't in the cache", evt.ID);
                return Result.Success;
            }

            var guildConfig = await guildRepository.GetAsync(evt.GuildID);

            var builder = new EmbedBuilder()
                .WithTitle(
                    evt.Type switch
                    {
                        ChannelType.GuildVoice => "Voice channel edited",
                        ChannelType.GuildCategory => "Category channel edited",
                        ChannelType.GuildAnnouncement or ChannelType.GuildText =>
                            "Text channel edited",
                        _ => "Channel edited",
                    }
                )
                .WithColour(DiscordUtils.Blue)
                .WithFooter($"ID: {evt.ID} | Name: {evt.Name}")
                .WithCurrentTimestamp();

            if (oldChannel.ParentID != evt.ParentID)
            {
                var categoryUpdate = CategoryUpdate(
                    oldChannel.ParentID.OrDefault(),
                    evt.ParentID.OrDefault()
                );
                if (!string.IsNullOrWhiteSpace(categoryUpdate))
                    builder.AddField("Category", categoryUpdate);
            }

            if (oldChannel.Name != evt.Name)
                builder.AddField("Name", $"**Before:** {oldChannel.Name}\n**After:** {evt.Name}");

            if (oldChannel.Topic != evt.Topic)
            {
                var oldTopic = oldChannel.Topic.OrDefault() ?? "(none)";
                var newTopic = evt.Topic.OrDefault() ?? "(none)";

                var topicField = $"**Before:** {oldTopic}\n\n**After:** {newTopic}";
                if (topicField.Length > 1000)
                    topicField = topicField[..1000] + "…";

                builder.AddField("Description", topicField);
            }

            var oldOverrides = oldChannel.PermissionOverwrites.OrDefault() ?? [];
            var newOverrides = evt.PermissionOverwrites.OrDefault() ?? [];

            var addedOverrides = newOverrides
                .Where(o => oldOverrides.All(o2 => o.ID != o2.ID))
                .ToList();
            var removedOverrides = oldOverrides
                .Where(o => newOverrides.All(o2 => o.ID != o2.ID))
                .ToList();
            // Overrides filtered to ones that exist in both lists, but have different allow or deny values
            var editedOverrides = newOverrides.Where(o =>
                oldOverrides.Any(o2 =>
                    o.ID == o2.ID
                    && (o.Allow.Value != o2.Allow.Value || o.Deny.Value != o2.Deny.Value)
                )
            );

            if (addedOverrides.Count != 0)
            {
                var addedOverrideNames = new List<string>();
                foreach (var o in addedOverrides)
                {
                    if (o.Type is PermissionOverwriteType.Member)
                    {
                        var user = await userCache.GetUserAsync(o.ID);
                        addedOverrideNames.Add(user != null ? $"<@{user.ID}>" : $"user {o.ID}");
                    }
                    else
                    {
                        addedOverrideNames.Add(
                            roleCache.TryGet(o.ID, out var role) ? role.Name : $"role {o.ID}"
                        );
                        break;
                    }

                    builder.AddField("Added overrides", string.Join(", ", addedOverrideNames));
                }
            }

            if (removedOverrides.Count != 0)
            {
                var removedOverrideNames = new List<string>();
                foreach (var o in removedOverrides)
                {
                    if (o.Type is PermissionOverwriteType.Member)
                    {
                        var user = await userCache.GetUserAsync(o.ID);
                        removedOverrideNames.Add(user != null ? $"<@{user.ID}>" : $"user {o.ID}");
                    }
                    else
                    {
                        removedOverrideNames.Add(
                            roleCache.TryGet(o.ID, out var role) ? role.Name : $"role {o.ID}"
                        );
                        break;
                    }
                }

                builder.AddField("Removed overrides", string.Join(", ", removedOverrideNames));
            }

            foreach (var overwrite in editedOverrides)
            {
                var perms = string.Join(
                    "\n",
                    PermissionUpdate(oldOverrides.First(o => o.ID == overwrite.ID), overwrite)
                );
                if (string.IsNullOrWhiteSpace(perms))
                    continue;

                builder.AddField(await OverwriteName(overwrite), perms.Trim());
            }

            foreach (var overwrite in addedOverrides)
            {
                var embedFieldValue = "";
                if (overwrite.Allow.GetPermissions().Count != 0)
                    embedFieldValue += $"\u2705 {overwrite.Allow.ToPrettyString()}";
                if (overwrite.Deny.GetPermissions().Count != 0)
                    embedFieldValue += $"\n\n\u274c {overwrite.Deny.ToPrettyString()}";
                if (string.IsNullOrWhiteSpace(embedFieldValue))
                    continue;
                builder.AddField(await OverwriteName(overwrite), embedFieldValue.Trim());
            }

            // Sometimes we get channel update events for channels that didn't actually have anything loggable change.
            // If that happens, there will be no embed fields, so just check for that
            if (builder.Fields.Count == 0)
                return Result.Success;

            webhookExecutor.QueueLog(
                webhookExecutor.GetLogChannel(
                    guildConfig,
                    LogChannelType.ChannelUpdate,
                    channelId: evt.ID
                ),
                builder.Build().GetOrThrow()
            );

            return Result.Success;
        }
        finally
        {
            channelCache.Set(evt);
        }
    }

    private string CategoryUpdate(Snowflake? oldCategory, Snowflake? newCategory)
    {
        var value = "";
        if (oldCategory != null && channelCache.TryGet(oldCategory.Value, out var oldChannel))
            value += $"**Before:** {oldChannel.Name}";
        if (newCategory != null && channelCache.TryGet(newCategory.Value, out var newChannel))
            value += $"\n**After:** {newChannel.Name}";
        return value.Trim();
    }

    private async Task<string> OverwriteName(IPermissionOverwrite overwrite)
    {
        switch (overwrite.Type)
        {
            case PermissionOverwriteType.Role:
                return roleCache.TryGet(overwrite.ID, out var role)
                    ? $"Override for {role.Name}"
                    : $"Override for role {overwrite.ID}";
            case PermissionOverwriteType.Member:
                var user = await userCache.GetUserAsync(overwrite.ID);
                return user != null
                    ? $"Override for {user.Tag()}"
                    : $"Override for user {overwrite.ID}";
            default:
                throw new ArgumentOutOfRangeException(
                    nameof(overwrite),
                    overwrite.Type,
                    "Invalid PermissionOverwriteType"
                );
        }
    }

    private static IEnumerable<string> PermissionUpdate(
        IPermissionOverwrite oldOverwrite,
        IPermissionOverwrite newOverwrite
    )
    {
        foreach (var perm in Enum.GetValues<DiscordPermission>())
        {
            if (
                newOverwrite.Allow.HasPermission(perm)
                && !oldOverwrite.Allow.HasPermission(perm)
                && !oldOverwrite.Deny.HasPermission(perm)
            )
            {
                yield return $"\u2b1c \u279c \u2705 {perm.Humanize(LetterCasing.Title)}";
            }
            else if (
                newOverwrite.Deny.HasPermission(perm)
                && !oldOverwrite.Allow.HasPermission(perm)
                && !oldOverwrite.Deny.HasPermission(perm)
            )
            {
                yield return $"\u2b1c \u279c \u274c {perm.Humanize(LetterCasing.Title)}";
            }
            else if (
                newOverwrite.Allow.HasPermission(perm) && oldOverwrite.Deny.HasPermission(perm)
            )
            {
                yield return $"\u274c \u279c \u2705 {perm.Humanize(LetterCasing.Title)}";
            }
            else if (
                newOverwrite.Deny.HasPermission(perm) && oldOverwrite.Allow.HasPermission(perm)
            )
            {
                yield return $"\u2705 \u279c \u274c {perm.Humanize(LetterCasing.Title)}";
            }
            else if (
                !newOverwrite.Allow.HasPermission(perm)
                && !newOverwrite.Deny.HasPermission(perm)
                && oldOverwrite.Allow.HasPermission(perm)
            )
            {
                yield return $"\u2705 \u279c \u2b1c {perm.Humanize(LetterCasing.Title)}";
            }
            else if (
                !newOverwrite.Allow.HasPermission(perm)
                && !newOverwrite.Deny.HasPermission(perm)
                && oldOverwrite.Allow.HasPermission(perm)
            )
            {
                yield return $"\u274c \u279c \u2b1c {perm.Humanize(LetterCasing.Title)}";
            }
        }
    }
}
