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.HandleRsvp;

public sealed record HandleRsvpCommand(
    Guid SessionId,
    PlatformUser User,
    string Status,
    string InteractionId,
    PlatformGroup Group,
    PlatformMessageRef ConfirmationMessage);

internal sealed record RsvpCounts(int Total, int Confirmed, int Declined);

internal sealed record RsvpSessionContext(
    Guid GroupId,
    string Title,
    DateTime ScheduledAt,
    string Status);

internal sealed record ParticipantRsvpRow(
    string Platform,
    string ExternalUserId,
    string DisplayName,
    string? ExternalUsername,
    string RsvpStatus,
    string RegistrationStatus,
    bool IsGm);

internal sealed record RsvpRecipientRow(
    string Platform,
    string ExternalUserId,
    string DisplayName,
    string? ExternalUsername);

public sealed class HandleRsvpHandler(
    NpgsqlDataSource dataSource,
    IPlatformMessenger messenger,
    ILogger<HandleRsvpHandler> logger)
{
    public async Task HandleAsync(HandleRsvpCommand command, CancellationToken ct)
    {
        await using var connection = await dataSource.OpenConnectionAsync(ct);
        await using var transaction = await connection.BeginTransactionAsync(ct);

        var participantExists = await connection.ExecuteScalarAsync<bool>(
            """
            SELECT EXISTS (
                SELECT 1
                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
                  AND sp.registration_status = @Active
            )
            """,
            new
            {
                command.SessionId,
                Platform = command.User.Platform.ToString(),
                command.User.ExternalUserId,
                Active = ParticipantRegistrationStatus.Active
            },
            transaction);

        if (!participantExists)
        {
            await messenger.AnswerInteractionAsync(
                new PlatformInteractionReply(
                    command.InteractionId,
                    "Вы не являетесь участником этой сессии."),
                ct);
            return;
        }

        var updated = await connection.ExecuteAsync(
            """
            UPDATE session_participants
            SET    rsvp_status  = @Status,
                   responded_at = now()
            WHERE  session_id = @SessionId
              AND  player_id = (
                    SELECT id
                    FROM players
                    WHERE platform = @Platform
                      AND external_user_id = @ExternalUserId
                    LIMIT 1
              )
              AND  registration_status = @Active
              AND  rsvp_status != @Status
            """,
            new
            {
                command.SessionId,
                command.Status,
                Platform = command.User.Platform.ToString(),
                command.User.ExternalUserId,
                Active = ParticipantRegistrationStatus.Active
            },
            transaction);

        if (updated == 0)
        {
            var alreadyText = command.Status == RsvpStatus.Confirmed
                ? "Вы уже подтвердили участие."
                : "Вы уже отказались от участия.";

            await messenger.AnswerInteractionAsync(
                new PlatformInteractionReply(command.InteractionId, alreadyText),
                ct);
            return;
        }

        var session = await connection.QuerySingleAsync<RsvpSessionContext>(
            """
            SELECT s.group_id AS GroupId,
                   s.title,
                   s.scheduled_at AS ScheduledAt,
                   s.status AS Status
            FROM sessions s
            WHERE s.id = @SessionId
            """,
            new { command.SessionId },
            transaction);

        if (command.Status == RsvpStatus.Declined)
        {
            var decision = RsvpFlowRules.Evaluate(
                command.Status,
                session.Status,
                totalParticipants: 0,
                confirmedParticipants: 0);

            if (decision.ShouldRevertSessionToConfirmationSent)
            {
                await connection.ExecuteAsync(
                    """
                    UPDATE sessions
                    SET    status = @ConfirmationSent, updated_at = now()
                    WHERE  id = @SessionId AND status = @Confirmed
                    """,
                    new
                    {
                        command.SessionId,
                        ConfirmationSent = SessionStatus.ConfirmationSent,
                        Confirmed = SessionStatus.Confirmed
                    },
                    transaction);
            }

            var gmRecipients = (await GetGmRecipientsAsync(connection, session.GroupId, transaction))
                .ToList();

            await transaction.CommitAsync(ct);

            if (gmRecipients.Count > 0)
            {
                await messenger.SendRsvpOutcomeAsync(
                    new PlatformRsvpOutcomeNotification(
                        PlatformRsvpOutcomeKind.GmPlayerDeclined,
                        Group: null,
                        gmRecipients,
                        command.SessionId,
                        session.Title,
                        session.ScheduledAt,
                        ActorDisplayName: command.User.DisplayName),
                    ct);
            }

            await messenger.AnswerInteractionAsync(
                new PlatformInteractionReply(command.InteractionId, decision.CallbackText),
                ct);
        }
        else
        {
            var counts = await connection.QuerySingleAsync<RsvpCounts>(
                """
                SELECT
                    count(*)                                         AS Total,
                    count(*) FILTER (WHERE rsvp_status = @Confirmed) AS Confirmed,
                    count(*) FILTER (WHERE rsvp_status = @Declined)  AS Declined
                FROM session_participants
                WHERE session_id = @SessionId AND is_gm = false
                  AND registration_status = @Active
                """,
                new
                {
                    command.SessionId,
                    Confirmed = RsvpStatus.Confirmed,
                    Declined = RsvpStatus.Declined,
                    Active = ParticipantRegistrationStatus.Active
                },
                transaction);

            var decision = RsvpFlowRules.Evaluate(command.Status, session.Status, counts.Total, counts.Confirmed);

            if (decision.ShouldMarkSessionConfirmed)
            {
                await connection.ExecuteAsync(
                    """
                    UPDATE sessions
                    SET    status = @Confirmed, updated_at = now()
                    WHERE  id = @SessionId
                    """,
                    new { command.SessionId, Confirmed = SessionStatus.Confirmed },
                    transaction);
            }

            var gmRecipients = decision.ShouldNotifyGm
                ? (await GetGmRecipientsAsync(connection, session.GroupId, transaction)).ToList()
                : [];

            await transaction.CommitAsync(ct);

            if (decision.ShouldNotifyGroup)
            {
                await messenger.SendRsvpOutcomeAsync(
                    new PlatformRsvpOutcomeNotification(
                        PlatformRsvpOutcomeKind.GroupAllConfirmed,
                        command.Group,
                        [],
                        command.SessionId,
                        session.Title,
                        session.ScheduledAt),
                    ct);
            }

            if (decision.ShouldNotifyGm && gmRecipients.Count > 0)
            {
                await messenger.SendRsvpOutcomeAsync(
                    new PlatformRsvpOutcomeNotification(
                        PlatformRsvpOutcomeKind.GmAllConfirmed,
                        Group: null,
                        gmRecipients,
                        command.SessionId,
                        session.Title,
                        session.ScheduledAt),
                    ct);
            }

            await messenger.AnswerInteractionAsync(
                new PlatformInteractionReply(command.InteractionId, decision.CallbackText),
                ct);
        }

        await UpdateConfirmationMessage(command, session, ct);
    }

    private async Task UpdateConfirmationMessage(
        HandleRsvpCommand command,
        RsvpSessionContext session,
        CancellationToken ct)
    {
        try
        {
            await using var connection = await dataSource.OpenConnectionAsync(ct);

            var participants = (await connection.QueryAsync<ParticipantRsvpRow>(
                """
                SELECT p.platform AS Platform,
                       p.external_user_id AS ExternalUserId,
                       p.display_name AS DisplayName,
                       p.external_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.responded_at NULLS LAST
                """,
                new { command.SessionId, Active = ParticipantRegistrationStatus.Active }))
                .Select(ToParticipant)
                .ToList();

            var disableActions = participants.Count > 0 &&
                participants.All(participant => participant.RsvpStatus == RsvpStatus.Confirmed);

            await messenger.UpdateConfirmationRequestAsync(
                new PlatformRsvpMessageUpdate(
                    new PlatformConfirmationRequest(
                        command.Group,
                        command.SessionId,
                        session.Title,
                        session.ScheduledAt,
                        participants,
                        command.ConfirmationMessage),
                    disableActions),
                ct);
        }
        catch (Exception ex)
        {
            logger.LogWarning(ex, "Failed to update confirmation message for session {SessionId}", command.SessionId);
        }
    }

    private static async Task<IEnumerable<PlatformUser>> GetGmRecipientsAsync(
        NpgsqlConnection connection,
        Guid groupId,
        NpgsqlTransaction transaction)
    {
        var rows = await connection.QueryAsync<RsvpRecipientRow>(
            """
            SELECT DISTINCT
                   p.platform AS Platform,
                   p.external_user_id AS ExternalUserId,
                   p.display_name AS DisplayName,
                   p.external_username AS ExternalUsername
            FROM group_managers gm
            JOIN players p ON p.id = gm.player_id
            WHERE gm.group_id = @GroupId
            """,
            new { GroupId = groupId },
            transaction);

        return rows.Select(row => new PlatformUser(
            ParsePlatform(row.Platform),
            row.ExternalUserId,
            row.DisplayName,
            row.ExternalUsername));
    }

    private static PlatformSessionParticipant ToParticipant(ParticipantRsvpRow row) =>
        new(
            new PlatformUser(
                ParsePlatform(row.Platform),
                row.ExternalUserId,
                row.DisplayName,
                row.ExternalUsername),
            row.RsvpStatus,
            row.RegistrationStatus,
            row.IsGm);

    private static PlatformKind ParsePlatform(string platform) =>
        Enum.Parse<PlatformKind>(platform, ignoreCase: true);
}
