using System.Globalization; using Dapper; using GmRelay.Shared.Domain; using GmRelay.Shared.Features.Notifications; using GmRelay.Shared.Platform; using Microsoft.Extensions.Logging; using Npgsql; namespace GmRelay.Shared.Features.Confirmation.SendConfirmation; internal sealed record ConfirmationSessionRow( Guid Id, string Title, DateTime ScheduledAt, Guid GroupId, string Platform, string ExternalGroupId, string DisplayName, string? ExternalChannelId, int? ThreadId, string NotificationMode); internal sealed record ConfirmationParticipantRow( string Platform, string ExternalUserId, string DisplayName, string? ExternalUsername, string RsvpStatus, string RegistrationStatus, bool IsGm); public sealed class SendConfirmationHandler( NpgsqlDataSource dataSource, IPlatformMessenger messenger, PlatformDirectNotificationSender directSender, ILogger logger) : ISendConfirmationHandler { public async Task HandleAsync(Guid sessionId, CancellationToken ct) { await using var connection = await dataSource.OpenConnectionAsync(ct); var session = await connection.QuerySingleOrDefaultAsync( """ SELECT s.id, s.title, s.scheduled_at AS ScheduledAt, s.group_id AS GroupId, COALESCE(g.platform, 'Telegram') AS Platform, COALESCE(g.external_group_id, g.telegram_chat_id::TEXT) AS ExternalGroupId, g.name AS DisplayName, COALESCE(g.external_channel_id, g.telegram_chat_id::TEXT) AS ExternalChannelId, s.thread_id AS ThreadId, s.notification_mode AS NotificationMode FROM sessions s JOIN game_groups g ON g.id = s.group_id WHERE s.id = @SessionId AND s.status = @Planned """, new { SessionId = sessionId, Planned = SessionStatus.Planned }); if (session is null) { logger.LogWarning("Session {SessionId} not found or not in Planned status", sessionId); return; } var participants = (await connection.QueryAsync( """ SELECT COALESCE(p.platform, 'Telegram') AS Platform, COALESCE(p.external_user_id, p.telegram_id::TEXT) AS ExternalUserId, p.display_name AS DisplayName, COALESCE(p.external_username, p.telegram_username) AS ExternalUsername, sp.rsvp_status AS RsvpStatus, sp.registration_status AS RegistrationStatus, sp.is_gm AS IsGm 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 sp.created_at ASC """, new { SessionId = sessionId, Active = ParticipantRegistrationStatus.Active })) .Select(ToParticipant) .ToList(); if (participants.Count == 0) { logger.LogWarning("Session {SessionId} has no non-GM participants", sessionId); return; } var group = CreateGroup(session); var message = await messenger.SendConfirmationRequestAsync( new PlatformConfirmationRequest( group, session.Id, session.Title, session.ScheduledAt, participants), ct); await connection.ExecuteAsync( """ UPDATE sessions SET status = @Status, confirmation_message_id = @MessageId, confirmation_sent_at = now(), updated_at = now() WHERE id = @SessionId AND confirmation_sent_at IS NULL """, new { SessionId = sessionId, Status = SessionStatus.ConfirmationSent, MessageId = TryGetTelegramMessageId(message) }); await PersistPlatformMessageAsync( connection, message, session.GroupId, session.Id, batchId: null, purpose: "confirmation"); var mode = SessionNotificationModeExtensions.FromDatabaseValue(session.NotificationMode); if (mode.ShouldSendDirectMessages()) { await directSender.SendAsync( PlatformDirectSessionNotificationKind.ConfirmationRequest, participants.Select(p => p.User), session.Id, session.Title, session.ScheduledAt, joinLink: null, actorDisplayName: null, reason: null, ct); } logger.LogInformation( "Confirmation sent for session {SessionId} ({Title}), platform={Platform}, message_id={MessageId}", sessionId, session.Title, message.Platform, message.ExternalMessageId); } private static PlatformSessionParticipant ToParticipant(ConfirmationParticipantRow row) => new( new PlatformUser( ParsePlatform(row.Platform), row.ExternalUserId, row.DisplayName, row.ExternalUsername), row.RsvpStatus, row.RegistrationStatus, row.IsGm); private static PlatformGroup CreateGroup(ConfirmationSessionRow row) => new( ParsePlatform(row.Platform), row.ExternalGroupId, row.DisplayName, row.ExternalChannelId, row.ThreadId?.ToString(CultureInfo.InvariantCulture)); private static PlatformKind ParsePlatform(string platform) => Enum.Parse(platform, ignoreCase: true); private static int? TryGetTelegramMessageId(PlatformMessageRef message) => message.Platform == PlatformKind.Telegram && int.TryParse(message.ExternalMessageId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var messageId) ? messageId : null; private static Task PersistPlatformMessageAsync( NpgsqlConnection connection, PlatformMessageRef message, Guid groupId, Guid? sessionId, Guid? batchId, string purpose) => connection.ExecuteAsync( """ INSERT INTO platform_messages ( platform, group_id, batch_id, session_id, external_channel_id, external_thread_id, external_message_id, purpose) VALUES ( @Platform, @GroupId, @BatchId, @SessionId, @ExternalChannelId, @ExternalThreadId, @ExternalMessageId, @Purpose) """, new { Platform = message.Platform.ToString(), GroupId = groupId, BatchId = batchId, SessionId = sessionId, ExternalChannelId = message.ExternalGroupId, message.ExternalThreadId, message.ExternalMessageId, Purpose = purpose }); }