using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Text;
using DiffPlex.DiffBuilder.Model;
using Remora.Discord.API;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Objects;
using Remora.Discord.Commands.Contexts;
using Remora.Discord.Commands.Extensions;
using Remora.Discord.Extensions.Embeds;
using Remora.Discord.Extensions.Formatting;
using Remora.Rest.Core;

namespace Boyfriend;

public static class Extensions {
    /// <summary>
    ///     Adds a footer with the <paramref name="user" />'s avatar and tag (@username or username#0000).
    /// </summary>
    /// <param name="builder">The builder to add the footer to.</param>
    /// <param name="user">The user whose tag and avatar to add.</param>
    /// <returns>The builder with the added footer.</returns>
    public static EmbedBuilder WithUserFooter(this EmbedBuilder builder, IUser user) {
        var avatarUrlResult = CDN.GetUserAvatarUrl(user, imageSize: 256);
        var avatarUrl = avatarUrlResult.IsSuccess
            ? avatarUrlResult.Entity.AbsoluteUri
            : CDN.GetDefaultUserAvatarUrl(user, imageSize: 256).Entity.AbsoluteUri;

        return builder.WithFooter(new EmbedFooter(user.GetTag(), avatarUrl));
    }

    /// <summary>
    ///     Adds a footer representing that an action was performed by a <paramref name="user" />.
    /// </summary>
    /// <param name="builder">The builder to add the footer to.</param>
    /// <param name="user">The user that performed the action whose tag and avatar to use.</param>
    /// <returns>The builder with the added footer.</returns>
    public static EmbedBuilder WithActionFooter(this EmbedBuilder builder, IUser user) {
        var avatarUrlResult = CDN.GetUserAvatarUrl(user, imageSize: 256);
        var avatarUrl = avatarUrlResult.IsSuccess
            ? avatarUrlResult.Entity.AbsoluteUri
            : CDN.GetDefaultUserAvatarUrl(user, imageSize: 256).Entity.AbsoluteUri;

        return builder.WithFooter(
            new EmbedFooter($"{Messages.IssuedBy}:\n{user.GetTag()}", avatarUrl));
    }

    /// <summary>
    ///     Adds a title using the author field, making it smaller than using the title field.
    /// </summary>
    /// <param name="builder">The builder to add the small title to.</param>
    /// <param name="text">The text of the small title.</param>
    /// <param name="avatarSource">The user whose avatar to use in the small title.</param>
    /// <param name="url">The URL that will be opened if a user clicks on the small title.</param>
    /// <returns>The builder with the added small title in the author field.</returns>
    public static EmbedBuilder WithSmallTitle(
        this EmbedBuilder builder, string text, IUser? avatarSource = null, string? url = default) {
        Uri? avatarUrl = null;
        if (avatarSource is not null) {
            var avatarUrlResult = CDN.GetUserAvatarUrl(avatarSource, imageSize: 256);

            avatarUrl = avatarUrlResult.IsSuccess
                ? avatarUrlResult.Entity
                : CDN.GetDefaultUserAvatarUrl(avatarSource, imageSize: 256).Entity;
        }

        builder.Author = new EmbedAuthorBuilder(text, url, avatarUrl?.AbsoluteUri);
        return builder;
    }

    /// <summary>
    ///     Adds a footer representing that the action was performed in the <paramref name="guild" />.
    /// </summary>
    /// <param name="builder">The builder to add the footer to.</param>
    /// <param name="guild">The guild whose name and icon to use.</param>
    /// <returns>The builder with the added footer.</returns>
    public static EmbedBuilder WithGuildFooter(this EmbedBuilder builder, IGuild guild) {
        var iconUrlResult = CDN.GetGuildIconUrl(guild, imageSize: 256);
        var iconUrl = iconUrlResult.IsSuccess
            ? iconUrlResult.Entity.AbsoluteUri
            : default(Optional<string>);

        return builder.WithFooter(new EmbedFooter(guild.Name, iconUrl));
    }

    /// <summary>
    ///     Adds a title representing that the action happened in the <paramref name="guild" />.
    /// </summary>
    /// <param name="builder">The builder to add the title to.</param>
    /// <param name="guild">The guild whose name and icon to use.</param>
    /// <returns>The builder with the added title.</returns>
    public static EmbedBuilder WithGuildTitle(this EmbedBuilder builder, IGuild guild) {
        var iconUrlResult = CDN.GetGuildIconUrl(guild, imageSize: 256);
        var iconUrl = iconUrlResult.IsSuccess
            ? iconUrlResult.Entity.AbsoluteUri
            : null;

        builder.Author = new EmbedAuthorBuilder(guild.Name, iconUrl: iconUrl);
        return builder;
    }

    /// <summary>
    ///     Adds a scheduled event's cover image.
    /// </summary>
    /// <param name="builder">The builder to add the image to.</param>
    /// <param name="eventId">The ID of the scheduled event whose image to use.</param>
    /// <param name="imageHashOptional">The Optional containing the image hash.</param>
    /// <returns>The builder with the added cover image.</returns>
    public static EmbedBuilder WithEventCover(
        this EmbedBuilder builder, Snowflake eventId, Optional<IImageHash?> imageHashOptional) {
        if (!imageHashOptional.IsDefined(out var imageHash)) return builder;

        var iconUrlResult = CDN.GetGuildScheduledEventCoverUrl(eventId, imageHash, imageSize: 1024);
        return iconUrlResult.IsDefined(out var iconUrl) ? builder.WithImageUrl(iconUrl.AbsoluteUri) : builder;
    }

    /// <summary>
    ///     Sanitizes a string for use in <see cref="Markdown.BlockCode(string)" /> by inserting zero-width spaces in between
    ///     symbols used to format the string with block code.
    /// </summary>
    /// <param name="s">The string to sanitize.</param>
    /// <returns>The sanitized string that can be safely used in <see cref="Markdown.BlockCode(string)" />.</returns>
    private static string SanitizeForBlockCode(this string s) {
        return s.Replace("```", "​`​`​`​");
    }

    /// <summary>
    ///     Sanitizes a string (see <see cref="SanitizeForBlockCode" />) and formats the string with block code.
    /// </summary>
    /// <param name="s">The string to sanitize and format.</param>
    /// <returns>The sanitized string formatted with <see cref="Markdown.BlockCode(string)" />.</returns>
    public static string InBlockCode(this string s) {
        s = s.SanitizeForBlockCode();
        return $"```{s.SanitizeForBlockCode()}{(s.EndsWith("`") || string.IsNullOrWhiteSpace(s) ? " " : "")}```";
    }

    public static string Localized(this string key) {
        return Messages.ResourceManager.GetString(key, Messages.Culture) ?? key;
    }

    /// <summary>
    ///     Encodes a string to allow its transmission in request headers.
    /// </summary>
    /// <remarks>Used when encountering "Request headers must contain only ASCII characters".</remarks>
    /// <param name="s">The string to encode.</param>
    /// <returns>An encoded string with spaces kept intact.</returns>
    public static string EncodeHeader(this string s) {
        return WebUtility.UrlEncode(s).Replace('+', ' ');
    }

    public static string AsMarkdown(this DiffPaneModel model) {
        var builder = new StringBuilder();
        foreach (var line in model.Lines) {
            if (line.Type is ChangeType.Deleted)
                builder.Append("-- ");
            if (line.Type is ChangeType.Inserted)
                builder.Append("++ ");
            if (line.Type is not ChangeType.Imaginary)
                builder.AppendLine(line.Text);
        }

        return Markdown.BlockCode(builder.ToString().SanitizeForBlockCode(), "diff");
    }

    public static string GetTag(this IUser user) {
        return user.Discriminator is 0000 ? $"@{user.Username}" : $"{user.Username}#{user.Discriminator:0000}";
    }

    public static Snowflake ToDiscordSnowflake(this ulong id) {
        return DiscordSnowflake.New(id);
    }

    public static TResult? MaxOrDefault<TSource, TResult>(
        this IEnumerable<TSource> source, Func<TSource, TResult> selector) {
        var list = source.ToList();
        return list.Any() ? list.Max(selector) : default;
    }

    public static bool TryGetContextIDs(
        this                    ICommandContext context,   [NotNullWhen(true)] out Snowflake? guildId,
        [NotNullWhen(true)] out Snowflake?      channelId, [NotNullWhen(true)] out Snowflake? userId) {
        guildId = null;
        channelId = null;
        userId = null;
        return context.TryGetGuildID(out guildId)
               && context.TryGetChannelID(out channelId)
               && context.TryGetUserID(out userId);
    }
}