@page "/group/{GroupId:guid}"
@using GmRelay.Web.Services
@using GmRelay.Shared.Domain
@using GmRelay.Web.Services.Portfolio
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@attribute [Authorize]
@inject AuthorizedSessionService SessionService
@inject AuthorizedPortfolioService PortfolioService
@inject AuthorizedMembershipService MembershipService
@inject AuthenticationStateProvider AuthStateProvider
@inject NavigationManager Navigation

<PageTitle>Сессии группы — GM-Relay</PageTitle>

<div class="page-container">
    <ul class="gm-breadcrumb animate-fade-in">
        <li><a href="/">Главная</a></li>
        <li class="active">Сессии группы</li>
    </ul>

    <div class="page-header animate-fade-in">
        <h2>📅 Предстоящие игры</h2>
    </div>

    @if (groupManagement is not null)
    {
        <div class="glass-card animate-slide-up" style="margin-bottom: 1rem;">
            <div class="batch-bulk-header">
                <div>
                    <h3>Управление группой</h3>
                    <p>@groupManagement.Group.Name · @FormatRole(CurrentUserRole)</p>
                </div>
                <span class="status-badge status-info">@FormatRole(CurrentUserRole)</span>
            </div>

            <div style="display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 1rem;">
                <a href="/groupstats/@GroupId" class="btn-gm btn-gm-outline">📊 Статистика</a>
                @foreach (var manager in groupManagement.Managers)
                {
                    <span class="status-badge @(manager.Role == GroupManagerRoleExtensions.OwnerValue ? "status-success" : "status-info")">
                        @FormatManager(manager)
                    </span>
                    @if (groupManagement.CurrentUserIsOwner && manager.Role == GroupManagerRoleExtensions.CoGmValue)
                    {
                        <button type="button" class="btn-gm btn-gm-outline" style="font-size: 0.75rem; padding: 0.25rem 0.5rem;" disabled="@(removingCoGmId == ManagerKey(manager))" @onclick="() => RemoveCoGm(manager)">
                            @(removingCoGmId == ManagerKey(manager) ? "⏳ Удаляем..." : "Убрать")
                        </button>
                    }
                }
            </div>

            @if (groupManagement.CurrentUserIsOwner)
            {
                <EditForm Model="@coGmModel" OnValidSubmit="AddCoGm">
                    <div class="batch-bulk-fields">
                        <div class="gm-form-group">
                            <label class="gm-form-label">@CoGmIdLabel</label>
                            <InputText @bind-Value="coGmModel.ExternalUserId" class="gm-form-control" />
                        </div>
                        <div class="gm-form-group">
                            <label class="gm-form-label">Имя</label>
                            <InputText @bind-Value="coGmModel.DisplayName" class="gm-form-control" />
                        </div>
                        <div class="gm-form-group">
                            <label class="gm-form-label">Username</label>
                            <InputText @bind-Value="coGmModel.ExternalUsername" class="gm-form-control" />
                        </div>
                    </div>
                    <button type="submit" class="btn-gm btn-gm-primary" disabled="@isAddingCoGm">
                        @(isAddingCoGm ? "⏳ Добавляем..." : "➕ Добавить co-GM")
                    </button>
                </EditForm>
            }
        </div>
    }

    @if (publicSettings is not null)
    {
        <div class="glass-card animate-slide-up public-settings-panel" style="margin-bottom: 1rem;">
            <div class="batch-bulk-header">
                <div>
                    <h3>Публичная страница клуба</h3>
                    <p>@publicSettings.PublicSessionCount опубликованных игр без состава игроков и приватных ссылок</p>
                </div>
                <span class="status-badge @(publicSettings.PublicScheduleEnabled ? "status-success" : "status-neutral")">
                    @(publicSettings.PublicScheduleEnabled ? "Включена" : "Выключена")
                </span>
            </div>

            <EditForm Model="@publicSettingsModel" OnValidSubmit="SavePublicSettings">
                <div class="batch-bulk-fields">
                    <div class="gm-form-group public-toggle-field">
                        <label class="gm-checkbox-label">
                            <InputCheckbox @bind-Value="publicSettingsModel.PublicScheduleEnabled" />
                            <span>Включить публичное расписание</span>
                        </label>
                        <div class="gm-form-hint">Если выключено, публичная страница и ссылки на сессии недоступны.</div>
                    </div>
                    <div class="gm-form-group">
                        <label class="gm-form-label">Короткий адрес</label>
                        <InputText @bind-Value="publicSettingsModel.PublicSlug" class="gm-form-control" />
                        <div class="gm-form-hint">Латиница, цифры и дефисы, например `night-city-club`.</div>
                    </div>
                </div>

                <div class="public-settings-actions">
                    <button type="submit" class="btn-gm btn-gm-primary" disabled="@savingPublicSettings">
                        @(savingPublicSettings ? "Сохраняем..." : "Сохранить публикацию")
                    </button>
                    @if (PublicClubUrl is not null && publicSettings.PublicScheduleEnabled)
                    {
                        <a href="@PublicClubUrl" target="_blank" rel="noopener noreferrer" class="btn-gm btn-gm-outline">
                            Открыть публичную страницу
                        </a>
                    }
                </div>
            </EditForm>

            @if (PublicClubUrl is not null && publicSettings.PublicScheduleEnabled)
            {
                <div class="public-link-row">
                    <span>Ссылка клуба</span>
                    <a href="@PublicClubUrl" target="_blank" rel="noopener noreferrer">@PublicClubUrl</a>
                </div>
            }
        </div>
    }

    @if (pendingApplicationsCount > 0)
    {
        <a class="glass-card applications-card" href="@($"/group/{GroupId}/applications")">
            <span class="status-badge status-warning">📨 Заявки участников (@pendingApplicationsCount)</span>
            <span>Рассмотреть заявки на участие в клубе</span>
        </a>
    }

    @if (!string.IsNullOrEmpty(errorMessage))
    {
        <div class="gm-alert gm-alert-danger" style="margin-bottom: 1rem;">
            ⚠️ @errorMessage
        </div>
    }

    @if (!string.IsNullOrEmpty(successMessage))
    {
        <div class="gm-alert gm-alert-success" style="margin-bottom: 1rem;">
            ✅ @successMessage
        </div>
    }

    @if (portfolioGames is not null)
    {
        <div class="glass-card animate-slide-up" style="margin-bottom: 1rem;">
            <div class="batch-bulk-header">
                <div>
                    <h3>Проведённые приключения</h3>
                    <p>Черновики и опубликованные приключения для каталога мастера.</p>
                </div>
                <button type="button" class="btn-gm btn-gm-success" disabled="@isCreatingDraft" @onclick="CreateDraft">
                    @(isCreatingDraft ? "⏳ Создаём..." : "➕ Создать")
                </button>
            </div>

            @if (portfolioGames.Count == 0)
            {
                <div class="empty-state empty-state-compact">
                    <div class="empty-state-title">Приключений пока нет</div>
                    <p class="empty-state-text">Создайте первый черновик и добавьте проведённые сессии.</p>
                </div>
            }
            else
            {
                <div class="portfolio-management-list">
                    @foreach (var game in portfolioGames)
                    {
                        <div class="portfolio-management-row">
                            <div class="portfolio-management-info">
                                <a href="/portfolio/manage/@game.Id" class="portfolio-management-title">@game.Title</a>
                                <span class="status-badge @(game.IsPublic ? "status-success" : "status-neutral")">
                                    @(game.IsPublic ? "Опубликовано" : "Черновик")
                                </span>
                            </div>
                            <div class="portfolio-management-meta">
                                <span class="status-badge status-info">@game.SessionCount игр</span>
                                <span class="status-badge status-info">@game.MasterCount мастеров</span>
                                @if (game.PendingReviewCount > 0)
                                {
                                    <span class="status-badge status-warning">@game.PendingReviewCount на модерации</span>
                                }
                            </div>
                            <div class="portfolio-management-actions">
                                <a href="/portfolio/manage/@game.Id" class="btn-gm btn-gm-outline">✏️ Изменить</a>
                            </div>
                        </div>
                    }
                </div>
            }

            <div style="margin-top: 0.75rem;">
                <a href="/group/@GroupId/completed" class="btn-gm btn-gm-outline">📜 Все проведённые сессии</a>
            </div>
        </div>
    }

    @if (campaignTemplates is not null)
    {
        <div class="glass-card campaign-template-panel animate-slide-up">
            <div class="batch-bulk-header">
                <div>
                    <h3>Применить шаблон</h3>
                    <p>@campaignTemplateModels.Count доступных для этой группы</p>
                </div>
                <a href="/templates" class="btn-gm btn-gm-outline">⚙️ Управлять шаблонами</a>
            </div>

            @if (campaignTemplateModels.Count == 0)
            {
                <div class="empty-state empty-state-compact">
                    <div class="empty-state-title">Шаблонов пока нет</div>
                    <p class="empty-state-text">Создайте шаблоны в отдельной вкладке, а здесь запускайте из них новые batch-расписания.</p>
                </div>
            }
            else
            {
                <div class="campaign-template-list">
                    @foreach (var template in campaignTemplateModels)
                    {
                        <div class="campaign-template-row">
                            <div class="campaign-template-info">
                                <h3>@template.Name</h3>
                                <p>@FormatTemplateSummary(template)</p>
                            </div>
                            <div class="campaign-template-actions">
                                <input type="datetime-local" @bind="template.FirstScheduledAtLocal" class="gm-form-control" />
                                <button type="button" class="btn-gm btn-gm-success" disabled="@IsTemplateBusy(template)" @onclick="() => CreateBatchFromTemplate(template)">
                                    @(processingTemplateId == template.Id ? "⏳ Создаём..." : "📅 Создать batch")
                                </button>
                            </div>
                        </div>
                    }
                </div>
            }
        </div>
    }

    @if (sessions == null)
    {
        <div class="glass-card" style="padding: 2rem;">
            <div class="skeleton skeleton-text" style="width: 80%; margin-bottom: 1rem;"></div>
            <div class="skeleton skeleton-text" style="width: 60%; margin-bottom: 0.75rem;"></div>
            <div class="skeleton skeleton-text" style="width: 70%; margin-bottom: 0.75rem;"></div>
            <div class="skeleton skeleton-text" style="width: 50%;"></div>
        </div>
    }
    else if (sessions.Count == 0)
    {
        <div class="glass-card">
            <div class="empty-state">
                <div class="empty-state-icon">🎯</div>
                <div class="empty-state-title">Нет предстоящих сессий</div>
                <p class="empty-state-text">Для этой группы пока не запланировано игровых сессий.</p>
            </div>
        </div>
    }
    else
    {
        <div class="batch-bulk-grid animate-slide-up">
            @foreach (var batch in batchModels)
            {
                <div class="batch-bulk-card">
                    <div class="batch-bulk-header">
                        <div>
                            <h3>@batch.Title</h3>
                            <p>@FormatBatchSummary(batch)</p>
                        </div>
                        <span class="status-badge status-info">Batch</span>
                    </div>

                    <EditForm Model="@batch" OnValidSubmit="@(() => UpdateBatchDetails(batch))">
                        <div class="batch-bulk-fields">
                            <div class="gm-form-group">
                                <label class="gm-form-label">Общее название</label>
                                <InputText @bind-Value="batch.Title" class="gm-form-control" />
                            </div>
                            <div class="gm-form-group">
                                <label class="gm-form-label">Общая ссылка</label>
                                <InputText @bind-Value="batch.JoinLink" class="gm-form-control" />
                            </div>
                            <div class="gm-form-group">
                                <label class="gm-form-label">Уведомления игрокам</label>
                                <select @bind="batch.NotificationMode" class="gm-form-control">
                                    <option value="@SessionNotificationModeExtensions.GroupAndDirectValue">В группе и в личку</option>
                                    <option value="@SessionNotificationModeExtensions.GroupOnlyValue">Только в группе</option>
                                </select>
                            </div>
                        </div>
                        <button type="submit" class="btn-gm btn-gm-primary" disabled="@IsBatchBusy(batch)">
                            @(IsBatchBusy(batch) ? "⏳ Обновляем..." : "💾 Обновить batch")
                        </button>
                    </EditForm>

                    <div class="batch-bulk-divider"></div>

                    <EditForm Model="@batch" OnValidSubmit="@(() => RescheduleBatch(batch))">
                        <div class="batch-bulk-fields">
                            <div class="gm-form-group">
                                <label class="gm-form-label">Первая дата пачки (МСК, UTC+3)</label>
                                <input type="datetime-local" @bind="batch.FirstScheduledAtLocal" class="gm-form-control" />
                            </div>
                            <div class="gm-form-group">
                                <label class="gm-form-label">Шаг между играми, дней</label>
                                <InputNumber @bind-Value="batch.IntervalDays" class="gm-form-control" min="1" />
                            </div>
                        </div>
                        <button type="submit" class="btn-gm btn-gm-outline" disabled="@IsBatchBusy(batch)">
                            @(IsBatchBusy(batch) ? "⏳ Переносим..." : "📅 Перенести пачку")
                        </button>
                    </EditForm>

                    <div class="batch-publish-row">
                        <span class="status-badge @(batch.AllSessionsPublic ? "status-success" : batch.PublicSessionCount > 0 ? "status-warning" : "status-neutral")">
                            @FormatBatchPublication(batch)
                        </span>
                        <select class="gm-form-control" disabled="@IsBatchPublishBusy(batch)" @onchange="args => SetBatchPublicationMode(batch, ParseMode(args.Value))">
                            <option value="None" selected="@(batch.PublicationMode == PublicationMode.None)">Скрыта</option>
                            <option value="Catalog" selected="@(batch.PublicationMode == PublicationMode.Catalog)">Каталог</option>
                            <option value="ClubOnly" selected="@(batch.PublicationMode == PublicationMode.ClubOnly)">Только участники</option>
                            <option value="Both" selected="@(batch.PublicationMode == PublicationMode.Both)">Каталог + клуб</option>
                        </select>
                    </div>

                    <div class="batch-clone-row">
                        <select @bind="batch.CloneInterval" class="gm-form-control">
                            <option value="week">Следующая неделя</option>
                            <option value="month">Следующий месяц</option>
                        </select>
                        <button type="button" class="btn-gm btn-gm-success" disabled="@IsBatchBusy(batch)" @onclick="() => CloneBatch(batch)">
                            @(IsBatchBusy(batch) ? "⏳ Клонируем..." : "🧬 Клонировать")
                        </button>
                    </div>
                </div>
            }
        </div>

        @* Desktop table *@
        <div class="glass-card session-table-desktop session-table-desktop-card animate-slide-up">
            <table class="gm-table">
                <thead>
                    <tr>
                        <th>Название</th>
                        <th>Время (МСК)</th>
                        <th>Места</th>
                        <th>Статус</th>
                        <th>Ссылка</th>
                        <th>Действие</th>
                    </tr>
                </thead>
                <tbody>
                    @foreach (var session in sessions)
                    {
                        var isExpanded = expandedSessions.Contains(session.Id);
                        <tr>
                            <td style="color: var(--text-primary); font-weight: 500;">
                                <button type="button" class="btn-gm btn-gm-link" @onclick="() => ToggleParticipants(session.Id)">
                                    @(isExpanded ? "▼" : "▶") @session.Title
                                </button>
                            </td>
                            <td>@session.ScheduledAt.FormatMoscow()</td>
                            <td>@FormatSeats(session)</td>
                            <td>
                                <span class="status-badge @GetStatusClass(session.Status)">@TranslateStatus(session.Status)</span>
                            </td>
                            <td>
                                <a href="@session.JoinLink" target="_blank" rel="noopener noreferrer" class="session-join-link">
                                    Подключиться ↗
                                </a>
                            </td>
                            <td>
                                <div class="session-table-actions">
                                    <span class="status-badge @GetPublicationStatusClass(session)">@FormatPublicationStatus(session)</span>
                                    <select class="gm-form-control" disabled="@(publishingSessionId == session.Id)" @onchange="args => SetSessionPublicationMode(session.Id, ParseMode(args.Value))">
                                        <option value="None" selected="@(session.PublicationMode == PublicationModeExtensions.NoneValue)">Скрыта</option>
                                        <option value="Catalog" selected="@(session.PublicationMode == PublicationModeExtensions.CatalogValue)">Каталог</option>
                                        <option value="ClubOnly" selected="@(session.PublicationMode == PublicationModeExtensions.ClubOnlyValue)">Только участники</option>
                                        <option value="Both" selected="@(session.PublicationMode == PublicationModeExtensions.BothValue)">Каталог + клуб</option>
                                    </select>
                                    @if (session.IsPublic && publicSettings?.PublicScheduleEnabled == true)
                                    {
                                        <a href="@PublicSessionUrl(session.Id)" target="_blank" rel="noopener noreferrer" class="btn-gm btn-gm-outline">Публичная ссылка</a>
                                    }
                                    <a href="/session/edit/@session.Id" class="btn-gm btn-gm-outline">
                                        ✏️ Изменить
                                    </a>
                                    <a href="/session/@session.Id/history" class="btn-gm btn-gm-outline">📜 История</a>
                                    @if (CanPromote(session))
                                    {
                                        <button type="button" class="btn-gm btn-gm-success" disabled="@(promotingSessionId == session.Id)" @onclick="() => PromoteWaitlisted(session.Id)">
                                            @(promotingSessionId == session.Id ? "⏳ Поднимаем..." : "⬆️ Из ожидания")
                                        </button>
                                    }
                                </div>
                            </td>
                        </tr>
                        @if (isExpanded)
                        {
                            <tr>
                                <td colspan="6" style="padding: 0; border: none;">
                                    <div class="participant-panel">
                                        @if (loadingParticipantsSessionId == session.Id)
                                        {
                                            <div class="skeleton skeleton-text" style="width: 60%;"></div>
                                        }
                                        else if (participantsCache.TryGetValue(session.Id, out var participants))
                                        {
                                            @if (participants.Count == 0)
                                            {
                                                <div class="empty-state empty-state-compact">
                                                    <div class="empty-state-title">Нет участников</div>
                                                </div>
                                            }
                                            else
                                            {
                                                <div class="participant-list">
                                                    @foreach (var p in participants)
                                                    {
                                                        <div class="participant-row">
                                                            <div class="participant-info">
                                                                <span class="participant-name">@p.DisplayName</span>
                                                                <span class="participant-username">@FormatParticipantUsername(p)</span>
                                                                <span class="status-badge @GetParticipantStatusClass(p)">@TranslateParticipantStatus(p)</span>
                                                            </div>
                                                            @if (!p.IsGm)
                                                            {
                                                                <button type="button" class="btn-gm btn-gm-danger" style="font-size: 0.75rem; padding: 0.25rem 0.5rem;" disabled="@(kickingParticipantId == p.Id)" @onclick="() => KickParticipant(session.Id, p.Id)">
                                                                    @(kickingParticipantId == p.Id ? "⏳..." : "🚪 Исключить")
                                                                </button>
                                                            }
                                                        </div>
                                                    }
                                                </div>
                                            }
                                        }
                                    </div>
                                </td>
                            </tr>
                        }
                    }
                </tbody>
            </table>
        </div>

        @* Mobile cards *@
        <div class="session-card-mobile stagger-children">
            @foreach (var session in sessions)
            {
                var isExpanded = expandedSessions.Contains(session.Id);
                <div class="session-card">
                    <div class="session-card-header">
                        <button type="button" class="btn-gm btn-gm-link" style="text-align: left; padding: 0;" @onclick="() => ToggleParticipants(session.Id)">
                            @(isExpanded ? "▼" : "▶") @session.Title
                        </button>
                        <span class="status-badge @GetStatusClass(session.Status)">@TranslateStatus(session.Status)</span>
                    </div>
                    <div class="session-card-body">
                        <div class="session-card-row">
                            <span>🕐 Время</span>
                            <span style="color: var(--text-primary);">@session.ScheduledAt.FormatMoscow()</span>
                        </div>
                        <div class="session-card-row">
                            <span>👥 Места</span>
                            <span style="color: var(--text-primary);">@FormatSeats(session)</span>
                        </div>
                        <div class="session-card-row">
                            <span>🔗 Ссылка</span>
                            <a href="@session.JoinLink" target="_blank" rel="noopener noreferrer">Подключиться ↗</a>
                        </div>
                    </div>
                    <div class="session-card-actions">
                        <select class="gm-form-control" style="flex: 1; font-size: 0.8125rem; padding: 0.5rem;" disabled="@(publishingSessionId == session.Id)" @onchange="args => SetSessionPublicationMode(session.Id, ParseMode(args.Value))">
                            <option value="None" selected="@(session.PublicationMode == PublicationModeExtensions.NoneValue)">Скрыта</option>
                            <option value="Catalog" selected="@(session.PublicationMode == PublicationModeExtensions.CatalogValue)">Каталог</option>
                            <option value="ClubOnly" selected="@(session.PublicationMode == PublicationModeExtensions.ClubOnlyValue)">Только участники</option>
                            <option value="Both" selected="@(session.PublicationMode == PublicationModeExtensions.BothValue)">Каталог + клуб</option>
                        </select>
                        @if (session.IsPublic && publicSettings?.PublicScheduleEnabled == true)
                        {
                            <a href="@PublicSessionUrl(session.Id)" target="_blank" rel="noopener noreferrer" class="btn-gm btn-gm-outline" style="flex: 1; justify-content: center; font-size: 0.8125rem; padding: 0.5rem;">Публичная ссылка</a>
                        }
                        <a href="/session/edit/@session.Id" class="btn-gm btn-gm-outline" style="flex: 1; justify-content: center; font-size: 0.8125rem; padding: 0.5rem;">
                            ✏️ Изменить
                        </a>
                        <a href="/session/@session.Id/history" class="btn-gm btn-gm-outline" style="flex: 1; justify-content: center; font-size: 0.8125rem; padding: 0.5rem;">📜 История</a>
                        @if (CanPromote(session))
                        {
                            <button type="button" class="btn-gm btn-gm-success" style="flex: 1; justify-content: center; font-size: 0.8125rem; padding: 0.5rem;" disabled="@(promotingSessionId == session.Id)" @onclick="() => PromoteWaitlisted(session.Id)">
                                @(promotingSessionId == session.Id ? "⏳ Поднимаем..." : "⬆️ Из ожидания")
                            </button>
                        }
                    </div>
                    @if (isExpanded)
                    {
                        <div class="participant-panel" style="margin-top: 0.75rem;">
                            @if (loadingParticipantsSessionId == session.Id)
                            {
                                <div class="skeleton skeleton-text" style="width: 60%;"></div>
                            }
                            else if (participantsCache.TryGetValue(session.Id, out var participants))
                            {
                                @if (participants.Count == 0)
                                {
                                    <div class="empty-state empty-state-compact">
                                        <div class="empty-state-title">Нет участников</div>
                                    </div>
                                }
                                else
                                {
                                    <div class="participant-list">
                                        @foreach (var p in participants)
                                        {
                                            <div class="participant-row">
                                                <div class="participant-info">
                                                    <span class="participant-name">@p.DisplayName</span>
                                                    <span class="participant-username">@FormatParticipantUsername(p)</span>
                                                    <span class="status-badge @GetParticipantStatusClass(p)">@TranslateParticipantStatus(p)</span>
                                                </div>
                                                @if (!p.IsGm)
                                                {
                                                    <button type="button" class="btn-gm btn-gm-danger" style="font-size: 0.75rem; padding: 0.25rem 0.5rem;" disabled="@(kickingParticipantId == p.Id)" @onclick="() => KickParticipant(session.Id, p.Id)">
                                                        @(kickingParticipantId == p.Id ? "⏳..." : "🚪 Исключить")
                                                    </button>
                                                }
                                            </div>
                                        }
                                    </div>
                                }
                            }
                        </div>
                    }
                </div>
            }
        </div>
    }
</div>

@code {
    [Parameter] public Guid GroupId { get; set; }
    private List<WebSession>? sessions;
    private List<WebCampaignTemplate>? campaignTemplates;
    private WebGroupManagement? groupManagement;
    private WebPublicGroupSettings? publicSettings;
    private IReadOnlyList<PortfolioGameSummary>? portfolioGames;
    private List<BatchBulkEditModel> batchModels = [];
    private List<CampaignTemplateUsageModel> campaignTemplateModels = [];
    private int pendingApplicationsCount;
    private Guid? promotingSessionId;
    private Guid? processingBatchId;
    private Guid? processingTemplateId;
    private Guid? publishingBatchId;
    private Guid? publishingSessionId;
    private string? removingCoGmId;
    private bool isAddingCoGm;
    private bool isCreatingDraft;
    private bool savingPublicSettings;
    private string? currentPlatform;
    private string? externalUserId;
    private string? errorMessage;
    private string? successMessage;
    private CoGmEditModel coGmModel = new();
    private PublicSettingsEditModel publicSettingsModel = new();
    private Dictionary<Guid, List<WebParticipant>> participantsCache = new();
    private HashSet<Guid> expandedSessions = new();
    private Guid? kickingParticipantId;
    private Guid? loadingParticipantsSessionId;

    protected override async Task OnInitializedAsync()
    {
        var authState = await AuthStateProvider.GetAuthenticationStateAsync();
        if (!authState.User.TryGetPlatformIdentity(out var platform, out externalUserId))
        {
            Navigation.NavigateTo("/access-denied");
            return;
        }

        currentPlatform = platform;
        await LoadSessions();
    }

    private async Task LoadSessions()
    {
        groupManagement = await SessionService.GetGroupManagementForCurrentUserAsync(GroupId);
        if (groupManagement is null)
        {
            Navigation.NavigateTo("/access-denied");
            return;
        }

        sessions = await SessionService.GetUpcomingSessionsForCurrentUserAsync(GroupId);
        if (sessions is null)
        {
            Navigation.NavigateTo("/access-denied");
            return;
        }

        publicSettings = await SessionService.GetPublicGroupSettingsForCurrentUserAsync(GroupId);
        if (publicSettings is null)
        {
            Navigation.NavigateTo("/access-denied");
            return;
        }

        campaignTemplates = await SessionService.GetCampaignTemplatesForCurrentUserAsync(GroupId);
        if (campaignTemplates is null)
        {
            Navigation.NavigateTo("/access-denied");
            return;
        }

        portfolioGames = await PortfolioService.GetPortfolioGamesForCurrentUserAsync(GroupId);

        pendingApplicationsCount = await MembershipService.GetPendingApplicationsCountForCurrentGmAsync(GroupId);

        RebuildBatchModels();
        RebuildCampaignTemplateModels();
        RebuildPublicSettingsModel();
    }

    private async Task CreateDraft()
    {
        errorMessage = null;
        successMessage = null;
        isCreatingDraft = true;

        try
        {
            var portfolioId = await PortfolioService.CreateDraftForCurrentUserAsync(GroupId, null);
            Navigation.NavigateTo($"/portfolio/manage/{portfolioId}");
        }
        catch (SessionAccessDeniedException)
        {
            Navigation.NavigateTo("/access-denied");
        }
        catch (Exception ex)
        {
            errorMessage = "Не удалось создать черновик: " + ex.Message;
        }
        finally
        {
            isCreatingDraft = false;
        }
    }

    private async Task SavePublicSettings()
    {
        errorMessage = null;
        successMessage = null;
        savingPublicSettings = true;

        try
        {
            await SessionService.UpdatePublicGroupSettingsForCurrentUserAsync(
                GroupId,
                publicSettingsModel.PublicSlug,
                publicSettingsModel.PublicScheduleEnabled);
            successMessage = "Настройки публичной страницы обновлены.";
            await LoadSessions();
        }
        catch (SessionAccessDeniedException)
        {
            Navigation.NavigateTo("/access-denied");
        }
        catch (Exception ex)
        {
            errorMessage = "Не удалось обновить публичную страницу: " + ex.Message;
        }
        finally
        {
            savingPublicSettings = false;
        }
    }

    private async Task SetBatchPublicationMode(BatchBulkEditModel batch, PublicationMode mode)
    {
        errorMessage = null;
        successMessage = null;
        publishingBatchId = batch.BatchId;

        try
        {
            await SessionService.SetBatchPublicationModeForCurrentUserAsync(batch.BatchId, mode);
            successMessage = mode switch
            {
                PublicationMode.Catalog => "Batch опубликован в общем каталоге.",
                PublicationMode.ClubOnly => "Batch доступен только участникам клуба.",
                PublicationMode.Both => "Batch опубликован в каталоге и доступен участникам клуба.",
                _ => "Batch скрыт из публичного расписания."
            };
            await LoadSessions();
        }
        catch (SessionAccessDeniedException)
        {
            Navigation.NavigateTo("/access-denied");
        }
        catch (Exception ex)
        {
            errorMessage = "Не удалось обновить публичность batch: " + ex.Message;
        }
        finally
        {
            publishingBatchId = null;
        }
    }

    private async Task SetSessionPublicationMode(Guid sessionId, PublicationMode mode)
    {
        errorMessage = null;
        successMessage = null;
        publishingSessionId = sessionId;

        try
        {
            await SessionService.SetSessionPublicationModeForCurrentUserAsync(sessionId, mode);
            successMessage = mode switch
            {
                PublicationMode.Catalog => "Сессия опубликована в общем каталоге.",
                PublicationMode.ClubOnly => "Сессия доступна только участникам клуба.",
                PublicationMode.Both => "Сессия опубликована в каталоге и доступна участникам клуба.",
                _ => "Сессия скрыта из публичного расписания."
            };
            await LoadSessions();
        }
        catch (SessionAccessDeniedException)
        {
            Navigation.NavigateTo("/access-denied");
        }
        catch (Exception ex)
        {
            errorMessage = "Не удалось обновить публичность сессии: " + ex.Message;
        }
        finally
        {
            publishingSessionId = null;
        }
    }

    private async Task AddCoGm()
    {
        errorMessage = null;
        successMessage = null;

        var coGmExternalUserId = coGmModel.ExternalUserId.Trim();
        if (coGmExternalUserId.Length == 0)
        {
            errorMessage = $"{CoGmIdLabel} должен быть заполнен.";
            return;
        }

        if (!IsValidPlatformUserId(CoGmPlatform, coGmExternalUserId))
        {
            errorMessage = $"{CoGmIdLabel} должен быть положительным числом.";
            return;
        }

        isAddingCoGm = true;

        try
        {
            await SessionService.AddCoGmForOwnerAsync(
                GroupId,
                CoGmPlatform,
                coGmExternalUserId,
                coGmModel.DisplayName,
                coGmModel.ExternalUsername);

            coGmModel = new();
            successMessage = "Co-GM добавлен.";
            await LoadSessions();
        }
        catch (SessionAccessDeniedException)
        {
            Navigation.NavigateTo("/access-denied");
        }
        catch (Exception ex)
        {
            errorMessage = "Не удалось добавить co-GM: " + ex.Message;
        }
        finally
        {
            isAddingCoGm = false;
        }
    }

    private async Task RemoveCoGm(WebGroupManager manager)
    {
        errorMessage = null;
        successMessage = null;
        removingCoGmId = ManagerKey(manager);
        var platform = ManagerPlatform(manager);
        var coGmExternalUserId = ManagerExternalUserId(manager);

        try
        {
            await SessionService.RemoveCoGmForOwnerAsync(GroupId, platform, coGmExternalUserId);
            successMessage = "Co-GM удалён.";
            await LoadSessions();
        }
        catch (SessionAccessDeniedException)
        {
            Navigation.NavigateTo("/access-denied");
        }
        catch (Exception ex)
        {
            errorMessage = "Не удалось удалить co-GM: " + ex.Message;
        }
        finally
        {
            removingCoGmId = null;
        }
    }

    private async Task PromoteWaitlisted(Guid sessionId)
    {
        errorMessage = null;
        successMessage = null;
        promotingSessionId = sessionId;

        try
        {
            await SessionService.PromoteWaitlistedPlayerForCurrentUserAsync(sessionId);
            await LoadSessions();
        }
        catch (SessionAccessDeniedException)
        {
            Navigation.NavigateTo("/access-denied");
        }
        catch (Exception ex)
        {
            errorMessage = ex.Message;
        }
        finally
        {
            promotingSessionId = null;
        }
    }

    private async Task ToggleParticipants(Guid sessionId)
    {
        if (expandedSessions.Contains(sessionId))
        {
            expandedSessions.Remove(sessionId);
            return;
        }

        expandedSessions.Add(sessionId);

        if (!participantsCache.ContainsKey(sessionId))
        {
            loadingParticipantsSessionId = sessionId;
            try
            {
                var participants = await SessionService.GetSessionParticipantsForCurrentUserAsync(sessionId);
                participantsCache[sessionId] = participants ?? [];
            }
            catch (Exception ex)
            {
                errorMessage = "Не удалось загрузить участников: " + ex.Message;
                expandedSessions.Remove(sessionId);
            }
            finally
            {
                loadingParticipantsSessionId = null;
            }
        }
    }

    private async Task KickParticipant(Guid sessionId, Guid participantId)
    {
        errorMessage = null;
        successMessage = null;
        kickingParticipantId = participantId;

        try
        {
            await SessionService.RemovePlayerFromSessionForCurrentUserAsync(sessionId, participantId);
            participantsCache.Remove(sessionId);
            successMessage = "Игрок исключён.";
            await LoadSessions();
            if (expandedSessions.Contains(sessionId))
            {
                await ToggleParticipants(sessionId);
            }
        }
        catch (SessionAccessDeniedException)
        {
            Navigation.NavigateTo("/access-denied");
        }
        catch (Exception ex)
        {
            errorMessage = ex.Message;
        }
        finally
        {
            kickingParticipantId = null;
        }
    }

    private static string FormatParticipantUsername(WebParticipant p)
    {
        var username = string.IsNullOrWhiteSpace(p.TelegramUsername)
            ? p.TelegramId.ToString(System.Globalization.CultureInfo.InvariantCulture)
            : "@" + p.TelegramUsername;
        return $"{username} · {FormatParticipantRsvp(p.RsvpStatus)}";
    }

    private static string FormatParticipantRsvp(string rsvp) => rsvp switch
    {
        RsvpStatus.Pending => "⏳ не ответил",
        RsvpStatus.Confirmed => "✅ подтвердил",
        RsvpStatus.Declined => "❌ отказался",
        _ => rsvp
    };

    private static string GetParticipantStatusClass(WebParticipant p)
    {
        if (p.IsGm) return "status-success";
        return p.RegistrationStatus switch
        {
            "Active" => "status-info",
            "Waitlisted" => "status-warning",
            _ => "status-neutral"
        };
    }

    private static string TranslateParticipantStatus(WebParticipant p)
    {
        if (p.IsGm) return "ГМ";
        return p.RegistrationStatus switch
        {
            "Active" => "Основной состав",
            "Waitlisted" => "Ожидание",
            _ => p.RegistrationStatus
        };
    }

    private async Task UpdateBatchDetails(BatchBulkEditModel batch)
    {
        errorMessage = null;
        successMessage = null;

        if (!ValidateBatchDetails(batch))
        {
            errorMessage = "Название и ссылка для batch не должны быть пустыми.";
            return;
        }

        processingBatchId = batch.BatchId;

        try
        {
            await SessionService.UpdateBatchDetailsForCurrentUserAsync(batch.BatchId, batch.Title, batch.JoinLink);
            await SessionService.UpdateBatchNotificationModeForCurrentUserAsync(
                batch.BatchId,
                SessionNotificationModeExtensions.FromDatabaseValue(batch.NotificationMode));
            successMessage = "Настройки batch обновлены.";
            await LoadSessions();
        }
        catch (SessionAccessDeniedException)
        {
            Navigation.NavigateTo("/access-denied");
        }
        catch (Exception ex)
        {
            errorMessage = "Не удалось обновить пачку: " + ex.Message;
        }
        finally
        {
            processingBatchId = null;
        }
    }

    private async Task RescheduleBatch(BatchBulkEditModel batch)
    {
        errorMessage = null;
        successMessage = null;

        if (batch.IntervalDays <= 0)
        {
            errorMessage = "Шаг между играми должен быть больше 0 дней.";
            return;
        }

        processingBatchId = batch.BatchId;

        try
        {
            var utcTime = new DateTimeOffset(batch.FirstScheduledAtLocal, TimeSpan.FromHours(3)).ToUniversalTime().UtcDateTime;
            await SessionService.RescheduleBatchForCurrentUserAsync(batch.BatchId, utcTime, batch.IntervalDays);
            successMessage = "Расписание пачки обновлено.";
            await LoadSessions();
        }
        catch (SessionAccessDeniedException)
        {
            Navigation.NavigateTo("/access-denied");
        }
        catch (Exception ex)
        {
            errorMessage = "Не удалось перенести пачку: " + ex.Message;
        }
        finally
        {
            processingBatchId = null;
        }
    }

    private async Task CloneBatch(BatchBulkEditModel batch)
    {
        errorMessage = null;
        successMessage = null;
        processingBatchId = batch.BatchId;

        try
        {
            var interval = batch.CloneInterval == "month"
                ? BatchCloneInterval.NextMonth
                : BatchCloneInterval.NextWeek;

            var clonedBatch = await SessionService.CloneBatchForCurrentUserAsync(batch.BatchId, interval);
            successMessage = $"Пачка склонирована: {clonedBatch.SessionCount} игр.";
            await LoadSessions();
        }
        catch (SessionAccessDeniedException)
        {
            Navigation.NavigateTo("/access-denied");
        }
        catch (Exception ex)
        {
            errorMessage = "Не удалось клонировать пачку: " + ex.Message;
        }
        finally
        {
            processingBatchId = null;
        }
    }

    private async Task CreateBatchFromTemplate(CampaignTemplateUsageModel template)
    {
        errorMessage = null;
        successMessage = null;
        processingTemplateId = template.Id;

        try
        {
            var utcTime = new DateTimeOffset(template.FirstScheduledAtLocal, TimeSpan.FromHours(3)).ToUniversalTime().UtcDateTime;
            if (utcTime <= DateTime.UtcNow)
            {
                errorMessage = "Первая дата batch должна быть в будущем.";
                return;
            }

            var createdBatch = await SessionService.CreateBatchFromCampaignTemplateForCurrentUserAsync(template.Id, utcTime);
            successMessage = $"Batch создан из шаблона: {createdBatch.SessionCount} игр.";
            await LoadSessions();
        }
        catch (SessionAccessDeniedException)
        {
            Navigation.NavigateTo("/access-denied");
        }
        catch (Exception ex)
        {
            errorMessage = "Не удалось создать batch из шаблона: " + ex.Message;
        }
        finally
        {
            processingTemplateId = null;
        }
    }

    private void RebuildBatchModels()
    {
        batchModels = sessions?
            .GroupBy(session => session.BatchId)
            .Select(group =>
            {
                var orderedSessions = group.OrderBy(session => session.ScheduledAt).ToList();
                var firstSession = orderedSessions[0];
                var lastSession = orderedSessions[^1];

                return new BatchBulkEditModel
                {
                    BatchId = group.Key,
                    Title = firstSession.Title,
                    JoinLink = firstSession.JoinLink,
                    NotificationMode = firstSession.NotificationMode,
                    FirstScheduledAtLocal = firstSession.ScheduledAt.ToMoscow(),
                    LastScheduledAtLocal = lastSession.ScheduledAt.ToMoscow(),
                    IntervalDays = InferIntervalDays(orderedSessions),
                    SessionCount = orderedSessions.Count,
                    PublicSessionCount = orderedSessions.Count(session => session.IsPublic),
                    AllSessionsPublic = orderedSessions.All(session => session.IsPublic),
                    PublicationMode = orderedSessions
                        .Select(s => PublicationModeExtensions.FromDatabaseValue(s.PublicationMode))
                        .GroupBy(m => m)
                        .OrderByDescending(g => g.Count())
                        .First()
                        .Key
                };
            })
            .OrderBy(batch => batch.FirstScheduledAtLocal)
            .ToList() ?? [];
    }

    private void RebuildCampaignTemplateModels()
    {
        var defaultStart = DateTime.UtcNow.AddDays(7).ToMoscow();
        campaignTemplateModels = campaignTemplates?
            .OrderByDescending(template => template.UpdatedAt)
            .ThenBy(template => template.Name)
            .Select(template => new CampaignTemplateUsageModel
            {
                Id = template.Id,
                Name = template.Name,
                Title = template.Title,
                JoinLink = template.JoinLink,
                SessionCount = template.SessionCount,
                IntervalDays = template.IntervalDays,
                MaxPlayers = template.MaxPlayers,
                NotificationMode = template.NotificationMode,
                FirstScheduledAtLocal = defaultStart
            })
            .ToList() ?? [];
    }

    private void RebuildPublicSettingsModel()
    {
        if (publicSettings is null)
        {
            return;
        }

        publicSettingsModel = new PublicSettingsEditModel
        {
            PublicScheduleEnabled = publicSettings.PublicScheduleEnabled,
            PublicSlug = publicSettings.PublicSlug ?? ""
        };
    }

    private static bool ValidateBatchDetails(BatchBulkEditModel batch)
    {
        batch.Title = batch.Title.Trim();
        batch.JoinLink = batch.JoinLink.Trim();
        return batch.Title.Length > 0 && batch.JoinLink.Length > 0;
    }

    private bool IsBatchBusy(BatchBulkEditModel batch) => processingBatchId == batch.BatchId;

    private bool IsBatchPublishBusy(BatchBulkEditModel batch) => publishingBatchId == batch.BatchId;

    private bool IsTemplateBusy(CampaignTemplateUsageModel template) => processingTemplateId == template.Id;

    private string? PublicClubUrl =>
        string.IsNullOrWhiteSpace(publicSettings?.PublicSlug)
            ? null
            : Navigation.ToAbsoluteUri($"/club/{publicSettings.PublicSlug}").ToString();

    private string PublicSessionUrl(Guid sessionId) =>
        Navigation.ToAbsoluteUri($"/s/{sessionId}").ToString();

    private static string FormatPublicationStatus(WebSession session) =>
        session.IsPublic ? "Опубликована" : "Скрыта";

    private static string GetPublicationStatusClass(WebSession session) =>
        session.IsPublic ? "status-success" : "status-neutral";

    private static string FormatBatchPublication(BatchBulkEditModel batch) =>
        batch.PublicSessionCount == 0
            ? "Все игры скрыты"
            : batch.PublicSessionCount == batch.SessionCount
                ? "Все игры опубликованы"
                : $"{batch.PublicSessionCount}/{batch.SessionCount} опубликовано";

    private string CoGmPlatform =>
        string.IsNullOrWhiteSpace(groupManagement?.Group.Platform)
            ? "Telegram"
            : groupManagement.Group.Platform;

    private string CoGmIdLabel => $"{CoGmPlatform} ID co-GM";

    private string CurrentUserRole =>
        groupManagement?.Managers.FirstOrDefault(manager =>
            string.Equals(ManagerPlatform(manager), currentPlatform, StringComparison.OrdinalIgnoreCase) &&
            ManagerExternalUserId(manager) == externalUserId)?.Role
        ?? GroupManagerRoleExtensions.CoGmValue;

    private static string FormatRole(string role) =>
        GroupManagerRoleExtensions.FromDatabaseValue(role).ToDisplayName();

    private string FormatManager(WebGroupManager manager)
    {
        var username = string.IsNullOrWhiteSpace(manager.ExternalUsername)
            ? manager.TelegramUsername
            : manager.ExternalUsername;
        var identity = string.IsNullOrWhiteSpace(username)
            ? $"{ManagerPlatform(manager)} {ManagerExternalUserId(manager)}"
            : "@" + username.TrimStart('@');

        return $"{FormatRole(manager.Role)} · {manager.DisplayName} · {identity}";
    }

    private string ManagerPlatform(WebGroupManager manager) =>
        string.IsNullOrWhiteSpace(manager.Platform) ? CoGmPlatform : manager.Platform;

    private static string ManagerExternalUserId(WebGroupManager manager) =>
        string.IsNullOrWhiteSpace(manager.ExternalUserId)
            ? manager.TelegramId.ToString(System.Globalization.CultureInfo.InvariantCulture)
            : manager.ExternalUserId;

    private string ManagerKey(WebGroupManager manager) =>
        $"{ManagerPlatform(manager)}:{ManagerExternalUserId(manager)}";

    private static bool IsValidPlatformUserId(string platform, string externalUserId) =>
        string.Equals(platform, "Telegram", StringComparison.OrdinalIgnoreCase)
            ? long.TryParse(externalUserId, out var telegramId) && telegramId > 0
            : !string.Equals(platform, "Discord", StringComparison.OrdinalIgnoreCase) ||
              (ulong.TryParse(externalUserId, out var platformId) && platformId > 0);

    private static int InferIntervalDays(IReadOnlyList<WebSession> orderedSessions)
    {
        if (orderedSessions.Count < 2)
        {
            return 7;
        }

        var days = (orderedSessions[1].ScheduledAt - orderedSessions[0].ScheduledAt).TotalDays;
        return Math.Max(1, (int)Math.Round(days, MidpointRounding.AwayFromZero));
    }

    private static bool CanPromote(WebSession session) =>
        session.WaitlistedPlayerCount > 0 &&
        (!session.MaxPlayers.HasValue || session.ActivePlayerCount < session.MaxPlayers.Value);

    private static string FormatSeats(WebSession session)
    {
        var seats = session.MaxPlayers.HasValue
            ? $"{session.ActivePlayerCount}/{session.MaxPlayers.Value}"
            : session.ActivePlayerCount.ToString(System.Globalization.CultureInfo.InvariantCulture);

        return session.WaitlistedPlayerCount > 0
            ? $"{seats} · ожидание {session.WaitlistedPlayerCount}"
            : seats;
    }

    private static PublicationMode ParseMode(object? value) =>
        Enum.TryParse<PublicationMode>(value?.ToString(), out var mode) ? mode : PublicationMode.None;

    private static string FormatBatchSummary(BatchBulkEditModel batch) =>
        $"{batch.SessionCount} игр · {FormatLocalMoscow(batch.FirstScheduledAtLocal)} — {FormatLocalMoscow(batch.LastScheduledAtLocal)}";

    private static string FormatTemplateSummary(CampaignTemplateUsageModel template)
    {
        var seats = template.MaxPlayers.HasValue
            ? $"{template.MaxPlayers.Value.ToString(System.Globalization.CultureInfo.InvariantCulture)} мест"
            : "без лимита";

        return $"{template.Title} · {template.SessionCount} игр · каждые {template.IntervalDays} дн. · {seats} · {FormatNotificationMode(template.NotificationMode)}";
    }

    private static string FormatNotificationMode(string notificationMode) =>
        SessionNotificationModeExtensions.FromDatabaseValue(notificationMode) switch
        {
            SessionNotificationMode.GroupOnly => "только группа",
            _ => "группа и личка"
        };

    private static string FormatLocalMoscow(DateTime localMoscow) =>
        localMoscow.ToString("d MMMM yyyy, HH:mm", System.Globalization.CultureInfo.GetCultureInfo("ru-RU"));

    private string GetStatusClass(string status) => status switch
    {
        SessionStatus.Confirmed => "status-success",
        SessionStatus.Cancelled => "status-danger",
        SessionStatus.ConfirmationSent => "status-warning",
        SessionStatus.Planned => "status-info",
        _ => "status-neutral"
    };

    private string TranslateStatus(string status) => status switch
    {
        SessionStatus.Planned => "Запланировано",
        SessionStatus.ConfirmationSent => "Ждём подтверждения",
        SessionStatus.Confirmed => "Подтверждено",
        SessionStatus.Cancelled => "Отменено",
        _ => status
    };

    private sealed class BatchBulkEditModel
    {
        public Guid BatchId { get; init; }
        public string Title { get; set; } = "";
        public string JoinLink { get; set; } = "";
        public string NotificationMode { get; set; } = SessionNotificationModeExtensions.GroupAndDirectValue;
        public DateTime FirstScheduledAtLocal { get; set; } = DateTime.Now;
        public DateTime LastScheduledAtLocal { get; init; } = DateTime.Now;
        public int IntervalDays { get; set; } = 7;
        public int SessionCount { get; init; }
        public int PublicSessionCount { get; init; }
        public bool AllSessionsPublic { get; init; }
        public PublicationMode PublicationMode { get; set; } = PublicationMode.None;
        public string CloneInterval { get; set; } = "week";
    }

    private sealed class CampaignTemplateUsageModel
    {
        public Guid Id { get; init; }
        public string Name { get; init; } = "";
        public string Title { get; init; } = "";
        public string JoinLink { get; init; } = "";
        public int SessionCount { get; init; }
        public int IntervalDays { get; init; }
        public int? MaxPlayers { get; init; }
        public string NotificationMode { get; init; } = SessionNotificationModeExtensions.GroupAndDirectValue;
        public DateTime FirstScheduledAtLocal { get; set; } = DateTime.Now;
    }

    private sealed class CoGmEditModel
    {
        public string ExternalUserId { get; set; } = "";
        public string DisplayName { get; set; } = "";
        public string? ExternalUsername { get; set; }
    }

    private sealed class PublicSettingsEditModel
    {
        public bool PublicScheduleEnabled { get; set; }
        public string? PublicSlug { get; set; }
    }
}
