// 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 Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Abstractions.Rest;
using Remora.Discord.API.Objects;
using Remora.Discord.Commands.Attributes;
using Remora.Discord.Commands.Contexts;
using Remora.Discord.Commands.Extensions;
using Remora.Discord.Commands.Feedback.Services;
using Remora.Discord.Commands.Services;
using Remora.Discord.Interactivity;
using Remora.Discord.Interactivity.Services;
using Remora.Rest.Core;
using Remora.Results;

namespace Catalogger.Backend.Bot.Commands;

public class ChannelCommandsComponents(
    ILogger logger,
    GuildRepository guildRepository,
    GuildCache guildCache,
    ChannelCache channelCache,
    ContextInjectionService contextInjection,
    IFeedbackService feedbackService,
    IDiscordRestInteractionAPI interactionApi,
    InMemoryDataService<Snowflake, ChannelCommandData> dataService
) : InteractionGroup
{
    private readonly ILogger _logger = logger.ForContext<ChannelCommandsComponents>();

    [SelectMenu("select-log-type")]
    [SuppressInteractionResponse(true)]
    public async Task<Result> OnMenuSelectionAsync(IReadOnlyList<string> values)
    {
        if (contextInjection.Context is not IInteractionCommandContext ctx)
            throw new CataloggerError("No context");
        if (!ctx.TryGetUserID(out var userId))
            throw new CataloggerError("No user ID in context");
        if (!ctx.Interaction.Message.TryGet(out var msg))
            throw new CataloggerError("No message ID in context");
        if (!ctx.TryGetGuildID(out var guildId))
            throw new CataloggerError("No guild ID in context");
        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 result = await dataService.LeaseDataAsync(msg.ID);
        await using var lease = result.GetOrThrow();
        if (lease.Data.UserId != userId)
        {
            return (Result)
                await feedbackService.ReplyAsync(
                    "This is not your configuration menu.",
                    isEphemeral: true
                );
        }

        var state = values[0];

        if (!Enum.TryParse<LogChannelType>(state, out var logChannelType))
            throw new CataloggerError($"Invalid config-channels state {state}");

        var channelId = WebhookExecutorService.GetDefaultLogChannel(guildConfig, logChannelType);
        string? channelMention;
        if (channelId is 0)
            channelMention = null;
        else if (guildChannels.All(c => c.ID != channelId))
            channelMention = $"unknown channel {channelId}";
        else
            channelMention = $"<#{channelId}>";

        List<IEmbed> embeds =
        [
            new Embed(
                Title: ChannelCommands.PrettyLogTypeName(logChannelType),
                Description: channelMention == null
                    ? "This event is not currently logged.\nTo start logging it somewhere, select a channel below."
                    : $"This event is currently set to log to {channelMention}."
                        + "\nTo change where it is logged, select a channel below."
                        + "\nTo disable logging this event entirely, select \"Stop logging\" below.",
                Colour: DiscordUtils.Purple
            ),
        ];

        List<IMessageComponent> components =
        [
            new ActionRowComponent(
                new[]
                {
                    new ChannelSelectComponent(
                        CustomID: CustomIDHelpers.CreateSelectMenuID("config-channels"),
                        ChannelTypes: new[] { ChannelType.GuildText }
                    ),
                }
            ),
            new ActionRowComponent(
                new[]
                {
                    new ButtonComponent(
                        ButtonComponentStyle.Danger,
                        Label: "Stop logging",
                        CustomID: CustomIDHelpers.CreateButtonIDWithState(
                            "config-channels",
                            "reset"
                        ),
                        IsDisabled: channelMention == null
                    ),
                    new ButtonComponent(
                        ButtonComponentStyle.Secondary,
                        Label: "Return to menu",
                        CustomID: CustomIDHelpers.CreateButtonIDWithState(
                            "config-channels",
                            "return"
                        )
                    ),
                }
            ),
        ];

        lease.Data = new ChannelCommandData(userId, CurrentPage: state);
        return await interactionApi.UpdateMessageAsync(
            ctx.Interaction,
            new InteractionMessageCallbackData(Embeds: embeds, Components: components)
        );
    }

    [Button("config-channels")]
    [SuppressInteractionResponse(true)]
    public async Task<Result> OnButtonPressedAsync(string state)
    {
        if (contextInjection.Context is not IInteractionCommandContext ctx)
            throw new CataloggerError("No context");
        if (!ctx.TryGetUserID(out var userId))
            throw new CataloggerError("No user ID in context");
        if (!ctx.Interaction.Message.TryGet(out var msg))
            throw new CataloggerError("No message ID in context");
        if (!ctx.TryGetGuildID(out var guildId))
            throw new CataloggerError("No guild ID in context");
        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 result = await dataService.LeaseDataAsync(msg.ID);
        await using var lease = result.GetOrThrow();
        if (lease.Data.UserId != userId)
        {
            return (Result)
                await feedbackService.ReplyAsync(
                    "This is not your configuration menu.",
                    isEphemeral: true
                );
        }

        switch (state)
        {
            case "close":
                return await interactionApi.UpdateMessageAsync(
                    ctx.Interaction,
                    new InteractionMessageCallbackData(Components: Array.Empty<IMessageComponent>())
                );
            case "reset":
                if (lease.Data.CurrentPage == null)
                    throw new CataloggerError("CurrentPage was null in reset button callback");
                if (!Enum.TryParse<LogChannelType>(lease.Data.CurrentPage, out var channelType))
                    throw new CataloggerError(
                        $"Invalid config-channels CurrentPage: '{lease.Data.CurrentPage}'"
                    );

                // TODO: figure out some way to make this less verbose?
                switch (channelType)
                {
                    case LogChannelType.GuildUpdate:
                        guildConfig.Channels.GuildUpdate = 0;
                        break;
                    case LogChannelType.GuildEmojisUpdate:
                        guildConfig.Channels.GuildEmojisUpdate = 0;
                        break;
                    case LogChannelType.GuildRoleCreate:
                        guildConfig.Channels.GuildRoleCreate = 0;
                        break;
                    case LogChannelType.GuildRoleUpdate:
                        guildConfig.Channels.GuildRoleUpdate = 0;
                        break;
                    case LogChannelType.GuildRoleDelete:
                        guildConfig.Channels.GuildRoleDelete = 0;
                        break;
                    case LogChannelType.ChannelCreate:
                        guildConfig.Channels.ChannelCreate = 0;
                        break;
                    case LogChannelType.ChannelUpdate:
                        guildConfig.Channels.ChannelUpdate = 0;
                        break;
                    case LogChannelType.ChannelDelete:
                        guildConfig.Channels.ChannelDelete = 0;
                        break;
                    case LogChannelType.GuildMemberAdd:
                        guildConfig.Channels.GuildMemberAdd = 0;
                        break;
                    case LogChannelType.GuildMemberUpdate:
                        guildConfig.Channels.GuildMemberUpdate = 0;
                        break;
                    case LogChannelType.GuildKeyRoleUpdate:
                        guildConfig.Channels.GuildKeyRoleUpdate = 0;
                        break;
                    case LogChannelType.GuildMemberNickUpdate:
                        guildConfig.Channels.GuildMemberNickUpdate = 0;
                        break;
                    case LogChannelType.GuildMemberAvatarUpdate:
                        guildConfig.Channels.GuildMemberAvatarUpdate = 0;
                        break;
                    case LogChannelType.GuildMemberRemove:
                        guildConfig.Channels.GuildMemberRemove = 0;
                        break;
                    case LogChannelType.GuildMemberTimeout:
                        guildConfig.Channels.GuildMemberTimeout = 0;
                        break;
                    case LogChannelType.GuildMemberKick:
                        guildConfig.Channels.GuildMemberKick = 0;
                        break;
                    case LogChannelType.GuildBanAdd:
                        guildConfig.Channels.GuildBanAdd = 0;
                        break;
                    case LogChannelType.GuildBanRemove:
                        guildConfig.Channels.GuildBanRemove = 0;
                        break;
                    case LogChannelType.InviteCreate:
                        guildConfig.Channels.InviteCreate = 0;
                        break;
                    case LogChannelType.InviteDelete:
                        guildConfig.Channels.InviteDelete = 0;
                        break;
                    case LogChannelType.MessageUpdate:
                        guildConfig.Channels.MessageUpdate = 0;
                        break;
                    case LogChannelType.MessageDelete:
                        guildConfig.Channels.MessageDelete = 0;
                        break;
                    case LogChannelType.MessageDeleteBulk:
                        guildConfig.Channels.MessageDeleteBulk = 0;
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }

                await guildRepository.UpdateConfigAsync(guildId, guildConfig);
                goto case "return";
            case "return":
                var (e, c) = ChannelCommands.BuildRootMenu(guildChannels, guild, guildConfig);
                await interactionApi.UpdateMessageAsync(
                    ctx.Interaction,
                    new InteractionMessageCallbackData(Embeds: e, Components: c)
                );
                lease.Data = new ChannelCommandData(userId, CurrentPage: null);
                return Result.Success;
        }

        return Result.Success;
    }

    [SelectMenu("config-channels")]
    [SuppressInteractionResponse(true)]
    public async Task<Result> OnMenuSelectionAsync(IReadOnlyList<IPartialChannel> channels)
    {
        if (contextInjection.Context is not IInteractionCommandContext ctx)
            throw new CataloggerError("No context");
        if (!ctx.TryGetUserID(out var userId))
            throw new CataloggerError("No user ID in context");
        if (!ctx.Interaction.Message.TryGet(out var msg))
            throw new CataloggerError("No message ID in context");
        if (!ctx.TryGetGuildID(out var guildId))
            throw new CataloggerError("No guild ID in context");
        if (!guildCache.TryGet(guildId, out var guild))
            throw new CataloggerError("Guild not in cache");
        var guildConfig = await guildRepository.GetAsync(guildId);
        var channelId = channels[0].ID.ToUlong();

        var result = await dataService.LeaseDataAsync(msg.ID);
        await using var lease = result.GetOrThrow();
        if (lease.Data.UserId != userId)
        {
            return (Result)
                await feedbackService.ReplyAsync(
                    "This is not your configuration menu.",
                    isEphemeral: true
                );
        }

        if (!Enum.TryParse<LogChannelType>(lease.Data.CurrentPage, out var channelType))
            throw new CataloggerError(
                $"Invalid config-channels CurrentPage '{lease.Data.CurrentPage}'"
            );

        switch (channelType)
        {
            case LogChannelType.GuildUpdate:
                guildConfig.Channels.GuildUpdate = channelId;
                break;
            case LogChannelType.GuildEmojisUpdate:
                guildConfig.Channels.GuildEmojisUpdate = channelId;
                break;
            case LogChannelType.GuildRoleCreate:
                guildConfig.Channels.GuildRoleCreate = channelId;
                break;
            case LogChannelType.GuildRoleUpdate:
                guildConfig.Channels.GuildRoleUpdate = channelId;
                break;
            case LogChannelType.GuildRoleDelete:
                guildConfig.Channels.GuildRoleDelete = channelId;
                break;
            case LogChannelType.ChannelCreate:
                guildConfig.Channels.ChannelCreate = channelId;
                break;
            case LogChannelType.ChannelUpdate:
                guildConfig.Channels.ChannelUpdate = channelId;
                break;
            case LogChannelType.ChannelDelete:
                guildConfig.Channels.ChannelDelete = channelId;
                break;
            case LogChannelType.GuildMemberAdd:
                guildConfig.Channels.GuildMemberAdd = channelId;
                break;
            case LogChannelType.GuildMemberUpdate:
                guildConfig.Channels.GuildMemberUpdate = channelId;
                break;
            case LogChannelType.GuildKeyRoleUpdate:
                guildConfig.Channels.GuildKeyRoleUpdate = channelId;
                break;
            case LogChannelType.GuildMemberNickUpdate:
                guildConfig.Channels.GuildMemberNickUpdate = channelId;
                break;
            case LogChannelType.GuildMemberAvatarUpdate:
                guildConfig.Channels.GuildMemberAvatarUpdate = channelId;
                break;
            case LogChannelType.GuildMemberRemove:
                guildConfig.Channels.GuildMemberRemove = channelId;
                break;
            case LogChannelType.GuildMemberTimeout:
                guildConfig.Channels.GuildMemberTimeout = channelId;
                break;
            case LogChannelType.GuildMemberKick:
                guildConfig.Channels.GuildMemberKick = channelId;
                break;
            case LogChannelType.GuildBanAdd:
                guildConfig.Channels.GuildBanAdd = channelId;
                break;
            case LogChannelType.GuildBanRemove:
                guildConfig.Channels.GuildBanRemove = channelId;
                break;
            case LogChannelType.InviteCreate:
                guildConfig.Channels.InviteCreate = channelId;
                break;
            case LogChannelType.InviteDelete:
                guildConfig.Channels.InviteDelete = channelId;
                break;
            case LogChannelType.MessageUpdate:
                guildConfig.Channels.MessageUpdate = channelId;
                break;
            case LogChannelType.MessageDelete:
                guildConfig.Channels.MessageDelete = channelId;
                break;
            case LogChannelType.MessageDeleteBulk:
                guildConfig.Channels.MessageDeleteBulk = channelId;
                break;
            default:
                throw new ArgumentOutOfRangeException();
        }

        await guildRepository.UpdateConfigAsync(guildId, guildConfig);

        List<IEmbed> embeds =
        [
            new Embed(
                Title: ChannelCommands.PrettyLogTypeName(channelType),
                Description: $"This event is currently set to log to <#{channelId}>."
                    + "\nTo change where it is logged, select a channel below."
                    + "\nTo disable logging this event entirely, select \"Stop logging\" below.",
                Colour: DiscordUtils.Purple
            ),
        ];

        List<IMessageComponent> components =
        [
            new ActionRowComponent(
                new[]
                {
                    new ChannelSelectComponent(
                        CustomID: CustomIDHelpers.CreateSelectMenuID("config-channels"),
                        ChannelTypes: new[] { ChannelType.GuildText }
                    ),
                }
            ),
            new ActionRowComponent(
                new[]
                {
                    new ButtonComponent(
                        ButtonComponentStyle.Danger,
                        Label: "Stop logging",
                        CustomID: CustomIDHelpers.CreateButtonIDWithState(
                            "config-channels",
                            "reset"
                        )
                    ),
                    new ButtonComponent(
                        ButtonComponentStyle.Secondary,
                        Label: "Return to menu",
                        CustomID: CustomIDHelpers.CreateButtonIDWithState(
                            "config-channels",
                            "return"
                        )
                    ),
                }
            ),
        ];

        lease.Data = lease.Data with { UserId = userId };
        return await interactionApi.UpdateMessageAsync(
            ctx.Interaction,
            new InteractionMessageCallbackData(Embeds: embeds, Components: components)
        );
    }
}

public record ChannelCommandData(Snowflake UserId, string? CurrentPage);
