using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using GmRelay.Shared.Domain;
using GmRelay.Shared.Features.Sessions.CreateSession.Wizard;
using Microsoft.Extensions.Logging;
using Telegram.Bot.Types;
using Telegram.Bot.Types.ReplyMarkups;

namespace GmRelay.Bot.Features.Sessions.CreateSession.Wizard;

/// <summary>
/// Central state machine for the game/pool creation wizard.
/// </summary>
public sealed class GameCreationWizard
{
    private readonly IWizardDraftRepository _drafts;
    private readonly ITelegramWizardMessenger _messenger;
    private readonly ILogger<GameCreationWizard> _log;

    public GameCreationWizard(
        IWizardDraftRepository drafts,
        ITelegramWizardMessenger messenger,
        ILogger<GameCreationWizard> log)
    {
        _drafts = drafts;
        _messenger = messenger;
        _log = log;
    }

    /// <summary>Handle a text or callback update from the owning GM.</summary>
    public async Task HandleUpdateAsync(Update update, WizardDraft draft, CancellationToken ct)
    {
        try
        {
            if (update.CallbackQuery is { } cb)
            {
                await HandleCallbackAsync(draft, cb, ct);
            }
            else if (update.Message is { } msg)
            {
                await HandleTextAsync(draft, msg, ct);
            }
        }
        catch (WizardStorageException)
        {
            // Surface storage failure; do not crash the update loop.
            if (update.CallbackQuery is { } cb2)
            {
                await _messenger.AnswerCallbackAsync(cb2.Id, "💥 Ошибка хранилища, попробуйте /newsession", ct);
            }
        }
        catch (Exception ex)
        {
            _log.LogError(ex, "Wizard update failed for draft {DraftId}", draft.Id);
            if (update.CallbackQuery is { } cb3)
            {
                try { await _messenger.AnswerCallbackAsync(cb3.Id, "⚠️ Ошибка", ct); }
                catch { /* swallow — we're already in error path */ }
            }
        }
    }

    private async Task HandleCallbackAsync(WizardDraft draft, CallbackQuery cb, CancellationToken ct)
    {
        if (!WizardCallbackData.TryParse(cb.Data, out var action, out var step, out var choice))
        {
            await _messenger.AnswerCallbackAsync(cb.Id, "Неизвестная команда", ct);
            return;
        }

        switch (action)
        {
            case "cancel":
                await _drafts.DeleteAsync(draft.Id, ct);
                await _messenger.EditMessageTextAsync(
                    draft.ChatId, draft.MessageThreadId, draft.DraftMessageId ?? 0,
                    "❌ Мастер отменён.", EmptyKeyboard, ct);
                await _messenger.AnswerCallbackAsync(cb.Id, null, ct);
                return;

            case "back":
                ApplyBack(draft, step);
                await PersistAndRenderAsync(draft, cb.Id, ct);
                return;

            case "create":
                // Routed by CreateSessionHandler, not here.
                await _messenger.AnswerCallbackAsync(cb.Id, null, ct);
                return;

            default:
                // For "Choice" callbacks, action == step.
                await ApplyChoiceAsync(draft, step, choice, cb.Id, ct);
                return;
        }
    }

    private async Task HandleTextAsync(WizardDraft draft, Message msg, CancellationToken ct)
    {
        if (msg.Text is not { } text)
        {
            // Photo or other non-text — handle cover step only.
            if (msg.Photo is { Length: > 0 } && draft.Step == WizardStepNames.Cover)
            {
                var fileId = msg.Photo[^1].FileId;
                ApplyCoverPhoto(draft, fileId);
                await PersistAndRenderAsync(draft, null, ct);
            }
            return;
        }

        var (nextStep, error, payload) = ApplyText(draft, text);
        if (payload is { } p) SavePayload(draft, p);
        if (error is { } errMsg && draft.DraftMessageId is { } mid)
        {
            // Re-render the same step with ⚠️ prefix.
            var (rendered, kb) = WizardStep.Render(draft, LoadPayload(draft), null);
            await _messenger.EditMessageTextAsync(
                draft.ChatId, draft.MessageThreadId, mid,
                "⚠️ " + errMsg + "\n\n" + rendered, kb, ct);
            return;
        }

        if (nextStep is { } step)
        {
            draft.Step = step;
        }
        await PersistAndRenderAsync(draft, null, ct);
    }

    private async Task ApplyChoiceAsync(WizardDraft draft, string step, string choice, string callbackId, CancellationToken ct)
    {
        var (nextStep, error, payload) = ApplyChoice(draft, step, choice);
        if (error is { } err)
        {
            await _messenger.AnswerCallbackAsync(callbackId, err, ct);
            return;
        }
        if (payload is { } p) SavePayload(draft, p);
        if (nextStep is { } s)
        {
            draft.Step = s;
        }
        await PersistAndRenderAsync(draft, callbackId, ct);
    }

    private async Task PersistAndRenderAsync(WizardDraft draft, string? callbackId, CancellationToken ct)
    {
        draft.UpdatedAt = DateTimeOffset.UtcNow;
        await _drafts.UpsertAsync(draft, ct);
        var payload = LoadPayload(draft);
        IReadOnlyList<WizardClubOption>? clubs = null;
        if (draft.Step == WizardStepNames.PickClub)
        {
            clubs = await _messenger.GetGmClubsAsync(draft.OwnerTelegramId, ct);
        }
        var (text, kb) = WizardStep.Render(draft, payload, clubs);
        await _messenger.EditMessageTextAsync(
            draft.ChatId, draft.MessageThreadId, draft.DraftMessageId ?? 0,
            text, kb, ct);
        if (callbackId is { } id)
        {
            await _messenger.AnswerCallbackAsync(id, null, ct);
        }
    }

    // ── Text input dispatcher ─────────────────────────────────────────
    private static (string? nextStep, string? error, WizardPayload payload) ApplyText(WizardDraft draft, string input)
    {
        var payload = LoadPayload(draft);
        switch (draft.Step)
        {
            case WizardStepNames.Title:
                return ValidateText(input, WizardStep.MaxTitleLength, "Название не может быть пустым", "Слишком длинное название", out var title)
                    ? (WizardStepNames.Description, SetTitle(payload, title), payload)
                    : (null, title, payload);

            case WizardStepNames.Description:
                if (input == "-") return (WizardStepNames.Cover, SetDescription(payload, null), payload);
                return ValidateText(input, WizardStep.MaxDescriptionLength, "Описание не может быть пустым", "Слишком длинное описание", out var desc)
                    ? (WizardStepNames.Cover, SetDescription(payload, desc), payload)
                    : (null, desc, payload);

            case WizardStepNames.Cover:
                if (input == "-") return (NextAfterCover(payload), SetImageUrl(payload, null), payload);
                if (Uri.TryCreate(input, UriKind.Absolute, out var uri) && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps))
                    return (NextAfterCover(payload), SetImageUrl(payload, input), payload);
                return (null, "Некорректный URL", payload);

            case WizardStepNames.System when payload.System is null:
                // "Other" branch — only active if free-text was offered.
                return ValidateText(input, WizardStep.MaxSystemLength, "Слишком длинное название системы", "Слишком длинное название системы", out var sys)
                    ? (WizardStepNames.Duration, SetSystem(payload, sys), payload)
                    : (null, sys, payload);

            case WizardStepNames.Duration when payload.DurationMinutes is null:
                return TryParseHours(input, out var durMin)
                    ? (WizardStepNames.DateTime, SetDurationMinutes(payload, durMin), payload)
                    : (null, "Неверная длительность (1..12 ч)", payload);

            case WizardStepNames.DateTime:
                return MoscowTime.TryParseMoscow(input, out var dt) && dt > DateTimeOffset.UtcNow
                    ? (WizardStepNames.Capacity, SetScheduledAt(payload, dt), payload)
                    : (null, dt == default ? "Не удалось разобрать дату" : "Дата в прошлом", payload);

            case WizardStepNames.Capacity when payload.Single?.MaxPlayers is null:
                return int.TryParse(input, out var cap) && cap >= WizardStep.MinCapacity && cap <= WizardStep.MaxCapacity
                    ? (WizardStepNames.Visibility, SetMaxPlayers(payload, cap), payload)
                    : (null, "Лимит должен быть 1..50", payload);

            case WizardStepNames.PoolSystemDuration when payload.System is null:
                return ValidateText(input, WizardStep.MaxSystemLength, "Слишком длинное название системы", "Слишком длинное название системы", out var psys)
                    ? (WizardStepNames.PoolSystemDuration, SetSystem(payload, psys), payload)
                    : (null, psys, payload);

            case WizardStepNames.PoolSystemDuration when payload.DurationMinutes is null:
                return TryParseHours(input, out var pdur)
                    ? (WizardStepNames.Visibility, SetDurationMinutes(payload, pdur), payload)
                    : (null, "Неверная длительность (1..12 ч)", payload);

            case WizardStepNames.PoolSlotDateTime:
                return MoscowTime.TryParseMoscow(input, out var slotDt) && slotDt > DateTimeOffset.UtcNow
                    ? (WizardStepNames.PoolSlotCapacity, SetCurrentSlotDateTime(payload, slotDt), payload)
                    : (null, slotDt == default ? "Не удалось разобрать дату" : "Дата в прошлом", payload);

            case WizardStepNames.PoolSlotCapacity:
                return int.TryParse(input, out var slotCap) && slotCap >= WizardStep.MinCapacity && slotCap <= WizardStep.MaxCapacity
                    ? (WizardStepNames.PoolAddSlots, SetCurrentSlotMaxPlayers(payload, slotCap), payload)
                    : (null, "Лимит должен быть 1..50", payload);

            default:
                return (null, "Ожидается выбор кнопкой", payload);
        }
    }

    // ── Callback (button) dispatcher ──────────────────────────────────
    private static (string? nextStep, string? error, WizardPayload payload) ApplyChoice(WizardDraft draft, string step, string choice)
    {
        var payload = LoadPayload(draft);
        var (next, err) = step switch
        {
            WizardStepNames.Type => ApplyTypeChoice(payload, choice),
            WizardStepNames.System => ApplySystemChoice(payload, choice),
            WizardStepNames.Duration => ApplyDurationChoice(payload, choice),
            WizardStepNames.Capacity => ApplyCapacityChoice(payload, choice),
            WizardStepNames.Visibility => ApplyVisibilityChoice(payload, choice),
            WizardStepNames.PickClub => ApplyPickClubChoice(payload, choice),
            WizardStepNames.Publish => ApplyPublishChoice(payload, choice),
            WizardStepNames.PoolSystemDuration => ApplyPoolSystemDurationChoice(payload, choice),
            WizardStepNames.PoolAddSlots => ApplyPoolAddSlotsChoice(payload, choice),
            WizardStepNames.PoolSlotCapacity => ApplyPoolSlotCapacityChoice(payload, choice),
            _ => (null, "Неизвестный шаг"),
        };
        return (next, err, payload);
    }

    private static (string?, string?) ApplyTypeChoice(WizardPayload p, string choice) => choice switch
    {
        "single" => (WizardStepNames.Title, SetType(p, WizardCreationType.Single)),
        "pool" => (WizardStepNames.Title, SetType(p, WizardCreationType.Pool)),
        _ => (null, "Неизвестный выбор"),
    };

    private static (string?, string?) ApplySystemChoice(WizardPayload p, string choice) => choice switch
    {
        "_other" => (WizardStepNames.System, null),                            // stay, await text
        "_skip" => (NextAfterSystem(p), SetSystem(p, null)),
        { } code => (WizardStepNames.Duration, SetSystem(p, code)),
    };

    private static (string?, string?) ApplyDurationChoice(WizardPayload p, string choice) => choice switch
    {
        "_other" => (WizardStepNames.Duration, null),
        "_skip" => (NextAfterDuration(p), SetDurationMinutes(p, null)),
        { } d => int.TryParse(d, out var min)
                    ? (NextAfterDuration(p), SetDurationMinutes(p, min))
                    : (null, "Неверная длительность"),
    };

    private static (string?, string?) ApplyCapacityChoice(WizardPayload p, string choice) => choice switch
    {
        "waitlist:on" => (WizardStepNames.Visibility, SetWaitlist(p, true)),
        "waitlist:off" => (WizardStepNames.Visibility, SetWaitlist(p, false)),
        _ => (null, "Неизвестный выбор"),
    };

    private static (string?, string?) ApplyVisibilityChoice(WizardPayload p, string choice) => choice switch
    {
        "public" => (NextAfterVisibility(p), SetVisibility(p, WizardVisibility.Public)),
        "club" => (WizardStepNames.PickClub, SetVisibility(p, WizardVisibility.Club)),
        "members" => (WizardStepNames.PickClub, SetVisibility(p, WizardVisibility.Members)),
        "pickclub" => (WizardStepNames.PickClub, null),
        _ => (null, "Неизвестный выбор"),
    };

    private static (string?, string?) ApplyPickClubChoice(WizardPayload p, string choice)
        => Guid.TryParse(choice, out var id)
            ? (NextAfterVisibility(p), SetClubId(p, id))
            : (null, "Неверный идентификатор клуба");

    private static (string?, string?) ApplyPublishChoice(WizardPayload p, string choice) => choice switch
    {
        "yes" => (WizardStepNames.Confirm, SetPublishInShowcase(p, true)),
        "no" => (WizardStepNames.Confirm, SetPublishInShowcase(p, false)),
        _ => (null, "Неизвестный выбор"),
    };

    private static (string?, string?) ApplyPoolSystemDurationChoice(WizardPayload p, string choice) => choice switch
    {
        "_custom" => (WizardStepNames.PoolSystemDuration, null),
        { } c when c.Contains(':') => SplitSystemDuration(c) is (var sys, var dur)
            ? (WizardStepNames.Visibility, SetSystem(p, sys) ?? SetDurationMinutes(p, dur))
            : (null, "Неверный выбор"),
        _ => (null, "Неизвестный выбор"),
    };

    private static (string?, string?) ApplyPoolAddSlotsChoice(WizardPayload p, string choice) => choice switch
    {
        "add" => BeginNewPoolSlot(p),
        "done" => p.Pool?.Slots.Count > 0
                    ? (WizardStepNames.PoolConfirm, null)
                    : (null, "Добавьте хотя бы один слот"),
        _ => (null, "Неизвестный выбор"),
    };

    private static (string?, string?) ApplyPoolSlotCapacityChoice(WizardPayload p, string choice) => choice switch
    {
        "waitlist:on" => (WizardStepNames.PoolAddSlots, CommitCurrentPoolSlot(p, true)),
        "waitlist:off" => (WizardStepNames.PoolAddSlots, CommitCurrentPoolSlot(p, false)),
        _ => (null, "Неизвестный выбор"),
    };

    // ── Back navigation ───────────────────────────────────────────────
    private static void ApplyBack(WizardDraft draft, string fromStep)
    {
        // The callback's "step" portion is the step the user is currently on (e.g. the
        // Confirm button emits `wizard:back` with no step, in which case we fall back to
        // the draft's current step). Both should produce the same result.
        var current = string.IsNullOrEmpty(fromStep) ? draft.Step : fromStep;
        var payload = LoadPayload(draft);
        var previous = PreviousStep(current, payload);
        if (previous is { } step) draft.Step = step;
    }

    private static string? PreviousStep(string step, WizardPayload p) => step switch
    {
        WizardStepNames.Title => null,   // first step
        WizardStepNames.Description => WizardStepNames.Title,
        WizardStepNames.Cover => WizardStepNames.Description,
        WizardStepNames.System => WizardStepNames.Cover,
        WizardStepNames.Duration => WizardStepNames.System,
        WizardStepNames.DateTime => WizardStepNames.Duration,
        WizardStepNames.Capacity => WizardStepNames.DateTime,
        WizardStepNames.Visibility => WizardStepNames.Capacity,
        WizardStepNames.PickClub => WizardStepNames.Visibility,
        WizardStepNames.Publish => WizardStepNames.PickClub,
        WizardStepNames.Confirm => WizardStepNames.Publish,

        WizardStepNames.PoolSystemDuration => null, // first pool step
        WizardStepNames.PoolAddSlots => WizardStepNames.PoolSystemDuration,
        WizardStepNames.PoolSlotDateTime => WizardStepNames.PoolAddSlots,
        WizardStepNames.PoolSlotCapacity => WizardStepNames.PoolSlotDateTime,
        WizardStepNames.PoolConfirm => WizardStepNames.PoolAddSlots,
        _ => null,
    };

    // ── Payload I/O ───────────────────────────────────────────────────
    internal static WizardPayload LoadPayload(WizardDraft draft)
    {
        if (string.IsNullOrEmpty(draft.PayloadJson)) return new WizardPayload();
        return System.Text.Json.JsonSerializer.Deserialize(
            draft.PayloadJson, WizardPayloadJsonContext.Default.WizardPayload) ?? new WizardPayload();
    }

    private static void SavePayload(WizardDraft draft, WizardPayload payload)
    {
        draft.PayloadJson = System.Text.Json.JsonSerializer.Serialize(
            payload, WizardPayloadJsonContext.Default.WizardPayload);
    }

    // Mutators — return the error message if any (kept here to centralise flow).
    private static string? SetTitle(WizardPayload p, string v) { p.Title = v; return null; }
    private static string? SetDescription(WizardPayload p, string? v) { p.Description = v; return null; }
    private static string? SetImageUrl(WizardPayload p, string? v) { p.ImageUrl = v; p.ImageFileId = null; return null; }
    private static void ApplyCoverPhoto(WizardDraft d, string fileId)
    {
        var p = LoadPayload(d);
        p.ImageFileId = fileId;
        p.ImageUrl = null;
        SavePayload(d, p);
        var next = NextAfterCover(p);
        if (next is { } s) d.Step = s;
    }
    private static string? SetSystem(WizardPayload p, string? v) { p.System = v; return null; }
    private static string? SetDurationMinutes(WizardPayload p, int? v) { p.DurationMinutes = v; return null; }
    private static string? SetScheduledAt(WizardPayload p, DateTimeOffset v)
    { p.Single ??= new WizardSingleInput(); p.Single.ScheduledAt = v; return null; }
    private static string? SetMaxPlayers(WizardPayload p, int v)
    { p.Single ??= new WizardSingleInput(); p.Single.MaxPlayers = v; return null; }
    private static string? SetWaitlist(WizardPayload p, bool v) { p.Waitlist = v; return null; }
    private static string? SetVisibility(WizardPayload p, WizardVisibility? v) { p.Visibility = v; return null; }
    private static string? SetClubId(WizardPayload p, Guid v) { p.ClubId = v; return null; }
    private static string? SetType(WizardPayload p, WizardCreationType v) { p.Type = v; return null; }
    private static string? SetPublishInShowcase(WizardPayload p, bool v) { p.PublishInShowcase = v; return null; }

    private static string? SetCurrentSlotDateTime(WizardPayload p, DateTimeOffset v)
    {
        p.Pool ??= new WizardPoolInput();
        var current = EnsureCurrentPoolSlot(p);
        current.ScheduledAt = v;
        return null;
    }
    private static string? SetCurrentSlotMaxPlayers(WizardPayload p, int v)
    {
        p.Pool ??= new WizardPoolInput();
        var current = EnsureCurrentPoolSlot(p);
        current.MaxPlayers = v;
        return null;
    }
    private static string? CommitCurrentPoolSlot(WizardPayload p, bool waitlist)
    {
        p.Pool ??= new WizardPoolInput();
        var current = EnsureCurrentPoolSlot(p);
        current.Waitlist = waitlist;
        return null;
    }
    private static (string? nextStep, string? error) BeginNewPoolSlot(WizardPayload p)
    {
        p.Pool ??= new WizardPoolInput();
        p.Pool.Slots.Add(new WizardSlotInput());
        return (WizardStepNames.PoolSlotDateTime, null);
    }
    private static WizardSlotInput EnsureCurrentPoolSlot(WizardPayload p)
    {
        // Slots added via BeginNewPoolSlot are always committed before they leave the
        // PoolSlotCapacity step (CommitCurrentPoolSlot). If we somehow get here without
        // a slot, start a new one to keep the flow recoverable.
        p.Pool ??= new WizardPoolInput();
        var last = p.Pool.Slots.LastOrDefault();
        if (last is not null && last.MaxPlayers == 0) return last;
        p.Pool.Slots.Add(new WizardSlotInput());
        return p.Pool.Slots[^1];
    }

    // ── Flow helpers ──────────────────────────────────────────────────
    private static string? NextAfterCover(WizardPayload p) => p.Type == WizardCreationType.Pool
        ? WizardStepNames.PoolSystemDuration : WizardStepNames.System;
    private static string? NextAfterSystem(WizardPayload p) => WizardStepNames.Duration;
    private static string? NextAfterDuration(WizardPayload p)
    {
        if (p.Type == WizardCreationType.Pool) return WizardStepNames.Visibility;
        return p.Single?.MaxPlayers is not null ? WizardStepNames.Visibility : WizardStepNames.DateTime;
    }
    private static string? NextAfterVisibility(WizardPayload p)
    {
        if (p.Visibility is WizardVisibility.Club or WizardVisibility.Members)
        {
            if (p.ClubId is null) return WizardStepNames.PickClub;
        }
        return p.Type == WizardCreationType.Pool ? WizardStepNames.PoolAddSlots : WizardStepNames.Publish;
    }

    private static (string? sys, int? dur) SplitSystemDuration(string s)
    {
        var idx = s.IndexOf(':');
        if (idx <= 0 || idx >= s.Length - 1) return (null, null);
        var sys = s.Substring(0, idx);
        if (!int.TryParse(s.Substring(idx + 1), out var durMin)) return (null, null);
        return (sys, durMin);
    }

    private static bool ValidateText(
        string input, int maxLength, string emptyMsg, string tooLongMsg, out string trimmed)
    {
        trimmed = input.Trim();
        if (string.IsNullOrEmpty(trimmed))
        {
            trimmed = emptyMsg;
            return false;
        }
        if (trimmed.Length > maxLength)
        {
            trimmed = tooLongMsg;
            return false;
        }
        return true;
    }

    private static bool TryParseHours(string input, out int minutes)
    {
        minutes = 0;
        var s = input.Trim();
        if (s.EndsWith("h", StringComparison.OrdinalIgnoreCase)) s = s.Substring(0, s.Length - 1);
        if (s.EndsWith("ч", StringComparison.OrdinalIgnoreCase)) s = s.Substring(0, s.Length - 1);
        if (!double.TryParse(s, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var hours)) return false;
        if (hours < WizardStep.MinDurationHours || hours > WizardStep.MaxDurationHours) return false;
        minutes = (int)Math.Round(hours * 60);
        return true;
    }

    private static readonly InlineKeyboardMarkup EmptyKeyboard = new(Array.Empty<InlineKeyboardButton[]>());
}
