// 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;
using Remora.Discord.API.Abstractions.Gateway.Events;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Gateway.Events;
using Remora.Discord.API.Objects;
using Remora.Discord.Extensions.Embeds;
using Remora.Discord.Gateway.Responders;
using Remora.Results;

namespace Catalogger.Backend.Bot.Responders.Messages;

public class MessageUpdateResponder(
    ILogger logger,
    GuildRepository guildRepository,
    ChannelCache channelCache,
    UserCache userCache,
    MessageRepository messageRepository,
    WebhookExecutorService webhookExecutor,
    PluralkitApiService pluralkitApi
) : IResponder<IMessageUpdate>
{
    private readonly ILogger _logger = logger.ForContext<MessageUpdateResponder>();

    public async Task<Result> RespondAsync(IMessageUpdate evt, CancellationToken ct = default)
    {
        // Discord only *very* recently changed message update events to have all fields,
        // so we convert the event to a MessageCreate to avoid having to unwrap every single field
        var msg = ConvertToMessageCreate(evt);

        if (!msg.GuildID.IsDefined())
        {
            _logger.Debug(
                "Received message create event for message {MessageId} despite it not being in a guild",
                msg.ID
            );
            return Result.Success;
        }

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

        if (await messageRepository.IsMessageIgnoredAsync(msg.ID.Value))
            return Result.Success;

        try
        {
            var logChannel = webhookExecutor.GetLogChannel(
                guildConfig,
                LogChannelType.MessageUpdate,
                msg.ChannelID,
                msg.Author.ID.Value
            );
            if (logChannel == null)
                return Result.Success;

            var oldMessage = await messageRepository.GetMessageAsync(msg.ID.Value, ct);
            if (oldMessage == null)
            {
                _logger.Debug(
                    "Message {MessageId} was edited and should be logged but is not in the database",
                    msg.ID
                );
                return Result.Success;
            }

            if (
                oldMessage.Content == msg.Content
                || (oldMessage.Content == "None" && string.IsNullOrEmpty(msg.Content))
            )
                return Result.Success;

            var user = msg.Author;
            if (msg.Author.ID != oldMessage.UserId)
            {
                var systemAccount = await userCache.GetUserAsync(
                    DiscordSnowflake.New(oldMessage.UserId)
                );
                if (systemAccount != null)
                    user = systemAccount;
            }

            var embedBuilder = new EmbedBuilder()
                .WithAuthor(user.Tag(), null, user.AvatarUrl())
                .WithTitle("Message edited")
                .WithDescription(oldMessage.Content)
                .WithColour(DiscordUtils.Purple)
                .WithFooter($"ID: {msg.ID}")
                .WithTimestamp(msg.ID.Timestamp);

            var fields = ChunksUpTo(msg.Content, 1000)
                .Select<string, IEmbedField>(
                    (s, i) => new EmbedField($"New content{(i != 0 ? " (cont.)" : "")}", s, false)
                )
                .ToList();
            embedBuilder.SetFields(fields);

            string channelMention;
            if (!channelCache.TryGet(msg.ChannelID, out var channel))
                channelMention = $"<#{msg.ChannelID}>";
            else if (
                channel.Type
                is ChannelType.AnnouncementThread
                    or ChannelType.PrivateThread
                    or ChannelType.PublicThread
            )
                channelMention =
                    $"<#{channel.ParentID.Value}>\nID: {channel.ParentID.Value}\n\nThread: {channel.Name} (<#{channel.ID}>)";
            else
                channelMention = $"<#{channel.ID}>\nID: {channel.ID}";

            embedBuilder.AddField("Channel", channelMention, true);
            embedBuilder.AddField("Sender", $"<@{user.ID}>\n{user.Tag()}\nID: {user.ID}", true);

            if (oldMessage is { System: not null, Member: not null })
            {
                embedBuilder.WithTitle($"Message by {msg.Author.Username} edited");
                embedBuilder.AddField("\u200b", "**PluralKit information**", false);
                embedBuilder.AddField("System ID", oldMessage.System, true);
                embedBuilder.AddField("Member ID", oldMessage.Member, true);
            }

            embedBuilder.AddField(
                "Link",
                $"https://discord.com/channels/{msg.GuildID}/{msg.ChannelID}/{msg.ID}"
            );

            webhookExecutor.QueueLog(logChannel.Value, embedBuilder.Build().GetOrThrow());
            return Result.Success;
        }
        finally
        {
            // Messages should be *saved* if any of the message events are enabled for this channel, but should only
            // be *logged* if the MessageUpdate event is enabled, so we check if we should save here.
            // You also can't return early in `finally` blocks, so this has to be nested :(
            if (
                webhookExecutor.GetLogChannel(
                    guildConfig,
                    LogChannelType.MessageUpdate,
                    msg.ChannelID,
                    msg.Author.ID.Value
                ) != null
                || webhookExecutor.GetLogChannel(
                    guildConfig,
                    LogChannelType.MessageDelete,
                    msg.ChannelID,
                    msg.Author.ID.Value
                ) != null
                || webhookExecutor.GetLogChannel(
                    guildConfig,
                    LogChannelType.MessageDeleteBulk,
                    msg.ChannelID,
                    msg.Author.ID.Value
                ) != null
            )
            {
                if (
                    !await messageRepository.SaveMessageAsync(msg, ct)
                    && msg.ApplicationID.Is(DiscordUtils.PkUserId)
                )
                {
                    _logger.Debug(
                        "Message {MessageId} wasn't stored yet and was proxied by PluralKit, fetching proxy information from its API",
                        msg.ID
                    );
                    var pkMsg = await pluralkitApi.GetPluralKitMessageAsync(msg.ID.Value, ct);
                    if (pkMsg != null)
                        await messageRepository.SetProxiedMessageDataAsync(
                            msg.ID.Value,
                            pkMsg.Original,
                            pkMsg.Sender,
                            pkMsg.System?.Id,
                            pkMsg.Member?.Id
                        );
                }
            }
        }
    }

    private static MessageCreate ConvertToMessageCreate(IMessageUpdate evt) =>
        new(
            evt.GuildID,
            evt.Member,
            evt.Mentions.GetOrThrow(),
            evt.ID.GetOrThrow(),
            evt.ChannelID.GetOrThrow(),
            evt.Author.GetOrThrow(),
            evt.Content.GetOrThrow(),
            evt.Timestamp.GetOrThrow(),
            evt.EditedTimestamp.GetOrThrow(),
            IsTTS: false,
            evt.MentionsEveryone.GetOrThrow(),
            evt.MentionedRoles.GetOrThrow(),
            evt.MentionedChannels,
            evt.Attachments.GetOrThrow(),
            evt.Embeds.GetOrThrow(),
            evt.Reactions,
            evt.Nonce,
            evt.IsPinned.GetOrThrow(),
            evt.WebhookID,
            evt.Type.GetOrThrow(),
            evt.Activity,
            evt.Application,
            evt.ApplicationID,
            evt.MessageReference,
            evt.Flags,
            evt.ReferencedMessage,
            evt.Interaction,
            evt.Thread,
            evt.Components,
            evt.StickerItems,
            evt.Position,
            evt.Resolved,
            evt.InteractionMetadata,
            evt.Poll
        );

    private static IEnumerable<string> ChunksUpTo(string str, int maxChunkSize)
    {
        for (var i = 0; i < str.Length; i += maxChunkSize)
            yield return str.Substring(i, Math.Min(maxChunkSize, str.Length - i));
    }
}
