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 Microsoft.Extensions.Logging;
using NetCord;
using NetCord.Rest;
using NetCord.Services.ComponentInteractions;
using Npgsql;
using SharedWizard = GmRelay.Shared.Features.Sessions.CreateSession.Wizard.GameCreationWizard;

namespace GmRelay.DiscordBot.Features.Sessions.Wizard;

/// <summary>
/// Inbound component-interaction handler for the Discord wizard.
///
/// One class per interaction context type — NetCord's
/// <c>ComponentInteractionModule&lt;TContext&gt;</c> is single-context. All
/// three classes share the same dispatch table (parse customId → load
/// draft → call the shared
/// <see cref="SharedWizard.HandleInteractionAsync"/>) implemented in
/// <see cref="WizardInteractionDispatcher"/>.
///
/// Custom-id wire format (see <see cref="DiscordWizardStep"/>):
/// <list type="bullet">
///   <item><c>wizard:btn:choice:&lt;step&gt;:&lt;value&gt;</c> — choice buttons</item>
///   <item><c>wizard:btn:cancel</c>, <c>wizard:btn:back</c>, <c>wizard:btn:create</c></item>
///   <item><c>wizard:btn:resume:&lt;continue|restart&gt;</c></item>
///   <item><c>wizard:select:&lt;step&gt;</c> — StringSelectMenu</item>
///   <item><c>wizard:modal:&lt;step&gt;</c> — Modal submit</item>
/// </list>
///
/// The active draft is looked up by (platform="Discord", ownerId=userId);
/// the custom-id never carries a draft id because the wizard assumes one
/// active draft per owner.
/// </summary>
public sealed class DiscordWizardButtonModule : ComponentInteractionModule<ButtonInteractionContext>
{
    private readonly WizardInteractionDispatcher _dispatcher;

    public DiscordWizardButtonModule(WizardInteractionDispatcher dispatcher)
    {
        _dispatcher = dispatcher;
    }

    [ComponentInteraction("wizard")]
    public Task HandleAsync(string args) =>
        _dispatcher.HandleButtonAsync(Context, args);
}

public sealed class DiscordWizardStringMenuModule : ComponentInteractionModule<StringMenuInteractionContext>
{
    private readonly WizardInteractionDispatcher _dispatcher;

    public DiscordWizardStringMenuModule(WizardInteractionDispatcher dispatcher)
    {
        _dispatcher = dispatcher;
    }

    [ComponentInteraction("wizard")]
    public Task HandleAsync(string args) =>
        _dispatcher.HandleStringMenuAsync(Context, args);
}

public sealed class DiscordWizardModalModule : ComponentInteractionModule<ModalInteractionContext>
{
    private readonly WizardInteractionDispatcher _dispatcher;

    public DiscordWizardModalModule(WizardInteractionDispatcher dispatcher)
    {
        _dispatcher = dispatcher;
    }

    [ComponentInteraction("wizard")]
    public Task HandleAsync(string args) =>
        _dispatcher.HandleModalAsync(Context, args);
}

/// <summary>
/// Shared dispatch table for the three wizard interaction modules.
/// Owns all the stateful collaborators (drafts, context store, wizard
/// state machine, submitter, messenger, reply cache) so the three
/// NetCord module shells can stay trivially thin.
/// </summary>
public sealed class WizardInteractionDispatcher
{
    private readonly IWizardDraftRepository _drafts;
    private readonly IWizardContextStore _contextStore;
    private readonly SharedWizard _wizard;
    private readonly DiscordWizardSubmitter _submitter;
    private readonly DiscordWizardMessenger _messenger;
    private readonly DiscordInteractionReplyCache _replies;
    private readonly NpgsqlDataSource _dataSource;
    private readonly ILogger<WizardInteractionDispatcher> _log;

    public WizardInteractionDispatcher(
        IWizardDraftRepository drafts,
        IWizardContextStore contextStore,
        SharedWizard wizard,
        DiscordWizardSubmitter submitter,
        DiscordWizardMessenger messenger,
        DiscordInteractionReplyCache replies,
        NpgsqlDataSource dataSource,
        ILogger<WizardInteractionDispatcher> log)
    {
        _drafts = drafts;
        _contextStore = contextStore;
        _wizard = wizard;
        _submitter = submitter;
        _messenger = messenger;
        _replies = replies;
        _dataSource = dataSource;
        _log = log;
    }

    /// <summary>
    /// Steps that, after the wizard's state advance, expect the user
    /// to fill a popup. The dispatcher uses this to decide whether
    /// the interaction response is a Modal() (the user sees a popup)
    /// or a DeferredMessage() (the wizard's edit is the only visible
    /// feedback). Keep in sync with <see cref="DiscordWizardStep.OpenModalStep"/>
    /// returns.
    /// </summary>
    private static readonly IReadOnlySet<string> StepsThatOpenModal = new HashSet<string>(StringComparer.Ordinal)
    {
        WizardStepNames.Title,
        WizardStepNames.Description,
        WizardStepNames.Cover,
        WizardStepNames.DateTime,
        WizardStepNames.Capacity,
        WizardStepNames.PoolSlotDateTime,
        WizardStepNames.PoolSlotCapacity,
        "SystemFreeText",
        "DurationFreeText",
        "PoolSystemDurationFreeText",
    };

    // ── Button handler ────────────────────────────────────────────────
    public async Task HandleButtonAsync(ButtonInteractionContext context, string args)
    {
        // NetCord only allows one response per interaction. The
        // previous implementation deferred too early and then
        // tried to "swap" the deferred response for a Modal — which
        // NetCord forbids. The new flow is: do the wizard work
        // (which is a separate REST call to edit the draft message),
        // THEN send the interaction response. The response is
        // either a Modal popup (when the new step needs text input)
        // or a plain DeferredMessage ack.
        var ct = CancellationToken.None;
        // args looks like one of:
        //   "btn:choice:<step>:<value>"   (a choice)
        //   "btn:cancel"
        //   "btn:back"
        //   "btn:create"
        //   "btn:resume:continue"
        //   "btn:resume:restart"
        var ownerId = context.User.Id.ToString(CultureInfo.InvariantCulture);
        var interactionId = context.Interaction.Id.ToString(CultureInfo.InvariantCulture);

        var draft = await _drafts.GetActiveAsync("Discord", ownerId, ct);
        if (draft is null)
        {
            await context.Interaction.SendResponseAsync(InteractionCallback.Message(
                new InteractionMessageProperties()
                    .WithContent("📭 Нет активного мастера. Запустите /newsession-wizard.")
                    .WithFlags(MessageFlags.Ephemeral)));
            return;
        }

        var parts = args.Split(':', 4);
        if (parts.Length < 2 || parts[0] != "btn")
        {
            await AckWithErrorAsync(context.Interaction, "Неизвестная команда");
            return;
        }

        // Special case: "create" doesn't go through the wizard — the
        // submitter edits the draft message directly with the result
        // embed ("✅ Создано" or retry buttons). After the submitter
        // returns, ack the click so the user doesn't see "Application
        // did not respond".
        if (parts[1] == "create")
        {
            await _submitter.SubmitAsync(draft, ct);
            _contextStore.Remove(draft.Id);
            await context.Interaction.SendResponseAsync(InteractionCallback.DeferredMessage(
                MessageFlags.Ephemeral));
            return;
        }

        // Special case: "modal:<step>" — the renderer emits this for the
        // "Другое…" free-text buttons on System, Duration, and
        // PoolSystemDuration. The click intent is "open a modal for
        // free-text input" — NOT "advance the wizard". The wizard's
        // state advance happens when the user submits the modal.
        if (parts[1] == "modal" && parts.Length >= 3)
        {
            var modal = DiscordWizardStep.BuildModal(parts[2], draft.ChatId);
            if (modal is not null)
            {
                await context.Interaction.SendResponseAsync(InteractionCallback.Modal(modal));
            }
            else
            {
                await AckWithErrorAsync(context.Interaction, "Не удалось открыть форму");
            }
            return;
        }

        // Special case: "resume" — the slash command's resume row
        // gives the user a chance to keep or restart their active
        // draft. The wizard has no built-in resume case, so we
        // handle the two resume kinds directly.
        if (parts[1] == "resume")
        {
            await HandleResumeAsync(context, parts, draft, ownerId, interactionId, ct);
            return;
        }

        // Choice / back / cancel — route through the shared wizard.
        string callback;
        switch (parts[1])
        {
            case "choice":
                if (parts.Length < 4)
                {
                    await AckWithErrorAsync(context.Interaction, "Некорректная кнопка");
                    return;
                }
                callback = WizardCallbackData.Choice(parts[2], parts[3]);
                break;
            case "back":
                callback = WizardCallbackData.Back();
                break;
            case "cancel":
                callback = WizardCallbackData.Cancel();
                break;
            default:
                await AckWithErrorAsync(context.Interaction, "Неизвестная кнопка");
                return;
        }

        var interaction = new WizardInteraction(
            OwnerId: ownerId,
            Text: null,
            CallbackPayload: callback,
            PhotoFileId: null,
            PhotoUrl: null,
            InteractionId: interactionId);
        await _wizard.HandleInteractionAsync(interaction, draft, ct);

        // After the wizard's state advance, decide the response.
        // The wizard's EditDraftMessageAsync already updated the
        // draft embed; we just need the interaction response to
        // either pop a modal or quietly ack.
        if (parts[1] == "cancel")
        {
            _contextStore.Remove(draft.Id);
        }
        await RespondAfterWizardAsync(context, draft, ct);
    }

    private async Task HandleResumeAsync(
        ButtonInteractionContext context,
        string[] parts,
        WizardDraft draft,
        string ownerId,
        string interactionId,
        CancellationToken ct)
    {
        // resume:continue → re-render the current step (the wizard
        // itself doesn't know about resume, so we just edit the
        // draft message via the messenger).
        // resume:restart → delete the draft and prompt the user to
        // re-run /newsession-wizard.
        if (parts.Length >= 3 && parts[2] == "restart")
        {
            await _drafts.DeleteAsync(draft.Id, ct);
            _contextStore.Remove(draft.Id);
            await context.Interaction.SendResponseAsync(InteractionCallback.Message(
                new InteractionMessageProperties()
                    .WithContent("♻️ Мастер сброшен. Запустите /newsession-wizard заново.")
                    .WithFlags(MessageFlags.Ephemeral)));
            return;
        }
        // continue
        var payload = LoadPayload(draft);
        var (text, actions) = WizardStepViewBuilder.Build(draft, payload, await LoadClubsAsync(draft, ct));
        await _messenger.EditDraftMessageAsync(draft, text, actions, ct);
        await context.Interaction.SendResponseAsync(InteractionCallback.DeferredMessage(
            MessageFlags.Ephemeral));
    }

    // ── StringSelectMenu handler ───────────────────────────────────────
    public async Task HandleStringMenuAsync(StringMenuInteractionContext context, string args)
    {
        // NetCord's interaction response type is locked after the
        // first SendResponse call. For menu selections we don't
        // need a popup, so we always defer. The wizard's edit is
        // a separate REST call.
        var ct = CancellationToken.None;
        // args looks like "select:<step>" (e.g. "select:Visibility" or
        // "select:PoolSystemDuration"). The chosen value lives in
        // SelectedValues[0].
        var ownerId = context.User.Id.ToString(CultureInfo.InvariantCulture);
        var interactionId = context.Interaction.Id.ToString(CultureInfo.InvariantCulture);

        var draft = await _drafts.GetActiveAsync("Discord", ownerId, ct);
        if (draft is null)
        {
            await context.Interaction.SendResponseAsync(InteractionCallback.Message(
                new InteractionMessageProperties()
                    .WithContent("📭 Нет активного мастера. Запустите /newsession-wizard.")
                    .WithFlags(MessageFlags.Ephemeral)));
            return;
        }

        // NetCord strips the matching ComponentInteraction "wizard"
        // prefix from the custom id and passes the remainder as `args`.
        // For `wizard:select:Visibility` the args arrive as
        // `select:Visibility` (2 parts when split on `:`), so the
        // length check uses `< 2` and the step lives at parts[1].
        var parts = args.Split(':', 2);
        if (parts.Length < 2 || parts[0] != "select" || context.Interaction.Data.SelectedValues.Count == 0)
        {
            await AckWithErrorAsync(context.Interaction, "Неизвестный выбор");
            return;
        }
        var step = parts[1];
        var value = context.Interaction.Data.SelectedValues[0];

        await context.Interaction.SendResponseAsync(InteractionCallback.DeferredModifyMessage);

        var interaction = new WizardInteraction(
            OwnerId: ownerId,
            Text: null,
            CallbackPayload: WizardCallbackData.Choice(step, value),
            PhotoFileId: null,
            PhotoUrl: null,
            InteractionId: interactionId);
        await _wizard.HandleInteractionAsync(interaction, draft, ct);
    }

    // ── Modal submit handler ──────────────────────────────────────────
    public async Task HandleModalAsync(ModalInteractionContext context, string args)
    {
        // The modal text becomes the user's input. The wizard's
        // ApplyText dispatcher consumes it as either a text-input
        // step (Title, Description, etc.) or, via the helper, a
        // free-text variant of System/Duration/PoolSystemDuration.
        var ct = CancellationToken.None;
        // args looks like "modal:<step>" (e.g. "modal:Title" or
        // "modal:SystemFreeText"). The text value lives in
        // Data.Components[0].Component.Value (the wizard sends a
        // single Label wrapping a single TextInput).
        var ownerId = context.User.Id.ToString(CultureInfo.InvariantCulture);
        var interactionId = context.Interaction.Id.ToString(CultureInfo.InvariantCulture);

        var draft = await _drafts.GetActiveAsync("Discord", ownerId, ct);
        if (draft is null)
        {
            await context.Interaction.SendResponseAsync(InteractionCallback.Message(
                new InteractionMessageProperties()
                    .WithContent("📭 Нет активного мастера. Запустите /newsession-wizard.")
                    .WithFlags(MessageFlags.Ephemeral)));
            return;
        }

        // Same NetCord prefix-stripping as the select handler:
        // for `wizard:modal:Title` the args arrive as `modal:Title`
        // (2 parts when split on `:`).
        var parts = args.Split(':', 2);
        if (parts.Length < 2 || parts[0] != "modal" || context.Interaction.Data.Components.Count == 0)
        {
            await AckWithErrorAsync(context.Interaction, "Некорректный модал");
            return;
        }
        var step = parts[1];

        var text = ExtractModalText(context);
        if (text is null)
        {
            await AckWithErrorAsync(context.Interaction, "Модал без ввода");
            return;
        }

        // Modal values are routed by step name. The shared wizard knows
        // how to apply Title, Description, etc.; the free-text variants
        // (SystemFreeText, DurationFreeText, PoolSystemDurationFreeText)
        // are mapped to the canonical step here so the wizard's existing
        // ApplyText dispatcher handles them.
        var wizardStep = MapModalStepToWizardStep(step);
        var interaction = new WizardInteraction(
            OwnerId: ownerId,
            Text: text,
            CallbackPayload: null,
            PhotoFileId: null,
            PhotoUrl: null,
            InteractionId: interactionId);

        // For free-text modal steps the wizard's "current step" is the
        // canonical step (System, Duration, etc.), but the user just
        // submitted via the free-text modal. Temporarily set the
        // draft.Step to the canonical step so the wizard's ApplyText
        // runs the right branch. The wizard then advances draft.Step
        // to the NEXT step (e.g. Duration) and persists that via
        // _drafts.UpsertAsync. We must NOT restore draft.Step to
        // the original value afterwards — the DB has already been
        // updated to the new step, and restoring locally would only
        // mask the truth from the next interaction's GetActiveAsync.
        if (draft.Step != wizardStep)
        {
            draft.Step = wizardStep;
        }

        await context.Interaction.SendResponseAsync(InteractionCallback.DeferredMessage(
            MessageFlags.Ephemeral));

        await _wizard.HandleInteractionAsync(interaction, draft, ct);
    }

    // ── Response helper ───────────────────────────────────────────────
    private async Task RespondAfterWizardAsync(
        ButtonInteractionContext context,
        WizardDraft draft,
        CancellationToken ct)
    {
        // The wizard's state machine has advanced draft.Step. Re-render
        // the new step locally to discover whether it expects a popup,
        // then send the appropriate response.
        try
        {
            if (StepsThatOpenModal.Contains(draft.Step))
            {
                var modal = DiscordWizardStep.BuildModal(draft.Step, draft.ChatId);
                if (modal is not null)
                {
                    await context.Interaction.SendResponseAsync(InteractionCallback.Modal(modal));
                    return;
                }
            }
        }
        catch (Exception ex)
        {
            _log.LogWarning(ex, "Modal popup failed for step {Step}; falling back to ack.", draft.Step);
        }
        // No popup needed — the wizard's edit is the only visible
        // feedback. Acknowledge with a deferred message so Discord
        // doesn't show "Application did not respond".
        await context.Interaction.SendResponseAsync(InteractionCallback.DeferredMessage(
            MessageFlags.Ephemeral));
    }

    // ── Helpers ───────────────────────────────────────────────────────
    private static string ExtractModalText(ModalInteractionContext context)
    {
        // The wizard builds each modal with a single Label wrapping a
        // single TextInput. We walk the (Component → Component) chain.
        if (context.Interaction.Data.Components.Count == 0) return null!;
        var first = context.Interaction.Data.Components[0];
        if (first is Label label && label.Component is TextInput input)
        {
            return input.Value ?? string.Empty;
        }
        return null!;
    }

    private static string MapModalStepToWizardStep(string modalStep) => modalStep switch
    {
        // Free-text modals map back to the canonical wizard step that
        // knows how to apply the text.
        "SystemFreeText" => WizardStepNames.System,
        "DurationFreeText" => WizardStepNames.Duration,
        "PoolSystemDurationFreeText" => WizardStepNames.PoolSystemDuration,
        // Direct mappings.
        _ => modalStep,
    };

    private async Task<IReadOnlyList<WizardClubOption>?> LoadClubsAsync(WizardDraft draft, CancellationToken ct)
    {
        // Inline the same query the messenger would run, so the
        // dispatcher's PickClub step sees the owner's real club list
        // instead of an empty array.
        return await WizardClubLookup.LoadClubsAsync(_dataSource, draft.OwnerId, ct);
    }

    private static WizardPayload LoadPayload(WizardDraft draft) =>
        string.IsNullOrEmpty(draft.PayloadJson)
            ? new WizardPayload()
            : System.Text.Json.JsonSerializer.Deserialize(
                draft.PayloadJson, WizardPayloadJsonContext.Default.WizardPayload) ?? new WizardPayload();

    private static async Task AckWithErrorAsync(Interaction interaction, string text)
    {
        try
        {
            await interaction.SendResponseAsync(InteractionCallback.Message(
                new InteractionMessageProperties()
                    .WithContent($"⚠️ {text}")
                    .WithFlags(MessageFlags.Ephemeral)));
        }
        catch
        {
            /* best effort */
        }
    }
}

/// <summary>
/// Standalone helper that queries the owner-club list without going
/// through <see cref="DiscordWizardMessenger"/>. The dispatcher needs
/// the list at the PickClub step; reusing the messenger's GetOwnerClubsAsync
/// would create a circular DI graph (the messenger depends on
/// IWizardContextStore which the dispatcher also needs).
/// </summary>
internal static class WizardClubLookup
{
    public static async Task<IReadOnlyList<WizardClubOption>> LoadClubsAsync(
        NpgsqlDataSource dataSource, string ownerId, CancellationToken ct)
    {
        // Same SQL the messenger runs for the wizard's render path.
        // Filter by Owner|CoGm role and group by club to dedupe.
        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 = @OwnerId
              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 overload — see TelegramWizardMessenger.
        var rows = await conn.QueryAsync<WizardClubOption>(
            sql,
            new { Platform = "Discord", OwnerId = ownerId });
        return rows.AsList();
    }
}
