admin.js

import { LoginDialog } from './components/login_dialog.js';
import { firebaseAuth, signOut, onAuthStateChanged } from './firebase.js';

const LOCAL_MODE = document.querySelector('meta[name="local-mode"]').content === 'true';

let authToken       = null;
let loginDialog     = null;
let _currentAdminId = null;  // ID of the signed-in admin, to prevent self-deactivation

// ── Tab switching ─────────────────────────────────────────────────────────────

const tabs   = document.querySelectorAll('.account-tab[data-tab]');
const panels = document.querySelectorAll('.account-panel');

const _panelInited = new Set();

tabs.forEach(tab => {
    tab.addEventListener('click', () => {
        const target = tab.dataset.tab;
        tabs.forEach(t   => t.classList.toggle('active', t.dataset.tab === target));
        panels.forEach(p => p.classList.toggle('active', p.id === 'tab-' + target));

        closeDrawer();
        closeProjectDrawer();
        closeFolderDrawer();
        closeEmbedDrawer();
        closeSubDrawer();

        if (target === 'users' && !_panelInited.has('users')) {
            _panelInited.add('users');
            initUsersPanel();
        }
        if (target === 'projects' && !_panelInited.has('projects')) {
            _panelInited.add('projects');
            initProjectsPanel();
        }
        if (target === 'folders' && !_panelInited.has('folders')) {
            _panelInited.add('folders');
            initFoldersPanel();
        }
        if (target === 'embeds' && !_panelInited.has('embeds')) {
            _panelInited.add('embeds');
            initEmbedsPanel();
        }
        if (target === 'subscriptions' && !_panelInited.has('subscriptions')) {
            _panelInited.add('subscriptions');
            initSubsPanel();
        }
        if (target === 'analytics' && !_panelInited.has('analytics')) {
            _panelInited.add('analytics');
            initAnalyticsPanel();
        }
        if (target === 'dispatch' && !_panelInited.has('dispatch')) {
            _panelInited.add('dispatch');
            initDispatchPanel();
        }
        if (target === 'schema' && !_panelInited.has('schema')) {
            _panelInited.add('schema');
            initSchemaPanel();
        }
    });
});

document.getElementById('adminBackBtn').addEventListener('click', () => {
    window.location.href = '/app';
});

// ── UI state helpers ──────────────────────────────────────────────────────────

/**
 * Reveals the admin page UI and populates the header with the current admin user's info.
 * @param {object} user - The admin user object from the API.
 */
function showAdminPage(user) {
    _currentAdminId = user.id;
    document.getElementById('adminLoading').style.display = 'none';
    document.getElementById('adminPage').style.display = '';

    const initials = (user.display_name || user.email || '?')
        .split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase();
    document.getElementById('adminHeaderAvatar').textContent = initials;
    document.getElementById('adminHeaderName').textContent   = user.display_name || user.email;

    _panelInited.add('analytics');
    initAnalyticsPanel();
}

/** Hides the loading spinner overlay. */
function hideLoading() {
    document.getElementById('adminLoading').style.display = 'none';
}

// ── Logout ────────────────────────────────────────────────────────────────────

document.getElementById('adminLogoutBtn').addEventListener('click', async () => {
    document.getElementById('adminPage').style.display = 'none';
    document.getElementById('adminLoading').style.display = '';
    authToken = null;
    if (firebaseAuth) await signOut(firebaseAuth);
});

// ── Shared helpers ────────────────────────────────────────────────────────────

/**
 * Escapes HTML special characters in a string.
 * @param {*} s - Value to escape (coerced to string).
 * @returns {string} HTML-safe string.
 */
function esc(s) {
    return String(s ?? '')
        .replace(/&/g, '&amp;').replace(/</g, '&lt;')
        .replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}

/**
 * Formats an ISO timestamp as a short local date string.
 * @param {string|null} iso - ISO 8601 date string, or null/empty.
 * @returns {string} Formatted date, or '—' if not provided.
 */
function fmtDate(iso) {
    if (!iso) return '—';
    return new Date(iso).toLocaleDateString(undefined, {
        year: 'numeric', month: 'short', day: 'numeric',
    });
}

/**
 * Formats an ISO timestamp as a full local date and time string.
 * @param {string|null} iso - ISO 8601 date string, or null/empty.
 * @returns {string} Formatted date-time, or '—' if not provided.
 */
function fmtDatetime(iso) {
    if (!iso) return '—';
    return new Date(iso).toLocaleString(undefined, {
        year: 'numeric', month: 'short', day: 'numeric',
        hour: '2-digit', minute: '2-digit',
    });
}

// ── Status toggle (shared between table and drawer) ───────────────────────────

/**
 * PATCH is_active for a user, update _users, and refresh the table.
 * @param {string}      userId   - ID of the target user.
 * @param {boolean}     activate - Desired active state.
 * @param {HTMLElement} btn      - The button that triggered the action (disabled while in-flight).
 */
async function doSetUserActive(userId, activate, btn) {
    btn.disabled = true;
    btn.textContent = '…';
    try {
        const headers = { 'Content-Type': 'application/json' };
        if (authToken) headers['X-Auth-Token'] = authToken;
        const resp = await fetch(`/api/admin/users/${encodeURIComponent(userId)}/active`, {
            method: 'PATCH', headers,
            body: JSON.stringify({ is_active: activate }),
        });
        if (!resp.ok) throw new Error(resp.status);

        // Patch local list data and re-render the table
        const listUser = _users.find(u => u.id === userId);
        if (listUser) listUser.is_active = activate;
        renderTable();

        // If this user's drawer is open, patch the cached full user and re-render it
        if (_drawerUser?.id === userId) {
            _drawerUser.is_active = activate;
            renderDrawerContent();
        }
    } catch {
        btn.disabled = false;
        btn.textContent = activate ? 'Activate' : 'Deactivate';
    }
}

// ── Users panel ───────────────────────────────────────────────────────────────

let _users   = [];
let _sortCol = 'created_at';
let _sortDir = 'desc';
let _groupBy = '';

const COLS = [
    { key: 'email',              label: 'Email',        minWidth: '180px' },
    { key: 'display_name',       label: 'Name',         minWidth: '130px' },
    { key: 'is_active',          label: 'Status',       minWidth: '80px'  },
    { key: 'auth_provider',      label: 'Provider',     minWidth: '85px'  },
    { key: 'subscription_name',  label: 'Subscription', minWidth: '110px' },
    { key: 'project_count',      label: 'Projects',     minWidth: '75px'  },
    { key: 'folder_count',       label: 'Folders',      minWidth: '70px'  },
    { key: 'embed_count',        label: 'Embeds',       minWidth: '70px'  },
    { key: 'last_login_at',      label: 'Last Login',   minWidth: '110px' },
    { key: 'created_at',         label: 'Created',      minWidth: '110px' },
    { key: 'modified_at',        label: 'Modified',     minWidth: '110px' },
    { key: 'id',                 label: 'User ID',      minWidth: '100px' },
    { key: 'external_id',        label: 'Firebase UID', minWidth: '110px' },
    { key: '_actions',           label: '',             minWidth: '170px', noSort: true },
];

const GROUP_OPTIONS = [
    { value: '',                 label: 'None'         },
    { value: 'is_active',        label: 'Status'       },
    { value: 'auth_provider',    label: 'Provider'     },
    { value: 'subscription_name',label: 'Subscription' },
];

/**
 * Renders a user table cell as an HTML string for the given column.
 * @param {object} col - Column descriptor from COLS.
 * @param {object} user - User data object.
 * @returns {string} Inner HTML for the cell.
 */
function fmtCell(col, user) {
    const v = user[col.key];

    if (col.key === 'is_active') {
        const statusBadge = v
            ? '<span class="admin-badge admin-badge--active">Active</span>'
            : '<span class="admin-badge admin-badge--inactive">Inactive</span>';
        const adminBadge = user.is_admin
            ? '<span class="admin-badge admin-badge--admin">Admin</span>'
            : '';
        return `<span class="admin-badge-group">${statusBadge}${adminBadge}</span>`;
    }

    if (col.key === 'last_login_at' || col.key === 'created_at' || col.key === 'modified_at') {
        return `<span title="${esc(v ?? '')}">${fmtDate(v)}</span>`;
    }

    if (col.key === 'subscription_name') {
        return v
            ? `<span>${esc(v)}</span>`
            : '<span class="admin-cell-muted">—</span>';
    }

    if (col.key === 'project_count' || col.key === 'folder_count' || col.key === 'embed_count') {
        return `<span class="admin-cell-mono">${v ?? 0}</span>`;
    }

    if (col.key === 'id' || col.key === 'external_id') {
        if (!v) return '<span class="admin-cell-muted">—</span>';
        const trunc = v.length > 14 ? v.slice(0, 14) + '…' : v;
        return `<span class="admin-cell-mono admin-cell-trunc" title="${esc(v)}">${esc(trunc)}</span>`;
    }

    if (col.key === '_actions') {
        const action = user.is_active ? 'deactivate' : 'activate';
        const label  = user.is_active ? 'Deactivate' : 'Activate';
        const cls    = user.is_active ? 'btn btn-danger admin-btn-sm' : 'btn btn-primary admin-btn-sm';
        const toggleBtn = `<button class="${cls}" data-action="${action}" data-user-id="${esc(user.id)}">${label}</button>`;
        const deleteBtn = `<button class="btn btn-danger admin-btn-sm" data-action="delete" data-user-id="${esc(user.id)}" data-user-label="${esc(user.display_name || user.email)}">Delete</button>`;
        return `<span style="display:inline-flex;gap:0.3rem">${toggleBtn}${deleteBtn}</span>`;
    }

    if (!v && v !== false) return '<span class="admin-cell-muted">—</span>';
    return `<span>${esc(v)}</span>`;
}

/**
 * Returns a human-readable label for the current group-by field value.
 * @param {*} value - The raw group field value.
 * @returns {string} Display label for the group.
 */
function getGroupLabel(value) {
    if (_groupBy === 'is_active')        return value ? 'Active' : 'Inactive';
    if (_groupBy === 'subscription_name') return value ? String(value) : 'No subscription';
    return value ? String(value) : '—';
}

/**
 * Null-safe comparator for table sorting, handles booleans and strings.
 * @param {*} a - First value.
 * @param {*} b - Second value.
 * @returns {number} Negative, zero, or positive sort order.
 */
function cmpValues(a, b) {
    if (a == null && b == null) return 0;
    if (a == null) return 1;
    if (b == null) return -1;
    if (typeof a === 'boolean') return a === b ? 0 : (a ? -1 : 1);
    return String(a).localeCompare(String(b), undefined, { sensitivity: 'base', numeric: true });
}

/**
 * Returns a sorted copy of the _users array based on the current sort column and direction.
 * @returns {object[]} Sorted array of user objects.
 */
function sortedUsers() {
    return [..._users].sort((a, b) => {
        const cmp = cmpValues(a[_sortCol], b[_sortCol]);
        return _sortDir === 'asc' ? cmp : -cmp;
    });
}

/** Rebuild and inject the table HTML, then wire up interaction handlers. */
function renderTable() {
    const wrap  = document.getElementById('adminUsersTableWrap');
    const count = document.getElementById('adminUsersCount');

    count.textContent = `${_users.length} user${_users.length !== 1 ? 's' : ''}`;

    const sorted = sortedUsers();

    const thead = COLS.map(c => {
        if (c.noSort) return `<th style="min-width:${c.minWidth}"></th>`;
        const active = c.key === _sortCol;
        const arrow  = active ? (_sortDir === 'asc' ? ' ↑' : ' ↓') : '';
        return `<th class="${active ? 'sorted' : ''}" data-col="${c.key}" style="min-width:${c.minWidth}">${c.label}${arrow}</th>`;
    }).join('');

    let tbody = '';
    if (_groupBy) {
        const groups = new Map();
        for (const user of sorted) {
            const label = getGroupLabel(user[_groupBy]);
            if (!groups.has(label)) groups.set(label, []);
            groups.get(label).push(user);
        }
        for (const [label, rows] of groups) {
            tbody += `<tr class="admin-group-row"><td colspan="${COLS.length}">${esc(label)}&ensp;·&ensp;${rows.length}</td></tr>`;
            for (const user of rows) {
                tbody += `<tr class="admin-table-row" data-user-id="${esc(user.id)}">${COLS.map(c => `<td>${fmtCell(c, user)}</td>`).join('')}</tr>`;
            }
        }
    } else {
        for (const user of sorted) {
            tbody += `<tr class="admin-table-row" data-user-id="${esc(user.id)}">${COLS.map(c => `<td>${fmtCell(c, user)}</td>`).join('')}</tr>`;
        }
    }

    if (!tbody) {
        tbody = `<tr><td colspan="${COLS.length}" class="admin-table-empty">No users found.</td></tr>`;
    }

    wrap.innerHTML = `
        <table class="admin-table">
            <thead><tr>${thead}</tr></thead>
            <tbody>${tbody}</tbody>
        </table>`;

    // Sort header clicks
    wrap.querySelectorAll('thead th[data-col]').forEach(th => {
        th.addEventListener('click', () => {
            const col = th.dataset.col;
            _sortDir = (_sortCol === col && _sortDir === 'asc') ? 'desc' : 'asc';
            _sortCol = col;
            renderTable();
        });
    });

    // Restore selected highlight if a drawer is open
    if (_drawerUser) _setUserRowSelected(_drawerUser.id);
}

/** Fetches the user list from the API and re-renders the table. */
async function fetchUsers() {
    const btn = document.getElementById('adminUsersRefreshBtn');
    btn.disabled = true;
    btn.textContent = 'Refreshing…';
    try {
        const headers = {};
        if (authToken) headers['X-Auth-Token'] = authToken;
        const resp = await fetch('/api/admin/users', { headers });
        if (!resp.ok) throw new Error(resp.status);
        _users = await resp.json();
        renderTable();
    } catch {
        document.getElementById('adminUsersCount').textContent = 'Failed to load users.';
    } finally {
        btn.disabled = false;
        btn.textContent = 'Refresh';
    }
}

/** Builds and injects the users panel toolbar and table into the DOM, then fetches data. */
function initUsersPanel() {
    const section = document.getElementById('tab-users');

    // Toolbar
    const toolbar = document.createElement('div');
    toolbar.className = 'admin-toolbar';

    const countEl = document.createElement('span');
    countEl.id = 'adminUsersCount';
    countEl.className = 'admin-row-count';
    countEl.textContent = 'Loading…';

    const groupLabelEl = document.createElement('label');
    groupLabelEl.className = 'admin-toolbar-label';
    groupLabelEl.textContent = 'Group by';

    const groupSelect = document.createElement('select');
    groupSelect.className = 'admin-group-select';
    GROUP_OPTIONS.forEach(opt => {
        const o = document.createElement('option');
        o.value = opt.value;
        o.textContent = opt.label;
        groupSelect.appendChild(o);
    });
    groupSelect.addEventListener('change', () => {
        _groupBy = groupSelect.value;
        renderTable();
    });

    const refreshBtn = document.createElement('button');
    refreshBtn.id = 'adminUsersRefreshBtn';
    refreshBtn.className = 'btn btn-ghost';
    refreshBtn.textContent = 'Refresh';
    refreshBtn.addEventListener('click', fetchUsers);

    toolbar.appendChild(countEl);
    toolbar.appendChild(groupLabelEl);
    toolbar.appendChild(groupSelect);
    toolbar.appendChild(refreshBtn);

    const tableWrap = document.createElement('div');
    tableWrap.id = 'adminUsersTableWrap';
    tableWrap.className = 'admin-table-wrap';

    // Delegated click handler for both row-open and action buttons
    tableWrap.addEventListener('click', async (e) => {
        const btn = e.target.closest('button[data-action]');
        if (btn && !btn.disabled) {
            if (btn.dataset.action === 'delete') {
                openDeleteConfirm(btn.dataset.userId, btn.dataset.userLabel);
            } else {
                await doSetUserActive(btn.dataset.userId, btn.dataset.action === 'activate', btn);
            }
            return;
        }
        // Row click → open detail drawer
        const row = e.target.closest('tr.admin-table-row[data-user-id]');
        if (row) openDrawer(row.dataset.userId);
    });

    section.appendChild(toolbar);
    section.appendChild(tableWrap);

    fetchUsers();
}

// ── Projects panel ────────────────────────────────────────────────────────────

let _projects   = [];
let _projSortCol = 'modified_at';
let _projSortDir = 'desc';
let _projGroupBy = '';

const PROJ_COLS = [
    { key: 'name',           label: 'Name',        minWidth: '180px' },
    { key: 'owner',          label: 'Owner',        minWidth: '130px' },
    { key: 'folder',         label: 'Folder',       minWidth: '110px' },
    { key: 'has_transcript', label: 'Transcript',   minWidth: '90px'  },
    { key: 'audio_format',   label: 'Format',       minWidth: '70px'  },
    { key: 'duration',       label: 'Duration',     minWidth: '80px'  },
    { key: 'embed_count',    label: 'Embeds',       minWidth: '70px'  },
    { key: 'any_with_link',  label: 'Public',       minWidth: '70px'  },
    { key: 'modified_at',    label: 'Modified',     minWidth: '110px' },
    { key: 'created_at',     label: 'Created',      minWidth: '110px' },
    { key: 'id',             label: 'Project ID',   minWidth: '100px' },
    { key: '_actions',       label: '',             minWidth: '175px', noSort: true },
];

const PROJ_GROUP_OPTIONS = [
    { value: '',              label: 'None'        },
    { value: 'owner',         label: 'Owner'       },
    { value: 'folder',        label: 'Folder'      },
    { value: 'has_transcript',label: 'Transcript'  },
    { value: 'audio_format',  label: 'Format'      },
    { value: 'any_with_link', label: 'Public'      },
];

/**
 * Formats a duration in seconds as m:ss or h:mm:ss.
 * @param {number|null} secs - Duration in seconds.
 * @returns {string} Formatted duration string, or '—' if not provided.
 */
function fmtDuration(secs) {
    if (secs == null || secs < 0) return '—';
    const s = Math.round(secs);
    const h = Math.floor(s / 3600);
    const m = Math.floor((s % 3600) / 60);
    const sec = s % 60;
    if (h > 0) return `${h}:${String(m).padStart(2,'0')}:${String(sec).padStart(2,'0')}`;
    return `${m}:${String(sec).padStart(2,'0')}`;
}

/**
 * Renders a projects table cell as an HTML string for the given column.
 * @param {object} col - Column descriptor from PROJ_COLS.
 * @param {object} proj - Project data object.
 * @returns {string} Inner HTML for the cell.
 */
function fmtProjCell(col, proj) {
    const v = proj[col.key];

    if (col.key === 'has_transcript') {
        return v
            ? '<span class="admin-badge admin-badge--active">Yes</span>'
            : '<span class="admin-cell-muted">—</span>';
    }

    if (col.key === 'any_with_link') {
        const projBadge   = v ? '<span class="admin-badge admin-badge--admin">Public</span>' : '';
        const folderBadge = proj.folder_is_public
            ? '<span class="admin-badge admin-badge--active">Public Folder</span>' : '';
        const badges = projBadge || folderBadge
            ? `<span class="admin-badge-group">${projBadge}${folderBadge}</span>`
            : '<span class="admin-cell-muted">—</span>';
        return badges;
    }

    if (col.key === 'duration') {
        return `<span class="admin-cell-mono">${fmtDuration(v)}</span>`;
    }

    if (col.key === 'embed_count') {
        return `<span class="admin-cell-mono">${v ?? 0}</span>`;
    }

    if (col.key === 'audio_format') {
        return v ? `<span class="admin-cell-mono">${esc(v.toUpperCase())}</span>`
                 : '<span class="admin-cell-muted">—</span>';
    }

    if (col.key === 'modified_at' || col.key === 'created_at') {
        return `<span title="${esc(v ?? '')}">${fmtDate(v)}</span>`;
    }

    if (col.key === 'folder') {
        return v ? `<span class="admin-cell-muted">${esc(v)}</span>`
                 : '<span class="admin-cell-muted">root</span>';
    }

    if (col.key === 'id') {
        if (!v) return '<span class="admin-cell-muted">—</span>';
        const trunc = v.length > 14 ? v.slice(0, 14) + '…' : v;
        return `<span class="admin-cell-mono admin-cell-trunc" title="${esc(v)}">${esc(trunc)}</span>`;
    }

    if (col.key === '_actions') {
        const canEdit = proj.owner_id === null || proj.owner_id === _currentAdminId;
        const disabled = canEdit ? '' : ' disabled title="You do not own this project"';
        const pubBtn = proj.any_with_link
            ? `<button class="btn btn-ghost admin-btn-sm" data-proj-action="make-private" data-proj-id="${esc(proj.id)}"${disabled}>Make Private</button>`
            : `<button class="btn btn-ghost admin-btn-sm" data-proj-action="make-public"  data-proj-id="${esc(proj.id)}"${disabled}>Make Public</button>`;
        const delBtn = `<button class="btn btn-danger admin-btn-sm" data-proj-action="delete"
                                data-proj-id="${esc(proj.id)}"
                                data-proj-label="${esc(proj.name)}"${disabled}>Delete</button>`;
        return `<span style="display:inline-flex;gap:0.3rem">${pubBtn}${delBtn}</span>`;
    }

    if (!v && v !== false) return '<span class="admin-cell-muted">—</span>';
    return `<span>${esc(v)}</span>`;
}

/**
 * Returns a sorted copy of the _projects array based on the current sort column and direction.
 * @returns {object[]} Sorted array of project objects.
 */
function sortedProjects() {
    return [..._projects].sort((a, b) => {
        const cmp = cmpValues(a[_projSortCol], b[_projSortCol]);
        return _projSortDir === 'asc' ? cmp : -cmp;
    });
}

/** Rebuilds and injects the projects table HTML, then wires up interaction handlers. */
function renderProjectsTable() {
    const wrap  = document.getElementById('adminProjectsTableWrap');
    const count = document.getElementById('adminProjectsCount');
    count.textContent = `${_projects.length} project${_projects.length !== 1 ? 's' : ''}`;

    const sorted = sortedProjects();

    const thead = PROJ_COLS.map(c => {
        if (c.noSort) return `<th style="min-width:${c.minWidth}"></th>`;
        const active = c.key === _projSortCol;
        const arrow  = active ? (_projSortDir === 'asc' ? ' ↑' : ' ↓') : '';
        return `<th class="${active ? 'sorted' : ''}" data-col="${c.key}" style="min-width:${c.minWidth}">${c.label}${arrow}</th>`;
    }).join('');

    let tbody = '';
    if (_projGroupBy) {
        const groups = new Map();
        for (const proj of sorted) {
            const gv    = proj[_projGroupBy];
            const label = _projGroupBy === 'has_transcript' ? (gv ? 'Has transcript' : 'No transcript')
                        : _projGroupBy === 'any_with_link'  ? (gv ? 'Public' : 'Private')
                        : (gv ? String(gv) : '—');
            if (!groups.has(label)) groups.set(label, []);
            groups.get(label).push(proj);
        }
        for (const [label, rows] of groups) {
            tbody += `<tr class="admin-group-row"><td colspan="${PROJ_COLS.length}">${esc(label)}&ensp;·&ensp;${rows.length}</td></tr>`;
            for (const proj of rows) {
                tbody += `<tr class="admin-table-row" data-proj-id="${esc(proj.id)}">${PROJ_COLS.map(c => `<td>${fmtProjCell(c, proj)}</td>`).join('')}</tr>`;
            }
        }
    } else {
        for (const proj of sorted) {
            tbody += `<tr class="admin-table-row" data-proj-id="${esc(proj.id)}">${PROJ_COLS.map(c => `<td>${fmtProjCell(c, proj)}</td>`).join('')}</tr>`;
        }
    }

    if (!tbody) {
        tbody = `<tr><td colspan="${PROJ_COLS.length}" class="admin-table-empty">No projects found.</td></tr>`;
    }

    wrap.innerHTML = `
        <table class="admin-table">
            <thead><tr>${thead}</tr></thead>
            <tbody>${tbody}</tbody>
        </table>`;

    wrap.querySelectorAll('thead th[data-col]').forEach(th => {
        th.addEventListener('click', () => {
            const col = th.dataset.col;
            _projSortDir = (_projSortCol === col && _projSortDir === 'asc') ? 'desc' : 'asc';
            _projSortCol = col;
            renderProjectsTable();
        });
    });

    // Restore selected highlight if a drawer is open
    if (_drawerProject) _setProjRowSelected(_drawerProject.id);
}

/** Fetches the project list from the API and re-renders the table. */
async function fetchProjects() {
    const btn = document.getElementById('adminProjectsRefreshBtn');
    btn.disabled = true;
    btn.textContent = 'Refreshing…';
    try {
        const headers = {};
        if (authToken) headers['X-Auth-Token'] = authToken;
        const resp = await fetch('/api/admin/projects', { headers });
        if (!resp.ok) throw new Error(resp.status);
        _projects = await resp.json();
        renderProjectsTable();
    } catch {
        document.getElementById('adminProjectsCount').textContent = 'Failed to load projects.';
    } finally {
        btn.disabled = false;
        btn.textContent = 'Refresh';
    }
}

/** Builds and injects the projects panel toolbar and table into the DOM, then fetches data. */
function initProjectsPanel() {
    const section = document.getElementById('tab-projects');
    section.innerHTML = '';

    const toolbar = document.createElement('div');
    toolbar.className = 'admin-toolbar';

    const countEl = document.createElement('span');
    countEl.id = 'adminProjectsCount';
    countEl.className = 'admin-row-count';
    countEl.textContent = 'Loading…';

    const groupLabelEl = document.createElement('label');
    groupLabelEl.className = 'admin-toolbar-label';
    groupLabelEl.textContent = 'Group by';

    const groupSelect = document.createElement('select');
    groupSelect.className = 'admin-group-select';
    PROJ_GROUP_OPTIONS.forEach(opt => {
        const o = document.createElement('option');
        o.value = opt.value;
        o.textContent = opt.label;
        groupSelect.appendChild(o);
    });
    groupSelect.addEventListener('change', () => {
        _projGroupBy = groupSelect.value;
        renderProjectsTable();
    });

    const refreshBtn = document.createElement('button');
    refreshBtn.id = 'adminProjectsRefreshBtn';
    refreshBtn.className = 'btn btn-ghost';
    refreshBtn.textContent = 'Refresh';
    refreshBtn.addEventListener('click', fetchProjects);

    toolbar.appendChild(countEl);
    toolbar.appendChild(groupLabelEl);
    toolbar.appendChild(groupSelect);
    toolbar.appendChild(refreshBtn);

    const tableWrap = document.createElement('div');
    tableWrap.id = 'adminProjectsTableWrap';
    tableWrap.className = 'admin-table-wrap';

    tableWrap.addEventListener('click', (e) => {
        const btn = e.target.closest('button[data-proj-action]');
        if (btn) {
            if (btn.disabled) return;
            const { projAction, projId, projLabel } = btn.dataset;
            if (projAction === 'delete') {
                openDeleteProjectConfirm(projId, projLabel);
            } else if (projAction === 'make-public' || projAction === 'make-private') {
                doSetProjectPublic(projId, projAction === 'make-public', btn);
            }
            return;
        }
        const row = e.target.closest('tr.admin-table-row[data-proj-id]');
        if (row) openProjectDrawer(row.dataset.projId);
    });

    section.appendChild(toolbar);
    section.appendChild(tableWrap);

    fetchProjects();
}

/**
 * Opens the delete confirmation dialog for a project.
 * @param {string} projectId - ID of the project to delete.
 * @param {string} label - Display name of the project shown in the confirmation message.
 */
function openDeleteProjectConfirm(projectId, label) {
    _confirmMsg.textContent = `Permanently delete project "${label}"? This will delete all associated files and cannot be undone.`;
    _confirmOverlay.classList.add('open');
    _confirmOkBtn.onclick = () => {
        closeConfirm();
        doDeleteProject(projectId);
    };
}

/**
 * Sends a DELETE request for the given project and removes it from the local list.
 * @param {string} projectId - ID of the project to delete.
 */
async function doDeleteProject(projectId) {
    try {
        const headers = { 'Content-Type': 'application/json' };
        if (authToken) headers['X-Auth-Token'] = authToken;
        const resp = await fetch(`/api/admin/projects/${encodeURIComponent(projectId)}`, {
            method: 'DELETE', headers,
        });
        if (!resp.ok) throw new Error(resp.status);
        _projects = _projects.filter(p => p.id !== projectId);
        if (_drawerProject?.id === projectId) closeProjectDrawer();
        renderProjectsTable();
    } catch {
        // Silently ignore — user can retry
    }
}

/**
 * Toggles the public/private state of a project via PATCH and updates the local list.
 * @param {string} projectId - ID of the project to update.
 * @param {boolean} makePublic - True to make the project public, false to make it private.
 * @param {HTMLElement} btn - The button that triggered the action (disabled while in-flight).
 */
async function doSetProjectPublic(projectId, makePublic, btn) {
    btn.disabled = true;
    btn.textContent = '…';
    try {
        const headers = { 'Content-Type': 'application/json' };
        if (authToken) headers['X-Auth-Token'] = authToken;
        const resp = await fetch(`/api/admin/projects/${encodeURIComponent(projectId)}/public`, {
            method: 'PATCH', headers,
            body: JSON.stringify({ public: makePublic }),
        });
        if (!resp.ok) throw new Error(resp.status);
        const proj = _projects.find(p => p.id === projectId);
        if (proj) {
            proj.any_with_link = makePublic;
            if (makePublic) { proj.owner = '—'; proj.owner_id = null; }
        }
        renderProjectsTable();
    } catch {
        btn.disabled = false;
        btn.textContent = makePublic ? 'Make Public' : 'Make Private';
    }
}

// ── Project detail drawer ─────────────────────────────────────────────────────

let _drawerProject = null;

const _projDrawerOverlay = document.getElementById('projectDrawerOverlay');
const _projDrawerPanel   = document.getElementById('projectDrawerPanel');

/**
 * Applies or clears the selected-row highlight in the projects table.
 * @param {string|null} projectId - Project ID to highlight, or null to clear.
 */
function _setProjRowSelected(projectId) {
    document.querySelectorAll('#adminProjectsTableWrap tr.admin-table-row').forEach(r => {
        r.classList.toggle('admin-table-row--selected', !!projectId && r.dataset.projId === projectId);
    });
}

/**
 * Opens the project detail drawer for the given project.
 * @param {string} projectId - ID of the project to display.
 */
function openProjectDrawer(projectId) {
    const proj = _projects.find(p => p.id === projectId);
    if (!proj) return;

    if (_drawerUser) closeDrawer();

    _drawerProject = proj;
    renderProjectDrawerContent();
    _projDrawerOverlay.classList.add('open');
    _setProjRowSelected(projectId);
}

/** Closes the project detail drawer and clears the selection highlight. */
function closeProjectDrawer() {
    _projDrawerOverlay.classList.remove('open');
    _drawerProject = null;
    _setProjRowSelected(null);
}

/** Re-renders the project drawer panel from _drawerProject. */
function renderProjectDrawerContent() {
    const proj = _drawerProject;
    if (!proj) return;

    const canEdit = proj.owner_id === null || proj.owner_id === _currentAdminId;

    // Header badges
    const transcriptBadge = proj.has_transcript
        ? '<span class="admin-badge admin-badge--active">Transcript</span>' : '';
    const publicBadge = proj.any_with_link
        ? '<span class="admin-badge admin-badge--admin">Public</span>' : '';
    const folderBadge = proj.folder_is_public
        ? '<span class="admin-badge admin-badge--active">Public Folder</span>' : '';
    const badgeGroup = (transcriptBadge || publicBadge || folderBadge)
        ? `<div class="admin-badge-group" style="margin-top:0.4rem">${transcriptBadge}${publicBadge}${folderBadge}</div>`
        : '';

    // Actions
    const pubToggle = proj.any_with_link
        ? `<button class="btn btn-ghost" id="projDrawerPubBtn" data-action="make-private" data-proj-id="${esc(proj.id)}">Make Private</button>`
        : `<button class="btn btn-ghost" id="projDrawerPubBtn" data-action="make-public"  data-proj-id="${esc(proj.id)}">Make Public</button>`;
    const deleteBtn = `<button class="btn btn-danger" id="projDrawerDelBtn" data-proj-id="${esc(proj.id)}" data-proj-label="${esc(proj.name)}">Delete project</button>`;
    const actionsSection = canEdit ? `
        <div class="user-drawer-section">
            <div class="user-drawer-section-title">Actions</div>
            <div style="display:flex;gap:0.5rem;flex-wrap:wrap">
                ${pubToggle}
                ${deleteBtn}
            </div>
        </div>` : '';

    _projDrawerPanel.innerHTML = `
        <div class="user-drawer-header">
            <div class="user-drawer-identity" style="padding-top:0.1rem">
                <div class="user-drawer-name">${esc(proj.name)}</div>
                <div class="user-drawer-email">${esc(proj.owner || '—')}</div>
                ${badgeGroup}
            </div>
            <button class="user-drawer-close" id="projDrawerClose" title="Close">
                <span class="icon icon-close" style="width:12px;height:12px;"></span>
            </button>
        </div>

        <div class="user-drawer-body">

            <div class="user-drawer-section">
                <div class="user-drawer-section-title">Identity</div>
                ${drawerRow('Project ID', proj.id,       true)}
                ${drawerRow('Owner',      proj.owner || '—')}
                ${drawerRow('Folder',     proj.folder || 'root')}
            </div>

            <div class="user-drawer-section">
                <div class="user-drawer-section-title">Audio</div>
                ${drawerRow('Format',      proj.audio_format ? proj.audio_format.toUpperCase() : '—')}
                ${drawerRow('Duration',    fmtDuration(proj.duration))}
                ${drawerRow('Sample rate', proj.sample_rate > 0 ? `${proj.sample_rate} Hz` : '—')}
                ${drawerRow('MP3 copy',    proj.has_audio_mp3 ? 'Yes' : 'No')}
            </div>

            <div class="user-drawer-section">
                <div class="user-drawer-section-title">Content</div>
                ${drawerRow('Transcript', proj.has_transcript ? 'Yes' : 'No')}
                ${drawerRow('Embeds',     String(proj.embed_count))}
            </div>

            <div class="user-drawer-section">
                <div class="user-drawer-section-title">Timestamps</div>
                ${drawerRow('Created',  fmtDatetime(proj.created_at))}
                ${drawerRow('Modified', fmtDatetime(proj.modified_at))}
            </div>

            ${actionsSection}

        </div>`;

    document.getElementById('projDrawerClose').addEventListener('click', closeProjectDrawer);

    const pubBtn = document.getElementById('projDrawerPubBtn');
    if (pubBtn) {
        pubBtn.addEventListener('click', async () => {
            const makePublic = pubBtn.dataset.action === 'make-public';
            await doSetProjectPublic(proj.id, makePublic, pubBtn);
            // Re-render drawer with updated state
            _drawerProject = _projects.find(p => p.id === proj.id) ?? _drawerProject;
            renderProjectDrawerContent();
        });
    }

    const delBtn = document.getElementById('projDrawerDelBtn');
    if (delBtn) {
        delBtn.addEventListener('click', () => {
            openDeleteProjectConfirm(delBtn.dataset.projId, delBtn.dataset.projLabel);
        });
    }
}

// ── User detail drawer ────────────────────────────────────────────────────────

let _drawerUser = null;  // Full user object (includes preferences after fetch)

const _drawerOverlay = document.getElementById('userDrawerOverlay');
const _drawerPanel   = document.getElementById('userDrawerPanel');

/**
 * Applies or clears the selected-row highlight in the users table.
 * @param {string|null} userId - User ID to highlight, or null to clear.
 */
function _setUserRowSelected(userId) {
    document.querySelectorAll('#adminUsersTableWrap tr.admin-table-row').forEach(r => {
        r.classList.toggle('admin-table-row--selected', !!userId && r.dataset.userId === userId);
    });
}

/**
 * Opens the user detail drawer, immediately showing list data then fetching the full record.
 * @param {string} userId - ID of the user to display.
 */
function openDrawer(userId) {
    const listUser = _users.find(u => u.id === userId);
    if (!listUser) return;

    if (_drawerProject) closeProjectDrawer();

    // Show immediately with data we already have (no preferences yet)
    _drawerUser = { ...listUser };
    renderDrawerContent();
    _drawerOverlay.classList.add('open');
    _setUserRowSelected(userId);

    // Fetch full record (includes preferences)
    const headers = {};
    if (authToken) headers['X-Auth-Token'] = authToken;
    fetch(`/api/admin/users/${encodeURIComponent(userId)}`, { headers })
        .then(r => r.ok ? r.json() : null)
        .then(full => {
            if (full && _drawerUser?.id === userId) {
                _drawerUser = full;
                renderDrawerContent();
            }
        })
        .catch(() => {});
}

/** Closes the user detail drawer and clears the selection highlight. */
function closeDrawer() {
    _drawerOverlay.classList.remove('open');
    _drawerUser = null;
    _setUserRowSelected(null);
}

/** Re-render the drawer panel from _drawerUser. */
function renderDrawerContent() {
    const user = _drawerUser;
    if (!user) return;

    const isSelf    = user.id === _currentAdminId;
    const initials  = (user.display_name || user.email || '?')
        .split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase();

    const statusBadge = user.is_active
        ? '<span class="admin-badge admin-badge--active">Active</span>'
        : '<span class="admin-badge admin-badge--inactive">Inactive</span>';
    const adminBadge = user.is_admin
        ? '<span class="admin-badge admin-badge--admin">Admin</span>'
        : '';

    // Preferences section
    const hasPrefs = user.preferences != null;
    const prefsHtml = hasPrefs
        ? renderPreferences(user.preferences)
        : '<p class="admin-detail-hint">Loading…</p>';

    _drawerPanel.innerHTML = `
        <div class="user-drawer-header">
            <div class="user-drawer-avatar">${esc(initials)}</div>
            <div class="user-drawer-identity">
                <div class="user-drawer-name">${esc(user.display_name || '—')}</div>
                <div class="user-drawer-email">${esc(user.email || '—')}</div>
                <div class="admin-badge-group" style="margin-top:0.4rem">${statusBadge}${adminBadge}</div>
            </div>
            <button class="user-drawer-close" id="userDrawerClose" title="Close">
                <span class="icon icon-close" style="width:12px;height:12px;"></span>
            </button>
        </div>

        <div class="user-drawer-body">

            <div class="user-drawer-section">
                <div class="user-drawer-section-title">Identity</div>
                ${drawerRow('User ID',      user.id,           true)}
                ${drawerRow('Firebase UID', user.external_id,  true)}
                ${drawerRow('Provider',     user.auth_provider)}
            </div>

            <div class="user-drawer-section">
                <div class="user-drawer-section-title">Account</div>
                ${drawerRow('Created',    fmtDatetime(user.created_at))}
                ${drawerRow('Last login', fmtDatetime(user.last_login_at))}
                ${drawerRow('Modified',   fmtDatetime(user.modified_at))}
                ${user.subscription_id
                    ? drawerRow('Subscription', user.subscription_name ?? user.subscription_id, !user.subscription_name)
                    : ''}
            </div>

            ${!isSelf ? `
            <div class="user-drawer-section">
                <div class="user-drawer-section-title">Actions</div>
                <div style="display:flex;gap:0.5rem;flex-wrap:wrap">
                    <button class="${user.is_active ? 'btn btn-danger' : 'btn btn-primary'}"
                            id="drawerToggleBtn"
                            data-action="${user.is_active ? 'deactivate' : 'activate'}"
                            data-user-id="${esc(user.id)}">
                        ${user.is_active ? 'Deactivate account' : 'Activate account'}
                    </button>
                    <button class="btn btn-danger" id="drawerDeleteBtn" data-user-id="${esc(user.id)}">
                        Delete account
                    </button>
                </div>
            </div>` : ''}

            <div class="user-drawer-section">
                <div class="user-drawer-section-title">Settings</div>
                ${prefsHtml}
            </div>

        </div>`;

    document.getElementById('userDrawerClose').addEventListener('click', closeDrawer);

    const toggleBtn = document.getElementById('drawerToggleBtn');
    if (toggleBtn) {
        toggleBtn.addEventListener('click', () => {
            doSetUserActive(toggleBtn.dataset.userId, toggleBtn.dataset.action === 'activate', toggleBtn);
        });
    }

    const deleteBtn = document.getElementById('drawerDeleteBtn');
    if (deleteBtn) {
        deleteBtn.addEventListener('click', () => {
            openDeleteConfirm(deleteBtn.dataset.userId, user.display_name || user.email);
        });
    }
}

/**
 * Renders a label/value row for the drawer.
 * @param {string}  label - Display label.
 * @param {string}  value - Value to display.
 * @param {boolean} mono  - Whether to render the value in monospace.
 * @returns {string} HTML string for the row.
 */
function drawerRow(label, value, mono = false) {
    const valHtml = value
        ? `<span class="${mono ? 'admin-cell-mono' : ''} user-drawer-row-value-text">${esc(value)}</span>`
        : '<span class="admin-cell-muted">—</span>';
    return `
        <div class="user-drawer-row">
            <span class="user-drawer-row-label">${esc(label)}</span>
            <span class="user-drawer-row-value">${valHtml}</span>
        </div>`;
}

const STARTUP_LABELS = { last_project: 'Open last project', home_screen: 'Show home screen' };
const KNOWN_PREF_KEYS = new Set([
    'theme', 'startup_behavior', 'autosave', 'undo_queue_size',
    'custom_themes', 'custom_colors_light', 'custom_colors_dark',
    'subscription_since', 'survey_answers',
]);

/**
 * Renders the preferences object as a series of drawer rows.
 * @param {object} prefs - User preferences object.
 * @returns {string} HTML string of drawer rows, or a hint message if empty.
 */
function renderPreferences(prefs) {
    if (!prefs || !Object.keys(prefs).length) {
        return '<p class="admin-detail-hint">No preferences saved.</p>';
    }

    const rows = [];

    if (prefs.theme !== undefined)
        rows.push(drawerRow('Theme', prefs.theme));
    if (prefs.startup_behavior !== undefined)
        rows.push(drawerRow('On startup', STARTUP_LABELS[prefs.startup_behavior] ?? prefs.startup_behavior));
    if (prefs.autosave !== undefined)
        rows.push(drawerRow('Autosave', prefs.autosave ? 'Enabled' : 'Disabled'));
    if (prefs.undo_queue_size !== undefined)
        rows.push(drawerRow('Undo queue', `${prefs.undo_queue_size} steps`));
    if (prefs.custom_themes?.length)
        rows.push(drawerRow('Custom themes', String(prefs.custom_themes.length)));

    const hasLightColors = prefs.custom_colors_light && Object.keys(prefs.custom_colors_light).length;
    const hasDarkColors  = prefs.custom_colors_dark  && Object.keys(prefs.custom_colors_dark).length;
    if (hasLightColors || hasDarkColors)
        rows.push(drawerRow('Custom colors', [
            hasLightColors ? 'light' : '',
            hasDarkColors  ? 'dark'  : '',
        ].filter(Boolean).join(', ')));

    if (prefs.subscription_since)
        rows.push(drawerRow('Subscriber since', fmtDate(prefs.subscription_since)));

    if (prefs.survey_answers && Object.keys(prefs.survey_answers).length) {
        for (const [k, v] of Object.entries(prefs.survey_answers)) {
            if (v) rows.push(drawerRow(k.replace(/_/g, ' '), String(v)));
        }
    }

    // Any unrecognised keys
    for (const [k, v] of Object.entries(prefs)) {
        if (!KNOWN_PREF_KEYS.has(k)) {
            const display = typeof v === 'object' ? JSON.stringify(v) : String(v);
            rows.push(drawerRow(k, display));
        }
    }

    return rows.join('');
}

// ── Folders panel ─────────────────────────────────────────────────────────────

let _folders      = [];
let _fldSortCol   = 'created_at';
let _fldSortDir   = 'desc';
let _fldGroupBy   = '';

const FLD_COLS = [
    { key: 'name',            label: 'Name',       minWidth: '160px' },
    { key: 'owner',           label: 'Owner',       minWidth: '160px' },
    { key: 'parent_name',     label: 'Parent',      minWidth: '120px' },
    { key: 'is_public',       label: 'Visibility',  minWidth: '100px' },
    { key: 'project_count',   label: 'Projects',    minWidth: '75px'  },
    { key: 'subfolder_count', label: 'Subfolders',  minWidth: '80px'  },
    { key: 'created_at',      label: 'Created',     minWidth: '110px' },
    { key: 'id',              label: 'Folder ID',   minWidth: '100px' },
    { key: '_actions',        label: '',            minWidth: '175px', noSort: true },
];

const FLD_GROUP_OPTIONS = [
    { value: '',          label: 'None'      },
    { value: 'owner',     label: 'Owner'     },
    { value: 'parent_name', label: 'Parent'  },
    { value: 'is_public', label: 'Visibility'},
];

/**
 * Renders a folders table cell as an HTML string for the given column.
 * @param {object} col - Column descriptor from FLD_COLS.
 * @param {object} fld - Folder data object.
 * @returns {string} Inner HTML for the cell.
 */
function fmtFldCell(col, fld) {
    const v = fld[col.key];

    if (col.key === 'is_public') {
        return v
            ? '<span class="admin-badge admin-badge--admin">Public</span>'
            : '<span class="admin-cell-muted">Private</span>';
    }

    if (col.key === 'project_count' || col.key === 'subfolder_count') {
        return `<span class="admin-cell-mono">${v ?? 0}</span>`;
    }

    if (col.key === 'created_at') {
        return `<span title="${esc(v ?? '')}">${fmtDate(v)}</span>`;
    }

    if (col.key === 'parent_name') {
        return v ? `<span class="admin-cell-muted">${esc(v)}</span>`
                 : '<span class="admin-cell-muted">root</span>';
    }

    if (col.key === 'id') {
        const trunc = v && v.length > 14 ? v.slice(0, 14) + '…' : v;
        return v
            ? `<span class="admin-cell-mono admin-cell-trunc" title="${esc(v)}">${esc(trunc)}</span>`
            : '<span class="admin-cell-muted">—</span>';
    }

    if (col.key === '_actions') {
        const canEdit = fld.owner_id === null || fld.owner_id === _currentAdminId;
        const disabled = canEdit ? '' : ' disabled title="You do not own this folder"';
        const pubBtn = fld.is_public
            ? `<button class="btn btn-ghost admin-btn-sm" data-fld-action="make-private" data-fld-id="${esc(fld.id)}"${disabled}>Make Private</button>`
            : `<button class="btn btn-ghost admin-btn-sm" data-fld-action="make-public"  data-fld-id="${esc(fld.id)}"${disabled}>Make Public</button>`;
        const delBtn = `<button class="btn btn-danger admin-btn-sm" data-fld-action="delete"
                                data-fld-id="${esc(fld.id)}"
                                data-fld-label="${esc(fld.name)}"${disabled}>Delete</button>`;
        return `<span style="display:inline-flex;gap:0.3rem">${pubBtn}${delBtn}</span>`;
    }

    if (!v && v !== false) return '<span class="admin-cell-muted">—</span>';
    return `<span>${esc(v)}</span>`;
}

/**
 * Returns a sorted copy of the _folders array based on the current sort column and direction.
 * @returns {object[]} Sorted array of folder objects.
 */
function sortedFolders() {
    return [..._folders].sort((a, b) => {
        const cmp = cmpValues(a[_fldSortCol], b[_fldSortCol]);
        return _fldSortDir === 'asc' ? cmp : -cmp;
    });
}

/** Rebuilds and injects the folders table HTML, then wires up interaction handlers. */
function renderFoldersTable() {
    const wrap  = document.getElementById('adminFoldersTableWrap');
    const count = document.getElementById('adminFoldersCount');
    count.textContent = `${_folders.length} folder${_folders.length !== 1 ? 's' : ''}`;

    const sorted = sortedFolders();

    const thead = FLD_COLS.map(c => {
        if (c.noSort) return `<th style="min-width:${c.minWidth}"></th>`;
        const active = c.key === _fldSortCol;
        const arrow  = active ? (_fldSortDir === 'asc' ? ' ↑' : ' ↓') : '';
        return `<th class="${active ? 'sorted' : ''}" data-col="${c.key}" style="min-width:${c.minWidth}">${c.label}${arrow}</th>`;
    }).join('');

    let tbody = '';
    if (_fldGroupBy) {
        const groups = new Map();
        for (const fld of sorted) {
            const gv    = fld[_fldGroupBy];
            const label = _fldGroupBy === 'is_public' ? (gv ? 'Public' : 'Private')
                        : (gv ? String(gv) : '—');
            if (!groups.has(label)) groups.set(label, []);
            groups.get(label).push(fld);
        }
        for (const [label, rows] of groups) {
            tbody += `<tr class="admin-group-row"><td colspan="${FLD_COLS.length}">${esc(label)}&ensp;·&ensp;${rows.length}</td></tr>`;
            for (const fld of rows) {
                tbody += `<tr class="admin-table-row" data-fld-id="${esc(fld.id)}">${FLD_COLS.map(c => `<td>${fmtFldCell(c, fld)}</td>`).join('')}</tr>`;
            }
        }
    } else {
        for (const fld of sorted) {
            tbody += `<tr class="admin-table-row" data-fld-id="${esc(fld.id)}">${FLD_COLS.map(c => `<td>${fmtFldCell(c, fld)}</td>`).join('')}</tr>`;
        }
    }

    if (!tbody) {
        tbody = `<tr><td colspan="${FLD_COLS.length}" class="admin-table-empty">No folders found.</td></tr>`;
    }

    wrap.innerHTML = `
        <table class="admin-table">
            <thead><tr>${thead}</tr></thead>
            <tbody>${tbody}</tbody>
        </table>`;

    wrap.querySelectorAll('thead th[data-col]').forEach(th => {
        th.addEventListener('click', () => {
            const col = th.dataset.col;
            _fldSortDir = (_fldSortCol === col && _fldSortDir === 'asc') ? 'desc' : 'asc';
            _fldSortCol = col;
            renderFoldersTable();
        });
    });

    if (_drawerFolder) _setFldRowSelected(_drawerFolder.id);
}

/** Fetches the folder list from the API and re-renders the table. */
async function fetchFolders() {
    const btn = document.getElementById('adminFoldersRefreshBtn');
    btn.disabled = true;
    btn.textContent = 'Refreshing…';
    try {
        const headers = {};
        if (authToken) headers['X-Auth-Token'] = authToken;
        const resp = await fetch('/api/admin/folders', { headers });
        if (!resp.ok) throw new Error(resp.status);
        _folders = await resp.json();
        renderFoldersTable();
    } catch {
        document.getElementById('adminFoldersCount').textContent = 'Failed to load folders.';
    } finally {
        btn.disabled = false;
        btn.textContent = 'Refresh';
    }
}

/** Builds and injects the folders panel toolbar and table into the DOM, then fetches data. */
function initFoldersPanel() {
    const section = document.getElementById('tab-folders');
    section.innerHTML = '<h2 class="account-panel-title">Folders</h2>';

    const toolbar = document.createElement('div');
    toolbar.className = 'admin-toolbar';

    const countEl = document.createElement('span');
    countEl.id = 'adminFoldersCount';
    countEl.className = 'admin-row-count';
    countEl.textContent = 'Loading…';

    const groupLabelEl = document.createElement('label');
    groupLabelEl.className = 'admin-toolbar-label';
    groupLabelEl.textContent = 'Group by';

    const groupSelect = document.createElement('select');
    groupSelect.className = 'admin-group-select';
    FLD_GROUP_OPTIONS.forEach(opt => {
        const o = document.createElement('option');
        o.value = opt.value;
        o.textContent = opt.label;
        groupSelect.appendChild(o);
    });
    groupSelect.addEventListener('change', () => {
        _fldGroupBy = groupSelect.value;
        renderFoldersTable();
    });

    const refreshBtn = document.createElement('button');
    refreshBtn.id = 'adminFoldersRefreshBtn';
    refreshBtn.className = 'btn btn-ghost';
    refreshBtn.textContent = 'Refresh';
    refreshBtn.addEventListener('click', fetchFolders);

    toolbar.appendChild(countEl);
    toolbar.appendChild(groupLabelEl);
    toolbar.appendChild(groupSelect);
    toolbar.appendChild(refreshBtn);

    const tableWrap = document.createElement('div');
    tableWrap.id = 'adminFoldersTableWrap';
    tableWrap.className = 'admin-table-wrap';

    tableWrap.addEventListener('click', (e) => {
        const btn = e.target.closest('button[data-fld-action]');
        if (btn) {
            if (btn.disabled) return;
            const { fldAction, fldId, fldLabel } = btn.dataset;
            if (fldAction === 'delete') {
                openDeleteFolderConfirm(fldId, fldLabel);
            } else if (fldAction === 'make-public' || fldAction === 'make-private') {
                doSetFolderPublic(fldId, fldAction === 'make-public', btn);
            }
            return;
        }
        const row = e.target.closest('tr.admin-table-row[data-fld-id]');
        if (row) openFolderDrawer(row.dataset.fldId);
    });

    section.appendChild(toolbar);
    section.appendChild(tableWrap);

    fetchFolders();
}

/**
 * Opens the delete confirmation dialog for a folder.
 * @param {string} folderId - ID of the folder to delete.
 * @param {string} label - Display name of the folder shown in the confirmation message.
 */
function openDeleteFolderConfirm(folderId, label) {
    _confirmMsg.textContent = `Permanently delete folder "${label}"? All subfolders and projects inside will also be deleted. This cannot be undone.`;
    _confirmOverlay.classList.add('open');
    _confirmOkBtn.onclick = () => {
        closeConfirm();
        doDeleteFolder(folderId);
    };
}

/**
 * Sends a DELETE request for the given folder and removes it from the local list.
 * @param {string} folderId - ID of the folder to delete.
 */
async function doDeleteFolder(folderId) {
    try {
        const headers = { 'Content-Type': 'application/json' };
        if (authToken) headers['X-Auth-Token'] = authToken;
        const resp = await fetch(`/api/admin/folders/${encodeURIComponent(folderId)}`, {
            method: 'DELETE', headers,
        });
        if (!resp.ok) throw new Error(resp.status);
        _folders = _folders.filter(f => f.id !== folderId);
        if (_drawerFolder?.id === folderId) closeFolderDrawer();
        renderFoldersTable();
    } catch {
        // Silently ignore — user can retry
    }
}

/**
 * Toggles the public/private state of a folder via PATCH and updates the local list.
 * @param {string} folderId - ID of the folder to update.
 * @param {boolean} makePublic - True to make the folder public, false to make it private.
 * @param {HTMLElement} btn - The button that triggered the action (disabled while in-flight).
 */
async function doSetFolderPublic(folderId, makePublic, btn) {
    btn.disabled = true;
    btn.textContent = '…';
    try {
        const headers = { 'Content-Type': 'application/json' };
        if (authToken) headers['X-Auth-Token'] = authToken;
        const resp = await fetch(`/api/admin/folders/${encodeURIComponent(folderId)}/public`, {
            method: 'PATCH', headers,
            body: JSON.stringify({ public: makePublic }),
        });
        if (!resp.ok) throw new Error(resp.status);
        const fld = _folders.find(f => f.id === folderId);
        if (fld) fld.is_public = makePublic;
        if (_drawerFolder?.id === folderId) {
            _drawerFolder.is_public = makePublic;
            renderFolderDrawerContent();
        }
        renderFoldersTable();
    } catch {
        btn.disabled = false;
        btn.textContent = makePublic ? 'Make Public' : 'Make Private';
    }
}

// ── Folder detail drawer ──────────────────────────────────────────────────────

let _drawerFolder = null;

const _fldDrawerOverlay = document.getElementById('folderDrawerOverlay');
const _fldDrawerPanel   = document.getElementById('folderDrawerPanel');

/**
 * Applies or clears the selected-row highlight in the folders table.
 * @param {string|null} folderId - Folder ID to highlight, or null to clear.
 */
function _setFldRowSelected(folderId) {
    document.querySelectorAll('#adminFoldersTableWrap tr.admin-table-row').forEach(r => {
        r.classList.toggle('admin-table-row--selected', !!folderId && r.dataset.fldId === folderId);
    });
}

/**
 * Opens the folder detail drawer for the given folder.
 * @param {string} folderId - ID of the folder to display.
 */
function openFolderDrawer(folderId) {
    const fld = _folders.find(f => f.id === folderId);
    if (!fld) return;

    if (_drawerUser)    closeDrawer();
    if (_drawerProject) closeProjectDrawer();

    _drawerFolder = fld;
    renderFolderDrawerContent();
    _fldDrawerOverlay.classList.add('open');
    _setFldRowSelected(folderId);
}

/** Closes the folder detail drawer and clears the selection highlight. */
function closeFolderDrawer() {
    _fldDrawerOverlay.classList.remove('open');
    _drawerFolder = null;
    _setFldRowSelected(null);
}

/** Re-renders the folder drawer panel from _drawerFolder. */
function renderFolderDrawerContent() {
    const fld = _drawerFolder;
    if (!fld) return;

    const canEdit = fld.owner_id === null || fld.owner_id === _currentAdminId;

    const publicBadge = fld.is_public
        ? '<span class="admin-badge admin-badge--admin">Public</span>' : '';
    const badgeGroup = publicBadge
        ? `<div class="admin-badge-group" style="margin-top:0.4rem">${publicBadge}</div>` : '';

    const pubToggle = fld.is_public
        ? `<button class="btn btn-ghost" id="fldDrawerPubBtn" data-action="make-private" data-fld-id="${esc(fld.id)}">Make Private</button>`
        : `<button class="btn btn-ghost" id="fldDrawerPubBtn" data-action="make-public"  data-fld-id="${esc(fld.id)}">Make Public</button>`;
    const deleteBtn = `<button class="btn btn-danger" id="fldDrawerDelBtn" data-fld-id="${esc(fld.id)}" data-fld-label="${esc(fld.name)}">Delete folder</button>`;
    const actionsSection = canEdit ? `
        <div class="user-drawer-section">
            <div class="user-drawer-section-title">Actions</div>
            <div style="display:flex;gap:0.5rem;flex-wrap:wrap">
                ${pubToggle}
                ${deleteBtn}
            </div>
        </div>` : '';

    _fldDrawerPanel.innerHTML = `
        <div class="user-drawer-header">
            <div class="user-drawer-identity" style="padding-top:0.1rem">
                <div class="user-drawer-name">${esc(fld.name)}</div>
                <div class="user-drawer-email">${esc(fld.owner || '—')}</div>
                ${badgeGroup}
            </div>
            <button class="user-drawer-close" id="fldDrawerClose" title="Close">
                <span class="icon icon-close" style="width:12px;height:12px;"></span>
            </button>
        </div>

        <div class="user-drawer-body">

            <div class="user-drawer-section">
                <div class="user-drawer-section-title">Identity</div>
                ${drawerRow('Folder ID', fld.id,                    true)}
                ${drawerRow('Parent',    fld.parent_name || 'root'      )}
                ${drawerRow('Owner',     fld.owner || '—'               )}
            </div>

            <div class="user-drawer-section">
                <div class="user-drawer-section-title">Contents</div>
                ${drawerRow('Projects',   String(fld.project_count)  )}
                ${drawerRow('Subfolders', String(fld.subfolder_count))}
            </div>

            <div class="user-drawer-section">
                <div class="user-drawer-section-title">Timestamps</div>
                ${drawerRow('Created', fmtDatetime(fld.created_at))}
            </div>

            ${actionsSection}

        </div>`;

    document.getElementById('fldDrawerClose').addEventListener('click', closeFolderDrawer);

    const pubBtn = document.getElementById('fldDrawerPubBtn');
    if (pubBtn) {
        pubBtn.addEventListener('click', async () => {
            const makePublic = pubBtn.dataset.action === 'make-public';
            await doSetFolderPublic(fld.id, makePublic, pubBtn);
        });
    }

    const delBtn = document.getElementById('fldDrawerDelBtn');
    if (delBtn) {
        delBtn.addEventListener('click', () => {
            openDeleteFolderConfirm(delBtn.dataset.fldId, delBtn.dataset.fldLabel);
        });
    }
}

// ── Embeds panel ──────────────────────────────────────────────────────────────

let _embeds     = [];
let _embSortCol = 'created_at';
let _embSortDir = 'desc';
let _embGroupBy = '';

const EMB_COLS = [
    { key: 'id',           label: 'Embed ID',   minWidth: '120px' },
    { key: 'project_name', label: 'Project',    minWidth: '160px' },
    { key: 'owner',        label: 'Owner',      minWidth: '160px' },
    { key: 'invoke_count', label: 'Invocations',minWidth: '90px'  },
    { key: 'created_at',   label: 'Created',    minWidth: '110px' },
    { key: '_actions',     label: '',           minWidth: '80px',  noSort: true },
];

const EMB_GROUP_OPTIONS = [
    { value: '',             label: 'None'    },
    { value: 'owner',        label: 'Owner'   },
    { value: 'project_name', label: 'Project' },
];

/**
 * Renders an embeds table cell as an HTML string for the given column.
 * @param {object} col - Column descriptor from EMB_COLS.
 * @param {object} emb - Embed data object.
 * @returns {string} Inner HTML for the cell.
 */
function fmtEmbCell(col, emb) {
    const v = emb[col.key];

    if (col.key === 'id') {
        const trunc = v && v.length > 16 ? v.slice(0, 16) + '…' : v;
        return `<span class="admin-cell-mono admin-cell-trunc" title="${esc(v)}">${esc(trunc)}</span>`;
    }

    if (col.key === 'invoke_count') {
        return `<span class="admin-cell-mono">${v ?? 0}</span>`;
    }

    if (col.key === 'created_at') {
        return `<span title="${esc(v ?? '')}">${fmtDate(v)}</span>`;
    }

    if (col.key === '_actions') {
        return `<button class="btn btn-danger admin-btn-sm" data-emb-action="delete"
                        data-emb-id="${esc(emb.id)}"
                        data-emb-label="${esc(emb.id.slice(0, 8))}…">Delete</button>`;
    }

    if (!v) return '<span class="admin-cell-muted">—</span>';
    return `<span>${esc(v)}</span>`;
}

/**
 * Returns a sorted copy of the _embeds array based on the current sort column and direction.
 * @returns {object[]} Sorted array of embed objects.
 */
function sortedEmbeds() {
    return [..._embeds].sort((a, b) => {
        const cmp = cmpValues(a[_embSortCol], b[_embSortCol]);
        return _embSortDir === 'asc' ? cmp : -cmp;
    });
}

/** Rebuilds and injects the embeds table HTML, then wires up interaction handlers. */
function renderEmbedsTable() {
    const wrap  = document.getElementById('adminEmbedsTableWrap');
    const count = document.getElementById('adminEmbedsCount');
    count.textContent = `${_embeds.length} embed${_embeds.length !== 1 ? 's' : ''}`;

    const sorted = sortedEmbeds();

    const thead = EMB_COLS.map(c => {
        if (c.noSort) return `<th style="min-width:${c.minWidth}"></th>`;
        const active = c.key === _embSortCol;
        const arrow  = active ? (_embSortDir === 'asc' ? ' ↑' : ' ↓') : '';
        return `<th class="${active ? 'sorted' : ''}" data-col="${c.key}" style="min-width:${c.minWidth}">${c.label}${arrow}</th>`;
    }).join('');

    let tbody = '';
    if (_embGroupBy) {
        const groups = new Map();
        for (const emb of sorted) {
            const label = emb[_embGroupBy] || '—';
            if (!groups.has(label)) groups.set(label, []);
            groups.get(label).push(emb);
        }
        for (const [label, rows] of groups) {
            tbody += `<tr class="admin-group-row"><td colspan="${EMB_COLS.length}">${esc(label)}&ensp;·&ensp;${rows.length}</td></tr>`;
            for (const emb of rows) {
                tbody += `<tr class="admin-table-row" data-emb-id="${esc(emb.id)}">${EMB_COLS.map(c => `<td>${fmtEmbCell(c, emb)}</td>`).join('')}</tr>`;
            }
        }
    } else {
        for (const emb of sorted) {
            tbody += `<tr class="admin-table-row" data-emb-id="${esc(emb.id)}">${EMB_COLS.map(c => `<td>${fmtEmbCell(c, emb)}</td>`).join('')}</tr>`;
        }
    }

    if (!tbody) {
        tbody = `<tr><td colspan="${EMB_COLS.length}" class="admin-table-empty">No embeds found.</td></tr>`;
    }

    wrap.innerHTML = `
        <table class="admin-table">
            <thead><tr>${thead}</tr></thead>
            <tbody>${tbody}</tbody>
        </table>`;

    wrap.querySelectorAll('thead th[data-col]').forEach(th => {
        th.addEventListener('click', () => {
            const col = th.dataset.col;
            _embSortDir = (_embSortCol === col && _embSortDir === 'asc') ? 'desc' : 'asc';
            _embSortCol = col;
            renderEmbedsTable();
        });
    });

    if (_drawerEmbed) _setEmbRowSelected(_drawerEmbed.id);
}

/** Fetches the embed list from the API and re-renders the table. */
async function fetchEmbeds() {
    const btn = document.getElementById('adminEmbedsRefreshBtn');
    btn.disabled = true;
    btn.textContent = 'Refreshing…';
    try {
        const headers = {};
        if (authToken) headers['X-Auth-Token'] = authToken;
        const resp = await fetch('/api/admin/embeds', { headers });
        if (!resp.ok) throw new Error(resp.status);
        _embeds = await resp.json();
        renderEmbedsTable();
    } catch {
        document.getElementById('adminEmbedsCount').textContent = 'Failed to load embeds.';
    } finally {
        btn.disabled = false;
        btn.textContent = 'Refresh';
    }
}

/** Builds and injects the embeds panel toolbar and table into the DOM, then fetches data. */
function initEmbedsPanel() {
    const section = document.getElementById('tab-embeds');
    section.innerHTML = '<h2 class="account-panel-title">Embeds</h2>';

    const toolbar = document.createElement('div');
    toolbar.className = 'admin-toolbar';

    const countEl = document.createElement('span');
    countEl.id = 'adminEmbedsCount';
    countEl.className = 'admin-row-count';
    countEl.textContent = 'Loading…';

    const groupLabelEl = document.createElement('label');
    groupLabelEl.className = 'admin-toolbar-label';
    groupLabelEl.textContent = 'Group by';

    const groupSelect = document.createElement('select');
    groupSelect.className = 'admin-group-select';
    EMB_GROUP_OPTIONS.forEach(opt => {
        const o = document.createElement('option');
        o.value = opt.value;
        o.textContent = opt.label;
        groupSelect.appendChild(o);
    });
    groupSelect.addEventListener('change', () => {
        _embGroupBy = groupSelect.value;
        renderEmbedsTable();
    });

    const refreshBtn = document.createElement('button');
    refreshBtn.id = 'adminEmbedsRefreshBtn';
    refreshBtn.className = 'btn btn-ghost';
    refreshBtn.textContent = 'Refresh';
    refreshBtn.addEventListener('click', fetchEmbeds);

    toolbar.appendChild(countEl);
    toolbar.appendChild(groupLabelEl);
    toolbar.appendChild(groupSelect);
    toolbar.appendChild(refreshBtn);

    const tableWrap = document.createElement('div');
    tableWrap.id = 'adminEmbedsTableWrap';
    tableWrap.className = 'admin-table-wrap';

    tableWrap.addEventListener('click', (e) => {
        const btn = e.target.closest('button[data-emb-action]');
        if (btn) {
            if (btn.disabled) return;
            if (btn.dataset.embAction === 'delete') {
                openDeleteEmbedConfirm(btn.dataset.embId, btn.dataset.embLabel);
            }
            return;
        }
        const row = e.target.closest('tr.admin-table-row[data-emb-id]');
        if (row) openEmbedDrawer(row.dataset.embId);
    });

    section.appendChild(toolbar);
    section.appendChild(tableWrap);

    fetchEmbeds();
}

/**
 * Opens the delete confirmation dialog for an embed.
 * @param {string} embedId - ID of the embed to delete.
 * @param {string} label - Truncated embed ID shown in the confirmation message.
 */
function openDeleteEmbedConfirm(embedId, label) {
    _confirmMsg.textContent = `Permanently delete embed "${label}"? Any pages using this embed will stop working.`;
    _confirmOverlay.classList.add('open');
    _confirmOkBtn.onclick = () => {
        closeConfirm();
        doDeleteEmbed(embedId);
    };
}

/**
 * Sends a DELETE request for the given embed and removes it from the local list.
 * @param {string} embedId - ID of the embed to delete.
 */
async function doDeleteEmbed(embedId) {
    try {
        const headers = { 'Content-Type': 'application/json' };
        if (authToken) headers['X-Auth-Token'] = authToken;
        const resp = await fetch(`/api/admin/embeds/${encodeURIComponent(embedId)}`, {
            method: 'DELETE', headers,
        });
        if (!resp.ok) throw new Error(resp.status);
        _embeds = _embeds.filter(e => e.id !== embedId);
        if (_drawerEmbed?.id === embedId) closeEmbedDrawer();
        renderEmbedsTable();
    } catch {
        // Silently ignore — user can retry
    }
}

// ── Embed detail drawer ───────────────────────────────────────────────────────

let _drawerEmbed = null;

const _embDrawerOverlay = document.getElementById('embedDrawerOverlay');
const _embDrawerPanel   = document.getElementById('embedDrawerPanel');

/**
 * Applies or clears the selected-row highlight in the embeds table.
 * @param {string|null} embedId - Embed ID to highlight, or null to clear.
 */
function _setEmbRowSelected(embedId) {
    document.querySelectorAll('#adminEmbedsTableWrap tr.admin-table-row').forEach(r => {
        r.classList.toggle('admin-table-row--selected', !!embedId && r.dataset.embId === embedId);
    });
}

/**
 * Opens the embed detail drawer for the given embed.
 * @param {string} embedId - ID of the embed to display.
 */
function openEmbedDrawer(embedId) {
    const emb = _embeds.find(e => e.id === embedId);
    if (!emb) return;

    if (_drawerUser)    closeDrawer();
    if (_drawerProject) closeProjectDrawer();
    if (_drawerFolder)  closeFolderDrawer();

    _drawerEmbed = emb;
    renderEmbedDrawerContent();
    _embDrawerOverlay.classList.add('open');
    _setEmbRowSelected(embedId);
}

/** Closes the embed detail drawer and clears the selection highlight. */
function closeEmbedDrawer() {
    _embDrawerOverlay.classList.remove('open');
    _drawerEmbed = null;
    _setEmbRowSelected(null);
}

/** Re-renders the embed drawer panel from _drawerEmbed. */
function renderEmbedDrawerContent() {
    const emb = _drawerEmbed;
    if (!emb) return;

    _embDrawerPanel.innerHTML = `
        <div class="user-drawer-header">
            <div class="user-drawer-identity" style="padding-top:0.1rem">
                <div class="user-drawer-name" style="font-size:var(--fs-hint)">${esc(emb.id)}</div>
                <div class="user-drawer-email">${esc(emb.owner || '—')}</div>
            </div>
            <button class="user-drawer-close" id="embDrawerClose" title="Close">
                <span class="icon icon-close" style="width:12px;height:12px;"></span>
            </button>
        </div>

        <div class="user-drawer-body">

            <div class="user-drawer-section">
                <div class="user-drawer-section-title">Identity</div>
                ${drawerRow('Embed ID',   emb.id,           true)}
                ${drawerRow('Project',    emb.project_name      )}
                ${drawerRow('Project ID', emb.project_id,   true)}
                ${drawerRow('Owner',      emb.owner             )}
            </div>

            <div class="user-drawer-section">
                <div class="user-drawer-section-title">Stats</div>
                ${drawerRow('Invocations', String(emb.invoke_count ?? 0))}
                ${drawerRow('Created',     fmtDatetime(emb.created_at)  )}
            </div>

            <div class="user-drawer-section">
                <div class="user-drawer-section-title">Actions</div>
                <button class="btn btn-danger" id="embDrawerDelBtn"
                        data-emb-id="${esc(emb.id)}">Delete embed</button>
            </div>

        </div>`;

    document.getElementById('embDrawerClose').addEventListener('click', closeEmbedDrawer);
    document.getElementById('embDrawerDelBtn').addEventListener('click', () => {
        openDeleteEmbedConfirm(emb.id, emb.id.slice(0, 8) + '…');
    });
}

// ── Subscriptions panel ───────────────────────────────────────────────────────

let _subs       = [];
let _subSortCol  = 'sort_order';
let _subSortDir  = 'asc';
let _subGroupBy  = '';

const SUB_GROUP_OPTIONS = [
    { value: '',               label: 'None'    },
    { value: 'billing_period', label: 'Billing' },
];

const SUB_COLS = [
    { key: 'display_name',   label: 'Display Name',  minWidth: '110px' },
    { key: 'name',           label: 'Key',           minWidth: '120px' },
    { key: 'billing_period', label: 'Billing',       minWidth: '80px'  },
    { key: 'sort_order',     label: 'Order',         minWidth: '60px'  },
    { key: 'price',          label: 'Price / mo',    minWidth: '90px'  },
    { key: 'user_count',     label: 'Users',         minWidth: '65px'  },
    { key: 'modified_at',    label: 'Modified',      minWidth: '110px' },
    { key: '_actions',       label: '',              minWidth: '80px',  noSort: true },
];

/**
 * Extracts a flat sortable value for virtual subscription columns.
 * @param {string} col - Column key.
 * @param {object} sub - Subscription tier object.
 * @returns {*} Sortable value for the column.
 */
function _subVal(col, sub) {
    if (col === 'display_name') return sub.features?.display_name ?? sub.name;
    if (col === 'sort_order')   return sub.features?.sort_order ?? 0;
    if (col === 'price')        return sub.features?.price_usd_month ?? 0;
    return sub[col];
}

/**
 * Renders a subscriptions table cell as an HTML string for the given column key.
 * @param {string} col - Column key string.
 * @param {object} sub - Subscription tier data object.
 * @returns {string} Inner HTML for the cell.
 */
function fmtSubCell(col, sub) {
    if (col === 'display_name') {
        const dn = sub.features?.display_name;
        return dn ? `<span>${esc(dn)}</span>` : '<span class="admin-cell-muted">—</span>';
    }
    if (col === 'sort_order') {
        const v = sub.features?.sort_order;
        return v != null ? `<span class="admin-cell-mono">${v}</span>` : '<span class="admin-cell-muted">—</span>';
    }
    if (col === 'price') {
        const p = sub.features?.price_usd_month;
        return p != null ? `<span class="admin-cell-mono">$${Number(p).toFixed(2)}</span>`
                         : '<span class="admin-cell-muted">—</span>';
    }
    if (col === 'user_count') {
        return `<span class="admin-cell-mono">${sub.user_count ?? 0}</span>`;
    }
    if (col === 'billing_period') {
        const v = sub[col];
        return v ? `<span class="admin-badge admin-badge--active" style="text-transform:none;letter-spacing:0">${esc(v)}</span>`
                 : '<span class="admin-cell-muted">—</span>';
    }
    if (col === 'modified_at') {
        return `<span title="${esc(sub.modified_at ?? '')}">${fmtDate(sub.modified_at)}</span>`;
    }
    if (col === '_actions') {
        return `<button class="btn btn-danger admin-btn-sm" data-sub-action="delete"
                        data-sub-id="${esc(sub.id)}"
                        data-sub-label="${esc(sub.features?.display_name ?? sub.name)}">Delete</button>`;
    }
    const v = sub[col];
    if (!v && v !== 0) return '<span class="admin-cell-muted">—</span>';
    return `<span>${esc(v)}</span>`;
}

/**
 * Returns a sorted copy of the _subs array based on the current sort column and direction.
 * @returns {object[]} Sorted array of subscription tier objects.
 */
function sortedSubs() {
    return [..._subs].sort((a, b) => {
        const cmp = cmpValues(_subVal(_subSortCol, a), _subVal(_subSortCol, b));
        return _subSortDir === 'asc' ? cmp : -cmp;
    });
}

/** Rebuilds and injects the subscriptions table HTML, then wires up interaction handlers. */
function renderSubsTable() {
    const wrap  = document.getElementById('adminSubsTableWrap');
    const count = document.getElementById('adminSubsCount');
    count.textContent = `${_subs.length} tier${_subs.length !== 1 ? 's' : ''}`;

    const sorted = sortedSubs();

    const thead = SUB_COLS.map(c => {
        if (c.noSort) return `<th style="min-width:${c.minWidth}"></th>`;
        const active = c.key === _subSortCol;
        const arrow  = active ? (_subSortDir === 'asc' ? ' ↑' : ' ↓') : '';
        return `<th class="${active ? 'sorted' : ''}" data-col="${c.key}" style="min-width:${c.minWidth}">${c.label}${arrow}</th>`;
    }).join('');

    let tbody = '';
    if (_subGroupBy) {
        const groups = new Map();
        for (const sub of sorted) {
            const label = _subVal(_subGroupBy, sub) || '—';
            if (!groups.has(label)) groups.set(label, []);
            groups.get(label).push(sub);
        }
        for (const [label, rows] of groups) {
            tbody += `<tr class="admin-group-row"><td colspan="${SUB_COLS.length}">${esc(String(label))}&ensp;·&ensp;${rows.length}</td></tr>`;
            for (const sub of rows) {
                tbody += `<tr class="admin-table-row" data-sub-id="${esc(sub.id)}">${SUB_COLS.map(c => `<td>${fmtSubCell(c.key, sub)}</td>`).join('')}</tr>`;
            }
        }
    } else {
        for (const sub of sorted) {
            tbody += `<tr class="admin-table-row" data-sub-id="${esc(sub.id)}">${SUB_COLS.map(c => `<td>${fmtSubCell(c.key, sub)}</td>`).join('')}</tr>`;
        }
    }
    if (!tbody) tbody = `<tr><td colspan="${SUB_COLS.length}" class="admin-table-empty">No subscription tiers found.</td></tr>`;

    wrap.innerHTML = `
        <table class="admin-table">
            <thead><tr>${thead}</tr></thead>
            <tbody>${tbody}</tbody>
        </table>`;

    wrap.querySelectorAll('thead th[data-col]').forEach(th => {
        th.addEventListener('click', () => {
            const col = th.dataset.col;
            _subSortDir = (_subSortCol === col && _subSortDir === 'asc') ? 'desc' : 'asc';
            _subSortCol = col;
            renderSubsTable();
        });
    });

    if (_drawerSub) _setSubRowSelected(_drawerSub?.id ?? null);
}

/** Fetches the subscription tier list from the API and re-renders the table. */
async function fetchSubs() {
    const btn = document.getElementById('adminSubsRefreshBtn');
    btn.disabled = true;
    btn.textContent = 'Refreshing…';
    try {
        const headers = {};
        if (authToken) headers['X-Auth-Token'] = authToken;
        const resp = await fetch('/api/admin/subscriptions', { headers });
        if (!resp.ok) throw new Error(resp.status);
        _subs = await resp.json();
        renderSubsTable();
    } catch {
        document.getElementById('adminSubsCount').textContent = 'Failed to load tiers.';
    } finally {
        btn.disabled = false;
        btn.textContent = 'Refresh';
    }
}

/** Builds and injects the subscriptions panel toolbar and table into the DOM, then fetches data. */
function initSubsPanel() {
    const section = document.getElementById('tab-subscriptions');
    section.innerHTML = '<h2 class="account-panel-title">Subscriptions</h2>';

    const toolbar = document.createElement('div');
    toolbar.className = 'admin-toolbar';

    const countEl = document.createElement('span');
    countEl.id = 'adminSubsCount';
    countEl.className = 'admin-row-count';
    countEl.textContent = 'Loading…';

    const groupLabelEl = document.createElement('label');
    groupLabelEl.className = 'admin-toolbar-label';
    groupLabelEl.textContent = 'Group by';

    const groupSelect = document.createElement('select');
    groupSelect.className = 'admin-group-select';
    SUB_GROUP_OPTIONS.forEach(opt => {
        const o = document.createElement('option');
        o.value = opt.value;
        o.textContent = opt.label;
        groupSelect.appendChild(o);
    });
    groupSelect.addEventListener('change', () => {
        _subGroupBy = groupSelect.value;
        renderSubsTable();
    });

    const newBtn = document.createElement('button');
    newBtn.className = 'btn btn-primary';
    newBtn.textContent = '+ New Tier';
    newBtn.addEventListener('click', () => openSubDrawer(null));

    const refreshBtn = document.createElement('button');
    refreshBtn.id = 'adminSubsRefreshBtn';
    refreshBtn.className = 'btn btn-ghost';
    refreshBtn.textContent = 'Refresh';
    refreshBtn.addEventListener('click', fetchSubs);

    toolbar.appendChild(countEl);
    toolbar.appendChild(groupLabelEl);
    toolbar.appendChild(groupSelect);
    toolbar.appendChild(newBtn);
    toolbar.appendChild(refreshBtn);

    const tableWrap = document.createElement('div');
    tableWrap.id = 'adminSubsTableWrap';
    tableWrap.className = 'admin-table-wrap';

    tableWrap.addEventListener('click', (e) => {
        const btn = e.target.closest('button[data-sub-action]');
        if (btn) {
            if (btn.disabled) return;
            if (btn.dataset.subAction === 'delete') {
                openDeleteSubConfirm(btn.dataset.subId, btn.dataset.subLabel);
            }
            return;
        }
        const row = e.target.closest('tr.admin-table-row[data-sub-id]');
        if (row) openSubDrawer(row.dataset.subId);
    });

    section.appendChild(toolbar);
    section.appendChild(tableWrap);
    fetchSubs();
}

/**
 * Opens the delete confirmation dialog for a subscription tier.
 * @param {string} tierId - ID of the tier to delete.
 * @param {string} label - Display name of the tier shown in the confirmation message.
 */
function openDeleteSubConfirm(tierId, label) {
    const sub = _subs.find(s => s.id === tierId);
    const userWarn = sub?.user_count
        ? ` ${sub.user_count} user(s) are currently on this tier and will lose their subscription.`
        : '';
    _confirmMsg.textContent = `Permanently delete the "${label}" tier?${userWarn} This cannot be undone.`;
    _confirmOverlay.classList.add('open');
    _confirmOkBtn.onclick = () => {
        closeConfirm();
        doDeleteSub(tierId);
    };
}

/**
 * Sends a DELETE request for the given subscription tier and removes it from the local list.
 * @param {string} tierId - ID of the tier to delete.
 */
async function doDeleteSub(tierId) {
    try {
        const headers = { 'Content-Type': 'application/json' };
        if (authToken) headers['X-Auth-Token'] = authToken;
        const resp = await fetch(`/api/admin/subscriptions/${encodeURIComponent(tierId)}`, {
            method: 'DELETE', headers,
        });
        if (!resp.ok) {
            const err = await resp.json().catch(() => ({}));
            alert(err.error || 'Delete failed.');
            return;
        }
        _subs = _subs.filter(s => s.id !== tierId);
        if (_drawerSub?.id === tierId) closeSubDrawer();
        renderSubsTable();
    } catch {
        // Silently ignore
    }
}

// ── Subscription edit drawer ──────────────────────────────────────────────────

let _drawerSub = null;   // null = new, object = editing existing
let _subIsNew  = false;

const _subDrawerOverlay = document.getElementById('subDrawerOverlay');
const _subDrawerPanel   = document.getElementById('subDrawerPanel');

/**
 * Applies or clears the selected-row highlight in the subscriptions table.
 * @param {string|null} subId - Subscription tier ID to highlight, or null to clear.
 */
function _setSubRowSelected(subId) {
    document.querySelectorAll('#adminSubsTableWrap tr.admin-table-row').forEach(r => {
        r.classList.toggle('admin-table-row--selected', !!subId && r.dataset.subId === subId);
    });
}

/**
 * Opens the subscription edit drawer for an existing tier or for creating a new one.
 * @param {string|null} subId - ID of the subscription tier to edit, or null to create a new tier.
 */
function openSubDrawer(subId) {
    if (_drawerUser)    closeDrawer();
    if (_drawerProject) closeProjectDrawer();
    if (_drawerFolder)  closeFolderDrawer();
    if (_drawerEmbed)   closeEmbedDrawer();

    _subIsNew  = subId === null;
    _drawerSub = _subIsNew ? null : (_subs.find(s => s.id === subId) ?? null);

    renderSubDrawerContent();
    _subDrawerOverlay.classList.add('open');
    _setSubRowSelected(_subIsNew ? null : subId);
}

/** Closes the subscription edit drawer and clears the selection highlight. */
function closeSubDrawer() {
    _subDrawerOverlay.classList.remove('open');
    _drawerSub = null;
    _setSubRowSelected(null);
}

/** Re-renders the subscription edit drawer panel from _drawerSub. */
function renderSubDrawerContent() {
    const sub = _drawerSub;

    const title     = _subIsNew ? 'New Tier' : (sub?.features?.display_name ?? sub?.name ?? 'Edit Tier');
    const name      = sub?.name           ?? '';
    const desc      = sub?.description    ?? '';
    const billing   = sub?.billing_period ?? 'monthly';
    const featJson  = JSON.stringify(sub?.features ?? {}, null, 2);
    const userCount = sub?.user_count ?? 0;

    _subDrawerPanel.innerHTML = `
        <div class="user-drawer-header">
            <div class="user-drawer-identity" style="padding-top:0.1rem">
                <div class="user-drawer-name">${esc(title)}</div>
                ${!_subIsNew ? `<div class="user-drawer-email">${userCount} user${userCount !== 1 ? 's' : ''}</div>` : ''}
            </div>
            <button class="user-drawer-close" id="subDrawerClose" title="Close">
                <span class="icon icon-close" style="width:12px;height:12px;"></span>
            </button>
        </div>

        <div class="user-drawer-body">

            <div class="user-drawer-section">
                <div class="user-drawer-section-title">Details</div>

                <div class="sub-form-group">
                    <label class="sub-form-label" for="subFormName">Name (key)</label>
                    <input class="sub-form-input" id="subFormName" type="text"
                           value="${esc(name)}" placeholder="e.g. pro_monthly">
                </div>

                <div class="sub-form-group">
                    <label class="sub-form-label" for="subFormDesc">Description</label>
                    <input class="sub-form-input" id="subFormDesc" type="text"
                           value="${esc(desc)}" placeholder="Short description">
                </div>

                <div class="sub-form-group">
                    <label class="sub-form-label" for="subFormBilling">Billing Period</label>
                    <select class="sub-form-input" id="subFormBilling">
                        <option value="monthly" ${billing === 'monthly' ? 'selected' : ''}>Monthly</option>
                        <option value="yearly"  ${billing === 'yearly'  ? 'selected' : ''}>Yearly</option>
                    </select>
                </div>
            </div>

            <div class="user-drawer-section">
                <div class="user-drawer-section-title">Features (JSON)</div>
                <textarea class="sub-features-textarea" id="subFormFeatures">${esc(featJson)}</textarea>
                <p class="sub-form-error" id="subFormError"></p>
            </div>

            <div class="user-drawer-section">
                <div class="user-drawer-section-title">Actions</div>
                <div style="display:flex;gap:0.5rem;flex-wrap:wrap;align-items:center">
                    <button class="btn btn-primary" id="subFormSave">
                        ${_subIsNew ? 'Create' : 'Save changes'}
                    </button>
                    <button class="btn btn-ghost" id="subFormCancel">Cancel</button>
                    ${!_subIsNew ? `<button class="btn btn-danger" id="subFormDelete"
                                           data-sub-id="${esc(sub.id)}"
                                           data-sub-label="${esc(sub.features?.display_name ?? sub.name)}">
                        Delete tier
                    </button>` : ''}
                </div>
                <p class="sub-form-error" id="subSaveError"></p>
            </div>

        </div>`;

    document.getElementById('subDrawerClose').addEventListener('click', closeSubDrawer);
    document.getElementById('subFormCancel').addEventListener('click', closeSubDrawer);

    document.getElementById('subFormSave').addEventListener('click', () => doSaveSub());

    const delBtn = document.getElementById('subFormDelete');
    if (delBtn) {
        delBtn.addEventListener('click', () => {
            openDeleteSubConfirm(delBtn.dataset.subId, delBtn.dataset.subLabel);
        });
    }
}

/** Validates the subscription form and POSTs or PATCHes the tier via the API. */
async function doSaveSub() {
    const nameVal    = document.getElementById('subFormName').value.trim();
    const descVal    = document.getElementById('subFormDesc').value.trim();
    const billingVal = document.getElementById('subFormBilling').value;
    const featRaw    = document.getElementById('subFormFeatures').value;
    const featEl     = document.getElementById('subFormFeatures');
    const errorEl    = document.getElementById('subFormError');
    const saveErrEl  = document.getElementById('subSaveError');

    // Validate JSON
    let featObj;
    try {
        featObj = JSON.parse(featRaw);
        featEl.classList.remove('error');
        errorEl.textContent = '';
    } catch (e) {
        featEl.classList.add('error');
        errorEl.textContent = `Invalid JSON: ${e.message}`;
        return;
    }

    if (!nameVal) {
        saveErrEl.textContent = 'Name is required.';
        return;
    }

    const saveBtn = document.getElementById('subFormSave');
    saveBtn.disabled = true;
    saveBtn.textContent = 'Saving…';
    saveErrEl.textContent = '';

    try {
        const headers = { 'Content-Type': 'application/json' };
        if (authToken) headers['X-Auth-Token'] = authToken;

        const body = JSON.stringify({
            name: nameVal, description: descVal,
            billing_period: billingVal, features: featObj,
        });

        let resp;
        if (_subIsNew) {
            resp = await fetch('/api/admin/subscriptions', { method: 'POST', headers, body });
        } else {
            resp = await fetch(`/api/admin/subscriptions/${encodeURIComponent(_drawerSub.id)}`,
                               { method: 'PATCH', headers, body });
        }

        if (!resp.ok) {
            const err = await resp.json().catch(() => ({}));
            saveErrEl.textContent = err.error || 'Save failed.';
            return;
        }

        const saved = await resp.json();
        saved.user_count = _subIsNew ? 0 : (_drawerSub?.user_count ?? 0);

        if (_subIsNew) {
            _subs.push(saved);
        } else {
            const idx = _subs.findIndex(s => s.id === saved.id);
            if (idx !== -1) _subs[idx] = saved;
        }

        _drawerSub = saved;
        _subIsNew  = false;
        renderSubsTable();
        renderSubDrawerContent();          // refresh header (new name/user count)
        _setSubRowSelected(saved.id);

    } catch {
        saveErrEl.textContent = 'Unexpected error. Please try again.';
    } finally {
        const btn = document.getElementById('subFormSave');
        if (btn) { btn.disabled = false; btn.textContent = _subIsNew ? 'Create' : 'Save changes'; }
    }
}

// ── Click-outside to close drawers ────────────────────────────────────────────

document.addEventListener('click', (e) => {
    // Clicks inside an open drawer or on a table row are handled elsewhere
    if (e.target.closest('tr.admin-table-row')) return;

    if (_drawerUser    && !_drawerPanel.contains(e.target))     closeDrawer();
    if (_drawerProject && !_projDrawerPanel.contains(e.target)) closeProjectDrawer();
    if (_drawerFolder  && !_fldDrawerPanel.contains(e.target))  closeFolderDrawer();
    if (_drawerEmbed   && !_embDrawerPanel.contains(e.target))  closeEmbedDrawer();
    if (_drawerSub     && !_subDrawerPanel.contains(e.target))  closeSubDrawer();
});

// ── Delete user confirm dialog ────────────────────────────────────────────────

const _confirmOverlay = document.getElementById('adminConfirmOverlay');
const _confirmMsg     = document.getElementById('adminConfirmMsg');
const _confirmOkBtn   = document.getElementById('adminConfirmOk');
const _confirmCancelBtn = document.getElementById('adminConfirmCancel');

let _confirmCallback = null;

_confirmCancelBtn.addEventListener('click', closeConfirm);
_confirmOverlay.addEventListener('click', (e) => {
    if (e.target === _confirmOverlay) closeConfirm();
});

/**
 * Opens the delete confirmation dialog for a user account.
 * @param {string} userId - ID of the user to delete.
 * @param {string} displayLabel - Display name or email shown in the confirmation message.
 */
function openDeleteConfirm(userId, displayLabel) {
    _confirmMsg.textContent = `Permanently delete "${displayLabel}"? This cannot be undone. Their projects and folders will remain but become ownerless.`;
    _confirmCallback = () => doDeleteUser(userId);
    _confirmOverlay.classList.add('open');

    _confirmOkBtn.onclick = () => {
        closeConfirm();
        if (_confirmCallback) _confirmCallback();
        _confirmCallback = null;
    };
}

/** Closes the confirmation dialog and clears any pending callback. */
function closeConfirm() {
    _confirmOverlay.classList.remove('open');
    _confirmCallback = null;
}

/**
 * Deletes a user account, removes them from _users, closes the drawer, and re-renders the table.
 * @param {string} userId - ID of the user to delete.
 */
async function doDeleteUser(userId) {
    try {
        const headers = { 'Content-Type': 'application/json' };
        if (authToken) headers['X-Auth-Token'] = authToken;
        const resp = await fetch(`/api/admin/users/${encodeURIComponent(userId)}`, {
            method: 'DELETE', headers,
        });
        if (!resp.ok) throw new Error(resp.status);

        _users = _users.filter(u => u.id !== userId);
        if (_drawerUser?.id === userId) closeDrawer();
        renderTable();
    } catch {
        // Silently ignore — user can retry
    }
}

// ── Analytics panel ───────────────────────────────────────────────────────────

const ANALYTICS_METRICS = [
    { key: 'total_users',          label: 'Total Users',             color: '#60a5fa' },
    { key: 'active_users_30d',     label: 'Active Users (30d)',      color: '#34d399' },
    { key: 'total_subscribers',    label: 'Paid Subscribers',        color: '#2dd4bf' },
    { key: 'total_projects',       label: 'Total Projects',          color: '#a78bfa' },
    { key: 'transcribed_projects', label: 'Transcribed Projects',    color: '#fb923c' },
    { key: 'total_audio_seconds',  label: 'Audio Duration (hrs)',    color: '#f472b6' },
    { key: 'total_embeds',         label: 'Total Embeds',            color: '#facc15' },
    { key: 'total_embed_views',    label: 'Embed Views',             color: '#38bdf8' },
    { key: 'storage_bytes',        label: 'Storage',                 color: '#4ade80' },
    { key: 'mrr',                  label: 'MRR ($)',                 color: '#f87171' },
];

let _analyticsSnapshots  = [];
let _analyticsChart      = null;
let _analyticsVisibleKeys = new Set(ANALYTICS_METRICS.map(m => m.key));
let _analyticsRange      = 'all';   // '7d' | '30d' | '90d' | '1y' | 'all' | 'custom'
let _analyticsUnit       = 'auto';  // 'auto' | 'day' | 'week' | 'month'
let _analyticsStartDate  = '';      // ISO date string (custom range)
let _analyticsEndDate    = '';      // ISO date string (custom range)

const ANALYTICS_RANGES = [
    { value: '7d',    label: 'Last 7 days' },
    { value: '30d',   label: 'Last 30 days' },
    { value: '90d',   label: 'Last 90 days' },
    { value: '1y',    label: 'Last year' },
    { value: 'all',   label: 'All time' },
    { value: 'custom',label: 'Custom…' },
];

const ANALYTICS_UNITS = [
    { value: 'auto',  label: 'Auto' },
    { value: 'day',   label: 'Day' },
    { value: 'week',  label: 'Week' },
    { value: 'month', label: 'Month' },
];

/**
 * Returns the {min, max} Date objects for the current analytics range selection.
 * @returns {{ min: Date|null, max: Date|null }} Axis bounds; null means unbounded.
 */
function _getAnalyticsAxisRange() {
    const now = new Date();
    switch (_analyticsRange) {
        case '7d':    return { min: new Date(now - 7   * 86400000), max: now };
        case '30d':   return { min: new Date(now - 30  * 86400000), max: now };
        case '90d':   return { min: new Date(now - 90  * 86400000), max: now };
        case '1y':    return { min: new Date(now - 365 * 86400000), max: now };
        case 'all':   return { min: null, max: null };
        case 'custom': return {
            min: _analyticsStartDate ? new Date(_analyticsStartDate) : null,
            max: _analyticsEndDate   ? new Date(_analyticsEndDate + 'T23:59:59') : null,
        };
    }
    return { min: null, max: null };
}

/**
 * Returns analytics snapshots filtered to the current date range.
 * @returns {object[]} Filtered array of snapshot objects.
 */
function _getFilteredSnapshots() {
    const { min, max } = _getAnalyticsAxisRange();
    return _analyticsSnapshots.filter(s => {
        const d = new Date(s.recorded_at);
        if (min && d < min) return false;
        if (max && d > max) return false;
        return true;
    });
}

/**
 * Formats an analytics metric value for display on a summary card.
 * @param {string} key - Metric key (e.g. 'total_audio_seconds', 'storage_bytes').
 * @param {number|null} value - Raw metric value.
 * @returns {string} Human-readable formatted value.
 */
function _fmtAnalyticsCard(key, value) {
    if (value == null) return '—';
    switch (key) {
        case 'total_audio_seconds': {
            const h = value / 3600;
            return h >= 1 ? h.toFixed(1) + ' hr' : (value / 60).toFixed(1) + ' min';
        }
        case 'storage_bytes':        return _fmtBytes(value);
        case 'mrr':
        case 'prev_month_mrr':
        case 'est_next_month_mrr':   return '$' + Number(value).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
        default:                     return Number(value).toLocaleString();
    }
}

/**
 * Formats a byte count as a human-readable string (B, KB, MB, GB).
 * @param {number} b - Byte count.
 * @returns {string} Formatted size string.
 */
function _fmtBytes(b) {
    if (b >= 1e9) return (b / 1e9).toFixed(2) + ' GB';
    if (b >= 1e6) return (b / 1e6).toFixed(1) + ' MB';
    if (b >= 1e3) return (b / 1e3).toFixed(0) + ' KB';
    return b + ' B';
}

/**
 * Converts a hex colour string and alpha to an rgba() CSS string.
 * @param {string} hex - Six-digit hex colour (e.g. '#60a5fa').
 * @param {number} alpha - Alpha value between 0 and 1.
 * @returns {string} CSS rgba() colour string.
 */
function _hexToRgba(hex, alpha) {
    const r = parseInt(hex.slice(1, 3), 16);
    const g = parseInt(hex.slice(3, 5), 16);
    const b = parseInt(hex.slice(5, 7), 16);
    return `rgba(${r},${g},${b},${alpha})`;
}

/** Synchronises the metric dropdown value to reflect the current _analyticsVisibleKeys state. */
function _syncMetricDropdown() {
    const sel = document.getElementById('adminAnalyticsMetricSelect');
    if (!sel) return;
    const allVisible = ANALYTICS_METRICS.every(m => _analyticsVisibleKeys.has(m.key));
    const isSingle   = _analyticsVisibleKeys.size === 1;
    let customOpt    = sel.querySelector('option[value="custom"]');
    if (!allVisible && !isSingle) {
        if (!customOpt) {
            customOpt = document.createElement('option');
            customOpt.value = 'custom';
            customOpt.textContent = 'Custom';
            sel.appendChild(customOpt);
        }
        sel.value = 'custom';
    } else {
        customOpt?.remove();
        sel.value = allVisible ? 'all' : [..._analyticsVisibleKeys][0];
    }
}

/**
 * Toggles visibility of a metric on the analytics chart.
 * @param {string} key - Metric key to toggle.
 */
function _toggleMetric(key) {
    if (_analyticsVisibleKeys.has(key)) {
        if (_analyticsVisibleKeys.size <= 1) return; // keep at least one visible
        _analyticsVisibleKeys.delete(key);
    } else {
        _analyticsVisibleKeys.add(key);
    }
    _syncMetricDropdown();
    const latest = _analyticsSnapshots[_analyticsSnapshots.length - 1];
    if (latest) renderAnalyticsCards(latest.data);
    renderAnalyticsChart();
}

/**
 * Highlights a single metric dataset on the chart, dimming all others.
 * @param {string} key - Metric key to highlight.
 */
function _highlightChartMetric(key) {
    if (!_analyticsChart || _analyticsVisibleKeys.size <= 1) return;
    const visibleMetrics = ANALYTICS_METRICS.filter(m => _analyticsVisibleKeys.has(m.key));
    _analyticsChart.data.datasets.forEach((ds, i) => {
        const m = visibleMetrics[i];
        if (!m) return;
        const active = m.key === key;
        ds.borderColor     = active ? m.color : _hexToRgba(m.color, 0.15);
        ds.backgroundColor = active ? m.color + '33' : 'transparent';
        ds.borderWidth     = active ? 3 : 1;
        ds.order           = active ? 0 : 1;
    });
    _analyticsChart.update('none');
}

/** Resets all chart datasets to their default colours after a hover highlight. */
function _resetChartHighlight() {
    if (!_analyticsChart) return;
    const visibleMetrics = ANALYTICS_METRICS.filter(m => _analyticsVisibleKeys.has(m.key));
    _analyticsChart.data.datasets.forEach((ds, i) => {
        const m = visibleMetrics[i];
        if (!m) return;
        ds.borderColor     = m.color;
        ds.backgroundColor = visibleMetrics.length === 1 ? m.color + '33' : m.color + '22';
        ds.borderWidth     = 2;
        ds.order           = 0;
    });
    _analyticsChart.update('none');
}

/**
 * Converts a raw metric value to the unit used on the chart axis.
 * @param {string} key - Metric key.
 * @param {number} value - Raw metric value.
 * @returns {number} Converted value (e.g. seconds → hours, bytes → GB).
 */
function _toChartValue(key, value) {
    switch (key) {
        case 'total_audio_seconds': return value / 3600;
        case 'storage_bytes':       return value / 1e9;
        default:                    return value;
    }
}

/** Builds and injects the analytics panel UI (toolbar, cards, chart, subscribers table) into the DOM. */
function initAnalyticsPanel() {
    const section = document.getElementById('tab-analytics');

    const toolbar = document.createElement('div');
    toolbar.className = 'admin-toolbar';

    const statusEl = document.createElement('span');
    statusEl.id = 'adminAnalyticsStatus';
    statusEl.className = 'admin-row-count';
    statusEl.textContent = 'Loading…';

    const refreshBtn = document.createElement('button');
    refreshBtn.id = 'adminAnalyticsRefreshBtn';
    refreshBtn.className = 'btn btn-ghost';
    refreshBtn.textContent = 'Refresh';
    refreshBtn.addEventListener('click', fetchAnalytics);

    const collectBtn = document.createElement('button');
    collectBtn.id = 'adminAnalyticsCollectBtn';
    collectBtn.className = 'btn btn-ghost';
    collectBtn.textContent = 'Collect Now';
    collectBtn.addEventListener('click', collectAnalyticsNow);

    toolbar.appendChild(statusEl);
    toolbar.appendChild(refreshBtn);
    toolbar.appendChild(collectBtn);

    const cardsGrid = document.createElement('div');
    cardsGrid.id = 'adminAnalyticsCards';
    cardsGrid.className = 'analytics-cards';

    const chartSection = document.createElement('div');
    chartSection.className = 'analytics-chart-section';

    // ── Row 1: metric / range / unit selects ──────────────────────────────────
    const chartToolbar = document.createElement('div');
    chartToolbar.className = 'admin-toolbar';

    /**
     * Creates a labelled select element and appends it to the chart toolbar.
     * @param {string} id - Element ID for the select.
     * @param {string} labelText - Label text displayed before the select.
     * @param {Array<{value:string,label:string}>} options - Option list.
     * @param {string} current - Currently selected value.
     * @param {function} onChange - Callback invoked with the new value on change.
     */
    function makeSelect(id, labelText, options, current, onChange) {
        const lbl = document.createElement('label');
        lbl.className = 'admin-toolbar-label';
        lbl.textContent = labelText;
        const sel = document.createElement('select');
        sel.id = id;
        sel.className = 'admin-group-select';
        options.forEach(o => {
            const opt = document.createElement('option');
            opt.value = o.value;
            opt.textContent = o.label;
            opt.selected = o.value === current;
            sel.appendChild(opt);
        });
        sel.addEventListener('change', () => onChange(sel.value));
        chartToolbar.appendChild(lbl);
        chartToolbar.appendChild(sel);
    }

    makeSelect('adminAnalyticsMetricSelect', 'Metric',
        [{ value: 'all', label: 'All Metrics' }, ...ANALYTICS_METRICS.map(m => ({ value: m.key, label: m.label }))],
        'all',
        v => {
            if (v === 'all') {
                _analyticsVisibleKeys = new Set(ANALYTICS_METRICS.map(m => m.key));
            } else if (v !== 'custom') {
                _analyticsVisibleKeys = new Set([v]);
            }
            _refreshAnalyticsView();
        }
    );
    makeSelect('adminAnalyticsRangeSelect', 'Range', ANALYTICS_RANGES, _analyticsRange, v => {
        _analyticsRange = v;
        customDateRow.style.display = v === 'custom' ? 'flex' : 'none';
        renderAnalyticsChart();
    });
    makeSelect('adminAnalyticsUnitSelect', 'Unit', ANALYTICS_UNITS, _analyticsUnit, v => {
        _analyticsUnit = v;
        renderAnalyticsChart();
    });

    // ── Row 2: custom date range (hidden unless range === 'custom') ────────────
    const customDateRow = document.createElement('div');
    customDateRow.className = 'analytics-date-row';
    customDateRow.style.display = _analyticsRange === 'custom' ? 'flex' : 'none';

    /**
     * Creates a labelled date input and appends it to the custom date row.
     * @param {string} id - Element ID for the input.
     * @param {string} labelText - Label text displayed before the input.
     * @param {string} stateKey - Either 'start' or 'end' to determine which state variable to update.
     */
    function makeDateInput(id, labelText, stateKey) {
        const lbl = document.createElement('label');
        lbl.className = 'admin-toolbar-label';
        lbl.textContent = labelText;
        lbl.htmlFor = id;
        const inp = document.createElement('input');
        inp.type = 'date';
        inp.id = id;
        inp.className = 'analytics-date-input';
        inp.value = stateKey === 'start' ? _analyticsStartDate : _analyticsEndDate;
        inp.addEventListener('change', () => {
            if (stateKey === 'start') _analyticsStartDate = inp.value;
            else                      _analyticsEndDate   = inp.value;
            renderAnalyticsChart();
        });
        customDateRow.appendChild(lbl);
        customDateRow.appendChild(inp);
    }
    makeDateInput('adminAnalyticsFrom', 'From', 'start');
    makeDateInput('adminAnalyticsTo',   'To',   'end');

    const canvasWrap = document.createElement('div');
    canvasWrap.className = 'analytics-chart-wrap';

    const canvas = document.createElement('canvas');
    canvas.id = 'adminAnalyticsChart';
    canvasWrap.appendChild(canvas);

    chartSection.appendChild(chartToolbar);
    chartSection.appendChild(customDateRow);
    chartSection.appendChild(canvasWrap);

    const subscribersSection = document.createElement('div');
    subscribersSection.id = 'adminAnalyticsSubscribers';
    subscribersSection.className = 'analytics-subscribers-section';

    const revenueHero = document.createElement('div');
    revenueHero.id = 'adminAnalyticsRevenueHero';
    revenueHero.className = 'analytics-revenue-hero';

    section.appendChild(toolbar);
    section.appendChild(revenueHero);
    section.appendChild(cardsGrid);
    section.appendChild(chartSection);
    section.appendChild(subscribersSection);

    fetchAnalytics();
}

/** Fetches all analytics snapshots from the API and triggers a full re-render. */
async function fetchAnalytics() {
    const statusEl   = document.getElementById('adminAnalyticsStatus');
    const refreshBtn = document.getElementById('adminAnalyticsRefreshBtn');
    if (refreshBtn) { refreshBtn.disabled = true; refreshBtn.textContent = 'Refreshing…'; }

    try {
        const headers = {};
        if (authToken) headers['X-Auth-Token'] = authToken;
        const resp = await fetch('/api/admin/analytics', { headers });
        if (!resp.ok) throw new Error(resp.status);
        _analyticsSnapshots = await resp.json();
        renderAnalytics();
    } catch {
        if (statusEl) statusEl.textContent = 'Failed to load analytics.';
    } finally {
        if (refreshBtn) { refreshBtn.disabled = false; refreshBtn.textContent = 'Refresh'; }
    }
}

/** Triggers an immediate analytics collection via the API then refreshes the display. */
async function collectAnalyticsNow() {
    const statusEl   = document.getElementById('adminAnalyticsStatus');
    const collectBtn = document.getElementById('adminAnalyticsCollectBtn');
    if (collectBtn) { collectBtn.disabled = true; collectBtn.textContent = 'Collecting…'; }

    try {
        const headers = { 'Content-Type': 'application/json' };
        if (authToken) headers['X-Auth-Token'] = authToken;
        const resp = await fetch('/api/admin/analytics/collect', { method: 'POST', headers });
        if (!resp.ok) throw new Error(resp.status);
        await fetchAnalytics();
    } catch {
        if (statusEl) statusEl.textContent = 'Collection failed.';
    } finally {
        if (collectBtn) { collectBtn.disabled = false; collectBtn.textContent = 'Collect Now'; }
    }
}

/** Updates the analytics status bar and triggers the full analytics view refresh. */
function renderAnalytics() {
    const statusEl = document.getElementById('adminAnalyticsStatus');

    if (!_analyticsSnapshots.length) {
        if (statusEl) statusEl.textContent = 'No data yet — run collect-analytics to generate the first snapshot.';
        const cards = document.getElementById('adminAnalyticsCards');
        if (cards) cards.innerHTML = '';
        return;
    }

    const latest = _analyticsSnapshots[_analyticsSnapshots.length - 1];
    const d      = new Date(latest.recorded_at);
    const when   = d.toLocaleDateString([], { month: 'short', day: 'numeric', year: 'numeric' })
                 + ' · ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
    const count  = _analyticsSnapshots.length;
    if (statusEl) statusEl.textContent =
        `Last snapshot: ${when} — ${count} snapshot${count !== 1 ? 's' : ''} total`;

    _refreshAnalyticsView();
}

/** Refreshes all analytics view components (revenue hero, cards, subscribers table, chart). */
function _refreshAnalyticsView() {
    if (!_analyticsSnapshots.length) return;
    const latest = _analyticsSnapshots[_analyticsSnapshots.length - 1];
    renderRevenueHero(latest.data);
    renderAnalyticsCards(latest.data);
    renderSubscribersTable(latest.data);
    renderAnalyticsChart();
}

/**
 * Renders the MRR revenue hero cards from the latest analytics snapshot data.
 * @param {object} data - Analytics snapshot data object containing mrr fields.
 */
function renderRevenueHero(data) {
    const hero = document.getElementById('adminAnalyticsRevenueHero');
    if (!hero) return;

    const fmtUsd = v => v != null
        ? '$' + Number(v).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
        : '—';

    const items = [
        { label: 'Current MRR',          value: fmtUsd(data?.mrr),                color: '#f87171' },
        { label: 'Previous Month MRR',   value: fmtUsd(data?.prev_month_mrr),      color: '#fbbf24' },
        { label: 'Est. Next Month MRR',  value: fmtUsd(data?.est_next_month_mrr),  color: '#c084fc' },
    ];

    hero.innerHTML = items.map((item, i) => `
        <div class="analytics-revenue-card${i === 0 ? ' analytics-revenue-card--primary' : ''}" style="border-color:${item.color}">
            <div class="analytics-revenue-label">${item.label}</div>
            <div class="analytics-revenue-value" style="color:${item.color}">${item.value}</div>
        </div>
    `).join('');
}

/**
 * Renders the subscribers-by-tier breakdown table from the latest analytics snapshot data.
 * @param {object} data - Analytics snapshot data object containing subscribers_by_tier.
 */
function renderSubscribersTable(data) {
    const section = document.getElementById('adminAnalyticsSubscribers');
    if (!section) return;

    const tiers = data?.subscribers_by_tier ?? [];
    if (!tiers.length) { section.innerHTML = ''; return; }

    const totalMrr    = tiers.reduce((s, t) => s + (t.mrr ?? 0), 0);
    const totalSubs   = tiers.reduce((s, t) => s + (t.count ?? 0), 0);
    const fmtUsd = v => '$' + Number(v).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });

    const rows = tiers.map(t => {
        const billingLabel = t.billing_period === 'monthly' ? 'Monthly'
                           : t.billing_period === 'yearly'  ? 'Yearly (÷12 MRR)'
                           : '—';
        const priceLabel = t.monthly_price > 0 ? fmtUsd(t.monthly_price) + '/mo' : '—';
        return `<tr>
            <td>${esc(t.tier)}</td>
            <td class="admin-cell-muted">${billingLabel}</td>
            <td>${t.count.toLocaleString()}</td>
            <td class="admin-cell-muted">${priceLabel}</td>
            <td>${t.mrr > 0 ? fmtUsd(t.mrr) : '—'}</td>
        </tr>`;
    }).join('');

    section.innerHTML = `
        <div class="analytics-section-title">Subscribers by Tier</div>
        <div class="admin-table-wrap analytics-subscribers-table-wrap">
            <table class="admin-table">
                <thead><tr>
                    <th>Tier</th><th>Billing</th><th>Subscribers</th>
                    <th>Monthly Price (MRR basis)</th><th>MRR</th>
                </tr></thead>
                <tbody>
                    ${rows}
                    <tr class="analytics-subscribers-total">
                        <td colspan="2" class="admin-cell-muted">Total</td>
                        <td>${totalSubs.toLocaleString()}</td>
                        <td></td>
                        <td>${fmtUsd(totalMrr)}</td>
                    </tr>
                </tbody>
            </table>
        </div>`;
}

/**
 * Renders the analytics metric summary cards from the latest snapshot data.
 * @param {object} data - Analytics snapshot data object.
 */
function renderAnalyticsCards(data) {
    const grid = document.getElementById('adminAnalyticsCards');
    if (!grid) return;
    grid.innerHTML = '';

    ANALYTICS_METRICS.forEach(m => {
        const visible = _analyticsVisibleKeys.has(m.key);
        const card = document.createElement('div');
        card.className = 'analytics-card' + (visible ? '' : ' analytics-card--disabled');
        card.style.borderColor = visible ? m.color : '';
        card.title = visible ? 'Click to hide' : 'Click to show';

        const labelEl = document.createElement('div');
        labelEl.className = 'analytics-card-label';
        labelEl.textContent = m.label;

        const valueEl = document.createElement('div');
        valueEl.className = 'analytics-card-value';
        valueEl.textContent = _fmtAnalyticsCard(m.key, data?.[m.key]);

        card.appendChild(labelEl);
        card.appendChild(valueEl);
        card.addEventListener('click',      () => _toggleMetric(m.key));
        card.addEventListener('mouseenter', () => _highlightChartMetric(m.key));
        card.addEventListener('mouseleave', () => _resetChartHighlight());
        grid.appendChild(card);
    });
}

/** Destroys and rebuilds the analytics Chart.js line chart from the current snapshots and filters. */
function renderAnalyticsChart() {
    const canvas = document.getElementById('adminAnalyticsChart');
    if (!canvas || !_analyticsSnapshots.length) return;

    const filtered = _getFilteredSnapshots();

    const style     = getComputedStyle(document.documentElement);
    const gridColor = style.getPropertyValue('--border').trim()  || 'rgba(255,255,255,0.1)';
    const textColor = style.getPropertyValue('--muted').trim()   || '#888';
    const surface   = style.getPropertyValue('--surface').trim() || '#1a1a2e';
    const tickFont  = { family: 'IBM Plex Mono', size: 11 };

    if (_analyticsChart) { _analyticsChart.destroy(); _analyticsChart = null; }

    const { min: axisMin, max: axisMax } = _getAnalyticsAxisRange();
    const xAxis = {
        type: 'time',
        time: {
            ...((_analyticsUnit !== 'auto') ? { unit: _analyticsUnit } : {}),
            tooltipFormat: 'MMM d, yyyy',
            displayFormats: { day: 'MMM d', week: 'MMM d', month: 'MMM yyyy', year: 'yyyy' },
        },
        ...(axisMin ? { min: axisMin.toISOString() } : {}),
        ...(axisMax ? { max: axisMax.toISOString() } : {}),
        ticks: { color: textColor, font: tickFont },
        grid:  { color: gridColor },
    };

    const visibleMetrics = ANALYTICS_METRICS.filter(m => _analyticsVisibleKeys.has(m.key));
    const isMulti        = visibleMetrics.length > 1;

    let datasets, yAxis, legendCfg, tooltipCallbacks;

    if (isMulti) {
        const rawByMetric = visibleMetrics.map(m =>
            filtered.map(s => _toChartValue(m.key, s.data?.[m.key] ?? 0))
        );
        datasets = visibleMetrics.map((m, i) => {
            const raw   = rawByMetric[i];
            const lo    = Math.min(...raw);
            const hi    = Math.max(...raw);
            const range = hi - lo || 1;
            return {
                label:            m.label,
                data:             filtered.map((s, j) => ({ x: new Date(s.recorded_at), y: (raw[j] - lo) / range })),
                borderColor:      m.color,
                backgroundColor:  m.color + '22',
                borderWidth:      2,
                pointRadius:      3,
                pointHoverRadius: 5,
                tension:          0.3,
                fill:             false,
            };
        });
        yAxis = {
            min: -0.05, max: 1.05,
            afterBuildTicks: scale => {
                scale.ticks = [0, 0.2, 0.4, 0.6, 0.8, 1.0].map(v => ({ value: v }));
            },
            ticks: { color: textColor, font: tickFont, callback: v => v.toFixed(1) },
            grid:  { color: gridColor },
        };
        legendCfg        = { display: true, labels: { color: textColor, font: tickFont, boxWidth: 12, padding: 10 } };
        tooltipCallbacks = {
            label: ctx => {
                const m    = visibleMetrics[ctx.datasetIndex];
                const snap = filtered[ctx.dataIndex];
                return ` ${m.label}: ${_fmtAnalyticsCard(m.key, snap?.data?.[m.key] ?? null)}`;
            },
        };
    } else {
        const m = visibleMetrics[0];
        datasets = [{
            label:            m.label,
            data:             filtered.map(s => ({ x: new Date(s.recorded_at), y: _toChartValue(m.key, s.data?.[m.key] ?? 0) })),
            borderColor:      m.color,
            backgroundColor:  m.color + '33',
            borderWidth:      2,
            pointRadius:      3,
            pointHoverRadius: 5,
            tension:          0.3,
            fill:             true,
        }];
        yAxis            = { beginAtZero: true, grace: '5%', ticks: { color: textColor, font: tickFont }, grid: { color: gridColor } };
        legendCfg        = { display: false };
        tooltipCallbacks = {};
    }

    _analyticsChart = new Chart(canvas, {
        type: 'line',
        data: { datasets },
        options: {
            responsive:          true,
            maintainAspectRatio: false,
            plugins: {
                legend: legendCfg,
                tooltip: {
                    backgroundColor: surface,
                    borderColor:     gridColor,
                    borderWidth:     1,
                    titleColor:      textColor,
                    bodyColor:       textColor,
                    callbacks:       tooltipCallbacks,
                },
            },
            scales: { x: xAxis, y: yAxis },
        },
    });
}

// ── Dispatch ──────────────────────────────────────────────────────────────────

async function initDispatchPanel() {
    const container = document.getElementById('dispatchPanelContent');

    container.innerHTML = `
        <div class="dispatch-form">
            <div class="sub-form-group">
                <label class="sub-form-label" for="dispatchAudience">Audience</label>
                <div class="dispatch-audience-group">
                    <label class="dispatch-radio-label">
                        <input type="radio" name="dispatchAudience" value="all" checked>
                        All accounts
                    </label>
                    <label class="dispatch-radio-label">
                        <input type="radio" name="dispatchAudience" value="updates_only">
                        Opted-in to updates only
                    </label>
                    <label class="dispatch-radio-label">
                        <input type="radio" name="dispatchAudience" value="admins">
                        Test (Admins)
                    </label>
                </div>
                <div class="dispatch-count" id="dispatchCount">—</div>
            </div>
            <div class="sub-form-group">
                <label class="sub-form-label" for="dispatchSubject">Subject</label>
                <input class="sub-form-input" type="text" id="dispatchSubject" maxlength="200" placeholder="Email subject line">
            </div>
            <div class="sub-form-group">
                <label class="sub-form-label">Format</label>
                <div class="dispatch-audience-group">
                    <label class="dispatch-radio-label">
                        <input type="radio" name="dispatchFormat" value="text" checked>
                        Plain text
                    </label>
                    <label class="dispatch-radio-label">
                        <input type="radio" name="dispatchFormat" value="html">
                        HTML
                    </label>
                </div>
                <div class="dispatch-format-hint" id="dispatchFormatHint"></div>
            </div>
            <div class="dispatch-body-row">
                <div class="sub-form-group dispatch-body-group">
                    <label class="sub-form-label" for="dispatchBody">Message</label>
                    <textarea class="sub-features-textarea dispatch-body" id="dispatchBody"
                        placeholder="Write your message here. Separate paragraphs with a blank line."></textarea>
                </div>
                <div class="sub-form-group dispatch-snippet-group" id="dispatchSnippetGroup" style="display:none;">
                    <label class="sub-form-label">Button snippets</label>
                    <div class="dispatch-snippet-label">Primary (blue)</div>
                    <div class="dispatch-snippet-wrap">
                        <pre class="dispatch-snippet" id="dispatchSnippetPrimary"></pre>
                        <button class="dispatch-snippet-copy" id="dispatchSnippetCopyPrimary" title="Copy">Copy</button>
                    </div>
                    <div class="dispatch-snippet-label">Secondary (grey)</div>
                    <div class="dispatch-snippet-wrap">
                        <pre class="dispatch-snippet" id="dispatchSnippetSecondary"></pre>
                        <button class="dispatch-snippet-copy" id="dispatchSnippetCopySecondary" title="Copy">Copy</button>
                    </div>
                </div>
            </div>
            <div class="dispatch-actions">
                <span class="dispatch-status" id="dispatchStatus"></span>
                <button class="btn btn-primary" id="dispatchSendBtn">Send Email</button>
            </div>
        </div>`;

    async function fetchCount() {
        const audience = container.querySelector('input[name="dispatchAudience"]:checked').value;
        const countEl  = document.getElementById('dispatchCount');
        countEl.textContent = '…';
        try {
            const headers = {};
            if (authToken) headers['X-Auth-Token'] = authToken;
            const resp = await fetch(`/api/admin/dispatch/count?audience=${audience}`, { headers });
            if (resp.ok) {
                const { count } = await resp.json();
                countEl.textContent = `${count} recipient${count !== 1 ? 's' : ''}`;
            } else {
                countEl.textContent = 'Error fetching count';
            }
        } catch {
            countEl.textContent = 'Error fetching count';
        }
    }

    container.querySelectorAll('input[name="dispatchAudience"]').forEach(r => {
        r.addEventListener('change', fetchCount);
    });

    const hintEl        = document.getElementById('dispatchFormatHint');
    const bodyEl        = document.getElementById('dispatchBody');
    const snippetGroup      = document.getElementById('dispatchSnippetGroup');
    const snippetPrimaryEl  = document.getElementById('dispatchSnippetPrimary');
    const snippetSecondaryEl = document.getElementById('dispatchSnippetSecondary');
    const htmlHint          = 'HTML is embedded inside the standard email card. Use inline styles for buttons and links.';
    const textHint          = 'Blank lines become paragraph breaks.';
    const SNIPPET_PRIMARY   = `<p style="margin:0 0 24px;text-align:center;">
  <a href="https://example.com"
     target="_blank"
     style="display:inline-block;padding:11px 24px;background:#3b82f6;
            color:#ffffff;text-decoration:none;border-radius:4px;
            font-family:ui-monospace,'IBM Plex Mono',monospace;
            font-size:13px;font-weight:600;letter-spacing:0.04em;
            mso-padding-alt:0;">
    Button label
  </a>
</p>`;
    const SNIPPET_SECONDARY = `<p style="margin:0 0 24px;text-align:center;">
  <a href="https://example.com"
     target="_blank"
     style="display:inline-block;padding:11px 24px;background:#52525b;
            color:#ffffff;text-decoration:none;border-radius:4px;
            font-family:ui-monospace,'IBM Plex Mono',monospace;
            font-size:13px;font-weight:600;letter-spacing:0.04em;
            mso-padding-alt:0;">
    Button label
  </a>
</p>`;

    function updateFormatHint() {
        const fmt = container.querySelector('input[name="dispatchFormat"]:checked').value;
        hintEl.textContent = fmt === 'html' ? htmlHint : textHint;
        bodyEl.placeholder = fmt === 'html'
            ? '<p>Your message here.</p>'
            : 'Write your message here. Separate paragraphs with a blank line.';
        snippetGroup.style.display = fmt === 'html' ? '' : 'none';
        snippetPrimaryEl.textContent   = SNIPPET_PRIMARY;
        snippetSecondaryEl.textContent = SNIPPET_SECONDARY;
    }

    function makeCopyHandler(btnId, text) {
        document.getElementById(btnId).addEventListener('click', () => {
            navigator.clipboard.writeText(text).then(() => {
                const btn = document.getElementById(btnId);
                btn.textContent = 'Copied!';
                setTimeout(() => { btn.textContent = 'Copy'; }, 1500);
            });
        });
    }
    makeCopyHandler('dispatchSnippetCopyPrimary',   SNIPPET_PRIMARY);
    makeCopyHandler('dispatchSnippetCopySecondary', SNIPPET_SECONDARY);

    container.querySelectorAll('input[name="dispatchFormat"]').forEach(r => {
        r.addEventListener('change', updateFormatHint);
    });
    updateFormatHint();

    document.getElementById('dispatchSendBtn').addEventListener('click', async () => {
        const audience  = container.querySelector('input[name="dispatchAudience"]:checked').value;
        const format    = container.querySelector('input[name="dispatchFormat"]:checked').value;
        const subject   = document.getElementById('dispatchSubject').value.trim();
        const body      = document.getElementById('dispatchBody').value.trim();
        const statusEl  = document.getElementById('dispatchStatus');
        const sendBtn   = document.getElementById('dispatchSendBtn');

        if (!subject) { statusEl.textContent = 'Subject is required.'; statusEl.className = 'dispatch-status dispatch-status--error'; return; }
        if (!body)    { statusEl.textContent = 'Message body is required.'; statusEl.className = 'dispatch-status dispatch-status--error'; return; }

        const countEl  = document.getElementById('dispatchCount');
        const countText = countEl.textContent;
        if (!confirm(`Send to ${countText}?\n\nSubject: ${subject}`)) return;

        sendBtn.disabled = true;
        statusEl.textContent = 'Sending…';
        statusEl.className = 'dispatch-status';

        try {
            const headers = { 'Content-Type': 'application/json' };
            if (authToken) headers['X-Auth-Token'] = authToken;
            const resp = await fetch('/api/admin/dispatch', {
                method: 'POST',
                headers,
                body: JSON.stringify({ subject, body, audience, format }),
            });
            const data = await resp.json();
            if (resp.ok) {
                statusEl.textContent = `Sent to ${data.sent} recipient${data.sent !== 1 ? 's' : ''}.`;
                statusEl.className = 'dispatch-status dispatch-status--ok';
                document.getElementById('dispatchSubject').value = '';
                document.getElementById('dispatchBody').value = '';
            } else {
                statusEl.textContent = data.error || 'Send failed.';
                statusEl.className = 'dispatch-status dispatch-status--error';
            }
        } catch {
            statusEl.textContent = 'Network error.';
            statusEl.className = 'dispatch-status dispatch-status--error';
        } finally {
            sendBtn.disabled = false;
        }
    });

    fetchCount();
}

// ── Schema diagram ────────────────────────────────────────────────────────────

/** Builds and renders the database schema ER diagram into the schema panel container. */
function initSchemaPanel() {
    const container = document.getElementById('adminSchemaDiagram');

    const definition = `
erDiagram
    USERS {
        uuid        id              PK
        text        external_id
        text        auth_provider
        text        email
        text        display_name
        boolean     is_active
        uuid        subscription_id FK
        jsonb       preferences
        timestamptz last_login_at
        timestamptz created_at
        timestamptz modified_at
    }
    SUBSCRIPTIONS {
        uuid        id             PK
        text        name
        text        description
        text        billing_period
        jsonb       features
        timestamptz created_at
        timestamptz modified_at
    }
    FOLDERS {
        uuid        id         PK
        text        name
        uuid        parent_id  FK
        uuid        owner_id   FK
        jsonb       permissions
        timestamptz created_at
    }
    PROJECTS {
        uuid             id           PK
        text             name
        text             version
        uuid             folder_id    FK
        uuid             owner_id     FK
        text             audio_format
        boolean          has_audio_mp3
        boolean          has_transcript
        boolean          has_peaks
        jsonb            speakers
        jsonb            permissions
        integer          sample_rate
        double_precision duration
        timestamptz      created_at
        timestamptz      modified_at
    }
    EMBEDS {
        uuid        id           PK
        uuid        project_id   FK
        uuid        owner_id     FK
        integer     invoke_count
        timestamptz created_at
    }

    SUBSCRIPTIONS ||--o{ USERS       : "subscription_id"
    USERS         ||--o{ PROJECTS    : "owner_id"
    USERS         ||--o{ FOLDERS     : "owner_id"
    USERS         ||--o{ EMBEDS      : "owner_id"
    FOLDERS       ||--o{ FOLDERS     : "parent_id"
    FOLDERS       ||--o{ PROJECTS    : "folder_id"
    PROJECTS      ||--o{ EMBEDS      : "project_id"

    ANALYTICS {
        uuid        id          PK
        jsonb       data
        timestamptz recorded_at
    }
`.trim();

    const pre = document.createElement('pre');
    pre.className = 'mermaid';
    pre.textContent = definition;
    container.appendChild(pre);

    if (window.mermaid) {
        const isDark = document.documentElement.dataset.theme !== 'light';
        mermaid.initialize({
            startOnLoad: false,
            theme: isDark ? 'dark' : 'default',
            er: { layoutDirection: 'LR' },
        });
        mermaid.run({ nodes: [pre] });
    }
}

// ── Auth ──────────────────────────────────────────────────────────────────────

if (LOCAL_MODE) {
    fetch('/api/admin/me').then(async r => {
        if (!r.ok) { hideLoading(); return; }
        showAdminPage(await r.json());
    }).catch(() => hideLoading());

} else if (!firebaseAuth) {
    window.location.href = '/';

} else {
    loginDialog = new LoginDialog();
    let _pendingError = null;

    onAuthStateChanged(firebaseAuth, async (fbUser) => {
        if (!fbUser) {
            hideLoading();
            document.getElementById('adminPage').style.display = 'none';
            loginDialog.openRequired();
            if (_pendingError) {
                loginDialog.showConnectError(_pendingError);
                _pendingError = null;
            }
            return;
        }

        authToken = await fbUser.getIdToken();

        let resp;
        try {
            resp = await fetch('/api/admin/me', { headers: { 'X-Auth-Token': authToken } });
        } catch {
            _pendingError = 'Could not connect to server.';
            authToken = null;
            await signOut(firebaseAuth);
            return;
        }

        if (!resp.ok) {
            _pendingError = resp.status === 403
                ? 'This account does not have admin access.'
                : 'Server error. Please try again.';
            authToken = null;
            await signOut(firebaseAuth);
            return;
        }

        loginDialog.close();
        showAdminPage(await resp.json());
    });
}