using Dapper; using GmRelay.Shared.Domain; using GmRelay.Shared.Features.Sessions.RescheduleSession; using GmRelay.Shared.Platform; using Npgsql; using Telegram.Bot; namespace GmRelay.Bot.Features.Sessions.RescheduleSession; public sealed record HandleRescheduleVoteCommand( Guid OptionId, long TelegramUserId, string CallbackQueryId, long ChatId, int MessageId); public sealed class HandleRescheduleVoteHandler( NpgsqlDataSource dataSource, ITelegramBotClient bot, IPlatformMessenger messenger, ILogger logger) { public async Task HandleAsync(HandleRescheduleVoteCommand command, CancellationToken ct) { await using var connection = await dataSource.OpenConnectionAsync(ct); await using var transaction = await connection.BeginTransactionAsync(ct); var proposal = await connection.QuerySingleOrDefaultAsync( """ SELECT rp.id AS Id, rp.session_id AS SessionId, rp.voting_deadline_at AS VotingDeadlineAt, s.title AS Title, s.scheduled_at AS CurrentScheduledAt FROM reschedule_options ro JOIN reschedule_proposals rp ON rp.id = ro.proposal_id JOIN sessions s ON s.id = rp.session_id WHERE ro.id = @OptionId AND rp.status = 'Voting' """, new { command.OptionId }, transaction); if (proposal is null) { await AnswerAsync(command.CallbackQueryId, "Голосование уже завершено или не найдено.", ct); return; } if (proposal.VotingDeadlineAt <= DateTimeOffset.UtcNow) { await AnswerAsync(command.CallbackQueryId, "Дедлайн уже прошёл. Результаты скоро будут применены.", ct, showAlert: true); return; } var playerId = await connection.ExecuteScalarAsync( """ SELECT p.id FROM session_participants sp JOIN players p ON p.id = sp.player_id WHERE sp.session_id = @SessionId AND p.telegram_id = @TelegramUserId AND sp.is_gm = false AND sp.registration_status = @Active """, new { proposal.SessionId, command.TelegramUserId, Active = ParticipantRegistrationStatus.Active }, transaction); if (playerId is null) { await AnswerAsync(command.CallbackQueryId, "Вы не являетесь участником этой сессии.", ct); return; } await connection.ExecuteAsync( """ INSERT INTO reschedule_option_votes (proposal_id, player_id, option_id) VALUES (@ProposalId, @PlayerId, @OptionId) ON CONFLICT (proposal_id, player_id) DO UPDATE SET option_id = EXCLUDED.option_id, voted_at = now() """, new { ProposalId = proposal.Id, PlayerId = playerId.Value, command.OptionId }, transaction); var participants = (await connection.QueryAsync( """ 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( """ 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( """ 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(); await transaction.CommitAsync(ct); var voteText = HandleRescheduleTimeInputHandler.BuildVotingMessage( proposal.Title, proposal.CurrentScheduledAt, proposal.VotingDeadlineAt, options, participants, votes); var keyboard = HandleRescheduleTimeInputHandler.BuildVotingKeyboard(options); try { await bot.EditMessageText( chatId: command.ChatId, messageId: command.MessageId, text: voteText, parseMode: Telegram.Bot.Types.Enums.ParseMode.Html, replyMarkup: keyboard, cancellationToken: ct); } catch (Exception ex) { logger.LogWarning(ex, "Failed to update reschedule vote message for proposal {ProposalId}", proposal.Id); } await AnswerAsync(command.CallbackQueryId, "Ваш голос учтён. До дедлайна его можно изменить.", ct); } private Task AnswerAsync(string callbackQueryId, string text, CancellationToken ct, bool showAlert = false) => messenger.AnswerInteractionAsync(new PlatformInteractionReply(callbackQueryId, text, showAlert), ct); }