namespace GmRelay.DiscordBot.Features.Sessions;

using Dapper;
using GmRelay.DiscordBot.Infrastructure.Discord;
using GmRelay.DiscordBot.Rendering;
using GmRelay.Shared.Domain;
using GmRelay.Shared.Features.Sessions.RescheduleSession;
using GmRelay.Shared.Platform;
using NetCord;
using NetCord.Rest;
using Npgsql;

public sealed record DiscordRescheduleResult(Guid ProposalId, IReadOnlyList<RescheduleOptionDto> Options, DateTimeOffset Deadline);

public sealed class DiscordRescheduleHandler(
    NpgsqlDataSource dataSource,
    DiscordPermissionChecker permissionChecker,
    RestClient restClient,
    ILogger<DiscordRescheduleHandler> logger)
{
    public async Task<DiscordRescheduleResult> HandleAsync(
        string guildId,
        string channelId,
        ulong userId,
        string userDisplayName,
        ulong resolvedPermissions,
        ulong guildOwnerId,
        Guid sessionId,
        IReadOnlyList<DateTimeOffset> options,
        DateTimeOffset deadline,
        CancellationToken ct)
    {
        // 1. Permission check + read-only validation (before Discord message)
        await using var readConnection = await dataSource.OpenConnectionAsync(ct);

        var dbManagerUserIds = await readConnection.QueryAsync<ulong>(
            @"SELECT CAST(p.external_user_id AS BIGINT)
              FROM group_managers gm
              JOIN players p ON p.id = gm.player_id
              JOIN game_groups g ON g.id = gm.group_id
              WHERE g.platform = 'Discord' AND g.external_group_id = @GuildId",
            new { GuildId = guildId });

        if (!permissionChecker.CanManageSchedule(guildOwnerId, userId, dbManagerUserIds, resolvedPermissions))
        {
            throw new UnauthorizedAccessException("⛔ Только owner, администратор или manager могут переносить сессии.");
        }

        // 2. Ensure player exists
        await readConnection.ExecuteAsync(
            @"INSERT INTO players (display_name, platform, external_user_id, external_username)
              VALUES (@Name, 'Discord', @UserId, @Name)
              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",
            new { Name = userDisplayName, UserId = userId.ToString() });

        // 3. Verify session exists
        var session = await readConnection.QuerySingleOrDefaultAsync<RescheduleSessionInfoDto>(
            """
            SELECT s.title AS Title, s.scheduled_at AS CurrentScheduledAt
            FROM sessions s
            WHERE s.id = @SessionId AND s.status != @Cancelled
            """,
            new { SessionId = sessionId, Cancelled = SessionStatus.Cancelled });

        if (session is null)
            throw new InvalidOperationException("Сессия не найдена или отменена.");

        // 4. Check no active proposal
        var hasActive = await readConnection.ExecuteScalarAsync<bool>(
            "SELECT EXISTS (SELECT 1 FROM reschedule_proposals WHERE session_id = @SessionId AND status IN ('AwaitingTime', 'Voting'))",
            new { SessionId = sessionId });

        if (hasActive)
            throw new InvalidOperationException("Уже есть активный запрос на перенос этой сессии.");

        // 5. Load participants for rendering
        var participants = (await readConnection.QueryAsync<VoteParticipantDto>(
            """
            SELECT p.id AS PlayerId, p.display_name AS DisplayName, p.external_username AS TelegramUsername, 0 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
            """,
            new { SessionId = sessionId, Active = ParticipantRegistrationStatus.Active })).ToList();

        // 6. Prepare proposal data
        var proposalId = Guid.NewGuid();
        var optionDtos = options.Select((o, i) => new RescheduleOptionDto(Guid.NewGuid(), i + 1, o)).ToList();

        // 7. Build and send Discord vote message BEFORE transaction
        var (embed, actionRow) = DiscordRescheduleVotingRenderer.Render(session.Title, session.CurrentScheduledAt, deadline, optionDtos, participants, []);

        var channelIdUlong = ulong.Parse(channelId);

        // NOTE: Discord message is sent before DB transaction to avoid orphaned proposals
        // if the send fails. There is a negligible race window where the message is visible
        // before the DB commit; in practice users cannot click faster than the transaction commits.
        var sentMessage = await restClient.SendMessageAsync(
            channelIdUlong,
            new MessageProperties()
                .WithEmbeds(new[] { embed })
                .WithComponents(new[] { actionRow }));

        // 8. Create proposal + options + platform_messages in transaction
        try
        {
            await using var connection = await dataSource.OpenConnectionAsync(ct);
            await using var transaction = await connection.BeginTransactionAsync(ct);

            await connection.ExecuteAsync(
                """
                INSERT INTO reschedule_proposals (id, session_id, proposed_by, source_platform, proposed_by_external_user_id, status, voting_deadline_at)
                VALUES (@Id, @SessionId, NULL, 'Discord', @ProposedBy, 'Voting', @Deadline)
                """,
                new { Id = proposalId, SessionId = sessionId, ProposedBy = userId.ToString(), Deadline = deadline.UtcDateTime },
                transaction);

            foreach (var option in optionDtos)
            {
                await connection.ExecuteAsync(
                    """
                    INSERT INTO reschedule_options (id, proposal_id, proposed_at, display_order)
                    VALUES (@OptionId, @ProposalId, @ProposedAt, @DisplayOrder)
                    """,
                    new { option.OptionId, ProposalId = proposalId, option.ProposedAt, option.DisplayOrder },
                    transaction);
            }

            await connection.ExecuteAsync(
                """
                INSERT INTO platform_messages (platform, group_id, session_id, external_channel_id, external_message_id, purpose)
                VALUES ('Discord', (SELECT id FROM game_groups WHERE platform = 'Discord' AND external_group_id = @GuildId), @SessionId, @ChannelId, @MessageId, 'reschedule_vote')
                """,
                new { GuildId = guildId, SessionId = sessionId, ChannelId = channelId, MessageId = sentMessage.Id.ToString() },
                transaction);

            await transaction.CommitAsync(ct);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Transaction failed after Discord message sent; deleting orphaned message");
            try { await restClient.DeleteMessageAsync(channelIdUlong, sentMessage.Id); } catch { /* best effort */ }
            throw;
        }

        logger.LogInformation("Discord reschedule voting started for session {SessionId}, proposal {ProposalId}", sessionId, proposalId);

        return new DiscordRescheduleResult(proposalId, optionDtos, deadline);
    }

}

internal sealed record RescheduleSessionInfoDto(string Title, DateTime CurrentScheduledAt);
