// 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.API.Objects;
using Remora.Discord.Commands.Attributes;
using Remora.Discord.Commands.Feedback.Messages;
using Remora.Discord.Commands.Feedback.Services;
using Remora.Discord.Commands.Services;
using Remora.Discord.Extensions.Embeds;
using Remora.Discord.Interactivity;
using Remora.Discord.Interactivity.Services;
using Remora.Rest.Core;
using Remora.Results;
using DbGuild = Catalogger.Backend.Database.Models.Guild;
using IResult = Remora.Results.IResult;

namespace Catalogger.Backend.Bot.Commands;

public class ChannelCommands(
    ILogger logger,
    Config config,
    GuildRepository guildRepository,
    GuildCache guildCache,
    ChannelCache channelCache,
    IMemberCache memberCache,
    IFeedbackService feedbackService,
    ContextInjectionService contextInjection,
    InMemoryDataService<Snowflake, ChannelCommandData> dataService,
    PermissionResolverService permissionResolver
) : CommandGroup
{
    private readonly ILogger _logger = logger.ForContext<ChannelCommands>();

    private static readonly DiscordPermission[] RequiredGuildPermissions =
    [
        DiscordPermission.ManageGuild,
        DiscordPermission.ViewAuditLog,
    ];

    // TODO: i hate this
    [Command("check-permissions")]
    [Description(
        "Check for any permission issues that would prevent Catalogger from sending logs."
    )]
    [DiscordDefaultMemberPermissions(DiscordPermission.ManageGuild)]
    public async Task<IResult> CheckPermissionsAsync()
    {
        var (userId, guildId) = contextInjection.GetUserAndGuild();
        if (!guildCache.TryGet(guildId, out var guild))
            throw new CataloggerError("Guild not in cache");

        var embed = new EmbedBuilder().WithTitle($"Permission check for {guild.Name}");

        var botUser = await memberCache.TryGetAsync(
            guildId,
            DiscordSnowflake.New(config.Discord.ApplicationId)
        );
        var currentUser = await memberCache.TryGetAsync(guildId, userId);
        if (botUser == null || currentUser == null)
            throw new CataloggerError("Bot member or invoking member not found in cache");

        // We don't want to check categories or threads
        var guildChannels = channelCache
            .GuildChannels(guildId)
            .Where(c =>
                c.Type
                    is not (
                        ChannelType.GuildCategory
                        or ChannelType.PublicThread
                        or ChannelType.PrivateThread
                        or ChannelType.AnnouncementThread
                    )
            )
            .ToList();

        // We'll only check channels the user can see, to not leak any information.
        var checkChannels = guildChannels
            .Where(c =>
            {
                var perms = permissionResolver.GetChannelPermissions(guildId, currentUser, c);
                return perms.HasPermission(DiscordPermission.ViewChannel)
                    || perms.HasPermission(DiscordPermission.Administrator);
            })
            .ToList();

        var ignoredChannels = guildChannels.Count - checkChannels.Count;
        if (ignoredChannels != 0)
            embed = embed.WithFooter(
                $"{ignoredChannels} channel(s) were ignored as you do not have access to them"
            );

        var guildPerms = permissionResolver.GetGuildPermissions(guildId, botUser);
        // If the bot has admin perms, we can ignore the rest--we'll never get permission errors
        if (guildPerms.HasPermission(DiscordPermission.Administrator))
        {
            return await feedbackService.ReplyAsync(
                embeds:
                [
                    embed
                        .WithColour(DiscordUtils.Green)
                        .WithDescription("No issues found, all channels can be logged to and from!")
                        .Build()
                        .GetOrThrow(),
                ]
            );
        }

        var missingGuildPerms = string.Join(
            ", ",
            RequiredGuildPermissions.Where(p => !guildPerms.HasPermission(p))
        );
        if (!string.IsNullOrWhiteSpace(missingGuildPerms))
            embed.AddField("Server-level permissions", missingGuildPerms);

        var missingManageChannel = new List<Snowflake>();
        var missingSendMessages = new List<Snowflake>();
        var missingViewChannel = new List<Snowflake>();
        var missingReadMessageHistory = new List<Snowflake>();
        var missingManageWebhooks = new List<Snowflake>();

        foreach (var channel in checkChannels)
        {
            var channelPerms = permissionResolver.GetChannelPermissions(guildId, botUser, channel);
            if (!channelPerms.HasPermission(DiscordPermission.ManageChannels))
                missingManageChannel.Add(channel.ID);
            if (!channelPerms.HasPermission(DiscordPermission.SendMessages))
                missingSendMessages.Add(channel.ID);
            if (!channelPerms.HasPermission(DiscordPermission.ViewChannel))
                missingViewChannel.Add(channel.ID);
            if (!channelPerms.HasPermission(DiscordPermission.ReadMessageHistory))
                missingReadMessageHistory.Add(channel.ID);
            if (!channelPerms.HasPermission(DiscordPermission.ManageWebhooks))
                missingManageWebhooks.Add(channel.ID);
        }

        if (missingManageChannel.Count != 0)
            embed.AddField(
                "Manage Channel",
                string.Join("\n", missingManageChannel.Select(id => $"<#{id}>"))
            );
        if (missingSendMessages.Count != 0)
            embed.AddField(
                "Send Messages",
                string.Join("\n", missingSendMessages.Select(id => $"<#{id}>"))
            );
        if (missingReadMessageHistory.Count != 0)
            embed.AddField(
                "Read Message History",
                string.Join("\n", missingReadMessageHistory.Select(id => $"<#{id}>"))
            );
        if (missingViewChannel.Count != 0)
            embed.AddField(
                "View Channel",
                string.Join("\n", missingViewChannel.Select(id => $"<#{id}>"))
            );
        if (missingManageWebhooks.Count != 0)
            embed.AddField(
                "Manage Webhooks",
                string.Join("\n", missingManageWebhooks.Select(id => $"<#{id}>"))
            );

        if (embed.Fields.Count == 0)
        {
            embed = embed
                .WithColour(DiscordUtils.Green)
                .WithDescription("No issues found, all channels can be logged to and from!");
        }
        else
        {
            embed = embed
                .WithColour(DiscordUtils.Red)
                .WithDescription("Permission issues found, please fix them and try again.");
        }

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

    [Command("configure-channels")]
    [Description("Configure log channels for this server.")]
    [DiscordDefaultMemberPermissions(DiscordPermission.ManageGuild)]
    public async Task<IResult> ConfigureChannelsAsync()
    {
        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 (embeds, components) = BuildRootMenu(guildChannels, guild, guildConfig);

        var msg = await feedbackService
            .SendContextualAsync(
                embeds: embeds,
                options: new FeedbackMessageOptions(MessageComponents: components)
            )
            .GetOrThrow();

        dataService.TryAddData(msg.ID, new ChannelCommandData(userId, CurrentPage: null));

        return Result.Success;
    }

    public static IStringSelectComponent LogTypeSelect =>
        new StringSelectComponent(
            CustomID: CustomIDHelpers.CreateSelectMenuID("select-log-type"),
            MinValues: 1,
            MaxValues: 1,
            Options:
            [
                new SelectOption(
                    Label: "Server changes",
                    Value: nameof(LogChannelType.GuildUpdate)
                ),
                new SelectOption(
                    Label: "Emoji changes",
                    Value: nameof(LogChannelType.GuildEmojisUpdate)
                ),
                new SelectOption(Label: "New roles", Value: nameof(LogChannelType.GuildRoleCreate)),
                new SelectOption(
                    Label: "Edited roles",
                    Value: nameof(LogChannelType.GuildRoleUpdate)
                ),
                new SelectOption(
                    Label: "Deleted roles",
                    Value: nameof(LogChannelType.GuildRoleDelete)
                ),
                new SelectOption(
                    Label: "New channels",
                    Value: nameof(LogChannelType.ChannelCreate)
                ),
                new SelectOption(
                    Label: "Edited channels",
                    Value: nameof(LogChannelType.ChannelUpdate)
                ),
                new SelectOption(
                    Label: "Deleted channels",
                    Value: nameof(LogChannelType.ChannelDelete)
                ),
                new SelectOption(
                    Label: "Members joining",
                    Value: nameof(LogChannelType.GuildMemberAdd)
                ),
                new SelectOption(
                    Label: "Members leaving",
                    Value: nameof(LogChannelType.GuildMemberRemove)
                ),
                new SelectOption(
                    Label: "Member role changes",
                    Value: nameof(LogChannelType.GuildMemberUpdate)
                ),
                new SelectOption(
                    Label: "Key role changes",
                    Value: nameof(LogChannelType.GuildKeyRoleUpdate)
                ),
                new SelectOption(
                    Label: "Member name changes",
                    Value: nameof(LogChannelType.GuildMemberNickUpdate)
                ),
                new SelectOption(
                    Label: "Member avatar changes",
                    Value: nameof(LogChannelType.GuildMemberAvatarUpdate)
                ),
                new SelectOption(
                    Label: "Timeouts",
                    Value: nameof(LogChannelType.GuildMemberTimeout)
                ),
                new SelectOption(Label: "Kicks", Value: nameof(LogChannelType.GuildMemberKick)),
                new SelectOption(Label: "Bans", Value: nameof(LogChannelType.GuildBanAdd)),
                new SelectOption(Label: "Unbans", Value: nameof(LogChannelType.GuildBanRemove)),
                new SelectOption(Label: "New invites", Value: nameof(LogChannelType.InviteCreate)),
                new SelectOption(
                    Label: "Deleted invites",
                    Value: nameof(LogChannelType.InviteDelete)
                ),
                new SelectOption(
                    Label: "Edited messages",
                    Value: nameof(LogChannelType.MessageUpdate)
                ),
                new SelectOption(
                    Label: "Deleted messages",
                    Value: nameof(LogChannelType.MessageDelete)
                ),
                new SelectOption(
                    Label: "Bulk deleted messages",
                    Value: nameof(LogChannelType.MessageDeleteBulk)
                ),
            ]
        );

    public static (List<IEmbed>, List<IMessageComponent>) BuildRootMenu(
        List<IChannel> guildChannels,
        IGuild guild,
        DbGuild guildConfig
    )
    {
        List<IEmbed> embeds =
        [
            new Embed(
                Title: $"Log channels for {guild.Name}",
                Description: "Press one of the buttons below to change the channel for that log type.",
                Colour: DiscordUtils.Purple,
                Fields: new[]
                {
                    new EmbedField(
                        "Server changes",
                        PrettyChannelString(guildConfig.Channels.GuildUpdate),
                        true
                    ),
                    new EmbedField(
                        "Emoji changes",
                        PrettyChannelString(guildConfig.Channels.GuildEmojisUpdate),
                        true
                    ),
                    new EmbedField(
                        "New roles",
                        PrettyChannelString(guildConfig.Channels.GuildRoleCreate),
                        true
                    ),
                    new EmbedField(
                        "Edited roles",
                        PrettyChannelString(guildConfig.Channels.GuildRoleUpdate),
                        true
                    ),
                    new EmbedField(
                        "Deleted roles",
                        PrettyChannelString(guildConfig.Channels.GuildRoleDelete),
                        true
                    ),
                    new EmbedField(
                        "New channels",
                        PrettyChannelString(guildConfig.Channels.ChannelCreate),
                        true
                    ),
                    new EmbedField(
                        "Edited channels",
                        PrettyChannelString(guildConfig.Channels.ChannelUpdate),
                        true
                    ),
                    new EmbedField(
                        "Deleted channels",
                        PrettyChannelString(guildConfig.Channels.ChannelDelete),
                        true
                    ),
                    new EmbedField(
                        "Members joining",
                        PrettyChannelString(guildConfig.Channels.GuildMemberAdd),
                        true
                    ),
                    new EmbedField(
                        "Members leaving",
                        PrettyChannelString(guildConfig.Channels.GuildMemberRemove),
                        true
                    ),
                    new EmbedField(
                        "Member role changes",
                        PrettyChannelString(guildConfig.Channels.GuildMemberUpdate),
                        true
                    ),
                    new EmbedField(
                        "Key role changes",
                        PrettyChannelString(guildConfig.Channels.GuildKeyRoleUpdate),
                        true
                    ),
                    new EmbedField(
                        "Member name changes",
                        PrettyChannelString(guildConfig.Channels.GuildMemberNickUpdate),
                        true
                    ),
                    new EmbedField(
                        "Member avatar changes",
                        PrettyChannelString(guildConfig.Channels.GuildMemberAvatarUpdate),
                        true
                    ),
                    new EmbedField(
                        "Timeouts",
                        PrettyChannelString(guildConfig.Channels.GuildMemberTimeout),
                        true
                    ),
                    new EmbedField(
                        "Kicks",
                        PrettyChannelString(guildConfig.Channels.GuildMemberKick),
                        true
                    ),
                    new EmbedField(
                        "Bans",
                        PrettyChannelString(guildConfig.Channels.GuildBanAdd),
                        true
                    ),
                    new EmbedField(
                        "Unbans",
                        PrettyChannelString(guildConfig.Channels.GuildBanRemove),
                        true
                    ),
                    new EmbedField(
                        "New invites",
                        PrettyChannelString(guildConfig.Channels.InviteCreate),
                        true
                    ),
                    new EmbedField(
                        "Deleted invites",
                        PrettyChannelString(guildConfig.Channels.InviteDelete),
                        true
                    ),
                    new EmbedField(
                        "Edited messages",
                        PrettyChannelString(guildConfig.Channels.MessageUpdate),
                        true
                    ),
                    new EmbedField(
                        "Deleted messages",
                        PrettyChannelString(guildConfig.Channels.MessageDelete),
                        true
                    ),
                    new EmbedField(
                        "Bulk deleted messages",
                        PrettyChannelString(guildConfig.Channels.MessageDeleteBulk),
                        true
                    ),
                }
            ),
        ];

        List<IMessageComponent> components =
        [
            new ActionRowComponent([LogTypeSelect]),
            new ActionRowComponent(
                [
                    new ButtonComponent(
                        ButtonComponentStyle.Secondary,
                        Label: "Close",
                        CustomID: CustomIDHelpers.CreateButtonIDWithState(
                            "config-channels",
                            "close"
                        )
                    ),
                ]
            ),
        ];

        return (embeds, components);

        string PrettyChannelString(ulong id)
        {
            if (id == 0)
                return "Not set";
            if (guildChannels.All(c => c.ID != id))
                return $"unknown channel {id}";
            return $"<#{id}>";
        }
    }

    public static string PrettyLogTypeName(LogChannelType type) =>
        type switch
        {
            LogChannelType.GuildUpdate => "Server changes",
            LogChannelType.GuildEmojisUpdate => "Emoji changes",
            LogChannelType.GuildRoleCreate => "New roles",
            LogChannelType.GuildRoleUpdate => "Edited roles",
            LogChannelType.GuildRoleDelete => "Deleted roles",
            LogChannelType.ChannelCreate => "New channels",
            LogChannelType.ChannelUpdate => "Edited channels",
            LogChannelType.ChannelDelete => "Deleted channels",
            LogChannelType.GuildMemberAdd => "Members joining",
            LogChannelType.GuildMemberUpdate => "Member role changes",
            LogChannelType.GuildKeyRoleUpdate => "Key role changes",
            LogChannelType.GuildMemberNickUpdate => "Member name changes",
            LogChannelType.GuildMemberAvatarUpdate => "Member avatar changes",
            LogChannelType.GuildMemberTimeout => "Timeouts",
            LogChannelType.GuildMemberRemove => "Members leaving",
            LogChannelType.GuildMemberKick => "Kicks",
            LogChannelType.GuildBanAdd => "Bans",
            LogChannelType.GuildBanRemove => "Unbans",
            LogChannelType.InviteCreate => "New invites",
            LogChannelType.InviteDelete => "Deleted invites",
            LogChannelType.MessageUpdate => "Edited messages",
            LogChannelType.MessageDelete => "Deleted messages",
            LogChannelType.MessageDeleteBulk => "Bulk deleted messages",
            _ => throw new ArgumentOutOfRangeException(
                nameof(type),
                type,
                "Invalid LogChannelType value"
            ),
        };
}
