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 PromoteWaitlistedPlayerCommand(
    Guid SessionId,
    long TelegramUserId,
    string CallbackQueryId,
    long ChatId,
    int MessageId);

internal sealed record PromoteWaitlistSessionDto(string Title, Guid BatchId, int? BatchMessageId, bool CanManage, int? MaxPlayers);
internal sealed record WaitlistedParticipantDto(Guid ParticipantRowId, string DisplayName);

public sealed class PromoteWaitlistedPlayerHandler(
    NpgsqlDataSource dataSource,
    ITelegramBotClient bot,
    ILogger<PromoteWaitlistedPlayerHandler> logger)
{
    public async Task HandleAsync(PromoteWaitlistedPlayerCommand 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<PromoteWaitlistSessionDto>(
                """
                SELECT s.title AS Title,
                       s.batch_id AS BatchId,
                       s.batch_message_id AS BatchMessageId,
                       s.max_players AS MaxPlayers,
                       EXISTS (
                           SELECT 1
                           FROM group_managers gm
                           JOIN players p ON p.id = gm.player_id
                           WHERE gm.group_id = s.group_id
                             AND p.telegram_id = @TelegramUserId
                       ) AS CanManage
                FROM sessions s
                WHERE s.id = @SessionId
                FOR UPDATE
                """,
                new { command.SessionId, command.TelegramUserId },
                transaction);

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

            if (!session.CanManage)
            {
                await transaction.RollbackAsync(ct);
                await bot.AnswerCallbackQuery(command.CallbackQueryId, "Только owner или co-GM может поднимать игроков из листа ожидания.", showAlert: true, cancellationToken: ct);
                return;
            }

            var activeParticipants = 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);

            if (waitlistedParticipants == 0)
            {
                await transaction.RollbackAsync(ct);
                await bot.AnswerCallbackQuery(command.CallbackQueryId, "Лист ожидания пуст.", cancellationToken: ct);
                return;
            }

            if (!SessionCapacityRules.CanPromoteWaitlistedPlayer(session.MaxPlayers, activeParticipants, waitlistedParticipants))
            {
                await transaction.RollbackAsync(ct);
                await bot.AnswerCallbackQuery(command.CallbackQueryId, "Нет свободных мест. Увеличьте лимит перед повышением игрока.", showAlert: true, cancellationToken: ct);
                return;
            }

            var promoted = await connection.QuerySingleAsync<WaitlistedParticipantDto>(
                """
                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);

            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: session.BatchMessageId ?? command.MessageId,
                text: renderResult.Text,
                replyMarkup: renderResult.Markup,
                ct);

            await bot.AnswerCallbackQuery(command.CallbackQueryId, $"{promoted.DisplayName} переведен(а) в основной состав.", 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);
        }
    }
}
