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

namespace GmRelay.Shared.Features.Sessions.CreateSession;

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

public sealed record SessionInteractionResult(
    string ReplyText,
    SessionBatchViewModel? UpdatedView = null);

// DTOs for AOT compilation
internal sealed record JoinSessionBatchDto(Guid BatchId, string Title, string Status, int? MaxPlayers);

public sealed class JoinSessionHandler(
    NpgsqlDataSource dataSource,
    IPlatformMessenger messenger,
    IScheduleMessageUpdateLock scheduleUpdateLock,
    ILogger<JoinSessionHandler> logger)
{
    public async Task<SessionInteractionResult> HandleAsync(JoinSessionCommand 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
        {
            // 1. Убеждаемся, что игрок есть в базе
            var platform = command.User.Platform.ToString();

            var playerId = await connection.ExecuteScalarAsync<Guid>(
                @"INSERT INTO players (display_name, platform, external_user_id, external_username)
                  VALUES (@Name, @Platform, @ExternalUserId, @ExternalUsername)
                  ON CONFLICT (platform, external_user_id)
                  WHERE platform IS NOT NULL AND external_user_id IS NOT NULL
                  DO UPDATE
                  SET display_name = EXCLUDED.display_name,
                      external_username = EXCLUDED.external_username
                  RETURNING id;",
                new
                {
                    Name = command.User.DisplayName,
                    Platform = platform,
                    command.User.ExternalUserId,
                    command.User.ExternalUsername
                },
                transaction);

            // 2. Блокируем сессию на время расчета мест, чтобы параллельные нажатия не переполнили состав.
            var batchInfo = await connection.QuerySingleOrDefaultAsync<JoinSessionBatchDto>(
                @"SELECT batch_id as BatchId, title as Title, status as Status, max_players as MaxPlayers
                  FROM sessions
                  WHERE id = @SessionId
                  FOR UPDATE",
                new { command.SessionId },
                transaction);

            if (batchInfo is null)
            {
                await transaction.RollbackAsync(ct);
                return await AnswerAsync(command.InteractionId, "Сессия не найдена.", ct);
            }

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

            var existingRegistrationStatus = await connection.ExecuteScalarAsync<string?>(
                """
                SELECT sp.registration_status
                FROM session_participants sp
                WHERE sp.session_id = @SessionId
                  AND sp.player_id = @PlayerId
                  AND sp.is_gm = false
                """,
                new { command.SessionId, PlayerId = playerId },
                transaction);

            if (existingRegistrationStatus is not null)
            {
                await transaction.RollbackAsync(ct);
                var alreadyText = existingRegistrationStatus == ParticipantRegistrationStatus.Waitlisted
                    ? "Вы уже в листе ожидания!"
                    : "Вы уже записаны!";
                return await AnswerAsync(command.InteractionId, alreadyText, ct);
            }

            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 registrationStatus = SessionCapacityRules.DecideJoinStatus(batchInfo.MaxPlayers, activeParticipants);

            // 3. Добавляем в основной состав или лист ожидания. RSVP остается Pending до финального подтверждения.
            var inserted = await connection.ExecuteAsync(
                @"INSERT INTO session_participants (session_id, player_id, is_gm, rsvp_status, registration_status)
                  VALUES (@SessionId, @PlayerId, false, @Pending, @RegistrationStatus)
                  ON CONFLICT (session_id, player_id) DO NOTHING;",
                new
                {
                    command.SessionId,
                    PlayerId = playerId,
                    Pending = RsvpStatus.Pending,
                    RegistrationStatus = registrationStatus
                },
                transaction);

            if (inserted == 0)
            {
                await transaction.RollbackAsync(ct);
                return await AnswerAsync(command.InteractionId, "Вы уже записаны!", ct);
            }

            // Загружаем весь батч для перерисовки
            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 { BatchId = batchInfo.BatchId }, transaction);

            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 { BatchId = batchInfo.BatchId }, transaction);

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

            // 4. Перерисовываем сообщение
            var view = SessionBatchViewBuilder.Build(batchInfo.Title, batchSessions.ToList(), batchParticipants.ToList());
            if (!command.DeferScheduleUpdate)
            {
                await messenger.UpdateScheduleAsync(
                    new PlatformScheduleMessage(
                        command.Group,
                        view,
                        command.ScheduleMessage),
                    ct);
            }

            var callbackText = registrationStatus == ParticipantRegistrationStatus.Waitlisted
                ? "Основной состав заполнен. Вы добавлены в лист ожидания."
                : "Вы успешно записаны!";
            return await AnswerAsync(command.InteractionId, callbackText, ct, view);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Ошибка при добавлении игрока к сессии");
            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);
    }
}
