using Dapper;
using GmRelay.Bot.Infrastructure.Telegram;
using GmRelay.Shared.Domain;
using GmRelay.Shared.Features.Notifications;
using GmRelay.Shared.Features.Sessions.RescheduleSession;
using GmRelay.Shared.Platform;
using GmRelay.Shared.Rendering;
using Npgsql;

namespace GmRelay.Bot.Features.Sessions.RescheduleSession;

internal sealed record TelegramProposalFieldsDto(
    int? VoteMessageId,
    int? BatchMessageId,
    long TelegramChatId,
    int? ThreadId);

public sealed class RescheduleVotingDeadlineService(
    NpgsqlDataSource dataSource,
    IPlatformMessenger messenger,
    PlatformDirectNotificationSender directSender,
    RescheduleVotingFinalizer finalizer,
    ILogger<RescheduleVotingDeadlineService> 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("Telegram", ct);

            foreach (var proposalId in proposalIds)
            {
                await FinalizeProposal(proposalId, ct);
            }
        }
        catch (OperationCanceledException) when (ct.IsCancellationRequested)
        {
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to process due reschedule voting proposals");
        }
    }

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

        if (result.SourcePlatform != "Telegram")
        {
            logger.LogInformation(
                "Skipping Telegram message handling for proposal {ProposalId} with source platform {SourcePlatform}",
                proposalId,
                result.SourcePlatform);
            return;
        }

        await using var connection = await dataSource.OpenConnectionAsync(ct);
        var telegramFields = await connection.QuerySingleOrDefaultAsync<TelegramProposalFieldsDto>(
            """
            SELECT rp.vote_message_id AS VoteMessageId,
                   s.batch_message_id AS BatchMessageId,
                   g.external_group_id::BIGINT AS TelegramChatId,
                   s.thread_id AS ThreadId
            FROM reschedule_proposals rp
            JOIN sessions s ON s.id = rp.session_id
            JOIN game_groups g ON g.id = s.group_id
            WHERE rp.id = @ProposalId
            """,
            new { ProposalId = proposalId });

        if (telegramFields is null)
        {
            logger.LogWarning("Could not find Telegram fields for proposal {ProposalId}", proposalId);
            return;
        }

        var directRecipients = result.Participants
            .Select(p => TelegramPlatformIds.User(p.TelegramId, p.DisplayName))
            .ToList();

        await TryUpdateVoteMessage(result, telegramFields, ct);

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

        var mode = SessionNotificationModeExtensions.FromDatabaseValue(result.NotificationMode);
        if (mode.ShouldSendDirectMessages())
        {
            await SendDirectResult(result, directRecipients, ct);
        }

        logger.LogInformation(
            "Updated Telegram messages for finalized reschedule proposal {ProposalId} for session {SessionId}",
            result.ProposalId,
            result.SessionId);
    }

    private async Task TryUpdateVoteMessage(
        RescheduleVotingFinalizerResult result,
        TelegramProposalFieldsDto telegramFields,
        CancellationToken ct)
    {
        if (telegramFields.VoteMessageId is null)
            return;

        try
        {
            await messenger.UpdateRescheduleVoteAsync(
                new PlatformRescheduleVoteUpdate(
                    TelegramPlatformIds.Group(telegramFields.TelegramChatId, telegramFields.ThreadId),
                    TelegramPlatformIds.Message(
                        telegramFields.TelegramChatId,
                        telegramFields.ThreadId,
                        telegramFields.VoteMessageId.Value),
                    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 finalized reschedule vote message for proposal {ProposalId}", result.ProposalId);
        }
    }

    private async Task TryUpdateBatchMessage(
        RescheduleVotingFinalizerResult result,
        TelegramProposalFieldsDto telegramFields,
        CancellationToken ct)
    {
        try
        {
            await using var connection = await dataSource.OpenConnectionAsync(ct);

            var batchSessions = (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 batchParticipants = (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 sp.player_id = p.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, sp.responded_at ASC, p.created_at ASC
                """,
                new { result.BatchId })).ToList();

            if (telegramFields.BatchMessageId.HasValue)
            {
                var view = SessionBatchViewBuilder.Build(result.Title, batchSessions, batchParticipants);

                await messenger.UpdateScheduleAsync(
                    new PlatformScheduleMessage(
                        TelegramPlatformIds.Group(telegramFields.TelegramChatId, telegramFields.ThreadId),
                        view,
                        TelegramPlatformIds.Message(telegramFields.TelegramChatId, telegramFields.ThreadId, telegramFields.BatchMessageId.Value)),
                    ct);
            }
            else
            {
                await messenger.SendGroupMessageAsync(
                    TelegramPlatformIds.Group(telegramFields.TelegramChatId, telegramFields.ThreadId),
                    $"Расписание обновлено после голосования за перенос сессии \"{System.Net.WebUtility.HtmlEncode(result.Title)}\".",
                    ct);
            }
        }
        catch (Exception ex)
        {
            logger.LogWarning(ex, "Failed to update batch message for finalized proposal {ProposalId}", result.ProposalId);
        }
    }

    private async Task SendDirectResult(
        RescheduleVotingFinalizerResult result,
        IReadOnlyList<PlatformUser> recipients,
        CancellationToken ct)
    {
        await directSender.SendAsync(
            result.SelectedOption is not null
                ? PlatformDirectSessionNotificationKind.RescheduleApproved
                : PlatformDirectSessionNotificationKind.RescheduleRejected,
            recipients,
            result.SessionId,
            result.Title,
            result.SelectedOption?.ProposedAt.UtcDateTime ?? result.CurrentScheduledAt,
            joinLink: null,
            actorDisplayName: null,
            reason: result.SelectedOption is null ? result.Decision.Reason : null,
            ct);
    }
}
