
<script>
window.CAT = (function() {
    const CSRF = '<?= Router::csrfToken() ?>';
    const selectedVideos = new Map();
    let currentPage = 1;
    let catPerPage = 25;
    let totalPages = 1;
    let currentTotal = 0;
    let currentVideos = [];
    let filterOptions = { sources: [], categories: [], qualities: [] };
    // Sidebar filter state
    let activeVideoType = '<?= Video::defaultType() ?>';
    let activeSource = '';
    const activeCategories = new Set();   // multi-select
    const activeQualities = new Set();    // multi-select
    let activeHas480 = false;
    const activeViewRanges = new Set();   // multi-select (indices into viewRanges)
    const activeDurations = new Set();    // multi-select (indices into durationRanges)

    function toggleInSet(set, v) { if (set.has(v)) set.delete(v); else set.add(v); }
    function applyFilterParams(params) {
        if (activeCategories.size) params.set('category', [...activeCategories].join(','));
        if (activeQualities.size) params.set('quality', [...activeQualities].join(','));
        if (activeViewRanges.size) params.set('views', [...activeViewRanges].map(i => viewRanges[i].min + '-' + viewRanges[i].max).join(','));
        if (activeDurations.size) params.set('durations', [...activeDurations].map(i => durationRanges[i].min + '-' + durationRanges[i].max).join(','));
    }

    // Collapsed sidebar sections (categories, quality, views collapsed by default; sources open)
    let categoriesCollapsed = true;
    let qualityCollapsed = true;
    let viewsCollapsed = true;

    // ── Gallery (image catalogue) state ──
    let activeMedia = 'videos';
    const activePerformers = new Map(); // slug -> label
    const activeChannels   = new Map();
    const activeTags       = new Map();
    const activeImages     = new Set(); // indices into imageRanges
    let galleryFacets = { performers: [], tags: [], channels: [] };
    const imageRanges = [
        { min: '',   max: '10', label: '< 10' },
        { min: '10', max: '20', label: '10–19' },
        { min: '20', max: '40', label: '20–39' },
        { min: '40', max: '60', label: '40–59' },
        { min: '60', max: '',   label: '60+' },
    ];
    const GFACET = {
        performer: { map: () => activePerformers, list: 'catSidebarPerformers', lookup: 'performers' },
        channel:   { map: () => activeChannels,   list: 'catSidebarChannels',   lookup: 'channels' },
        tag:       { map: () => activeTags,       list: 'catSidebarTags',       lookup: 'tags' },
    };
    let facetTimers = {};

    function init() {
        loadFilters();
        browse(1);
        setTimeout(prefetchSiblings, 1200);
        const apiKey = document.getElementById('catApiKey').value;
        if (apiKey) {
            setConnected(true);
            fetchBalance();
        }
    }

    // ── Config Drawer ──

    function openConfigDrawer() {
        const overlay = document.getElementById('catConfigOverlay');
        const drawer = document.getElementById('catConfigDrawer');
        overlay.classList.remove('hidden');
        requestAnimationFrame(() => {
            drawer.style.transform = 'translateX(0)';
        });
    }

    function closeConfigDrawer() {
        const overlay = document.getElementById('catConfigOverlay');
        const drawer = document.getElementById('catConfigDrawer');
        drawer.style.transform = 'translateX(100%)';
        setTimeout(() => overlay.classList.add('hidden'), 300);
    }

    async function saveConfig() {
        const status = document.getElementById('catConfigStatus');
        status.textContent = 'Saving...';
        status.className = 'text-sm text-gray-500';

        try {
            const resp = await fetch('/admin/catalogue/save-config', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': CSRF },
                body: JSON.stringify({
                    catalogue_api_key: document.getElementById('catApiKey').value.trim(),
                    catalogue_api_secret: document.getElementById('catApiSecret').value.trim(),
                    catalogue_api_url: 'https://tubepress.io/v1',
                    catalogue_auto_publish: document.getElementById('catAutoPublish').checked ? '1' : '0',
                    catalogue_download_thumbnails: document.getElementById('catDownloadThumbs').value,
                })
            });
            const data = await resp.json();
            if (data.error) {
                status.textContent = data.error;
                status.className = 'text-sm text-red-600';
                return;
            }
            if (data.configured && data.balance) {
                updateBalance(data.balance.credits);
                setConnected(true);
                status.textContent = 'Connected! Balance: ' + formatCredits(data.balance.credits) + ' Credits';
                status.className = 'text-sm text-green-600';
                closeConfigDrawer();
                loadFilters();
                browse(1);
            } else if (data.configured) {
                status.textContent = 'Saved. Could not verify connection.';
                status.className = 'text-sm text-amber-600';
            } else {
                status.textContent = 'Saved (no API key/secret set).';
                status.className = 'text-sm text-gray-500';
            }
        } catch (e) {
            status.textContent = 'Network error: ' + e.message;
            status.className = 'text-sm text-red-600';
        }
    }

    function setConnected(connected) {
        const dot = document.getElementById('catConnectionDot');
        const label = document.getElementById('catConnectionLabel');
        const connectBtn = document.getElementById('catConnectBtn');
        const disconnectBtn = document.getElementById('catDisconnectBtn');
        const topUpBtn = document.getElementById('catTopUpBtn');
        const purchaseBtn = document.getElementById('catPurchaseBtn');
        dot.className = 'w-2 h-2 rounded-full ' + (connected ? 'bg-green-500' : 'bg-gray-300');
        label.textContent = connected ? 'Connected' : 'Not connected';
        label.className = 'text-xs ' + (connected ? 'text-green-600' : 'text-gray-500');
        connectBtn.classList.toggle('hidden', connected);
        disconnectBtn.classList.toggle('hidden', !connected);
        if (topUpBtn) topUpBtn.classList.toggle('hidden', !connected);
        if (purchaseBtn) {
            const svg = connected
                ? '<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 00-3 3h15.75m-12.75-3h11.218c1.121 0 2.002-.881 2.002-2.002V6.75m-14.002 0h14.002"/></svg>'
                : '<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m9.86-2.54a4.5 4.5 0 00-1.242-7.244l-4.5-4.5a4.5 4.5 0 00-6.364 6.364L4.343 8.28"/></svg>';
            purchaseBtn.innerHTML = svg + '<span>' + (connected ? 'Purchase & Download' : 'Connect to download') + '</span>';
        }
    }

    async function disconnect() {
        if (!confirm('Disconnect from TubePress? Your API credentials will be removed.')) return;
        try {
            await fetch('/admin/catalogue/save-config', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': CSRF },
                body: JSON.stringify({
                    catalogue_api_key: '',
                    catalogue_api_secret: '',
                    catalogue_api_url: '',
                    catalogue_auto_publish: document.getElementById('catAutoPublish').checked ? '1' : '0',
                    catalogue_download_thumbnails: document.getElementById('catDownloadThumbs').value,
                })
            });
        } catch (e) { /* silent */ }
        document.getElementById('catApiKey').value = '';
        document.getElementById('catApiSecret').value = '';
        document.getElementById('catBalanceBadge').classList.add('hidden');
        setConnected(false);
        browse(1);
    }

    async function fetchBalance() {
        try {
            const resp = await fetch('/admin/catalogue/balance');
            const data = await resp.json();
            if (data.credit_rate) {
                creditRate = parseFloat(data.credit_rate);
                updateAiCostUsd();
            }
            if (data.credits !== undefined) {
                updateBalance(data.credits);
                setConnected(true);
            }
        } catch (e) { /* silent */ }
    }

    let creditRate = 0.01;

    function updateBalance(credits) {
        const badge = document.getElementById('catBalanceBadge');
        const amount = document.getElementById('catBalanceAmount');
        const usd = (parseFloat(credits) * creditRate).toFixed(2);
        amount.textContent = formatCredits(credits) + ' Credits (~$' + usd + ')';
        badge.classList.remove('hidden');
        // Sync balance to the shared MP top-up modal
        if (window.MP) MP.updateBalance(credits);
    }

    function formatCredits(n) {
        return parseFloat(n).toFixed(2);
    }

    // ── Sidebar Rendering ──

    function applyFilters(data) {
        filterOptions = data;
        if (activeMedia === 'galleries') {
            galleryFacets.performers = data.performers || [];
            galleryFacets.tags = data.tags || [];
            galleryFacets.channels = data.channels || [];
        }
        renderSidebar(data);
    }

    async function loadFilters() {
        const params = new URLSearchParams();
        if (activeMedia === 'galleries') params.set('media', 'galleries');
        else params.set('video_type', activeVideoType);
        const url = '/admin/catalogue/filters?' + params.toString();

        const cached = respGet(url);
        if (cached) {
            applyFilters(cached);
            fetch(url).then(r => r.json()).then(d => { if (d && !d.error) { respPut(url, d); } }).catch(() => {});
            return;
        }
        try {
            const resp = await fetch(url);
            const data = await resp.json();
            if (data && !data.error) respPut(url, data);
            applyFilters(data);
        } catch (e) { /* silent */ }
    }

    function renderSidebar(data) {
        renderSources(data.sources || []);
        renderCategories(data.categories || []);
        if (activeMedia === 'galleries') {
            renderFacetList('performer', galleryFacets.performers);
            renderFacetList('channel', galleryFacets.channels);
            renderFacetList('tag', galleryFacets.tags);
            renderImages();
        } else {
            renderQualities(data.qualities || []);
            renderViews();
            renderDuration();
        }
    }

    function renderSources(sources) {
        const container = document.getElementById('catSidebarSources');
        const allIcon = '<svg class="w-4 h-4 shrink-0 text-gray-500" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z"/></svg>';
        let html = renderSourceItem('', 'All Sources', null, allIcon, activeSource === '');
        sources.forEach(s => {
            const slug = s.value || s.slug || '';
            const name = s.label || s.name || slug;
            const count = s.count || 0;
            const icon = s.icon || s.icon_url || null;
            html += renderSourceItem(slug, name, count, icon, activeSource === slug);
        });
        container.innerHTML = html;
    }

    function renderSourceItem(slug, name, count, iconOrUrl, isActive) {
        const activeClass = isActive ? 'bg-primary-50 text-primary-700 font-semibold' : 'text-gray-600 hover:bg-gray-100';
        const initial = (name || '?').charAt(0).toUpperCase();
        const fallbackSpan = `<span class="w-4 h-4 rounded bg-gray-300 flex items-center justify-center text-[9px] font-bold text-gray-600 shrink-0">${escHtml(initial)}</span>`;
        const hiddenFallback = `<span class="w-4 h-4 rounded bg-gray-300 items-center justify-center text-[9px] font-bold text-gray-600 shrink-0" style="display:none">${escHtml(initial)}</span>`;
        let iconHtml;
        if (iconOrUrl && iconOrUrl.startsWith('<')) {
            iconHtml = iconOrUrl;
        } else if (iconOrUrl) {
            iconHtml = `<img src="${escAttr(iconOrUrl)}" alt="" class="w-4 h-4 rounded object-cover shrink-0" onerror="this.style.display='none';this.nextElementSibling.style.display='flex'">${hiddenFallback}`;
        } else {
            iconHtml = fallbackSpan;
        }
        const countHtml = count !== null ? `<span class="text-[10px] text-gray-400 ml-auto">${count}</span>` : '';
        return `<button type="button" onclick="CAT.selectSource('${escAttr(slug)}')"
                class="cat-source-item w-full flex items-center gap-2 px-2.5 py-1.5 rounded-lg text-xs transition-colors ${activeClass}">
                ${iconHtml}
                <span class="flex-1 text-left truncate">${escHtml(name)}</span>
                ${countHtml}
            </button>`;
    }

    let catVisibleCount = 10;

    function renderCategories(categories) {
        const container = document.getElementById('catSidebarCategories');
        if (!categories.length) {
            container.innerHTML = '<span class="text-[10px] text-gray-400">No categories</span>';
            return;
        }
        const visible = categories.slice(0, catVisibleCount);
        const remaining = categories.length - catVisibleCount;
        let html = visible.map(c => {
            const slug = c.value || c.slug || '';
            const name = c.label || c.name || slug;
            const count = c.count || 0;
            const isActive = activeCategories.has(slug);
            const activeClass = isActive ? 'bg-primary-50 text-primary-700 font-semibold' : 'text-gray-600 hover:bg-gray-100';
            return `<button type="button" onclick="CAT.selectCategory('${escAttr(slug)}')"
                class="w-full flex items-center justify-between px-2.5 py-1 rounded-lg text-xs transition-colors ${activeClass}">
                <span class="truncate">${escHtml(name)}</span>
                <span class="text-[10px] text-gray-400 ml-1">${count}</span>
            </button>`;
        }).join('');
        if (remaining > 0) {
            html += `<button type="button" onclick="CAT.showMoreCategories()" class="w-full text-center px-2.5 py-1.5 text-[11px] font-medium text-primary-600 hover:text-primary-700 hover:bg-primary-50 rounded-lg transition-colors">Show ${Math.min(remaining, 5)} more of ${remaining} remaining</button>`;
        }
        if (catVisibleCount > 10) {
            html += `<button type="button" onclick="CAT.collapseCategories()" class="w-full text-center px-2.5 py-1 text-[11px] font-medium text-gray-400 hover:text-gray-600 rounded-lg transition-colors">Collapse</button>`;
        }
        container.innerHTML = html;
    }

    function showMoreCategories() {
        catVisibleCount += 5;
        renderCategories(filterOptions.categories || []);
    }

    function collapseCategories() {
        catVisibleCount = 10;
        renderCategories(filterOptions.categories || []);
    }

    function renderQualities(qualities) {
        const container = document.getElementById('catSidebarQuality');
        if (!qualities.length && !filterOptions.has_480p_count) {
            container.innerHTML = '<span class="text-[10px] text-gray-400">No qualities</span>';
            return;
        }
        let html = qualities.map(q => {
            const val = typeof q === 'object' ? (q.value || q) : q;
            const label = typeof q === 'object' ? (q.label || val) : val;
            const isActive = activeQualities.has(val);
            const activeClass = isActive ? 'bg-primary-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200';
            return `<button type="button" onclick="CAT.selectQuality('${escAttr(val)}')"
                class="px-2.5 py-1 rounded-full text-[11px] font-medium transition-colors ${activeClass}">
                ${escHtml(label)}
            </button>`;
        }).join('');
        if (filterOptions.has_480p_count > 0) {
            const cls480 = activeHas480 ? 'bg-primary-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200';
            html += `<button type="button" onclick="CAT.toggle480()"
                class="px-2.5 py-1 rounded-full text-[11px] font-medium transition-colors ${cls480}">
                480p
            </button>`;
        }
        container.innerHTML = html;
    }

    const viewRanges = [
        { min: '', max: '1000', label: '< 1K' },
        { min: '1000', max: '10000', label: '1K–10K' },
        { min: '10000', max: '50000', label: '10K–50K' },
        { min: '50000', max: '100000', label: '50K–100K' },
        { min: '100000', max: '500000', label: '100K–500K' },
        { min: '500000', max: '', label: '500K+' },
    ];

    function renderViews() {
        const container = document.getElementById('catSidebarViews');
        container.innerHTML = viewRanges.map((r, i) => {
            const isActive = activeViewRanges.has(i);
            const activeClass = isActive ? 'bg-primary-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200';
            return `<button type="button" onclick="CAT.selectViewRange(${i})"
                class="px-2.5 py-1 rounded-full text-[11px] font-medium transition-colors ${activeClass}">
                ${r.label}
            </button>`;
        }).join('');
    }

    function selectViewRange(idx) {
        toggleInSet(activeViewRanges, idx);
        renderViews();
        renderActiveFilters();
        browse(1);
    }

    const durationRanges = [
        { min: '', max: '300', label: '< 5 min' },
        { min: '300', max: '600', label: '5-10 min' },
        { min: '600', max: '1200', label: '10-20 min' },
        { min: '1200', max: '1800', label: '20-30 min' },
        { min: '1800', max: '3600', label: '30-60 min' },
        { min: '3600', max: '', label: '60+ min' },
    ];

    function renderDuration() {
        const container = document.getElementById('catSidebarDuration');
        container.innerHTML = durationRanges.map((r, i) => {
            const isActive = activeDurations.has(i);
            const activeClass = isActive ? 'bg-primary-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200';
            return `<button type="button" onclick="CAT.selectDuration(${i})"
                class="px-2.5 py-1 rounded-full text-[11px] font-medium transition-colors ${activeClass}">
                ${r.label}
            </button>`;
        }).join('');
    }

    function selectDuration(idx) {
        toggleInSet(activeDurations, idx);
        renderDuration();
        renderActiveFilters();
        browse(1);
    }

    // ── Sidebar Filter Actions ──

    function selectSource(slug) {
        activeSource = (activeSource === slug) ? '' : slug;
        renderSources(filterOptions.sources || []);
        renderActiveFilters();
        browse(1);
    }

    function selectCategory(slug) {
        toggleInSet(activeCategories, slug);
        renderCategories(filterOptions.categories || []);
        renderActiveFilters();
        browse(1);
    }

    // ── Gallery searchable facets (performers / channels / tags) ──
    function renderFacetList(facet, items) {
        const cfg = GFACET[facet];
        const el = document.getElementById(cfg.list);
        if (!el) return;
        cfg._last = items;
        const sel = cfg.map();
        if (!items.length) { el.innerHTML = '<span class="text-[10px] text-gray-400">No matches</span>'; return; }
        el.innerHTML = items.map(it => {
            const slug = it.value || it.slug || '';
            const label = it.label || it.name || slug;
            const cnt = (it.count || 0).toLocaleString();
            const on = sel.has(slug);
            const cls = on ? 'bg-primary-50 text-primary-700 font-semibold' : 'text-gray-600 hover:bg-gray-100';
            return `<button type="button" onclick="CAT.toggleFacet('${facet}','${escAttr(slug)}','${escAttr(label)}')"
                class="w-full flex items-center justify-between px-2.5 py-1 rounded-lg text-xs transition-colors ${cls}">
                <span class="truncate">${escHtml(label)}</span>
                <span class="text-[10px] text-gray-400 ml-1">${cnt}</span>
            </button>`;
        }).join('');
    }

    function toggleFacet(facet, slug, label) {
        const sel = GFACET[facet].map();
        if (sel.has(slug)) sel.delete(slug); else sel.set(slug, label || slug);
        renderFacetList(facet, GFACET[facet]._last || []);
        renderActiveFilters();
        browse(1);
    }

    function facetSearch(facet, q) {
        clearTimeout(facetTimers[facet]);
        const cfg = GFACET[facet];
        const term = q.trim();
        facetTimers[facet] = setTimeout(async () => {
            if (term === '') { renderFacetList(facet, galleryFacets[cfg.lookup] || []); return; }
            try {
                const resp = await fetch('/admin/catalogue/gallery-lookup?facet=' + encodeURIComponent(cfg.lookup) + '&q=' + encodeURIComponent(term));
                const data = await resp.json();
                renderFacetList(facet, data.items || []);
            } catch (e) { /* silent */ }
        }, 220);
    }

    function renderImages() {
        const c = document.getElementById('catSidebarImages');
        if (!c) return;
        c.innerHTML = imageRanges.map((r, i) => {
            const on = activeImages.has(i);
            const cls = on ? 'bg-primary-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200';
            return `<button type="button" onclick="CAT.selectImageRange(${i})" class="px-2.5 py-1 rounded-full text-[11px] font-medium transition-colors ${cls}">${r.label}</button>`;
        }).join('');
    }

    function selectImageRange(i) {
        toggleInSet(activeImages, i);
        renderImages();
        renderActiveFilters();
        browse(1);
    }

    function selectQuality(val) {
        toggleInSet(activeQualities, val);
        renderQualities(filterOptions.qualities || []);
        renderActiveFilters();
        browse(1);
    }

    function toggle480() {
        activeHas480 = !activeHas480;
        renderQualities(filterOptions.qualities || []);
        renderActiveFilters();
        browse(1);
    }

    function clearFilter(type) {
        if (type === 'source') activeSource = '';
        else if (type === 'category') activeCategories.clear();
        else if (type === 'quality') activeQualities.clear();
        else if (type === 'has480') activeHas480 = false;
        else if (type === 'views') activeViewRanges.clear();
        else if (type === 'duration') activeDurations.clear();
        renderSidebar(filterOptions);
        renderActiveFilters();
        browse(1);
    }

    // Remove ONE value from a multi-select filter (per-chip ×).
    function removeFilterValue(type, val) {
        if (type === 'category') activeCategories.delete(val);
        else if (type === 'quality') activeQualities.delete(val);
        else if (type === 'views') activeViewRanges.delete(parseInt(val, 10));
        else if (type === 'duration') activeDurations.delete(parseInt(val, 10));
        else if (type === 'source') activeSource = '';
        else if (type === 'has480') activeHas480 = false;
        else if (type === 'performer') activePerformers.delete(val);
        else if (type === 'channel') activeChannels.delete(val);
        else if (type === 'tag') activeTags.delete(val);
        else if (type === 'images') activeImages.delete(parseInt(val, 10));
        renderSidebar(filterOptions);
        renderActiveFilters();
        browse(1);
    }

    function renderActiveFilters() {
        const container = document.getElementById('catActiveFilters');
        let html = '';

        if (activeSource) {
            const src = (filterOptions.sources || []).find(s => (s.value || s.slug) === activeSource);
            const label = src ? (src.label || src.name) : activeSource;
            html += `<span class="inline-flex items-center gap-1 px-2 py-0.5 bg-primary-50 text-primary-700 text-[11px] font-medium rounded-full">
                ${escHtml(label)}
                <button type="button" onclick="CAT.clearFilter('source')" class="text-primary-400 hover:text-primary-700">&times;</button>
            </span>`;
        }
        activeCategories.forEach(slug => {
            const cat = (filterOptions.categories || []).find(c => (c.value || c.slug) === slug);
            const label = cat ? (cat.label || cat.name) : slug;
            html += `<span class="inline-flex items-center gap-1 px-2 py-0.5 bg-purple-50 text-purple-700 text-[11px] font-medium rounded-full">
                ${escHtml(label)}
                <button type="button" onclick="CAT.removeFilterValue('category','${escAttr(slug)}')" class="text-purple-400 hover:text-purple-700">&times;</button>
            </span>`;
        });
        activeQualities.forEach(val => {
            html += `<span class="inline-flex items-center gap-1 px-2 py-0.5 bg-blue-50 text-blue-700 text-[11px] font-medium rounded-full">
                ${escHtml(val)}
                <button type="button" onclick="CAT.removeFilterValue('quality','${escAttr(val)}')" class="text-blue-400 hover:text-blue-700">&times;</button>
            </span>`;
        });
        if (activeHas480) {
            html += `<span class="inline-flex items-center gap-1 px-2 py-0.5 bg-emerald-50 text-emerald-700 text-[11px] font-medium rounded-full">
                480p
                <button type="button" onclick="CAT.clearFilter('has480')" class="text-emerald-400 hover:text-emerald-700">&times;</button>
            </span>`;
        }
        activeViewRanges.forEach(i => {
            const r = viewRanges[i];
            html += `<span class="inline-flex items-center gap-1 px-2 py-0.5 bg-amber-50 text-amber-700 text-[11px] font-medium rounded-full">
                ${r ? r.label : ''} views
                <button type="button" onclick="CAT.removeFilterValue('views','${i}')" class="text-amber-400 hover:text-amber-700">&times;</button>
            </span>`;
        });
        activeDurations.forEach(i => {
            const r = durationRanges[i];
            html += `<span class="inline-flex items-center gap-1 px-2 py-0.5 bg-cyan-50 text-cyan-700 text-[11px] font-medium rounded-full">
                ${r ? r.label : 'Duration'}
                <button type="button" onclick="CAT.removeFilterValue('duration','${i}')" class="text-cyan-400 hover:text-cyan-700">&times;</button>
            </span>`;
        });
        activePerformers.forEach((label, slug) => { html += `<span class="inline-flex items-center gap-1 px-2 py-0.5 bg-blue-50 text-blue-700 text-[11px] font-medium rounded-full">${escHtml(label)}<button type="button" onclick="CAT.removeFilterValue('performer','${escAttr(slug)}')" class="text-blue-400 hover:text-blue-700">&times;</button></span>`; });
        activeChannels.forEach((label, slug) => { html += `<span class="inline-flex items-center gap-1 px-2 py-0.5 bg-cyan-50 text-cyan-700 text-[11px] font-medium rounded-full">${escHtml(label)}<button type="button" onclick="CAT.removeFilterValue('channel','${escAttr(slug)}')" class="text-cyan-400 hover:text-cyan-700">&times;</button></span>`; });
        activeTags.forEach((label, slug) => { html += `<span class="inline-flex items-center gap-1 px-2 py-0.5 bg-gray-100 text-gray-600 text-[11px] font-medium rounded-full">${escHtml(label)}<button type="button" onclick="CAT.removeFilterValue('tag','${escAttr(slug)}')" class="text-gray-400 hover:text-gray-700">&times;</button></span>`; });
        activeImages.forEach(i => { const r = imageRanges[i]; html += `<span class="inline-flex items-center gap-1 px-2 py-0.5 bg-amber-50 text-amber-700 text-[11px] font-medium rounded-full">${r ? r.label : ''} imgs<button type="button" onclick="CAT.removeFilterValue('images','${i}')" class="text-amber-400 hover:text-amber-700">&times;</button></span>`; });

        container.innerHTML = html;
    }

    // ── Sidebar section toggles ──

    function toggleSidebarSection(section) {
        const map = {
            categories: ['catSidebarCategories', 'catCatChevron'],
            quality:    ['catSidebarQuality',    'catQualChevron'],
            views:      ['catSidebarViews',      'catViewsChevron'],
            duration:   ['catSidebarDuration',   'catDurationChevron'],
            performers: ['catSecPerformersBody', 'catPerformersChevron'],
            channels:   ['catSecChannelsBody',   'catChannelsChevron'],
            tags:       ['catSecTagsBody',        'catTagsChevron'],
            images:     ['catSidebarImages',      'catImagesChevron'],
        };
        const ids = map[section];
        if (!ids) return;
        const el = document.getElementById(ids[0]);
        const ch = document.getElementById(ids[1]);
        if (!el) return;
        const hidden = el.classList.toggle('hidden');
        if (ch) ch.style.transform = hidden ? 'rotate(-90deg)' : '';
    }

    var catMobileSidebarOpen = false;
    function toggleMobileSidebar() {
        catMobileSidebarOpen = !catMobileSidebarOpen;
        const sidebar = document.getElementById('catSidebar');
        const overlay = document.getElementById('catSidebarOverlay');
        if (catMobileSidebarOpen) {
            sidebar.classList.remove('hidden');
            sidebar.classList.add('fixed','inset-y-0','left-0','z-40','w-64','shadow-xl','flex');
            sidebar.style.background = '#fff';
            overlay.classList.remove('hidden');
        } else {
            sidebar.classList.add('hidden');
            sidebar.classList.remove('fixed','inset-y-0','left-0','z-40','w-64','shadow-xl','flex');
            sidebar.style.background = '#f9fafb';
            overlay.classList.add('hidden');
        }
    }

    // ── Browse ──

    // ── Response micro-cache: tab flips render instantly from memory, then a
    //    background fetch refreshes the data (stale-while-revalidate). ──
    const respCache = new Map(); // url -> { t, data }
    const RESP_TTL = 300000;     // 5 min — server data barely moves between publishes
    function respGet(url) { const e = respCache.get(url); return (e && Date.now() - e.t < RESP_TTL) ? e.data : null; }
    function respPut(url, data) {
        if (respCache.size > 80) { respCache.delete(respCache.keys().next().value); }
        respCache.set(url, { t: Date.now(), data });
    }

    function skeletonGrid() {
        const n = Math.min(catPerPage, 24);
        return Array.from({ length: n }, () =>
            '<div class="rounded-lg border border-gray-200 bg-white overflow-hidden">'
            + '<div class="aspect-video bg-gray-100 animate-pulse"></div>'
            + '<div class="p-2 space-y-1.5"><div class="h-3 bg-gray-100 rounded animate-pulse"></div><div class="h-3 w-2/3 bg-gray-100 rounded animate-pulse"></div></div>'
            + '</div>').join('');
    }

    function browseParams(page) {
        const searchVal = document.getElementById('catSearch').value.trim();
        let params;
        if (activeMedia === 'galleries') {
            params = new URLSearchParams({ media: 'galleries', page: page, per_page: catPerPage, search: searchVal, source: activeSource });
            if (activeCategories.size) params.set('category', [...activeCategories].join(','));
            if (activePerformers.size) params.set('performer', [...activePerformers.keys()].join(','));
            if (activeChannels.size) params.set('channel', [...activeChannels.keys()].join(','));
            if (activeTags.size) params.set('tag', [...activeTags.keys()].join(','));
            if (activeImages.size) params.set('images', [...activeImages].map(i => imageRanges[i].min + '-' + imageRanges[i].max).join(','));
        } else {
            params = new URLSearchParams({
                page: page,
                per_page: catPerPage,
                search: searchVal,
                source: activeSource,
                video_type: activeVideoType,
                has_480p: activeHas480 ? '1' : '',
            });
            applyFilterParams(params);
        }
        return params;
    }

    function applyBrowse(data) {
        const grid = document.getElementById('catGrid');
        if (data._error || data.error) {
            grid.innerHTML = '<div class="col-span-full text-center py-12 text-red-500 text-sm">' + escHtml(data.message || 'Catalogue API error — please retry.') + '</div>';
            document.getElementById('catPagination').innerHTML = '';
            return;
        }
        currentVideos = (activeMedia === 'galleries') ? (data.galleries || []) : (data.videos || []);
        currentTotal = data.total || 0;
        totalPages = Math.max(1, Math.ceil(currentTotal / catPerPage));
        renderGrid(currentVideos);
        renderPagination();
        updateQuickBar();
    }

    async function browse(page) {
        currentPage = Math.max(1, parseInt(page, 10) || 1);
        const grid = document.getElementById('catGrid');
        const url = '/admin/catalogue/browse?' + browseParams(currentPage).toString();

        // Instant path: render straight from the cache, refresh silently after.
        const cached = respGet(url);
        if (cached) {
            applyBrowse(cached);
            fetch(url).then(r => r.json()).then(data => {
                if (!data || data._error || data.error) return;
                respPut(url, data);
                // Only re-apply if the user is still looking at this exact view.
                if (url === '/admin/catalogue/browse?' + browseParams(currentPage).toString()) applyBrowse(data);
            }).catch(() => {});
            return;
        }

        grid.innerHTML = skeletonGrid();
        try {
            const resp = await fetch(url);
            const data = await resp.json();
            if (!data._error && !data.error) respPut(url, data);
            applyBrowse(data);
        } catch (e) {
            grid.innerHTML = '<div class="col-span-full text-center py-12 text-red-500 text-sm">Could not connect to the catalogue API. Check your configuration.</div>';
        }
    }

    // ── Idle prefetch: warm the sibling tabs (other video types + galleries page 1
    //    and their filter sets) so the first click on any tab is instant. ──
    let prefetched = false;
    function prefetchSiblings() {
        if (prefetched) return;
        prefetched = true;
        const jobs = [];
        if (activeMedia !== 'galleries') {
            ['hetero', 'gay', 'hentai'].filter(t => t !== activeVideoType).forEach(t => {
                jobs.push('/admin/catalogue/browse?' + new URLSearchParams({ page: 1, per_page: catPerPage, search: '', source: '', video_type: t, has_480p: '' }).toString());
                jobs.push('/admin/catalogue/filters?' + new URLSearchParams({ video_type: t }).toString());
            });
            jobs.push('/admin/catalogue/browse?' + new URLSearchParams({ media: 'galleries', page: 1, per_page: catPerPage, search: '', source: '' }).toString());
            jobs.push('/admin/catalogue/filters?' + new URLSearchParams({ media: 'galleries' }).toString());
        }
        jobs.forEach((u, i) => {
            setTimeout(() => {
                if (respGet(u)) return;
                fetch(u).then(r => r.json()).then(d => { if (d && !d._error && !d.error) respPut(u, d); }).catch(() => {});
            }, 900 + i * 700);
        });
    }

    function renderGrid(videos) {
        const grid = document.getElementById('catGrid');
        if (!videos.length) {
            grid.innerHTML = '<div class="col-span-full text-center py-12 text-gray-400 text-sm">No ' + (activeMedia === 'galleries' ? 'galleries' : 'videos') + ' found matching your filters.</div>';
            return;
        }

        if (activeMedia === 'galleries') { grid.innerHTML = videos.map(renderGalleryCard).join(''); return; }

        const sorted = [...videos].sort((a, b) => (a.already_imported ? 1 : 0) - (b.already_imported ? 1 : 0));
        grid.innerHTML = sorted.map(v => {
            const id = v.remote_video_id;
            const isSelected = selectedVideos.has(id);
            const imported = v.already_imported;
            const dur = formatDuration(v.duration || 0);
            const cost = formatCredits(v.cost || 0);
            return `<div class="relative group ${imported ? 'cursor-default opacity-50 grayscale' : 'cursor-pointer hover:shadow-md'} rounded-lg border border-gray-200 shadow-sm transition-all ${isSelected ? 'border-primary-500 bg-primary-50/30' : imported ? 'border-gray-100 bg-gray-50' : 'bg-white hover:border-gray-300 hover:shadow'}"
                        ${imported ? '' : `onclick="CAT.toggleSelect('${escAttr(id)}')"` }>
                <div class="relative aspect-video bg-gray-100 rounded-t-lg overflow-hidden">
                    <img src="${escAttr(v.thumbnail_small || v.thumbnail || '')}" data-fb="${escAttr(v.thumbnail || '')}" alt="" class="w-full h-full object-cover" loading="lazy" onerror="if(!this._fb&&this.dataset.fb){this._fb=1;this.src=this.dataset.fb;}else{this.onerror=null;this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 16 9%22><rect fill=%22%23e5e7eb%22 width=%2216%22 height=%229%22/></svg>';}">
                    <div class="absolute top-1.5 left-1.5 z-20">
                        <div class="w-5 h-5 rounded border-2 ${isSelected ? 'bg-primary-600 border-primary-600' : 'bg-white/80 border-gray-300'} flex items-center justify-center">
                            ${isSelected ? '<svg class="w-3 h-3 text-white" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/></svg>' : ''}
                        </div>
                    </div>
                    ${imported ? '<div class="absolute top-1.5 right-1.5 px-1.5 py-0.5 bg-green-600 text-white text-[10px] font-bold rounded z-20">IMPORTED</div>' : ''}
                    <button onclick="event.stopPropagation();CAT.showVideoInfo('${escAttr(id)}')" class="absolute ${imported ? 'top-1.5 right-[5.5rem]' : 'top-1.5 right-1.5'} z-20 w-6 h-6 rounded-full bg-black/50 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity" title="View details">
                        <svg class="w-3.5 h-3.5 text-white" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z"/><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
                    </button>
                    ${dur ? `<div class="absolute bottom-1.5 right-1.5 px-1.5 py-0.5 bg-black/70 text-white text-[10px] font-medium rounded">${dur}</div>` : ''}
                    <div class="absolute bottom-1.5 left-1.5 px-1.5 py-0.5 bg-amber-500/90 text-white text-[10px] font-bold rounded">${cost} cr</div>
                    ${v.preview ? `<div class="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity bg-black/20 pointer-events-none"><button onclick="event.stopPropagation();CAT.playPreview('${escAttr(v.preview)}','${escAttr(v.title||'')}')" class="w-10 h-10 rounded-full bg-black/60 flex items-center justify-center pointer-events-auto"><svg class="w-4 h-4 text-white ml-0.5" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg></button></div>` : ''}
                </div>
                <div class="p-2">
                    <h4 class="text-xs font-medium text-gray-900 truncate leading-tight mb-1" title="${escAttr(v.title || 'Untitled')}">${escHtml(v.title || 'Untitled')}</h4>
                    <div class="flex items-center gap-1.5 flex-wrap">
                        ${v.source ? `<span class="px-1.5 py-0.5 bg-gray-100 text-gray-500 text-[10px] rounded">${escHtml(v.source)}</span>` : ''}
                        ${v.quality ? `<span class="px-1.5 py-0.5 bg-blue-50 text-blue-600 text-[10px] rounded font-medium">${escHtml(v.quality)}</span>` : ''}
                        ${v.has_480p ? '<span class="px-1.5 py-0.5 bg-emerald-50 text-emerald-600 text-[10px] rounded font-medium">480p</span>' : ''}
                        <button onclick="event.stopPropagation();CAT.showVideoInfo('${escAttr(id)}')" class="ml-auto w-5 h-5 flex items-center justify-center text-primary-600 bg-primary-50 hover:bg-primary-100 border border-primary-200 rounded transition-colors" title="View details"><svg class="w-3 h-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z"/><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg></button>
                    </div>
                </div>
            </div>`;
        }).join('');
    }

    const GALLERY_PLACEHOLDER = 'data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 3 4%22%3E%3Crect fill=%22%23e5e7eb%22 width=%223%22 height=%224%22/%3E%3C/svg%3E';
    function renderGalleryCard(g) {
        const id = g.id;
        const isSelected = selectedVideos.has(String(id));
        const cover = g.cover || '';
        const n = (g.image_count || 0).toLocaleString();
        const cats = (g.categories || []).slice(0, 2).map(c => `<span class="px-1.5 py-0.5 bg-gray-100 text-gray-500 text-[10px] rounded truncate max-w-[5rem]">${escHtml(c.name || c.slug)}</span>`).join('');
        const src = g.source_name || g.source || '';
        return `<div class="relative group cursor-pointer rounded-lg border shadow-sm hover:shadow-md transition-all ${isSelected ? 'border-primary-500 bg-primary-50/30' : 'border-gray-200 hover:border-gray-300 bg-white'}" onclick="CAT.toggleSelect('${escAttr(String(id))}')">
            <div class="relative bg-gray-100 rounded-t-lg overflow-hidden" style="aspect-ratio:3/4">
                <img src="${escAttr(cover)}" alt="" class="w-full h-full object-cover" loading="lazy" onerror="this.onerror=null;this.src='${GALLERY_PLACEHOLDER}'">
                <div class="absolute top-1.5 left-1.5 z-20">
                    <div class="w-5 h-5 rounded border-2 ${isSelected ? 'bg-primary-600 border-primary-600' : 'bg-white/80 border-gray-300'} flex items-center justify-center">
                        ${isSelected ? '<svg class="w-3 h-3 text-white" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/></svg>' : ''}
                    </div>
                </div>
                ${src ? `<div class="absolute top-1.5 right-1.5 px-1.5 py-0.5 bg-black/55 text-white text-[10px] rounded z-10">${escHtml(src)}</div>` : ''}
                <div class="absolute bottom-1.5 left-1.5 px-1.5 py-0.5 bg-amber-500/90 text-white text-[10px] font-bold rounded z-10">${formatCredits(g.cost || 0)} cr</div>
                <div class="absolute bottom-1.5 right-1.5 px-1.5 py-0.5 bg-black/70 text-white text-[10px] font-medium rounded inline-flex items-center gap-1 z-10">
                    <svg class="w-3 h-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5z"/></svg>
                    ${n}
                </div>
                <div class="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity bg-black/20">
                    <button type="button" onclick="event.stopPropagation();CAT.openGalleryViewer(${id})" class="px-2.5 py-1 bg-white/90 rounded-full text-[11px] font-semibold text-gray-800 hover:bg-white">View gallery</button>
                </div>
            </div>
            <div class="p-2">
                <h4 class="text-xs font-medium text-gray-900 truncate leading-tight mb-1" title="${escAttr(g.title || '')}">${escHtml(g.title || 'Untitled')}</h4>
                <div class="flex items-center gap-1.5 flex-wrap">${cats}</div>
            </div>
        </div>`;
    }

    async function openGalleryViewer(id) {
        const modal = document.getElementById('catGalleryModal');
        const body = document.getElementById('catGalleryModalBody');
        modal.classList.remove('hidden');
        body.innerHTML = '<div class="text-center py-16"><svg class="animate-spin h-8 w-8 mx-auto text-primary-600" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg></div>';
        try {
            const resp = await fetch('/admin/catalogue/gallery-item?id=' + encodeURIComponent(id));
            const g = await resp.json();
            if (!g || g.error) { body.innerHTML = '<div class="text-center py-16 text-red-500 text-sm">Could not load this gallery.</div><div class="text-center"><button onclick="CAT.closeGalleryViewer()" class="px-4 py-2 bg-gray-100 rounded-lg text-sm">Close</button></div>'; return; }
            const chip = (txt, cls) => `<span class="px-2 py-0.5 ${cls} text-[11px] font-medium rounded-full">${escHtml(txt)}</span>`;
            const cats = (g.categories || []).map(c => chip(c.name || c.slug, 'bg-purple-50 text-purple-700')).join('');
            const perfs = (g.performers || []).map(p => chip(p, 'bg-blue-50 text-blue-700')).join('');
            const tags = (g.tags || []).map(t => chip(t, 'bg-gray-100 text-gray-600')).join('');
            const imgs = (g.images || []).map(im => `<a href="${escAttr(im.url)}" target="_blank" rel="noopener" class="block rounded-lg overflow-hidden bg-gray-100" style="aspect-ratio:3/4"><img src="${escAttr(im.url)}" loading="lazy" class="w-full h-full object-cover" onerror="this.onerror=null;this.src='${GALLERY_PLACEHOLDER}'"></a>`).join('');
            body.innerHTML = `
                <div class="flex items-start justify-between gap-3 mb-3">
                    <div class="min-w-0">
                        <h3 class="text-base font-bold text-gray-900 leading-snug">${escHtml(g.title || 'Untitled')}</h3>
                        <div class="text-xs text-gray-500 mt-0.5">${g.image_count || 0} images${g.source_name ? ' · ' + escHtml(g.source_name) : ''}${g.channel ? ' · ' + escHtml(g.channel) : ''}</div>
                    </div>
                    <button onclick="CAT.closeGalleryViewer()" class="shrink-0 w-8 h-8 flex items-center justify-center rounded-lg text-gray-400 hover:text-gray-700 hover:bg-gray-100"><svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg></button>
                </div>
                ${(cats || perfs || tags) ? `<div class="flex flex-wrap gap-1.5 mb-4 pb-4 border-b border-gray-100">${cats}${perfs}${tags}</div>` : ''}
                <div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-2">${imgs || '<div class="col-span-full text-center text-gray-400 text-sm py-8">No images available yet.</div>'}</div>`;
        } catch (e) {
            body.innerHTML = '<div class="text-center py-16 text-red-500 text-sm">Could not load this gallery.</div>';
        }
    }

    function closeGalleryViewer() {
        const modal = document.getElementById('catGalleryModal');
        modal.classList.add('hidden');
        document.getElementById('catGalleryModalBody').innerHTML = '';
    }

    // ── Gallery import (download catalogue galleries into the CMS as Albums) ──
    let _galBaseCost = 0, _galCount = 0;
    var galStep = 1, galBase = 0, galCount = 0;
    function openGalleryImportModal() {
        if (selectedVideos.size === 0) return;
        galCount = selectedVideos.size; galBase = 0;
        selectedVideos.forEach(function (g) { galBase += parseFloat(g.cost || 0); });
        document.getElementById('catGalCount').textContent = galCount;
        document.getElementById('catGalCountPlural').textContent = (galCount === 1 ? 'y' : 'ies');
        ['galAiRewriteTitle','galAiTranslate','galAiRewriteDesc','galAiTransDesc'].forEach(function (id) { var c = document.getElementById(id); if (c) c.checked = false; });
        ['galAiTitlePromptWrap','galAiTranslatePromptWrap','galAiDescPromptWrap','galAiTransDescPromptWrap'].forEach(function (id) { var w = document.getElementById(id); if (w) w.classList.add('hidden'); });
        document.querySelectorAll('#catGalStep1 .cat-gal-opt[data-galkey] input[type=checkbox]').forEach(function (cb) { var k = cb.closest('[data-galkey]').dataset.galkey; cb.checked = (k !== 'views'); });
        var bt = document.getElementById('catGalImportBtn'); if (bt) { bt.disabled = false; bt.innerHTML = '<i class="fa-solid fa-cart-shopping"></i> Purchase & Download'; }
        galGoToStep(1);
        document.getElementById('catGalleryImportModal').classList.remove('hidden');
    }
    function closeGalleryImportModal() { var m = document.getElementById('catGalleryImportModal'); if (m) m.classList.add('hidden'); }
    function galleryAiPerItem() {
        var c = 0;
        if (document.getElementById('galAiRewriteTitle') && document.getElementById('galAiRewriteTitle').checked) c += AI_COST.title;
        if (document.getElementById('galAiTranslate') && document.getElementById('galAiTranslate').checked) c += aiOpCost('translate');
        if (document.getElementById('galAiRewriteDesc') && document.getElementById('galAiRewriteDesc').checked) c += AI_COST.desc;
        if (document.getElementById('galAiTransDesc') && document.getElementById('galAiTransDesc').checked) c += aiOpCost('transdesc');
        return c;
    }
    function recalcGalleryImportCost() {
        var per = galleryAiPerItem(), total = galBase + per * galCount;
        var h = document.getElementById('catGalCostHdr'); if (h) h.textContent = formatCredits(total) + ' cr';
    }
    function galleryAiToggle(cbId, wrapId) {
        var cb = document.getElementById(cbId), w = document.getElementById(wrapId);
        if (w) w.classList.toggle('hidden', !(cb && cb.checked));
        recalcGalleryImportCost();
    }
    function getGalleryAiSettings() {
        var ai = { enabled: false };
        var rt = document.getElementById('galAiRewriteTitle');
        if (rt && rt.checked) { ai.enabled = true; ai.rewrite_title = true; ai.title_prompt = (document.getElementById('galAiTitlePromptText') ? document.getElementById('galAiTitlePromptText').value : '').trim(); }
        var tr = document.getElementById('galAiTranslate');
        if (tr && tr.checked && AI_LANGUAGES.length > 0) { ai.enabled = true; ai.translate = true; ai.translate_prompt = (document.getElementById('galAiTranslatePromptText') ? document.getElementById('galAiTranslatePromptText').value : '').trim(); ai.translate_languages = AI_LANGUAGES; ai.translate_format = AI_LANGUAGES.map(function (l) { return l.name + ': <' + l.code.toUpperCase() + '></' + l.code.toUpperCase() + '>'; }).join('\n'); }
        var rd = document.getElementById('galAiRewriteDesc');
        if (rd && rd.checked) { ai.enabled = true; ai.rewrite_description = true; ai.desc_prompt = (document.getElementById('galAiDescPromptText') ? document.getElementById('galAiDescPromptText').value : '').trim(); }
        var td = document.getElementById('galAiTransDesc');
        if (td && td.checked && AI_LANGUAGES.length > 0) { ai.enabled = true; ai.translate_description = true; ai.translate_desc_prompt = (document.getElementById('galAiTransDescPromptText') ? document.getElementById('galAiTransDescPromptText').value : '').trim(); ai.translate_languages = AI_LANGUAGES; ai.translate_format = AI_LANGUAGES.map(function (l) { return l.name + ': <' + l.code.toUpperCase() + '></' + l.code.toUpperCase() + '>'; }).join('\n'); }
        return ai;
    }
    function getGalleryImportSettings() {
        var s = {};
        document.querySelectorAll('#catGalStep1 .cat-gal-opt[data-galkey]').forEach(function (l) { s[l.dataset.galkey] = l.querySelector('input[type=checkbox]').checked; });
        return s;
    }
    function galStepTitle(n) { return n === 1 ? 'Import Settings' : (n === 2 ? 'AI Processing' : 'Confirm & Purchase'); }
    function galGoToStep(n) {
        galStep = n;
        [1, 2, 3].forEach(function (i) {
            var el = document.getElementById('catGalStep' + i); if (el) el.classList.toggle('hidden', i !== n);
            var dot = document.querySelector('[data-galdot="' + i + '"]');
            var lbl = document.querySelector('[data-gallabel="' + i + '"]');
            if (dot) dot.className = 'w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold transition-all ' + (i <= n ? 'bg-primary-600 text-white' + (i === n ? ' ring-2 ring-primary-200' : '') : 'bg-gray-200 text-gray-400');
            if (lbl) lbl.className = 'text-[11px] ' + (i === n ? 'font-semibold text-primary-700' : 'font-medium text-gray-400');
        });
        document.getElementById('catGalStepTitle').textContent = galStepTitle(n);
        document.getElementById('catGalBackBtn').classList.toggle('hidden', n === 1);
        document.getElementById('catGalNextBtn').classList.toggle('hidden', n === 3);
        document.getElementById('catGalImportBtn').classList.toggle('hidden', n !== 3);
        recalcGalleryImportCost();
        if (n === 3) renderGalSummary();
    }
    function galNextStep() { if (galStep < 3) galGoToStep(galStep + 1); }
    function galPrevStep() { if (galStep > 1) galGoToStep(galStep - 1); }
    function renderGalSummary() {
        var inc = getGalleryImportSettings(), ai = getGalleryAiSettings(), per = galleryAiPerItem(), total = galBase + per * galCount;
        var labels = { description: 'Description', categories: 'Categories', tags: 'Tags', performers: 'Pornstars', channels: 'Channels', views: 'Views' };
        var incList = Object.keys(labels).filter(function (k) { return inc[k]; }).map(function (k) { return labels[k]; });
        var aiList = [];
        if (ai.rewrite_title) aiList.push('Rewrite title');
        if (ai.translate) aiList.push('Translate title');
        if (ai.rewrite_description) aiList.push('Rewrite description');
        if (ai.translate_description) aiList.push('Translate description');
        var html = '<div class="space-y-3">'
            + '<div class="flex items-center gap-3 px-4 py-3 rounded-xl bg-primary-50 border border-primary-100"><i class="fa-solid fa-images text-primary-600"></i><div><div class="text-sm font-bold text-gray-900">' + galCount + ' galler' + (galCount === 1 ? 'y' : 'ies') + '</div><div class="text-[11px] text-gray-500">Imported as Albums</div></div><div class="ml-auto text-right"><div class="text-lg font-bold text-amber-600">' + formatCredits(total) + ' cr</div><div class="text-[10px] text-gray-400">~$' + (total * creditRate).toFixed(2) + '</div></div></div>'
            + '<div class="text-xs text-gray-700"><span class="font-semibold text-gray-500 uppercase tracking-wide text-[10px]">Include</span><br>' + (incList.length ? incList.join(', ') : 'Images only') + '</div>'
            + (aiList.length ? '<div class="text-xs text-gray-700"><span class="font-semibold text-gray-500 uppercase tracking-wide text-[10px]">AI (runs in background)</span><br>' + aiList.join(', ') + ' <span class="text-gray-400">(+' + formatCredits(per * galCount) + ' cr)</span></div>' : '<div class="text-xs text-gray-400">No AI enhancement</div>')
            + '<div id="catGalImportResult" class="hidden text-xs"></div></div>';
        document.getElementById('catGalSummary').innerHTML = html;
    }
    async function confirmGalleryImport() {
        if (selectedVideos.size === 0) return;
        var ids = Array.from(selectedVideos.keys()).map(function (x) { return parseInt(x, 10); }).filter(function (x) { return x > 0; });
        var btn = document.getElementById('catGalImportBtn');
        if (btn) { btn.disabled = true; btn.innerHTML = '<i class="fa-solid fa-spinner-third fa-spin"></i> Importing...'; }
        try {
            var resp = await fetch('/admin/catalogue/gallery-import', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': CSRF },
                body: JSON.stringify({ gallery_ids: ids, import_settings: getGalleryImportSettings(), ai_settings: getGalleryAiSettings() })
            });
            var data = await resp.json();
            if (data.error) { if (btn) { btn.disabled = false; btn.innerHTML = '<i class="fa-solid fa-cart-shopping"></i> Purchase & Download'; } showNotification('Import failed: ' + data.error, 'error'); return; }
            var imp = (data.imported || []).length, skp = (data.skipped || []).length, err = (data.errors || []).length;
            if (data.new_balance !== undefined && data.new_balance !== null && typeof updateBalance === 'function') updateBalance(data.new_balance);
            closeGalleryImportModal(); selectedVideos.clear(); updateBulkBar(); browse(currentPage);
            var msg = imp + ' galler' + (imp === 1 ? 'y' : 'ies') + ' imported as album' + (imp === 1 ? '' : 's');
            if (skp) msg += ', ' + skp + ' already imported';
            if (err) msg += ', ' + err + ' failed';
            if (data.remaining) msg += ' — ' + data.remaining + ' over the per-batch limit; import the rest in another batch';
            if (data.ai_queued) msg += '. AI enhancement is running in the background';
            showNotification(msg + '.', err ? 'error' : 'success');
        } catch (e) { if (btn) { btn.disabled = false; btn.innerHTML = '<i class="fa-solid fa-cart-shopping"></i> Purchase & Download'; } showNotification('Network error: ' + e.message, 'error'); }
    }

    function renderPagination() {
        const container = document.getElementById('catPagination');
        if (!currentTotal) { container.innerHTML = ''; return; }
        const cur = currentPage;
        const perPageSel = '<span class="text-xs text-gray-400">Show</span>'
            + '<select id="catPerPageSel" onchange="CAT.setPerPage(this.value)" class="px-1.5 py-1 text-xs border border-gray-200 rounded-lg bg-white cursor-pointer">'
            + [25, 50, 100, 200].map(function (n) { return '<option value="' + n + '"' + (n === catPerPage ? ' selected' : '') + '>' + n + ' / page</option>'; }).join('')
            + '</select>';
        if (totalPages <= 1) {
            container.innerHTML = '<div class="flex items-center justify-between w-full gap-3 flex-wrap">'
                + '<span class="text-xs text-gray-500 shrink-0">' + currentTotal.toLocaleString() + ' ' + (activeMedia === 'galleries' ? (currentTotal !== 1 ? 'galleries' : 'gallery') : (currentTotal !== 1 ? 'videos' : 'video')) + '</span>'
                + '<div class="flex items-center gap-1.5 shrink-0">' + perPageSel + '</div>'
                + '</div>';
            return;
        }
        const win = [];
        const add = n => { if (n >= 1 && n <= totalPages && !win.includes(n)) win.push(n); };
        add(1); add(2);
        for (let i = cur - 2; i <= cur + 2; i++) add(i);
        add(totalPages - 1); add(totalPages);
        win.sort((a, b) => a - b);
        let nums = '';
        let prev = 0;
        win.forEach(n => {
            if (n - prev > 1) nums += '<span class="px-1.5 text-gray-400 text-xs">…</span>';
            nums += '<button onclick="CAT.browse(' + n + ')" class="min-w-[2rem] px-2 py-1.5 text-xs border rounded-lg ' + (n === cur ? 'bg-primary-600 text-white border-primary-600 font-semibold' : 'border-gray-200 hover:bg-gray-50') + '">' + n + '</button>';
            prev = n;
        });
        const nav = (label, target, enabled) =>
            '<button ' + (enabled ? 'onclick="CAT.browse(' + target + ')"' : 'disabled') + ' class="px-2.5 py-1.5 text-xs border border-gray-200 rounded-lg ' + (enabled ? 'hover:bg-gray-50' : 'opacity-40 cursor-not-allowed') + '">' + label + '</button>';
        container.innerHTML =
            '<div class="flex items-center justify-between w-full gap-3 flex-wrap">'
          + '<span class="text-xs text-gray-500 shrink-0">' + currentTotal.toLocaleString() + ' ' + (activeMedia === 'galleries' ? (currentTotal !== 1 ? 'galleries' : 'gallery') : (currentTotal !== 1 ? 'videos' : 'video')) + '</span>'
          + '<div class="flex items-center gap-1 flex-wrap justify-center">'
            + nav('&laquo;', cur - 1, cur > 1) + nums + nav('&raquo;', cur + 1, cur < totalPages)
          + '</div>'
          + '<div class="flex items-center gap-1.5 shrink-0">'
            + perPageSel
            + '<span class="text-xs text-gray-400 ml-1">Go to</span>'
            + '<input type="number" min="1" max="' + totalPages + '" value="' + cur + '" id="catGotoPage" class="w-14 px-2 py-1 text-xs border border-gray-200 rounded-lg" onkeydown="if(event.key===\'Enter\')CAT.gotoPage()">'
            + '<span class="text-xs text-gray-400">/ ' + totalPages.toLocaleString() + '</span>'
          + '</div>'
          + '</div>';
    }

    function gotoPage() {
        const el = document.getElementById('catGotoPage');
        if (!el) return;
        let n = parseInt(el.value, 10) || 1;
        n = Math.min(totalPages, Math.max(1, n));
        browse(n);
    }

    function setPerPage(n) {
        catPerPage = Math.max(1, parseInt(n, 10) || 25);
        browse(1);
    }

    // ── Selection ──

    function toggleSelect(id) {
        if (selectedVideos.has(id)) {
            selectedVideos.delete(id);
        } else if (activeMedia === 'galleries') {
            const gal = currentVideos.find(g => String(g.id) === String(id));
            if (gal) selectedVideos.set(id, gal);
        } else {
            const video = currentVideos.find(v => v.remote_video_id === id);
            if (video && !video.already_imported) {
                selectedVideos.set(id, video);
            }
        }
        updateBulkBar();
        renderGrid(currentVideos);
    }

    function selectAllPage() {
        if (activeMedia === 'galleries') {
            currentVideos.forEach(g => { if (!selectedVideos.has(String(g.id))) selectedVideos.set(String(g.id), g); });
        } else {
            currentVideos.forEach(v => {
                if (!v.already_imported && !selectedVideos.has(v.remote_video_id)) {
                    selectedVideos.set(v.remote_video_id, v);
                }
            });
        }
        updateBulkBar();
        renderGrid(currentVideos);
    }

    function clearSelection() {
        selectedVideos.clear();
        updateBulkBar();
        renderGrid(currentVideos);
    }

    function updateQuickBar() {
        const bar = document.getElementById('catQuickBar');
        const label = document.getElementById('catQuickLabel');
        const dlBtn = document.getElementById('catDownloadAllBtn');
        const dlLabel = document.getElementById('catDownloadAllLabel');
        if (!currentTotal) { bar.classList.add('hidden'); return; }
        bar.classList.remove('hidden');

        const qa = document.getElementById('catQuickActions');
        if (activeMedia === 'galleries') {
            if (qa) qa.classList.add('hidden');
            label.textContent = currentTotal.toLocaleString() + ' ' + (currentTotal !== 1 ? 'galleries' : 'gallery');
            return;
        }
        if (qa) qa.classList.remove('hidden');

        const hasFilters = activeSource || activeCategories.size || activeQualities.size || activeHas480 || activeViewRanges.size || activeDurations.size;
        const parts = [];
        if (activeSource) {
            const src = (filterOptions.sources || []).find(s => (s.value || s.slug) === activeSource);
            parts.push(src ? (src.label || src.name) : activeSource);
        }
        activeCategories.forEach(slug => {
            const cat = (filterOptions.categories || []).find(c => (c.value || c.slug) === slug);
            parts.push(cat ? (cat.label || cat.name) : slug);
        });
        activeQualities.forEach(val => parts.push(String(val).toUpperCase()));
        if (activeHas480) parts.push('480p');
        activeViewRanges.forEach(i => { if (viewRanges[i]) parts.push(viewRanges[i].label + ' views'); });
        activeDurations.forEach(i => { if (durationRanges[i]) parts.push(durationRanges[i].label); });

        if (hasFilters) {
            label.textContent = currentTotal.toLocaleString() + ' video' + (currentTotal !== 1 ? 's' : '') + ' matching: ' + parts.join(' + ');
            dlBtn.classList.remove('hidden');
            dlLabel.textContent = 'Download all ' + currentTotal.toLocaleString();
        } else {
            label.textContent = currentTotal.toLocaleString() + ' video' + (currentTotal !== 1 ? 's' : '') + ' in catalogue';
            dlBtn.classList.add('hidden');
        }
    }

    async function downloadAllMatching() {
        if (!activeSource && !activeCategories.size && !activeQualities.size && !activeHas480 && !activeViewRanges.size && !activeDurations.size) return;

        const apiKey = document.getElementById('catApiKey').value;
        if (!apiKey) { autoConnect(); return; }

        // Collect every matching video, then hand off to the classic import
        // workflow (modal: quality + AI options) — same path as manual selection.
        showLoading('Collecting all matching videos...');
        let page = 1;
        let totalPages = 1;

        try {
            while (page <= totalPages) {
                const params = new URLSearchParams({
                    page: page,
                    per_page: 500,
                    source: activeSource,
                    has_480p: activeHas480 ? '1' : '',
                    video_type: activeVideoType,
                    search: document.getElementById('catSearch').value.trim(),
                });
                applyFilterParams(params);
                const resp = await fetch('/admin/catalogue/browse?' + params.toString());
                const data = await resp.json();
                if (data._error || data.error) break;
                (data.videos || []).forEach(v => {
                    if (!v.already_imported && !selectedVideos.has(v.remote_video_id)) {
                        selectedVideos.set(v.remote_video_id, v);
                    }
                });
                totalPages = Math.ceil((data.total || 0) / 500);
                const lt = document.getElementById('catLoadingText');
                if (lt) lt.textContent = 'Collecting videos... ' + selectedVideos.size + ' selected';
                page++;
                if (page > 500) break;
            }

            hideLoading();

            if (selectedVideos.size === 0) {
                showNotification('No new videos to import (all already imported).', 'info');
                return;
            }

            updateBulkBar();
            renderGrid(currentVideos);
            // Hand off to the standard import wizard (quality + AI + purchase).
            openImportModal();
        } catch (e) {
            hideLoading();
            showNotification('Error: ' + e.message, 'error');
        }
    }

    function updateBulkBar() {
        const bar = document.getElementById('catBulkBar');
        const countEl = document.getElementById('catSelectedCount');
        const costEl = document.getElementById('catEstCost');

        const count = selectedVideos.size;
        countEl.textContent = count;

        let totalCost = 0;
        selectedVideos.forEach(v => totalCost += parseFloat(v.cost || 0));
        const usd = (totalCost * creditRate).toFixed(2);
        costEl.textContent = formatCredits(totalCost) + ' Credits (~$' + usd + ')';

        bar.classList.toggle('hidden', count === 0);

        // Lift the pagination above the fixed purchase bar so it isn't hidden behind it.
        const pag = document.getElementById('catPagination');
        if (pag) pag.style.marginBottom = count > 0 ? (bar.offsetHeight + 16) + 'px' : '';

        updateTestButtons();
        updateMediaTabsLock();
    }

    function updateMediaTabsLock() {
        const locked = selectedVideos.size > 0;
        document.querySelectorAll('.cat-media-tab').forEach(btn => {
            const isActive = btn.dataset.media === activeMedia;
            if (locked && !isActive) {
                btn.classList.add('opacity-40', 'cursor-not-allowed');
                btn.style.pointerEvents = 'none';
                btn.title = 'Clear your selection to switch media type';
                if (!btn.querySelector('.cat-lock-ico')) {
                    btn.insertAdjacentHTML('afterbegin', '<i class="fa-solid fa-lock cat-lock-ico mr-1 text-[9px]"></i>');
                }
            } else {
                btn.classList.remove('opacity-40', 'cursor-not-allowed');
                btn.style.pointerEvents = '';
                btn.title = '';
                const _ic = btn.querySelector('.cat-lock-ico');
                if (_ic) _ic.remove();
            }
        });
    }

    // ── Import Modal (3-step) ──

    const CONFIRM_BTN_HTML = '<i class="fa-solid fa-cart-shopping"></i> Purchase & Download';
    const STEP_TITLES = { 1: 'Import Settings', 2: 'AI Processing', 3: 'Confirm & Purchase' };
    // LIVE catalogue prices (was hard-coded and drifted from real billing). Flat ops bill
    // per item; translate ops bill PER TARGET LANGUAGE — use aiOpCost(), never AI_COST directly.
    const AI_COST = <?= json_encode([
        'title'     => (float) ($_priceAi['rewrite_title'] ?? 0.30),
        'translate' => (float) ($_priceAi['translate'] ?? 0.40),
        'desc'      => (float) ($_priceAi['rewrite_description'] ?? 0.30),
        'transdesc' => (float) ($_priceAi['translate_description'] ?? 0.40),
    ]) ?>;
    const AI_COST_PER_LANG = <?= json_encode([
        'translate' => isset($_priceAiPerLang['translate']) ? (float) $_priceAiPerLang['translate'] : null,
        'transdesc' => isset($_priceAiPerLang['translate_description']) ? (float) $_priceAiPerLang['translate_description'] : null,
    ]) ?>;
    let importStep = 1;
    let importBaseCost = 0; // credits cost (videos only, no AI)

    // Languages injected from PHP
    const AI_LANGUAGES = <?= json_encode(array_map(fn($l) => ['code' => $l['code'], 'name' => $l['name']], $_enabledLangs)) ?>;
    const AI_DEFAULT_LANG = <?= json_encode($_defaultLang['name'] ?? 'English') ?>;

    // Credits for one AI op on one item. Translate ops: per-language price x enabled
    // languages (falls back to the flat price when the server has no per-lang pricing).
    function aiOpCost(type) {
        if (type === 'translate' || type === 'transdesc') {
            const per = AI_COST_PER_LANG[type];
            if (per !== null && AI_LANGUAGES.length > 0) return per * AI_LANGUAGES.length;
        }
        return AI_COST[type];
    }

    function updateAiCostUsd() {
        const fmt = (cr) => '$' + (cr * creditRate).toFixed(3);
        const map = { title: 'catAiTitleUsd', translate: 'catAiTranslateUsd', desc: 'catAiDescUsd', transdesc: 'catAiTransDescUsd' };
        for (const [type, id] of Object.entries(map)) {
            const el = document.getElementById(id);
            if (el) el.textContent = '(' + fmt(aiOpCost(type)) + ')';
        }
    }

    // ── AI Test Prompt ──
    const aiTestCounts = { title: 0, translate: 0, desc: 0, transdesc: 0 };
    const AI_MAX_TESTS = 3;
    const AI_TEST_BTN_DEFAULT = {
        title: '<i class="fa-solid fa-flask text-xs"></i> Test with random video',
        translate: '<i class="fa-solid fa-flask text-xs"></i> Test with random video',
        desc: '<i class="fa-solid fa-flask text-xs"></i> Test with random video',
        transdesc: '<i class="fa-solid fa-flask text-xs"></i> Test with random video',
    };

    function capType(type) {
        return { title: 'Title', translate: 'Translate', desc: 'Desc', transdesc: 'TransDesc' }[type] || type;
    }

    function updateTestButtons() {
        ['title', 'translate', 'desc', 'transdesc'].forEach(type => {
            const btn = document.getElementById('catAi' + capType(type) + 'TestBtn');
            if (!btn) return;
            const cbMap = { title: 'catAiRewriteTitle', translate: 'catAiTranslate', desc: 'catAiRewriteDesc', transdesc: 'catAiTransDesc' };
            const cb = document.getElementById(cbMap[type]);
            const checked = cb ? cb.checked : false;
            const hasVideos = selectedVideos.size > 0;
            const underLimit = aiTestCounts[type] < AI_MAX_TESTS;
            btn.disabled = !(checked && hasVideos && underLimit);
        });
    }

    function aiTestResultHTML(type, original, aiResult, color, videoTitle) {
        const truncTitle = videoTitle.length > 60 ? videoTitle.substring(0, 60) + '...' : videoTitle;
        const labelOriginal = type === 'desc' ? 'Original Description' : 'Original Title';
        const labelAi = type === 'desc' ? 'AI Description' : 'AI Title';
        return '<div class="px-3 py-2 bg-gray-50 border-b text-[10px] text-gray-500 font-medium">'
            + 'Testing on: &quot;' + escH(truncTitle) + '&quot;</div>'
            + '<div class="px-3 py-2 border-b border-gray-100">'
            + '<div class="text-[10px] font-semibold text-gray-400 uppercase mb-0.5">' + labelOriginal + '</div>'
            + '<div class="text-gray-600">' + escH(original) + '</div></div>'
            + '<div class="px-3 py-2 bg-' + color + '-50/30">'
            + '<div class="text-[10px] font-semibold text-' + color + '-500 uppercase mb-0.5">' + labelAi + '</div>'
            + '<div class="text-gray-900 font-medium">' + escH(aiResult) + '</div></div>';
    }

    function aiTestTranslateHTML(originalTitle, translations, color) {
        const truncTitle = originalTitle.length > 60 ? originalTitle.substring(0, 60) + '...' : originalTitle;
        let html = '<div class="px-3 py-2 bg-gray-50 border-b text-[10px] text-gray-500 font-medium">'
            + 'Testing on: &quot;' + escH(truncTitle) + '&quot;</div>'
            + '<div class="px-3 py-2 border-b border-gray-100">'
            + '<span class="font-semibold text-gray-400 text-[10px]">ORIGINAL</span>'
            + '<span class="ml-2 text-gray-600">' + escH(originalTitle) + '</span></div>';
        for (const [code, text] of Object.entries(translations)) {
            html += '<div class="px-3 py-1.5 border-b last:border-0 bg-' + color + '-50/30">'
                + '<span class="font-semibold text-' + color + '-500 text-[10px] w-6 inline-block">' + escH(code) + '</span>'
                + '<span class="ml-2 text-gray-900">' + escH(text) + '</span></div>';
        }
        return html;
    }

    function aiTestErrorHTML(message) {
        return '<div class="px-3 py-2 bg-red-50 text-red-600 text-xs ai-test-result-enter">'
            + '<i class="fa-solid fa-circle-exclamation inline -mt-0.5 mr-1"></i>'
            + escH(message) + '</div>';
    }

    function escH(str) {
        const d = document.createElement('div');
        d.textContent = str;
        return d.innerHTML;
    }

    async function testAiPrompt(type) {
        if (aiTestCounts[type] >= AI_MAX_TESTS || selectedVideos.size === 0) return;

        // Pick random video from selection. For description-based tests, prefer a
        // selected video that actually has a description (freshly-scraped catalogue
        // videos may not have one yet — the description worker fills them oldest-first).
        let videos = Array.from(selectedVideos.values());
        if (type === 'desc' || type === 'transdesc') {
            const withDesc = videos.filter(v => ((v.description || '') + '').trim() !== '');
            if (withDesc.length) videos = withDesc;
        }
        const video = videos[Math.floor(Math.random() * videos.length)];

        // Get user prompt from textarea
        const promptMap = { title: 'catAiTitlePromptText', translate: 'catAiTranslatePromptText', desc: 'catAiDescPromptText', transdesc: 'catAiTransDescPromptText' };
        const prompt = document.getElementById(promptMap[type]).value.trim();
        if (!prompt) return;

        // Show loading state
        const cap = capType(type);
        const btn = document.getElementById('catAi' + cap + 'TestBtn');
        const prevHTML = btn.innerHTML;
        btn.disabled = true;
        btn.innerHTML = '<i class="fa-solid fa-spinner-third fa-spin text-xs"></i> Testing...';

        // Build request body
        const body = { type, prompt, title: video.title || '', description: video.description || '' };
        if (type === 'translate' || type === 'transdesc') {
            body.languages = AI_LANGUAGES;
            body.translate_format = AI_LANGUAGES.map(l => l.name + ': <' + l.code.toUpperCase() + '></' + l.code.toUpperCase() + '>').join('\n');
        }

        try {
            const resp = await fetch('/admin/catalogue/test-ai', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json', 'X-Csrf-Token': CSRF },
                body: JSON.stringify(body)
            });
            const data = await resp.json();

            // Increment counter
            aiTestCounts[type]++;
            const countEl = document.getElementById('catAi' + cap + 'TestCount');
            if (countEl) countEl.textContent = aiTestCounts[type] + ' / ' + AI_MAX_TESTS;

            // Render result panel
            const panel = document.getElementById('catAi' + cap + 'TestResult');
            panel.classList.remove('hidden');
            panel.classList.add('ai-test-result-enter');

            if (data.error) {
                panel.innerHTML = aiTestErrorHTML(data.error);
                btn.innerHTML = AI_TEST_BTN_DEFAULT[type];
                updateTestButtons();
                return;
            }

            const content = data.content || '';
            if (type === 'title') {
                const match = content.match(/<title>(.*?)<\/title>/si);
                const aiTitle = match ? match[1].trim() : content.trim();
                panel.innerHTML = aiTestResultHTML('title', video.title || '', aiTitle, 'indigo', video.title || '');
            } else if (type === 'translate') {
                const translations = {};
                AI_LANGUAGES.forEach(l => {
                    const re = new RegExp('<' + l.code.toUpperCase() + '>([\\s\\S]*?)</' + l.code.toUpperCase() + '>', 'i');
                    const m = content.match(re);
                    if (m) translations[l.code.toUpperCase()] = m[1].trim();
                });
                panel.innerHTML = Object.keys(translations).length
                    ? aiTestTranslateHTML(video.title || '', translations, 'sky')
                    : aiTestErrorHTML('The model returned no translations in the expected format. Please click Test again.');
            } else if (type === 'desc') {
                const match = content.match(/<desc>(.*?)<\/desc>/si);
                const aiDesc = match ? match[1].trim() : content.trim();
                panel.innerHTML = aiTestResultHTML('desc', video.description || '', aiDesc, 'emerald', video.title || '');
            } else if (type === 'transdesc') {
                const translations = {};
                AI_LANGUAGES.forEach(l => {
                    const re = new RegExp('<' + l.code.toUpperCase() + '>([\\s\\S]*?)</' + l.code.toUpperCase() + '>', 'i');
                    const m = content.match(re);
                    if (m) translations[l.code.toUpperCase()] = m[1].trim();
                });
                panel.innerHTML = Object.keys(translations).length
                    ? aiTestTranslateHTML(video.description || '', translations, 'teal')
                    : aiTestErrorHTML('The model returned no translations in the expected format. Please click Test again.');
            }
        } catch (e) {
            const panel = document.getElementById('catAi' + cap + 'TestResult');
            panel.classList.remove('hidden');
            panel.classList.add('ai-test-result-enter');
            panel.innerHTML = aiTestErrorHTML('Network error: ' + e.message);
        }

        // Restore button
        btn.innerHTML = AI_TEST_BTN_DEFAULT[type];
        updateTestButtons();
    }

    function openImportModal() {
        if (selectedVideos.size === 0) return;
        if (activeMedia === 'galleries') { openGalleryImportModal(); return; }

        const apiKey = document.getElementById('catApiKey').value;
        if (!apiKey) {
            autoConnect();
            return;
        }

        // Populate header info
        const count = selectedVideos.size;
        let totalCost = 0;
        let count480 = 0;
        selectedVideos.forEach(v => {
            totalCost += parseFloat(v.cost || 0);
            if (v.has_480p) count480++;
        });
        importBaseCost = totalCost;

        document.getElementById('catImportCount').textContent = count;
        updateImportCostDisplay();

        // Update AI cost USD display
        updateAiCostUsd();

        // Reset AI options, test counters and panels
        const aiColorMap = { title: 'indigo', translate: 'sky', desc: 'emerald', transdesc: 'teal' };
        ['title', 'translate', 'desc', 'transdesc'].forEach(t => {
            aiTestCounts[t] = 0;
            const cap = capType(t);
            const countEl = document.getElementById('catAi' + cap + 'TestCount');
            if (countEl) countEl.textContent = '0 / ' + AI_MAX_TESTS;
            const panel = document.getElementById('catAi' + cap + 'TestResult');
            if (panel) { panel.classList.add('hidden'); panel.innerHTML = ''; }
            // Close expanded AI card bodies
            const promptEl = document.getElementById('catAi' + cap + 'Prompt');
            if (promptEl) promptEl.classList.remove('open');
            // Reset card border styles
            const blockEl = document.getElementById('catAi' + cap + 'Block');
            if (blockEl) {
                const c = aiColorMap[t];
                blockEl.classList.add('border-gray-200');
                blockEl.classList.remove('border-' + c + '-300', 'bg-' + c + '-50/20', 'shadow-sm');
            }
        });

        // Reset to step 1
        importStep = 1;
        updateImportStepUI();

        const btn = document.getElementById('catImportConfirmBtn');
        btn.disabled = false;
        btn.innerHTML = CONFIRM_BTN_HTML;

        // Show modal
        // Hentai videos credit "Artists" rather than "Pornstars" for the performer field.
        var _perfLabelEl = document.getElementById('catImpPerfLabel');
        if (_perfLabelEl) _perfLabelEl.textContent = (activeVideoType === 'hentai') ? 'Artists' : 'Pornstars';
        if (window.tcInit) tcInit();
        document.getElementById('catImportOverlay').classList.remove('hidden');
        document.getElementById('catImportModal').classList.remove('hidden');
        document.body.style.overflow = 'hidden';
    }

    function closeImportModal() {
        document.getElementById('catImportOverlay').classList.add('hidden');
        document.getElementById('catImportModal').classList.add('hidden');
        document.body.style.overflow = '';
    }

    function goToImportStep(step) {
        if (step < 1 || step > 3) return;
        importStep = step;
        updateImportStepUI();
    }

    function nextImportStep() {
        if (importStep === 2) saveAiPrompts();
        if (importStep < 3) {
            importStep++;
            if (importStep === 3) buildSummary();
            updateImportStepUI();
        }
    }

    function saveAiPrompts() {
        var body = {
            title_prompt: (document.getElementById('catAiTitlePromptText')?.value || '').trim(),
            translate_prompt: (document.getElementById('catAiTranslatePromptText')?.value || '').trim(),
            desc_prompt: (document.getElementById('catAiDescPromptText')?.value || '').trim(),
            translate_desc_prompt: (document.getElementById('catAiTransDescPromptText')?.value || '').trim(),
        };
        fetch('/admin/catalogue/save-ai-prompts', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': '<?= Router::csrfToken() ?>' },
            body: JSON.stringify(body),
        }).catch(function() {});
    }

    function prevImportStep() {
        if (importStep > 1) {
            importStep--;
            updateImportStepUI();
        }
    }

    function updateImportStepUI() {
        // Show/hide step content with fade animation
        [1, 2, 3].forEach(s => {
            const el = document.getElementById('catImportStep' + s);
            if (s === importStep) {
                el.classList.remove('hidden');
                // Re-trigger animation
                el.classList.remove('import-step-fade');
                void el.offsetWidth;
                el.classList.add('import-step-fade');
            } else {
                el.classList.add('hidden');
            }
        });

        // Update title
        document.getElementById('catImportStepTitle').textContent = STEP_TITLES[importStep];

        // Update stepper circles + labels
        document.querySelectorAll('#catImportSteps .cat-step-dot').forEach(dot => {
            const s = parseInt(dot.dataset.step);
            const circle = dot.querySelector('span:first-child');
            const label = dot.querySelector('span:last-child');
            if (s < importStep) {
                // Completed
                circle.className = 'w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold bg-green-500 text-white shadow-sm shadow-green-200 transition-all duration-300';
                circle.innerHTML = '<i class="fa-solid fa-check text-xs"></i>';
                label.className = 'text-[11px] font-semibold text-green-600 transition-colors';
            } else if (s === importStep) {
                // Active
                circle.className = 'w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold bg-primary-600 text-white shadow-sm shadow-primary-200 transition-all duration-300 ring-2 ring-primary-200';
                circle.innerHTML = s;
                label.className = 'text-[11px] font-semibold text-primary-700 transition-colors';
            } else {
                // Future
                circle.className = 'w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold bg-gray-200 text-gray-400 transition-all duration-300';
                circle.innerHTML = s;
                label.className = 'text-[11px] font-medium text-gray-400 transition-colors';
            }
        });

        // Update connector lines
        document.querySelectorAll('#catImportSteps [data-line]').forEach(line => {
            const l = parseInt(line.dataset.line);
            line.className = 'flex-1 h-0.5 mx-1 rounded-full import-stepper-line ' + (l < importStep ? 'bg-green-400' : 'bg-gray-200');
        });

        // Update footer buttons
        const backBtn = document.getElementById('catImportBackBtn');
        const cancelBtn = document.getElementById('catImportCancelBtn');
        const nextBtn = document.getElementById('catImportNextBtn');
        const confirmBtn = document.getElementById('catImportConfirmBtn');

        backBtn.classList.toggle('hidden', importStep === 1);
        cancelBtn.classList.toggle('hidden', importStep > 1);
        nextBtn.classList.toggle('hidden', importStep === 3);
        confirmBtn.classList.toggle('hidden', importStep !== 3);

        if (window.tcGateProceed) window.tcGateProceed();
    }

    function toggleAiOption(type) {
        const map = {
            title: { cb: 'catAiRewriteTitle', prompt: 'catAiTitlePrompt', block: 'catAiTitleBlock', color: 'indigo' },
            translate: { cb: 'catAiTranslate', prompt: 'catAiTranslatePrompt', block: 'catAiTranslateBlock', color: 'sky' },
            desc: { cb: 'catAiRewriteDesc', prompt: 'catAiDescPrompt', block: 'catAiDescBlock', color: 'emerald' },
            transdesc: { cb: 'catAiTransDesc', prompt: 'catAiTransDescPrompt', block: 'catAiTransDescBlock', color: 'teal' },
        };
        const cfg = map[type];
        if (!cfg) return;
        const cbEl = document.getElementById(cfg.cb);
        if (!cbEl) return;
        const checked = cbEl.checked;
        const promptEl = document.getElementById(cfg.prompt);
        const blockEl = document.getElementById(cfg.block);
        // Animate expand/collapse
        if (promptEl) promptEl.classList.toggle('open', checked);
        if (blockEl) {
            if (checked) {
                blockEl.classList.remove('border-gray-200');
                blockEl.classList.add('border-' + cfg.color + '-300', 'bg-' + cfg.color + '-50/20', 'shadow-sm');
            } else {
                blockEl.classList.add('border-gray-200');
                blockEl.classList.remove('border-' + cfg.color + '-300', 'bg-' + cfg.color + '-50/20', 'shadow-sm');
            }
        }

        // Update translate notices when rewrite state changes
        updateTranslateRewriteNotice();
        updateTransDescRewriteNotice();

        // Update AI test buttons enable/disable
        updateTestButtons();

        // Recalculate total cost in header
        updateImportCostDisplay();
    }

    function updateTranslateRewriteNotice() {
        const notice = document.getElementById('catAiTranslateRewriteNotice');
        if (!notice) return;
        const titleChecked = document.getElementById('catAiRewriteTitle')?.checked || false;
        const translateChecked = document.getElementById('catAiTranslate')?.checked || false;
        notice.classList.toggle('hidden', !(titleChecked && translateChecked));
    }

    function updateTransDescRewriteNotice() {
        const notice = document.getElementById('catAiTransDescRewriteNotice');
        if (!notice) return;
        const descChecked = document.getElementById('catAiRewriteDesc')?.checked || false;
        const transDescChecked = document.getElementById('catAiTransDesc')?.checked || false;
        notice.classList.toggle('hidden', !(descChecked && transDescChecked));
    }

    function calcAiCostPerVideo() {
        let cost = 0;
        if (document.getElementById('catAiRewriteTitle')?.checked) cost += AI_COST.title;
        if (document.getElementById('catAiTranslate')?.checked) cost += aiOpCost('translate');
        if (document.getElementById('catAiRewriteDesc')?.checked) cost += AI_COST.desc;
        if (document.getElementById('catAiTransDesc')?.checked) cost += aiOpCost('transdesc');
        return cost;
    }

    function updateImportCostDisplay() {
        const count = selectedVideos.size;
        const aiPerVideo = calcAiCostPerVideo();
        const aiTotalCredits = aiPerVideo * count;
        const totalCredits = importBaseCost + aiTotalCredits;
        const totalUsd = totalCredits * creditRate;

        let text = formatCredits(totalCredits) + ' Credits (~$' + totalUsd.toFixed(2) + ')';
        if (aiPerVideo > 0) {
            text += ' <span class="text-xs text-indigo-500">(incl. ' + formatCredits(aiTotalCredits) + ' AI)</span>';
        }

        document.getElementById('catImportCost').innerHTML = text;
    }

    // -- Output-quality controller: 3 outcomes (keep / stream / shrink) + live per-selection breakdown --
    function tcActiveFormats() { return (window.TC_FORMATS || []).slice().sort(function (a, b) { return b.h - a.h; }); } // high -> low
    function tcParseH(q) { var m = /(\d+)/.exec(q || ''); return m ? parseInt(m[1], 10) : 0; }
    function tcFmt(n) { try { return (n || 0).toLocaleString('en-US'); } catch (e) { return '' + (n || 0); } }
    function tcN() { try { return selectedVideos.size; } catch (e) { return 0; } }
    function tcByQ() {
        var by = {}, unknown = 0;
        try { selectedVideos.forEach(function (v) { var h = tcParseH(v.quality); if (h > 0) by[h] = (by[h] || 0) + 1; else unknown++; }); } catch (e) {}
        return { by: by, unknown: unknown };
    }
    function tcMaxSrc() { var b = tcByQ().by, mx = 0; for (var k in b) { if (+k > mx) mx = +k; } return mx; }
    function tcOutcome() { var r = document.querySelector('input[name="tcOut"]:checked'); return r ? r.value : 'stream'; }
    function tcCapOptions() { var mx = tcMaxSrc(); return tcActiveFormats().filter(function (f) { return f.h < mx; }); } // rungs strictly below the biggest source
    function tcCap() { var sel = document.getElementById('tcCapSel'); if (sel && sel.value) return parseInt(sel.value, 10); var o = tcCapOptions(); return o.length ? o[0].h : 0; }
    function tcDelCount(C) { var b = tcByQ().by, n = 0; for (var k in b) { if (+k > C) n += b[k]; } return n; }

    function tcComputeFormats() {
        var out = tcOutcome(), fmts = tcActiveFormats(), list = [];
        if (out === 'stream') list = fmts.map(function (f) { return f.task; });                 // ALL active rungs => originals always kept + ladder
        else if (out === 'shrink') { var C = tcCap(); list = fmts.filter(function (f) { return f.h <= C; }).map(function (f) { return f.task; }); }
        // 'keep' => [] (no transcode)
        if (window.TC_TIMELINE) list.push('timeline'); // honor the global Settings → Transcoding → Timeline toggle automatically
        return list;
    }
    window.tcComputeFormats = tcComputeFormats;

    function tcPopulateCap() {
        var sel = document.getElementById('tcCapSel'); if (!sel) return;
        var opts = tcCapOptions(), prev = sel.value;
        sel.innerHTML = opts.map(function (f) {
            var lbl = f.h >= 2160 ? '2160p (4K)' : (f.h >= 1440 ? '1440p (2K)' : f.h + 'p');
            var del = tcDelCount(f.h);
            return '<option value="' + f.h + '">' + lbl + '  ·  deletes ' + tcFmt(del) + ' original' + (del === 1 ? '' : 's') + '</option>';
        }).join('');
        if (prev && opts.some(function (f) { return f.h == prev; })) sel.value = prev;
    }

    function tcDestructive() { return tcOutcome() === 'shrink' && tcDelCount(tcCap()) > 0; }
    function tcGateProceed() {
        var btn = document.getElementById('catImportNextBtn'); if (!btn) return;
        var cc = document.getElementById('tcConfirm');
        var blocked = (typeof importStep !== 'undefined') && importStep === 1 && tcDestructive() && !(cc && cc.checked);
        btn.disabled = blocked;
        btn.classList.toggle('opacity-40', blocked);
        btn.classList.toggle('cursor-not-allowed', blocked);
    }
    window.tcGateProceed = tcGateProceed;

    function tcRender() {
        var n = tcN(), out = tcOutcome(), q = tcByQ(), by = q.by;
        var heights = Object.keys(by).map(Number).sort(function (a, b) { return b - a; });
        var chips = heights.map(function (h) { return tcFmt(by[h]) + ' × ' + h + 'p'; }).join(' ');

        var sub = document.getElementById('tcSubtitle');
        if (sub) sub.textContent = (n === 1) ? 'What should we do with this video after import?' : ('What should we do with these ' + tcFmt(n) + ' videos after import?');

        // hide 'Shrink' if nothing can be shrunk (whole selection already at the lowest active rung)
        var shrinkRadio = document.querySelector('input[name="tcOut"][value="shrink"]');
        var canShrink = tcCapOptions().length > 0;
        if (shrinkRadio) { var card = shrinkRadio.closest('.tc-out'); if (card) card.style.display = canShrink ? '' : 'none'; if (!canShrink && out === 'shrink') { var sr = document.querySelector('input[name="tcOut"][value="stream"]'); if (sr) sr.checked = true; out = 'stream'; } }

        var capWrap = document.getElementById('tcCapWrap'); if (capWrap) capWrap.style.display = (out === 'shrink') ? 'inline-flex' : 'none';
        if (out === 'shrink') tcPopulateCap();

        var box = document.getElementById('tcReassure'), confirmWrap = document.getElementById('tcConfirmWrap'), confirmLbl = document.getElementById('tcConfirmLbl');
        var cls = '', msg = '';

        if (out === 'keep') {
            cls = 'bg-gray-50 border border-gray-200 text-gray-600';
            msg = '<div class="flex items-start gap-2"><i class="fa-solid fa-circle-info mt-0.5 shrink-0 text-gray-400"></i><div><b>' + tcFmt(n) + (n === 1 ? ' video' : ' videos') + '</b> kept exactly as imported. No conversion, nothing deleted.</div></div>';
            if (confirmWrap) confirmWrap.style.display = 'none';
        } else if (out === 'stream') {
            cls = 'bg-blue-50/60 border border-blue-100 text-blue-800';
            msg = '<div class="flex items-start gap-2"><i class="fa-solid fa-shield-halved mt-0.5 shrink-0 text-blue-400"></i><div>We never enlarge a video &mdash; each one stays at its own quality or lower, never higher.'
                + (heights.length ? '<div class="mt-1 text-blue-500/90"><span class="text-blue-400">In this selection:</span> ' + chips + '</div>' : '')
                + '<div class="mt-1 font-medium">&rarr; all ' + tcFmt(n) + ' kept at their own quality, with smaller versions added.</div></div></div>';
            if (confirmWrap) confirmWrap.style.display = 'none';
        } else {
            var C = tcCap(), del = tcDelCount(C), capLabel = C + 'p';
            var rows = heights.map(function (h) {
                if (h > C) return '<div>' + tcFmt(by[h]) + ' × ' + h + 'p <span class="font-semibold text-amber-700">→ ' + capLabel + '</span></div>';
                return '<div>' + tcFmt(by[h]) + ' × ' + h + 'p <span class="text-gray-400">→ stays ' + h + 'p (untouched)</span></div>';
            }).join('');
            if (del > 0) {
                cls = 'bg-amber-50 border border-amber-200 text-amber-800';
                msg = '<div class="flex items-start gap-2"><i class="fa-solid fa-triangle-exclamation mt-0.5 shrink-0 text-amber-500"></i><div>'
                    + 'This permanently <b>deletes the original of ' + tcFmt(del) + ' video' + (del === 1 ? '' : 's') + '</b> and shrinks them to ' + capLabel + '. Cannot be undone.'
                    + '<div class="mt-1 text-amber-700/90">We never enlarge &mdash; smaller videos are left exactly as they are.</div>'
                    + '<div class="mt-1.5 grid gap-0.5 leading-snug">' + rows + '</div>'
                    + (q.unknown ? '<div class="mt-1 text-amber-600/80">' + tcFmt(q.unknown) + ' with unknown quality are kept safe (never deleted).</div>' : '')
                    + '</div></div>';
                if (confirmWrap && confirmLbl) { confirmWrap.style.display = 'flex'; confirmLbl.textContent = 'I understand ' + tcFmt(del) + ' original file' + (del === 1 ? '' : 's') + ' will be permanently deleted.'; }
            } else {
                cls = 'bg-blue-50/60 border border-blue-100 text-blue-800';
                msg = '<div class="flex items-start gap-2"><i class="fa-solid fa-circle-info mt-0.5 shrink-0 text-blue-400"></i><div>Every video is already ' + capLabel + ' or smaller &mdash; nothing is deleted.<div class="mt-1.5 grid gap-0.5 leading-snug">' + rows + '</div></div></div>';
                if (confirmWrap) confirmWrap.style.display = 'none';
            }
        }
        if (box) { box.className = 'mt-2.5 px-3 py-2.5 rounded-lg text-[11px] leading-relaxed ' + cls; box.innerHTML = msg; }
        tcGateProceed();
    }

    window.tcInit = function () { tcRender(); };
    document.addEventListener('change', function (e) {
        if (!e.target) return;
        if (e.target.name === 'tcOut' || e.target.id === 'tcCapSel') { var cc = document.getElementById('tcConfirm'); if (cc) cc.checked = false; tcRender(); }
        else if (e.target.id === 'tcConfirm') tcGateProceed();
    });
    function getImportSettings() {
        const settings = {};
        document.querySelectorAll('#catImportStep1 .cat-import-opt').forEach(label => {
            const key = label.dataset.key;
            if (!key || label.hasAttribute('data-locked')) return;
            const cb = label.querySelector('input[type="checkbox"]');
            if (!cb.classList.contains('cat-format-cb')) {
                settings[key] = cb.checked;
            }
        });
        settings.thumbnail = true;
        settings.thumbnail_small = true;
        settings.transcode_formats = (typeof window.tcComputeFormats === 'function') ? window.tcComputeFormats() : [];
        return settings;
    }

    function getAiSettings() {
        const ai = { enabled: false };

        const titleCb = document.getElementById('catAiRewriteTitle');
        if (titleCb && titleCb.checked) {
            ai.enabled = true;
            ai.rewrite_title = true;
            ai.title_prompt = document.getElementById('catAiTitlePromptText').value.trim();
        }

        const translateCb = document.getElementById('catAiTranslate');
        if (translateCb && translateCb.checked && AI_LANGUAGES.length > 0) {
            ai.enabled = true;
            ai.translate = true;
            ai.translate_prompt = document.getElementById('catAiTranslatePromptText').value.trim();
            ai.translate_languages = AI_LANGUAGES;
            // Build the output format string
            ai.translate_format = AI_LANGUAGES.map(l => l.name + ': <' + l.code.toUpperCase() + '></' + l.code.toUpperCase() + '>').join('\n');
        }

        const descCb = document.getElementById('catAiRewriteDesc');
        if (descCb && descCb.checked) {
            ai.enabled = true;
            ai.rewrite_description = true;
            ai.desc_prompt = document.getElementById('catAiDescPromptText').value.trim();
        }

        const transDescCb = document.getElementById('catAiTransDesc');
        if (transDescCb && transDescCb.checked && AI_LANGUAGES.length > 0) {
            ai.enabled = true;
            ai.translate_description = true;
            ai.translate_desc_prompt = document.getElementById('catAiTransDescPromptText').value.trim();
            ai.translate_languages = AI_LANGUAGES;
            ai.translate_format = AI_LANGUAGES.map(l => l.name + ': <' + l.code.toUpperCase() + '></' + l.code.toUpperCase() + '>').join('\n');
        }

        return ai;
    }

    function buildSummary() {
        const settings = getImportSettings();
        const ai = getAiSettings();
        const count = selectedVideos.size;
        let totalCost = 0;
        selectedVideos.forEach(v => { totalCost += parseFloat(v.cost || 0); });

        const qualities = [];
        var _out = (typeof tcOutcome === 'function') ? tcOutcome() : 'stream';
        var _vidFormats = (settings.transcode_formats || []).filter(f => f !== 'timeline');
        if (_out === 'keep' || _vidFormats.length === 0) {
            qualities.push('Original quality only');
        } else if (_out === 'shrink') {
            var _C = (typeof tcCap === 'function') ? tcCap() : 0;
            qualities.push('Shrink to ' + _C + 'p max — bigger originals deleted');
        } else {
            qualities.push('Full quality + smaller streaming versions');
        }
        const media = ['Thumbnail', 'Small Thumb'];
        if (settings.preview) media.push('Preview MP4');
        const meta = [];
        if (settings.description) meta.push('Description');
        if (settings.categories) meta.push('Categories');
        if (settings.tags) meta.push('Tags');
        if (settings.performers) meta.push('Pornstars');
        if (settings.views) meta.push('Views');

        const aiPerVideo = calcAiCostPerVideo();
        const aiTotalCredits = aiPerVideo * count;
        const grandTotalCredits = totalCost + aiTotalCredits;
        const grandTotalUsd = grandTotalCredits * creditRate;

        const check = '<i class="fa-solid fa-check text-green-500 text-[10px] w-4 text-center shrink-0"></i>';
        const dot = '<i class="fa-solid fa-circle text-gray-300 text-[4px] w-4 text-center shrink-0"></i>';

        let html = '<div class="rounded-xl border border-gray-200 overflow-hidden">';

        // ── Order header
        html += '<div class="px-4 py-3 bg-gray-50 border-b border-gray-100 flex items-center justify-between">';
        html += '<span class="text-xs font-bold text-gray-900 uppercase tracking-wider">Order Summary</span>';
        html += '<span class="text-xs text-gray-400">' + count + ' video(s)</span>';
        html += '</div>';

        // ── Items list
        html += '<div class="divide-y divide-gray-100">';

        // Quality
        html += '<div class="px-4 py-2.5 flex items-center gap-2 text-sm">' + check + '<span class="text-gray-700">Quality: <span class="font-medium text-gray-900">' + qualities.join(' + ') + '</span></span></div>';

        // Media
        media.forEach(m => {
            html += '<div class="px-4 py-2 flex items-center gap-2 text-sm">' + check + '<span class="text-gray-600">' + m + '</span></div>';
        });

        // Metadata
        if (meta.length) {
            html += '<div class="px-4 py-2 flex items-center gap-2 text-sm">' + check + '<span class="text-gray-600">Metadata: ' + meta.join(', ') + '</span></div>';
        }

        // AI options
        if (ai.enabled) {
            if (ai.rewrite_title) {
                html += '<div class="px-4 py-2 flex items-center justify-between text-sm">';
                html += '<span class="flex items-center gap-2"><i class="fa-solid fa-pen-fancy text-indigo-400 text-[10px] w-4 text-center shrink-0"></i><span class="text-gray-700">Rewrite Title</span></span>';
                html += '<span class="text-xs font-medium text-indigo-600">+' + (AI_COST.title * count).toFixed(2) + ' Cr</span>';
                html += '</div>';
            }
            if (ai.translate) {
                html += '<div class="px-4 py-2 flex items-center justify-between text-sm">';
                html += '<span class="flex items-center gap-2"><i class="fa-solid fa-language text-sky-400 text-[10px] w-4 text-center shrink-0"></i><span class="text-gray-700">Translate Title (' + AI_LANGUAGES.length + ' languages)</span></span>';
                html += '<span class="text-xs font-medium text-sky-600">+' + (aiOpCost('translate') * count).toFixed(2) + ' Cr</span>';
                html += '</div>';
            }
            if (ai.rewrite_description) {
                html += '<div class="px-4 py-2 flex items-center justify-between text-sm">';
                html += '<span class="flex items-center gap-2"><i class="fa-solid fa-paragraph text-emerald-400 text-[10px] w-4 text-center shrink-0"></i><span class="text-gray-700">Rewrite Description</span></span>';
                html += '<span class="text-xs font-medium text-emerald-600">+' + (AI_COST.desc * count).toFixed(2) + ' Cr</span>';
                html += '</div>';
            }
            if (ai.translate_description) {
                html += '<div class="px-4 py-2 flex items-center justify-between text-sm">';
                html += '<span class="flex items-center gap-2"><i class="fa-solid fa-language text-teal-400 text-[10px] w-4 text-center shrink-0"></i><span class="text-gray-700">Translate Description (' + AI_LANGUAGES.length + ' languages)</span></span>';
                html += '<span class="text-xs font-medium text-teal-600">+' + (aiOpCost('transdesc') * count).toFixed(2) + ' Cr</span>';
                html += '</div>';
            }
        }

        html += '</div>';

        // ── Total
        html += '<div class="px-4 py-3 bg-green-50/60 border-t border-green-100">';
        html += '<div class="flex items-center justify-between text-xs text-gray-500 mb-1">';
        html += '<span>Content (' + count + ' videos)</span><span>' + formatCredits(totalCost) + ' Cr</span></div>';
        if (aiPerVideo > 0) {
            html += '<div class="flex items-center justify-between text-xs text-gray-500 mb-1.5">';
            html += '<span>AI Processing</span><span>' + formatCredits(aiTotalCredits) + ' Cr</span></div>';
        }
        html += '<div class="flex items-center justify-between pt-1.5 border-t border-green-200">';
        html += '<span class="text-sm font-bold text-gray-900">Total</span>';
        html += '<span class="text-base font-bold text-green-700">' + formatCredits(grandTotalCredits) + ' Credits <span class="text-xs font-normal text-gray-400">(~$' + grandTotalUsd.toFixed(2) + ')</span></span>';
        html += '</div></div>';

        html += '</div>';

        document.getElementById('catImportSummary').innerHTML = html;
    }

    function validateImportQuality() {
        return true;
    }

    async function confirmImport() {
        if (selectedVideos.size === 0) return;

        // Safety net: never delete originals without an explicit ticked confirmation.
        if (typeof tcDestructive === 'function' && tcDestructive()) {
            var _cc = document.getElementById('tcConfirm');
            if (!_cc || !_cc.checked) {
                alert('To shrink and delete the larger originals, please tick the confirmation box under "Shrink to save space" — or choose another option.');
                return;
            }
        }

        const settings = getImportSettings();
        const aiSettings = getAiSettings();

        const btn = document.getElementById('catImportConfirmBtn');
        btn.disabled = true;
        btn.innerHTML = '<i class="fa-solid fa-spinner-third fa-spin"></i> Purchasing...';

        // Send IDs only — the server re-fetches metadata by ID in batches of <=500.
        // (Shipping all per-video metadata used to OOM the request past ~40k videos.)
        const videoIds = Array.from(selectedVideos.keys());

        try {
            const resp = await fetch('/admin/catalogue/purchase', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': CSRF },
                body: JSON.stringify({
                    video_ids: videoIds,
                    import_settings: settings,
                    ai_settings: aiSettings
                })
            });
            const data = await resp.json();

            if (data.error) {
                btn.disabled = false;
                btn.innerHTML = CONFIRM_BTN_HTML;
                showNotification('Purchase failed: ' + data.error, 'error');
                return;
            }

            if (data.new_balance !== null && data.new_balance !== undefined) {
                updateBalance(data.new_balance);
            }

            closeImportModal();

            selectedVideos.clear();
            updateBulkBar();
            respCache.clear(); // imported flags changed — drop every cached page
            browse(currentPage);

            const msg = data.queued + ' video(s) queued for download.';
            showNotification(msg, 'success');
            showQueueLink();

        } catch (e) {
            btn.disabled = false;
            btn.innerHTML = CONFIRM_BTN_HTML;
            showNotification('Network error: ' + e.message, 'error');
        }
    }

    // ── Show link to download queue after purchase ──

    function showQueueLink() {
        let bar = document.getElementById('catQueueLink');
        if (bar) { bar.classList.remove('hidden'); return; }
        bar = document.createElement('div');
        bar.id = 'catQueueLink';
        bar.className = 'flex items-center justify-between gap-3 px-4 py-3 bg-blue-50 border border-blue-200 rounded-lg mb-4';
        bar.innerHTML = '<div class="flex items-center gap-2 text-sm text-blue-700">' +
            '<svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>' +
            '<span>Downloads are being processed...</span></div>' +
            '<a href="/admin/import/downloads" class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors no-underline">' +
            '<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"/></svg>' +
            'View Download Queue</a>';
        const grid = document.getElementById('catGrid');
        grid.parentNode.insertBefore(bar, grid);
    }

    // ── Helpers ──

    function showLoading(text) {
        document.getElementById('catLoadingText').textContent = text || 'Loading...';
        document.getElementById('catLoading').classList.remove('hidden');
    }

    function hideLoading() {
        document.getElementById('catLoading').classList.add('hidden');
    }

    function showNotification(msg, type) {
        const colors = { success: 'bg-green-500', error: 'bg-red-500', info: 'bg-blue-500' };
        const div = document.createElement('div');
        div.className = `fixed top-4 right-4 z-50 px-4 py-3 rounded-lg text-white text-sm shadow-lg ${colors[type] || colors.info} transition-opacity`;
        div.textContent = msg;
        document.body.appendChild(div);
        setTimeout(() => { div.style.opacity = '0'; setTimeout(() => div.remove(), 300); }, 3000);
    }

    function formatDuration(seconds) {
        if (!seconds) return '';
        const h = Math.floor(seconds / 3600);
        const m = Math.floor((seconds % 3600) / 60);
        const s = seconds % 60;
        if (h > 0) return h + ':' + String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0');
        return m + ':' + String(s).padStart(2, '0');
    }

    function escHtml(str) {
        const div = document.createElement('div');
        div.textContent = str || '';
        return div.innerHTML;
    }

    function escAttr(str) {
        return (str || '').replace(/'/g, "\\'").replace(/"/g, '&quot;');
    }

    // Auto-init when catalogue tab is shown
    let initialized = false;
    const origSwitch = window.IMP_TABS.switchTo;
    window.IMP_TABS.switchTo = function(tab) {
        origSwitch(tab);
        if (tab === 'catalogue' && !initialized) {
            initialized = true;
            init();
        }
    };

    // ── Auto-Connect via tubepress.io popup ──

    function autoConnect() {
        const w = 480, h = 550;
        const left = (screen.width - w) / 2;
        const top = (screen.height - h) / 2;
        const popup = window.open(
            'https://tubepress.io/connect?origin=' + encodeURIComponent(window.location.origin),
            'tubepress_connect',
            'width=' + w + ',height=' + h + ',left=' + left + ',top=' + top + ',resizable=no,scrollbars=no'
        );
        if (!popup) {
            alert('Please allow popups for this site.');
        }
    }

    // Listen for postMessage from the connect popup
    // Note: tubepress-connect-error is handled by the MP module (included via _mp_modal.php)
    window.addEventListener('message', async function(e) {
        if (!e.data || e.data.type !== 'tubepress-connect') return;

        const { api_key, api_secret, credits } = e.data;

        // Fill in the config fields
        document.getElementById('catApiKey').value = api_key;
        document.getElementById('catApiSecret').value = api_secret;

        // Save the config to the CMS
        const status = document.getElementById('catConfigStatus');
        try {
            const resp = await fetch('/admin/catalogue/save-config', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': CSRF },
                body: JSON.stringify({
                    catalogue_api_key: api_key,
                    catalogue_api_secret: api_secret,
                    catalogue_api_url: 'https://tubepress.io/v1',
                    catalogue_auto_publish: document.getElementById('catAutoPublish').checked ? '1' : '0',
                    catalogue_download_thumbnails: document.getElementById('catDownloadThumbs').value,
                })
            });
            const data = await resp.json();
            if (data.configured && data.balance) {
                updateBalance(data.balance.credits);
                setConnected(true);
            } else if (credits !== undefined) {
                updateBalance(credits);
                setConnected(true);
            }
        } catch (err) {
            // Fallback: just use the credentials we received
            updateBalance(credits || 0);
            setConnected(true);
            fetchBalance();
        }

        // Update CSV import API state and re-render cards so AI options appear
        IMP_API_CONNECTED = true;
        if (window.IMP && window.IMP._refreshCards) {
            window.IMP._refreshCards();
        }
    });

    // Init immediately since catalogue is the default tab
    document.addEventListener('DOMContentLoaded', () => {
        initialized = true;
        init();
    });

    // ── Top Up Modal (delegated to shared MP modal) ──

    function openTopUpModal() {
        // Sync balance to MP before opening
        if (window.MP) {
            const balanceEl = document.getElementById('catBalanceAmount');
            if (balanceEl) {
                const text = balanceEl.textContent;
                const match = text.match(/([\d,]+)/);
                if (match) MP.updateBalance(parseFloat(match[1].replace(/,/g, '')));
            }
            MP.openTopUpModal();
        }
    }

    function closeTopUpModal() {
        if (window.MP) MP.closeTopUpModal();
    }

    function buyPack(packId) {
        if (window.MP) MP.buyPack(packId);
    }

    var _previewFP = null;

    function playPreview(url, title) {
        let modal = document.getElementById('previewModal');
        // Destroy previous FluidPlayer instance
        if (_previewFP) {
            try { _previewFP.destroy(); } catch(e) {}
            _previewFP = null;
        }
        if (!modal) {
            modal = document.createElement('div');
            modal.id = 'previewModal';
            modal.style.cssText = 'position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,0.8);display:flex;align-items:center;justify-content:center;padding:1rem;';
            modal.addEventListener('click', function(e) { if (e.target === modal) CAT.closePreviewModal(); });
            document.body.appendChild(modal);
        }
        modal.innerHTML = `
            <div style="position:relative;width:100%;max-width:720px;background:#000;border-radius:0.75rem;overflow:hidden;" class="fp-preview-modal">
                <div style="display:flex;align-items:center;justify-content:space-between;padding:0.5rem 0.75rem;background:#111;">
                    <span id="previewTitle" style="font-size:0.75rem;color:#ccc;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:85%;"></span>
                    <button onclick="CAT.closePreviewModal()" style="background:none;border:none;color:#999;font-size:1.2rem;cursor:pointer;padding:0.25rem;">&times;</button>
                </div>
                <div style="position:relative;aspect-ratio:16/9;background:#000;">
                    <video id="previewVideo"><source src="${escAttr(url)}" type="video/mp4"></video>
                </div>
            </div>`;
        modal.style.display = 'flex';
        document.getElementById('previewTitle').textContent = title;
        if (typeof fluidPlayer === 'function') {
            _previewFP = fluidPlayer('previewVideo', {
                layoutControls: {
                    fillToContainer: true,
                    primaryColor: getComputedStyle(document.documentElement).getPropertyValue('--primary').trim() || '#2563eb',
                    playButtonShowing: true,
                    playPauseAnimation: true,
                    autoPlay: true,
                    allowTheatre: false,
                    allowDownload: false,
                    doubleclickFullscreen: true,
                    playbackRateEnabled: true,
                    miniPlayer: { enabled: false },
                    controlBar: { autoHide: true, autoHideTimeout: 3, animated: true, playbackRates: ['x2','x1.5','x1','x0.5'] }
                }
            });
        }
    }

    function closePreviewModal() {
        if (_previewFP) {
            try { _previewFP.destroy(); } catch(e) {}
            _previewFP = null;
        }
        const modal = document.getElementById('previewModal');
        if (modal) {
            modal.innerHTML = '';
            modal.style.display = 'none';
        }
    }

    // ── Video Info Modal ──

    function infoRow(label, val) {
        return `<div class="flex items-baseline py-1 border-b border-gray-50">
            <span class="w-20 shrink-0 text-[10px] font-semibold text-gray-400 uppercase tracking-wide">${label}</span>
            <span class="text-xs text-gray-800 min-w-0">${val}</span>
        </div>`;
    }

    function infoChips(label, items, bgClass, textClass) {
        if (!items.length) return '';
        return `<div class="flex py-1 border-b border-gray-50">
            <span class="w-20 shrink-0 text-[10px] font-semibold text-gray-400 uppercase tracking-wide pt-0.5">${label}</span>
            <div class="flex flex-wrap gap-1 min-w-0">${items.map(t => `<span class="px-1.5 py-0.5 ${bgClass} ${textClass} text-[10px] rounded">${escHtml(t)}</span>`).join('')}</div>
        </div>`;
    }

    function showVideoInfo(id) {
        const v = currentVideos.find(x => x.remote_video_id === id);
        if (!v) return;

        let modal = document.getElementById('videoInfoModal');
        if (!modal) {
            modal = document.createElement('div');
            modal.id = 'videoInfoModal';
            modal.style.cssText = 'position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;padding:1rem;';
            modal.addEventListener('click', function(e) { if (e.target === modal) closeVideoInfoModal(); });
            document.body.appendChild(modal);
        }

        const dur = formatDuration(v.duration || 0);
        const cost = formatCredits(v.cost || 0);
        const usd = (parseFloat(v.cost || 0) * creditRate).toFixed(2);
        const cats = (v.categories || []).map(c => typeof c === 'string' ? c : (c.name || '')).filter(Boolean);
        const tags = (v.tags || []).map(t => typeof t === 'string' ? t : (t.name || '')).filter(Boolean);
        const perfs = (v.performers || []).map(p => typeof p === 'string' ? p : (p.name || '')).filter(Boolean);
        const createdAt = v.created_at || v.published_at || '';

        // Key-value rows next to thumbnail
        const viewsCount = v.views != null ? Number(v.views).toLocaleString() : '—';
        const sideRows =
            infoRow('Duration', dur || '—') +
            infoRow('Views', viewsCount) +
            infoRow('Quality', escHtml(v.quality || '—') + (v.has_480p ? ' <span class="text-emerald-600">+ 480p</span>' : '')) +
            infoRow('Source', escHtml(v.source ? v.source.charAt(0).toUpperCase() + v.source.slice(1) : '—')) +
            infoRow('Cost', `<span class="font-semibold text-amber-600">${cost} cr</span> <span class="text-gray-400">(~$${usd})</span>`);

        // Remaining detail rows
        let detailRows =
            infoRow('Date', escHtml(createdAt ? createdAt.substring(0, 10) : '—')) +
            infoRow('ID', `<span class="font-mono text-[11px] break-all">${escHtml(v.remote_video_id || '—')}</span>`);
        if (v.embed_url || v.video_url) detailRows += infoRow('URL', `<span class="break-all">${escHtml(v.embed_url || v.video_url)}</span>`);
        if (v.description) detailRows += `<div class="flex py-1 border-b border-gray-50">
            <span class="w-20 shrink-0 text-[10px] font-semibold text-gray-400 uppercase tracking-wide pt-0.5">Desc.</span>
            <p class="text-[11px] text-gray-600 leading-relaxed line-clamp-4 min-w-0">${escHtml(v.description)}</p>
        </div>`;

        const chipsRows =
            infoChips('Performers', perfs, 'bg-pink-50', 'text-pink-700') +
            infoChips('Categories', cats, 'bg-purple-50', 'text-purple-700') +
            infoChips('Tags', tags, 'bg-gray-100', 'text-gray-600');

        // Included assets
        const assets = [];
        const checkSvg = '<svg class="w-3 h-3 text-green-500 shrink-0" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/></svg>';
        assets.push(checkSvg + ' Full-length 720p HD video');
        if (v.has_480p) assets.push(checkSvg + ' Full-length 480p video');
        assets.push(checkSvg + ' High-quality thumbnail');
        if (v.thumbnail_small) assets.push(checkSvg + ' Small thumbnail (optimized)');
        if (v.preview) assets.push(checkSvg + ' Preview clip (15s)');
        const includedHtml = `<div class="mt-2 p-2 bg-gray-50 rounded-lg">
            <div class="text-[10px] font-semibold text-gray-400 uppercase tracking-wide mb-1">Included after purchase</div>
            <div class="flex flex-col gap-0.5">${assets.map(a => `<div class="flex items-center gap-1.5 text-[11px] text-gray-700">${a}</div>`).join('')}</div>
        </div>`;

        // Preview player
        let previewHtml = '';
        const infoVideoId = 'infoPreviewVideo_' + Date.now();
        if (v.preview) {
            previewHtml = `<div class="mt-3 fp-info-preview">
                <div style="aspect-ratio:16/9;background:#000;border-radius:0.375rem;overflow:hidden;">
                    <video id="${infoVideoId}"><source src="${escAttr(v.preview)}" type="video/mp4"></video>
                </div>
            </div>`;
        }

        modal.innerHTML = `
            <div style="position:relative;width:100%;max-width:500px;max-height:90vh;overflow-y:auto;background:#fff;border-radius:0.75rem;box-shadow:0 20px 40px rgba(0,0,0,0.2);">
                <div class="sticky top-0 bg-white z-10 flex items-center justify-between px-4 py-2 border-b border-gray-200 rounded-t-xl">
                    <h3 class="text-xs font-bold text-gray-900 truncate pr-3">${escHtml(v.title || 'Untitled')}</h3>
                    <button onclick="CAT.closeVideoInfoModal()" class="shrink-0 p-1 text-gray-400 hover:text-gray-600">
                        <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
                    </button>
                </div>
                <div class="p-3">
                    ${v.already_imported ? '<div class="mb-2 px-2 py-1 bg-green-50 border border-green-200 text-green-700 text-[10px] font-semibold rounded">Already imported</div>' : ''}
                    <div class="flex gap-3 mb-2">
                        <img src="${escAttr(v.thumbnail || '')}" alt="" class="w-40 aspect-video rounded object-cover shrink-0 bg-gray-100" onerror="this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 16 9%22><rect fill=%22%23e5e7eb%22 width=%2216%22 height=%229%22/></svg>'">
                        <div class="flex-1 min-w-0">${sideRows}</div>
                    </div>
                    ${detailRows}
                    ${chipsRows}
                    ${includedHtml}
                    ${previewHtml}
                </div>
            </div>`;
        modal.style.display = 'flex';
        // Init FluidPlayer on info modal preview
        if (v.preview && typeof fluidPlayer === 'function') {
            try {
                modal._fp = fluidPlayer(infoVideoId, {
                    layoutControls: {
                        fillToContainer: true,
                        primaryColor: getComputedStyle(document.documentElement).getPropertyValue('--primary').trim() || '#2563eb',
                        playButtonShowing: true,
                        playPauseAnimation: true,
                        autoPlay: false,
                        allowTheatre: false,
                        allowDownload: false,
                        doubleclickFullscreen: true,
                        miniPlayer: { enabled: false },
                        controlBar: { autoHide: true, autoHideTimeout: 3, animated: true }
                    }
                });
            } catch(e) {}
        }
    }

    function closeVideoInfoModal() {
        const modal = document.getElementById('videoInfoModal');
        if (modal) {
            // Destroy FluidPlayer instance
            if (modal._fp) {
                try { modal._fp.destroy(); } catch(e) {}
                modal._fp = null;
            }
            modal.style.display = 'none';
            modal.innerHTML = '';
        }
    }

    function selectMedia(m) {
        if (m === activeMedia) return;
        if (selectedVideos.size > 0) return; // locked: clear the selection before switching media type
        activeMedia = m;
        const gal = m === 'galleries';
        activeSource = '';
        activeCategories.clear(); activeQualities.clear(); activeHas480 = false;
        activeViewRanges.clear(); activeDurations.clear();
        activePerformers.clear(); activeChannels.clear(); activeTags.clear(); activeImages.clear();
        selectedVideos.clear(); updateBulkBar();
        document.querySelectorAll('.cat-media-tab').forEach(btn => {
            const on = btn.dataset.media === m;
            btn.className = 'cat-media-tab flex-1 py-1.5 text-[11px] font-bold tracking-wide rounded-md transition-all ' +
                (on ? 'bg-white shadow text-primary-700' : 'text-gray-500 hover:text-gray-700');
        });
        ['catSecType', 'catSecQuality', 'catSecViews', 'catSecDuration'].forEach(id => { const e = document.getElementById(id); if (e) e.classList.toggle('hidden', gal); });
        ['catSecPerformers', 'catSecChannels', 'catSecTags', 'catSecImages'].forEach(id => { const e = document.getElementById(id); if (e) e.classList.toggle('hidden', !gal); });
        const cw = document.getElementById('catCalcWrapper'); if (cw) cw.classList.toggle('hidden', gal);
        const ib = document.getElementById('catInfoBanner'); if (ib) ib.classList.toggle('hidden', gal);
        const s = document.getElementById('catSearch'); if (s) { s.value = ''; s.placeholder = gal ? 'Search galleries...' : 'Search videos...'; }
        renderActiveFilters();
        loadFilters();
        browse(1);
    }

    function selectVideoType(type) {
        activeVideoType = type;
        // Reset sub-filters when switching type
        activeSource = '';
        activeCategories.clear();
        activeQualities.clear();
        activeHas480 = false;
        activeViewRanges.clear();
        activeDurations.clear();
        // Update tab UI
        document.querySelectorAll('.cat-type-tab').forEach(btn => {
            const isActive = btn.dataset.type === type;
            btn.className = 'cat-type-tab flex-1 py-1.5 text-[10px] font-bold tracking-wider rounded-md transition-all ' +
                (isActive ? 'bg-white shadow text-blue-700' : 'text-gray-500 hover:text-gray-700');
        });
        loadFilters();
        browse(1);
    }

    return {
        init, openConfigDrawer, closeConfigDrawer, saveConfig, browse,
        toggleSelect, selectAllPage, clearSelection, gotoPage, setPerPage,
        openImportModal, closeImportModal, confirmImport, validateImportQuality,
        goToImportStep, nextImportStep, prevImportStep, toggleAiOption, testAiPrompt,
        downloadAllMatching,
        selectSource, selectCategory, selectQuality, toggle480, selectViewRange, selectDuration, clearFilter, removeFilterValue,
        selectVideoType, selectMedia,
        toggleFacet, facetSearch, selectImageRange, openGalleryViewer, closeGalleryViewer, openGalleryImportModal, closeGalleryImportModal, confirmGalleryImport, galleryAiToggle, galGoToStep, galNextStep, galPrevStep,
        toggleSidebarSection, toggleMobileSidebar,
        autoConnect, disconnect,
        openTopUpModal, closeTopUpModal, buyPack,
        playPreview, closePreviewModal,
        showVideoInfo, closeVideoInfoModal,
        showMoreCategories, collapseCategories
    };
})();
</script>
