using Dapper;
using GmRelay.Shared.Domain;
using GmRelay.Shared.Rendering;
using Npgsql;
using Telegram.Bot;
using GmRelay.Bot.Infrastructure.Telegram;

namespace GmRelay.Bot.Features.Sessions.CreateSession;

public sealed record LeaveSessionCommand(
    Guid SessionId,
    long TelegramUserId,
    string CallbackQueryId,
    long ChatId,
    int MessageId);

internal sealed record LeaveSessionInfoDto(string Title, Guid BatchId, string Status, int? MaxPlayers);
internal sealed record LeaveSessionParticipantDto(Guid ParticipantRowId, string DisplayName, string RegistrationStatus);
internal sealed record LeaveSessionPromotionDto(Guid ParticipantRowId, string DisplayName);

public sealed class LeaveSessionHandler(
    NpgsqlDataSource dataSource,
    ITelegramBotClient bot,
    ILogger<LeaveSessionHandler> logger)
{
    public async Task HandleAsync(LeaveSessionCommand command, CancellationToken ct)
    {
        await using var connection = await dataSource.OpenConnectionAsync(ct);
        await using var transaction = await connection.BeginTransactionAsync(ct);
        var transactionCommitted = false;

        try
        {
            var session = await connection.QuerySingleOrDefaultAsync<LeaveSessionInfoDto>(
                """
                SELECT title AS Title,
                       batch_id AS BatchId,
                       status AS Status,
                       max_players AS MaxPlayers
                FROM sessions
                WHERE id = @SessionId
                FOR UPDATE
                """,
                new { command.SessionId },
                transaction);

            if (session is null)
            {
                await transaction.RollbackAsync(ct);
                await bot.AnswerCallbackQuery(command.CallbackQueryId, "Сессия не найдена.", cancellationToken: ct);
                return;
            }

            if (SessionStatus.IsCancelled(session.Status))
            {
                await transaction.RollbackAsync(ct);
                await bot.AnswerCallbackQuery(command.CallbackQueryId, "Сессия уже отменена.", cancellationToken: ct);
                return;
            }

            var participant = await connection.QuerySingleOrDefaultAsync<LeaveSessionParticipantDto>(
                """
                SELECT sp.id AS ParticipantRowId,
                       p.display_name AS DisplayName,
                       sp.registration_status AS RegistrationStatus
                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
                FOR UPDATE OF sp
                """,
                new { command.SessionId, command.TelegramUserId },
                transaction);

            if (participant is null)
            {
                await transaction.RollbackAsync(ct);
                await bot.AnswerCallbackQuery(command.CallbackQueryId, "Вы не записаны на эту сессию.", cancellationToken: ct);
                return;
            }

            await connection.ExecuteAsync(
                """
                DELETE FROM session_participants
                WHERE id = @ParticipantRowId
                """,
                new { participant.ParticipantRowId },
                transaction);

            var activeParticipantsAfterLeave = await connection.ExecuteScalarAsync<int>(
                """
                SELECT COUNT(*)
                FROM session_participants
                WHERE session_id = @SessionId
                  AND is_gm = false
                  AND registration_status = @Active
                """,
                new { command.SessionId, Active = ParticipantRegistrationStatus.Active },
                transaction);

            var waitlistedParticipants = await connection.ExecuteScalarAsync<int>(
                """
                SELECT COUNT(*)
                FROM session_participants
                WHERE session_id = @SessionId
                  AND is_gm = false
                  AND registration_status = @Waitlisted
                """,
                new { command.SessionId, Waitlisted = ParticipantRegistrationStatus.Waitlisted },
                transaction);

            string? promotedDisplayName = null;
            if (SessionCapacityRules.ShouldPromoteAfterParticipantLeaves(
                participant.RegistrationStatus,
                session.MaxPlayers,
                activeParticipantsAfterLeave,
                waitlistedParticipants))
            {
                var promoted = await connection.QuerySingleAsync<LeaveSessionPromotionDto>(
                    """
                    SELECT sp.id AS ParticipantRowId,
                           p.display_name AS DisplayName
                    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 = @Waitlisted
                    ORDER BY sp.created_at ASC, sp.id ASC
                    LIMIT 1
                    FOR UPDATE OF sp
                    """,
                    new { command.SessionId, Waitlisted = ParticipantRegistrationStatus.Waitlisted },
                    transaction);

                await connection.ExecuteAsync(
                    """
                    UPDATE session_participants
                    SET registration_status = @Active,
                        rsvp_status = @Pending,
                        responded_at = NULL
                    WHERE id = @ParticipantRowId
                    """,
                    new
                    {
                        promoted.ParticipantRowId,
                        Active = ParticipantRegistrationStatus.Active,
                        Pending = RsvpStatus.Pending
                    },
                    transaction);

                promotedDisplayName = promoted.DisplayName;
            }

            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 { session.BatchId },
                transaction)).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 { session.BatchId },
                transaction)).ToList();

            await transaction.CommitAsync(ct);
            transactionCommitted = true;

            var view = SessionBatchViewBuilder.Build(session.Title, batchSessions, batchParticipants);
            var renderResult = TelegramSessionBatchRenderer.Render(view);

            await BatchMessageEditor.EditBatchMessageAsync(
                bot,
                chatId: command.ChatId,
                messageId: command.MessageId,
                text: renderResult.Text,
                replyMarkup: renderResult.Markup,
                ct);

            var callbackText = participant.RegistrationStatus == ParticipantRegistrationStatus.Waitlisted
                ? "Вы удалены из листа ожидания."
                : promotedDisplayName is null
                    ? "Вы отписались от сессии."
                    : $"Вы отписались от сессии. Место получил(а) {promotedDisplayName}.";

            await bot.AnswerCallbackQuery(command.CallbackQueryId, callbackText, cancellationToken: ct);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Ошибка при самостоятельной отмене записи на сессию {SessionId}", command.SessionId);
            if (!transactionCommitted)
            {
                await transaction.RollbackAsync(ct);
            }

            var errorText = transactionCommitted
                ? "Запись снята, но не удалось обновить сообщение расписания."
                : "Произошла ошибка при отмене записи.";
            await bot.AnswerCallbackQuery(command.CallbackQueryId, errorText, cancellationToken: ct);
        }
    }
}
