using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using Dapper;
using GmRelay.DiscordBot.Infrastructure.Discord;
using GmRelay.Shared.Features.Sessions.CreateSession.Wizard;
using GmRelay.Shared.Platform;
using NetCord;
using NetCord.Rest;
using Npgsql;

namespace GmRelay.DiscordBot.Features.Sessions.Wizard;

/// <summary>
/// Discord-side implementation of <see cref="IWizardMessenger"/>.
/// Translates the platform-neutral wizard contract into NetCord REST
/// calls and ephemeral follow-ups. The messenger has no access to the
/// live interaction context — that lives on the inbound handler — so
/// <see cref="AnswerInteractionAsync"/> stashes the toast text in
/// <see cref="DiscordInteractionReplyCache"/>; the inbound module
/// drains the cache and ships the actual <c>SendResponseAsync</c> call
/// via the existing helper used by <c>DiscordPlatformMessenger</c>.
/// </summary>
public sealed class DiscordWizardMessenger : IWizardMessenger
{
    private readonly RestClient _rest;
    private readonly NpgsqlDataSource _dataSource;
    private readonly DiscordInteractionReplyCache _replies;
    private readonly IWizardContextStore _contextStore;
    private readonly ILogger<DiscordWizardMessenger>? _log;

    public DiscordWizardMessenger(
        RestClient rest,
        NpgsqlDataSource dataSource,
        DiscordInteractionReplyCache replies,
        IWizardContextStore contextStore)
        : this(rest, dataSource, replies, contextStore, logger: null)
    {
    }

    public DiscordWizardMessenger(
        RestClient rest,
        NpgsqlDataSource dataSource,
        DiscordInteractionReplyCache replies,
        IWizardContextStore contextStore,
        ILogger<DiscordWizardMessenger>? logger)
    {
        _rest = rest;
        _dataSource = dataSource;
        _replies = replies;
        _contextStore = contextStore;
        _log = logger;
    }

    public async Task<string> EditDraftMessageAsync(
        WizardDraft draft,
        string text,
        IReadOnlyList<WizardAction> keyboard,
        CancellationToken ct)
    {
        if (!_contextStore.TryGet(draft.Id, out var ctx))
        {
            // No stored context (e.g. service restart, draft from another
            // process). Fall back to sending a brand new message — the
            // caller will persist the returned id.
            return await SendDraftMessageAsync(draft, text, keyboard, ct);
        }

        if (!ulong.TryParse(ctx.ChannelId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var channelId) ||
            !ulong.TryParse(ctx.MessageId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var messageId))
        {
            // Context is corrupt — recreate the message.
            return await SendDraftMessageAsync(draft, text, keyboard, ct);
        }

        try
        {
            var (embed, rows) = BuildEmbedAndRows(draft, text, keyboard);
            await _rest.ModifyMessageAsync(
                channelId,
                messageId,
                options =>
                {
                    options.Embeds = embed is null ? null : new[] { embed };
                    options.Components = rows;
                });
            return ctx.MessageId;
        }
        catch (RestException ex) when (IsExpiredOrUnknownMessage(ex))
        {
            // Message was deleted or interaction token expired —
            // recreate the message in the original channel.
            _log?.LogWarning(
                ex,
                "Edit failed for draft {DraftId} (channel={ChannelId}, message={MessageId}); re-sending.",
                draft.Id,
                ctx.ChannelId,
                ctx.MessageId);
            return await SendDraftMessageAsync(draft, text, keyboard, ct);
        }
    }

    public async Task<string> SendDraftMessageAsync(
        WizardDraft draft,
        string text,
        IReadOnlyList<WizardAction> keyboard,
        CancellationToken ct)
    {
        if (!_contextStore.TryGet(draft.Id, out var ctx))
        {
            throw new InvalidOperationException(
                $"Cannot send wizard message: no context for draft {draft.Id}.");
        }

        if (!ulong.TryParse(ctx.ChannelId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var channelId))
        {
            throw new InvalidOperationException(
                $"Wizard draft {draft.Id} has un-parseable channel id '{ctx.ChannelId}'.");
        }

        var (embed, rows) = BuildEmbedAndRows(draft, text, keyboard);
        var message = await _rest.SendMessageAsync(
            channelId,
            new MessageProperties()
                .WithEmbeds(embed is null ? null : new[] { embed })
                .WithComponents(rows));

        var newMessageId = message.Id.ToString(CultureInfo.InvariantCulture);
        _contextStore.Set(draft.Id, ctx with { MessageId = newMessageId });
        return newMessageId;
    }

    public Task AnswerInteractionAsync(string interactionId, string? text, CancellationToken ct)
    {
        // The wizard's "answer" is just a toast shown to the user.
        // Stash it in the existing reply cache; the inbound interaction
        // module drains it once the wizard returns. ShowAlert=false so
        // it appears as a quiet follow-up rather than a popup.
        _replies.Store(new PlatformInteractionReply(
            InteractionId: interactionId,
            Text: text ?? string.Empty,
            ShowAlert: false));
        return Task.CompletedTask;
    }

    public async Task<IReadOnlyList<WizardClubOption>> GetOwnerClubsAsync(
        string ownerId, CancellationToken ct)
    {
        // The Telegram messenger enumerates game_groups the owner
        // manages as a GM (V008 added group_managers with role
        // 'Owner'|'CoGm'). Discord follows the same convention.
        const string sql = """
            SELECT g.id   AS ClubId,
                   g.name AS Name
            FROM game_groups g
            JOIN group_managers gm ON gm.group_id = g.id
            JOIN players p ON p.id = gm.player_id
            WHERE p.platform = @Platform
              AND p.external_user_id = @ExternalId
              AND gm.role IN ('Owner', 'CoGm')
            GROUP BY g.id, g.name
            ORDER BY g.name
            """;
        await using var conn = await _dataSource.OpenConnectionAsync(ct);
        // NativeAOT: direct (sql, params) overload — see
        // TelegramWizardMessenger.GetOwnerClubsAsync for why.
        var rows = await conn.QueryAsync<WizardClubOption>(
            sql,
            new { Platform = "Discord", ExternalId = ownerId });
        return rows.AsList();
    }

    // ── Embed + component construction ────────────────────────────────
    private (EmbedProperties? embed, IReadOnlyList<IMessageComponentProperties> rows) BuildEmbedAndRows(
        WizardDraft draft,
        string text,
        IReadOnlyList<WizardAction> keyboard)
    {
        // Embeds have a hard 4096-char limit — truncate to 3900 so the
        // wizard's own prefix/suffix additions still fit.
        var safeText = Truncate(text, 3900);
        var rows = BuildActionRowsFromActions(keyboard);
        return (BuildEmbed(safeText), rows);
    }

    private static EmbedProperties BuildEmbed(string description) =>
        new EmbedProperties()
            .WithTitle("Мастер создания сессии")
            .WithDescription(description)
            .WithColor(new Color(0x5865F2));

    private static string Truncate(string text, int max) =>
        text.Length <= max ? text : text[..max];

    private static IReadOnlyList<IMessageComponentProperties> BuildActionRowsFromActions(
        IReadOnlyList<WizardAction> actions)
    {
        if (actions.Count == 0)
        {
            return Array.Empty<IMessageComponentProperties>();
        }
        var rows = new List<IMessageComponentProperties>();
        // Discord allows up to 5 buttons per ActionRow. Lay them out
        // left-to-right, 5 per row.
        foreach (var chunk in actions.Chunk(5))
        {
            var row = new ActionRowProperties();
            foreach (var action in chunk)
            {
                var style = action.Style switch
                {
                    WizardActionStyle.Primary => ButtonStyle.Primary,
                    WizardActionStyle.Success => ButtonStyle.Success,
                    WizardActionStyle.Danger => ButtonStyle.Danger,
                    _ => ButtonStyle.Secondary,
                };
                var cid = action.Payload;
                if (cid.Length > DiscordWizardStep.MaxCustomIdLength)
                {
                    // Truncate-by-omission isn't safe (customId must be
                    // unique). The wizard's callback format is already
                    // bounded — if we hit this, it's a bug.
                    throw new InvalidOperationException(
                        $"Wizard action custom id '{cid}' exceeds Discord's 100-char limit.");
                }
                row.Add(new ButtonProperties(cid, action.Label, style));
            }
            rows.Add(row);
        }
        return rows;
    }

    private static bool IsExpiredOrUnknownMessage(RestException ex) =>
        ex.StatusCode == System.Net.HttpStatusCode.NotFound
        || ex.StatusCode == System.Net.HttpStatusCode.Unauthorized
        || ex.StatusCode == System.Net.HttpStatusCode.Forbidden;
}
