using System.Globalization;
using System.Text;
using GmRelay.DiscordBot.Rendering;
using GmRelay.Shared.Domain;
using GmRelay.Shared.Platform;
using GmRelay.Shared.Rendering;
using Microsoft.Extensions.Logging;
using NetCord;
using NetCord.Rest;

namespace GmRelay.DiscordBot.Infrastructure.Discord;

public sealed class DiscordPlatformMessenger : IPlatformMessenger
{
    private readonly RestClient restClient;
    private readonly DiscordInteractionReplyCache interactionReplies;
    private readonly ILogger<DiscordPlatformMessenger>? logger;

    public DiscordPlatformMessenger(
        RestClient restClient,
        DiscordInteractionReplyCache interactionReplies)
        : this(restClient, interactionReplies, logger: null)
    {
    }

    public DiscordPlatformMessenger(
        RestClient restClient,
        DiscordInteractionReplyCache interactionReplies,
        ILogger<DiscordPlatformMessenger>? logger)
    {
        this.restClient = restClient;
        this.interactionReplies = interactionReplies;
        this.logger = logger;
    }

    public async Task<PlatformMessageRef> SendScheduleAsync(PlatformScheduleMessage message, CancellationToken ct)
    {
        var (embeds, actionRows) = DiscordSessionBatchRenderer.Render(message.View);

        var channelId = GetChannelId(message.Group);

        var msg = await restClient.SendMessageAsync(
            channelId,
            new MessageProperties()
                .WithEmbeds(embeds)
                .WithComponents(actionRows));

        return new PlatformMessageRef(
            PlatformKind.Discord,
            message.Group.ExternalGroupId,
            null,
            msg.Id.ToString(CultureInfo.InvariantCulture));
    }

    public async Task UpdateScheduleAsync(PlatformScheduleMessage message, CancellationToken ct)
    {
        if (message.ExistingMessage is null)
            return;

        var (embeds, actionRows) = DiscordSessionBatchRenderer.Render(message.View);

        var channelId = GetChannelId(message.Group);
        var messageId = ParseSnowflake(message.ExistingMessage.ExternalMessageId);

        await restClient.ModifyMessageAsync(
            channelId,
            messageId,
            options =>
            {
                options.Embeds = embeds;
                options.Components = actionRows;
            });
    }

    public async Task SendGroupMessageAsync(PlatformGroup group, string htmlText, CancellationToken ct)
    {
        await restClient.SendMessageAsync(GetChannelId(group), htmlText);
    }

    public async Task SendGroupMessageAsync(PlatformGroup group, string htmlText, IReadOnlyList<PlatformMessageAction> actions, CancellationToken ct)
    {
        var rows = BuildActionRows(actions);
        await restClient.SendMessageAsync(GetChannelId(group), new MessageProperties().WithContent(htmlText).WithComponents(rows));
    }

    public async Task UpdateGroupMessageAsync(PlatformMessageRef messageRef, string htmlText, IReadOnlyList<PlatformMessageAction> actions, CancellationToken ct)
    {
        var channelId = GetChannelId(new PlatformGroup(messageRef.Platform, messageRef.ExternalGroupId, string.Empty, messageRef.ExternalThreadId));
        var messageId = ParseSnowflake(messageRef.ExternalMessageId);
        var rows = BuildActionRows(actions);
        await restClient.ModifyMessageAsync(channelId, messageId, options =>
        {
            options.Content = htmlText;
            options.Components = rows;
        });
    }

    public Task<PlatformMessageRef> CreateThreadAsync(PlatformGroup group, string title, CancellationToken ct)
    {
        // Discord thread creation is not implemented in this adapter
        return Task.FromResult(new PlatformMessageRef(PlatformKind.Discord, group.ExternalGroupId, group.ExternalThreadId, string.Empty));
    }

    public Task DeleteThreadAsync(PlatformGroup group, CancellationToken ct) => Task.CompletedTask;

    public async Task DeleteMessageAsync(PlatformMessageRef messageRef, CancellationToken ct)
    {
        var channelId = GetChannelId(new PlatformGroup(messageRef.Platform, messageRef.ExternalGroupId, string.Empty, messageRef.ExternalThreadId));
        await restClient.DeleteMessageAsync(channelId, ParseSnowflake(messageRef.ExternalMessageId));
    }

    public async Task SendPrivateMessageAsync(PlatformPrivateMessage message, CancellationToken ct)
    {
        await SendDirectContentAsync(message.Recipient, message.HtmlText, ct);
    }

    public Task AnswerInteractionAsync(PlatformInteractionReply reply, CancellationToken ct)
    {
        interactionReplies.Store(reply);
        return Task.CompletedTask;
    }

    public Task SendCalendarFileAsync(PlatformCalendarFile file, CancellationToken ct)
    {
        return Task.CompletedTask;
    }

    public async Task<PlatformMessageRef> SendConfirmationRequestAsync(
        PlatformConfirmationRequest request,
        CancellationToken ct)
    {
        var channelId = GetChannelId(request.Group);
        try
        {
            var message = await restClient.SendMessageAsync(
                channelId,
                new MessageProperties()
                    .WithEmbeds([BuildConfirmationEmbed(request)])
                    .WithComponents(BuildRsvpRows(request.SessionId, disabled: false)));

            logger?.LogInformation(
                "Confirmation request sent to Discord channel {ChannelId}, message id {MessageId}",
                channelId,
                message.Id);

            return new PlatformMessageRef(
                PlatformKind.Discord,
                request.Group.ExternalGroupId,
                null,
                message.Id.ToString(CultureInfo.InvariantCulture));
        }
        catch (Exception ex)
        {
            logger?.LogError(
                ex,
                "Failed to send confirmation request to Discord channel {ChannelId} for session {SessionId}",
                channelId,
                request.SessionId);
            throw;
        }
    }

    public async Task UpdateConfirmationRequestAsync(PlatformRsvpMessageUpdate update, CancellationToken ct)
    {
        if (update.Request.ExistingMessage is null)
            return;

        var channelId = GetChannelId(update.Request.Group);
        var messageId = ParseSnowflake(update.Request.ExistingMessage.ExternalMessageId);
        var components = BuildRsvpRows(update.Request.SessionId, update.DisableActions);

        await restClient.ModifyMessageAsync(
            channelId,
            messageId,
            options =>
            {
                options.Embeds = [BuildConfirmationEmbed(update.Request)];
                options.Components = components;
            });
    }

    public async Task<PlatformMessageRef> SendJoinLinkNotificationAsync(
        PlatformJoinLinkNotification notification,
        CancellationToken ct)
    {
        var channelId = GetChannelId(notification.Group);
        try
        {
            var message = await restClient.SendMessageAsync(
                channelId,
                new MessageProperties().WithEmbeds([BuildJoinLinkEmbed(notification)]));

            logger?.LogInformation(
                "Join link sent to Discord channel {ChannelId}, message id {MessageId}",
                channelId,
                message.Id);

            return new PlatformMessageRef(
                PlatformKind.Discord,
                notification.Group.ExternalGroupId,
                null,
                message.Id.ToString(CultureInfo.InvariantCulture));
        }
        catch (Exception ex)
        {
            logger?.LogError(
                ex,
                "Failed to send join link to Discord channel {ChannelId} for session {SessionId}",
                channelId,
                notification.SessionId);
            throw;
        }
    }

    public async Task SendDirectSessionNotificationAsync(
        PlatformDirectSessionNotification notification,
        CancellationToken ct)
    {
        try
        {
            await SendDirectContentAsync(
                notification.Recipient,
                BuildDirectContent(notification),
                ct);
        }
        catch (Exception ex)
        {
            logger?.LogWarning(
                ex,
                "Failed to send Discord direct notification {NotificationKind} for session {SessionId} to user {ExternalUserId}",
                notification.Kind,
                notification.SessionId,
                notification.Recipient.ExternalUserId);
        }
    }

    public async Task SendRsvpOutcomeAsync(PlatformRsvpOutcomeNotification notification, CancellationToken ct)
    {
        if (notification.Kind == PlatformRsvpOutcomeKind.GroupAllConfirmed && notification.Group is not null)
        {
            await restClient.SendMessageAsync(
                GetChannelId(notification.Group),
                BuildRsvpGroupOutcomeContent(notification));
            return;
        }

        var directKind = notification.Kind == PlatformRsvpOutcomeKind.GmPlayerDeclined
            ? PlatformDirectSessionNotificationKind.RsvpDeclined
            : PlatformDirectSessionNotificationKind.RsvpAllConfirmed;

        foreach (var recipient in notification.Recipients)
        {
            await SendDirectSessionNotificationAsync(
                new PlatformDirectSessionNotification(
                    directKind,
                    recipient,
                    notification.SessionId,
                    notification.Title,
                    notification.ScheduledAt,
                    ActorDisplayName: notification.ActorDisplayName),
                ct);
        }
    }

    public async Task UpdateRescheduleVoteAsync(PlatformRescheduleVoteUpdate update, CancellationToken ct)
    {
        var (embed, actionRow) = DiscordRescheduleVotingRenderer.Render(
            update.Title,
            update.CurrentScheduledAt,
            update.VotingDeadlineAt,
            update.Options,
            update.Participants,
            update.Votes);

        var disabledRow = new ActionRowProperties();
        foreach (var button in actionRow.OfType<ButtonProperties>())
        {
            disabledRow.Add(new ButtonProperties(
                button.CustomId,
                button.Label ?? string.Empty,
                ButtonStyle.Secondary)
            {
                Disabled = true
            });
        }

        var updatedEmbed = embed.WithDescription(
            $"{embed.Description}\n\n{BuildRescheduleResultText(update)}");

        await restClient.ModifyMessageAsync(
            GetChannelId(update.Group),
            ParseSnowflake(update.ExistingMessage.ExternalMessageId),
            options =>
            {
                options.Embeds = [updatedEmbed];
                options.Components = [disabledRow];
            });
    }

    private static EmbedProperties BuildConfirmationEmbed(PlatformConfirmationRequest request)
    {
        var embed = new EmbedProperties()
            .WithTitle($"Подтверждение: {request.Title}")
            .WithDescription(BuildConfirmationDescription(request))
            .WithColor(new Color(0x5865F2));

        return embed.AddFields(
        [
            BuildParticipantField("Подтвердили", request.Participants, RsvpStatus.Confirmed),
            BuildParticipantField("Отказались", request.Participants, RsvpStatus.Declined),
            BuildParticipantField("Ожидаем ответ", request.Participants, RsvpStatus.Pending)
        ]);
    }

    private static string BuildConfirmationDescription(PlatformConfirmationRequest request) =>
        $"Время: **{request.ScheduledAt.FormatMoscow()}** (МСК)\n" +
        "Подтвердите участие кнопкой ниже.";

    private static EmbedFieldProperties BuildParticipantField(
        string title,
        IReadOnlyList<PlatformSessionParticipant> participants,
        string status)
    {
        var values = participants
            .Where(participant => participant.RsvpStatus == status)
            .Select(FormatDiscordParticipant)
            .ToList();

        return new EmbedFieldProperties()
            .WithName(title)
            .WithValue(values.Count == 0 ? "—" : string.Join("\n", values))
            .WithInline();
    }

    private static EmbedProperties BuildJoinLinkEmbed(PlatformJoinLinkNotification notification)
    {
        var mentions = notification.ConfirmedPlayers.Count == 0
            ? "—"
            : string.Join(", ", notification.ConfirmedPlayers.Select(p => Mention(p.User)));

        var embed = new EmbedProperties()
            .WithTitle($"Ссылка на игру: {notification.Title}")
            .WithDescription(
                $"Время: **{notification.ScheduledAt.FormatMoscow()}** (МСК)\n" +
                $"Ссылка: {notification.JoinLink}\n\n" +
                $"Участники: {mentions}")
            .WithColor(new Color(0x57F287));

        var embedUrl = DiscordEmbedUrls.NormalizeHttpUrl(notification.JoinLink);
        return embedUrl is null ? embed : embed.WithUrl(embedUrl);
    }

    private static IReadOnlyList<ActionRowProperties> BuildRsvpRows(Guid sessionId, bool disabled)
    {
        var row = new ActionRowProperties();
        row.Add(new ButtonProperties($"rsvp:confirm:{sessionId}", "Буду", ButtonStyle.Success)
        {
            Disabled = disabled
        });
        row.Add(new ButtonProperties($"rsvp:decline:{sessionId}", "Не смогу", ButtonStyle.Danger)
        {
            Disabled = disabled
        });

        return [row];
    }

    private static string BuildDirectContent(PlatformDirectSessionNotification notification)
    {
        var builder = new StringBuilder();
        builder.AppendLine(notification.Kind switch
        {
            PlatformDirectSessionNotificationKind.ConfirmationRequest => "Нужно подтвердить участие",
            PlatformDirectSessionNotificationKind.OneHourReminder => "Напоминание: сессия через час",
            PlatformDirectSessionNotificationKind.JoinLink => "Ссылка на игру",
            PlatformDirectSessionNotificationKind.RsvpAllConfirmed => "Все игроки подтвердили участие",
            PlatformDirectSessionNotificationKind.RsvpDeclined => "Игрок отказался от участия",
            PlatformDirectSessionNotificationKind.RescheduleApproved => "Сессия перенесена",
            PlatformDirectSessionNotificationKind.RescheduleRejected => "Перенос сессии отклонен",
            _ => "Уведомление по сессии"
        });

        builder.AppendLine();
        builder.AppendLine($"**{notification.Title}**");
        builder.AppendLine($"Время: **{notification.ScheduledAt.FormatMoscow()}** (МСК)");

        if (!string.IsNullOrWhiteSpace(notification.JoinLink))
            builder.AppendLine($"Ссылка: {notification.JoinLink}");

        if (!string.IsNullOrWhiteSpace(notification.ActorDisplayName))
            builder.AppendLine($"Игрок: {notification.ActorDisplayName}");

        if (!string.IsNullOrWhiteSpace(notification.Reason))
            builder.AppendLine($"Причина: {notification.Reason}");

        return builder.ToString();
    }

    private static string BuildRsvpGroupOutcomeContent(PlatformRsvpOutcomeNotification notification) =>
        $"Все участники подтвердили сессию **{notification.Title}** на " +
        $"**{notification.ScheduledAt.FormatMoscow()}** (МСК).";

    private static string BuildRescheduleResultText(PlatformRescheduleVoteUpdate update)
    {
        if (update.SelectedOption is not null)
        {
            return "Голосование завершено. " +
                $"Победил вариант {update.SelectedOption.DisplayOrder}: " +
                $"**{update.SelectedOption.ProposedAt.FormatMoscow()}** (МСК).";
        }

        return $"Голосование завершено. {update.Decision.Reason}";
    }

    private async Task SendDirectContentAsync(PlatformUser recipient, string content, CancellationToken ct)
    {
        var userId = ParseSnowflake(recipient.ExternalUserId);
        var dm = await restClient.GetDMChannelAsync(userId, cancellationToken: ct);
        await restClient.SendMessageAsync(
            dm.Id,
            new MessageProperties().WithContent(content),
            cancellationToken: ct);
    }

    private static string FormatDiscordParticipant(PlatformSessionParticipant participant) =>
        $"{Mention(participant.User)} ({participant.User.DisplayName})";

    private static string Mention(PlatformUser user) => $"<@{user.ExternalUserId}>";

    private static ulong GetChannelId(PlatformGroup group)
    {
        var channelId = group.ExternalChannelId ?? group.ExternalGroupId
            ?? throw new InvalidOperationException("Discord group has no channel or group identifier.");

        return ParseSnowflake(channelId);
    }

    private static IReadOnlyList<ActionRowProperties> BuildActionRows(IReadOnlyList<PlatformMessageAction> actions)
    {
        if (actions.Count == 0)
        {
            return [];
        }

        var rows = new List<ActionRowProperties>();
        foreach (var chunk in actions.Chunk(5))
        {
            var row = new ActionRowProperties();
            foreach (var action in chunk)
            {
                row.Add(new ButtonProperties(action.Key, action.Label, ButtonStyle.Secondary)
                {
                    CustomId = action.Payload
                });
            }
            rows.Add(row);
        }

        return rows;
    }

    private static ulong ParseSnowflake(string value) =>
        ulong.Parse(value, CultureInfo.InvariantCulture);
}
