// 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.Drawing;
using Catalogger.Backend.Cache.InMemoryCache;
using Humanizer;
using Remora.Discord.API.Abstractions.Gateway.Events;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.Commands.Contexts;
using Remora.Discord.Commands.Extensions;
using Remora.Discord.Commands.Services;
using Remora.Rest.Core;
using Remora.Results;

namespace Catalogger.Backend.Extensions;

public static class DiscordExtensions
{
    public static string Tag(this IPartialUser user)
    {
        var discriminator = user.Discriminator.OrDefault();
        return discriminator == 0
            ? user.Username.Value
            : $"{user.Username.Value}#{discriminator:0000}";
    }

    // TODO: replace these avatar URL methods with the built-in CDN.* methods?
    public static string AvatarUrl(this IUser user, int size = 256)
    {
        if (user.Avatar != null)
        {
            var ext = user.Avatar.HasGif ? ".gif" : ".webp";
            return $"https://cdn.discordapp.com/avatars/{user.ID}/{user.Avatar.Value}{ext}?size={size}";
        }

        var avatarIndex =
            user.Discriminator == 0 ? (int)((user.ID.Value >> 22) % 6) : user.Discriminator % 5;
        return $"https://cdn.discordapp.com/embed/avatars/{avatarIndex}.png?size={size}";
    }

    public static string? AvatarUrl(this IGuildMemberUpdate member, int size = 256) =>
        GuildAvatarUrl(
            member.GuildID,
            member.User.ID,
            member.Avatar.OrDefault()?.Value,
            isAnimated: member.Avatar.OrDefault()?.HasGif,
            size
        );

    public static string? AvatarUrl(this IGuildMember member, Snowflake guildId, int size = 256) =>
        GuildAvatarUrl(
            guildId,
            member.User.GetOrThrow().ID,
            member.Avatar.OrDefault()?.Value,
            isAnimated: member.Avatar.OrDefault()?.HasGif,
            size
        );

    private static string? GuildAvatarUrl(
        Snowflake guildId,
        Snowflake userId,
        string? hash,
        bool? isAnimated,
        int size = 256
    )
    {
        if (hash == null)
            return null;

        var ext = isAnimated == true ? ".gif" : ".webp";

        return $"https://cdn.discordapp.com/guilds/{guildId}/users/{userId}/avatars/{hash}{ext}?size={size}";
    }

    public static string? IconUrl(this IGuild guild, int size = 256)
    {
        if (guild.Icon == null)
            return null;

        var ext = guild.Icon.HasGif ? ".gif" : ".webp";

        return $"https://cdn.discordapp.com/icons/{guild.ID}/{guild.Icon.Value}{ext}?size={size}";
    }

    public static ulong ToUlong(this Snowflake snowflake) => snowflake.Value;

    public static ulong ToUlong(this Optional<Snowflake> snowflake)
    {
        if (!snowflake.IsDefined())
            throw new Exception("ToUlong called on an undefined Snowflake");
        return snowflake.Value.Value;
    }

    public static string ToPrettyString(this Color color)
    {
        var r = color.R.ToString("X2");
        var g = color.G.ToString("X2");
        var b = color.B.ToString("X2");

        return $"#{r}{g}{b}";
    }

    public static bool Is(this Optional<Snowflake> s1, Snowflake s2) =>
        s1.IsDefined(out var value) && value == s2;

    public static bool Is(this Optional<Snowflake> s1, ulong s2) =>
        s1.IsDefined(out var value) && value == s2;

    public static T GetOrThrow<T>(this Result<T> result)
    {
        if (result.Error != null)
            throw new DiscordRestException(result.Error.Message);
        return result.Entity;
    }

    public static T GetOrThrow<T>(this Optional<T> optional) =>
        optional.OrThrow(() => new CataloggerError("Optional<T> was unset"));

    public static async Task<T> GetOrThrow<T>(this Task<Result<T>> result) =>
        (await result).GetOrThrow();

    public static string ToPrettyString(this IDiscordPermissionSet permissionSet) =>
        string.Join(
            ", ",
            permissionSet.GetPermissions().Select(p => p.Humanize(LetterCasing.Title))
        );

    public static (Snowflake UserId, Snowflake GuildId) GetUserAndGuild(
        this ContextInjectionService contextInjectionService
    )
    {
        if (contextInjectionService.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.TryGetGuildID(out var guildId))
            throw new CataloggerError("No guild ID in context");
        return (userId, guildId);
    }

    /// <summary>
    /// Sorts a list of roles by their position in the Discord interface.
    /// </summary>
    /// <param name="roles">The list of guild roles to filter.</param>
    /// <param name="filterByIds">An optional list of role IDs to return, from a member object or similar.
    /// If null, the entire list is returned.</param>
    /// <returns></returns>
    public static IEnumerable<IRole> Sorted(
        this IEnumerable<IRole> roles,
        IEnumerable<Snowflake>? filterByIds = null
    )
    {
        var sorted = roles.OrderByDescending(r => r.Position);
        return filterByIds != null ? sorted.Where(r => filterByIds.Contains(r.ID)) : sorted;
    }

    public static string PrettyFormat(this IUser user, bool addMention = true) =>
        addMention ? $"{user.Tag()} <@{user.ID}>" : user.Tag();

    public static async Task<string> TryFormatUserAsync(
        this UserCache userCache,
        Snowflake userId,
        bool addMention = true
    )
    {
        var user = await userCache.GetUserAsync(userId);
        if (user != null)
            return user.PrettyFormat(addMention);

        return addMention ? $"*(unknown user {userId})* <@{userId}>" : $"*(unknown user {userId})*";
    }

    public static int TextLength(this IEmbed embed)
    {
        var length = OptionalStringLength(embed.Description) + OptionalStringLength(embed.Title);
        var fieldLength = (embed.Fields.OrDefault() ?? [])
            .Select(f => f.Name.Length + f.Value.Length)
            .Sum();

        return length + fieldLength;
    }

    private static int OptionalStringLength(Optional<string> s) => s.OrDefault()?.Length ?? 0;

    public class DiscordRestException(string message) : Exception(message);
}
