namespace GmRelay.DiscordBot.Features.Sessions;

using Dapper;
using GmRelay.Shared.Features.Sessions.RescheduleSession;
using GmRelay.Shared.Platform;
using GmRelay.Shared.Rendering;
using Npgsql;

public sealed class DiscordRescheduleVotingDeadlineService(
    NpgsqlDataSource dataSource,
    RescheduleVotingFinalizer finalizer,
    IPlatformMessenger messenger,
    ILogger<DiscordRescheduleVotingDeadlineService> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        try
        {
            await ProcessDueProposals(stoppingToken);
            using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1));
            while (await timer.WaitForNextTickAsync(stoppingToken))
            {
                await ProcessDueProposals(stoppingToken);
            }
        }
        catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
        {
        }
    }

    private async Task ProcessDueProposals(CancellationToken ct)
    {
        try
        {
            var proposalIds = await finalizer.GetDueProposalIdsAsync("Discord", ct);
            foreach (var id in proposalIds)
            {
                await TryFinalizeAsync(id, ct);
            }
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to process Discord reschedule proposals");
        }
    }

    private async Task TryFinalizeAsync(Guid proposalId, CancellationToken ct)
    {
        try
        {
            var result = await finalizer.FinalizeAsync(proposalId, ct);
            if (result is null)
                return;

            if (result.SourcePlatform != "Discord")
                return;

            await TryUpdateDiscordVoteMessage(result, ct);

            if (result.SelectedOption is not null)
            {
                await TryUpdateBatchScheduleAsync(result, ct);
            }

            logger.LogInformation(
                "Finalized Discord reschedule proposal {ProposalId} for session {SessionId} with outcome {Outcome}",
                proposalId,
                result.SessionId,
                result.Decision.Outcome);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to finalize Discord proposal {ProposalId}", proposalId);
        }
    }

    private async Task TryUpdateDiscordVoteMessage(RescheduleVotingFinalizerResult result, CancellationToken ct)
    {
        try
        {
            await using var connection = await dataSource.OpenConnectionAsync(ct);
            var msgRef = await connection.QuerySingleOrDefaultAsync<PlatformMessageRefDto>(
                """
                SELECT g.external_group_id AS ExternalGroupId,
                       COALESCE(pm.external_channel_id, g.external_channel_id, g.external_group_id) AS ExternalChannelId,
                       pm.external_message_id AS ExternalMessageId
                FROM platform_messages pm
                JOIN game_groups g ON g.id = pm.group_id
                WHERE pm.session_id = @SessionId AND pm.purpose = 'reschedule_vote' AND pm.platform = 'Discord'
                ORDER BY pm.created_at DESC
                LIMIT 1
                """,
                new { result.SessionId });

            if (msgRef is null)
                return;

            var group = CreateDiscordGroup(msgRef);

            await messenger.UpdateRescheduleVoteAsync(
                new PlatformRescheduleVoteUpdate(
                    group,
                    new PlatformMessageRef(
                        PlatformKind.Discord,
                        msgRef.ExternalGroupId,
                        null,
                        msgRef.ExternalMessageId),
                    result.ProposalId,
                    result.SessionId,
                    result.Title,
                    result.CurrentScheduledAt,
                    result.VotingDeadlineAt,
                    result.Decision,
                    result.SelectedOption,
                    result.Options,
                    result.Votes,
                    result.Participants),
                ct);
        }
        catch (Exception ex)
        {
            logger.LogWarning(ex, "Failed to update Discord vote message for session {SessionId}", result.SessionId);
        }
    }

    private async Task TryUpdateBatchScheduleAsync(RescheduleVotingFinalizerResult result, CancellationToken ct)
    {
        try
        {
            await using var connection = await dataSource.OpenConnectionAsync(ct);
            var batchRef = await connection.QuerySingleOrDefaultAsync<PlatformMessageRefDto>(
                """
                SELECT g.external_group_id AS ExternalGroupId,
                       COALESCE(pm.external_channel_id, g.external_channel_id, g.external_group_id) AS ExternalChannelId,
                       pm.external_message_id AS ExternalMessageId
                FROM platform_messages pm
                JOIN game_groups g ON g.id = pm.group_id
                WHERE pm.batch_id = @BatchId AND pm.purpose = 'schedule' AND pm.platform = 'Discord'
                ORDER BY pm.created_at DESC
                LIMIT 1
                """,
                new { result.BatchId });

            if (batchRef is null)
                return;

            var sessions = (await connection.QueryAsync<SessionBatchDto>(
                "SELECT id AS SessionId, scheduled_at AS ScheduledAt, status AS Status, max_players AS MaxPlayers, join_link AS JoinLink FROM sessions WHERE batch_id = @BatchId ORDER BY scheduled_at",
                new { result.BatchId })).ToList();

            var participants = (await connection.QueryAsync<ParticipantBatchDto>(
                """
                SELECT sp.session_id AS SessionId,
                       p.display_name AS DisplayName,
                       p.external_username AS TelegramUsername,
                       sp.registration_status AS RegistrationStatus
                FROM session_participants sp
                JOIN players p ON p.id = sp.player_id
                JOIN sessions s ON sp.session_id = s.id
                WHERE s.batch_id = @BatchId AND sp.is_gm = false
                ORDER BY sp.registration_status ASC, sp.created_at ASC
                """,
                new { result.BatchId })).ToList();

            var view = SessionBatchViewBuilder.Build(result.Title, sessions, participants);
            var group = CreateDiscordGroup(batchRef);

            await messenger.UpdateScheduleAsync(
                new PlatformScheduleMessage(
                    group,
                    view,
                    new PlatformMessageRef(
                        PlatformKind.Discord,
                        batchRef.ExternalGroupId,
                        null,
                        batchRef.ExternalMessageId)),
                ct);
        }
        catch (Exception ex)
        {
            logger.LogWarning(ex, "Failed to update Discord batch schedule for session {SessionId}", result.SessionId);
        }
    }

    private static PlatformGroup CreateDiscordGroup(PlatformMessageRefDto message) =>
        new(
            PlatformKind.Discord,
            message.ExternalGroupId,
            message.ExternalGroupId,
            message.ExternalChannelId);

    internal sealed record PlatformMessageRefDto(
        string ExternalGroupId,
        string ExternalChannelId,
        string ExternalMessageId);
}
