const state = {
    session: null,
    activeSection: "overview",
    query: "",
    status: null,
    compatibility: null,
    users: [],
    chats: [],
    attachments: [],
    moderation: [],
    selectedChatId: null,
    selectedChatDetails: null,
    refreshTimer: null,
};

const sectionMeta = {
    overview: ["Обзор", "Операционная картина"],
    users: ["Пользователи", "Пользователи и боты"],
    chats: ["Чаты и каналы", "Управление чатами, группами и каналами"],
    media: ["Медиа", "Хранилище вложений"],
    moderation: ["Модерация", "Журнал действий"],
    updates: ["Обновления", "Android-релизы и совместимость"],
    operations: ["Операции", "Maintenance и контроль данных"],
};

const elements = {
    loginView: document.getElementById("loginView"),
    appView: document.getElementById("appView"),
    loginForm: document.getElementById("loginForm"),
    loginUsername: document.getElementById("loginUsername"),
    loginPassword: document.getElementById("loginPassword"),
    loginNotice: document.getElementById("loginNotice"),
    adminIdentity: document.getElementById("adminIdentity"),
    globalSearch: document.getElementById("globalSearch"),
    refreshButton: document.getElementById("refreshButton"),
    logoutButton: document.getElementById("logoutButton"),
    sectionKicker: document.getElementById("sectionKicker"),
    sectionTitle: document.getElementById("sectionTitle"),
    lastUpdated: document.getElementById("lastUpdated"),
    healthBadge: document.getElementById("healthBadge"),
    noticeBar: document.getElementById("noticeBar"),
    metricGrid: document.getElementById("metricGrid"),
    uptimeValue: document.getElementById("uptimeValue"),
    focusList: document.getElementById("focusList"),
    featureMatrix: document.getElementById("featureMatrix"),
    usersCount: document.getElementById("usersCount"),
    usersTable: document.getElementById("usersTable"),
    chatsCount: document.getElementById("chatsCount"),
    chatsTable: document.getElementById("chatsTable"),
    chatTypeFilter: document.getElementById("chatTypeFilter"),
    chatAttentionFilter: document.getElementById("chatAttentionFilter"),
    chatInspector: document.getElementById("chatInspector"),
    attachmentsCount: document.getElementById("attachmentsCount"),
    attachmentsTable: document.getElementById("attachmentsTable"),
    moderationCount: document.getElementById("moderationCount"),
    moderationTable: document.getElementById("moderationTable"),
    compatibilityCount: document.getElementById("compatibilityCount"),
    compatibilitySummary: document.getElementById("compatibilitySummary"),
    updateRiskList: document.getElementById("updateRiskList"),
    compatibilityRoutesTable: document.getElementById("compatibilityRoutesTable"),
    compatibilityVersionsTable: document.getElementById("compatibilityVersionsTable"),
    compatibilityHitsTable: document.getElementById("compatibilityHitsTable"),
    operationResult: document.getElementById("operationResult"),
    toast: document.getElementById("toast"),
};

document.addEventListener("DOMContentLoaded", () => {
    wireEvents();
    initialize().catch(handleError);
});

function wireEvents() {
    elements.loginForm.addEventListener("submit", async (event) => {
        event.preventDefault();
        await login();
    });

    elements.refreshButton.addEventListener("click", () => refreshAll().catch(handleError));
    elements.logoutButton.addEventListener("click", () => logout().catch(handleError));
    elements.chatTypeFilter.addEventListener("change", renderChats);
    elements.chatAttentionFilter.addEventListener("change", renderChats);

    elements.globalSearch.addEventListener("input", () => {
        window.clearTimeout(state.refreshTimer);
        state.refreshTimer = window.setTimeout(() => {
            state.query = elements.globalSearch.value.trim();
            refreshAll().catch(handleError);
        }, 360);
    });

    document.addEventListener("click", handleDocumentClick);
}

async function initialize() {
    state.session = await fetchJson("/admin/api/session", { method: "GET" }, false);
    renderSession();

    if (!state.session.configured) {
        showLogin("Административные учетные данные на сервере не настроены.");
        return;
    }

    if (!state.session.authenticated) {
        showLogin();
        return;
    }

    showApp();
    await refreshAll();
}

async function login() {
    elements.loginNotice.classList.add("hidden");
    await fetchJson("/admin/api/session/login", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
            username: elements.loginUsername.value.trim(),
            password: elements.loginPassword.value,
        }),
    }, false);

    state.session = await fetchJson("/admin/api/session", { method: "GET" }, false);
    elements.loginPassword.value = "";
    renderSession();
    showApp();
    await refreshAll();
}

async function logout() {
    await fetchJson("/admin/api/session/logout", { method: "POST" });
    state.session = await fetchJson("/admin/api/session", { method: "GET" }, false);
    state.selectedChatId = null;
    state.selectedChatDetails = null;
    renderSession();
    showLogin();
}

async function refreshAll() {
    state.query = elements.globalSearch.value.trim();
    const query = state.query ? `?q=${encodeURIComponent(state.query)}` : "";

    const [status, compatibility, users, chats, attachments, moderation] = await Promise.all([
        fetchJson("/admin/api/status"),
        fetchJson("/admin/api/app-updates/compatibility-report"),
        fetchJson(`/admin/api/users${query}`),
        fetchJson(`/admin/api/chats${query}`),
        fetchJson(`/admin/api/attachments${query}`),
        fetchJson("/admin/api/moderation-log?limit=150"),
    ]);

    state.status = status;
    state.compatibility = compatibility;
    state.users = users || [];
    state.chats = chats || [];
    state.attachments = attachments || [];
    state.moderation = moderation || [];

    if (state.selectedChatId) {
        await loadChatDetails(state.selectedChatId, false);
    }

    elements.lastUpdated.textContent = `обновлено ${new Date().toLocaleTimeString("ru-RU")}`;
    renderAll();
}

async function loadChatDetails(chatId, rerender = true) {
    state.selectedChatId = chatId;
    state.selectedChatDetails = await fetchJson(`/admin/api/chats/${chatId}/details`);
    if (rerender) {
        renderChats();
        renderChatInspector();
    }
}

function showLogin(message) {
    elements.appView.classList.add("hidden");
    elements.loginView.classList.remove("hidden");
    if (message) {
        elements.loginNotice.textContent = message;
        elements.loginNotice.classList.remove("hidden");
    } else {
        elements.loginNotice.classList.add("hidden");
    }
}

function showApp() {
    elements.loginView.classList.add("hidden");
    elements.appView.classList.remove("hidden");
    setSection(state.activeSection);
}

function setSection(section) {
    state.activeSection = section;
    document.querySelectorAll(".nav-item").forEach((item) => {
        item.classList.toggle("active", item.dataset.section === section);
    });
    document.querySelectorAll(".section").forEach((panel) => {
        panel.classList.toggle("active", panel.dataset.panel === section);
    });

    const [kicker, title] = sectionMeta[section] || sectionMeta.overview;
    elements.sectionKicker.textContent = kicker;
    elements.sectionTitle.textContent = title;
}

function renderAll() {
    renderStatus();
    renderUsers();
    renderChats();
    renderAttachments();
    renderModeration();
    renderCompatibility();
    renderChatInspector();
}

function renderSession() {
    elements.adminIdentity.textContent = state.session?.authenticated ? state.session.username : "Гость";
}

function renderStatus() {
    const status = state.status;
    if (!status) {
        return;
    }

    const healthy = Boolean(status.databaseReachable);
    elements.healthBadge.textContent = healthy ? "База доступна" : "База недоступна";
    elements.healthBadge.className = `status ${healthy ? "ok" : "danger"}`;
    elements.uptimeValue.textContent = formatDuration(status.startedAtUtc, status.currentTimeUtc);

    elements.metricGrid.innerHTML = [
        metric("Пользователи", status.totalUsers, `${status.blockedUsers} заблокировано, ${status.botUsers} ботов`),
        metric("Чаты", status.totalChats, `${status.groupChats} групп, ${status.channelChats} каналов`),
        metric("Сообщения", status.totalMessages, `${status.pinnedMessages} закреплено, ${status.expiringMessages} с TTL`),
        metric("Медиа", status.totalAttachments, formatBytes(status.attachmentBytes)),
        metric("Protected", status.protectedChats, `${status.protectedMessages} защищенных сообщений`),
        metric("Stories", status.totalStories, `${status.activeStories} активных, ${status.protectedStories} protected`),
        metric("Join requests", status.pendingJoinRequests, `${status.activeInviteLinks} активных invite links`),
        metric("Сессии", status.activeSessions, `${status.registeredPushDevices} push-устройств`),
    ].join("");

    elements.focusList.innerHTML = [
        focus(healthy ? "ok" : "danger", "База данных", healthy ? "Соединение активно." : "Нужно проверить подключение к БД."),
        focus(status.pendingJoinRequests > 0 ? "warn" : "ok", "Заявки на вход", `${status.pendingJoinRequests} ожидают решения.`),
        focus(status.expiringMessages > 0 ? "warn" : "ok", "TTL-сообщения", `${status.expiringMessages} активных сообщений с автоудалением.`),
        focus(status.blockedUsers > 0 ? "warn" : "ok", "Ограниченные аккаунты", `${status.blockedUsers} пользователей заблокировано.`),
        focus(status.moderationEvents > 0 ? "ok" : "warn", "Модерация", `${status.moderationEvents} событий в журнале.`),
    ].join("");

    elements.featureMatrix.innerHTML = [
        feature("Пользователи и privacy", `${status.totalUsers} аккаунтов`, status.totalUsers > 0),
        feature("Группы и каналы", `${status.groupChats + status.channelChats} публичных контуров`, status.groupChats + status.channelChats > 0),
        feature("Protected content", `${status.protectedChats} чатов`, status.protectedChats > 0),
        feature("Топики и роли", `${status.topics} топиков`, status.topics > 0),
        feature("Истории", `${status.activeStories} активных`, status.totalStories > 0),
        feature("Боты", `${status.enabledBots}/${status.totalBots} включено`, status.totalBots > 0),
    ].join("");
}

function renderUsers() {
    elements.usersCount.textContent = `${state.users.length} строк`;
    elements.usersTable.innerHTML = state.users.length === 0
        ? emptyRow("Пользователи по текущему фильтру не найдены.", 9)
        : state.users.map((user) => `
            <tr>
                <td>
                    <div class="cell-primary">${escapeHtml(displayName(user.displayName, user.username))}</div>
                    <div class="cell-secondary">@${escapeHtml(user.username)}${user.phoneNumber ? ` · ${escapeHtml(user.phoneNumber)}` : ""}</div>
                    ${user.about ? `<div class="cell-secondary">${escapeHtml(user.about)}</div>` : ""}
                </td>
                <td>${badge(user.isBot ? "Бот" : "Человек", user.isBot ? "warn" : "ok")}</td>
                <td>${formatDate(user.createdAt)}</td>
                <td>${user.lastSeenAt ? formatDate(user.lastSeenAt) : "—"}</td>
                <td>${renderUserClientVersion(user)}</td>
                <td>${user.activeSessions} / ${user.pushDevices}</td>
                <td>${user.chatMemberships} / ${user.messagesSent}</td>
                <td>
                    ${badge(user.passcodeLockEnabled ? "Passcode" : "Без lock", user.passcodeLockEnabled ? "ok" : "warn")}
                    <div class="cell-secondary">${user.showMessagePreview ? "Preview включены" : "Preview скрыты"}</div>
                </td>
                <td>
                    <div class="action-row">
                        <button class="button small" type="button" data-action="${user.isBlocked ? "unblock-user" : "block-user"}" data-user-id="${escapeAttribute(user.id)}">
                            ${user.isBlocked ? "Разблокировать" : "Заблокировать"}
                        </button>
                        <button class="button small danger" type="button" data-action="delete-user" data-user-id="${escapeAttribute(user.id)}" data-user-name="${escapeAttribute(user.username)}">
                            Удалить
                        </button>
                    </div>
                </td>
            </tr>
        `).join("");
}

function renderUserClientVersion(user) {
    const versions = Array.isArray(user.clientVersions) ? user.clientVersions : [];
    if (!versions.length && user.clientVersionCode == null && !user.clientVersionName && !user.clientPlatform) {
        return "—";
    }

    const platform = user.clientPlatform ? escapeHtml(user.clientPlatform) : "unknown";
    const details = versions.slice(0, 3).map((version) => {
        const sessions = Number(version.sessions || 0);
        const pushDevices = Number(version.pushDevices || 0);
        const countParts = [];
        if (sessions > 0) {
            countParts.push(`${sessions} сесс.`);
        }
        if (pushDevices > 0) {
            countParts.push(`${pushDevices} push`);
        }
        const countLabel = countParts.length ? ` · ${countParts.join(" / ")}` : "";
        const seenLabel = version.lastSeenAt ? ` · ${formatDate(version.lastSeenAt)}` : "";
        return `<div class="cell-secondary">${escapeHtml(version.clientPlatform || "unknown")} · ${formatVersion(version.clientVersionCode, version.clientVersionName)}${countLabel}${seenLabel}</div>`;
    }).join("");

    return `
        <div class="cell-primary">${formatVersion(user.clientVersionCode, user.clientVersionName)}</div>
        <div class="cell-secondary">${platform}</div>
        ${details}
    `;
}

function renderChats() {
    const typeFilter = elements.chatTypeFilter.value;
    const attentionFilter = elements.chatAttentionFilter.value;
    const rows = state.chats.filter((chat) => {
        if (typeFilter !== "all" && chat.type !== typeFilter) {
            return false;
        }
        if (attentionFilter === "pending") {
            return chat.pendingJoinRequests > 0;
        }
        if (attentionFilter === "protected") {
            return chat.hasProtectedContent || chat.protectedMessages > 0;
        }
        if (attentionFilter === "topics") {
            return chat.topicsEnabled || chat.topics > 0;
        }
        return true;
    });

    elements.chatsCount.textContent = `${rows.length} строк`;
    elements.chatsTable.innerHTML = rows.length === 0
        ? emptyRow("Чаты по текущему фильтру не найдены.", 8)
        : rows.map((chat) => `
            <tr data-chat-id="${escapeAttribute(chat.id)}" class="${chat.id === state.selectedChatId ? "selected" : ""}">
                <td>
                    <div class="cell-primary">${escapeHtml(chat.title)}</div>
                    <div class="cell-secondary">${chat.publicUsername ? `@${escapeHtml(chat.publicUsername)}` : "без public username"}</div>
                </td>
                <td>${formatChatType(chat.type)}</td>
                <td>${escapeHtml(chat.createdBy || "—")}</td>
                <td>${chat.lastActivityAt ? formatDate(chat.lastActivityAt) : formatDate(chat.createdAt)}</td>
                <td>${chat.members}</td>
                <td>
                    <div>${chat.messages}</div>
                    <div class="cell-secondary">${chat.pinnedMessages} pinned · ${chat.protectedMessages} protected</div>
                </td>
                <td>
                    <div class="action-row">
                        ${chat.hasProtectedContent ? badge("Protected", "ok") : badge("Обычный", "warn")}
                        ${chat.pendingJoinRequests > 0 ? badge(`${chat.pendingJoinRequests} заявок`, "warn") : ""}
                        ${chat.topicsEnabled ? badge(`${chat.topics} топиков`, "ok") : ""}
                    </div>
                </td>
                <td>
                    <div class="action-row">
                        <button class="button small" type="button" data-action="open-chat" data-chat-id="${escapeAttribute(chat.id)}">Инспектор</button>
                        <button class="button small danger" type="button" data-action="delete-chat" data-chat-id="${escapeAttribute(chat.id)}" data-chat-title="${escapeAttribute(chat.title)}">Удалить</button>
                    </div>
                </td>
            </tr>
        `).join("");
}

function renderChatInspector() {
    const details = state.selectedChatDetails;
    if (!details) {
        elements.chatInspector.innerHTML = `
            <div class="empty-state">
                <p class="eyebrow">Инспектор</p>
                <h2>Выберите чат</h2>
                <p class="muted">Здесь появятся настройки protected mode, TTL, public username, участники, права, invite links и заявки на вход.</p>
            </div>
        `;
        return;
    }

    const chat = details.chat;
    elements.chatInspector.innerHTML = `
        <div class="inspector-block">
            <p class="eyebrow">${formatChatType(chat.type)}</p>
            <h2>${escapeHtml(chat.title)}</h2>
            <p class="muted">${chat.members} участников · ${chat.messages} сообщений · ${chat.attachments} вложений</p>
        </div>

        <div class="inspector-block">
            <h3>Настройки чата</h3>
            <div class="inspector-grid">
                <label class="field">
                    <span>Public username</span>
                    <input id="chatPublicUsername" value="${escapeAttribute(chat.publicUsername || "")}" placeholder="channel_name">
                </label>
                <label class="field">
                    <span>TTL по умолчанию, сек.</span>
                    <input id="chatTtlSeconds" type="number" min="0" value="${chat.defaultMessageTtlSeconds ?? ""}" placeholder="0 = выключено">
                </label>
                <label class="check-line">
                    <input id="chatTopicsEnabled" type="checkbox" ${chat.topicsEnabled ? "checked" : ""}>
                    <span>Топики включены</span>
                </label>
                <label class="check-line">
                    <input id="chatProtected" type="checkbox" ${chat.hasProtectedContent ? "checked" : ""}>
                    <span>Protected content</span>
                </label>
            </div>
            <div class="action-row" style="margin-top: 12px">
                <button class="button secondary" type="button" data-action="save-chat-settings" data-chat-id="${escapeAttribute(chat.id)}">Сохранить настройки</button>
            </div>
        </div>

        <div class="inspector-block">
            <h3>Участники и права</h3>
            <div class="permissions-grid">
                ${details.members.length === 0 ? `<div class="muted">Участников нет.</div>` : details.members.map(renderMemberPermissions).join("")}
            </div>
        </div>

        <div class="inspector-block">
            <h3>Invite links</h3>
            <div class="inspector-grid">
                <label class="field">
                    <span>Название ссылки</span>
                    <input id="newInviteTitle" placeholder="Например: Публичная ссылка">
                </label>
                <label class="check-line">
                    <input id="newInviteRequiresApproval" type="checkbox">
                    <span>Требовать approval</span>
                </label>
            </div>
            <div class="action-row" style="margin-top: 12px">
                <button class="button secondary" type="button" data-action="create-invite" data-chat-id="${escapeAttribute(chat.id)}">Создать ссылку</button>
            </div>
            <div class="mini-list" style="margin-top: 12px">
                ${details.inviteLinks.length === 0 ? `<div class="muted">Ссылок пока нет.</div>` : details.inviteLinks.map((link) => `
                    <div class="mini-row">
                        <div class="cell-primary">${escapeHtml(link.title || "Без названия")}</div>
                        <div class="cell-secondary mono">${escapeHtml(link.link)}</div>
                        <div class="action-row" style="margin-top: 8px">
                            ${badge(link.createsJoinRequest ? "approval" : "direct join", link.createsJoinRequest ? "warn" : "ok")}
                            ${badge(link.isRevoked ? "отозвана" : "активна", link.isRevoked ? "danger" : "ok")}
                            ${link.isRevoked ? "" : `<button class="button small danger" type="button" data-action="revoke-invite" data-chat-id="${escapeAttribute(chat.id)}" data-invite-id="${escapeAttribute(link.id)}">Отозвать</button>`}
                        </div>
                    </div>
                `).join("")}
            </div>
        </div>

        <div class="inspector-block">
            <h3>Заявки на вход</h3>
            <div class="mini-list">
                ${details.joinRequests.length === 0 ? `<div class="muted">Заявок нет.</div>` : details.joinRequests.map((request) => `
                    <div class="mini-row">
                        <div class="cell-primary">${escapeHtml(displayName(request.displayName, request.username))}</div>
                        <div class="cell-secondary">${formatDate(request.requestedAt)} · ${formatJoinStatus(request.status)}</div>
                        ${isPendingJoinRequest(request.status) ? `
                            <div class="action-row" style="margin-top: 8px">
                                <button class="button small secondary" type="button" data-action="resolve-join" data-approve="true" data-chat-id="${escapeAttribute(chat.id)}" data-request-id="${escapeAttribute(request.id)}">Одобрить</button>
                                <button class="button small danger" type="button" data-action="resolve-join" data-approve="false" data-chat-id="${escapeAttribute(chat.id)}" data-request-id="${escapeAttribute(request.id)}">Отклонить</button>
                            </div>
                        ` : ""}
                    </div>
                `).join("")}
            </div>
        </div>

        <div class="inspector-block">
            <h3>Топики</h3>
            <div class="mini-list">
                ${details.topics.length === 0 ? `<div class="muted">Топиков нет.</div>` : details.topics.map((topic) => `
                    <div class="mini-row">
                        <div class="cell-primary">${escapeHtml(topic.icon || "")} ${escapeHtml(topic.title)}</div>
                        <div class="cell-secondary">${topic.messageCount} сообщений · ${topic.isPinned ? "закреплен" : "обычный"} · ${topic.isClosed ? "закрыт" : "открыт"}</div>
                    </div>
                `).join("")}
            </div>
        </div>

        <div class="inspector-block">
            <h3>Последние сообщения</h3>
            <div class="mini-list">
                ${details.recentMessages.length === 0 ? `<div class="muted">Сообщений нет.</div>` : details.recentMessages.map((message) => `
                    <div class="mini-row">
                        <div class="cell-primary">${escapeHtml(message.sender)} · ${formatDate(message.sentAt)}</div>
                        <div class="cell-secondary">${message.deletedAt ? "Сообщение удалено" : escapeHtml(shorten(message.text || "медиа-сообщение", 160))}</div>
                        <div class="cell-secondary">${message.attachments} вложений · ${message.reactions} реакций ${message.hasProtectedContent ? "· protected" : ""}</div>
                    </div>
                `).join("")}
            </div>
        </div>

        <div class="inspector-block">
            <h3>Модерация чата</h3>
            <div class="mini-list">
                ${details.moderationLog.length === 0 ? `<div class="muted">Событий нет.</div>` : details.moderationLog.map((event) => `
                    <div class="mini-row">
                        <div class="cell-primary">${escapeHtml(event.action)}</div>
                        <div class="cell-secondary">${formatDate(event.createdAt)} · ${escapeHtml(event.actorDisplayName || "—")}</div>
                    </div>
                `).join("")}
            </div>
        </div>
    `;
}

function renderMemberPermissions(member) {
    const role = normalizeRole(member.role);
    return `
        <div class="permission-row" data-member-id="${escapeAttribute(member.userId)}">
            <div class="cell-primary">${escapeHtml(displayName(member.displayName, member.username))}</div>
            <div class="cell-secondary">@${escapeHtml(member.username)}${member.isBot ? " · бот" : ""}</div>
            <div class="permission-controls">
                <select data-field="role">
                    <option value="0" ${role === "Member" ? "selected" : ""}>Member</option>
                    <option value="1" ${role === "Admin" ? "selected" : ""}>Admin</option>
                    <option value="2" ${role === "Owner" ? "selected" : ""}>Owner</option>
                </select>
                ${permissionCheckbox("pin", "Pin", member.canPinMessages)}
                ${permissionCheckbox("delete", "Delete", member.canDeleteMessages)}
                ${permissionCheckbox("invite", "Invite", member.canInviteUsers)}
                ${permissionCheckbox("post", "Post", member.canPostMessages)}
                <button class="button small" type="button" data-action="save-member-permissions" data-chat-id="${escapeAttribute(state.selectedChatId)}" data-member-id="${escapeAttribute(member.userId)}">Сохранить</button>
            </div>
        </div>
    `;
}

function renderAttachments() {
    elements.attachmentsCount.textContent = `${state.attachments.length} строк`;
    elements.attachmentsTable.innerHTML = state.attachments.length === 0
        ? emptyRow("Вложения по текущему фильтру не найдены.", 7)
        : state.attachments.map((attachment) => `
            <tr>
                <td>
                    <div class="cell-primary">${escapeHtml(attachment.fileName)}</div>
                    <div class="cell-secondary">${escapeHtml(attachment.contentType || "application/octet-stream")} · sort ${attachment.sortOrder}</div>
                    ${attachment.mediaAlbumId ? `<div class="cell-secondary mono">album ${escapeHtml(attachment.mediaAlbumId)}</div>` : ""}
                </td>
                <td>${escapeHtml(attachment.chatTitle)}</td>
                <td>${escapeHtml(attachment.uploadedBy)}</td>
                <td>${formatDate(attachment.uploadedAt)}</td>
                <td>${formatBytes(attachment.fileSizeBytes)}</td>
                <td>
                    <div class="action-row">
                        ${badge(attachment.exists ? "файл есть" : "нет файла", attachment.exists ? "ok" : "danger")}
                        ${attachment.isProtected ? badge("protected", "ok") : ""}
                    </div>
                </td>
                <td>
                    <div class="action-row">
                        ${attachment.exists ? `<a class="table-link" href="${escapeAttribute(attachment.downloadPath)}" target="_blank" rel="noopener">Скачать</a>` : ""}
                        <button class="button small danger" type="button" data-action="delete-attachment" data-attachment-id="${escapeAttribute(attachment.id)}" data-file-name="${escapeAttribute(attachment.fileName)}">Удалить</button>
                    </div>
                </td>
            </tr>
        `).join("");
}

function renderModeration() {
    elements.moderationCount.textContent = `${state.moderation.length} событий`;
    elements.moderationTable.innerHTML = state.moderation.length === 0
        ? emptyRow("Журнал модерации пуст.", 6)
        : state.moderation.map((event) => `
            <tr>
                <td>${formatDate(event.createdAt)}</td>
                <td>${escapeHtml(event.actorDisplayName || "—")}</td>
                <td>
                    <div class="cell-primary">${formatModerationTarget(event.targetType)}</div>
                    <div class="cell-secondary mono">${escapeHtml(event.targetId || "—")}</div>
                </td>
                <td>${escapeHtml(event.action)}</td>
                <td>${escapeHtml(event.reason || "—")}</td>
                <td class="mono">${escapeHtml(event.chatId || "—")}</td>
            </tr>
        `).join("");
}

function renderCompatibility() {
    const report = state.compatibility;
    if (!report) {
        return;
    }

    elements.compatibilityCount.textContent = `${report.recentHits.length} последних записей`;
    elements.compatibilitySummary.innerHTML = [
        metric("Хиты за 24 часа", report.totalHitsLast24Hours, `${report.hitsWithoutVersionLast24Hours} без версии`),
        metric("Хиты за 7 дней", report.totalHitsLast7Days, `${report.hitsWithoutVersionLast7Days} без версии`),
        metric("Последний запрос", report.latestHitAt ? formatDate(report.latestHitAt) : "нет данных", "мониторинг обновлений"),
    ].join("");

    elements.updateRiskList.innerHTML = [
        focus(report.hitsWithoutVersionLast24Hours > 0 ? "warn" : "ok", "Без versionCode за 24 часа", `${report.hitsWithoutVersionLast24Hours} обращений.`),
        focus(report.hitsWithoutVersionLast7Days > 0 ? "warn" : "ok", "Без versionCode за 7 дней", `${report.hitsWithoutVersionLast7Days} обращений.`),
        focus(report.totalHitsLast7Days > 0 ? "ok" : "warn", "Сигнал обновлений", `${report.totalHitsLast7Days} обращений за неделю.`),
    ].join("");

    elements.compatibilityRoutesTable.innerHTML = report.routesLast7Days.length === 0
        ? emptyRow("Данных по маршрутам пока нет.", 4)
        : report.routesLast7Days.map((route) => `
            <tr>
                <td>${escapeHtml(route.route)}</td>
                <td>${route.hitsLast24Hours}</td>
                <td>${route.hitsLast7Days}</td>
                <td>${route.latestHitAt ? formatDate(route.latestHitAt) : "—"}</td>
            </tr>
        `).join("");

    elements.compatibilityVersionsTable.innerHTML = report.versionsLast7Days.length === 0
        ? emptyRow("Данных по версиям пока нет.", 6)
        : report.versionsLast7Days.map((version) => `
            <tr>
                <td>${escapeHtml(version.clientPlatform || "unknown")}</td>
                <td>${version.clientVersionCode ?? "—"}</td>
                <td>${escapeHtml(version.clientVersionName || "—")}</td>
                <td>${version.hitsLast24Hours}</td>
                <td>${version.hitsLast7Days}</td>
                <td>${version.latestHitAt ? formatDate(version.latestHitAt) : "—"}</td>
            </tr>
        `).join("");

    elements.compatibilityHitsTable.innerHTML = report.recentHits.length === 0
        ? emptyRow("Последних обращений нет.", 6)
        : report.recentHits.map((hit) => `
            <tr>
                <td>${formatDate(hit.hitAt)}</td>
                <td>${escapeHtml(hit.route)}</td>
                <td>${escapeHtml(hit.requestPath)}</td>
                <td>${escapeHtml(hit.clientPlatform || "—")}</td>
                <td>${formatVersion(hit.clientVersionCode, hit.clientVersionName)}</td>
                <td>${escapeHtml(hit.remoteIp || "—")}</td>
            </tr>
        `).join("");
}

async function handleDocumentClick(event) {
    const nav = event.target.closest("[data-section]");
    if (nav) {
        setSection(nav.dataset.section);
        return;
    }

    const jump = event.target.closest("[data-section-jump]");
    if (jump) {
        setSection(jump.dataset.sectionJump);
        return;
    }

    const chatRow = event.target.closest("tr[data-chat-id]");
    if (chatRow && !event.target.closest("button,a,input,select")) {
        await loadChatDetails(chatRow.dataset.chatId);
        return;
    }

    const button = event.target.closest("[data-action]");
    if (!button) {
        return;
    }

    const action = button.dataset.action;
    try {
        if (action === "refresh") {
            await refreshAll();
            showToast("Данные обновлены.");
            return;
        }

        if (action === "open-chat") {
            await loadChatDetails(button.dataset.chatId);
            return;
        }

        if (action === "block-user" || action === "unblock-user") {
            await fetchJson(`/admin/api/users/${button.dataset.userId}/${action === "block-user" ? "block" : "unblock"}`, { method: "POST" });
            showToast(action === "block-user" ? "Пользователь заблокирован." : "Пользователь разблокирован.");
            await refreshAll();
            return;
        }

        if (action === "delete-user") {
            if (!window.confirm(`Удалить пользователя @${button.dataset.userName}? Будут затронуты его сообщения, сессии, push-устройства и личные чаты.`)) {
                return;
            }
            await fetchJson(`/admin/api/users/${button.dataset.userId}`, { method: "DELETE" });
            showToast("Пользователь удален.");
            await refreshAll();
            return;
        }

        if (action === "delete-chat") {
            if (!window.confirm(`Удалить чат «${button.dataset.chatTitle}» вместе с сообщениями и вложениями?`)) {
                return;
            }
            await fetchJson(`/admin/api/chats/${button.dataset.chatId}`, { method: "DELETE" });
            if (state.selectedChatId === button.dataset.chatId) {
                state.selectedChatId = null;
                state.selectedChatDetails = null;
            }
            showToast("Чат удален.");
            await refreshAll();
            return;
        }

        if (action === "delete-attachment") {
            if (!window.confirm(`Удалить вложение «${button.dataset.fileName}» из базы и файлового хранилища?`)) {
                return;
            }
            await fetchJson(`/admin/api/attachments/${button.dataset.attachmentId}`, { method: "DELETE" });
            showToast("Вложение удалено.");
            await refreshAll();
            return;
        }

        if (action === "save-chat-settings") {
            const ttlRaw = document.getElementById("chatTtlSeconds").value.trim();
            state.selectedChatDetails = await fetchJson(`/admin/api/chats/${button.dataset.chatId}/settings`, {
                method: "PUT",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({
                    publicUsername: document.getElementById("chatPublicUsername").value.trim() || null,
                    topicsEnabled: document.getElementById("chatTopicsEnabled").checked,
                    hasProtectedContent: document.getElementById("chatProtected").checked,
                    defaultMessageTtlSeconds: ttlRaw ? Number(ttlRaw) : null,
                }),
            });
            showToast("Настройки чата сохранены.");
            await refreshAll();
            return;
        }

        if (action === "save-member-permissions") {
            const row = button.closest(".permission-row");
            state.selectedChatDetails = await fetchJson(`/admin/api/chats/${button.dataset.chatId}/members/${button.dataset.memberId}/permissions`, {
                method: "PUT",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({
                    role: Number(row.querySelector('[data-field="role"]').value),
                    canPinMessages: row.querySelector('[data-field="pin"]').checked,
                    canDeleteMessages: row.querySelector('[data-field="delete"]').checked,
                    canInviteUsers: row.querySelector('[data-field="invite"]').checked,
                    canPostMessages: row.querySelector('[data-field="post"]').checked,
                }),
            });
            showToast("Права участника обновлены.");
            await refreshAll();
            return;
        }

        if (action === "create-invite") {
            state.selectedChatDetails = await fetchJson(`/admin/api/chats/${button.dataset.chatId}/invite-links`, {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({
                    title: document.getElementById("newInviteTitle").value.trim() || null,
                    createsJoinRequest: document.getElementById("newInviteRequiresApproval").checked,
                }),
            });
            showToast("Invite link создан.");
            await refreshAll();
            return;
        }

        if (action === "revoke-invite") {
            state.selectedChatDetails = await fetchJson(`/admin/api/chats/${button.dataset.chatId}/invite-links/${button.dataset.inviteId}`, {
                method: "DELETE",
            });
            showToast("Invite link отозван.");
            await refreshAll();
            return;
        }

        if (action === "resolve-join") {
            state.selectedChatDetails = await fetchJson(`/admin/api/chats/${button.dataset.chatId}/join-requests/${button.dataset.requestId}`, {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ approve: button.dataset.approve === "true" }),
            });
            showToast(button.dataset.approve === "true" ? "Заявка одобрена." : "Заявка отклонена.");
            await refreshAll();
            return;
        }

        if (action === "cleanup-expired") {
            if (!window.confirm("Очистить все истекшие TTL-сообщения на сервере?")) {
                return;
            }
            const result = await fetchJson("/admin/api/messages/cleanup-expired", { method: "POST" });
            elements.operationResult.textContent = `Удалено сообщений: ${result.deletedMessages}; вложений: ${result.deletedAttachments}.`;
            showToast("Очистка TTL-сообщений выполнена.");
            await refreshAll();
        }
    } catch (error) {
        handleError(error);
    }
}

async function fetchJson(url, options = {}, useAuth = true) {
    const response = await fetch(url, {
        credentials: "same-origin",
        headers: {
            Accept: "application/json",
            ...(options.headers || {}),
        },
        ...options,
    });

    if (response.status === 401 && useAuth) {
        state.session = { configured: true, authenticated: false, username: null };
        renderSession();
        showLogin("Сессия администратора истекла.");
        throw new Error("Сессия администратора истекла.");
    }

    if (!response.ok) {
        let message = `HTTP ${response.status}`;
        try {
            message = await response.text();
        } catch {
            // Тело ошибки не всегда доступно.
        }
        throw new Error(message || `HTTP ${response.status}`);
    }

    if (response.status === 204) {
        return null;
    }

    const contentType = response.headers.get("content-type") || "";
    return contentType.includes("application/json") ? response.json() : response.text();
}

function handleError(error) {
    const message = error instanceof Error ? error.message : String(error);
    if (!elements.appView.classList.contains("hidden")) {
        elements.noticeBar.textContent = message;
        elements.noticeBar.classList.remove("hidden");
        showToast(message);
        window.setTimeout(() => elements.noticeBar.classList.add("hidden"), 6000);
        return;
    }

    showLogin(message);
}

function showToast(message) {
    elements.toast.textContent = message;
    elements.toast.classList.remove("hidden");
    window.clearTimeout(showToast.timeoutId);
    showToast.timeoutId = window.setTimeout(() => elements.toast.classList.add("hidden"), 3600);
}

function metric(label, value, note) {
    return `
        <article class="metric">
            <div class="metric-label">${escapeHtml(label)}</div>
            <div class="metric-value">${escapeHtml(String(value ?? "—"))}</div>
            <div class="metric-note">${escapeHtml(note || "")}</div>
        </article>
    `;
}

function focus(level, title, text) {
    return `
        <div class="focus-item ${level}">
            <div class="cell-primary">${escapeHtml(title)}</div>
            <div class="cell-secondary">${escapeHtml(text)}</div>
        </div>
    `;
}

function feature(title, value, enabled) {
    return `
        <div class="feature-row">
            <span>${escapeHtml(title)}</span>
            <strong>${escapeHtml(value)}</strong>
            ${badge(enabled ? "активно" : "нет данных", enabled ? "ok" : "warn")}
        </div>
    `;
}

function badge(text, tone = "neutral") {
    return `<span class="rect-badge ${tone}">${escapeHtml(text)}</span>`;
}

function permissionCheckbox(field, label, checked) {
    return `
        <label class="check-line">
            <input type="checkbox" data-field="${field}" ${checked ? "checked" : ""}>
            <span>${escapeHtml(label)}</span>
        </label>
    `;
}

function emptyRow(text, colspan) {
    return `<tr><td colspan="${colspan}" class="cell-secondary">${escapeHtml(text)}</td></tr>`;
}

function displayName(displayNameValue, username) {
    return displayNameValue && displayNameValue.trim() ? displayNameValue : `@${username}`;
}

function formatDate(value) {
    if (!value) {
        return "—";
    }
    return new Date(value).toLocaleString("ru-RU");
}

function formatDuration(startValue, endValue) {
    if (!startValue || !endValue) {
        return "—";
    }

    const hours = Math.max(0, Math.round((new Date(endValue) - new Date(startValue)) / 3600000));
    if (hours < 24) {
        return `${hours} ч`;
    }

    const days = Math.floor(hours / 24);
    return `${days} д ${hours % 24} ч`;
}

function formatBytes(value) {
    const number = Number(value || 0);
    const units = ["B", "KB", "MB", "GB", "TB"];
    let size = number;
    let index = 0;
    while (size >= 1024 && index < units.length - 1) {
        size /= 1024;
        index += 1;
    }
    return `${size.toFixed(size >= 10 || index === 0 ? 0 : 1)} ${units[index]}`;
}

function formatVersion(versionCode, versionName) {
    if (versionCode == null && !versionName) {
        return "—";
    }
    if (versionCode == null) {
        return escapeHtml(versionName);
    }
    return escapeHtml(`${versionName || "без имени"} / ${versionCode}`);
}

function formatChatType(type) {
    if (type === "Direct" || type === 0) {
        return "Личный";
    }
    if (type === "Group" || type === 1) {
        return "Группа";
    }
    if (type === "Channel" || type === 2) {
        return "Канал";
    }
    return escapeHtml(String(type || "—"));
}

function normalizeRole(role) {
    if (role === "Owner" || role === 2) {
        return "Owner";
    }
    if (role === "Admin" || role === 1) {
        return "Admin";
    }
    return "Member";
}

function formatJoinStatus(status) {
    if (status === "Approved" || status === 1) {
        return "одобрена";
    }
    if (status === "Declined" || status === 2) {
        return "отклонена";
    }
    return "ожидает";
}

function isPendingJoinRequest(status) {
    return status === "Pending" || status === 0;
}

function formatModerationTarget(target) {
    const map = {
        0: "Чат",
        1: "Сообщение",
        2: "Пользователь",
        3: "Invite link",
        4: "Join request",
        5: "Story",
        Chat: "Чат",
        Message: "Сообщение",
        User: "Пользователь",
        InviteLink: "Invite link",
        JoinRequest: "Join request",
        Story: "Story",
    };
    return map[target] || String(target || "—");
}

function shorten(value, max) {
    const text = String(value || "");
    return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
}

function escapeHtml(value) {
    return String(value)
        .replaceAll("&", "&amp;")
        .replaceAll("<", "&lt;")
        .replaceAll(">", "&gt;")
        .replaceAll('"', "&quot;")
        .replaceAll("'", "&#039;");
}

function escapeAttribute(value) {
    return escapeHtml(value);
}
