using Dapper;
using GmRelay.Bot.Features.Notifications;
using GmRelay.Bot.Infrastructure.Scheduling;
using GmRelay.Shared.Domain;
using GmRelay.Shared.Rendering;
using Npgsql;
using Telegram.Bot;
using Telegram.Bot.Types.Enums;
using GmRelay.Bot.Infrastructure.Telegram;

namespace GmRelay.Bot.Features.Sessions.RescheduleSession;

internal sealed record DueRescheduleProposalDto(
    Guid Id,
    Guid SessionId,
    DateTimeOffset VotingDeadlineAt,
    string Title,
    DateTime CurrentScheduledAt,
    Guid BatchId,
    int? BatchMessageId,
    int? VoteMessageId,
    long TelegramChatId,
    string NotificationMode);

public sealed class RescheduleVotingDeadlineService(
    NpgsqlDataSource dataSource,
    ITelegramBotClient bot,
    DirectSessionNotificationSender directSender,
    ISystemClock clock,
    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
        {
            await using var connection = await dataSource.OpenConnectionAsync(ct);
            var proposalIds = (await connection.QueryAsync<Guid>(
                """
                SELECT id
                FROM reschedule_proposals
                WHERE status = 'Voting'
                  AND voting_deadline_at IS NOT NULL
                  AND voting_deadline_at <= @Now
                ORDER BY voting_deadline_at
                LIMIT 25
                """,
                new { Now = clock.UtcNow.UtcDateTime })).ToList();

            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)
    {
        await using var connection = await dataSource.OpenConnectionAsync(ct);
        await using var transaction = await connection.BeginTransactionAsync(ct);

        var proposal = await connection.QuerySingleOrDefaultAsync<DueRescheduleProposalDto>(
            """
            SELECT rp.id AS Id,
                   rp.session_id AS SessionId,
                   rp.voting_deadline_at AS VotingDeadlineAt,
                   rp.vote_message_id AS VoteMessageId,
                   s.title AS Title,
                   s.scheduled_at AS CurrentScheduledAt,
                   s.batch_id AS BatchId,
                   s.batch_message_id AS BatchMessageId,
                   s.notification_mode AS NotificationMode,
                   g.telegram_chat_id AS TelegramChatId
            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
              AND rp.status = 'Voting'
              AND rp.voting_deadline_at IS NOT NULL
              AND rp.voting_deadline_at <= @Now
            FOR UPDATE
            """,
            new { ProposalId = proposalId, Now = clock.UtcNow.UtcDateTime },
            transaction);

        if (proposal is null)
            return;

        var participants = (await connection.QueryAsync<VoteParticipantDto>(
            """
            SELECT p.id AS PlayerId,
                   p.display_name AS DisplayName,
                   p.telegram_username AS TelegramUsername,
                   p.telegram_id AS TelegramId
            FROM session_participants sp
            JOIN players p ON p.id = sp.player_id
            WHERE sp.session_id = @SessionId
              AND sp.is_gm = false
              AND sp.registration_status = @Active
            ORDER BY p.display_name
            """,
            new { proposal.SessionId, Active = ParticipantRegistrationStatus.Active },
            transaction)).ToList();

        var options = (await connection.QueryAsync<RescheduleOptionDto>(
            """
            SELECT id AS OptionId,
                   display_order AS DisplayOrder,
                   proposed_at AS ProposedAt
            FROM reschedule_options
            WHERE proposal_id = @ProposalId
            ORDER BY display_order
            """,
            new { ProposalId = proposal.Id },
            transaction)).ToList();

        var votes = (await connection.QueryAsync<RescheduleOptionVoteDto>(
            """
            SELECT rov.option_id AS OptionId,
                   p.id AS PlayerId,
                   p.display_name AS DisplayName,
                   p.telegram_username AS TelegramUsername
            FROM reschedule_option_votes rov
            JOIN players p ON p.id = rov.player_id
            WHERE rov.proposal_id = @ProposalId
            ORDER BY rov.voted_at, p.display_name
            """,
            new { ProposalId = proposal.Id },
            transaction)).ToList();

        var voteCounts = options
            .Select(option => new RescheduleOptionVoteCount(
                option.OptionId,
                votes.Count(vote => vote.OptionId == option.OptionId)))
            .ToList();
        var decision = RescheduleVoteRules.SelectWinner(voteCounts);
        var selectedOption = decision.SelectedOptionId is { } selectedOptionId
            ? options.Single(x => x.OptionId == selectedOptionId)
            : null;

        if (selectedOption is not null)
        {
            await connection.ExecuteAsync(
                """
                UPDATE sessions
                SET scheduled_at = @NewTime,
                    status = @Status,
                    confirmation_message_id = NULL,
                    confirmation_sent_at = NULL,
                    link_message_id = NULL,
                    one_hour_reminder_processed_at = NULL,
                    updated_at = now()
                WHERE id = @SessionId
                """,
                new { NewTime = selectedOption.ProposedAt, proposal.SessionId, Status = SessionStatus.Planned },
                transaction);

            await connection.ExecuteAsync(
                """
                UPDATE session_participants
                SET rsvp_status = 'Pending',
                    responded_at = NULL
                WHERE session_id = @SessionId
                  AND is_gm = false
                  AND registration_status = @Active
                """,
                new { proposal.SessionId, Active = ParticipantRegistrationStatus.Active },
                transaction);

            await connection.ExecuteAsync(
                """
                UPDATE reschedule_proposals
                SET status = 'Approved',
                    selected_option_id = @SelectedOptionId,
                    proposed_at = @ProposedAt
                WHERE id = @ProposalId
                """,
                new
                {
                    ProposalId = proposal.Id,
                    SelectedOptionId = selectedOption.OptionId,
                    ProposedAt = selectedOption.ProposedAt
                },
                transaction);
        }
        else
        {
            await connection.ExecuteAsync(
                "UPDATE reschedule_proposals SET status = 'Rejected' WHERE id = @ProposalId",
                new { ProposalId = proposal.Id },
                transaction);
        }

        var directRecipients = participants
            .Select(p => new DirectNotificationRecipient(p.TelegramId, p.DisplayName))
            .ToList();

        await transaction.CommitAsync(ct);

        await TryUpdateVoteMessage(proposal, options, participants, votes, decision, selectedOption, ct);

        if (selectedOption is not null)
        {
            await TryUpdateBatchMessage(proposal, ct);
        }

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

        logger.LogInformation(
            "Finalized reschedule proposal {ProposalId} for session {SessionId} with outcome {Outcome}",
            proposal.Id,
            proposal.SessionId,
            decision.Outcome);
    }

    private async Task TryUpdateVoteMessage(
        DueRescheduleProposalDto proposal,
        IReadOnlyList<RescheduleOptionDto> options,
        IReadOnlyList<VoteParticipantDto> participants,
        IReadOnlyList<RescheduleOptionVoteDto> votes,
        RescheduleVoteDecision decision,
        RescheduleOptionDto? selectedOption,
        CancellationToken ct)
    {
        if (proposal.VoteMessageId is null)
            return;

        try
        {
            var resultText = selectedOption is not null
                ? $"✅ <b>Голосование завершено.</b>\nПобедил вариант {selectedOption.DisplayOrder}: <b>{selectedOption.ProposedAt.FormatMoscow()}</b> (МСК)."
                : $"❌ <b>Голосование завершено.</b>\n{System.Net.WebUtility.HtmlEncode(decision.Reason)}";

            var text = $"""
                {HandleRescheduleTimeInputHandler.BuildVotingMessage(
                    proposal.Title,
                    proposal.CurrentScheduledAt,
                    proposal.VotingDeadlineAt,
                    options,
                    participants,
                    votes)}

                {resultText}
                """;

            await bot.EditMessageText(
                chatId: proposal.TelegramChatId,
                messageId: proposal.VoteMessageId.Value,
                text: text,
                parseMode: ParseMode.Html,
                cancellationToken: ct);
        }
        catch (Exception ex)
        {
            logger.LogWarning(ex, "Failed to update finalized reschedule vote message for proposal {ProposalId}", proposal.Id);
        }
    }

    private async Task TryUpdateBatchMessage(DueRescheduleProposalDto proposal, 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 { proposal.BatchId })).ToList();

            var batchParticipants = (await connection.QueryAsync<ParticipantBatchDto>(
                """
                SELECT sp.session_id AS SessionId,
                       p.display_name AS DisplayName,
                       p.telegram_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 { proposal.BatchId })).ToList();

            if (proposal.BatchMessageId.HasValue)
            {
                var view = SessionBatchViewBuilder.Build(proposal.Title, batchSessions, batchParticipants);
                var renderResult = TelegramSessionBatchRenderer.Render(view);

                await BatchMessageEditor.EditBatchMessageAsync(
                    bot,
                    chatId: proposal.TelegramChatId,
                    messageId: proposal.BatchMessageId.Value,
                    text: renderResult.Text,
                    replyMarkup: renderResult.Markup,
                    ct);
            }
            else
            {
                await bot.SendMessage(
                    chatId: proposal.TelegramChatId,
                    text: $"📣 Расписание обновлено после голосования за перенос сессии «{System.Net.WebUtility.HtmlEncode(proposal.Title)}».",
                    parseMode: ParseMode.Html,
                    cancellationToken: ct);
            }
        }
        catch (Exception ex)
        {
            logger.LogWarning(ex, "Failed to update batch message for finalized proposal {ProposalId}", proposal.Id);
        }
    }

    private async Task SendDirectResult(
        DueRescheduleProposalDto proposal,
        IReadOnlyList<DirectNotificationRecipient> recipients,
        RescheduleVoteDecision decision,
        RescheduleOptionDto? selectedOption,
        CancellationToken ct)
    {
        var htmlText = selectedOption is not null
            ? $"""
                ✅ <b>Сессия перенесена по итогам голосования</b>

                📌 <b>{System.Net.WebUtility.HtmlEncode(proposal.Title)}</b>
                📅 Новое время: <b>{selectedOption.ProposedAt.FormatMoscow()}</b> (МСК)
                """
            : $"""
                ❌ <b>Перенос сессии отклонён по итогам голосования</b>

                📌 <b>{System.Net.WebUtility.HtmlEncode(proposal.Title)}</b>
                📅 Время остаётся прежним: <b>{proposal.CurrentScheduledAt.FormatMoscow()}</b> (МСК)
                Причина: {System.Net.WebUtility.HtmlEncode(decision.Reason)}
                """;

        await directSender.SendAsync(
            recipients,
            htmlText,
            selectedOption is not null ? "reschedule-vote-approved" : "reschedule-vote-rejected",
            proposal.SessionId,
            ct);
    }
}
