using System; using System.Collections.Generic; using System.Text; using GmRelay.Shared.Domain; namespace GmRelay.Shared.Features.Sessions.CreateSession.Wizard; /// /// Produces a (text, list of s) pair for each /// wizard step. This is the "view builder" half of ADR-002: the same /// builder is used by every platform messenger, and each messenger is /// responsible for converting the action list into its native UI /// (Telegram's InlineKeyboardMarkup today, Discord components /// later). /// public static class WizardStepViewBuilder { public static (string Text, IReadOnlyList Actions) Build( WizardDraft draft, WizardPayload payload, IReadOnlyList? clubs = null) { return draft.Step switch { WizardStepNames.Type => BuildType(), WizardStepNames.Title => BuildTitle(), WizardStepNames.Description => BuildDescription(), WizardStepNames.Cover => BuildCover(), WizardStepNames.System => BuildSystem(), WizardStepNames.Duration => BuildDuration(), WizardStepNames.DateTime => BuildDateTime(), WizardStepNames.Capacity => BuildCapacity(), WizardStepNames.Format => BuildFormat(), WizardStepNames.Location => BuildLocation(payload), WizardStepNames.Visibility => BuildVisibility(), WizardStepNames.PickClub => BuildPickClub(clubs ?? Array.Empty()), WizardStepNames.Publish => BuildPublish(), WizardStepNames.Confirm => BuildSingleConfirm(payload), WizardStepNames.PoolSystemDuration => BuildPoolSystemDuration(), WizardStepNames.PoolAddSlots => BuildPoolAddSlots(payload), WizardStepNames.PoolSlotDateTime => BuildPoolSlotDateTime(), WizardStepNames.PoolSlotCapacity => BuildPoolSlotCapacity(), WizardStepNames.PoolConfirm => BuildPoolConfirm(payload), _ => throw new InvalidOperationException($"Unknown wizard step: {draft.Step}"), }; } // ── Single-game views ────────────────────────────────────────────── private static (string, IReadOnlyList) BuildType() => ( "🎲 Создание новой игровой сессии\n\nЧто создаём?", new[] { new WizardAction("🎯 Одну игру", WizardCallbackData.Choice(WizardStepNames.Type, "single"), WizardActionStyle.Primary), new WizardAction("📅 Пул игр", WizardCallbackData.Choice(WizardStepNames.Type, "pool"), WizardActionStyle.Primary), new WizardAction("❌ Отмена", WizardCallbackData.Cancel(), WizardActionStyle.Danger), }); private static (string, IReadOnlyList) BuildTitle() => ( "📝 Введите название игры одним сообщением.", BackCancel()); private static (string, IReadOnlyList) BuildDescription() => ( "📄 Введите описание (или «-», чтобы пропустить).", SkipBackCancel()); private static (string, IReadOnlyList) BuildCover() => ( "🖼 Пришлите картинку как вложение или URL (или «-»).", SkipBackCancel()); private static (string, IReadOnlyList) BuildSystem() => ( "🎲 Выберите систему.", new List { new("D&D 5e", WizardCallbackData.Choice(WizardStepNames.System, "Dnd5e")), new("Pathfinder 2e", WizardCallbackData.Choice(WizardStepNames.System, "Pathfinder2e")), new("Call of Cthulhu",WizardCallbackData.Choice(WizardStepNames.System, "CallOfCthulhu7e")), new("GURPS", WizardCallbackData.Choice(WizardStepNames.System, "GURPS")), new("Fate", WizardCallbackData.Choice(WizardStepNames.System, "Fate")), new("Другое… ✏️", WizardCallbackData.Choice(WizardStepNames.System, "_other")), new("⏭ Пропустить", WizardCallbackData.Choice(WizardStepNames.System, "_skip")), }); private static (string, IReadOnlyList) BuildDuration() => ( "⏱ Выберите длительность.", new List { new("3 часа", WizardCallbackData.Choice(WizardStepNames.Duration, "180")), new("4 часа", WizardCallbackData.Choice(WizardStepNames.Duration, "240")), new("5 часов", WizardCallbackData.Choice(WizardStepNames.Duration, "300")), new("6 часов", WizardCallbackData.Choice(WizardStepNames.Duration, "360")), new("Другое… ✏️", WizardCallbackData.Choice(WizardStepNames.Duration, "_other")), new("⏭ Пропустить", WizardCallbackData.Choice(WizardStepNames.Duration, "_skip")), }); private static (string, IReadOnlyList) BuildDateTime() => ( "📅 Введите дату и время в формате ДД.ММ.ГГГГ ЧЧ:ММ (Москва).", BackCancel()); private static (string, IReadOnlyList) BuildCapacity() => ( "👥 Введите лимит мест (1..50) одним числом.\nЗатем нажмите кнопку waitlist. Или сразу «♾ Без лимита».", new List { new("✅ Waitlist вкл", WizardCallbackData.Choice(WizardStepNames.Capacity, "waitlist:on"), WizardActionStyle.Success), new("❌ Без waitlist", WizardCallbackData.Choice(WizardStepNames.Capacity, "waitlist:off"), WizardActionStyle.Danger), new("♾ Без лимита", WizardCallbackData.Choice(WizardStepNames.Capacity, "no_limit"), WizardActionStyle.Primary), }); private static (string, IReadOnlyList) BuildFormat() => ( "🧭 Выберите формат игры.", new List { new("🌐 Online", WizardCallbackData.Choice(WizardStepNames.Format, "online"), WizardActionStyle.Primary), new("📍 Offline", WizardCallbackData.Choice(WizardStepNames.Format, "offline"), WizardActionStyle.Primary), new("⬅️ Назад", WizardCallbackData.Back()), new("❌ Отмена", WizardCallbackData.Cancel(), WizardActionStyle.Danger), }); private static (string, IReadOnlyList) BuildLocation(WizardPayload payload) => payload.Format switch { WizardSessionFormat.Offline => ("📍 Введите адрес места проведения.", BackCancel()), _ => ("🔗 Введите ссылку для подключения к online-игре.", BackCancel()), }; private static (string, IReadOnlyList) BuildVisibility() => ( "🔒 Выберите видимость.", new List { new("🌐 Публичная в общем showcase", WizardCallbackData.Choice(WizardStepNames.Visibility, "public"), WizardActionStyle.Primary), new("🏠 Публичная в витрине клуба", WizardCallbackData.Choice(WizardStepNames.Visibility, "club"), WizardActionStyle.Primary), new("🔐 Только для членов клуба", WizardCallbackData.Choice(WizardStepNames.Visibility, "members")), new("🏷 Выбрать клуб…", WizardCallbackData.Choice(WizardStepNames.Visibility, "pickclub")), }); private static (string, IReadOnlyList) BuildPickClub(IReadOnlyList clubs) { if (clubs.Count == 0) { return ( "🏷 У вас нет клубов. Создайте клуб в Web dashboard и вернитесь.", BackCancel()); } var actions = new List(clubs.Count); foreach (var club in clubs) { actions.Add(new WizardAction( club.Name, WizardCallbackData.Choice(WizardStepNames.PickClub, club.ClubId.ToString()))); } return ("🏷 Выберите клуб:", actions); } private static (string, IReadOnlyList) BuildPublish() => ( "✨ Опубликовать в витрине сейчас?", new List { new("✅ Опубликовать", WizardCallbackData.Choice(WizardStepNames.Publish, "yes"), WizardActionStyle.Success), new("📝 Только в чате", WizardCallbackData.Choice(WizardStepNames.Publish, "no")), }); private static (string, IReadOnlyList) BuildSingleConfirm(WizardPayload p) { var sb = new StringBuilder(); sb.AppendLine("👀 Проверьте перед созданием:"); sb.AppendLine(); sb.AppendLine($"🎲 {p.Title}"); if (!string.IsNullOrEmpty(p.Description)) sb.AppendLine($"📄 {p.Description}"); if (!string.IsNullOrEmpty(p.System)) sb.AppendLine($"🎲 Система: {p.System}"); if (p.DurationMinutes.HasValue) sb.AppendLine($"⏱ Длительность: {p.DurationMinutes / 60} ч"); AppendFormatLocation(sb, p); if (p.Single?.ScheduledAt is { } at) sb.AppendLine($"📅 {at.FormatMoscow()} (МСК)"); if (p.Single?.MaxPlayers is { } mp) sb.AppendLine($"👥 Мест: {mp}, waitlist {(p.Waitlist == true ? "вкл" : "выкл")}"); sb.AppendLine($"🔒 Видимость: {RenderVisibilityText(p.Visibility)}"); return ( sb.ToString(), new List { new("✅ Создать", WizardCallbackData.Create(), WizardActionStyle.Success), new("⬅️ Назад", WizardCallbackData.Back()), new("❌ Отмена", WizardCallbackData.Cancel(), WizardActionStyle.Danger), }); } // ── Pool views ───────────────────────────────────────────────────── private static (string, IReadOnlyList) BuildPoolSystemDuration() => ( "🎲 Выберите систему и длительность пула.", new List { new("D&D 5e · 4 ч", WizardCallbackData.Choice(WizardStepNames.PoolSystemDuration, "Dnd5e:240")), new("Pathfinder 2e · 4 ч", WizardCallbackData.Choice(WizardStepNames.PoolSystemDuration, "Pathfinder2e:240")), new("Call of Cthulhu · 3 ч",WizardCallbackData.Choice(WizardStepNames.PoolSystemDuration, "CallOfCthulhu7e:180")), new("GURPS · 4 ч", WizardCallbackData.Choice(WizardStepNames.PoolSystemDuration, "GURPS:240")), new("Другое… ✏️", WizardCallbackData.Choice(WizardStepNames.PoolSystemDuration, "_custom")), }); private static (string, IReadOnlyList) BuildPoolAddSlots(WizardPayload p) => ( $"📅 Слоты пула «{p.Title}»\n\nДобавлено: {(p.Pool?.Slots.Count ?? 0)}", new List { new("➕ Добавить слот", WizardCallbackData.Choice(WizardStepNames.PoolAddSlots, "add"), WizardActionStyle.Primary), new("✅ Готово, к превью", WizardCallbackData.Choice(WizardStepNames.PoolAddSlots, "done"), WizardActionStyle.Success), }); private static (string, IReadOnlyList) BuildPoolSlotDateTime() => ( "📅 Введите дату/время слота (ДД.ММ.ГГГГ ЧЧ:ММ).", BackCancel()); private static (string, IReadOnlyList) BuildPoolSlotCapacity() => ( "👥 Введите лимит мест (1..50) и выберите waitlist.", new List { new("✅ Waitlist вкл", WizardCallbackData.Choice(WizardStepNames.PoolSlotCapacity, "waitlist:on"), WizardActionStyle.Success), new("❌ Без waitlist", WizardCallbackData.Choice(WizardStepNames.PoolSlotCapacity, "waitlist:off"), WizardActionStyle.Danger), }); private static (string, IReadOnlyList) BuildPoolConfirm(WizardPayload p) { var sb = new StringBuilder(); sb.AppendLine("👀 Проверьте пул перед созданием:"); sb.AppendLine(); sb.AppendLine($"📝 {p.Title}"); if (!string.IsNullOrEmpty(p.Description)) sb.AppendLine($"📄 {p.Description}"); if (!string.IsNullOrEmpty(p.System)) sb.AppendLine($"🎲 Система: {p.System}"); if (p.DurationMinutes.HasValue) sb.AppendLine($"⏱ Длительность: {p.DurationMinutes / 60} ч"); AppendFormatLocation(sb, p); sb.AppendLine($"🔒 Видимость: {RenderVisibilityText(p.Visibility)}"); sb.AppendLine(); sb.AppendLine($"Слоты ({p.Pool?.Slots.Count ?? 0}):"); if (p.Pool is not null) { foreach (var s in p.Pool.Slots) { sb.AppendLine($" • {s.ScheduledAt.FormatMoscow()} — мест {s.MaxPlayers}, waitlist {(s.Waitlist ? "вкл" : "выкл")}"); } } return ( sb.ToString(), new List { new("✅ Создать пул", WizardCallbackData.Create(), WizardActionStyle.Success), new("⬅️ Назад", WizardCallbackData.Back()), new("❌ Отмена", WizardCallbackData.Cancel(), WizardActionStyle.Danger), }); } // ── Helpers ──────────────────────────────────────────────────────── private static IReadOnlyList BackCancel() => new[] { new WizardAction("⬅️ Назад", WizardCallbackData.Back()), new WizardAction("❌ Отмена", WizardCallbackData.Cancel(), WizardActionStyle.Danger), }; private static IReadOnlyList SkipBackCancel() => new[] { new WizardAction("⏭ Пропустить", WizardCallbackData.Choice("Skip", "1")), new WizardAction("⬅️ Назад", WizardCallbackData.Back()), new WizardAction("❌ Отмена", WizardCallbackData.Cancel(), WizardActionStyle.Danger), }; private static string RenderVisibilityText(WizardVisibility? v) => v switch { WizardVisibility.Public => "публичная в общем showcase", WizardVisibility.Club => "публичная в витрине клуба", WizardVisibility.Members => "только для членов клуба", _ => "не задана", }; private static void AppendFormatLocation(StringBuilder sb, WizardPayload p) { if (p.Format is null) return; sb.AppendLine($"🧭 Формат: {p.Format}"); if (p.Format == WizardSessionFormat.Online && !string.IsNullOrWhiteSpace(p.JoinLink)) { sb.AppendLine($"🔗 Ссылка: {p.JoinLink}"); } else if (p.Format == WizardSessionFormat.Offline && !string.IsNullOrWhiteSpace(p.LocationAddress)) { sb.AppendLine($"📍 Адрес: {p.LocationAddress}"); } } }