using Dapper;
using GmRelay.Shared.Domain;
using GmRelay.Shared.Platform;
using GmRelay.Shared.Rendering;
using Microsoft.Extensions.Logging;
using Npgsql;

namespace GmRelay.Shared.Features.Sessions.CreateSession;

public sealed record LeaveSessionCommand(
    Guid SessionId,
    PlatformUser User,
    string InteractionId,
    PlatformGroup Group,
    PlatformMessageRef ScheduleMessage,
    bool DeferScheduleUpdate = false);

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,
    IPlatformMessenger messenger,
    IScheduleMessageUpdateLock scheduleUpdateLock,
    ILogger<LeaveSessionHandler> logger)
{
    public async Task<SessionInteractionResult> HandleAsync(LeaveSessionCommand command, CancellationToken ct)
    {
        await using var updateLock = await scheduleUpdateLock.AcquireAsync(command.ScheduleMessage, 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);
                return await AnswerAsync(command.InteractionId, "Сессия не найдена.", ct);
            }

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

            var platform = command.User.Platform.ToString();

            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.platform = @Platform
                  AND p.external_user_id = @ExternalUserId
                  AND sp.is_gm = false
                FOR UPDATE OF sp
                """,
                new { command.SessionId, Platform = platform, command.User.ExternalUserId },
                transaction);

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

            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.external_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);
            if (!command.DeferScheduleUpdate)
            {
                await messenger.UpdateScheduleAsync(
                    new PlatformScheduleMessage(
                        command.Group,
                        view,
                        command.ScheduleMessage),
                    ct);
            }

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

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

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

    private async Task<SessionInteractionResult> AnswerAsync(
        string interactionId,
        string text,
        CancellationToken ct,
        SessionBatchViewModel? updatedView = null)
    {
        await messenger.AnswerInteractionAsync(new PlatformInteractionReply(interactionId, text), ct);
        return new SessionInteractionResult(text, updatedView);
    }
}
