/**
 * Streams — picture sources, capture templates, PP templates, filters.
 */

import {
    _cachedDisplays,
    displaysCache,
    _cachedStreams,
    _cachedPPTemplates,
    _cachedCaptureTemplates,
    _availableFilters,
    availableEngines, setAvailableEngines,
    currentEditingTemplateId, setCurrentEditingTemplateId,
    _templateNameManuallyEdited, set_templateNameManuallyEdited,
    _streamNameManuallyEdited, set_streamNameManuallyEdited,
    _streamModalPPTemplates, set_streamModalPPTemplates,
    _modalFilters, set_modalFilters,
    _ppTemplateNameManuallyEdited, set_ppTemplateNameManuallyEdited,
    currentTestingTemplate, setCurrentTestingTemplate,
    _currentTestStreamId, set_currentTestStreamId,
    _currentTestPPTemplateId, set_currentTestPPTemplateId,
    _cachedAudioSources,
    _cachedValueSources,
    _cachedSyncClocks,
    _cachedWeatherSources,
    _cachedHASources,
    _cachedAudioTemplates,
    _cachedCSPTemplates,
    _csptModalFilters, set_csptModalFilters,
    _csptNameManuallyEdited, set_csptNameManuallyEdited,
    _stripFilters,
    availableAudioEngines, setAvailableAudioEngines,
    currentEditingAudioTemplateId, setCurrentEditingAudioTemplateId,
    _audioTemplateNameManuallyEdited, set_audioTemplateNameManuallyEdited,
    _sourcesLoading, set_sourcesLoading,
    apiKey,
    streamsCache, ppTemplatesCache, captureTemplatesCache,
    audioSourcesCache, audioTemplatesCache, valueSourcesCache, syncClocksCache, weatherSourcesCache, haSourcesCache, assetsCache, _cachedAssets, filtersCache,
    colorStripSourcesCache,
    csptCache, stripFiltersCache,
    gradientsCache, GradientEntity,
} from '../core/state.ts';
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml } from '../core/api.ts';
import { t } from '../core/i18n.ts';
import { Modal } from '../core/modal.ts';
import { showToast, showConfirm, openLightbox, openFullImageLightbox, showOverlaySpinner, hideOverlaySpinner, updateOverlayPreview, setTabRefreshing, setupBackdropClose } from '../core/ui.ts';
import { openDisplayPicker, formatDisplayLabel } from './displays.ts';
import { CardSection } from '../core/card-sections.ts';
import { TreeNav } from '../core/tree-nav.ts';
import { updateSubTabHash } from './tabs.ts';
import { createValueSourceCard } from './value-sources.ts';
import { createSyncClockCard, initSyncClockDelegation } from './sync-clocks.ts';
import { createWeatherSourceCard, initWeatherSourceDelegation } from './weather-sources.ts';
import { createHASourceCard, initHASourceDelegation } from './home-assistant-sources.ts';
import { createAssetCard, initAssetDelegation } from './assets.ts';
import { createColorStripCard } from './color-strips.ts';
import { initAudioSourceDelegation } from './audio-sources.ts';
import {
    getEngineIcon, getAudioEngineIcon, getPictureSourceIcon, getAudioSourceIcon, getColorStripIcon,
    ICON_TEMPLATE, ICON_CLONE, ICON_EDIT, ICON_TEST, ICON_LINK_SOURCE,
    ICON_FPS, ICON_WEB, ICON_VALUE_SOURCE, ICON_CLOCK, ICON_AUDIO_LOOPBACK, ICON_AUDIO_INPUT,
    ICON_AUDIO_TEMPLATE, ICON_MONITOR, ICON_WRENCH, ICON_RADIO, ICON_ACTIVITY,
    ICON_CAPTURE_TEMPLATE, ICON_PP_TEMPLATE, ICON_CSPT, ICON_HELP, ICON_TRASH, ICON_PALETTE, ICON_ASSET,
    getAssetTypeIcon,
} from '../core/icons.ts';
import * as P from '../core/icon-paths.ts';

const _icon = (d: string) => `<svg class="icon" viewBox="0 0 24 24">${d}</svg>`;
import { wrapCard } from '../core/card-colors.ts';
import { TagInput, renderTagChips } from '../core/tag-input.ts';
import { IconSelect } from '../core/icon-select.ts';
import { EntitySelect } from '../core/entity-palette.ts';
import { FilterListManager } from '../core/filter-list.ts';

// ── TagInput instances for modals ──
let _streamTagsInput: TagInput | null = null;
let _ppTemplateTagsInput: TagInput | null = null;
let _csptTagsInput: TagInput | null = null;

// ── Bulk action handlers ──
function _bulkDeleteFactory(endpoint: string, cache: any, toast: string) {
    return async (ids: string[]) => {
        const results = await Promise.allSettled(ids.map(id =>
            fetchWithAuth(`/${endpoint}/${id}`, { method: 'DELETE' })
        ));
        const failed = results.filter(r => r.status === 'rejected' || ((r as any).value && !(r as any).value.ok)).length;
        if (failed) showToast(`${ids.length - failed}/${ids.length} deleted`, 'warning');
        else showToast(t(toast), 'success');
        cache.invalidate();
        await loadPictureSources();
    };
}

const _streamDeleteAction = [{ key: 'delete', labelKey: 'bulk.delete', icon: ICON_TRASH, style: 'danger', confirm: 'bulk.confirm_delete', handler: _bulkDeleteFactory('picture-sources', streamsCache, 'streams.deleted') }];
const _captureTemplateDeleteAction = [{ key: 'delete', labelKey: 'bulk.delete', icon: ICON_TRASH, style: 'danger', confirm: 'bulk.confirm_delete', handler: _bulkDeleteFactory('capture-templates', captureTemplatesCache, 'templates.deleted') }];
const _ppTemplateDeleteAction = [{ key: 'delete', labelKey: 'bulk.delete', icon: ICON_TRASH, style: 'danger', confirm: 'bulk.confirm_delete', handler: _bulkDeleteFactory('postprocessing-templates', ppTemplatesCache, 'templates.deleted') }];
const _audioSourceDeleteAction = [{ key: 'delete', labelKey: 'bulk.delete', icon: ICON_TRASH, style: 'danger', confirm: 'bulk.confirm_delete', handler: _bulkDeleteFactory('audio-sources', audioSourcesCache, 'audio_source.deleted') }];
const _audioTemplateDeleteAction = [{ key: 'delete', labelKey: 'bulk.delete', icon: ICON_TRASH, style: 'danger', confirm: 'bulk.confirm_delete', handler: _bulkDeleteFactory('audio-templates', audioTemplatesCache, 'templates.deleted') }];
const _colorStripDeleteAction = [{ key: 'delete', labelKey: 'bulk.delete', icon: ICON_TRASH, style: 'danger', confirm: 'bulk.confirm_delete', handler: _bulkDeleteFactory('color-strip-sources', colorStripSourcesCache, 'color_strip.deleted') }];
const _valueSourceDeleteAction = [{ key: 'delete', labelKey: 'bulk.delete', icon: ICON_TRASH, style: 'danger', confirm: 'bulk.confirm_delete', handler: _bulkDeleteFactory('value-sources', valueSourcesCache, 'value_source.deleted') }];
const _syncClockDeleteAction = [{ key: 'delete', labelKey: 'bulk.delete', icon: ICON_TRASH, style: 'danger', confirm: 'bulk.confirm_delete', handler: _bulkDeleteFactory('sync-clocks', syncClocksCache, 'sync_clock.deleted') }];
const _weatherSourceDeleteAction = [{ key: 'delete', labelKey: 'bulk.delete', icon: ICON_TRASH, style: 'danger', confirm: 'bulk.confirm_delete', handler: _bulkDeleteFactory('weather-sources', weatherSourcesCache, 'weather_source.deleted') }];
const _haSourceDeleteAction = [{ key: 'delete', labelKey: 'bulk.delete', icon: ICON_TRASH, style: 'danger', confirm: 'bulk.confirm_delete', handler: _bulkDeleteFactory('home-assistant/sources', haSourcesCache, 'ha_source.deleted') }];
const _assetDeleteAction = [{ key: 'delete', labelKey: 'bulk.delete', icon: ICON_TRASH, style: 'danger', confirm: 'bulk.confirm_delete', handler: _bulkDeleteFactory('assets', assetsCache, 'asset.deleted') }];
const _csptDeleteAction = [{ key: 'delete', labelKey: 'bulk.delete', icon: ICON_TRASH, style: 'danger', confirm: 'bulk.confirm_delete', handler: _bulkDeleteFactory('color-strip-processing-templates', csptCache, 'templates.deleted') }];

/** Resolve an asset ID to its display name. */
function _getAssetName(assetId?: string | null): string {
    if (!assetId) return '—';
    const asset = _cachedAssets.find((a: any) => a.id === assetId);
    return asset ? asset.name : assetId;
}

/** Get EntitySelect items for a given asset type (image/video). */
function _getAssetItems(assetType: string) {
    return _cachedAssets
        .filter((a: any) => a.asset_type === assetType)
        .map((a: any) => ({ value: a.id, label: a.name, icon: getAssetTypeIcon(assetType), desc: a.filename }));
}

let _imageAssetEntitySelect: EntitySelect | null = null;
let _videoAssetEntitySelect: EntitySelect | null = null;

function _ensureImageAssetEntitySelect() {
    const sel = document.getElementById('stream-image-asset') as HTMLSelectElement | null;
    if (!sel) return;
    if (_imageAssetEntitySelect) _imageAssetEntitySelect.destroy();
    _imageAssetEntitySelect = null;
    const items = _getAssetItems('image');
    sel.innerHTML = `<option value="">${t('streams.image_asset.select')}</option>` +
        items.map(a => `<option value="${a.value}">${escapeHtml(a.label)}</option>`).join('');
    _imageAssetEntitySelect = new EntitySelect({
        target: sel,
        getItems: () => _getAssetItems('image'),
        placeholder: t('streams.image_asset.search') || 'Search image assets…',
    });
}

function _ensureVideoAssetEntitySelect() {
    const sel = document.getElementById('stream-video-asset') as HTMLSelectElement | null;
    if (!sel) return;
    if (_videoAssetEntitySelect) _videoAssetEntitySelect.destroy();
    _videoAssetEntitySelect = null;
    const items = _getAssetItems('video');
    sel.innerHTML = `<option value="">${t('streams.video_asset.select')}</option>` +
        items.map(a => `<option value="${a.value}">${escapeHtml(a.label)}</option>`).join('');
    _videoAssetEntitySelect = new EntitySelect({
        target: sel,
        getItems: () => _getAssetItems('video'),
        placeholder: t('streams.video_asset.search') || 'Search video assets…',
    });
}

function _destroyAssetEntitySelects() {
    if (_imageAssetEntitySelect) { _imageAssetEntitySelect.destroy(); _imageAssetEntitySelect = null; }
    if (_videoAssetEntitySelect) { _videoAssetEntitySelect.destroy(); _videoAssetEntitySelect = null; }
}

// ── Card section instances ──
const csRawStreams = new CardSection('raw-streams', { titleKey: 'streams.section.streams', gridClass: 'templates-grid', addCardOnclick: "showAddStreamModal('raw')", keyAttr: 'data-stream-id', emptyKey: 'section.empty.picture_sources', bulkActions: _streamDeleteAction });
const csRawTemplates = new CardSection('raw-templates', { titleKey: 'templates.title', gridClass: 'templates-grid', addCardOnclick: "showAddTemplateModal()", keyAttr: 'data-template-id', emptyKey: 'section.empty.capture_templates', bulkActions: _captureTemplateDeleteAction });
const csProcStreams = new CardSection('proc-streams', { titleKey: 'streams.section.streams', gridClass: 'templates-grid', addCardOnclick: "showAddStreamModal('processed')", keyAttr: 'data-stream-id', emptyKey: 'section.empty.picture_sources', bulkActions: _streamDeleteAction });
const csProcTemplates = new CardSection('proc-templates', { titleKey: 'postprocessing.title', gridClass: 'templates-grid', addCardOnclick: "showAddPPTemplateModal()", keyAttr: 'data-pp-template-id', emptyKey: 'section.empty.pp_templates', bulkActions: _ppTemplateDeleteAction });
const csAudioMulti = new CardSection('audio-multi', { titleKey: 'audio_source.group.multichannel', gridClass: 'templates-grid', addCardOnclick: "showAudioSourceModal('multichannel')", keyAttr: 'data-id', emptyKey: 'section.empty.audio_sources', bulkActions: _audioSourceDeleteAction });
const csAudioMono = new CardSection('audio-mono', { titleKey: 'audio_source.group.mono', gridClass: 'templates-grid', addCardOnclick: "showAudioSourceModal('mono')", keyAttr: 'data-id', emptyKey: 'section.empty.audio_sources', bulkActions: _audioSourceDeleteAction });
const csAudioBandExtract = new CardSection('audio-band-extract', { titleKey: 'audio_source.group.band_extract', gridClass: 'templates-grid', addCardOnclick: "showAudioSourceModal('band_extract')", keyAttr: 'data-id', emptyKey: 'section.empty.audio_sources', bulkActions: _audioSourceDeleteAction });
const csStaticStreams = new CardSection('static-streams', { titleKey: 'streams.group.static_image', gridClass: 'templates-grid', addCardOnclick: "showAddStreamModal('static_image')", keyAttr: 'data-stream-id', emptyKey: 'section.empty.picture_sources', bulkActions: _streamDeleteAction });
const csVideoStreams = new CardSection('video-streams', { titleKey: 'streams.group.video', gridClass: 'templates-grid', addCardOnclick: "showAddStreamModal('video')", keyAttr: 'data-stream-id', emptyKey: 'section.empty.picture_sources', bulkActions: _streamDeleteAction });
const csAudioTemplates = new CardSection('audio-templates', { titleKey: 'audio_template.title', gridClass: 'templates-grid', addCardOnclick: "showAddAudioTemplateModal()", keyAttr: 'data-audio-template-id', emptyKey: 'section.empty.audio_templates', bulkActions: _audioTemplateDeleteAction });
const csColorStrips = new CardSection('color-strips', { titleKey: 'targets.section.color_strips', gridClass: 'templates-grid', addCardOnclick: "showCSSEditor()", keyAttr: 'data-css-id', emptyKey: 'section.empty.color_strips', bulkActions: _colorStripDeleteAction });
const csValueSources = new CardSection('value-sources', { titleKey: 'value_source.group.title', gridClass: 'templates-grid', addCardOnclick: "showValueSourceModal()", keyAttr: 'data-id', emptyKey: 'section.empty.value_sources', bulkActions: _valueSourceDeleteAction });
const csSyncClocks = new CardSection('sync-clocks', { titleKey: 'sync_clock.group.title', gridClass: 'templates-grid', addCardOnclick: "showSyncClockModal()", keyAttr: 'data-id', emptyKey: 'section.empty.sync_clocks', bulkActions: _syncClockDeleteAction });
const csWeatherSources = new CardSection('weather-sources', { titleKey: 'weather_source.group.title', gridClass: 'templates-grid', addCardOnclick: "showWeatherSourceModal()", keyAttr: 'data-id', emptyKey: 'section.empty.weather_sources', bulkActions: _weatherSourceDeleteAction });
const csHASources = new CardSection('ha-sources', { titleKey: 'ha_source.group.title', gridClass: 'templates-grid', addCardOnclick: "showHASourceModal()", keyAttr: 'data-id', emptyKey: 'section.empty.ha_sources', bulkActions: _haSourceDeleteAction });
const csAssets = new CardSection('assets', { titleKey: 'asset.group.title', gridClass: 'templates-grid', addCardOnclick: "showAssetUploadModal()", keyAttr: 'data-id', emptyKey: 'section.empty.assets', bulkActions: _assetDeleteAction });
const csCSPTemplates = new CardSection('css-proc-templates', { titleKey: 'css_processing.title', gridClass: 'templates-grid', addCardOnclick: "showAddCSPTModal()", keyAttr: 'data-cspt-id', emptyKey: 'section.empty.cspt', bulkActions: _csptDeleteAction });
const _gradientDeleteAction = [{ key: 'delete', labelKey: 'bulk.delete', icon: ICON_TRASH, style: 'danger', confirm: 'bulk.confirm_delete', handler: _bulkDeleteFactory('gradients', gradientsCache, 'gradient.deleted') }];
const csGradients = new CardSection('gradients', { titleKey: 'gradient.group.title', gridClass: 'templates-grid', addCardOnclick: "showGradientModal()", keyAttr: 'data-id', emptyKey: 'section.empty.gradients', bulkActions: _gradientDeleteAction });

// Re-render picture sources when language changes
document.addEventListener('languageChanged', () => { if (apiKey) loadPictureSources(); });

// ===== Modal instances =====


class StreamEditorModal extends Modal {
    constructor() { super('stream-modal'); }

    snapshotValues() {
        return {
            name: (document.getElementById('stream-name') as HTMLInputElement).value,
            description: (document.getElementById('stream-description') as HTMLInputElement).value,
            type: (document.getElementById('stream-type') as HTMLSelectElement).value,
            displayIndex: (document.getElementById('stream-display-index') as HTMLInputElement).value,
            captureTemplate: (document.getElementById('stream-capture-template') as HTMLSelectElement).value,
            targetFps: (document.getElementById('stream-target-fps') as HTMLInputElement).value,
            source: (document.getElementById('stream-source') as HTMLSelectElement).value,
            ppTemplate: (document.getElementById('stream-pp-template') as HTMLSelectElement).value,
            imageAsset: (document.getElementById('stream-image-asset') as HTMLSelectElement).value,
            tags: JSON.stringify(_streamTagsInput ? _streamTagsInput.getValue() : []),
        };
    }

    onForceClose() {
        if (_streamTagsInput) { _streamTagsInput.destroy(); _streamTagsInput = null; }
        _destroyAssetEntitySelects();
        (document.getElementById('stream-type') as HTMLSelectElement).disabled = false;
        set_streamNameManuallyEdited(false);
    }
}

class PPTemplateEditorModal extends Modal {
    constructor() { super('pp-template-modal'); }

    snapshotValues() {
        return {
            name: (document.getElementById('pp-template-name') as HTMLInputElement).value,
            description: (document.getElementById('pp-template-description') as HTMLInputElement).value,
            filters: JSON.stringify(_modalFilters.map(fi => ({ filter_id: fi.filter_id, options: fi.options }))),
            tags: JSON.stringify(_ppTemplateTagsInput ? _ppTemplateTagsInput.getValue() : []),
        };
    }

    onForceClose() {
        if (_ppTemplateTagsInput) { _ppTemplateTagsInput.destroy(); _ppTemplateTagsInput = null; }
        set_modalFilters([]);
        set_ppTemplateNameManuallyEdited(false);
    }
}


class CSPTEditorModal extends Modal {
    constructor() { super('cspt-modal'); }

    snapshotValues() {
        return {
            name: (document.getElementById('cspt-name') as HTMLInputElement).value,
            description: (document.getElementById('cspt-description') as HTMLInputElement).value,
            filters: JSON.stringify(_csptModalFilters.map(fi => ({ filter_id: fi.filter_id, options: fi.options }))),
            tags: JSON.stringify(_csptTagsInput ? _csptTagsInput.getValue() : []),
        };
    }

    onForceClose() {
        if (_csptTagsInput) { _csptTagsInput.destroy(); _csptTagsInput = null; }
        set_csptModalFilters([]);
        set_csptNameManuallyEdited(false);
    }
}

const streamModal = new StreamEditorModal();
const testStreamModal = new Modal('test-stream-modal');
const ppTemplateModal = new PPTemplateEditorModal();
const testPPTemplateModal = new Modal('test-pp-template-modal');
let _ppTestSourceEntitySelect: EntitySelect | null = null;
const csptModal = new CSPTEditorModal();


// ── Capture Templates (extracted to streams-capture-templates.ts) ──
import { showAddTemplateModal as _localShowAddTemplateModal, _runTestViaWS as _localRunTestViaWS } from './streams-capture-templates.ts';
export {
    showAddTemplateModal, editTemplate, closeTemplateModal, saveTemplate, deleteTemplate,
    showTestTemplateModal, closeTestTemplateModal, onEngineChange, runTemplateTest,
    updateCaptureDuration, _runTestViaWS,
} from './streams-capture-templates.ts';

// ── Audio Templates (extracted to streams-audio-templates.ts) ──
export {
    showAddAudioTemplateModal, editAudioTemplate, closeAudioTemplateModal, saveAudioTemplate, deleteAudioTemplate,
    cloneAudioTemplate, onAudioEngineChange,
    showTestAudioTemplateModal, closeTestAudioTemplateModal, startAudioTemplateTest,
} from './streams-audio-templates.ts';

// ===== Picture Sources =====

export async function loadPictureSources() {
    if (_sourcesLoading) return;
    set_sourcesLoading(true);
    if (!csRawStreams.isMounted()) setTabRefreshing('streams-list', true);
    try {
        const [streams] = await Promise.all([
            streamsCache.fetch(),
            ppTemplatesCache.fetch(),
            captureTemplatesCache.fetch(),
            audioSourcesCache.fetch(),
            valueSourcesCache.fetch(),
            syncClocksCache.fetch(),
            weatherSourcesCache.fetch(),
            haSourcesCache.fetch(),
            assetsCache.fetch(),
            audioTemplatesCache.fetch(),
            colorStripSourcesCache.fetch(),
            csptCache.fetch(),
            gradientsCache.fetch(),
            filtersCache.data.length === 0 ? filtersCache.fetch() : Promise.resolve(filtersCache.data),
        ]);
        renderPictureSourcesList(streams);
    } catch (error) {
        if (error.isAuth) return;
        console.error('Error loading picture sources:', error);
        document.getElementById('streams-list')!.innerHTML = `
            <div class="error-message">${t('streams.error.load')}: ${error.message}</div>
        `;
    } finally {
        set_sourcesLoading(false);
        setTabRefreshing('streams-list', false);
    }
}

let _streamsTreeTriggered = false;

const _streamsTree = new TreeNav('streams-tree-nav', {
    onSelect: (key) => {
        _streamsTreeTriggered = true;
        switchStreamTab(key);
        _streamsTreeTriggered = false;
    }
});

export function switchStreamTab(tabKey: string) {
    document.querySelectorAll('.stream-tab-panel[id^="stream-tab-"]').forEach(panel =>
        panel.classList.toggle('active', panel.id === `stream-tab-${tabKey}`)
    );
    localStorage.setItem('activeStreamTab', tabKey);
    updateSubTabHash('streams', tabKey);
    // Update tree active state (unless the tree triggered this switch)
    if (!_streamsTreeTriggered) {
        _streamsTree.setActive(tabKey);
    }
}

const _streamSectionMap = {
    raw: [csRawStreams],
    raw_templates: [csRawTemplates],
    static_image: [csStaticStreams],
    video: [csVideoStreams],
    processed: [csProcStreams],
    proc_templates: [csProcTemplates],
    css_processing: [csCSPTemplates],
    color_strip: [csColorStrips],
    audio_multi: [csAudioMulti],
    audio_mono: [csAudioMono],
    audio_band_extract: [csAudioBandExtract],
    audio_templates: [csAudioTemplates],
    value: [csValueSources],
    sync: [csSyncClocks],
    weather: [csWeatherSources],
    home_assistant: [csHASources],
};

type StreamCardRenderer = (stream: any) => string;

const PICTURE_SOURCE_CARD_RENDERERS: Record<string, StreamCardRenderer> = {
    raw: (stream) => {
        let capTmplName = '';
        if (stream.capture_template_id) {
            const capTmpl = _cachedCaptureTemplates.find(t => t.id === stream.capture_template_id);
            if (capTmpl) capTmplName = escapeHtml(capTmpl.name);
        }
        return `<div class="stream-card-props">
            <span class="stream-card-prop" title="${t('streams.display')}">${ICON_MONITOR} ${stream.display_index ?? 0}</span>
            <span class="stream-card-prop" title="${t('streams.target_fps')}">${ICON_FPS} ${stream.target_fps ?? 30}</span>
            ${capTmplName ? `<span class="stream-card-prop stream-card-link" title="${t('streams.capture_template')}" onclick="event.stopPropagation(); navigateToCard('streams','raw_templates','raw-templates','data-template-id','${stream.capture_template_id}')">${ICON_TEMPLATE} ${capTmplName}</span>` : ''}
        </div>`;
    },
    processed: (stream) => {
        const sourceStream = _cachedStreams.find(s => s.id === stream.source_stream_id);
        const sourceName = sourceStream ? escapeHtml(sourceStream.name) : (stream.source_stream_id || '-');
        const sourceSubTab = sourceStream ? (sourceStream.stream_type === 'static_image' ? 'static_image' : 'raw') : 'raw';
        const sourceSection = sourceStream ? (sourceStream.stream_type === 'static_image' ? 'static-streams' : 'raw-streams') : 'raw-streams';
        let ppTmplName = '';
        if (stream.postprocessing_template_id) {
            const ppTmpl = _cachedPPTemplates.find(p => p.id === stream.postprocessing_template_id);
            if (ppTmpl) ppTmplName = escapeHtml(ppTmpl.name);
        }
        return `<div class="stream-card-props">
            <span class="stream-card-prop stream-card-link" title="${t('streams.source')}" onclick="event.stopPropagation(); navigateToCard('streams','${sourceSubTab}','${sourceSection}','data-stream-id','${stream.source_stream_id}')">${ICON_LINK_SOURCE} ${sourceName}</span>
            ${ppTmplName ? `<span class="stream-card-prop stream-card-link" title="${t('streams.pp_template')}" onclick="event.stopPropagation(); navigateToCard('streams','proc_templates','proc-templates','data-pp-template-id','${stream.postprocessing_template_id}')">${ICON_TEMPLATE} ${ppTmplName}</span>` : ''}
        </div>`;
    },
    static_image: (stream) => {
        const assetName = _getAssetName(stream.image_asset_id);
        return `<div class="stream-card-props">
            <span class="stream-card-prop stream-card-prop-full" title="${escapeHtml(assetName)}">${ICON_ASSET} ${escapeHtml(assetName)}</span>
        </div>`;
    },
    video: (stream) => {
        const assetName = _getAssetName(stream.video_asset_id);
        return `<div class="stream-card-props">
            <span class="stream-card-prop stream-card-prop-full" title="${escapeHtml(assetName)}">${ICON_ASSET} ${escapeHtml(assetName)}</span>
            <span class="stream-card-prop" title="${t('streams.target_fps')}">${ICON_FPS} ${stream.target_fps ?? 30}</span>
            ${stream.loop !== false ? `<span class="stream-card-prop">↻</span>` : ''}
            ${stream.playback_speed && stream.playback_speed !== 1.0 ? `<span class="stream-card-prop">${stream.playback_speed}×</span>` : ''}
        </div>`;
    },
};

function renderPictureSourcesList(streams: any) {
    const container = document.getElementById('streams-list')!;
    const activeTab = localStorage.getItem('activeStreamTab') || 'raw';

    const renderStreamCard = (stream: any) => {
        const typeIcon = getPictureSourceIcon(stream.stream_type);

        const renderer = PICTURE_SOURCE_CARD_RENDERERS[stream.stream_type];
        const detailsHtml = renderer ? renderer(stream) : '';

        return wrapCard({
            type: 'template-card',
            dataAttr: 'data-stream-id',
            id: stream.id,
            removeOnclick: `deleteStream('${stream.id}')`,
            removeTitle: t('common.delete'),
            content: `
                <div class="template-card-header">
                    <div class="template-name" title="${escapeHtml(stream.name)}">${typeIcon} ${escapeHtml(stream.name)}</div>
                </div>
                ${detailsHtml}
                ${renderTagChips(stream.tags)}
                ${stream.description ? `<div class="template-config" style="opacity:0.7;">${escapeHtml(stream.description)}</div>` : ''}`,
            actions: `
                    <button class="btn btn-icon btn-secondary" onclick="showTestStreamModal('${stream.id}')" title="${t('streams.test.title')}">${ICON_TEST}</button>
                    <button class="btn btn-icon btn-secondary" onclick="cloneStream('${stream.id}')" title="${t('common.clone')}">${ICON_CLONE}</button>
                    <button class="btn btn-icon btn-secondary" onclick="editStream('${stream.id}')" title="${t('common.edit')}">${ICON_EDIT}</button>`,
        });
    };

    const renderCaptureTemplateCard = (template: any) => {
        const engineIcon = getEngineIcon(template.engine_type);
        const configEntries = Object.entries(template.engine_config);
        return wrapCard({
            type: 'template-card',
            dataAttr: 'data-template-id',
            id: template.id,
            removeOnclick: `deleteTemplate('${template.id}')`,
            removeTitle: t('common.delete'),
            content: `
                <div class="template-card-header">
                    <div class="template-name" title="${escapeHtml(template.name)}">${ICON_TEMPLATE} ${escapeHtml(template.name)}</div>
                </div>
                ${template.description ? `<div class="template-config" style="opacity:0.7;">${escapeHtml(template.description)}</div>` : ''}
                <div class="stream-card-props">
                    <span class="stream-card-prop" title="${t('templates.engine')}">${getEngineIcon(template.engine_type)} ${template.engine_type.toUpperCase()}</span>
                    ${configEntries.length > 0 ? `<span class="stream-card-prop" title="${t('templates.config.show')}">${ICON_WRENCH} ${configEntries.length}</span>` : ''}
                </div>
                ${renderTagChips(template.tags)}
                ${configEntries.length > 0 ? `
                    <div class="template-config-collapse">
                        <button type="button" class="template-config-toggle" onclick="this.parentElement.classList.toggle('open')">${t('templates.config.show')}</button>
                        <div class="template-config-animate">
                            <div class="template-config-inner">
                                <table class="config-table">
                                    ${configEntries.map(([key, val]) => `
                                        <tr>
                                            <td class="config-key">${escapeHtml(key)}</td>
                                            <td class="config-value">${escapeHtml(String(val))}</td>
                                        </tr>
                                    `).join('')}
                                </table>
                            </div>
                        </div>
                    </div>
                ` : ''}`,
            actions: `
                    <button class="btn btn-icon btn-secondary" onclick="showTestTemplateModal('${template.id}')" title="${t('templates.test.title')}">${ICON_TEST}</button>
                    <button class="btn btn-icon btn-secondary" onclick="cloneCaptureTemplate('${template.id}')" title="${t('common.clone')}">${ICON_CLONE}</button>
                    <button class="btn btn-icon btn-secondary" onclick="editTemplate('${template.id}')" title="${t('common.edit')}">${ICON_EDIT}</button>`,
        });
    };

    const renderPPTemplateCard = (tmpl: any) => {
        let filterChainHtml = '';
        if (tmpl.filters && tmpl.filters.length > 0) {
            const filterNames = tmpl.filters.map(fi => {
                let label = _getFilterName(fi.filter_id);
                if (fi.filter_id === 'filter_template' && fi.options?.template_id) {
                    const ref = _cachedPPTemplates.find(p => p.id === fi.options.template_id);
                    if (ref) label += `: ${ref.name}`;
                }
                return `<span class="filter-chain-item">${escapeHtml(label)}</span>`;
            });
            filterChainHtml = `<div class="filter-chain">${filterNames.join('<span class="filter-chain-arrow">→</span>')}</div>`;
        }
        return wrapCard({
            type: 'template-card',
            dataAttr: 'data-pp-template-id',
            id: tmpl.id,
            removeOnclick: `deletePPTemplate('${tmpl.id}')`,
            removeTitle: t('common.delete'),
            content: `
                <div class="template-card-header">
                    <div class="template-name" title="${escapeHtml(tmpl.name)}">${ICON_TEMPLATE} ${escapeHtml(tmpl.name)}</div>
                </div>
                ${tmpl.description ? `<div class="template-config" style="opacity:0.7;">${escapeHtml(tmpl.description)}</div>` : ''}
                ${filterChainHtml}
                ${renderTagChips(tmpl.tags)}`,
            actions: `
                    <button class="btn btn-icon btn-secondary" onclick="showTestPPTemplateModal('${tmpl.id}')" title="${t('postprocessing.test.title')}">${ICON_TEST}</button>
                    <button class="btn btn-icon btn-secondary" onclick="clonePPTemplate('${tmpl.id}')" title="${t('common.clone')}">${ICON_CLONE}</button>
                    <button class="btn btn-icon btn-secondary" onclick="editPPTemplate('${tmpl.id}')" title="${t('common.edit')}">${ICON_EDIT}</button>`,
        });
    };

    const renderCSPTCard = (tmpl: any) => {
        let filterChainHtml = '';
        if (tmpl.filters && tmpl.filters.length > 0) {
            const filterNames = tmpl.filters.map(fi => {
                let label = _getStripFilterName(fi.filter_id);
                if (fi.filter_id === 'css_filter_template' && fi.options?.template_id) {
                    const ref = _cachedCSPTemplates.find(p => p.id === fi.options.template_id);
                    if (ref) label += `: ${ref.name}`;
                }
                return `<span class="filter-chain-item">${escapeHtml(label)}</span>`;
            });
            filterChainHtml = `<div class="filter-chain">${filterNames.join('<span class="filter-chain-arrow">\u2192</span>')}</div>`;
        }
        return wrapCard({
            type: 'template-card',
            dataAttr: 'data-cspt-id',
            id: tmpl.id,
            removeOnclick: `deleteCSPT('${tmpl.id}')`,
            removeTitle: t('common.delete'),
            content: `
                <div class="template-card-header">
                    <div class="template-name" title="${escapeHtml(tmpl.name)}">${ICON_CSPT} ${escapeHtml(tmpl.name)}</div>
                </div>
                ${tmpl.description ? `<div class="template-config" style="opacity:0.7;">${escapeHtml(tmpl.description)}</div>` : ''}
                ${filterChainHtml}
                ${renderTagChips(tmpl.tags)}`,
            actions: `
                    <button class="btn btn-icon btn-secondary" onclick="event.stopPropagation(); testCSPT('${tmpl.id}')" title="${t('color_strip.test.title')}">${ICON_TEST}</button>
                    <button class="btn btn-icon btn-secondary" onclick="cloneCSPT('${tmpl.id}')" title="${t('common.clone')}">${ICON_CLONE}</button>
                    <button class="btn btn-icon btn-secondary" onclick="editCSPT('${tmpl.id}')" title="${t('common.edit')}">${ICON_EDIT}</button>`,
        });
    };

    const rawStreams = streams.filter(s => s.stream_type === 'raw');
    const processedStreams = streams.filter(s => s.stream_type === 'processed');
    const staticImageStreams = streams.filter(s => s.stream_type === 'static_image');
    const videoStreams = streams.filter(s => s.stream_type === 'video');

    const multichannelSources = _cachedAudioSources.filter(s => s.source_type === 'multichannel');
    const monoSources = _cachedAudioSources.filter(s => s.source_type === 'mono');
    const bandExtractSources = _cachedAudioSources.filter(s => s.source_type === 'band_extract');

    // CSPT templates
    const csptTemplates = csptCache.data;

    // Color strip sources (maps needed for card rendering)
    const colorStrips = colorStripSourcesCache.data;
    const pictureSourceMap = {};
    streams.forEach(s => { pictureSourceMap[s.id] = s; });
    const audioSourceMap = {};
    _cachedAudioSources.forEach(s => { audioSourceMap[s.id] = s; });

    const gradients = gradientsCache.data;

    const tabs = [
        { key: 'raw', icon: getPictureSourceIcon('raw'), titleKey: 'streams.group.raw', count: rawStreams.length },
        { key: 'raw_templates', icon: ICON_CAPTURE_TEMPLATE, titleKey: 'streams.group.raw_templates', count: _cachedCaptureTemplates.length },
        { key: 'static_image', icon: getPictureSourceIcon('static_image'), titleKey: 'streams.group.static_image', count: staticImageStreams.length },
        { key: 'video', icon: getPictureSourceIcon('video'), titleKey: 'streams.group.video', count: videoStreams.length },
        { key: 'processed', icon: getPictureSourceIcon('processed'), titleKey: 'streams.group.processed', count: processedStreams.length },
        { key: 'proc_templates', icon: ICON_PP_TEMPLATE, titleKey: 'streams.group.proc_templates', count: _cachedPPTemplates.length },
        { key: 'css_processing', icon: ICON_CSPT, titleKey: 'streams.group.css_processing', count: csptTemplates.length },
        { key: 'color_strip', icon: getColorStripIcon('static'), titleKey: 'streams.group.color_strip', count: colorStrips.length },
        { key: 'gradients', icon: ICON_PALETTE, titleKey: 'streams.group.gradients', count: gradients.length },
        { key: 'audio_multi', icon: getAudioSourceIcon('multichannel'), titleKey: 'audio_source.group.multichannel', count: multichannelSources.length },
        { key: 'audio_mono', icon: getAudioSourceIcon('mono'), titleKey: 'audio_source.group.mono', count: monoSources.length },
        { key: 'audio_band_extract', icon: getAudioSourceIcon('band_extract'), titleKey: 'audio_source.group.band_extract', count: bandExtractSources.length },
        { key: 'audio_templates', icon: ICON_AUDIO_TEMPLATE, titleKey: 'streams.group.audio_templates', count: _cachedAudioTemplates.length },
        { key: 'value', icon: ICON_VALUE_SOURCE, titleKey: 'streams.group.value', count: _cachedValueSources.length },
        { key: 'sync', icon: ICON_CLOCK, titleKey: 'streams.group.sync', count: _cachedSyncClocks.length },
        { key: 'weather', icon: `<svg class="icon" viewBox="0 0 24 24">${P.cloudSun}</svg>`, titleKey: 'streams.group.weather', count: _cachedWeatherSources.length },
        { key: 'home_assistant', icon: `<svg class="icon" viewBox="0 0 24 24">${P.home}</svg>`, titleKey: 'streams.group.home_assistant', count: _cachedHASources.length },
        { key: 'assets', icon: ICON_ASSET, titleKey: 'streams.group.assets', count: _cachedAssets.length },
    ];

    // Build tree navigation structure
    const treeGroups = [
        {
            key: 'picture_group', icon: getPictureSourceIcon('raw'), titleKey: 'tree.group.picture',
            children: [
                {
                    key: 'capture_group', icon: getPictureSourceIcon('raw'), titleKey: 'tree.group.capture',
                    children: [
                        { key: 'raw', titleKey: 'tree.leaf.sources', icon: getPictureSourceIcon('raw'), count: rawStreams.length },
                        { key: 'raw_templates', titleKey: 'tree.leaf.engine_templates', icon: ICON_CAPTURE_TEMPLATE, count: _cachedCaptureTemplates.length },
                    ]
                },
                {
                    key: 'static_group', icon: getPictureSourceIcon('static_image'), titleKey: 'tree.group.static',
                    children: [
                        { key: 'static_image', titleKey: 'tree.leaf.images', icon: getPictureSourceIcon('static_image'), count: staticImageStreams.length },
                        { key: 'video', titleKey: 'tree.leaf.video', icon: getPictureSourceIcon('video'), count: videoStreams.length },
                    ]
                },
                {
                    key: 'processing_group', icon: getPictureSourceIcon('processed'), titleKey: 'tree.group.processing',
                    children: [
                        { key: 'processed', titleKey: 'tree.leaf.sources', icon: getPictureSourceIcon('processed'), count: processedStreams.length },
                        { key: 'proc_templates', titleKey: 'tree.leaf.filter_templates', icon: ICON_PP_TEMPLATE, count: _cachedPPTemplates.length },
                    ]
                },
            ]
        },
        {
            key: 'strip_group', icon: getColorStripIcon('static'), titleKey: 'tree.group.strip',
            children: [
                { key: 'color_strip', titleKey: 'tree.leaf.sources', icon: getColorStripIcon('static'), count: colorStrips.length },
                { key: 'gradients', titleKey: 'streams.group.gradients', icon: ICON_PALETTE, count: gradients.length },
                { key: 'css_processing', titleKey: 'tree.leaf.processing_templates', icon: ICON_CSPT, count: csptTemplates.length },
            ]
        },
        {
            key: 'audio_group', icon: getAudioSourceIcon('multichannel'), titleKey: 'tree.group.audio',
            children: [
                { key: 'audio_multi', titleKey: 'audio_source.group.multichannel', icon: getAudioSourceIcon('multichannel'), count: multichannelSources.length },
                { key: 'audio_mono', titleKey: 'audio_source.group.mono', icon: getAudioSourceIcon('mono'), count: monoSources.length },
                { key: 'audio_band_extract', titleKey: 'audio_source.group.band_extract', icon: getAudioSourceIcon('band_extract'), count: bandExtractSources.length },
                { key: 'audio_templates', titleKey: 'tree.leaf.templates', icon: ICON_AUDIO_TEMPLATE, count: _cachedAudioTemplates.length },
            ]
        },
        {
            key: 'integrations_group', icon: `<svg class="icon" viewBox="0 0 24 24">${P.wifi}</svg>`, titleKey: 'tree.group.integrations',
            children: [
                { key: 'weather', titleKey: 'streams.group.weather', icon: `<svg class="icon" viewBox="0 0 24 24">${P.cloudSun}</svg>`, count: _cachedWeatherSources.length },
                { key: 'home_assistant', titleKey: 'streams.group.home_assistant', icon: `<svg class="icon" viewBox="0 0 24 24">${P.home}</svg>`, count: _cachedHASources.length },
            ]
        },
        {
            key: 'utility_group', icon: ICON_WRENCH, titleKey: 'tree.group.utility',
            children: [
                { key: 'value', titleKey: 'streams.group.value', icon: ICON_VALUE_SOURCE, count: _cachedValueSources.length },
                { key: 'sync', titleKey: 'streams.group.sync', icon: ICON_CLOCK, count: _cachedSyncClocks.length },
                { key: 'assets', titleKey: 'streams.group.assets', icon: ICON_ASSET, count: _cachedAssets.length },
            ]
        }
    ];

    const _bandLabels: Record<string, string> = { bass: 'Bass', mid: 'Mid', treble: 'Treble', custom: 'Custom' };

    const _getSectionForSource = (sourceType: string): string => {
        if (sourceType === 'multichannel') return 'audio-multi';
        if (sourceType === 'mono') return 'audio-mono';
        return 'audio-band-extract';
    };

    const _getTabForSource = (sourceType: string): string => {
        if (sourceType === 'multichannel') return 'audio_multi';
        if (sourceType === 'mono') return 'audio_mono';
        return 'audio_band_extract';
    };

    const renderAudioSourceCard = (src: any) => {
        const icon = getAudioSourceIcon(src.source_type);

        let propsHtml = '';
        if (src.source_type === 'mono') {
            const parent = _cachedAudioSources.find(s => s.id === src.audio_source_id);
            const parentName = parent ? parent.name : src.audio_source_id;
            const chLabel = src.channel === 'left' ? 'L' : src.channel === 'right' ? 'R' : 'M';
            const parentBadge = parent
                ? `<span class="stream-card-prop stream-card-link" title="${escapeHtml(t('audio_source.parent'))}" onclick="event.stopPropagation(); navigateToCard('streams','audio_multi','audio-multi','data-id','${src.audio_source_id}')">${ICON_AUDIO_LOOPBACK} ${escapeHtml(parentName)}</span>`
                : `<span class="stream-card-prop" title="${escapeHtml(t('audio_source.parent'))}">${ICON_AUDIO_LOOPBACK} ${escapeHtml(parentName)}</span>`;
            propsHtml = `
                ${parentBadge}
                <span class="stream-card-prop" title="${escapeHtml(t('audio_source.channel'))}">${ICON_RADIO} ${chLabel}</span>
            `;
        } else if (src.source_type === 'band_extract') {
            const parent = _cachedAudioSources.find(s => s.id === src.audio_source_id);
            const parentName = parent ? parent.name : src.audio_source_id;
            const parentSection = parent ? _getSectionForSource(parent.source_type) : 'audio-multi';
            const parentTab = parent ? _getTabForSource(parent.source_type) : 'audio_multi';
            const parentBadge = parent
                ? `<span class="stream-card-prop stream-card-link" title="${escapeHtml(t('audio_source.band_parent'))}" onclick="event.stopPropagation(); navigateToCard('streams','${parentTab}','${parentSection}','data-id','${src.audio_source_id}')">${getAudioSourceIcon(parent.source_type)} ${escapeHtml(parentName)}</span>`
                : `<span class="stream-card-prop" title="${escapeHtml(t('audio_source.band_parent'))}">${ICON_ACTIVITY} ${escapeHtml(parentName)}</span>`;
            const bandLabel = _bandLabels[src.band] || src.band;
            const freqRange = `${Math.round(src.freq_low)}–${Math.round(src.freq_high)} Hz`;
            propsHtml = `
                ${parentBadge}
                <span class="stream-card-prop" title="${escapeHtml(t('audio_source.band'))}">${ICON_ACTIVITY} ${bandLabel}</span>
                <span class="stream-card-prop" title="${escapeHtml(t('audio_source.freq_range'))}">${freqRange}</span>
            `;
        } else {
            const devIdx = src.device_index ?? -1;
            const loopback = src.is_loopback !== false;
            const devLabel = loopback ? `${ICON_AUDIO_LOOPBACK} Loopback` : `${ICON_AUDIO_INPUT} Input`;
            const tpl = src.audio_template_id ? _cachedAudioTemplates.find(t => t.id === src.audio_template_id) : null;
            const tplBadge = tpl ? `<span class="stream-card-prop stream-card-link" title="${escapeHtml(t('audio_source.audio_template'))}" onclick="event.stopPropagation(); navigateToCard('streams','audio_templates','audio-templates','data-audio-template-id','${src.audio_template_id}')">${ICON_AUDIO_TEMPLATE} ${escapeHtml(tpl.name)}</span>` : '';
            propsHtml = `<span class="stream-card-prop">${devLabel} #${devIdx}</span>${tplBadge}`;
        }

        return wrapCard({
            type: 'template-card',
            dataAttr: 'data-id',
            id: src.id,
            removeOnclick: `deleteAudioSource('${src.id}')`,
            removeTitle: t('common.delete'),
            content: `
                <div class="template-card-header">
                    <div class="template-name" title="${escapeHtml(src.name)}">${icon} ${escapeHtml(src.name)}</div>
                </div>
                <div class="stream-card-props">${propsHtml}</div>
                ${renderTagChips(src.tags)}
                ${src.description ? `<div class="template-config" style="opacity:0.7;">${escapeHtml(src.description)}</div>` : ''}`,
            actions: `
                    <button class="btn btn-icon btn-secondary" data-action="test-audio" title="${t('audio_source.test')}">${ICON_TEST}</button>
                    <button class="btn btn-icon btn-secondary" data-action="clone-audio" title="${t('common.clone')}">${ICON_CLONE}</button>
                    <button class="btn btn-icon btn-secondary" data-action="edit-audio" title="${t('common.edit')}">${ICON_EDIT}</button>`,
        });
    };

    const renderAudioTemplateCard = (template: any) => {
        const configEntries = Object.entries(template.engine_config || {});
        return wrapCard({
            type: 'template-card',
            dataAttr: 'data-audio-template-id',
            id: template.id,
            removeOnclick: `deleteAudioTemplate('${template.id}')`,
            removeTitle: t('common.delete'),
            content: `
                <div class="template-card-header">
                    <div class="template-name" title="${escapeHtml(template.name)}">${ICON_AUDIO_TEMPLATE} ${escapeHtml(template.name)}</div>
                </div>
                ${template.description ? `<div class="template-config" style="opacity:0.7;">${escapeHtml(template.description)}</div>` : ''}
                <div class="stream-card-props">
                    <span class="stream-card-prop" title="${t('audio_template.engine')}">${ICON_AUDIO_TEMPLATE} ${template.engine_type.toUpperCase()}</span>
                    ${configEntries.length > 0 ? `<span class="stream-card-prop" title="${t('audio_template.config.show')}">${ICON_WRENCH} ${configEntries.length}</span>` : ''}
                </div>
                ${renderTagChips(template.tags)}
                ${configEntries.length > 0 ? `
                    <div class="template-config-collapse">
                        <button type="button" class="template-config-toggle" onclick="this.parentElement.classList.toggle('open')">${t('audio_template.config.show')}</button>
                        <div class="template-config-animate">
                            <div class="template-config-inner">
                                <table class="config-table">
                                    ${configEntries.map(([key, val]) => `
                                        <tr>
                                            <td class="config-key">${escapeHtml(key)}</td>
                                            <td class="config-value">${escapeHtml(String(val))}</td>
                                        </tr>
                                    `).join('')}
                                </table>
                            </div>
                        </div>
                    </div>
                ` : ''}`,
            actions: `
                    <button class="btn btn-icon btn-secondary" onclick="showTestAudioTemplateModal('${template.id}')" title="${t('audio_template.test')}">${ICON_TEST}</button>
                    <button class="btn btn-icon btn-secondary" onclick="cloneAudioTemplate('${template.id}')" title="${t('common.clone')}">${ICON_CLONE}</button>
                    <button class="btn btn-icon btn-secondary" onclick="editAudioTemplate('${template.id}')" title="${t('common.edit')}">${ICON_EDIT}</button>`,
        });
    };

    // Gradient card renderer
    const renderGradientCard = (g: GradientEntity) => {
        const cssStops = g.stops.map(s => `rgb(${s.color.join(',')}) ${(s.position * 100).toFixed(0)}%`).join(', ');
        const stripPreview = `<div style="height:24px;border-radius:4px;background:linear-gradient(to right,${cssStops});margin-bottom:6px"></div>`;
        const lockBadge = g.is_builtin ? `<span class="badge badge-info" style="font-size:0.7em;margin-left:4px">${t('gradient.builtin')}</span>` : '';
        const cloneBtn = `<button class="btn btn-icon btn-secondary" onclick="cloneGradient('${g.id}')" title="${t('common.clone')}">${ICON_CLONE}</button>`;
        const editBtn = g.is_builtin ? '' : `<button class="btn btn-icon btn-secondary" onclick="editGradient('${g.id}')" title="${t('common.edit')}">${ICON_EDIT}</button>`;
        return wrapCard({
            type: 'template-card',
            dataAttr: 'data-id',
            id: g.id,
            removeOnclick: g.is_builtin ? '' : `deleteGradient('${g.id}')`,
            removeTitle: t('common.delete'),
            content: `
                <div class="template-card-header">
                    <div class="template-name">${ICON_PALETTE} ${escapeHtml(g.name)}${lockBadge}</div>
                </div>
                ${stripPreview}
                <div class="stream-card-props">
                    <span class="stream-card-prop">${g.stops.length} ${t('gradient.stops_label')}</span>
                </div>`,
            actions: `${cloneBtn}${editBtn}`,
        });
    };

    // Build item arrays for all sections
    const rawStreamItems = csRawStreams.applySortOrder(rawStreams.map(s => ({ key: s.id, html: renderStreamCard(s) })));
    const rawTemplateItems = csRawTemplates.applySortOrder(_cachedCaptureTemplates.map(t => ({ key: t.id, html: renderCaptureTemplateCard(t) })));
    const procStreamItems = csProcStreams.applySortOrder(processedStreams.map(s => ({ key: s.id, html: renderStreamCard(s) })));
    const procTemplateItems = csProcTemplates.applySortOrder(_cachedPPTemplates.map(t => ({ key: t.id, html: renderPPTemplateCard(t) })));
    const multiItems = csAudioMulti.applySortOrder(multichannelSources.map(s => ({ key: s.id, html: renderAudioSourceCard(s) })));
    const monoItems = csAudioMono.applySortOrder(monoSources.map(s => ({ key: s.id, html: renderAudioSourceCard(s) })));
    const bandExtractItems = csAudioBandExtract.applySortOrder(bandExtractSources.map(s => ({ key: s.id, html: renderAudioSourceCard(s) })));
    const audioTemplateItems = csAudioTemplates.applySortOrder(_cachedAudioTemplates.map(t => ({ key: t.id, html: renderAudioTemplateCard(t) })));
    const staticItems = csStaticStreams.applySortOrder(staticImageStreams.map(s => ({ key: s.id, html: renderStreamCard(s) })));
    const videoItems = csVideoStreams.applySortOrder(videoStreams.map(s => ({ key: s.id, html: renderStreamCard(s) })));
    const colorStripItems = csColorStrips.applySortOrder(colorStrips.map(s => ({ key: s.id, html: createColorStripCard(s, pictureSourceMap, audioSourceMap) })));
    const gradientItems = csGradients.applySortOrder(gradients.map(g => ({ key: g.id, html: renderGradientCard(g) })));
    const valueItems = csValueSources.applySortOrder(_cachedValueSources.map(s => ({ key: s.id, html: createValueSourceCard(s) })));
    const syncClockItems = csSyncClocks.applySortOrder(_cachedSyncClocks.map(s => ({ key: s.id, html: createSyncClockCard(s) })));
    const weatherSourceItems = csWeatherSources.applySortOrder(_cachedWeatherSources.map(s => ({ key: s.id, html: createWeatherSourceCard(s) })));
    const haSourceItems = csHASources.applySortOrder(_cachedHASources.map(s => ({ key: s.id, html: createHASourceCard(s) })));
    const assetItems = csAssets.applySortOrder(_cachedAssets.map(a => ({ key: a.id, html: createAssetCard(a) })));
    const csptItems = csCSPTemplates.applySortOrder(csptTemplates.map(t => ({ key: t.id, html: renderCSPTCard(t) })));

    if (csRawStreams.isMounted()) {
        // Incremental update: reconcile cards in-place
        _streamsTree.updateCounts({
            raw: rawStreams.length,
            raw_templates: _cachedCaptureTemplates.length,
            static_image: staticImageStreams.length,
            video: videoStreams.length,
            processed: processedStreams.length,
            proc_templates: _cachedPPTemplates.length,
            css_processing: csptTemplates.length,
            color_strip: colorStrips.length,
            gradients: gradients.length,
            audio: _cachedAudioSources.length,
            audio_templates: _cachedAudioTemplates.length,
            value: _cachedValueSources.length,
            sync: _cachedSyncClocks.length,
            weather: _cachedWeatherSources.length,
            home_assistant: _cachedHASources.length,
            assets: _cachedAssets.length,
        });
        csRawStreams.reconcile(rawStreamItems);
        csRawTemplates.reconcile(rawTemplateItems);
        csProcStreams.reconcile(procStreamItems);
        csProcTemplates.reconcile(procTemplateItems);
        csCSPTemplates.reconcile(csptItems);
        csColorStrips.reconcile(colorStripItems);
        csGradients.reconcile(gradientItems);
        csAudioMulti.reconcile(multiItems);
        csAudioMono.reconcile(monoItems);
        csAudioBandExtract.reconcile(bandExtractItems);
        csAudioTemplates.reconcile(audioTemplateItems);
        csStaticStreams.reconcile(staticItems);
        csVideoStreams.reconcile(videoItems);
        csValueSources.reconcile(valueItems);
        csSyncClocks.reconcile(syncClockItems);
        csWeatherSources.reconcile(weatherSourceItems);
        csHASources.reconcile(haSourceItems);
        csAssets.reconcile(assetItems);
    } else {
        // First render: build full HTML
        const panels = tabs.map(tab => {
            let panelContent = '';
            if (tab.key === 'raw') panelContent = csRawStreams.render(rawStreamItems);
            else if (tab.key === 'raw_templates') panelContent = csRawTemplates.render(rawTemplateItems);
            else if (tab.key === 'processed') panelContent = csProcStreams.render(procStreamItems);
            else if (tab.key === 'proc_templates') panelContent = csProcTemplates.render(procTemplateItems);
            else if (tab.key === 'css_processing') panelContent = csCSPTemplates.render(csptItems);
            else if (tab.key === 'color_strip') panelContent = csColorStrips.render(colorStripItems);
            else if (tab.key === 'gradients') panelContent = csGradients.render(gradientItems);
            else if (tab.key === 'audio_multi') panelContent = csAudioMulti.render(multiItems);
            else if (tab.key === 'audio_mono') panelContent = csAudioMono.render(monoItems);
            else if (tab.key === 'audio_band_extract') panelContent = csAudioBandExtract.render(bandExtractItems);
            else if (tab.key === 'audio_templates') panelContent = csAudioTemplates.render(audioTemplateItems);
            else if (tab.key === 'value') panelContent = csValueSources.render(valueItems);
            else if (tab.key === 'sync') panelContent = csSyncClocks.render(syncClockItems);
            else if (tab.key === 'weather') panelContent = csWeatherSources.render(weatherSourceItems);
            else if (tab.key === 'home_assistant') panelContent = csHASources.render(haSourceItems);
            else if (tab.key === 'assets') panelContent = csAssets.render(assetItems);
            else if (tab.key === 'video') panelContent = csVideoStreams.render(videoItems);
            else panelContent = csStaticStreams.render(staticItems);
            return `<div class="stream-tab-panel${tab.key === activeTab ? ' active' : ''}" id="stream-tab-${tab.key}">${panelContent}</div>`;
        }).join('');

        container.innerHTML = panels;
        CardSection.bindAll([csRawStreams, csRawTemplates, csProcStreams, csProcTemplates, csCSPTemplates, csColorStrips, csGradients, csAudioMulti, csAudioMono, csAudioBandExtract, csAudioTemplates, csStaticStreams, csVideoStreams, csValueSources, csSyncClocks, csWeatherSources, csHASources, csAssets]);

        // Event delegation for card actions (replaces inline onclick handlers)
        initSyncClockDelegation(container);
        initWeatherSourceDelegation(container);
        initHASourceDelegation(container);
        initAudioSourceDelegation(container);
        initAssetDelegation(container);

        // Render tree sidebar with expand/collapse buttons
        _streamsTree.setExtraHtml(`<button class="tutorial-trigger-btn" onclick="startSourcesTutorial()" data-i18n-title="tour.restart" title="${t('tour.restart')}">${ICON_HELP}</button>`);
        _streamsTree.update(treeGroups, activeTab);
        _streamsTree.observeSections('streams-list', {
            'raw-streams': 'raw', 'raw-templates': 'raw_templates',
            'static-streams': 'static_image',
            'video-streams': 'video',
            'proc-streams': 'processed', 'proc-templates': 'proc_templates',
            'css-proc-templates': 'css_processing',
            'color-strips': 'color_strip',
            'gradients': 'gradients',
            'audio-multi': 'audio_multi', 'audio-mono': 'audio_mono', 'audio-band-extract': 'audio_band_extract',
            'audio-templates': 'audio_templates',
            'value-sources': 'value',
            'sync-clocks': 'sync',
            'weather-sources': 'weather',
            'ha-sources': 'home_assistant',
            'assets': 'assets',
        });
    }
}

export function onStreamTypeChange() {
    const streamType = (document.getElementById('stream-type') as HTMLSelectElement).value;
    document.getElementById('stream-raw-fields')!.style.display = streamType === 'raw' ? '' : 'none';
    document.getElementById('stream-processed-fields')!.style.display = streamType === 'processed' ? '' : 'none';
    document.getElementById('stream-static-image-fields')!.style.display = streamType === 'static_image' ? '' : 'none';
    document.getElementById('stream-video-fields')!.style.display = streamType === 'video' ? '' : 'none';
}

export function onStreamDisplaySelected(displayIndex: any, display: any) {
    (document.getElementById('stream-display-index') as HTMLInputElement).value = displayIndex;
    const engineType = (document.getElementById('stream-capture-template') as HTMLSelectElement).selectedOptions[0]?.dataset?.engineType || null;
    document.getElementById('stream-display-picker-label')!.textContent = formatDisplayLabel(displayIndex, display, engineType);
    _autoGenerateStreamName();
}

export function onTestDisplaySelected(displayIndex: any, display: any) {
    (document.getElementById('test-template-display') as HTMLInputElement).value = displayIndex;
    const engineType = currentTestingTemplate?.engine_type || null;
    document.getElementById('test-display-picker-label')!.textContent = formatDisplayLabel(displayIndex, display, engineType);
}

function _autoGenerateStreamName() {
    if (_streamNameManuallyEdited) return;
    if ((document.getElementById('stream-id') as HTMLInputElement).value) return;
    const streamType = (document.getElementById('stream-type') as HTMLSelectElement).value;
    const nameInput = document.getElementById('stream-name') as HTMLInputElement;

    if (streamType === 'raw') {
        const displayIndex = (document.getElementById('stream-display-index') as HTMLInputElement).value;
        const templateSelect = document.getElementById('stream-capture-template') as HTMLSelectElement;
        const templateName = templateSelect.selectedOptions[0]?.dataset?.name || '';
        if (displayIndex === '' || !templateName) return;
        nameInput.value = `D${displayIndex}_${templateName}`;
    } else if (streamType === 'processed') {
        const sourceSelect = document.getElementById('stream-source') as HTMLSelectElement;
        const sourceName = sourceSelect.selectedOptions[0]?.dataset?.name || '';
        const ppTemplateId = (document.getElementById('stream-pp-template') as HTMLSelectElement).value;
        const ppTemplate = _streamModalPPTemplates.find((t: any) => t.id === ppTemplateId);
        if (!sourceName) return;
        if (ppTemplate && ppTemplate.name) {
            nameInput.value = `${sourceName} (${ppTemplate.name})`;
        } else {
            nameInput.value = sourceName;
        }
    }
}

export async function showAddStreamModal(presetType: any, cloneData: any = null) {
    const streamType = (cloneData && cloneData.stream_type) || presetType || 'raw';
    const titleKeys: any = { raw: 'streams.add.raw', processed: 'streams.add.processed', static_image: 'streams.add.static_image', video: 'streams.add.video' };
    document.getElementById('stream-modal-title')!.innerHTML = `${getPictureSourceIcon(streamType)} ${t(titleKeys[streamType] || 'streams.add')}`;
    (document.getElementById('stream-form') as HTMLFormElement).reset();
    (document.getElementById('stream-id') as HTMLInputElement).value = '';
    (document.getElementById('stream-display-index') as HTMLInputElement).value = '';
    document.getElementById('stream-display-picker-label')!.textContent = t('displays.picker.select');
    document.getElementById('stream-error')!.style.display = 'none';
    (document.getElementById('stream-type') as HTMLSelectElement).value = streamType;
    _ensureImageAssetEntitySelect();
    _ensureVideoAssetEntitySelect();
    onStreamTypeChange();

    set_streamNameManuallyEdited(!!cloneData);
    (document.getElementById('stream-name') as HTMLInputElement).oninput = () => { set_streamNameManuallyEdited(true); };
    (document.getElementById('stream-capture-template') as HTMLSelectElement).onchange = () => _autoGenerateStreamName();
    (document.getElementById('stream-source') as HTMLSelectElement).onchange = () => _autoGenerateStreamName();
    (document.getElementById('stream-pp-template') as HTMLSelectElement).onchange = () => _autoGenerateStreamName();

    // Open modal instantly with loading indicator
    _showStreamModalLoading(true);
    streamModal.open();

    await populateStreamModalDropdowns();

    // Pre-fill from clone data after dropdowns are populated
    if (cloneData) {
        (document.getElementById('stream-name') as HTMLInputElement).value = (cloneData.name || '') + ' (Copy)';
        (document.getElementById('stream-description') as HTMLInputElement).value = cloneData.description || '';
        if (streamType === 'raw') {
            (document.getElementById('stream-capture-template') as HTMLSelectElement).value = cloneData.capture_template_id || '';
            await _onCaptureTemplateChanged();
            const displayIdx = cloneData.display_index ?? 0;
            const display = _cachedDisplays ? _cachedDisplays.find((d: any) => d.index === displayIdx) : null;
            onStreamDisplaySelected(displayIdx, display);
            const fps = cloneData.target_fps ?? 30;
            (document.getElementById('stream-target-fps') as HTMLInputElement).value = fps;
            document.getElementById('stream-target-fps-value')!.textContent = fps;
        } else if (streamType === 'processed') {
            (document.getElementById('stream-source') as HTMLSelectElement).value = cloneData.source_stream_id || '';
            (document.getElementById('stream-pp-template') as HTMLSelectElement).value = cloneData.postprocessing_template_id || '';
        } else if (streamType === 'static_image') {
            if (cloneData.image_asset_id) {
                (document.getElementById('stream-image-asset') as HTMLSelectElement).value = cloneData.image_asset_id;
                if (_imageAssetEntitySelect) _imageAssetEntitySelect.setValue(cloneData.image_asset_id);
            }
        } else if (streamType === 'video') {
            if (cloneData.video_asset_id) {
                (document.getElementById('stream-video-asset') as HTMLSelectElement).value = cloneData.video_asset_id;
                if (_videoAssetEntitySelect) _videoAssetEntitySelect.setValue(cloneData.video_asset_id);
            }
            (document.getElementById('stream-video-loop') as HTMLInputElement).checked = cloneData.loop !== false;
            (document.getElementById('stream-video-speed') as HTMLInputElement).value = cloneData.playback_speed || 1.0;
            const cloneSpeedLabel = document.getElementById('stream-video-speed-value');
            if (cloneSpeedLabel) cloneSpeedLabel.textContent = cloneData.playback_speed || 1.0;
            (document.getElementById('stream-video-fps') as HTMLInputElement).value = cloneData.target_fps || 30;
            (document.getElementById('stream-video-start') as HTMLInputElement).value = cloneData.start_time || '';
            (document.getElementById('stream-video-end') as HTMLInputElement).value = cloneData.end_time || '';
            (document.getElementById('stream-video-resolution') as HTMLInputElement).value = cloneData.resolution_limit || '';
        }
    }

    _showStreamModalLoading(false);

    // Tags
    if (_streamTagsInput) { _streamTagsInput.destroy(); _streamTagsInput = null; }
    _streamTagsInput = new TagInput(document.getElementById('stream-tags-container'), { placeholder: t('tags.placeholder') });
    _streamTagsInput.setValue(cloneData ? (cloneData.tags || []) : []);

    streamModal.snapshot();
}

export async function editStream(streamId: any) {
    try {
        // Open modal instantly with loading indicator
        document.getElementById('stream-modal-title')!.innerHTML = t('streams.edit');
        (document.getElementById('stream-form') as HTMLFormElement).reset();
        document.getElementById('stream-error')!.style.display = 'none';
        _showStreamModalLoading(true);
        streamModal.open();

        const response = await fetchWithAuth(`/picture-sources/${streamId}`);
        if (!response.ok) throw new Error(`Failed to load stream: ${response.status}`);
        const stream = await response.json();

        const editTitleKeys: any = { raw: 'streams.edit.raw', processed: 'streams.edit.processed', static_image: 'streams.edit.static_image', video: 'streams.edit.video' };
        document.getElementById('stream-modal-title')!.innerHTML = `${getPictureSourceIcon(stream.stream_type)} ${t(editTitleKeys[stream.stream_type] || 'streams.edit')}`;
        (document.getElementById('stream-id') as HTMLInputElement).value = streamId;
        (document.getElementById('stream-name') as HTMLInputElement).value = stream.name;
        (document.getElementById('stream-description') as HTMLInputElement).value = stream.description || '';

        (document.getElementById('stream-type') as HTMLSelectElement).value = stream.stream_type;
        _ensureImageAssetEntitySelect();
        _ensureVideoAssetEntitySelect();
        onStreamTypeChange();

        await populateStreamModalDropdowns();

        if (stream.stream_type === 'raw') {
            (document.getElementById('stream-capture-template') as HTMLSelectElement).value = stream.capture_template_id || '';
            // Ensure correct engine displays are loaded for this template
            await _onCaptureTemplateChanged();
            const displayIdx = stream.display_index ?? 0;
            const display = _cachedDisplays ? _cachedDisplays.find((d: any) => d.index === displayIdx) : null;
            onStreamDisplaySelected(displayIdx, display);
            const fps = stream.target_fps ?? 30;
            (document.getElementById('stream-target-fps') as HTMLInputElement).value = fps;
            document.getElementById('stream-target-fps-value')!.textContent = fps;
        } else if (stream.stream_type === 'processed') {
            (document.getElementById('stream-source') as HTMLSelectElement).value = stream.source_stream_id || '';
            (document.getElementById('stream-pp-template') as HTMLSelectElement).value = stream.postprocessing_template_id || '';
        } else if (stream.stream_type === 'static_image') {
            if (stream.image_asset_id) {
                (document.getElementById('stream-image-asset') as HTMLSelectElement).value = stream.image_asset_id;
                if (_imageAssetEntitySelect) _imageAssetEntitySelect.setValue(stream.image_asset_id);
            }
        } else if (stream.stream_type === 'video') {
            if (stream.video_asset_id) {
                (document.getElementById('stream-video-asset') as HTMLSelectElement).value = stream.video_asset_id;
                if (_videoAssetEntitySelect) _videoAssetEntitySelect.setValue(stream.video_asset_id);
            }
            (document.getElementById('stream-video-loop') as HTMLInputElement).checked = stream.loop !== false;
            (document.getElementById('stream-video-speed') as HTMLInputElement).value = stream.playback_speed || 1.0;
            const speedLabel = document.getElementById('stream-video-speed-value');
            if (speedLabel) speedLabel.textContent = stream.playback_speed || 1.0;
            (document.getElementById('stream-video-fps') as HTMLInputElement).value = stream.target_fps || 30;
            (document.getElementById('stream-video-start') as HTMLInputElement).value = stream.start_time || '';
            (document.getElementById('stream-video-end') as HTMLInputElement).value = stream.end_time || '';
            (document.getElementById('stream-video-resolution') as HTMLInputElement).value = stream.resolution_limit || '';
        }

        _showStreamModalLoading(false);

        // Tags
        if (_streamTagsInput) { _streamTagsInput.destroy(); _streamTagsInput = null; }
        _streamTagsInput = new TagInput(document.getElementById('stream-tags-container'), { placeholder: t('tags.placeholder') });
        _streamTagsInput.setValue(stream.tags || []);

        streamModal.snapshot();
    } catch (error) {
        console.error('Error loading stream:', error);
        streamModal.forceClose();
        showToast(t('streams.error.load') + ': ' + error.message, 'error');
    }
}

/** Track which engine type the stream-modal displays were loaded for. */
let _streamModalDisplaysEngine: string | null = null;

// ── EntitySelect instances for stream modal ──
let _captureTemplateEntitySelect: EntitySelect | null = null;
let _sourceEntitySelect: EntitySelect | null = null;
let _ppTemplateEntitySelect: EntitySelect | null = null;

async function populateStreamModalDropdowns() {
    const [captureTemplates, streams, ppTemplates] = await Promise.all([
        captureTemplatesCache.fetch().catch((): any[] => []),
        streamsCache.fetch().catch((): any[] => []),
        ppTemplatesCache.fetch().catch((): any[] => []),
        displaysCache.fetch().catch((): any[] => []),
    ]);
    _streamModalDisplaysEngine = null;

    const templateSelect = document.getElementById('stream-capture-template') as HTMLSelectElement;
    templateSelect.innerHTML = '';
    captureTemplates.forEach((tmpl: any) => {
        const opt = document.createElement('option');
        opt.value = tmpl.id;
        opt.dataset.name = tmpl.name;
        opt.dataset.engineType = tmpl.engine_type;
        opt.dataset.hasOwnDisplays = availableEngines.find((e: any) => e.type === tmpl.engine_type)?.has_own_displays ? '1' : '';
        opt.textContent = `${tmpl.name} (${tmpl.engine_type})`;
        templateSelect.appendChild(opt);
    });

    // When template changes, refresh displays if engine type switched
    templateSelect.addEventListener('change', _onCaptureTemplateChanged);

    // Load displays for the selected engine (engine-specific or desktop)
    const firstOpt = templateSelect.selectedOptions[0];
    if (firstOpt?.dataset?.hasOwnDisplays === '1') {
        await _refreshStreamDisplaysForEngine(firstOpt.dataset.engineType);
    } else if (!(document.getElementById('stream-display-index') as HTMLInputElement).value && _cachedDisplays && _cachedDisplays.length > 0) {
        const primary = _cachedDisplays.find(d => d.is_primary) || _cachedDisplays[0];
        onStreamDisplaySelected(primary.index, primary);
    }

    const sourceSelect = document.getElementById('stream-source') as HTMLSelectElement;
    sourceSelect.innerHTML = '';
    const editingId = (document.getElementById('stream-id') as HTMLInputElement).value;
    streams.forEach(s => {
        if (s.id === editingId) return;
        const opt = document.createElement('option');
        opt.value = s.id;
        opt.dataset.name = s.name;
        opt.textContent = s.name;
        sourceSelect.appendChild(opt);
    });

    set_streamModalPPTemplates(ppTemplates);
    const ppSelect = document.getElementById('stream-pp-template') as HTMLSelectElement;
    ppSelect.innerHTML = '';
    ppTemplates.forEach(tmpl => {
        const opt = document.createElement('option');
        opt.value = tmpl.id;
        opt.textContent = tmpl.name;
        ppSelect.appendChild(opt);
    });

    // Entity palette selectors
    if (_captureTemplateEntitySelect) _captureTemplateEntitySelect.destroy();
    _captureTemplateEntitySelect = new EntitySelect({
        target: templateSelect,
        getItems: () => captureTemplates.map((tmpl: any) => ({
            value: tmpl.id,
            label: tmpl.name,
            icon: getEngineIcon(tmpl.engine_type),
            desc: tmpl.engine_type,
        })),
        placeholder: t('palette.search'),
    });

    if (_sourceEntitySelect) _sourceEntitySelect.destroy();
    _sourceEntitySelect = new EntitySelect({
        target: sourceSelect,
        getItems: () => {
            const editingId = (document.getElementById('stream-id') as HTMLInputElement).value;
            return streams.filter(s => s.id !== editingId).map(s => ({
                value: s.id,
                label: s.name,
                icon: getPictureSourceIcon(s.stream_type),
            }));
        },
        placeholder: t('palette.search'),
    });

    if (_ppTemplateEntitySelect) _ppTemplateEntitySelect.destroy();
    _ppTemplateEntitySelect = new EntitySelect({
        target: ppSelect,
        getItems: () => ppTemplates.map((tmpl: any) => ({
            value: tmpl.id,
            label: tmpl.name,
            icon: ICON_PP_TEMPLATE,
        })),
        placeholder: t('palette.search'),
    });

    _autoGenerateStreamName();
}

async function _onCaptureTemplateChanged() {
    const templateSelect = document.getElementById('stream-capture-template') as HTMLSelectElement;
    const engineType = templateSelect.selectedOptions[0]?.dataset?.engineType || null;
    const hasOwnDisplays = templateSelect.selectedOptions[0]?.dataset?.hasOwnDisplays === '1';
    const currentEngine = hasOwnDisplays ? engineType : null;

    // Only refetch if the engine category actually changed
    if (currentEngine !== _streamModalDisplaysEngine) {
        await _refreshStreamDisplaysForEngine(currentEngine);
    }
    _autoGenerateStreamName();
}

async function _refreshStreamDisplaysForEngine(engineType: any) {
    _streamModalDisplaysEngine = engineType;
    const url = engineType ? `/config/displays?engine_type=${engineType}` : '/config/displays';
    try {
        const resp = await fetchWithAuth(url);
        if (resp.ok) {
            const data = await resp.json();
            displaysCache.update(data.displays || []);
        }
    } catch (error) {
        console.error('Error refreshing displays for engine:', error);
    }

    // Reset display selection and pick the first available
    (document.getElementById('stream-display-index') as HTMLInputElement).value = '';
    document.getElementById('stream-display-picker-label')!.textContent = t('displays.picker.select');
    if (_cachedDisplays && _cachedDisplays.length > 0) {
        const primary = _cachedDisplays.find(d => d.is_primary) || _cachedDisplays[0];
        onStreamDisplaySelected(primary.index, primary);
    }
}

export async function saveStream() {
    const streamId = (document.getElementById('stream-id') as HTMLInputElement).value;
    const name = (document.getElementById('stream-name') as HTMLInputElement).value.trim();
    const streamType = (document.getElementById('stream-type') as HTMLSelectElement).value;
    const description = (document.getElementById('stream-description') as HTMLInputElement).value.trim();
    const errorEl = document.getElementById('stream-error')!;

    if (!name) { showToast(t('streams.error.required'), 'error'); return; }

    const payload: any = { name, description: description || null, tags: _streamTagsInput ? _streamTagsInput.getValue() : [] };
    if (!streamId) payload.stream_type = streamType;

    if (streamType === 'raw') {
        payload.display_index = parseInt((document.getElementById('stream-display-index') as HTMLInputElement).value) || 0;
        payload.capture_template_id = (document.getElementById('stream-capture-template') as HTMLSelectElement).value;
        payload.target_fps = parseInt((document.getElementById('stream-target-fps') as HTMLInputElement).value) || 30;
    } else if (streamType === 'processed') {
        payload.source_stream_id = (document.getElementById('stream-source') as HTMLSelectElement).value;
        payload.postprocessing_template_id = (document.getElementById('stream-pp-template') as HTMLSelectElement).value;
    } else if (streamType === 'static_image') {
        const imageAssetId = (document.getElementById('stream-image-asset') as HTMLSelectElement).value;
        if (!imageAssetId) { showToast(t('streams.error.required'), 'error'); return; }
        payload.image_asset_id = imageAssetId;
    } else if (streamType === 'video') {
        const videoAssetId = (document.getElementById('stream-video-asset') as HTMLSelectElement).value;
        if (!videoAssetId) { showToast(t('streams.error.required'), 'error'); return; }
        payload.video_asset_id = videoAssetId;
        payload.loop = (document.getElementById('stream-video-loop') as HTMLInputElement).checked;
        payload.playback_speed = parseFloat((document.getElementById('stream-video-speed') as HTMLInputElement).value) || 1.0;
        payload.target_fps = parseInt((document.getElementById('stream-video-fps') as HTMLInputElement).value) || 30;
        const startTime = parseFloat((document.getElementById('stream-video-start') as HTMLInputElement).value);
        if (!isNaN(startTime) && startTime > 0) payload.start_time = startTime;
        const endTime = parseFloat((document.getElementById('stream-video-end') as HTMLInputElement).value);
        if (!isNaN(endTime) && endTime > 0) payload.end_time = endTime;
        const resLimit = parseInt((document.getElementById('stream-video-resolution') as HTMLInputElement).value);
        if (!isNaN(resLimit) && resLimit > 0) payload.resolution_limit = resLimit;
    }

    try {
        let response;
        if (streamId) {
            response = await fetchWithAuth(`/picture-sources/${streamId}`, { method: 'PUT', body: JSON.stringify(payload) });
        } else {
            response = await fetchWithAuth('/picture-sources', { method: 'POST', body: JSON.stringify(payload) });
        }

        if (!response.ok) {
            const error = await response.json();
            throw new Error(error.detail || error.message || 'Failed to save stream');
        }

        showToast(streamId ? t('streams.updated') : t('streams.created'), 'success');
        streamModal.forceClose();
        streamsCache.invalidate();
        await loadPictureSources();
    } catch (error) {
        console.error('Error saving stream:', error);
        errorEl.textContent = error.message;
        errorEl.style.display = 'block';
    }
}

export async function deleteStream(streamId: any) {
    const confirmed = await showConfirm(t('streams.delete.confirm'));
    if (!confirmed) return;

    try {
        const response = await fetchWithAuth(`/picture-sources/${streamId}`, { method: 'DELETE' });
        if (!response.ok) {
            const error = await response.json();
            throw new Error(error.detail || error.message || 'Failed to delete stream');
        }
        showToast(t('streams.deleted'), 'success');
        streamsCache.invalidate();
        await loadPictureSources();
    } catch (error) {
        console.error('Error deleting stream:', error);
        showToast(t('streams.error.delete') + ': ' + error.message, 'error');
    }
}

/** Toggle loading overlay in stream modal — hides form while data loads. */
function _showStreamModalLoading(show: boolean) {
    const loading = document.getElementById('stream-modal-loading');
    const form = document.getElementById('stream-form');
    const footer = document.querySelector('#stream-modal .modal-footer') as HTMLElement | null;
    if (loading) loading.style.display = show ? '' : 'none';
    if (form) form.style.display = show ? 'none' : '';
    if (footer) footer.style.visibility = show ? 'hidden' : '';
}

export async function closeStreamModal() {
    await streamModal.close();
}

// ===== Picture Source Test =====

export async function showTestStreamModal(streamId: any) {
    set_currentTestStreamId(streamId);
    restoreStreamTestDuration();

    testStreamModal.open();
    setupBackdropClose((testStreamModal as any).el, () => closeTestStreamModal());
}

export function closeTestStreamModal() {
    testStreamModal.forceClose();
    set_currentTestStreamId(null);
}

export function updateStreamTestDuration(value: any) {
    document.getElementById('test-stream-duration-value')!.textContent = value;
    localStorage.setItem('lastStreamTestDuration', value);
}

function restoreStreamTestDuration() {
    const saved = localStorage.getItem('lastStreamTestDuration') || '5';
    (document.getElementById('test-stream-duration') as HTMLInputElement).value = saved;
    document.getElementById('test-stream-duration-value')!.textContent = saved;
}

export function runStreamTest() {
    if (!_currentTestStreamId) return;
    const captureDuration = parseFloat((document.getElementById('test-stream-duration') as HTMLInputElement).value);

    _localRunTestViaWS(
        `/picture-sources/${_currentTestStreamId}/test/ws`,
        { duration: captureDuration },
        null,
        captureDuration,
    );
}

// ===== PP Template Test =====

export async function showTestPPTemplateModal(templateId: any) {
    set_currentTestPPTemplateId(templateId);
    restorePPTestDuration();

    const select = document.getElementById('test-pp-source-stream') as HTMLSelectElement;
    select.innerHTML = '';
    if (_cachedStreams.length === 0) {
        try {
            await streamsCache.fetch();
        } catch (e) { console.warn('Could not load streams for PP test:', e); }
    }
    for (const s of _cachedStreams) {
        const opt = document.createElement('option');
        opt.value = s.id;
        opt.textContent = s.name;
        select.appendChild(opt);
    }
    const lastStream = localStorage.getItem('lastPPTestStreamId');
    if (lastStream && _cachedStreams.find(s => s.id === lastStream)) {
        select.value = lastStream;
    }

    // EntitySelect for source stream picker
    if (_ppTestSourceEntitySelect) _ppTestSourceEntitySelect.destroy();
    _ppTestSourceEntitySelect = new EntitySelect({
        target: select,
        getItems: () => _cachedStreams.map((s: any) => ({
            value: s.id,
            label: s.name,
            icon: getPictureSourceIcon(s.stream_type),
        })),
        placeholder: t('palette.search'),
    });

    testPPTemplateModal.open();
    setupBackdropClose((testPPTemplateModal as any).el, () => closeTestPPTemplateModal());
}

export function closeTestPPTemplateModal() {
    testPPTemplateModal.forceClose();
    set_currentTestPPTemplateId(null);
}

export function updatePPTestDuration(value: any) {
    document.getElementById('test-pp-duration-value')!.textContent = value;
    localStorage.setItem('lastPPTestDuration', value);
}

function restorePPTestDuration() {
    const saved = localStorage.getItem('lastPPTestDuration') || '5';
    (document.getElementById('test-pp-duration') as HTMLInputElement).value = saved;
    document.getElementById('test-pp-duration-value')!.textContent = saved;
}

export function runPPTemplateTest() {
    if (!_currentTestPPTemplateId) return;
    const sourceStreamId = (document.getElementById('test-pp-source-stream') as HTMLSelectElement).value;
    if (!sourceStreamId) { showToast(t('postprocessing.test.error.no_stream'), 'error'); return; }
    localStorage.setItem('lastPPTestStreamId', sourceStreamId);

    const captureDuration = parseFloat((document.getElementById('test-pp-duration') as HTMLInputElement).value);

    _localRunTestViaWS(
        `/postprocessing-templates/${_currentTestPPTemplateId}/test/ws`,
        { duration: captureDuration, source_stream_id: sourceStreamId },
        null,
        captureDuration,
    );
}

// ===== PP Templates =====

async function loadAvailableFilters() {
    await filtersCache.fetch();
}

async function loadPPTemplates() {
    try {
        if (_availableFilters.length === 0) await filtersCache.fetch();
        await ppTemplatesCache.fetch();
        renderPictureSourcesList(_cachedStreams);
    } catch (error) {
        console.error('Error loading PP templates:', error);
    }
}

function _getFilterName(filterId) {
    const key = 'filters.' + filterId;
    const translated = t(key);
    if (translated === key) {
        const def = _availableFilters.find(f => f.id === filterId);
        return def ? def.name : filterId;
    }
    return translated;
}

function _getStripFilterName(filterId) {
    const key = 'filters.' + filterId;
    const translated = t(key);
    if (translated === key) {
        const def = _stripFilters.find(f => f.id === filterId);
        return def ? def.name : filterId;
    }
    return translated;
}

// ── PP FilterListManager instance ──
const ppFilterManager = new FilterListManager({
    getFilters: () => _modalFilters,
    getFilterDefs: () => _availableFilters,
    getFilterName: _getFilterName,
    selectId: 'pp-add-filter-select',
    containerId: 'pp-filter-list',
    prefix: '',
    editingIdInputId: 'pp-template-id',
    selfRefFilterId: 'filter_template',
    autoNameFn: () => _autoGeneratePPTemplateName(),
    initDrag: _initFilterDragForContainer,
    initPaletteGrids: _initFilterPaletteGrids,
});

// _renderFilterListGeneric has been replaced by FilterListManager.render()

/** Stored IconSelect instances for filter option selects (keyed by select element id). */
const _filterOptionIconSelects = {};

function _paletteSwatchHTML(hexStr) {
    const hexColors = hexStr.split(',').map(s => s.trim());
    if (hexColors.length === 1) {
        return `<span style="display:inline-block;width:60px;height:14px;border-radius:3px;background:${hexColors[0]}"></span>`;
    }
    const stops = hexColors.map((c, i) => `${c} ${(i / (hexColors.length - 1) * 100).toFixed(0)}%`).join(', ');
    return `<span style="display:inline-block;width:60px;height:14px;border-radius:3px;background:linear-gradient(to right,${stops})"></span>`;
}

function _initFilterPaletteGrids(container: any) {
    // Palette-colored grids (e.g. palette quantization preset)
    container.querySelectorAll('select[data-palette-grid]').forEach((sel: any) => {
        if (_filterOptionIconSelects[sel.id]) _filterOptionIconSelects[sel.id].destroy();
        try {
            const choices = JSON.parse(sel.dataset.paletteGrid);
            const items = choices.map(c => ({
                value: c.value,
                label: c.label,
                icon: _paletteSwatchHTML(c.colors || ''),
            }));
            _filterOptionIconSelects[sel.id] = new IconSelect({ target: sel, items, columns: 2 });
        } catch { /* ignore parse errors */ }
    });
    // Template reference selects → EntitySelect (searchable palette)
    container.querySelectorAll('select[data-entity-select]').forEach((sel: any) => {
        if (_filterOptionIconSelects[sel.id]) _filterOptionIconSelects[sel.id].destroy();
        const icon = sel.dataset.entitySelect === 'template' ? ICON_PP_TEMPLATE : ICON_CSPT;
        _filterOptionIconSelects[sel.id] = new EntitySelect({
            target: sel,
            getItems: () => Array.from(sel.options).map((opt: any) => ({
                value: opt.value,
                label: opt.textContent,
                icon,
            })),
            placeholder: t('palette.search'),
        });
    });
}

export function renderModalFilterList() {
    ppFilterManager.render();
}

export function renderCSPTModalFilterList() {
    csptFilterManager.render();
}

/* ── Generic filter drag-and-drop reordering ── */

const _FILTER_DRAG_THRESHOLD = 5;
const _FILTER_SCROLL_EDGE = 60;
const _FILTER_SCROLL_SPEED = 12;
let _filterDragState: {
    card: HTMLElement;
    container: any;
    startY: number;
    started: boolean;
    clone: HTMLElement | null;
    placeholder: HTMLElement | null;
    offsetY: number;
    fromIndex: number;
    scrollRaf: number | null;
    filtersArr: any;
    rerenderFn: any;
    lastTarget?: HTMLElement;
    lastBefore?: boolean;
} | null = null;

function _initFilterDragForContainer(containerId: string, filtersArr: any, rerenderFn: any) {
    const container = document.getElementById(containerId) as any;
    if (!container) return;

    // Update refs each render so the pointerdown closure always sees current data
    container._filterDragFilters = filtersArr;
    container._filterDragRerender = rerenderFn;

    // Guard against stacking listeners across re-renders
    if (container._filterDragBound) return;
    container._filterDragBound = true;

    container.addEventListener('pointerdown', (e: any) => {
        const handle = (e.target as HTMLElement).closest('.pp-filter-drag-handle');
        if (!handle) return;
        const card = handle.closest('.pp-filter-card');
        if (!card) return;
        e.preventDefault();
        e.stopPropagation();

        const fromIndex = parseInt((card as HTMLElement).dataset.filterIndex!, 10);
        _filterDragState = {
            card: card as HTMLElement,
            container,
            startY: e.clientY,
            started: false,
            clone: null,
            placeholder: null,
            offsetY: 0,
            fromIndex,
            scrollRaf: null,
            filtersArr: (container as any)._filterDragFilters,
            rerenderFn: (container as any)._filterDragRerender,
        };

        const onMove = (ev) => _onFilterDragMove(ev);
        const cleanup = () => {
            document.removeEventListener('pointermove', onMove);
            document.removeEventListener('pointerup', cleanup);
            document.removeEventListener('pointercancel', cleanup);
            _onFilterDragEnd();
        };
        document.addEventListener('pointermove', onMove);
        document.addEventListener('pointerup', cleanup);
        document.addEventListener('pointercancel', cleanup);
    });
}

function _onFilterDragMove(e: any) {
    const ds = _filterDragState;
    if (!ds) return;

    if (!ds.started) {
        if (Math.abs(e.clientY - ds.startY) < _FILTER_DRAG_THRESHOLD) return;
        _startFilterDrag(ds, e);
    }

    // Position clone at pointer
    ds.clone!.style.top = (e.clientY - ds.offsetY) + 'px';

    // Find drop target by vertical midpoint
    const cards = ds.container.querySelectorAll('.pp-filter-card');
    for (const card of cards) {
        if (card.style.display === 'none') continue;
        const r = card.getBoundingClientRect();
        if (e.clientY >= r.top && e.clientY <= r.bottom) {
            const before = e.clientY < r.top + r.height / 2;
            if (card === ds.lastTarget && before === ds.lastBefore) break;
            ds.lastTarget = card;
            ds.lastBefore = before;
            if (before) {
                ds.container.insertBefore(ds.placeholder, card);
            } else {
                ds.container.insertBefore(ds.placeholder, card.nextSibling);
            }
            break;
        }
    }

    // Auto-scroll near viewport edges
    _filterAutoScroll(e.clientY, ds);
}

function _startFilterDrag(ds: any, e: any) {
    ds.started = true;
    const rect = ds.card.getBoundingClientRect();

    // Clone for visual feedback
    const clone = ds.card.cloneNode(true);
    clone.className = ds.card.className + ' pp-filter-drag-clone';
    clone.style.width = rect.width + 'px';
    clone.style.left = rect.left + 'px';
    clone.style.top = rect.top + 'px';
    document.body.appendChild(clone);
    ds.clone = clone;
    ds.offsetY = e.clientY - rect.top;

    // Placeholder
    const placeholder = document.createElement('div');
    placeholder.className = 'pp-filter-drag-placeholder';
    placeholder.style.height = rect.height + 'px';
    ds.card.parentNode.insertBefore(placeholder, ds.card);
    ds.placeholder = placeholder;

    // Hide original
    ds.card.style.display = 'none';
    document.body.classList.add('pp-filter-dragging');
}

function _onFilterDragEnd() {
    const ds = _filterDragState;
    _filterDragState = null;
    if (!ds || !ds.started) return;

    if (ds.scrollRaf) cancelAnimationFrame(ds.scrollRaf);

    // Determine new index from placeholder position among children
    let toIndex = 0;
    for (const child of ds.container.children) {
        if (child === ds.placeholder) break;
        if (child.classList.contains('pp-filter-card') && child.style.display !== 'none') {
            toIndex++;
        }
    }

    // Cleanup DOM
    ds.card.style.display = '';
    ds.placeholder!.remove();
    ds.clone!.remove();
    document.body.classList.remove('pp-filter-dragging');

    // Reorder filters array
    if (toIndex !== ds.fromIndex) {
        const [item] = ds.filtersArr.splice(ds.fromIndex, 1);
        ds.filtersArr.splice(toIndex, 0, item);
        ds.rerenderFn();
    }
}

function _filterAutoScroll(clientY: number, ds: any) {
    if (ds.scrollRaf) cancelAnimationFrame(ds.scrollRaf);
    const modal = ds.container.closest('.modal-body');
    if (!modal) return;
    const rect = modal.getBoundingClientRect();
    let speed = 0;
    if (clientY < rect.top + _FILTER_SCROLL_EDGE) {
        speed = -_FILTER_SCROLL_SPEED;
    } else if (clientY > rect.bottom - _FILTER_SCROLL_EDGE) {
        speed = _FILTER_SCROLL_SPEED;
    }
    if (speed === 0) return;
    const scroll = () => {
        modal.scrollTop += speed;
        ds.scrollRaf = requestAnimationFrame(scroll);
    };
    ds.scrollRaf = requestAnimationFrame(scroll);
}

// _addFilterGeneric and _updateFilterOptionGeneric have been replaced by FilterListManager methods

// ── PP filter actions (delegate to ppFilterManager) ──
export function addFilterFromSelect() { ppFilterManager.addFromSelect(); }
export function toggleFilterExpand(index: any) { ppFilterManager.toggleExpand(index); }
export function removeFilter(index: any) { ppFilterManager.remove(index); }
export function moveFilter(index: any, direction: any) { ppFilterManager.move(index, direction); }
export function updateFilterOption(filterIndex: any, optionKey: any, value: any) { ppFilterManager.updateOption(filterIndex, optionKey, value); }

// ── CSPT filter actions (delegate to csptFilterManager) ──
export function csptAddFilterFromSelect() { csptFilterManager.addFromSelect(); }
export function csptToggleFilterExpand(index: any) { csptFilterManager.toggleExpand(index); }
export function csptRemoveFilter(index: any) { csptFilterManager.remove(index); }
export function csptUpdateFilterOption(filterIndex: any, optionKey: any, value: any) { csptFilterManager.updateOption(filterIndex, optionKey, value); }

function collectFilters() {
    return ppFilterManager.collect();
}

function _autoGeneratePPTemplateName() {
    if (_ppTemplateNameManuallyEdited) return;
    if ((document.getElementById('pp-template-id') as HTMLInputElement).value) return;
    const nameInput = document.getElementById('pp-template-name') as HTMLInputElement;
    if (_modalFilters.length > 0) {
        const filterNames = _modalFilters.map(f => _getFilterName(f.filter_id)).join(' + ');
        nameInput.value = filterNames;
    } else {
        nameInput.value = '';
    }
}

export async function showAddPPTemplateModal(cloneData: any = null) {
    if (_availableFilters.length === 0) await loadAvailableFilters();

    document.getElementById('pp-template-modal-title')!.innerHTML = `${ICON_PP_TEMPLATE} ${t('postprocessing.add')}`;
    (document.getElementById('pp-template-form') as HTMLFormElement).reset();
    (document.getElementById('pp-template-id') as HTMLInputElement).value = '';
    document.getElementById('pp-template-error')!.style.display = 'none';

    if (cloneData) {
        set_modalFilters((cloneData.filters || []).map(fi => ({
            filter_id: fi.filter_id,
            options: { ...fi.options },
        })));
        set_ppTemplateNameManuallyEdited(true);
    } else {
        set_modalFilters([]);
        set_ppTemplateNameManuallyEdited(false);
    }
    (document.getElementById('pp-template-name') as HTMLInputElement).oninput = () => { set_ppTemplateNameManuallyEdited(true); };

    ppFilterManager.populateSelect(() => addFilterFromSelect());
    renderModalFilterList();

    // Pre-fill from clone data after form is set up
    if (cloneData) {
        (document.getElementById('pp-template-name') as HTMLInputElement).value = (cloneData.name || '') + ' (Copy)';
        (document.getElementById('pp-template-description') as HTMLInputElement).value = cloneData.description || '';
    }

    // Tags
    if (_ppTemplateTagsInput) { _ppTemplateTagsInput.destroy(); _ppTemplateTagsInput = null; }
    _ppTemplateTagsInput = new TagInput(document.getElementById('pp-template-tags-container'), { placeholder: t('tags.placeholder') });
    _ppTemplateTagsInput.setValue(cloneData ? (cloneData.tags || []) : []);

    ppTemplateModal.open();
    ppTemplateModal.snapshot();
}

export async function editPPTemplate(templateId: any) {
    try {
        if (_availableFilters.length === 0) await loadAvailableFilters();

        const response = await fetchWithAuth(`/postprocessing-templates/${templateId}`);
        if (!response.ok) throw new Error(`Failed to load template: ${response.status}`);
        const tmpl = await response.json();

        document.getElementById('pp-template-modal-title')!.innerHTML = `${ICON_PP_TEMPLATE} ${t('postprocessing.edit')}`;
        (document.getElementById('pp-template-id') as HTMLInputElement).value = templateId;
        (document.getElementById('pp-template-name') as HTMLInputElement).value = tmpl.name;
        (document.getElementById('pp-template-description') as HTMLInputElement).value = tmpl.description || '';
        document.getElementById('pp-template-error')!.style.display = 'none';

        set_modalFilters((tmpl.filters || []).map(fi => ({
            filter_id: fi.filter_id,
            options: { ...fi.options },
        })));

        ppFilterManager.populateSelect(() => addFilterFromSelect());
        renderModalFilterList();

        // Tags
        if (_ppTemplateTagsInput) { _ppTemplateTagsInput.destroy(); _ppTemplateTagsInput = null; }
        _ppTemplateTagsInput = new TagInput(document.getElementById('pp-template-tags-container'), { placeholder: t('tags.placeholder') });
        _ppTemplateTagsInput.setValue(tmpl.tags || []);

        ppTemplateModal.open();
        ppTemplateModal.snapshot();
    } catch (error) {
        console.error('Error loading PP template:', error);
        showToast(t('postprocessing.error.load') + ': ' + error.message, 'error');
    }
}

export async function savePPTemplate() {
    const templateId = (document.getElementById('pp-template-id') as HTMLInputElement).value;
    const name = (document.getElementById('pp-template-name') as HTMLInputElement).value.trim();
    const description = (document.getElementById('pp-template-description') as HTMLInputElement).value.trim();
    const errorEl = document.getElementById('pp-template-error')!;

    if (!name) { showToast(t('postprocessing.error.required'), 'error'); return; }

    const payload = { name, filters: collectFilters(), description: description || null, tags: _ppTemplateTagsInput ? _ppTemplateTagsInput.getValue() : [] };

    try {
        let response;
        if (templateId) {
            response = await fetchWithAuth(`/postprocessing-templates/${templateId}`, { method: 'PUT', body: JSON.stringify(payload) });
        } else {
            response = await fetchWithAuth('/postprocessing-templates', { method: 'POST', body: JSON.stringify(payload) });
        }

        if (!response.ok) {
            const error = await response.json();
            throw new Error(error.detail || error.message || 'Failed to save template');
        }

        showToast(templateId ? t('postprocessing.updated') : t('postprocessing.created'), 'success');
        ppTemplateModal.forceClose();
        ppTemplatesCache.invalidate();
        await loadPPTemplates();
    } catch (error) {
        console.error('Error saving PP template:', error);
        errorEl.textContent = error.message;
        errorEl.style.display = 'block';
    }
}

// ===== Clone functions =====

export async function cloneStream(streamId: any) {
    try {
        const resp = await fetchWithAuth(`/picture-sources/${streamId}`);
        if (!resp.ok) throw new Error('Failed to load stream');
        const stream = await resp.json();
        showAddStreamModal(stream.stream_type, stream);
    } catch (error) {
        if (error.isAuth) return;
        console.error('Failed to clone stream:', error);
        showToast(t('stream.error.clone_picture_failed'), 'error');
    }
}

export async function cloneCaptureTemplate(templateId: any) {
    try {
        const resp = await fetchWithAuth(`/capture-templates/${templateId}`);
        if (!resp.ok) throw new Error('Failed to load template');
        const tmpl = await resp.json();
        _localShowAddTemplateModal(tmpl);
    } catch (error) {
        if (error.isAuth) return;
        console.error('Failed to clone capture template:', error);
        showToast(t('stream.error.clone_capture_failed'), 'error');
    }
}

export async function clonePPTemplate(templateId: any) {
    try {
        const resp = await fetchWithAuth(`/postprocessing-templates/${templateId}`);
        if (!resp.ok) throw new Error('Failed to load template');
        const tmpl = await resp.json();
        showAddPPTemplateModal(tmpl);
    } catch (error) {
        if (error.isAuth) return;
        console.error('Failed to clone PP template:', error);
        showToast(t('stream.error.clone_pp_failed'), 'error');
    }
}

export async function deletePPTemplate(templateId: any) {
    const confirmed = await showConfirm(t('postprocessing.delete.confirm'));
    if (!confirmed) return;

    try {
        const response = await fetchWithAuth(`/postprocessing-templates/${templateId}`, { method: 'DELETE' });
        if (!response.ok) {
            const error = await response.json();
            throw new Error(error.detail || error.message || 'Failed to delete template');
        }
        showToast(t('postprocessing.deleted'), 'success');
        ppTemplatesCache.invalidate();
        await loadPPTemplates();
    } catch (error) {
        console.error('Error deleting PP template:', error);
        showToast(t('postprocessing.error.delete') + ': ' + error.message, 'error');
    }
}

export async function closePPTemplateModal() {
    await ppTemplateModal.close();
}

// ===== Color Strip Processing Templates (CSPT) =====

// ── CSPT FilterListManager instance ──
const csptFilterManager = new FilterListManager({
    getFilters: () => _csptModalFilters,
    getFilterDefs: () => _stripFilters,
    getFilterName: _getStripFilterName,
    selectId: 'cspt-add-filter-select',
    containerId: 'cspt-filter-list',
    prefix: 'cspt',
    editingIdInputId: 'cspt-id',
    selfRefFilterId: 'css_filter_template',
    autoNameFn: () => _autoGenerateCSPTName(),
    initDrag: _initFilterDragForContainer,
    initPaletteGrids: _initFilterPaletteGrids,
});

async function loadStripFilters() {
    await stripFiltersCache.fetch();
}

async function loadCSPTemplates() {
    try {
        if (_stripFilters.length === 0) await stripFiltersCache.fetch();
        await csptCache.fetch();
        renderPictureSourcesList(_cachedStreams);
    } catch (error) {
        console.error('Error loading CSPT:', error);
    }
}

function _autoGenerateCSPTName() {
    if (_csptNameManuallyEdited) return;
    if ((document.getElementById('cspt-id') as HTMLInputElement).value) return;
    const nameInput = document.getElementById('cspt-name') as HTMLInputElement;
    if (_csptModalFilters.length > 0) {
        const filterNames = _csptModalFilters.map(f => _getStripFilterName(f.filter_id)).join(' + ');
        nameInput.value = filterNames;
    } else {
        nameInput.value = '';
    }
}

function collectCSPTFilters() {
    return csptFilterManager.collect();
}

export async function showAddCSPTModal(cloneData: any = null) {
    if (_stripFilters.length === 0) await loadStripFilters();

    document.getElementById('cspt-modal-title')!.innerHTML = `${ICON_CSPT} ${t('css_processing.add')}`;
    (document.getElementById('cspt-form') as HTMLFormElement).reset();
    (document.getElementById('cspt-id') as HTMLInputElement).value = '';
    document.getElementById('cspt-error')!.style.display = 'none';

    if (cloneData) {
        set_csptModalFilters((cloneData.filters || []).map(fi => ({
            filter_id: fi.filter_id,
            options: { ...fi.options },
        })));
        set_csptNameManuallyEdited(true);
    } else {
        set_csptModalFilters([]);
        set_csptNameManuallyEdited(false);
    }
    (document.getElementById('cspt-name') as HTMLInputElement).oninput = () => { set_csptNameManuallyEdited(true); };

    csptFilterManager.populateSelect(() => csptAddFilterFromSelect());
    renderCSPTModalFilterList();

    if (cloneData) {
        (document.getElementById('cspt-name') as HTMLInputElement).value = (cloneData.name || '') + ' (Copy)';
        (document.getElementById('cspt-description') as HTMLInputElement).value = cloneData.description || '';
    }

    if (_csptTagsInput) { _csptTagsInput.destroy(); _csptTagsInput = null; }
    _csptTagsInput = new TagInput(document.getElementById('cspt-tags-container'), { placeholder: t('tags.placeholder') });
    _csptTagsInput.setValue(cloneData ? (cloneData.tags || []) : []);

    csptModal.open();
    csptModal.snapshot();
}

export async function editCSPT(templateId: any) {
    try {
        if (_stripFilters.length === 0) await loadStripFilters();

        const response = await fetchWithAuth(`/color-strip-processing-templates/${templateId}`);
        if (!response.ok) throw new Error(`Failed to load template: ${response.status}`);
        const tmpl = await response.json();

        document.getElementById('cspt-modal-title')!.innerHTML = `${ICON_CSPT} ${t('css_processing.edit')}`;
        (document.getElementById('cspt-id') as HTMLInputElement).value = templateId;
        (document.getElementById('cspt-name') as HTMLInputElement).value = tmpl.name;
        (document.getElementById('cspt-description') as HTMLInputElement).value = tmpl.description || '';
        document.getElementById('cspt-error')!.style.display = 'none';

        set_csptModalFilters((tmpl.filters || []).map(fi => ({
            filter_id: fi.filter_id,
            options: { ...fi.options },
        })));

        csptFilterManager.populateSelect(() => csptAddFilterFromSelect());
        renderCSPTModalFilterList();

        if (_csptTagsInput) { _csptTagsInput.destroy(); _csptTagsInput = null; }
        _csptTagsInput = new TagInput(document.getElementById('cspt-tags-container'), { placeholder: t('tags.placeholder') });
        _csptTagsInput.setValue(tmpl.tags || []);

        csptModal.open();
        csptModal.snapshot();
    } catch (error) {
        console.error('Error loading CSPT:', error);
        showToast(t('css_processing.error.load') + ': ' + error.message, 'error');
    }
}

export async function saveCSPT() {
    const templateId = (document.getElementById('cspt-id') as HTMLInputElement).value;
    const name = (document.getElementById('cspt-name') as HTMLInputElement).value.trim();
    const description = (document.getElementById('cspt-description') as HTMLInputElement).value.trim();
    const errorEl = document.getElementById('cspt-error')!;

    if (!name) { showToast(t('css_processing.error.required'), 'error'); return; }

    const payload = { name, filters: collectCSPTFilters(), description: description || null, tags: _csptTagsInput ? _csptTagsInput.getValue() : [] };

    try {
        let response;
        if (templateId) {
            response = await fetchWithAuth(`/color-strip-processing-templates/${templateId}`, { method: 'PUT', body: JSON.stringify(payload) });
        } else {
            response = await fetchWithAuth('/color-strip-processing-templates', { method: 'POST', body: JSON.stringify(payload) });
        }

        if (!response.ok) {
            const error = await response.json();
            throw new Error(error.detail || error.message || 'Failed to save template');
        }

        showToast(templateId ? t('css_processing.updated') : t('css_processing.created'), 'success');
        csptModal.forceClose();
        csptCache.invalidate();
        await loadCSPTemplates();
    } catch (error) {
        console.error('Error saving CSPT:', error);
        errorEl.textContent = error.message;
        errorEl.style.display = 'block';
    }
}

export async function cloneCSPT(templateId: any) {
    try {
        const resp = await fetchWithAuth(`/color-strip-processing-templates/${templateId}`);
        if (!resp.ok) throw new Error('Failed to load template');
        const tmpl = await resp.json();
        showAddCSPTModal(tmpl);
    } catch (error) {
        if (error.isAuth) return;
        console.error('Failed to clone CSPT:', error);
        showToast(t('css_processing.error.clone_failed') + ': ' + error.message, 'error');
    }
}

export async function deleteCSPT(templateId: any) {
    const confirmed = await showConfirm(t('css_processing.delete.confirm'));
    if (!confirmed) return;

    try {
        const response = await fetchWithAuth(`/color-strip-processing-templates/${templateId}`, { method: 'DELETE' });
        if (!response.ok) {
            const error = await response.json();
            throw new Error(error.detail || error.message || 'Failed to delete template');
        }
        showToast(t('css_processing.deleted'), 'success');
        csptCache.invalidate();
        await loadCSPTemplates();
    } catch (error) {
        console.error('Error deleting CSPT:', error);
        showToast(t('css_processing.error.delete') + ': ' + error.message, 'error');
    }
}

export async function closeCSPTModal() {
    await csptModal.close();
}

