// 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 System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Web;
using Catalogger.Backend.Cache.InMemoryCache;
using Catalogger.Backend.Extensions;
using Catalogger.Backend.Services;
using Humanizer;
using Humanizer.Localisation;
using Remora.Commands.Attributes;
using Remora.Commands.Groups;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Abstractions.Rest;
using Remora.Discord.Commands.Extensions;
using Remora.Discord.Commands.Feedback.Services;
using Remora.Discord.Commands.Services;
using Remora.Discord.Extensions.Embeds;
using Remora.Results;
using IClock = NodaTime.IClock;
using IResult = Remora.Results.IResult;

namespace Catalogger.Backend.Bot.Commands;

[Group("catalogger")]
[Description("Commands for information about the bot itself.")]
public class MetaCommands(
    ILogger logger,
    IClock clock,
    Config config,
    ShardedGatewayClient client,
    IFeedbackService feedbackService,
    ContextInjectionService contextInjection,
    GuildCache guildCache,
    RoleCache roleCache,
    ChannelCache channelCache,
    EmojiCache emojiCache,
    IDiscordRestChannelAPI channelApi,
    PermissionResolverService permissionResolver
) : CommandGroup
{
    private readonly ILogger _logger = logger.ForContext<MetaCommands>();
    private readonly HttpClient _client = new();

    [Command("help")]
    [Description("Learn more about Catalogger.")]
    public async Task<IResult> HelpAsync()
    {
        var embed = new EmbedBuilder()
            .WithColour(DiscordUtils.Purple)
            .WithTitle("Catalogger")
            .WithDescription(
                """
                A logging bot that integrates with PluralKit's message proxying.
                Use `/configure-channels` to get started!
                """
            );

        if (config.Discord.EnableDash)
            embed.Description +=
                $"\n\nYou can also use the dashboard for configuration: {config.Web.BaseUrl}";

        embed.AddField(
            "Configuration",
            """
            `/configure-channels`: Set which events will be logged to which channels
            `/ignored-channels`: Set which channels will be ignored entirely
            `/redirects`: Override where a channel's messages will be logged
            `/key-roles`: Set which roles are treated as key roles and are logged with more detail than others
            `/invites`: Manage invites and create new ones
            `/check-permissions`: Check for any issues with logging
            """
        );

        embed.AddField("Creator", "<@694563574386786314> / starshines.gay");
        embed.AddField(
            "Source code",
            "https://codeberg.org/starshine/catalogger / Licensed under the GNU AGPL v3"
        );

        if (config.Discord.SupportGuild != null)
            embed.AddField(
                "Support",
                $"Use this link to join the support server: {config.Discord.SupportGuild}"
            );

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

    [Command("invite")]
    [Description("Get a link to invite Catalogger to your server.")]
    public async Task<IResult> InviteAsync()
    {
        var inviteUrl =
            $"https://discord.com/oauth2/authorize?client_id={config.Discord.ApplicationId}"
            + "&permissions=537250993&scope=bot+applications.commands";

        return await feedbackService.ReplyAsync(
            $"Use this link to invite Catalogger to your server: {inviteUrl}",
            isEphemeral: true
        );
    }

    [Command("dashboard")]
    [Description("Get a link to the dashboard.")]
    public async Task<IResult> DashboardLinkAsync()
    {
        if (!config.Discord.EnableDash)
            return await feedbackService.ReplyAsync(
                "The dashboard is not enabled for this version of Catalogger.",
                isEphemeral: true
            );

        if (
            contextInjection.Context?.TryGetGuildID(out var guildId) != true
            || contextInjection.Context?.TryGetUserID(out var userId) != true
        )
            return await feedbackService.ReplyAsync(
                $"The dashboard is available here: {config.Web.BaseUrl}",
                isEphemeral: true
            );

        var perms = await permissionResolver.GetGuildPermissionsAsync(guildId, userId);
        if (
            perms.HasPermission(DiscordPermission.ManageGuild)
            || perms.HasPermission(DiscordPermission.Administrator)
        )
            return await feedbackService.ReplyAsync(
                $"The dashboard for this server is available here: {config.Web.BaseUrl}/dash/{guildId}"
            );

        return await feedbackService.ReplyAsync(
            $"The dashboard is available here: {config.Web.BaseUrl}",
            isEphemeral: true
        );
    }

    [Command("ping")]
    [Description("Ping pong! See the bot's latency")]
    public async Task<IResult> PingAsync()
    {
        var shardId =
            contextInjection.Context?.TryGetGuildID(out var guildId) == true
                ? client.ShardIdFor(guildId.Value)
                : 0;

        var averageLatency =
            client.Shards.Values.Select(x => x.Latency.TotalMilliseconds).Sum()
            / client.Shards.Count;

        var t1 = clock.GetCurrentInstant();
        var msg = await feedbackService.SendContextualAsync("...").GetOrThrow();
        var elapsed = clock.GetCurrentInstant() - t1;

        var process = Process.GetCurrentProcess();
        var memoryUsage = process.WorkingSet64;

        var embed = new EmbedBuilder()
            .WithColour(DiscordUtils.Purple)
            .WithFooter(
                $"{BuildInfo.Version}, {RuntimeInformation.FrameworkDescription} on {RuntimeInformation.RuntimeIdentifier}"
            )
            .WithCurrentTimestamp();
        embed.AddField(
            "Ping",
            $"Gateway: {client.Shards[shardId].Latency.TotalMilliseconds:N0}ms (average: {averageLatency:N0}ms)\n"
                + $"API: {elapsed.TotalMilliseconds:N0}ms",
            inline: true
        );
        embed.AddField("Memory usage", memoryUsage.Bytes().Humanize(), inline: true);

        var messageRate = await MessagesRate();
        embed.AddField(
            "Messages received",
            messageRate != null
                ? $"{messageRate / 5:F1}/m\n({CataloggerMetrics.MessagesReceived.Value:N0} since last restart)"
                : $"{CataloggerMetrics.MessagesReceived.Value:N0} since last restart",
            true
        );

        embed.AddField("Shard", $"{shardId + 1} of {client.Shards.Count}", true);

        embed.AddField(
            "Uptime",
            $"{(CataloggerMetrics.Startup - clock.GetCurrentInstant()).Prettify(TimeUnit.Second)}\n"
                + $"since <t:{CataloggerMetrics.Startup.ToUnixTimeSeconds()}:F>",
            true
        );

        embed.AddField(
            "Numbers",
            $"{CataloggerMetrics.MessagesStored.Value:N0} messages "
                + $"from {guildCache.Size:N0} servers\n"
                + $"Cached {channelCache.Size:N0} channels, {roleCache.Size:N0} roles, {emojiCache.Size:N0} emojis"
        );

        IEmbed[] embeds = [embed.Build().GetOrThrow()];

        return (Result)
            await channelApi.EditMessageAsync(msg.ChannelID, msg.ID, content: "", embeds: embeds);
    }

    // TODO: add more checks around response format, configurable prometheus endpoint
    private async Task<double?> MessagesRate()
    {
        if (!config.Logging.EnableMetrics)
            return null;

        try
        {
            var query = HttpUtility.UrlEncode("increase(catalogger_received_messages[5m])");
            var resp = await _client.GetAsync($"http://localhost:9090/api/v1/query?query={query}");
            resp.EnsureSuccessStatusCode();

            var data = await resp.Content.ReadFromJsonAsync<PrometheusResponse>();
            var rawNumber = (data?.data.result[0].value[1] as JsonElement?)?.GetString();
            return double.TryParse(rawNumber, out var rate) ? rate : null;
        }
        catch (Exception e)
        {
            _logger.Warning(e, "Failed querying Prometheus for message rate");
            return null;
        }
    }

    // ReSharper disable InconsistentNaming, ClassNeverInstantiated.Local
    private record PrometheusResponse(PrometheusData data);

    private record PrometheusData(PrometheusResult[] result);

    private record PrometheusResult(object[] value);
    // ReSharper restore InconsistentNaming, ClassNeverInstantiated.Local
}
