// TubePress admin — Videos → Bulk Upload. Loaded by templates/admin/_video_bulk_upload.php.
(function () {
    'use strict';

    var bootEl = document.getElementById('tp-bulk-boot');
    if (!bootEl || !window.FormData || !window.XMLHttpRequest) {
        return;
    }
    var CFG = {};
    try { CFG = JSON.parse(bootEl.textContent || '{}'); } catch (e) { CFG = {}; }
    var LIMIT = parseInt(CFG.limit, 10) || 0;
    var LIMIT_LABEL = CFG.limitLabel || '';
    var STORE_URL = CFG.storeUrl || '/admin/videos/bulk-upload/store';
    var PREFS_KEY = 'tp_bulk_upload_prefs';

    function $(id) { return document.getElementById(id); }

    var drop = $('bupDrop'), input = $('bupInput'), addBtn = $('bupAddBtn'), queueEl = $('bupQueue');
    var hint = $('bupHint'), countEl = $('bupCount'), filesCard = $('bupFilesCard');
    var startBtn = $('bupStartBtn'), startLabel = $('bupStartLabel'), startHint = $('bupStartHint');
    var cancelBtn = $('bupCancelBtn'), retryBtn = $('bupRetryBtn'), doneLink = $('bupDoneLink');
    var summary = $('bupSummary'), phaseEl = $('bupPhase'), currentEl = $('bupCurrent'), pctEl = $('bupPct');
    var track = $('bupTrack'), bar = $('bupBar'), statsEl = $('bupStats'), ping = $('bupPing'), dot = $('bupDot');
    var statusSel = $('bupStatus'), statusHint = $('bupStatusHint'), typeSel = $('bupType'), descEl = $('bupDescription');
    var captureCb = $('bupCapture'), settingsBox = $('bupSettings'), relationsBox = $('bupRelations'), thumbCard = $('bupThumbCard');
    if (!drop || !input || !queueEl || !startBtn || !summary) {
        return;
    }

    var items = [];
    var seq = 0;
    var running = false;
    var stopRequested = false;
    var current = null;
    var analyzing = null;
    var overall = { total: 0, done: 0, startedAt: 0, lastTime: 0, lastLoaded: 0, speed: 0 };

    var STATUS_HINTS = {
        published: 'Each video goes live the moment its upload finishes.',
        draft: 'Videos are saved hidden — review them, then publish from the video page.',
        private: 'Videos are reachable only by direct link and never listed.'
    };
    var STATE_CHIP = {
        queued:    ['bg-gray-100 text-gray-600', 'Waiting'],
        reading:   ['bg-gray-100 text-gray-600', 'Reading…'],
        uploading: ['bg-blue-100 text-blue-700', 'Uploading'],
        saving:    ['bg-blue-100 text-blue-700', 'Saving…'],
        done:      ['bg-green-100 text-green-800', 'Created'],
        failed:    ['bg-red-100 text-red-700', 'Failed'],
        skipped:   ['bg-yellow-100 text-yellow-800', 'Skipped'],
        cancelled: ['bg-gray-100 text-gray-600', 'Cancelled']
    };

    function formatBytes(value) {
        var units = ['B', 'KB', 'MB', 'GB', 'TB'];
        var v = value, i = 0;
        while (v >= 1024 && i < units.length - 1) { v = v / 1024; i++; }
        return (i === 0 || v >= 10 ? Math.round(v) : Math.round(v * 10) / 10) + ' ' + units[i];
    }

    function formatClock(seconds) {
        var s = Math.max(0, Math.round(seconds));
        var h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), r = s % 60;
        var mm = (h > 0 && m < 10 ? '0' : '') + m, rr = (r < 10 ? '0' : '') + r;
        return (h > 0 ? h + ':' : '') + mm + ':' + rr;
    }

    function formatEta(seconds) {
        if (!isFinite(seconds) || seconds < 0) { return 'calculating'; }
        var s = Math.round(seconds);
        if (s < 60) { return s + ' sec'; }
        var m = Math.floor(s / 60);
        if (m < 60) { var rest = s % 60; return m + ' min' + (rest > 0 ? ' ' + rest + ' sec' : ''); }
        var h = Math.floor(m / 60);
        return h + ' h ' + (m % 60) + ' min';
    }

    function titleFromName(name) {
        var base = String(name || '').replace(/\.[^.]+$/, '');
        base = base.replace(/[_.\-]+/g, ' ').replace(/\s+/g, ' ').trim();
        if (!base) { return 'Untitled video'; }
        return base.charAt(0).toUpperCase() + base.slice(1);
    }

    function isVideoFile(file) {
        if (file.type && file.type.indexOf('video/') === 0) { return true; }
        if (file.type) { return false; }
        return /\.(mp4|m4v|webm|ogg|ogv|mov|mkv|avi|wmv|flv|ts|m2ts|mpg|mpeg|3gp)$/i.test(file.name || '');
    }

    function isDuplicate(file) {
        for (var i = 0; i < items.length; i++) {
            var f = items[i].file;
            if (f.name === file.name && f.size === file.size && f.lastModified === file.lastModified) { return true; }
        }
        return false;
    }

    function esc(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }

    function relationIds(containerId) {
        var out = [];
        var box = $(containerId);
        if (!box) { return out; }
        box.querySelectorAll('input[type=hidden]').forEach(function (h) { out.push(h.value); });
        return out;
    }

    function loadPrefs() {
        try {
            var p = JSON.parse(localStorage.getItem(PREFS_KEY) || 'null');
            if (!p) { return; }
            if (statusSel && p.status && statusSel.querySelector('option[value="' + p.status + '"]')) { statusSel.value = p.status; }
            if (typeSel && p.type && typeSel.querySelector('option[value="' + p.type + '"]')) { typeSel.value = p.type; }
            if (captureCb && typeof p.capture === 'boolean') { captureCb.checked = p.capture; }
        } catch (e) {}
    }

    function savePrefs() {
        try {
            localStorage.setItem(PREFS_KEY, JSON.stringify({
                status: statusSel ? statusSel.value : 'published',
                type: typeSel ? typeSel.value : '',
                capture: captureCb ? !!captureCb.checked : true
            }));
        } catch (e) {}
    }

    function renderRow(it) {
        var row = document.createElement('div');
        row.className = 'bup-row flex items-center gap-3 px-3 py-2.5 bg-white';
        row.setAttribute('data-id', String(it.id));
        row.innerHTML =
            '<div class="bup-thumb w-16 h-9 rounded bg-gray-100 overflow-hidden shrink-0 flex items-center justify-center text-gray-300">' +
                '<img alt="" class="w-full h-full object-cover hidden">' +
                '<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"/><polygon points="10 8 16 12 10 16 10 8"/></svg>' +
            '</div>' +
            '<div class="flex-1 min-w-0">' +
                '<input type="text" class="bup-title w-full px-2 py-1 -mx-2 border border-transparent rounded-md text-sm font-medium text-gray-800 hover:border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-500 outline-none bg-transparent" maxlength="255" value="' + esc(it.title).replace(/"/g, '&quot;') + '" title="Click to edit the title">' +
                '<div class="mt-0.5 flex items-center gap-1.5 text-[11px] text-gray-400 truncate">' +
                    '<span class="bup-name truncate">' + esc(it.file.name) + '</span>' +
                    '<span aria-hidden="true">&middot;</span><span class="bup-size">' + formatBytes(it.size) + '</span>' +
                    '<span class="bup-dur hidden"></span>' +
                '</div>' +
                '<div class="bup-prog hidden mt-1.5 h-1.5 w-full overflow-hidden rounded-full bg-gray-200"><div class="bup-prog-bar h-full w-0 rounded-full bg-blue-600 transition-all duration-200"></div></div>' +
                '<p class="bup-msg hidden mt-1 text-[11px] text-red-600"></p>' +
            '</div>' +
            '<div class="shrink-0 flex items-center gap-1.5">' +
                '<span class="bup-state inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-medium bg-gray-100 text-gray-600 whitespace-nowrap">Waiting</span>' +
                '<a class="bup-edit hidden px-2 py-1 text-[11px] font-medium rounded border bg-white text-gray-600 border-gray-300 hover:bg-gray-50 no-underline" target="_blank" rel="noopener">Edit</a>' +
                '<button type="button" class="bup-retry hidden px-2 py-1 text-[11px] font-medium rounded border bg-white text-blue-600 border-blue-200 hover:bg-blue-50">Retry</button>' +
                '<button type="button" class="bup-remove p-1 text-gray-400 hover:text-red-600 rounded transition-colors" title="Remove from the list" aria-label="Remove from the list">' +
                    '<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M18 6L6 18M6 6l12 12"/></svg>' +
                '</button>' +
            '</div>';
        it.row = row;
        it.els = {
            img: row.querySelector('.bup-thumb img'),
            title: row.querySelector('.bup-title'),
            dur: row.querySelector('.bup-dur'),
            prog: row.querySelector('.bup-prog'),
            progBar: row.querySelector('.bup-prog-bar'),
            msg: row.querySelector('.bup-msg'),
            state: row.querySelector('.bup-state'),
            edit: row.querySelector('.bup-edit'),
            retry: row.querySelector('.bup-retry'),
            remove: row.querySelector('.bup-remove')
        };
        it.els.title.addEventListener('input', function () { it.title = it.els.title.value; });
        it.els.title.addEventListener('blur', function () {
            var t = it.els.title.value.trim();
            if (!t) { t = titleFromName(it.file.name); it.els.title.value = t; }
            it.title = t;
        });
        it.els.title.addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); it.els.title.blur(); } });
        it.els.remove.addEventListener('click', function () { removeItem(it); });
        it.els.retry.addEventListener('click', function () { retryItem(it); });
        queueEl.appendChild(row);
        setState(it, it.state, it.message);
    }

    function setState(it, state, message) {
        it.state = state;
        it.message = message || '';
        var chip = STATE_CHIP[state] || STATE_CHIP.queued;
        it.els.state.className = 'bup-state inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-medium whitespace-nowrap ' + chip[0];
        it.els.state.textContent = chip[1];
        it.els.msg.classList.toggle('hidden', !it.message);
        it.els.msg.textContent = it.message;
        it.els.msg.classList.toggle('text-red-600', state === 'failed');
        it.els.msg.classList.toggle('text-yellow-700', state === 'skipped');
        it.els.msg.classList.toggle('text-gray-500', state === 'cancelled');
        it.els.prog.classList.toggle('hidden', state !== 'uploading' && state !== 'saving');
        if (state !== 'uploading') { it.els.progBar.style.width = state === 'saving' ? '100%' : '0%'; }
        it.els.retry.classList.toggle('hidden', !(state === 'failed' || state === 'cancelled'));
        it.els.edit.classList.toggle('hidden', !(state === 'done' && it.editUrl));
        if (state === 'done' && it.editUrl) { it.els.edit.href = it.editUrl; }
        it.els.remove.classList.toggle('hidden', state === 'uploading' || state === 'saving');
        it.els.title.disabled = (state === 'uploading' || state === 'saving' || state === 'done');
        it.row.classList.toggle('bg-red-50', state === 'failed');
        it.row.classList.toggle('bg-green-50', state === 'done');
        it.row.classList.toggle('bg-yellow-50', state === 'skipped');
    }

    function setItemProgress(it, loaded, total) {
        var pct = total > 0 ? Math.max(0, Math.min(100, loaded / total * 100)) : 0;
        it.els.progBar.style.width = pct + '%';
        it.els.state.textContent = 'Uploading ' + Math.round(pct) + '%';
    }

    function showThumb(it) {
        if (!it.thumbUrl) { return; }
        it.els.img.src = it.thumbUrl;
        it.els.img.classList.remove('hidden');
    }

    function updateMeta(it) {
        if (it.duration > 0) {
            it.els.dur.innerHTML = '<span aria-hidden="true">&middot;</span> ' + formatClock(it.duration);
            it.els.dur.classList.remove('hidden');
        }
    }

    function pendingItems() {
        return items.filter(function (it) { return it.state === 'queued'; });
    }

    function countBy(state) {
        var n = 0;
        items.forEach(function (it) { if (it.state === state) { n++; } });
        return n;
    }

    function refresh() {
        var visible = items.length > 0;
        queueEl.classList.toggle('hidden', !visible);
        hint.classList.toggle('hidden', !visible);
        countEl.classList.toggle('hidden', !visible);
        countEl.textContent = String(items.length);
        drop.classList.toggle('is-compact', visible);
        var pending = pendingItems();
        var failed = countBy('failed') + countBy('cancelled');
        if (!running) {
            startBtn.disabled = pending.length === 0;
            startLabel.textContent = countBy('done') > 0 && pending.length > 0 ? 'Upload ' + pending.length + ' more' : 'Start upload';
            if (pending.length > 0) {
                var total = 0;
                pending.forEach(function (it) { total += it.size; });
                var statusText = statusSel ? statusSel.options[statusSel.selectedIndex].text : 'Published';
                startHint.textContent = pending.length + ' file' + (pending.length > 1 ? 's' : '') + ' · ' + formatBytes(total) + ' · created as ' + statusText;
            } else if (items.length > 0) {
                startHint.textContent = failed > 0 ? 'Use Retry on a row to send it again.' : 'Nothing left to upload.';
            } else {
                startHint.textContent = 'Add files to begin.';
            }
        }
        retryBtn.classList.toggle('hidden', running || failed === 0);
        if (statusHint && statusSel) { statusHint.textContent = STATUS_HINTS[statusSel.value] || ''; }
    }

    function addFiles(list) {
        var added = 0;
        for (var i = 0; i < list.length; i++) {
            var f = list[i];
            if (!f || !f.name || isDuplicate(f)) { continue; }
            var it = { id: ++seq, file: f, title: titleFromName(f.name), size: f.size, duration: 0, thumb: null, thumbUrl: '', state: 'queued', message: '', analyzed: false, waiters: [], xhr: null, editUrl: '' };
            if (!isVideoFile(f)) {
                it.state = 'skipped'; it.message = 'Not a video file.';
            } else if (f.size <= 0) {
                it.state = 'skipped'; it.message = 'The file is empty (0 bytes).';
            } else if (LIMIT > 0 && f.size > LIMIT) {
                it.state = 'skipped'; it.message = 'Larger than the ' + LIMIT_LABEL + ' this server accepts per file.';
            }
            items.push(it);
            renderRow(it);
            added++;
        }
        if (added > 0) {
            refresh();
            pumpAnalyze();
        }
        input.value = '';
    }

    function removeItem(it) {
        if (it.state === 'uploading' || it.state === 'saving') { return; }
        var idx = items.indexOf(it);
        if (idx >= 0) { items.splice(idx, 1); }
        if (it.row && it.row.parentNode) { it.row.parentNode.removeChild(it.row); }
        if (it.thumbUrl) { try { URL.revokeObjectURL(it.thumbUrl); } catch (e) {} }
        refresh();
    }

    function retryItem(it) {
        if (it.state !== 'failed' && it.state !== 'cancelled') { return; }
        setState(it, 'queued', '');
        refresh();
        if (!running) { start(); }
    }

    function ensureAnalyzed(it, cb) {
        if (it.analyzed) { cb(); return; }
        it.waiters.push(cb);
        if (analyzing !== it) { pumpAnalyze(it); }
    }

    function pumpAnalyze(priority) {
        if (analyzing) { return; }
        var next = priority && !priority.analyzed ? priority : null;
        if (!next) {
            for (var i = 0; i < items.length; i++) {
                if (!items[i].analyzed && items[i].state === 'queued') { next = items[i]; break; }
            }
        }
        if (!next) { return; }
        analyzing = next;
        analyze(next, function () {
            next.analyzed = true;
            analyzing = null;
            var w = next.waiters.splice(0);
            w.forEach(function (fn) { fn(); });
            pumpAnalyze();
        });
    }

    function analyze(it, done) {
        var finished = false;
        var url = '';
        var v = document.createElement('video');
        var timer = null;
        function finish() {
            if (finished) { return; }
            finished = true;
            if (timer) { clearTimeout(timer); }
            try { v.pause(); v.removeAttribute('src'); v.load(); } catch (e) {}
            if (url) { try { URL.revokeObjectURL(url); } catch (e) {} }
            done();
        }
        try { url = URL.createObjectURL(it.file); } catch (e) { finish(); return; }
        v.preload = 'metadata';
        v.muted = true;
        v.playsInline = true;
        v.setAttribute('playsinline', '');
        v.addEventListener('error', finish);
        v.addEventListener('loadedmetadata', function () {
            if (isFinite(v.duration) && v.duration > 0) {
                it.duration = Math.round(v.duration);
                updateMeta(it);
            }
            if (!isFinite(v.duration) || v.duration <= 0) { finish(); return; }
            var t = Math.min(Math.max(1, v.duration * 0.1), 30);
            if (t >= v.duration) { t = Math.max(0, v.duration / 2); }
            if (timer) { clearTimeout(timer); }
            timer = setTimeout(finish, 10000);
            v.addEventListener('seeked', function () {
                window.requestAnimationFrame(function () {
                    try {
                        var w = v.videoWidth, h = v.videoHeight;
                        if (!w || !h) { finish(); return; }
                        var scale = Math.min(1, 1280 / w);
                        var cw = Math.max(1, Math.round(w * scale)), ch = Math.max(1, Math.round(h * scale));
                        var canvas = document.createElement('canvas');
                        canvas.width = cw; canvas.height = ch;
                        canvas.getContext('2d').drawImage(v, 0, 0, cw, ch);
                        canvas.toBlob(function (blob) {
                            if (blob && blob.size > 0) {
                                it.thumb = blob;
                                try { it.thumbUrl = URL.createObjectURL(blob); showThumb(it); } catch (e) {}
                            }
                            finish();
                        }, 'image/jpeg', 0.86);
                    } catch (e) { finish(); }
                });
            }, { once: true });
            try { v.currentTime = t; } catch (e) { finish(); }
        }, { once: true });
        timer = setTimeout(finish, 15000);
        v.src = url;
        try { v.load(); } catch (e) {}
    }

    function lockUi(locked) {
        [settingsBox, relationsBox, thumbCard].forEach(function (box) { if (box) { box.classList.toggle('bup-locked', locked); } });
        startBtn.disabled = locked || pendingItems().length === 0;
        addBtn.disabled = false;
        cancelBtn.classList.toggle('hidden', !locked);
        if (locked) {
            startLabel.textContent = 'Uploading…';
            startHint.textContent = 'You can keep adding files — they join the end of the queue.';
        }
    }

    function guard(event) { event.preventDefault(); event.returnValue = ''; return ''; }
    function setRunning(state) {
        running = state;
        if (state) { window.addEventListener('beforeunload', guard); } else { window.removeEventListener('beforeunload', guard); }
    }

    function setTone(tone) {
        bar.classList.remove('bg-blue-600', 'bg-green-600', 'bg-red-600', 'bg-yellow-500');
        dot.classList.remove('bg-blue-600', 'bg-green-600', 'bg-red-600', 'bg-yellow-500');
        var cls = tone === 'ok' ? 'bg-green-600' : tone === 'error' ? 'bg-red-600' : tone === 'warn' ? 'bg-yellow-500' : 'bg-blue-600';
        bar.classList.add(cls);
        dot.classList.add(cls);
        ping.classList.toggle('hidden', tone !== 'busy');
    }

    function setOverall(bytesDone) {
        var pct = overall.total > 0 ? Math.max(0, Math.min(100, bytesDone / overall.total * 100)) : 0;
        bar.style.width = pct + '%';
        pctEl.textContent = Math.round(pct) + '%';
        track.setAttribute('aria-valuenow', String(Math.round(pct)));
    }

    function recomputeTotal() {
        var total = 0, done = 0;
        items.forEach(function (it) {
            if (it.state === 'queued' || it.state === 'uploading' || it.state === 'saving' || it.doneInRun) { total += it.size; }
            if (it.doneInRun) { done += it.size; }
        });
        overall.total = total;
        overall.done = done;
    }

    function start() {
        if (running) { return; }
        if (pendingItems().length === 0) { return; }
        items.forEach(function (it) { it.doneInRun = false; });
        stopRequested = false;
        setRunning(true);
        lockUi(true);
        savePrefs();
        overall = { total: 0, done: 0, startedAt: Date.now(), lastTime: Date.now(), lastLoaded: 0, speed: 0 };
        recomputeTotal();
        summary.classList.remove('hidden');
        doneLink.classList.add('hidden');
        retryBtn.classList.add('hidden');
        setTone('busy');
        setOverall(0);
        phaseEl.textContent = 'Uploading';
        currentEl.textContent = '';
        statsEl.textContent = 'Starting…';
        cancelBtn.textContent = 'Cancel';
        next();
    }

    function next() {
        if (stopRequested) { finishRun(); return; }
        var it = null;
        for (var i = 0; i < items.length; i++) { if (items[i].state === 'queued') { it = items[i]; break; } }
        if (!it) { finishRun(); return; }
        recomputeTotal();
        current = it;
        setState(it, 'reading', '');
        currentEl.textContent = it.file.name;
        ensureAnalyzed(it, function () {
            if (stopRequested) { setState(it, 'queued', ''); finishRun(); return; }
            if (it.state !== 'reading') { next(); return; }
            upload(it);
        });
    }

    function upload(it) {
        var fd = new FormData();
        fd.append('csrf_token', CFG.csrf || '');
        fd.append('title', it.title || titleFromName(it.file.name));
        fd.append('status', statusSel ? statusSel.value : 'published');
        fd.append('video_type', typeSel ? typeSel.value : '');
        fd.append('description', descEl ? descEl.value : '');
        fd.append('duration', String(it.duration || 0));
        relationIds('tokCategories').forEach(function (id) { fd.append('categories[]', id); });
        relationIds('tokTags').forEach(function (id) { fd.append('tags[]', id); });
        relationIds('tokPerformers').forEach(function (id) { fd.append('performers[]', id); });
        relationIds('tokChannels').forEach(function (id) { fd.append('channels[]', id); });
        if (it.thumb && captureCb && captureCb.checked) { fd.append('thumbnail', it.thumb, 'thumb.jpg'); }
        fd.append('video_file', it.file, it.file.name);

        setState(it, 'uploading', '');
        setItemProgress(it, 0, it.size);
        var xhr = new XMLHttpRequest();
        it.xhr = xhr;
        var itemLoaded = 0;
        xhr.open('POST', STORE_URL, true);
        xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
        xhr.setRequestHeader('Accept', 'application/json');

        xhr.upload.addEventListener('progress', function (e) {
            if (!e.lengthComputable) { return; }
            itemLoaded = Math.min(it.size, e.loaded);
            setItemProgress(it, e.loaded, e.total);
            var now = Date.now();
            var loadedOverall = overall.done + itemLoaded;
            var elapsed = (now - overall.lastTime) / 1000;
            if (elapsed >= 0.25) {
                var instant = (loadedOverall - overall.lastLoaded) / elapsed;
                overall.speed = overall.speed > 0 ? (overall.speed * 0.7 + instant * 0.3) : instant;
                overall.lastTime = now;
                overall.lastLoaded = loadedOverall;
            }
            setOverall(loadedOverall);
            var doneCount = countBy('done');
            var totalCount = doneCount + pendingItems().length + 1;
            var parts = [(doneCount + 1) + ' of ' + totalCount, formatBytes(loadedOverall) + ' of ' + formatBytes(overall.total)];
            if (overall.speed > 0) {
                parts.push(formatBytes(overall.speed) + '/s');
                parts.push(formatEta((overall.total - loadedOverall) / overall.speed) + ' left');
            }
            statsEl.textContent = parts.join(' · ');
        });

        xhr.upload.addEventListener('load', function () {
            setState(it, 'saving', '');
            phaseEl.textContent = 'Saving on the server';
        });

        xhr.addEventListener('load', function () {
            it.xhr = null;
            var payload = null;
            try { payload = JSON.parse(xhr.responseText); } catch (e) { payload = null; }
            phaseEl.textContent = 'Uploading';
            if (xhr.status === 413) {
                setState(it, 'failed', 'The web server refused this file: it exceeds its body size limit. Raise that limit and retry.');
                afterItem(it); return;
            }
            if (payload && payload.success) {
                it.editUrl = payload.edit_url || '';
                it.videoId = payload.id || 0;
                it.doneInRun = true;
                overall.done += it.size;
                overall.lastLoaded = overall.done;
                setState(it, 'done', payload.queued > 0 ? 'Transcoding queued.' : '');
                it.els.msg.classList.remove('text-red-600');
                it.els.msg.classList.add('text-gray-500');
                afterItem(it); return;
            }
            if (xhr.status === 401 || xhr.status === 403) {
                setState(it, 'failed', (payload && payload.message) || 'Your session has expired. Reload the page and sign in again.');
                stopRequested = true;
                afterItem(it); return;
            }
            if (payload && payload.message) {
                setState(it, 'failed', payload.message);
                afterItem(it); return;
            }
            if (xhr.status === 200 && /\/login/.test(xhr.responseURL || '')) {
                setState(it, 'failed', 'Your session has expired. Reload the page and sign in again.');
                stopRequested = true;
                afterItem(it); return;
            }
            setState(it, 'failed', 'The server returned an unexpected response (HTTP ' + xhr.status + '). Check the video list before retrying.');
            afterItem(it);
        });

        xhr.addEventListener('error', function () {
            it.xhr = null;
            phaseEl.textContent = 'Uploading';
            setState(it, 'failed', 'The connection was lost during the upload. Nothing was saved for this file.');
            afterItem(it);
        });

        xhr.addEventListener('abort', function () {
            it.xhr = null;
            phaseEl.textContent = 'Uploading';
            setState(it, 'cancelled', 'Upload cancelled — nothing was saved for this file.');
            afterItem(it);
        });

        xhr.send(fd);
    }

    function afterItem(it) {
        current = null;
        refresh();
        next();
    }

    function finishRun() {
        current = null;
        setRunning(false);
        lockUi(false);
        var done = 0, failed = 0, left = 0;
        items.forEach(function (it) {
            if (it.doneInRun) { done++; }
            if (it.state === 'failed') { failed++; }
            if (it.state === 'queued' || it.state === 'cancelled') { left++; }
        });
        var created = done + ' video' + (done === 1 ? '' : 's') + ' created';
        if (stopRequested) {
            setTone('warn');
            phaseEl.textContent = 'Upload stopped';
            statsEl.textContent = created + (failed ? ' · ' + failed + ' failed' : '') + (left ? ' · ' + left + ' not sent' : '');
        } else if (failed > 0) {
            setTone('error');
            phaseEl.textContent = 'Finished with errors';
            statsEl.textContent = created + ' · ' + failed + ' failed';
        } else {
            setTone('ok');
            setOverall(overall.total);
            phaseEl.textContent = 'All done';
            statsEl.textContent = created + (overall.startedAt ? ' in ' + formatEta((Date.now() - overall.startedAt) / 1000) : '');
        }
        currentEl.textContent = '';
        cancelBtn.classList.add('hidden');
        doneLink.classList.toggle('hidden', done === 0 && countBy('done') === 0);
        stopRequested = false;
        refresh();
        pumpAnalyze();
    }

    function cancelRun() {
        if (!running) { return; }
        stopRequested = true;
        cancelBtn.textContent = 'Stopping…';
        if (current && current.xhr) {
            current.xhr.abort();
        } else if (current && current.state === 'reading') {
            setState(current, 'queued', '');
            finishRun();
        }
    }

    function retryFailed() {
        items.forEach(function (it) { if (it.state === 'failed' || it.state === 'cancelled') { setState(it, 'queued', ''); } });
        refresh();
        start();
    }

    function openPicker() { input.click(); }

    drop.addEventListener('click', openPicker);
    drop.addEventListener('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openPicker(); } });
    addBtn.addEventListener('click', openPicker);
    input.addEventListener('change', function () { if (input.files && input.files.length) { addFiles(input.files); } });

    var dragDepth = 0;
    var dropTarget = filesCard || drop;
    dropTarget.addEventListener('dragenter', function (e) { e.preventDefault(); dragDepth++; drop.classList.add('is-over'); });
    dropTarget.addEventListener('dragover', function (e) { e.preventDefault(); if (e.dataTransfer) { e.dataTransfer.dropEffect = 'copy'; } });
    dropTarget.addEventListener('dragleave', function () { dragDepth = Math.max(0, dragDepth - 1); if (dragDepth === 0) { drop.classList.remove('is-over'); } });
    dropTarget.addEventListener('drop', function (e) {
        e.preventDefault();
        dragDepth = 0;
        drop.classList.remove('is-over');
        if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length) { addFiles(e.dataTransfer.files); }
    });
    window.addEventListener('dragover', function (e) { e.preventDefault(); });
    window.addEventListener('drop', function (e) { e.preventDefault(); });

    startBtn.addEventListener('click', start);
    cancelBtn.addEventListener('click', cancelRun);
    retryBtn.addEventListener('click', retryFailed);
    if (statusSel) { statusSel.addEventListener('change', function () { refresh(); savePrefs(); }); }
    if (typeSel) { typeSel.addEventListener('change', savePrefs); }
    if (captureCb) {
        captureCb.addEventListener('change', function () {
            queueEl.classList.toggle('bup-thumb-off', !captureCb.checked);
            savePrefs();
        });
    }

    loadPrefs();
    queueEl.classList.toggle('bup-thumb-off', !!(captureCb && !captureCb.checked));
    refresh();
})();
