/**
 * Wild Dragon MAM - Premiere Pro Panel
 * Main JavaScript file for the CEP panel
 */

// Adobe CEP interface — must be instantiated before any host (ExtendScript) calls
const csInterface = new CSInterface();

// ============================================================================
// State Management
// ============================================================================

const state = {
    serverUrl:        localStorage.getItem('mam_server_url') || 'http://localhost:7434',
    isConnected:      false,
    isConnecting:     false,
    selectedAsset:    null,
    assets:           [],
    projects:         [],
    selectedProject:  'all',
    searchQuery:      '',
    currentPage:      0,
    pageSize:         50,
    totalAssets:      0,
    downloadProgress: 0,
    isDownloading:    false,
    thumbCache:       {}, // assetId -> signed URL
    // Maps tempFilePath → { assetId, displayName } for timeline export resolution.
    // Also stores 'name:filename.mp4' → same as a fuzzy fallback.
    importedAssets:   JSON.parse(localStorage.getItem('mam_imported_assets') || '{}'),
    exportPanelVisible:  false,
    currentSequenceName: '',
};

// ============================================================================
// DOM Elements
// ============================================================================

let elements = {};

function initDOMElements() {
    elements = {
        serverUrlInput:    document.getElementById('server-url'),
        connectBtn:        document.getElementById('connect-btn'),
        statusIndicator:   document.getElementById('status-indicator'),
        searchInput:       document.getElementById('search-input'),
        projectFilter:     document.getElementById('project-filter'),
        assetGrid:         document.getElementById('asset-grid'),
        emptyState:        document.getElementById('empty-state'),
        detailsPanel:      document.getElementById('details-panel'),
        detailsFilename:   document.getElementById('details-filename'),
        detailsCodec:      document.getElementById('details-codec'),
        detailsResolution: document.getElementById('details-resolution'),
        detailsFps:        document.getElementById('details-fps'),
        detailsDuration:   document.getElementById('details-duration'),
        detailsSize:       document.getElementById('details-size'),
        detailsTags:       document.getElementById('details-tags'),
        importBtn:         document.getElementById('import-btn'),
        importHiresBtn:    document.getElementById('import-hires-btn'),
        importAllBtn:      document.getElementById('import-all-btn'),
        mountLiveBtn:      document.getElementById('mount-live-btn'),
        relinkBtn:         document.getElementById('relink-btn'),
        exportTimelineBtn: document.getElementById('export-timeline-btn'),
        progressContainer: document.getElementById('progress-container'),
        progressLabel:     document.getElementById('progress-label'),
        progressFill:      document.getElementById('progress-fill'),
        // Export panel
        exportPanel:       document.getElementById('export-panel'),
        exportSeqName:     document.getElementById('export-seq-name'),
        exportProjSelect:  document.getElementById('export-proj-select'),
        exportClipInfo:    document.getElementById('export-clip-info'),
        exportConfirmBtn:  document.getElementById('export-confirm-btn'),
        exportCancelBtn:   document.getElementById('export-cancel-btn'),
        // Sequence info bar
        seqInfoBar:        document.getElementById('seq-info-bar'),
        seqInfoName:       document.getElementById('seq-info-name'),
        seqRefreshBtn:     document.getElementById('seq-refresh-btn'),
    };
}

// ============================================================================
// Thumbnail Lazy Loading (IntersectionObserver)
// ============================================================================

const thumbObserver = new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
        if (entry.isIntersecting) {
            const img     = entry.target;
            const assetId = img.dataset.assetId;
            if (assetId && !img.dataset.loaded) loadThumbnail(img, assetId);
            thumbObserver.unobserve(img);
        }
    });
}, { rootMargin: '100px' });

async function loadThumbnail(img, assetId) {
    if (state.thumbCache[assetId]) {
        img.src = state.thumbCache[assetId];
        img.dataset.loaded = '1';
        return;
    }
    try {
        const r = await fetch(`${state.serverUrl}/api/v1/assets/${assetId}/thumbnail`, {
            headers: { Accept: 'application/json' },
        });
        if (!r.ok) return;
        const { url } = await r.json();
        if (!url) return;
        state.thumbCache[assetId] = url;
        img.src = url;
        img.dataset.loaded = '1';
    } catch (_) {
        // thumbnail unavailable — leave placeholder
    }
}

// ============================================================================
// Initialization
// ============================================================================

document.addEventListener('DOMContentLoaded', () => {
    initDOMElements();
    setupEventListeners();
    restoreSettings();
    logMessage('Wild Dragon MAM panel initialized');
});

function setupEventListeners() {
    elements.serverUrlInput.addEventListener('change', (e) => {
        state.serverUrl = e.target.value.trim().replace(/\/$/, '');
        localStorage.setItem('mam_server_url', state.serverUrl);
        state.thumbCache = {};
    });
    elements.connectBtn.addEventListener('click', connectToServer);
    elements.searchInput.addEventListener('input', debounce(handleSearch, 300));
    elements.projectFilter.addEventListener('change', handleProjectFilter);
    elements.assetGrid.addEventListener('click', handleAssetClick);
    elements.importBtn.addEventListener('click', importSelectedAsset);
    elements.importHiresBtn.addEventListener('click', importSelectedAssetHires);
    elements.importAllBtn.addEventListener('click', importAllAssets);
    elements.mountLiveBtn.addEventListener('click', mountLiveAsset);
    elements.relinkBtn.addEventListener('click', relinkSelectedAsset);
    elements.exportTimelineBtn.addEventListener('click', startExportTimeline);
    elements.exportConfirmBtn.addEventListener('click', confirmExportTimeline);
    elements.exportCancelBtn.addEventListener('click', cancelExportTimeline);
    elements.seqRefreshBtn.addEventListener('click', refreshCurrentSequenceInfo);
}

function restoreSettings() {
    elements.serverUrlInput.value = state.serverUrl;
}

// ============================================================================
// Server Connection
// ============================================================================

async function connectToServer() {
    if (state.isConnecting) return;

    state.isConnecting = true;
    updateConnectionStatus('connecting');
    elements.connectBtn.disabled = true;

    try {
        const response = await fetch(`${state.serverUrl}/api/v1/projects`, {
            method: 'GET',
            headers: { Accept: 'application/json' },
            credentials: 'include',
        });

        if (response.ok) {
            state.isConnected = true;
            updateConnectionStatus('connected');
            elements.connectBtn.textContent = 'Reconnect';
            logMessage('Connected to Wild Dragon MAM');

            const projectData = await response.json();
            await fetchProjects(projectData);
            await fetchAssets();
            refreshCurrentSequenceInfo();
        } else {
            throw new Error(`HTTP ${response.status}`);
        }
    } catch (error) {
        console.error('Connection error:', error);
        state.isConnected = false;
        updateConnectionStatus('disconnected');
        elements.connectBtn.textContent = 'Connect';
        showErrorMessage(`Failed to connect: ${error.message}`);
    } finally {
        state.isConnecting = false;
        elements.connectBtn.disabled = false;
    }
}

function updateConnectionStatus(status) {
    const indicator = elements.statusIndicator;
    indicator.classList.remove('connected', 'connecting');
    if (status === 'connected')       indicator.classList.add('connected');
    else if (status === 'connecting') indicator.classList.add('connecting');
}

// ============================================================================
// API Calls
// ============================================================================

async function fetchProjects(preloadedData) {
    try {
        let projects;
        if (preloadedData) {
            projects = Array.isArray(preloadedData) ? preloadedData : [];
        } else {
            const response = await fetch(`${state.serverUrl}/api/v1/projects`, {
                headers: { Accept: 'application/json' },
                credentials: 'include',
            });
            if (!response.ok) throw new Error(`HTTP ${response.status}`);
            const data = await response.json();
            projects = Array.isArray(data) ? data : [];
        }

        state.projects = projects;

        const savedProject = state.selectedProject;
        elements.projectFilter.innerHTML = '<option value="all">All Projects</option>';
        state.projects.forEach((p) => {
            const opt = document.createElement('option');
            opt.value       = p.id;
            opt.textContent = p.name;
            elements.projectFilter.appendChild(opt);
        });
        elements.projectFilter.value = savedProject;

        // Also populate the export panel project selector
        populateExportProjectSelect();

        logMessage(`Loaded ${state.projects.length} projects`);
    } catch (error) {
        console.error('Error fetching projects:', error);
    }
}

async function fetchAssets(page = 0) {
    if (!state.isConnected) return;
    try {
        const params = new URLSearchParams({
            offset: page * state.pageSize,
            limit:  state.pageSize,
        });
        if (state.searchQuery)               params.append('search',     state.searchQuery);
        if (state.selectedProject !== 'all') params.append('project_id', state.selectedProject);

        const response = await fetch(
            `${state.serverUrl}/api/v1/assets?${params.toString()}`,
            { headers: { Accept: 'application/json' }, credentials: 'include' }
        );
        if (!response.ok) throw new Error(`HTTP ${response.status}`);

        const data        = await response.json();
        state.assets      = data.assets || [];
        state.totalAssets = data.total  || 0;
        state.currentPage = page;

        renderAssets();
        logMessage(`Loaded ${state.assets.length} assets`);
    } catch (error) {
        console.error('Error fetching assets:', error);
        showErrorMessage('Failed to fetch assets');
    }
}

async function fetchAssetDetails(assetId) {
    if (!state.isConnected) return null;
    try {
        const response = await fetch(`${state.serverUrl}/api/v1/assets/${assetId}`, {
            headers: { Accept: 'application/json' },
            credentials: 'include',
        });
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        return await response.json();
    } catch (error) {
        console.error('Error fetching asset details:', error);
        return null;
    }
}

/**
 * Returns the download URL for the proxy.  /stream returns a server-relative
 * path (/api/v1/assets/:id/video) designed for browser playback; we prepend
 * serverUrl so Node.js http.get() receives a valid absolute URL.
 */
async function getSignedDownloadUrl(assetId) {
    const response = await fetch(`${state.serverUrl}/api/v1/assets/${assetId}/stream`, {
        headers: { Accept: 'application/json' },
        credentials: 'include',
    });
    if (!response.ok) throw new Error(`HTTP ${response.status} from /stream`);
    const { url } = await response.json();
    if (!url) throw new Error('Stream endpoint returned no URL');
    return url.startsWith('/') ? state.serverUrl + url : url;
}

/**
 * Returns presigned S3 URL + filename/ext/file_size for the hi-res original.
 * GET /api/v1/assets/:id/hires
 */
async function getHiresDownloadInfo(assetId) {
    const response = await fetch(`${state.serverUrl}/api/v1/assets/${assetId}/hires`, {
        headers: { Accept: 'application/json' },
        credentials: 'include',
    });
    if (!response.ok) throw new Error(`HTTP ${response.status} from /hires`);
    const data = await response.json();
    if (!data.url) throw new Error('Hi-res endpoint returned no URL');
    return data;
}

// ============================================================================
// UI Rendering
// ============================================================================

function renderAssets() {
    elements.assetGrid.innerHTML = '';

    if (state.assets.length === 0) {
        elements.emptyState.style.display = 'flex';
        return;
    }

    elements.emptyState.style.display = 'none';
    state.assets.forEach((asset) => {
        elements.assetGrid.appendChild(createAssetCard(asset));
    });
}

function createAssetCard(asset) {
    const card = document.createElement('div');
    card.className = 'asset-card';
    if (state.selectedAsset && state.selectedAsset.id === asset.id) {
        card.classList.add('selected');
    }
    card.dataset.assetId = asset.id;

    // Thumbnail — lazy loaded via IntersectionObserver
    const thumbnail = document.createElement('div');
    thumbnail.className = 'asset-thumbnail';

    const img = document.createElement('img');
    img.dataset.assetId = asset.id;
    img.alt             = escapeHtml(asset.display_name || asset.filename || '');
    img.style.cssText   = 'width:100%;height:100%;object-fit:cover;display:block;';
    img.onerror         = () => { img.style.display = 'none'; };
    thumbnail.appendChild(img);
    thumbObserver.observe(img);
    card.appendChild(thumbnail);

    // Info row
    const info = document.createElement('div');
    info.className = 'asset-info';

    const name       = asset.display_name || asset.filename || 'Untitled';
    const filenameEl = document.createElement('div');
    filenameEl.className   = 'asset-filename';
    filenameEl.title       = name;
    filenameEl.textContent = name;
    info.appendChild(filenameEl);

    const durationSec = asset.duration_ms ? asset.duration_ms / 1000 : null;
    const codec = asset.codec || asset.media_type || 'video';
    const meta  = document.createElement('div');
    meta.className   = 'asset-meta';
    meta.innerHTML   = [
        '<span>' + (durationSec ? formatDuration(durationSec) : 'N/A') + '</span>',
        '<span>' + escapeHtml(codec.toUpperCase()) + '</span>',
    ].join('');
    info.appendChild(meta);

    const statusStr   = asset.status || 'ready';
    const statusBadge = document.createElement('div');
    statusBadge.className   = 'asset-status-badge status-badge ' + statusStr;
    statusBadge.textContent = statusStr.toUpperCase();
    info.appendChild(statusBadge);

    card.appendChild(info);
    return card;
}

function showAssetDetails(asset) {
    state.selectedAsset = asset;
    elements.detailsPanel.classList.remove('hidden');

    elements.detailsFilename.textContent   = asset.display_name || asset.filename;
    elements.detailsCodec.textContent      = asset.codec || asset.media_type || 'Unknown';
    elements.detailsResolution.textContent = asset.resolution || 'N/A';
    elements.detailsFps.textContent        = asset.fps ? asset.fps + ' fps' : 'N/A';
    elements.detailsDuration.textContent   = asset.duration_ms
        ? formatDuration(asset.duration_ms / 1000)
        : 'N/A';
    elements.detailsSize.textContent       = asset.file_size
        ? formatFileSize(asset.file_size)
        : 'N/A';

    elements.detailsTags.innerHTML = '';
    const tags = asset.tags || [];
    if (tags.length > 0) {
        tags.forEach((tag) => {
            const el       = document.createElement('span');
            el.className   = 'tag';
            el.textContent = tag;
            elements.detailsTags.appendChild(el);
        });
    } else {
        elements.detailsTags.innerHTML =
            '<span style="color:var(--text-secondary)">No tags</span>';
    }

    var isLive = asset.status === 'live';
    elements.importBtn.disabled      = isLive;
    elements.importHiresBtn.disabled = isLive;
    elements.mountLiveBtn.disabled   = !isLive;
    // The relink button only makes sense once the live file has been finalized
    // AND we have a record of it being mounted in this session.
    elements.relinkBtn.disabled = !(asset.status === 'ready' && state.importedAssets['live:' + asset.id]);
}

function hideAssetDetails() {
    state.selectedAsset = null;
    elements.detailsPanel.classList.add('hidden');
    elements.importBtn.disabled      = true;
    elements.importHiresBtn.disabled = true;
    elements.mountLiveBtn.disabled   = true;
    elements.relinkBtn.disabled      = true;
}

// ============================================================================
// Mount Live — open the growing file directly from the SMB share
// ============================================================================

async function mountLiveAsset() {
    if (!state.selectedAsset) {
        showErrorMessage('No asset selected');
        return;
    }
    var asset = state.selectedAsset;
    try {
        elements.mountLiveBtn.disabled = true;
        showProgress('Resolving SMB path…', 10);

        var res = await fetch(state.serverUrl + '/api/v1/assets/' + asset.id + '/live-path', {
            headers: { Accept: 'application/json' },
            credentials: 'include',
        });
        if (!res.ok) {
            var body = await res.json().catch(function () { return {}; });
            throw new Error(body.error || ('HTTP ' + res.status));
        }
        var info = await res.json();

        // Premiere on Windows wants UNC paths with backslashes; on Mac
        // POSIX-style smb:// paths route through the Finder mount.
        var isMac = navigator.platform.indexOf('Mac') !== -1;
        var hostPath = isMac ? info.posix_path : info.win_path;

        showProgress('Importing live file…', 60);
        await importFileToPremiereProject(hostPath);

        // Remember this asset so the Relink button knows what to swap later.
        state.importedAssets['live:' + asset.id] = {
            assetId:     asset.id,
            displayName: info.display_name,
            livePath:    hostPath,
        };
        try { localStorage.setItem('mam_imported_assets', JSON.stringify(state.importedAssets)); } catch (_) {}

        startLiveStatusPoll(asset.id);
        hideProgress();
        showSuccessMessage('Mounted live: ' + info.display_name);
    } catch (err) {
        hideProgress();
        showErrorMessage('Mount live failed: ' + err.message);
    } finally {
        elements.mountLiveBtn.disabled = !(state.selectedAsset && state.selectedAsset.status === 'live');
    }
}

// Poll the asset until its status flips from 'live' to 'ready' so we can
// surface the Relink button. 5s cadence — matches the worker's promotion
// scan interval, so we typically catch the transition on the next tick.
var _livePolls = {};
function startLiveStatusPoll(assetId) {
    if (_livePolls[assetId]) return;
    _livePolls[assetId] = setInterval(async function () {
        try {
            var r = await fetch(state.serverUrl + '/api/v1/assets/' + assetId, {
                headers: { Accept: 'application/json' },
                credentials: 'include',
            });
            if (!r.ok) return;
            var a = await r.json();
            if (a.status === 'ready') {
                clearInterval(_livePolls[assetId]);
                delete _livePolls[assetId];
                logMessage('Live asset ' + assetId + ' finalized — relink available');
                if (state.selectedAsset && state.selectedAsset.id === assetId) {
                    state.selectedAsset = a;
                    showAssetDetails(a);
                }
                _showFlash('"' + (a.display_name || assetId) + '" finalized — click Relink to swap to hi-res', 'info-message');
            }
        } catch (_) { /* transient — try again next tick */ }
    }, 5000);
}

async function relinkSelectedAsset() {
    if (!state.selectedAsset) return;
    var asset = state.selectedAsset;
    var entry = state.importedAssets['live:' + asset.id];
    if (!entry) {
        showErrorMessage('No live mount recorded for this asset');
        return;
    }

    try {
        elements.relinkBtn.disabled = true;
        showProgress('Fetching hi-res link…', 10);

        var hires = await getHiresDownloadInfo(asset.id);
        var safeName = sanitizeFilename(hires.filename || (asset.display_name || asset.id) + '.mov');

        showProgress('Downloading hi-res' + (hires.file_size ? ' (' + formatFileSize(hires.file_size) + ')' : '') + '…', 20);
        var localPath = await downloadFile(hires.url, safeName);

        showProgress('Relinking in Premiere…', 85);
        await relinkInPremiere(entry.livePath, localPath);

        saveImportMapping(localPath, safeName, asset);
        hideProgress();
        showSuccessMessage('Relinked to hi-res: ' + safeName);
    } catch (err) {
        hideProgress();
        showErrorMessage('Relink failed: ' + err.message);
    } finally {
        elements.relinkBtn.disabled = false;
    }
}

// Walks every project item and swaps clip media paths matching `oldPath`
// onto `newPath`. Premiere's ProjectItem.changeMediaPath() does the relink
// without touching timeline placement.
function relinkInPremiere(oldPath, newPath) {
    var oldEsc = oldPath.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
    var newEsc = newPath.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
    var script = [
        '(function () {',
        '  var out = { success: false, relinked: 0, message: "" };',
        '  try {',
        '    if (!app.project) { out.message = "No project open"; return JSON.stringify(out); }',
        '    var oldPath = "' + oldEsc + '";',
        '    var newPath = "' + newEsc + '";',
        '    function walk(item) {',
        '      for (var i = 0; i < item.children.numItems; i++) {',
        '        var c = item.children[i];',
        '        if (c.type === 1 || c.type === 2) {',  /* clip or sub-clip */
        '          if (c.getMediaPath() === oldPath) {',
        '            c.changeMediaPath(newPath);',
        '            out.relinked++;',
        '          }',
        '        }',
        '        if (c.children && c.children.numItems > 0) walk(c);',
        '      }',
        '    }',
        '    walk(app.project.rootItem);',
        '    out.success = out.relinked > 0;',
        '    out.message = out.relinked + " clip(s) relinked";',
        '  } catch (e) { out.message = e.message; }',
        '  return JSON.stringify(out);',
        '})();',
    ].join('\n');

    return new Promise(function (resolve, reject) {
        csInterface.evalScript(script, function (resultStr) {
            try {
                var parsed = JSON.parse(resultStr);
                if (parsed.success) resolve(parsed);
                else reject(new Error(parsed.message || 'relink found no matching clips'));
            } catch (e) {
                reject(new Error('ExtendScript error: ' + resultStr));
            }
        });
    });
}

// ============================================================================
// Search and Filter
// ============================================================================

function handleSearch(e) {
    state.searchQuery = e.target.value;
    state.currentPage = 0;
    fetchAssets();
}

function handleProjectFilter(e) {
    state.selectedProject = e.target.value;
    state.currentPage     = 0;
    fetchAssets();
}

// ============================================================================
// Import — Proxy
// ============================================================================

async function importSelectedAsset() {
    if (!state.selectedAsset) {
        showErrorMessage('No asset selected');
        return;
    }
    await importAsset(state.selectedAsset);
}

async function importAllAssets() {
    if (state.assets.length === 0) {
        showErrorMessage('No assets to import');
        return;
    }
    for (const asset of state.assets) {
        await importAsset(asset);
    }
    showSuccessMessage('All assets imported');
}

async function importAsset(asset) {
    try {
        elements.importBtn.disabled = true;

        // 1. Get proxy URL (absolute — /stream returns a relative path)
        showProgress('Getting download link...', 5);
        const url = await getSignedDownloadUrl(asset.id);

        // 2. Download proxy to OS temp dir via Node.js
        const safeName = sanitizeFilename(
            (asset.display_name || asset.filename || asset.id) + '.mp4'
        );
        showProgress('Downloading ' + safeName + '...', 10);
        const filePath = await downloadFile(url, safeName);

        // 3. Hand the local path to Premiere Pro
        showProgress('Importing into Premiere Pro...', 85);
        await importFileToPremiereProject(filePath);

        // 4. Store path → assetId so timeline export can resolve this clip
        saveImportMapping(filePath, safeName, asset);

        hideProgress();
        showSuccessMessage('Imported: ' + safeName);
    } catch (error) {
        console.error('Import error:', error);
        hideProgress();
        showErrorMessage('Import failed: ' + error.message);
    } finally {
        elements.importBtn.disabled = !state.selectedAsset;
    }
}

// ============================================================================
// Import — Hi-Res Original
// ============================================================================

async function importSelectedAssetHires() {
    if (!state.selectedAsset) {
        showErrorMessage('No asset selected');
        return;
    }
    await importAssetHires(state.selectedAsset);
}

async function importAssetHires(asset) {
    try {
        elements.importHiresBtn.disabled = true;

        // 1. Get hi-res presigned URL and metadata
        showProgress('Getting hi-res link...', 5);
        let hiresInfo;
        try {
            hiresInfo = await getHiresDownloadInfo(asset.id);
        } catch (err) {
            throw new Error('No hi-res source: ' + err.message);
        }

        const sizeNote = hiresInfo.file_size ? ' (' + formatFileSize(hiresInfo.file_size) + ')' : '';
        const safeName = sanitizeFilename(
            hiresInfo.filename || (asset.display_name || asset.id) + '.mxf'
        );

        // 2. Download hi-res file to OS temp dir
        showProgress('Downloading hi-res' + sizeNote + '...', 10);
        const filePath = await downloadFile(hiresInfo.url, safeName);

        // 3. Import into Premiere Pro
        showProgress('Importing hi-res into Premiere Pro...', 85);
        await importFileToPremiereProject(filePath);

        // 4. Map this path to the same asset ID (hi-res round-trips to same MAM record)
        saveImportMapping(filePath, safeName, asset);

        hideProgress();
        showSuccessMessage('Hi-res imported: ' + safeName);
    } catch (error) {
        console.error('Hi-res import error:', error);
        hideProgress();
        showErrorMessage('Hi-res import failed: ' + error.message);
    } finally {
        elements.importHiresBtn.disabled = !state.selectedAsset;
    }
}

/**
 * Persists two keys per import:
 *   - exact temp path   → so Premiere getMediaPath() matches on the same machine
 *   - 'name:filename'   → fuzzy fallback across sessions / machines
 */
function saveImportMapping(filePath, safeName, asset) {
    const entry = {
        assetId:     asset.id,
        displayName: asset.display_name || asset.filename || '',
    };
    state.importedAssets[filePath]          = entry;
    state.importedAssets['name:' + safeName] = entry;
    try {
        localStorage.setItem('mam_imported_assets', JSON.stringify(state.importedAssets));
    } catch (_) {}
}

// ============================================================================
// Active Sequence Info Bar
// ============================================================================

function refreshCurrentSequenceInfo() {
    csInterface.evalScript('getActiveSequence()', function (resultStr) {
        try {
            var parsed = JSON.parse(resultStr);
            state.currentSequenceName = parsed.sequenceName || '';
            if (state.currentSequenceName) {
                elements.seqInfoName.textContent = state.currentSequenceName;
                elements.seqInfoBar.classList.remove('hidden');
            } else {
                elements.seqInfoBar.classList.add('hidden');
            }
        } catch (e) {
            elements.seqInfoBar.classList.add('hidden');
        }
    });
}

// ============================================================================
// Export Panel UI
// ============================================================================

function populateExportProjectSelect() {
    elements.exportProjSelect.innerHTML = '<option value="">— Select project —</option>';
    state.projects.forEach(function (p) {
        var opt = document.createElement('option');
        opt.value       = p.id;
        opt.textContent = p.name;
        if (p.id === state.selectedProject) opt.selected = true;
        elements.exportProjSelect.appendChild(opt);
    });
}

function showExportPanel(timelineData) {
    elements.exportSeqName.value = timelineData.sequenceName || 'Sequence 1';
    populateExportProjectSelect();

    var totalClips  = (timelineData.clips || []).length;
    var matchedClips = resolveClipsToAssets(timelineData.clips || []).filter(function (c) {
        return c.asset_id;
    }).length;

    elements.exportClipInfo.textContent =
        matchedClips + ' of ' + totalClips + ' clip(s) matched to MAM assets';
    elements.exportClipInfo.style.color =
        matchedClips === 0 ? 'var(--error)' : 'var(--text-secondary)';

    // Stash the full timeline data for confirmExportTimeline
    elements.exportPanel.dataset.timelineJson = JSON.stringify(timelineData);
    elements.exportPanel.classList.remove('hidden');
    state.exportPanelVisible = true;
    elements.exportSeqName.focus();
}

function hideExportPanel() {
    elements.exportPanel.classList.add('hidden');
    state.exportPanelVisible = false;
}

// ============================================================================
// Timeline Export — Premiere → MAM
// ============================================================================

/**
 * Step 1 of 2: read the Premiere timeline then show the export confirmation
 * panel with pre-filled sequence name and matched clip count.
 */
async function startExportTimeline() {
    if (!state.isConnected) {
        showErrorMessage('Connect to MAM first');
        return;
    }

    // Toggle: clicking again closes the panel
    if (state.exportPanelVisible) {
        hideExportPanel();
        return;
    }

    showProgress('Reading Premiere timeline...', 20);

    const timelineData = await new Promise(function (resolve) {
        csInterface.evalScript('exportTimelineData()', function (resultStr) {
            try { resolve(JSON.parse(resultStr)); }
            catch (e) { resolve({ success: false, message: 'Parse error: ' + e.message, clips: [] }); }
        });
    });

    hideProgress();

    if (!timelineData.success) {
        showErrorMessage('Timeline read failed: ' + (timelineData.message || 'unknown error'));
        return;
    }

    if (!timelineData.clips || timelineData.clips.length === 0) {
        showErrorMessage('No clips found in the active sequence');
        return;
    }

    showExportPanel(timelineData);
}

/**
 * Step 2 of 2: resolve clip paths to MAM asset IDs, upsert the sequence,
 * then PUT the clip array to /api/v1/sequences/:id/clips.
 */
async function confirmExportTimeline() {
    var timelineData;
    try {
        timelineData = JSON.parse(elements.exportPanel.dataset.timelineJson || '{}');
    } catch (e) {
        showErrorMessage('Invalid timeline data — try reading again');
        return;
    }

    var seqName   = (elements.exportSeqName.value || '').trim() || 'Sequence 1';
    var projectId = elements.exportProjSelect.value;
    if (!projectId) {
        showErrorMessage('Select a target project');
        return;
    }

    var resolved = resolveClipsToAssets(timelineData.clips || []);
    var matched  = resolved.filter(function (c) { return c.asset_id; });

    if (matched.length === 0) {
        hideExportPanel();
        showErrorMessage('No clips matched MAM assets — import proxies or hi-res first');
        return;
    }

    hideExportPanel();
    showProgress('Creating sequence in MAM...', 20);

    try {
        var seqId = await upsertSequence(
            projectId,
            seqName,
            timelineData.frameRate || 59.94,
            timelineData.width     || 1920,
            timelineData.height    || 1080
        );

        showProgress('Writing ' + matched.length + ' clip(s)...', 60);

        var clipPayload = matched.map(function (c) {
            return {
                asset_id:            c.asset_id,
                track:               c.trackIndex,
                timeline_in_frames:  c.timelineInFrames,
                timeline_out_frames: c.timelineOutFrames,
                source_in_frames:    c.sourceInFrames,
                source_out_frames:   c.sourceOutFrames,
            };
        });

        var clipsRes = await fetch(
            state.serverUrl + '/api/v1/sequences/' + seqId + '/clips',
            {
                method:      'PUT',
                headers:     { 'Content-Type': 'application/json', Accept: 'application/json' },
                credentials: 'include',
                body:        JSON.stringify(clipPayload),
            }
        );
        if (!clipsRes.ok) throw new Error('Clip push failed: HTTP ' + clipsRes.status);

        hideProgress();
        showSuccessMessage('Timeline pushed: ' + matched.length + ' clip(s) → "' + seqName + '"');

        var skipped = resolved.length - matched.length;
        if (skipped > 0) {
            _showFlash(skipped + ' clip(s) skipped — not in MAM (import them first)', 'info-message');
        }
    } catch (err) {
        hideProgress();
        showErrorMessage('Export failed: ' + err.message);
    }
}

function cancelExportTimeline() {
    hideExportPanel();
}

/**
 * Maps Premiere clip file paths back to MAM asset IDs.
 * Lookup order: exact temp path → name-based fallback.
 */
function resolveClipsToAssets(clips) {
    return clips.map(function (clip) {
        var entry = state.importedAssets[clip.filePath];
        if (!entry) entry = state.importedAssets['name:' + clip.fileName];
        return Object.assign({}, clip, { asset_id: entry ? entry.assetId : null });
    });
}

/**
 * Finds an existing sequence by name in the target project; creates it if
 * absent.  Updates frame_rate/width/height on the existing record.
 * Returns the sequence ID.
 */
async function upsertSequence(projectId, name, frameRate, width, height) {
    var listRes = await fetch(
        state.serverUrl + '/api/v1/sequences?project_id=' + encodeURIComponent(projectId),
        { headers: { Accept: 'application/json' }, credentials: 'include' }
    );
    if (listRes.ok) {
        var seqs     = await listRes.json();
        var existing = seqs.find(function (s) { return s.name === name; });
        if (existing) {
            await fetch(state.serverUrl + '/api/v1/sequences/' + existing.id, {
                method:      'PUT',
                headers:     { 'Content-Type': 'application/json', Accept: 'application/json' },
                credentials: 'include',
                body:        JSON.stringify({ frame_rate: frameRate, width: width, height: height }),
            });
            return existing.id;
        }
    }

    var createRes = await fetch(state.serverUrl + '/api/v1/sequences', {
        method:      'POST',
        headers:     { 'Content-Type': 'application/json', Accept: 'application/json' },
        credentials: 'include',
        body:        JSON.stringify({
            project_id: projectId,
            name:       name,
            frame_rate: frameRate,
            width:      width,
            height:     height,
        }),
    });
    if (!createRes.ok) throw new Error('Failed to create sequence: HTTP ' + createRes.status);
    var seq = await createRes.json();
    return seq.id;
}

// ============================================================================
// UI Helpers
// ============================================================================

function handleAssetClick(e) {
    var card = e.target.closest('.asset-card');
    if (!card) return;

    document.querySelectorAll('.asset-card.selected').forEach(function (el) {
        el.classList.remove('selected');
    });
    card.classList.add('selected');

    var assetId = card.dataset.assetId;
    var asset   = state.assets.find(function (a) { return a.id === assetId; });
    if (asset) showAssetDetails(asset);
}

function showProgress(label, percent) {
    elements.progressContainer.classList.add('visible');
    elements.progressLabel.textContent = label;
    state.downloadProgress = percent;
    updateProgressUI();
}

function hideProgress() {
    elements.progressContainer.classList.remove('visible');
    state.downloadProgress = 0;
    updateProgressUI();
}

function updateProgressUI() {
    elements.progressFill.style.width = state.downloadProgress + '%';
}

function showErrorMessage(message) {
    _showFlash(message, 'error-message');
}

function showSuccessMessage(message) {
    _showFlash(message, 'success-message');
}

function _showFlash(message, className) {
    var el         = document.createElement('div');
    el.className   = className;
    el.textContent = message;
    var anchor     = document.querySelector('.search-filter-area');
    anchor.insertBefore(el, anchor.firstChild);
    setTimeout(function () { el.remove(); }, 5000);
}

function logMessage(message) {
    console.log('[MAM Panel] ' + message);
}

// ============================================================================
// File Download and Premiere Import (Node.js / ExtendScript)
// ============================================================================

/**
 * Downloads a remote URL to a local temp file using Node.js http/https.
 * Requires --enable-nodejs in the CEP manifest.
 */
function downloadFile(url, filename) {
    return new Promise(function (resolve, reject) {
        try {
            var https = require('https');
            var http  = require('http');
            var fs    = require('fs');
            var path  = require('path');
            var os    = require('os');

            var tempPath = path.join(os.tmpdir(), filename);
            var file     = fs.createWriteStream(tempPath);
            var protocol = url.startsWith('https') ? https : http;

            protocol.get(url, function (res) {
                if (res.statusCode !== 200) {
                    file.close();
                    fs.unlink(tempPath, function () {});
                    reject(new Error('Download HTTP ' + res.statusCode));
                    return;
                }

                var total    = parseInt(res.headers['content-length'] || '0', 10);
                var received = 0;

                res.on('data', function (chunk) {
                    received += chunk.length;
                    if (total > 0) {
                        state.downloadProgress = 10 + (received / total) * 75;
                        updateProgressUI();
                    }
                });

                res.pipe(file);

                file.on('finish', function () {
                    file.close(function () { resolve(tempPath); });
                });

                file.on('error', function (err) {
                    fs.unlink(tempPath, function () {});
                    reject(err);
                });
            }).on('error', function (err) {
                fs.unlink(tempPath, function () {});
                reject(err);
            });
        } catch (err) {
            reject(new Error('Node.js unavailable for download: ' + err.message));
        }
    });
}

/**
 * Calls csInterface.evalScript to import a local file into the open Premiere project.
 */
function importFileToPremiereProject(filePath) {
    return new Promise(function (resolve, reject) {
        var safePath = filePath.replace(/\\/g, '\\\\');

        var script = [
            '(function() {',
            '  var result = { success: false, message: "" };',
            '  try {',
            '    if (!app.project) {',
            '      result.message = "No active Premiere Pro project";',
            '      return JSON.stringify(result);',
            '    }',
            '    var f = new File("' + safePath + '");',
            '    if (!f.exists) {',
            '      result.message = "File not found: ' + safePath + '";',
            '      return JSON.stringify(result);',
            '    }',
            '    app.project.importFiles(["' + safePath + '"]);',
            '    result.success = true;',
            '    result.message = "Imported successfully";',
            '  } catch (e) {',
            '    result.message = e.message;',
            '  }',
            '  return JSON.stringify(result);',
            '})();',
        ].join('\n');

        csInterface.evalScript(script, function (resultStr) {
            try {
                var parsed = JSON.parse(resultStr);
                if (parsed.success) resolve(parsed);
                else reject(new Error(parsed.message));
            } catch (e) {
                reject(new Error('ExtendScript error: ' + resultStr));
            }
        });
    });
}

// ============================================================================
// Utility Functions
// ============================================================================

function debounce(func, delay) {
    var timeout;
    return function () {
        var args = arguments;
        clearTimeout(timeout);
        timeout = setTimeout(function () { func.apply(null, args); }, delay);
    };
}

function escapeHtml(str) {
    return String(str)
        .replace(/&/g,  '&amp;')
        .replace(/</g,  '&lt;')
        .replace(/>/g,  '&gt;')
        .replace(/"/g,  '&quot;')
        .replace(/'/g,  '&#39;');
}

function sanitizeFilename(name) {
    return name.replace(/[<>:"/\\|?*\x00-\x1f]/g, '_');
}

function formatDuration(seconds) {
    if (!seconds) return 'N/A';
    var h = Math.floor(seconds / 3600);
    var m = Math.floor((seconds % 3600) / 60);
    var s = Math.floor(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 formatFileSize(bytes) {
    if (!bytes) return '0 B';
    var k     = 1024;
    var sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
    var i     = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
