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

namespace GmRelay.Shared.Features.Sessions.RescheduleSession;

public sealed record RescheduleVotingFinalizerResult(
    Guid ProposalId,
    Guid SessionId,
    string Title,
    DateTime CurrentScheduledAt,
    Guid BatchId,
    string NotificationMode,
    string SourcePlatform,
    DateTimeOffset VotingDeadlineAt,
    RescheduleVoteDecision Decision,
    RescheduleOptionDto? SelectedOption,
    IReadOnlyList<RescheduleOptionDto> Options,
    IReadOnlyList<RescheduleOptionVoteDto> Votes,
    IReadOnlyList<VoteParticipantDto> Participants);

public sealed class RescheduleVotingFinalizer(
    NpgsqlDataSource dataSource,
    ISystemClock clock,
    ILogger<RescheduleVotingFinalizer> logger)
{
    public async Task<IReadOnlyList<Guid>> GetDueProposalIdsAsync(string sourcePlatform, CancellationToken ct)
    {
        await using var connection = await dataSource.OpenConnectionAsync(ct);
        var proposalIds = (await connection.QueryAsync<Guid>(
            """
            SELECT id
            FROM reschedule_proposals
            WHERE status = 'Voting'
              AND voting_deadline_at IS NOT NULL
              AND voting_deadline_at <= @Now
              AND source_platform = @SourcePlatform
            ORDER BY voting_deadline_at
            LIMIT 25
            """,
            new { Now = clock.UtcNow.UtcDateTime, SourcePlatform = sourcePlatform })).ToList();

        return proposalIds;
    }

    public async Task<RescheduleVotingFinalizerResult?> FinalizeAsync(Guid proposalId, CancellationToken ct)
    {
        await using var connection = await dataSource.OpenConnectionAsync(ct);
        await using var transaction = await connection.BeginTransactionAsync(ct);

        var proposal = await connection.QuerySingleOrDefaultAsync<ProposalRow>(
            """
            SELECT rp.id AS ProposalId,
                   rp.session_id AS SessionId,
                   rp.voting_deadline_at AS VotingDeadlineAt,
                   rp.source_platform AS SourcePlatform,
                   s.title AS Title,
                   s.scheduled_at AS CurrentScheduledAt,
                   s.batch_id AS BatchId,
                   s.notification_mode AS NotificationMode
            FROM reschedule_proposals rp
            JOIN sessions s ON s.id = rp.session_id
            WHERE rp.id = @ProposalId
              AND rp.status = 'Voting'
              AND rp.voting_deadline_at IS NOT NULL
              AND rp.voting_deadline_at <= @Now
            FOR UPDATE
            """,
            new { ProposalId = proposalId, Now = clock.UtcNow.UtcDateTime },
            transaction);

        if (proposal is null)
            return null;

        var participants = (await connection.QueryAsync<VoteParticipantDto>(
            """
            SELECT p.id AS PlayerId,
                   p.display_name AS DisplayName,
                   p.external_username AS TelegramUsername,
                   p.external_user_id::BIGINT AS TelegramId
            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 p.display_name
            """,
            new { proposal.SessionId, Active = ParticipantRegistrationStatus.Active },
            transaction)).ToList();

        var options = (await connection.QueryAsync<RescheduleOptionDto>(
            """
            SELECT id AS OptionId,
                   display_order AS DisplayOrder,
                   proposed_at AS ProposedAt
            FROM reschedule_options
            WHERE proposal_id = @ProposalId
            ORDER BY display_order
            """,
            new { ProposalId = proposal.ProposalId },
            transaction)).ToList();

        var votes = (await connection.QueryAsync<RescheduleOptionVoteDto>(
            """
            SELECT rov.option_id AS OptionId,
                   p.id AS PlayerId,
                   p.display_name AS DisplayName,
                   p.external_username AS TelegramUsername
            FROM reschedule_option_votes rov
            JOIN players p ON p.id = rov.player_id
            WHERE rov.proposal_id = @ProposalId
            ORDER BY rov.voted_at, p.display_name
            """,
            new { ProposalId = proposal.ProposalId },
            transaction)).ToList();

        var voteCounts = options
            .Select(option => new RescheduleOptionVoteCount(
                option.OptionId,
                votes.Count(vote => vote.OptionId == option.OptionId)))
            .ToList();
        var decision = RescheduleVoteRules.SelectWinner(voteCounts);
        var selectedOption = decision.SelectedOptionId is { } selectedOptionId
            ? options.Single(x => x.OptionId == selectedOptionId)
            : null;

        if (selectedOption is not null)
        {
            await connection.ExecuteAsync(
                """
                UPDATE sessions
                SET scheduled_at = @NewTime,
                    status = @Status,
                    confirmation_message_id = NULL,
                    confirmation_sent_at = NULL,
                    link_message_id = NULL,
                    one_hour_reminder_processed_at = NULL,
                    updated_at = now()
                WHERE id = @SessionId
                """,
                new { NewTime = selectedOption.ProposedAt, proposal.SessionId, Status = SessionStatus.Planned },
                transaction);

            await connection.ExecuteAsync(
                """
                UPDATE session_participants
                SET rsvp_status = 'Pending',
                    responded_at = NULL
                WHERE session_id = @SessionId
                  AND is_gm = false
                  AND registration_status = @Active
                """,
                new { proposal.SessionId, Active = ParticipantRegistrationStatus.Active },
                transaction);

            await connection.ExecuteAsync(
                """
                UPDATE reschedule_proposals
                SET status = 'Approved',
                    selected_option_id = @SelectedOptionId,
                    proposed_at = @ProposedAt
                WHERE id = @ProposalId
                """,
                new
                {
                    ProposalId = proposal.ProposalId,
                    SelectedOptionId = selectedOption.OptionId,
                    ProposedAt = selectedOption.ProposedAt
                },
                transaction);
        }
        else
        {
            await connection.ExecuteAsync(
                "UPDATE reschedule_proposals SET status = 'Rejected' WHERE id = @ProposalId",
                new { ProposalId = proposal.ProposalId },
                transaction);
        }

        await transaction.CommitAsync(ct);

        logger.LogInformation(
            "Finalized reschedule proposal {ProposalId} for session {SessionId} with outcome {Outcome}",
            proposal.ProposalId,
            proposal.SessionId,
            decision.Outcome);

        return new RescheduleVotingFinalizerResult(
            proposal.ProposalId,
            proposal.SessionId,
            proposal.Title,
            proposal.CurrentScheduledAt,
            proposal.BatchId,
            proposal.NotificationMode,
            proposal.SourcePlatform,
            proposal.VotingDeadlineAt,
            decision,
            selectedOption,
            options,
            votes,
            participants);
    }

    private sealed record ProposalRow(
        Guid ProposalId,
        Guid SessionId,
        DateTimeOffset VotingDeadlineAt,
        string SourcePlatform,
        string Title,
        DateTime CurrentScheduledAt,
        Guid BatchId,
        string NotificationMode);
}
