using Dapper;
using GmRelay.Bot.Features.Notifications;
using GmRelay.Shared.Domain;
using GmRelay.Shared.Rendering;
using Npgsql;
using Telegram.Bot;
using Telegram.Bot.Types;
using Telegram.Bot.Types.ReplyMarkups;
using GmRelay.Bot.Infrastructure.Telegram;

namespace GmRelay.Bot.Features.Sessions.RescheduleSession;

// ── DTOs ─────────────────────────────────────────────────────────────

internal sealed record AwaitingProposalDto(
    Guid Id, Guid SessionId, string Title, DateTime CurrentScheduledAt,
    Guid BatchId, int? BatchMessageId, long TelegramChatId, string NotificationMode);

internal sealed record VoteParticipantDto(
    Guid PlayerId,
    string DisplayName,
    string? TelegramUsername,
    long TelegramId = 0);

// ── Handler ──────────────────────────────────────────────────────────

/// <summary>
/// Handles text input from the GM who has an AwaitingTime proposal.
/// Parses reschedule options with a voting deadline, creates a voting message,
/// and tags all participants.
/// If no participants are registered, reschedules immediately.
/// </summary>
public sealed class HandleRescheduleTimeInputHandler(
    NpgsqlDataSource dataSource,
    ITelegramBotClient bot,
    DirectSessionNotificationSender directSender,
    ILogger<HandleRescheduleTimeInputHandler> logger)
{
    /// <summary>
    /// Attempts to handle a text message as reschedule time input.
    /// Returns true if it was handled (i.e. user had an AwaitingTime proposal).
    /// </summary>
    public async Task<bool> TryHandleAsync(Message message, CancellationToken ct)
    {
        if (message.From is null || string.IsNullOrWhiteSpace(message.Text))
            return false;

        var gmTelegramId = message.From.Id;
        var chatId = message.Chat.Id;
        var text = message.Text.Trim();

        await using var connection = await dataSource.OpenConnectionAsync(ct);

        // 1. Check if this GM has an AwaitingTime proposal in this chat
        var proposal = await connection.QuerySingleOrDefaultAsync<AwaitingProposalDto>(
            """
            SELECT rp.id AS Id, rp.session_id AS SessionId, s.title AS Title, s.scheduled_at AS CurrentScheduledAt,
                   s.batch_id AS BatchId, s.batch_message_id AS BatchMessageId,
                   g.telegram_chat_id AS TelegramChatId,
                   s.notification_mode AS NotificationMode
            FROM reschedule_proposals rp
            JOIN sessions s ON s.id = rp.session_id
            JOIN game_groups g ON g.id = s.group_id
            WHERE rp.proposed_by = @GmId
              AND rp.status = 'AwaitingTime'
              AND g.telegram_chat_id = @ChatId
              AND EXISTS (
                  SELECT 1
                  FROM group_managers gm
                  JOIN players manager_player ON manager_player.id = gm.player_id
                  WHERE gm.group_id = s.group_id
                    AND manager_player.telegram_id = @GmId
              )
            ORDER BY rp.created_at DESC
            LIMIT 1
            """,
            new { GmId = gmTelegramId, ChatId = chatId });

        if (proposal is null)
            return false;

        // 2. Parse voting input
        if (!RescheduleVotingInput.TryParse(text, DateTimeOffset.UtcNow, out var votingInput, out var parseError))
        {
            await bot.SendMessage(
                chatId: chatId,
                text: $"⚠️ {parseError}\n\nИспользуйте формат:\n<code>25.04.2026 19:30\n26.04.2026 18:00\nДедлайн: 25.04.2026 12:00</code>",
                parseMode: Telegram.Bot.Types.Enums.ParseMode.Html,
                cancellationToken: ct);
            return true;
        }

        // 3. Load participants (non-GM) signed up for this session
        var participants = (await connection.QueryAsync<VoteParticipantDto>(
            """
            SELECT p.id AS PlayerId,
                   p.display_name AS DisplayName,
                   p.telegram_username AS TelegramUsername,
                   p.telegram_id 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 { proposal.SessionId, Active = ParticipantRegistrationStatus.Active })).ToList();

        // 4. If no participants — reschedule immediately
        if (participants.Count == 0)
        {
            await RescheduleImmediately(connection, proposal, votingInput.Options[0], chatId, ct);
            await TryDeleteMessage(chatId, message.MessageId, ct);
            return true;
        }

        // 5. Create voting message
        await using var transaction = await connection.BeginTransactionAsync(ct);
        var options = votingInput.Options
            .Select((proposedAt, index) => new RescheduleOptionDto(
                Guid.NewGuid(),
                index + 1,
                proposedAt))
            .ToList();

        await connection.ExecuteAsync(
            """
            UPDATE reschedule_proposals
            SET voting_deadline_at = @Deadline, status = 'Voting', vote_chat_id = @ChatId
            WHERE id = @Id
            """,
            new { votingInput.Deadline, ChatId = chatId, Id = proposal.Id },
            transaction);

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

        await transaction.CommitAsync(ct);

        var voteText = BuildVotingMessage(
            proposal.Title,
            proposal.CurrentScheduledAt,
            votingInput.Deadline,
            options,
            participants,
            []);
        var keyboard = BuildVotingKeyboard(options);

        var voteMsg = await bot.SendMessage(
            chatId: chatId,
            text: voteText,
            parseMode: Telegram.Bot.Types.Enums.ParseMode.Html,
            replyMarkup: keyboard,
            cancellationToken: ct);

        var mode = SessionNotificationModeExtensions.FromDatabaseValue(proposal.NotificationMode);
        if (mode.ShouldSendDirectMessages())
        {
            var optionsText = string.Join(
                "\n",
                options.Select(option => $"{option.DisplayOrder}. <b>{option.ProposedAt.FormatMoscow()}</b> (МСК)"));
            var directText = $"""
                🔄 <b>Голосование за перенос сессии</b>

                📌 <b>{System.Net.WebUtility.HtmlEncode(proposal.Title)}</b>
                📅 Текущее время: <b>{proposal.CurrentScheduledAt.FormatMoscow()}</b> (МСК)
                🗳 Варианты:
                {optionsText}

                ⏳ Дедлайн: <b>{votingInput.Deadline.FormatMoscow()}</b> (МСК)

                Проголосуйте кнопкой в групповом сообщении.
                """;

            await directSender.SendAsync(
                participants.Select(p => new DirectNotificationRecipient(
                    p.TelegramId,
                    p.DisplayName)),
                directText,
                "reschedule-vote",
                proposal.SessionId,
                ct);
        }

        // Store vote message ID
        await connection.ExecuteAsync(
            "UPDATE reschedule_proposals SET vote_message_id = @MsgId WHERE id = @Id",
            new { MsgId = voteMsg.MessageId, Id = proposal.Id });

        logger.LogInformation(
            "Reschedule voting started for session {SessionId}, proposal {ProposalId}, options {OptionCount}, deadline {Deadline}",
            proposal.SessionId,
            proposal.Id,
            options.Count,
            votingInput.Deadline);

        // Delete GM's time input message
        await TryDeleteMessage(chatId, message.MessageId, ct);

        return true;
    }

    private async Task RescheduleImmediately(
        NpgsqlConnection connection, AwaitingProposalDto proposal,
        DateTimeOffset newTime, long chatId, CancellationToken ct)
    {
        await using var transaction = await connection.BeginTransactionAsync(ct);

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

        await connection.ExecuteAsync(
            "UPDATE reschedule_proposals SET proposed_at = @NewTime, status = 'Approved' WHERE id = @Id",
            new { NewTime = newTime, Id = proposal.Id },
            transaction);

        await transaction.CommitAsync(ct);

        await bot.SendMessage(
            chatId: chatId,
            text: $"✅ Сессия «{proposal.Title}» перенесена!\n\n📅 Новое время: <b>{newTime.ToOffset(TimeSpan.FromHours(3)).ToString("d MMMM yyyy, HH:mm", System.Globalization.CultureInfo.GetCultureInfo("ru-RU"))}</b> (МСК)\n\n<i>Участников нет — голосование не требуется.</i>",
            parseMode: Telegram.Bot.Types.Enums.ParseMode.Html,
            cancellationToken: ct);

        // Re-render batch message with updated time
        await TryUpdateBatchMessage(proposal, ct);

        logger.LogInformation("Session {SessionId} rescheduled immediately (no participants)", proposal.SessionId);
    }

    internal static string BuildVotingMessage(
        string title,
        DateTime currentTime,
        DateTimeOffset deadline,
        IReadOnlyList<RescheduleOptionDto> options,
        IReadOnlyList<VoteParticipantDto> participants,
        IReadOnlyList<RescheduleOptionVoteDto> votes)
    {
        var votesByOption = votes
            .GroupBy(v => v.OptionId)
            .ToDictionary(g => g.Key, g => g.ToList());
        var votedPlayerIds = votes.Select(v => v.PlayerId).ToHashSet();
        var pendingParticipants = participants
            .Where(p => !votedPlayerIds.Contains(p.PlayerId))
            .Select(FormatParticipantName)
            .ToList();

        var lines = new List<string>
        {
            $"🔄 <b>Перенос сессии «{System.Net.WebUtility.HtmlEncode(title)}»</b>",
            "",
            $"📅 Текущее время: <b>{currentTime.FormatMoscow()}</b> (МСК)",
            $"⏳ Дедлайн: <b>{deadline.FormatMoscow()}</b> (МСК)",
            "",
            "Выберите один из вариантов:"
        };

        foreach (var option in options.OrderBy(x => x.DisplayOrder))
        {
            var optionVotes = votesByOption.GetValueOrDefault(option.OptionId, []);
            lines.Add(
                $"{option.DisplayOrder}. <b>{option.ProposedAt.FormatMoscow()}</b> (МСК) — {FormatVoteCount(optionVotes.Count)}");

            if (optionVotes.Count > 0)
            {
                lines.Add($"   {string.Join(", ", optionVotes.Select(FormatParticipantName))}");
            }
        }

        if (pendingParticipants.Count > 0)
        {
            lines.Add("");
            lines.Add($"Не проголосовали: {string.Join(", ", pendingParticipants)}");
        }

        lines.Add("");
        lines.Add($"Голосов: {votedPlayerIds.Count}/{participants.Count}");
        lines.Add("Правило: побеждает вариант с большинством голосов к дедлайну; при ничьей перенос не применяется.");

        return string.Join("\n", lines);
    }

    internal static InlineKeyboardMarkup BuildVotingKeyboard(IReadOnlyList<RescheduleOptionDto> options)
    {
        return new InlineKeyboardMarkup(
            options
                .OrderBy(option => option.DisplayOrder)
                .Select(option => new[]
                {
                    InlineKeyboardButton.WithCallbackData(
                        $"{option.DisplayOrder}. {FormatButtonTime(option.ProposedAt)}",
                        $"reschedule_vote:{option.OptionId}")
                }));
    }

    internal static string FormatParticipantName(VoteParticipantDto participant)
    {
        return participant.TelegramUsername is { Length: > 0 } username
            ? $"@{System.Net.WebUtility.HtmlEncode(username)}"
            : System.Net.WebUtility.HtmlEncode(participant.DisplayName);
    }

    internal static string FormatParticipantName(RescheduleOptionVoteDto vote)
    {
        return vote.TelegramUsername is { Length: > 0 } username
            ? $"@{System.Net.WebUtility.HtmlEncode(username)}"
            : System.Net.WebUtility.HtmlEncode(vote.DisplayName);
    }

    private static string FormatVoteCount(int count)
    {
        var modulo100 = count % 100;
        var modulo10 = count % 10;
        var word = modulo100 is >= 11 and <= 14
            ? "голосов"
            : modulo10 switch
            {
                1 => "голос",
                >= 2 and <= 4 => "голоса",
                _ => "голосов"
            };

        return $"{count} {word}";
    }

    private static string FormatButtonTime(DateTimeOffset utc)
        => utc.ToOffset(TimeSpan.FromHours(3)).ToString(
            "dd.MM HH:mm",
            System.Globalization.CultureInfo.InvariantCulture);

    private async Task TryUpdateBatchMessage(AwaitingProposalDto proposal, CancellationToken ct)
    {
        try
        {
            await using var conn = await dataSource.OpenConnectionAsync(ct);

            var batchSessions = (await conn.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 { proposal.BatchId })).ToList();

            var batchParticipants = (await conn.QueryAsync<ParticipantBatchDto>(
                """
                SELECT sp.session_id AS SessionId,
                       p.display_name AS DisplayName,
                       p.telegram_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 { proposal.BatchId })).ToList();

            if (proposal.BatchMessageId.HasValue)
            {
                var view = SessionBatchViewBuilder.Build(proposal.Title, batchSessions, batchParticipants);
                var renderResult = TelegramSessionBatchRenderer.Render(view);

                await BatchMessageEditor.EditBatchMessageAsync(
                    bot,
                    chatId: proposal.TelegramChatId,
                    messageId: proposal.BatchMessageId.Value,
                    text: renderResult.Text,
                    replyMarkup: renderResult.Markup,
                    ct);
            }
            else
            {
                logger.LogWarning("No batch_message_id stored for session {SessionId}, cannot edit batch message in-place", proposal.SessionId);
            }
        }
        catch (Exception ex)
        {
            logger.LogWarning(ex, "Failed to update batch message after immediate reschedule for session {SessionId}", proposal.SessionId);
        }
    }

    private async Task TryDeleteMessage(long chatId, int messageId, CancellationToken ct)
    {
        try
        {
            await bot.DeleteMessage(chatId, messageId, cancellationToken: ct);
        }
        catch (Exception ex)
        {
            logger.LogWarning(ex, "Failed to delete message {MessageId} in chat {ChatId}", messageId, chatId);
        }
    }
}
