// 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 System.ComponentModel;
using Catalogger.Backend.Cache;
using Catalogger.Backend.Cache.InMemoryCache;
using Catalogger.Backend.Database.Repositories;
using Catalogger.Backend.Extensions;
using Catalogger.Backend.Services;
using Remora.Commands.Attributes;
using Remora.Commands.Groups;
using Remora.Discord.API;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.Commands.Attributes;
using Remora.Discord.Commands.Feedback.Services;
using Remora.Discord.Commands.Services;
using Remora.Discord.Extensions.Embeds;
using Remora.Rest.Core;
using IResult = Remora.Results.IResult;

namespace Catalogger.Backend.Bot.Commands;

[Group("ignore-messages")]
[Description("Manage users, roles, and channels whose messages are not logged.")]
[DiscordDefaultMemberPermissions(DiscordPermission.ManageGuild)]
public partial class IgnoreMessageCommands : CommandGroup
{
    [Group("channels")]
    public class Channels(
        GuildRepository guildRepository,
        IMemberCache memberCache,
        GuildCache guildCache,
        ChannelCache channelCache,
        PermissionResolverService permissionResolver,
        ContextInjectionService contextInjection,
        FeedbackService feedbackService
    ) : CommandGroup
    {
        [Command("add")]
        [Description("Add a channel to the list of ignored channels.")]
        public async Task<IResult> AddIgnoredChannelAsync(
            [ChannelTypes(
                ChannelType.GuildCategory,
                ChannelType.GuildText,
                ChannelType.GuildAnnouncement,
                ChannelType.GuildForum,
                ChannelType.GuildMedia,
                ChannelType.GuildVoice,
                ChannelType.GuildStageVoice
            )]
            [Description("The channel to ignore")]
                IChannel channel
        )
        {
            var (_, guildId) = contextInjection.GetUserAndGuild();
            var guildConfig = await guildRepository.GetAsync(guildId);

            if (guildConfig.Messages.IgnoredChannels.Contains(channel.ID.Value))
                return await feedbackService.ReplyAsync(
                    "That channel is already being ignored.",
                    isEphemeral: true
                );

            guildConfig.Messages.IgnoredChannels.Add(channel.ID.Value);
            await guildRepository.UpdateConfigAsync(guildId, guildConfig);

            return await feedbackService.ReplyAsync(
                $"Successfully added {(channel.Type == ChannelType.GuildCategory ? channel.Name : $"<#{channel.ID}>")} to the list of ignored channels."
            );
        }

        [Command("remove")]
        [Description("Remove a channel from the list of ignored channels.")]
        public async Task<IResult> RemoveIgnoredChannelAsync(
            [Description("The channel to stop ignoring")] IChannel channel
        )
        {
            var (_, guildId) = contextInjection.GetUserAndGuild();
            var guildConfig = await guildRepository.GetAsync(guildId);

            if (!guildConfig.Messages.IgnoredChannels.Contains(channel.ID.Value))
                return await feedbackService.ReplyAsync(
                    "That channel is already not ignored.",
                    isEphemeral: true
                );

            guildConfig.Messages.IgnoredChannels.Remove(channel.ID.Value);
            await guildRepository.UpdateConfigAsync(guildId, guildConfig);

            return await feedbackService.ReplyAsync(
                $"Successfully removed {(channel.Type == ChannelType.GuildCategory ? channel.Name : $"<#{channel.ID}>")} from the list of ignored channels."
            );
        }

        [Command("list")]
        [Description("List channels ignored for logging.")]
        public async Task<IResult> ListIgnoredChannelsAsync()
        {
            var (userId, guildId) = contextInjection.GetUserAndGuild();
            if (!guildCache.TryGet(guildId, out var guild))
                throw new CataloggerError("Guild not in cache");

            var guildChannels = channelCache.GuildChannels(guildId).ToList();
            var guildConfig = await guildRepository.GetAsync(guildId);

            var member = await memberCache.TryGetAsync(guildId, userId);
            if (member == null)
                throw new CataloggerError("Executing member not found");

            var ignoredChannels = guildConfig
                .Messages.IgnoredChannels.Select(id =>
                {
                    var channel = guildChannels.FirstOrDefault(c => c.ID.Value == id);
                    if (channel == null)
                        return new IgnoredChannel(
                            IgnoredChannelType.Unknown,
                            DiscordSnowflake.New(id)
                        );

                    var type = channel.Type switch
                    {
                        ChannelType.GuildCategory => IgnoredChannelType.Category,
                        _ => IgnoredChannelType.Base,
                    };

                    return new IgnoredChannel(
                        type,
                        channel.ID,
                        permissionResolver
                            .GetChannelPermissions(guildId, member, channel)
                            .HasPermission(DiscordPermission.ViewChannel)
                    );
                })
                .ToList();

            var embed = new EmbedBuilder()
                .WithTitle($"Ignored channels in {guild.Name}")
                .WithColour(DiscordUtils.Purple);

            var nonVisibleCategories = ignoredChannels.Count(c =>
                c is { Type: IgnoredChannelType.Category, CanSee: false }
            );
            var visibleCategories = ignoredChannels
                .Where(c => c is { Type: IgnoredChannelType.Category, CanSee: true })
                .ToList();

            if (nonVisibleCategories != 0 || visibleCategories.Count != 0)
            {
                var value = string.Join("\n", visibleCategories.Select(c => $"<#{c.Id}>"));
                if (nonVisibleCategories != 0)
                    value +=
                        $"\n\n{nonVisibleCategories} channel(s) are not shown as you do not have access to them.";

                embed.AddField("Categories", value);
            }

            var nonVisibleBase = ignoredChannels.Count(c =>
                c is { Type: IgnoredChannelType.Base, CanSee: false }
            );
            var visibleBase = ignoredChannels
                .Where(c => c is { Type: IgnoredChannelType.Base, CanSee: true })
                .ToList();

            if (nonVisibleBase != 0 || visibleBase.Count != 0)
            {
                var value = string.Join("\n", visibleBase.Select(c => $"<#{c.Id}>"));
                if (nonVisibleBase != 0)
                    value +=
                        $"\n\n{nonVisibleBase} channel(s) are not shown as you do not have access to them.";

                embed.AddField("Channels", value);
            }

            var unknownChannels = string.Join(
                "\n",
                ignoredChannels
                    .Where(c => c.Type == IgnoredChannelType.Unknown)
                    .Select(c => $"{c.Id} <#{c.Id}>")
            );
            if (!string.IsNullOrWhiteSpace(unknownChannels))
            {
                embed.AddField("Unknown", unknownChannels);
            }

            return await feedbackService.ReplyAsync(embeds: [embed.Build().GetOrThrow()]);
        }

        private record struct IgnoredChannel(
            IgnoredChannelType Type,
            Snowflake Id,
            bool CanSee = true
        );

        private enum IgnoredChannelType
        {
            Unknown,
            Base,
            Category,
        }
    }
}
