using System;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using GmRelay.DiscordBot.Infrastructure.Discord;
using GmRelay.Shared.Features.Sessions.CreateSession.Wizard;
using NetCord;
using NetCord.Rest;
using NetCord.Services.ApplicationCommands;
using Npgsql;

namespace GmRelay.DiscordBot.Features.Sessions.Wizard;

/// <summary>
/// Slash entry point for the Discord wizard. Mirrors the Telegram
/// <c>/newsession-wizard</c> command: a fresh draft is created on
/// first invocation, the persisted first-step message is re-shown
/// when the user already has an active draft, and the owner/co-GM
/// permission check from <see cref="DiscordPermissionChecker"/> is
/// applied before any draft is created.
/// </summary>
public sealed class DiscordWizardCommand : ApplicationCommandModule<SlashCommandContext>
{
    private readonly DiscordWizardMessenger _messenger;
    private readonly DiscordPermissionChecker _permissions;
    private readonly IWizardDraftRepository _drafts;
    private readonly IWizardContextStore _contextStore;
    private readonly NpgsqlDataSource _dataSource;
    private readonly ILogger<DiscordWizardCommand> _log;

    public DiscordWizardCommand(
        DiscordWizardMessenger messenger,
        DiscordPermissionChecker permissions,
        IWizardDraftRepository drafts,
        IWizardContextStore contextStore,
        NpgsqlDataSource dataSource,
        ILogger<DiscordWizardCommand> log)
    {
        _messenger = messenger;
        _permissions = permissions;
        _drafts = drafts;
        _contextStore = contextStore;
        _dataSource = dataSource;
        _log = log;
    }

    [SlashCommand("newsession-wizard", "Пошаговое создание игры или пула")]
    public async Task ExecuteAsync(
        [SlashCommandParameter(Name = "mode", Description = "Пропустить выбор типа (single/pool)")] string? mode = null)
    {
        var guildId = Context.Interaction.GuildId
            ?? throw new InvalidOperationException("This command can only be used in a guild.");

        var channel = Context.Channel
            ?? throw new InvalidOperationException("Channel data not available in interaction.");
        var channelId = channel.Id.ToString(CultureInfo.InvariantCulture);

        var member = Context.User as GuildInteractionUser
            ?? throw new InvalidOperationException("Guild member data not available in interaction.");
        var resolvedPermissions = (ulong)member.Permissions;
        var userId = Context.User.Id.ToString(CultureInfo.InvariantCulture);

        // Slash commands don't expose a CancellationToken on the context;
        // the REST call already has its own per-request cancellation. We
        // pass CancellationToken.None for the DB calls — they're cheap and
        // the host's shutdown will tear them down.
        var ct = CancellationToken.None;
        var ownerId = userId;
        ulong guildOwnerId = 0;
        try
        {
            var guild = await Context.Client.Rest.GetGuildAsync(guildId);
            guildOwnerId = guild.OwnerId;
        }
        catch (RestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
        {
            _log.LogWarning(
                ex,
                "Bot is not a REST member of guild {GuildId}; falling back to permissions from interaction payload.",
                guildId);
        }

        // Permission check: server owner, guild admin, or DB manager
        // for the Discord game_group.
        var dbManagerIds = await DiscordPermissionLookup.LoadManagerUserIdsAsync(
            _dataSource, guildId, ct);
        if (!_permissions.CanManageSchedule(guildOwnerId, Context.User.Id, dbManagerIds, resolvedPermissions))
        {
            await Context.Interaction.SendResponseAsync(InteractionCallback.Message(
                new InteractionMessageProperties()
                    .WithContent("⛔ Только owner, администратор или manager могут создавать сессии.")
                    .WithFlags(MessageFlags.Ephemeral)));
            return;
        }

        // If there's already a draft, offer Continue / Start over.
        var existing = await _drafts.GetActiveAsync("Discord", ownerId, ct);
        if (existing is not null && existing.Id != Guid.Empty)
        {
            await Context.Interaction.SendResponseAsync(InteractionCallback.Message(
                new InteractionMessageProperties()
                    .WithContent("📝 У вас уже есть активный мастер. Продолжить?")
                    .WithFlags(MessageFlags.Ephemeral)
                    .WithComponents(BuildResumeRow(existing.Id))));
            return;
        }

        var draft = new WizardDraft
        {
            Id = Guid.NewGuid(),
            ChatId = guildId.ToString(CultureInfo.InvariantCulture),
            MessageThreadId = null,
            OwnerId = ownerId,
            Platform = "Discord",
            Step = NormalizeMode(mode) is { } m && m == WizardCreationType.Pool
                ? WizardStepNames.Title
                : WizardStepNames.Type,
            CreatedAt = DateTime.UtcNow,
            UpdatedAt = DateTime.UtcNow,
            ExpiresAt = DateTime.UtcNow.AddHours(24),
        };
        // If the user passed `mode=pool` we pre-seed the payload so the
        // wizard's own branching lands on the pool flow.
        if (NormalizeMode(mode) is { } mt)
        {
            draft.PayloadJson = System.Text.Json.JsonSerializer.Serialize(
                new WizardPayload { Type = mt },
                WizardPayloadJsonContext.Default.WizardPayload);
        }

        // Stash the context BEFORE sending so the messenger can both
        // send the message and persist the returned message id back.
        _contextStore.Set(draft.Id, new DiscordWizardContext(
            GuildId: guildId.ToString(CultureInfo.InvariantCulture),
            ChannelId: channelId,
            MessageId: string.Empty,
            ThreadId: null));

        // Render + send the first step. Defer the response so we can
        // show the wizard message in the channel.
        await Context.Interaction.SendResponseAsync(InteractionCallback.DeferredMessage(
            MessageFlags.Ephemeral));

        try
        {
            var payload = LoadPayload(draft);
            var (text, actions) = WizardStepViewBuilder.Build(draft, payload);
            var msgId = await _messenger.SendDraftMessageAsync(draft, text, actions, ct);
            draft.DraftMessageId = msgId;
            draft.UpdatedAt = DateTime.UtcNow;
            await _drafts.UpsertAsync(draft, ct);

            await Context.Interaction.ModifyResponseAsync(msg =>
            {
                msg.Content = "🎲 Мастер запущен. См. сообщение ниже.";
                msg.Flags = MessageFlags.Ephemeral;
            });
        }
        catch (Exception ex)
        {
            _log.LogError(ex, "Failed to start wizard for user {OwnerId} in guild {GuildId}", ownerId, guildId);
            _contextStore.Remove(draft.Id);
            try
            {
                await _drafts.DeleteAsync(draft.Id, ct);
            }
            catch
            {
                /* best effort */
            }
            await Context.Interaction.ModifyResponseAsync(msg =>
            {
                msg.Content = "💥 Не удалось запустить мастер. Попробуйте позже.";
                msg.Flags = MessageFlags.Ephemeral;
            });
        }
    }

    private static WizardCreationType? NormalizeMode(string? mode) =>
        mode?.ToLowerInvariant() switch
        {
            "single" or "одну" or "one" => WizardCreationType.Single,
            "pool" or "пул" => WizardCreationType.Pool,
            _ => null,
        };

    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 IReadOnlyList<IMessageComponentProperties> BuildResumeRow(Guid draftId)
    {
        // Direct format strings (not ChoiceButtonCustomId) — these
        // are control actions (resume:cancel), not wizard-step choices.
        // The dispatcher's switch matches parts[1] as "resume" or
        // "cancel" directly, not as a "choice" prefix.
        var row = new ActionRowProperties();
        row.Add(new ButtonProperties(
            "wizard:btn:resume:continue",
            "▶️ Продолжить",
            ButtonStyle.Primary));
        row.Add(new ButtonProperties(
            "wizard:btn:resume:restart",
            "🔄 Заново",
            ButtonStyle.Secondary));
        row.Add(new ButtonProperties(
            "wizard:btn:cancel:1",
            "❌ Отмена",
            ButtonStyle.Danger));
        return new IMessageComponentProperties[] { row };
    }
}
