import { getMetrics } from './screener-data.js';
import { getFilterConditions, getDefaultConditionsForMetric } from './filter-data.js';

(function() {
    'use strict';

    var config   = window.vunelix || {};
    var widgets  = config.widgets || {};
    var common   = config.commonFields || {};
    var cdnUrl   = config.cdnUrl || '';
    var apiUrl   = 'https://vunelix.com/api/';
    var savedType     = config.savedType || '';
    var savedSettings = config.savedSettings || {};
    // Cache-bust dynamically-loaded widget modules — admin.js attaches
    // `<script src="cdnUrl + ...">` tags at runtime, which WP cannot version
    // for us. Append the plugin version (or build timestamp fallback) so
    // browsers don't keep serving stale module code after a plugin update.
    var cacheBust = '?v=' + encodeURIComponent(config.version || Date.now());

    var loadedScripts = {};
    var ajaxCache = {}; // Cache API responses


    // ================================================================
    // API SERVICE — POST requests (no auth needed — server handles it)
    // ================================================================

    function apiPost(endpoint, data) {
        var body = new URLSearchParams();
        Object.keys(data || {}).forEach(function(k) {
            body.append(k, data[k]);
        });

        return fetch(apiUrl + endpoint, { method: 'POST', body: body })
            .then(function(r) { return r.json(); })
            .then(function(r) {
                if (r.status === false) throw new Error(r.msg || 'API error');
                return r;
            });
    }

    // Cached API call
    function apiPostCached(endpoint, data) {
        var cacheKey = endpoint + '|' + JSON.stringify(data || {});
        if (ajaxCache[cacheKey]) return Promise.resolve(ajaxCache[cacheKey]);
        return apiPost(endpoint, data).then(function(r) {
            ajaxCache[cacheKey] = r;
            return r;
        });
    }


    // ================================================================
    // STATIC DATA SOURCES — sectors, exchanges (same as site market-data.js)
    // ================================================================

    var SECTORS = {
        '': 'All Sectors',
        'Commercial Services': 'Commercial Services',
        'Communications': 'Communications',
        'Consumer Durables': 'Consumer Durables',
        'Consumer Non-Durables': 'Consumer Non-Durables',
        'Consumer Services': 'Consumer Services',
        'Distribution Services': 'Distribution Services',
        'Electronic Technology': 'Electronic Technology',
        'Energy Minerals': 'Energy Minerals',
        'Non-Energy Minerals': 'Non-Energy Minerals',
        'Finance': 'Finance',
        'Government Sector': 'Government Sector',
        'Health Services': 'Health Services',
        'Health Technology': 'Health Technology',
        'Industrial Services': 'Industrial Services',
        'Miscellaneous': 'Miscellaneous',
        'Process Industries': 'Process Industries',
        'Producer Manufacturing': 'Producer Manufacturing',
        'Retail Trade': 'Retail Trade',
        'Technology Services': 'Technology Services',
        'Transportation': 'Transportation',
        'Utilities': 'Utilities',
    };

    // Market data — simplified from site's market-data.js (no getters, plain objects)
    var MARKET_DATA = {
        crypto: {
            exchange: [
                { label: 'Crypto Coins', value: 'crypto', icon: 'crypto' },
                { label: 'Crypto Cap', value: 'CRYPTOCAP', icon: 'cryptocap' },
                { label: 'Binance', value: 'binance', icon: 'binance' },
                { label: 'MEXC', value: 'mexc', icon: 'mexc' },
                { label: 'KuCoin', value: 'kucoin', icon: 'kucoin' },
                { label: 'Coinbase', value: 'coinbase', icon: 'coinbase' },
                { label: 'OKX', value: 'okx', icon: 'okx' },
                { label: 'Kraken', value: 'kraken', icon: 'kraken' },
                { label: 'Bybit', value: 'bybit', icon: 'bybit' },
            ],
            sub_type: { spot: 'Spot', swap: 'Swap', futures: 'Futures', index: 'Index', dominance: 'Dominance' },
        },
        dex: {
            exchange: [
                { label: 'PancakeSwap', value: 'pancakeswap', icon: 'pancakeswap' },
                { label: 'PancakeSwap v3 (BSC)', value: 'pancakeswap3bsc', icon: 'pancakeswap3' },
                { label: 'PancakeSwap v3 (ETH)', value: 'pancakeswap3eth', icon: 'pancakeswap3' },
                { label: 'Uniswap', value: 'uniswap', icon: 'uniswap' },
                { label: 'Uniswap v3 (BSC)', value: 'uniswap3bsc', icon: 'uniswap3' },
                { label: 'Uniswap v3 (ETH)', value: 'uniswap3eth', icon: 'uniswap3' },
                { label: 'Uniswap v3 (Base)', value: 'uniswap3base', icon: 'uniswap3' },
                { label: 'Uniswap v3 (Polygon)', value: 'uniswap3polygon', icon: 'uniswap3' },
                { label: 'Uniswap v3 (Arbitrum)', value: 'uniswap3arbitrum', icon: 'uniswap3' },
                { label: 'Raydium', value: 'raydium', icon: 'raydium' },
                { label: 'PulseX', value: 'pulsex', icon: 'pulsex' },
                { label: 'PulseX v2', value: 'pulsex2', icon: 'pulsex' },
                { label: 'VVS Finance', value: 'vvsfinance', icon: 'vvsfinance' },
                { label: 'Orca', value: 'orca', icon: 'orca' },
                { label: 'Trader Joe', value: 'traderjoe', icon: 'traderjoe' },
                { label: 'Curve', value: 'curve', icon: 'curve' },
                { label: 'Shadow', value: 'shadow', icon: 'shadow' },
            ],
        },
        forex: {
            exchange: [
                { label: 'London Currency Banks', value: 'FX', icon: 'fx' },
                { label: 'Orion North America', value: 'ONA', icon: 'ona' },
                { label: 'FX Chicago Market', value: 'FCM', icon: 'fcm' },
                { label: 'Singapore Forex Office', value: 'SFO', icon: 'sfo' },
                { label: 'Institutional Forex Research', value: 'IFR', icon: 'ifr' },
            ],
            sub_type: { spot: 'Spot', synthetic: 'Synthetic' },
        },
        stock: {
            country: config.countries || {},
            sub_type: { common: 'Common', preferred: 'Preferred', dr: 'Depository Receipt', structured: 'Structured' },
            sector: SECTORS,
        },
        indices: { country: config.countries || {} },
        fund: {
            country: config.countries || {},
            sector: SECTORS,
            sub_type: { closedend: 'Closedend', etf: 'ETF', mutual: 'Mutual', reit: 'Reit', unit: 'Unit', trust: 'Trust' },
        },
        futures: {
            exchange: [
                { label: 'CME', value: 'CME', icon: 'cme' },
                { label: 'NYMEX', value: 'NYMEX', icon: 'nymex' },
                { label: 'COMEX', value: 'COMEX', icon: 'comex' },
                { label: 'CBOT', value: 'CBOT', icon: 'cbot' },
            ],
        },
    };

    // Convert exchange arrays to {value: label} objects for select fields
    var FOREX_EXCHANGES = {};
    (MARKET_DATA.forex.exchange || []).forEach(function(e) { FOREX_EXCHANGES[e.value] = e.label; });

    var STATIC_SOURCES = {
        'sectors': SECTORS,
        'forex_exchanges': FOREX_EXCHANGES,
    };

    // Screener metrics — from imported screener-data.js, filtered per asset type
    function getScreenerMetricsForType(assetType) {
        var result = {};
        var all = getMetrics();
        Object.keys(all).forEach(function(key) {
            var m = all[key];
            if (m.assets && m.assets.indexOf(assetType) === -1) return;
            if (m.exclude_from_column) return;
            result[key] = m.label || key;
        });
        return result;
    }

    function getScreenerSortForType(assetType) {
        var sort = { '': 'Default' };
        var all = getMetrics();
        Object.keys(all).forEach(function(key) {
            var m = all[key];
            if (m.assets && m.assets.indexOf(assetType) === -1) return;
            if (m.exclude_from_column || !m.path) return;
            var label = m.label || key;
            // Use API path for sort key (e.g. 'active.c_asc', 'perf.rank_desc')
            sort[m.path + '_asc'] = label + ' ↑';
            sort[m.path + '_desc'] = label + ' ↓';
        });
        return sort;
    }

    // Map screener widget slug → asset type
    var SLUG_TO_ASSET = {
        'crypto-screener': 'coin', 'stock-screener': 'stock', 'forex-screener': 'forex',
        'fund-screener': 'fund', 'cex-screener': 'crypto', 'dex-screener': 'dex', 'dr-screener': 'dr',
    };

    function resolveSource(sourceName) {
        if (STATIC_SOURCES[sourceName]) return STATIC_SOURCES[sourceName];
        var assetType = SLUG_TO_ASSET[typeSelect ? typeSelect.value : ''] || 'coin';
        if (sourceName === 'screener_sort') return getScreenerSortForType(assetType);
        if (sourceName === 'screener_metrics') return getScreenerMetricsForType(assetType);
        return {};
    }

    // DOM refs
    var typeSelect     = document.getElementById('vnxWidgetType');
    var typeDesc       = document.getElementById('vnxTypeDesc');
    var settingsEl     = document.getElementById('vnxSettings');
    var settingsJson   = document.getElementById('vnxSettingsJson');
    var previewBox     = document.getElementById('vnxPreviewBox');
    var previewPlaceholder = document.getElementById('vnxPreviewPlaceholder');
    var settingsPlaceholder = document.getElementById('vnxSettingsPlaceholder');
    var applyBtn       = document.getElementById('vnxApplyBtn');
    var copyBtn        = document.getElementById('vnxCopyShortcode');
    var shortcodeField = document.getElementById('vnxShortcodeField');

    // Dashboard slider — runs on dashboard page (no typeSelect)
    if (!typeSelect) {
        initDashboardSlider();
        return;
    }


    // ================================================================
    // WIDGET TYPE CHANGE
    // ================================================================

    typeSelect.addEventListener('change', function() {
        var slug = this.value;
        onTypeChange(slug);
        // Apply preview on type change
        setTimeout(function() { applyToPreview(); }, 300);
    });

    var typeHint = document.querySelector('.vnx-type-hint');

    function onTypeChange(slug) {
        var widget = widgets[slug];

        if (!widget) {
            typeDesc.textContent = '';
            if (typeHint) typeHint.style.display = '';
            if (settingsPlaceholder) settingsPlaceholder.style.display = '';
            if (applyBtnBottomWrap) applyBtnBottomWrap.style.display = 'none';
            settingsEl.querySelectorAll('.vnx-group').forEach(function(g) { g.remove(); });
            return;
        }
        if (typeHint) typeHint.style.display = 'none';

        typeDesc.textContent = widget.desc;
        if (settingsPlaceholder) settingsPlaceholder.style.display = 'none';
        if (applyBtnBottomWrap) applyBtnBottomWrap.style.display = '';

        renderSettings(widget, slug === savedType ? savedSettings : {});
        syncSettingsJson();
        setTimeout(updateColorPairVisibility, 100);

        // Pre-load symbol search widget with correct filter type for this widget
        setTimeout(spDetectAndLoad, 0);
    }

    // Init: if editing existing widget, load saved type
    if (savedType) {
        onTypeChange(savedType);
        // Apply preview on load if type exists
        setTimeout(function() { applyToPreview(); }, 500);
    }


    // ================================================================
    // SETTINGS RENDERER
    // ================================================================

    function renderSettings(widget, values) {
        // Remove old groups (keep placeholder)
        settingsEl.querySelectorAll('.vnx-group').forEach(function(g) { g.remove(); });

        var allSettings = widget.settings || {};
        var isFirst = true;

        Object.keys(allSettings).forEach(function(groupKey) {
            renderGroup(groupKey, allSettings[groupKey], isFirst, values);
            isFirst = false;
        });

        // Common fields — collapsed, skip if widget already defines the same group key
        Object.keys(common).forEach(function(groupKey) {
            if (allSettings[groupKey]) return;
            renderGroup(groupKey, common[groupKey], false, values);
        });

        // Wire up conditional visibility (hidden_when / visible_when)
        setTimeout(initConditionalVisibility, 0);
    }

    /**
     * Toggle-dependent field visibility.
     * hidden_when="enable-return-data" → hide when that toggle is ON
     * visible_when="enable-return-data" → show when that toggle is ON
     */
    function initConditionalVisibility() {
        settingsEl.querySelectorAll('input[type="checkbox"][data-key]').forEach(function(toggle) {
            var attrName = (toggle.dataset.key || '').replace(/_/g, '-');
            // Check if any fields depend on this toggle
            var dependents = settingsEl.querySelectorAll('[data-hidden-when="' + attrName + '"], [data-visible-when="' + attrName + '"]');
            if (!dependents.length) return;

            var update = function() {
                var isOn = toggle.checked;
                settingsEl.querySelectorAll('[data-hidden-when="' + attrName + '"]').forEach(function(el) {
                    el.style.display = isOn ? 'none' : '';
                });
                settingsEl.querySelectorAll('[data-visible-when="' + attrName + '"]').forEach(function(el) {
                    el.style.display = isOn ? '' : 'none';
                });
            };
            toggle.addEventListener('change', update);
            update(); // Set initial state
        });
    }

    function renderGroup(groupKey, group, startOpen, values) {
        if (!group || !group.fields) return;

        var section = document.createElement('div');
        section.className = 'vnx-group';

        var collapsed = !startOpen;

        var header = document.createElement('div');
        header.className = 'vnx-group-header' + (collapsed ? ' collapsed' : '');
        header.innerHTML = '<span class="vnx-group-title">' + escHtml(group.label || groupKey) + '</span>' +
            '<span class="vnx-group-arrow dashicons dashicons-arrow-' + (collapsed ? 'right' : 'down') + '-alt2"></span>';

        var body = document.createElement('div');
        body.className = 'vnx-group-body' + (collapsed ? ' collapsed' : '');

        header.addEventListener('click', function() {
            var isCol = body.classList.toggle('collapsed');
            header.classList.toggle('collapsed', isCol);
            var arrow = header.querySelector('.dashicons');
            arrow.className = 'vnx-group-arrow dashicons dashicons-arrow-' + (isCol ? 'right' : 'down') + '-alt2';
        });

        section.appendChild(header);

        var fields = group.fields;
        Object.keys(fields).forEach(function(fieldKey) {
            var fieldEl = buildField(fieldKey, fields[fieldKey], values);
            if (fieldEl) body.appendChild(fieldEl);
        });

        section.appendChild(body);
        settingsEl.appendChild(section);
    }

    function buildField(key, field, values) {
        var type     = field.type || 'text';
        var attrName = (field.key || key).replace(/_/g, '-');

        // Skip premium fields (pkg >= 2)
        var pkg = field.pkg || 1;
        if (pkg >= 2) return null;

        // Handle description — can be string or object (plan-based: {1: "free desc", 2: "premium desc"})
        var desc = field.desc || field.description || '';
        if (typeof desc === 'object' && desc !== null) {
            // Use lowest plan desc (free)
            var descKeys = Object.keys(desc).sort();
            desc = desc[descKeys[0]] || '';
        }

        // Handle max as object (plan-based: {1: 20, 2: 50}) — use free plan value
        var fieldMax = field.max;
        if (typeof fieldMax === 'object' && fieldMax !== null && !Array.isArray(fieldMax)) {
            var maxKeys = Object.keys(fieldMax).sort();
            fieldMax = fieldMax[maxKeys[0]];
        }
        var fieldMin = field.min;

        // Symbol picker — append "Max N symbols." to description (same as site docs.php line 394-411)
        if (type === 'symbol-picker' && field.multiple) {
            var widgetSlug = typeSelect.value;
            var widgetConfig = widgets[widgetSlug];
            var limits = widgetConfig ? (widgetConfig.limits || {}) : {};
            var fieldLimits = limits[field.key || key] || {};
            // Free plan = key "1", if not present use lowest key
            var freeMax = fieldLimits['1'] || fieldLimits[1];
            if (!freeMax) {
                var lk = Object.keys(fieldLimits).sort();
                if (lk.length) freeMax = fieldLimits[lk[0]];
            }
            if (freeMax) {
                desc = (desc ? desc + ' ' : '') + 'Max ' + freeMax + ' symbols.';
            }
        }

        var defaultVal = field.default;
        if (typeof defaultVal === 'boolean') defaultVal = defaultVal ? 'true' : 'false';
        defaultVal = String(defaultVal != null ? defaultVal : '');

        // Use saved value if exists
        var savedVal = values[field.key || key];
        if (savedVal === undefined) savedVal = values[key];
        var currentVal = savedVal !== undefined ? String(savedVal) : defaultVal;

        var wrap = document.createElement('div');
        // Full width from helper field config (full_width: true)
        var isFullWidth = !!field.full_width;
        wrap.className = 'vnx-field' + (type === 'toggle' ? ' vnx-field-toggle' : '') +
            (isFullWidth ? ' vnx-field-full' : '');

        // Conditional visibility — hidden_when / visible_when (toggle dependency)
        var hiddenWhen = field.hidden_when || '';
        var visibleWhen = field.visible_when || '';
        if (hiddenWhen) wrap.dataset.hiddenWhen = hiddenWhen;
        if (visibleWhen) {
            wrap.dataset.visibleWhen = visibleWhen;
            wrap.style.display = 'none'; // hidden by default, shown when toggle is ON
        }

        if (type === 'ajax-select') {
            // Ajax select — loads options from API on first open
            var fieldKey = field.key || key;
            var ajaxApi = field.api || '';
            var ajaxParams = field.api_params || {};
            var valueKey = field.value_key || '';
            var labelKey = field.label_key || '';
            var iconKey = field.icon_key || '';
            var placeholder = field.placeholder || 'Select...';
            var selectedLabel = currentVal || emptyLabel || placeholder;
            // Default flag — currency code first 2 chars = country code (USD → US, EUR → EU)
            var defaultFlagHtml = (currentVal && currentVal.length >= 2) ? '<img src="https://vunelix.com/assets/icons/country/' + escAttr(currentVal.substring(0, 2)) + '.svg" class="vnx-flag-icon" onerror="this.style.display=\'none\'" alt=""> ' : '';

            var dependsOn = field.depends_on || '';
            var dependParam = field.depend_param || '';
            var emptyLabel = field.empty_label || '';

            wrap.innerHTML = '<label class="vnx-field-label">' + escHtml(field.label || key) + '</label>' +
                '<div class="vnx-ajax-select" data-key="' + escAttr(fieldKey) + '" data-default="' + escAttr(defaultVal) + '"' +
                    ' data-api="' + escAttr(ajaxApi) + '"' +
                    ' data-api-params=\'' + JSON.stringify(ajaxParams).replace(/'/g, '&#39;') + '\'' +
                    ' data-value-key="' + escAttr(valueKey) + '"' +
                    ' data-label-key="' + escAttr(labelKey) + '"' +
                    ' data-icon-key="' + escAttr(iconKey) + '"' +
                    ' data-value="' + escAttr(currentVal) + '"' +
                    (dependsOn ? ' data-depends-on="' + escAttr(dependsOn) + '"' : '') +
                    (dependParam ? ' data-depend-param="' + escAttr(dependParam) + '"' : '') +
                    (emptyLabel ? ' data-empty-label="' + escAttr(emptyLabel) + '"' : '') + '>' +
                    '<div class="vnx-search-trigger" tabindex="0">' +
                        '<span class="vnx-search-label">' + defaultFlagHtml + escHtml(selectedLabel) + '</span>' +
                        '<span class="vnx-search-arrow">▾</span>' +
                    '</div>' +
                    '<div class="vnx-search-dropdown">' +
                        '<input type="text" class="vnx-search-input" placeholder="' + escAttr(placeholder) + '" autocomplete="off">' +
                        '<div class="vnx-search-list"><div class="vnx-ajax-loading">Loading...</div></div>' +
                    '</div>' +
                '</div>' +
                (desc ? '<p class="vnx-field-desc">' + desc + '</p>' : '');

        } else if (type === 'checkbox-group' || type === 'sortable-select') {
            // Sortable multi-select: draggable chips + dropdown to add
            var selectedVals = currentVal ? currentVal.split(',') : [];
            var maxItems = field.max || 99;
            var opts = field.options || {};
            var allOptKeys = Object.keys(opts);

            // Build selected items as draggable chips
            var itemsHtml = '';
            selectedVals.forEach(function(v) {
                if (!opts[v]) return;
                itemsHtml += '<div class="vnx-ss-item" draggable="true" data-value="' + escAttr(v) + '">' +
                    '<span class="vnx-ss-drag">⠿</span>' +
                    '<span class="vnx-ss-label">' + escHtml(opts[v]) + '</span>' +
                    '<button type="button" class="vnx-ss-remove" title="Remove">×</button>' +
                '</div>';
            });

            // Build dropdown options (exclude already selected)
            var dropdownHtml = '';
            allOptKeys.forEach(function(optVal) {
                var disabled = selectedVals.indexOf(optVal) !== -1 ? ' disabled' : '';
                dropdownHtml += '<div class="vnx-ss-option' + disabled + '" data-value="' + escAttr(optVal) + '">' + escHtml(opts[optVal]) + '</div>';
            });

            wrap.innerHTML =
                '<label class="vnx-field-label">' + escHtml(field.label || key) + '</label>' +
                '<div class="vnx-sortable-select" data-key="' + escAttr(field.key || key) + '" data-default="' + escAttr(defaultVal) + '" data-max="' + maxItems + '"' +
                    ' data-options=\'' + JSON.stringify(opts).replace(/'/g, '&#39;') + '\'>' +
                    '<div class="vnx-ss-items">' + itemsHtml + '</div>' +
                    '<div class="vnx-ss-dropdown">' +
                        '<div class="vnx-ss-trigger">Add metric... <span class="vnx-ss-arrow">▾</span></div>' +
                        '<div class="vnx-ss-menu" style="display:none;">' + dropdownHtml + '</div>' +
                    '</div>' +
                '</div>' +
                (desc ? '<p class="vnx-field-desc">' + desc + '</p>' : '');

        } else if (type === 'toggle') {
            wrap.innerHTML =
                '<div class="vnx-toggle-row">' +
                    '<div>' +
                        '<label class="vnx-field-label">' + escHtml(field.label || key) + '</label>' +
                        (desc ? '<p class="vnx-field-desc">' + desc + '</p>' : '') +
                    '</div>' +
                    '<label class="vnx-toggle">' +
                        '<input type="checkbox" data-key="' + escAttr(field.key || key) + '" data-default="' + escAttr(defaultVal) + '"' +
                        (currentVal === 'true' ? ' checked' : '') + '>' +
                        '<span class="vnx-toggle-slider"></span>' +
                    '</label>' +
                '</div>';

        } else if (type === 'select') {
            var opts = field.options || {};
            // Populate from source if available (e.g. source: 'sectors', 'screener_sort')
            if (field.source) {
                var sourceData = resolveSource(field.source);
                if (sourceData) opts = Object.assign({}, opts, sourceData);
            }
            var optKeys = Object.keys(opts);
            var isSearchable = !!field.searchable || optKeys.length > 15;

            if (isSearchable) {
                // Searchable select — custom dropdown with search + optional flag icons
                var iconType = field.icon || '';
                var iconBase = 'https://vunelix.com/assets/icons/';
                var selectedLabel = opts[currentVal] || currentVal || '— Select —';

                function buildOptHtml(optVal, optLabel, isSelected) {
                    var flagHtml = iconType ? '<img src="' + iconBase + escAttr(iconType) + '/' + escAttr(optVal) + '.svg" class="vnx-flag-icon" onerror="this.style.display=\'none\'" alt=""> ' : '';
                    return '<div class="vnx-search-option' + (isSelected ? ' selected' : '') + '" data-value="' + escAttr(optVal) + '" data-search="' + escAttr((optVal + ' ' + optLabel).toLowerCase()) + '">' +
                        flagHtml + escHtml(optLabel) + '</div>';
                }

                var selectedFlagHtml = iconType && currentVal ? '<img src="' + iconBase + escAttr(iconType) + '/' + escAttr(currentVal) + '.svg" class="vnx-flag-icon" onerror="this.style.display=\'none\'" alt=""> ' : '';

                var optionsHtml = '';
                optKeys.forEach(function(optVal) {
                    optionsHtml += buildOptHtml(optVal, opts[optVal], String(optVal) === currentVal);
                });
                wrap.innerHTML = '<label class="vnx-field-label">' + escHtml(field.label || key) + '</label>' +
                    '<div class="vnx-searchable-wrap">' +
                        '<select data-key="' + escAttr(field.key || key) + '" data-default="' + escAttr(defaultVal) + '" style="display:none;">' +
                            optKeys.map(function(ov) { return '<option value="' + escAttr(ov) + '"' + (String(ov) === currentVal ? ' selected' : '') + '>' + escHtml(opts[ov]) + '</option>'; }).join('') +
                        '</select>' +
                        '<div class="vnx-search-trigger" tabindex="0">' +
                            '<span class="vnx-search-label">' + selectedFlagHtml + escHtml(selectedLabel) + '</span>' +
                            '<span class="vnx-search-arrow">▾</span>' +
                        '</div>' +
                        '<div class="vnx-search-dropdown">' +
                            '<input type="text" class="vnx-search-input" placeholder="Search..." autocomplete="off">' +
                            '<div class="vnx-search-list">' + optionsHtml + '</div>' +
                        '</div>' +
                    '</div>' +
                    (desc ? '<p class="vnx-field-desc">' + desc + '</p>' : '');
            } else {
                // Regular small select
                var sel = '<select data-key="' + escAttr(field.key || key) + '" data-default="' + escAttr(defaultVal) + '">';
                optKeys.forEach(function(optVal) {
                    sel += '<option value="' + escAttr(optVal) + '"' + (String(optVal) === currentVal ? ' selected' : '') + '>' + escHtml(opts[optVal]) + '</option>';
                });
                sel += '</select>';
                wrap.innerHTML = '<label class="vnx-field-label">' + escHtml(field.label || key) + '</label>' + sel +
                    (desc ? '<p class="vnx-field-desc">' + desc + '</p>' : '');
            }

        } else if (type === 'number') {
            var minAttr = fieldMin != null ? ' min="' + fieldMin + '"' : '';
            var maxAttr = fieldMax != null ? ' max="' + fieldMax + '"' : '';
            wrap.innerHTML = '<label class="vnx-field-label">' + escHtml(field.label || key) + '</label>' +
                '<input type="number" data-key="' + escAttr(field.key || key) + '" data-default="' + escAttr(defaultVal) + '" value="' + escAttr(currentVal) + '"' + minAttr + maxAttr + '>' +
                (desc ? '<p class="vnx-field-desc">' + desc + '</p>' : '');

        } else if (type === 'textarea') {
            wrap.innerHTML = '<label class="vnx-field-label">' + escHtml(field.label || key) + '</label>' +
                '<textarea data-key="' + escAttr(field.key || key) + '" data-default="' + escAttr(defaultVal) + '" rows="3" placeholder="' + escAttr(field.placeholder || '') + '">' + escHtml(currentVal) + '</textarea>' +
                (desc ? '<p class="vnx-field-desc">' + desc + '</p>' : '');

        } else if (type === 'color-pair') {
            var lightDef = field.light || '#ffffff';
            var darkDef  = field.dark || '#000000';
            // Parse saved "light,dark" value
            var lightVal = lightDef, darkVal = darkDef;
            if (currentVal && currentVal.indexOf(',') !== -1) {
                var parts = currentVal.split(',');
                lightVal = parts[0]; darkVal = parts[1];
            }
            wrap.innerHTML = '<label class="vnx-field-label">' + escHtml(field.label || key) + '</label>' +
                '<div class="vnx-color-pair" data-key="' + escAttr(field.key || key) + '" data-default="" data-light-default="' + escAttr(lightDef) + '" data-dark-default="' + escAttr(darkDef) + '">' +
                    '<div class="vnx-color-item"><span>Light</span><input type="color" class="vnx-cp-picker vnx-cp-light-picker" value="' + escAttr(lightVal) + '"><input type="text" class="vnx-cp-text vnx-cp-light" value="' + escAttr(lightVal) + '"></div>' +
                    '<div class="vnx-color-item"><span>Dark</span><input type="color" class="vnx-cp-picker vnx-cp-dark-picker" value="' + escAttr(darkVal) + '"><input type="text" class="vnx-cp-text vnx-cp-dark" value="' + escAttr(darkVal) + '"></div>' +
                '</div>' +
                (desc ? '<p class="vnx-field-desc">' + desc + '</p>' : '');

        } else if (type === 'filter-builder') {
            // Filter builder — interactive metric + condition + value picker
            wrap.className += ' vnx-field-full';

            // Parse saved filters JSON
            var savedFilters = {};
            if (currentVal) { try { savedFilters = JSON.parse(currentVal); } catch(e) {} }

            // Build existing filter chips from saved data
            var filterChipsHtml = '';
            Object.keys(savedFilters).forEach(function(filterKey) {
                // filterKey = "active.c_gt" → metric path = "active.c", condition = "gt"
                var lastUnderscore = filterKey.lastIndexOf('_');
                var metricPath = filterKey.substring(0, lastUnderscore);
                var condOp = filterKey.substring(lastUnderscore + 1);
                // Find metric label from METRICS
                var metricLabel = metricPath;
                var allMetrics = getMetrics();
                Object.keys(allMetrics).forEach(function(mk) {
                    if (allMetrics[mk].path === metricPath) metricLabel = allMetrics[mk].label || mk;
                });
                filterChipsHtml += '<div class="vnx-fb-chip" data-metric-path="' + escAttr(metricPath) + '" data-condition="' + escAttr(condOp) + '" data-value="' + escAttr(savedFilters[filterKey]) + '">' +
                    '<span class="vnx-fb-chip-label">' + escHtml(metricLabel) + '</span>' +
                    '<button type="button" class="vnx-fb-chip-toggle">▾</button>' +
                    '<button type="button" class="vnx-fb-chip-remove" title="Remove">×</button>' +
                '</div>';
            });

            wrap.innerHTML = '<label class="vnx-field-label">' + escHtml(field.label || key) + '</label>' +
                '<div class="vnx-filter-builder" data-key="' + escAttr(field.key || key) + '" data-default="">' +
                    '<div class="vnx-fb-chips">' + filterChipsHtml + '</div>' +
                    '<div class="vnx-fb-add">' +
                        '<div class="vnx-search-trigger vnx-fb-add-trigger"><span>+ ' + escHtml(field.add_label || 'Add Filter') + '</span></div>' +
                    '</div>' +
                    '<div class="vnx-fb-detail" style="display:none;"></div>' +
                '</div>' +
                (desc ? '<p class="vnx-field-desc">' + desc + '</p>' : '');

        } else if (type === 'symbol-picker') {
            // Symbol picker — uses vunelix-symbol-search widget as search modal
            var fieldKey = field.key || key;
            var isMultiple = !!field.multiple;
            var searchType = field.search_type || '';
            var searchExchange = field.search_exchange || '';

            // Parse saved symbols
            var savedSymbols = currentVal ? currentVal.split(',').filter(Boolean) : [];

            // Max symbols from limits
            var widgetSlug = typeSelect.value;
            var widgetConfig = widgets[widgetSlug];
            var fieldLimits = (widgetConfig && widgetConfig.limits) ? (widgetConfig.limits[fieldKey] || {}) : {};
            var freeMax = fieldLimits['1'] || fieldLimits[1];
            if (!freeMax) {
                var lk = Object.keys(fieldLimits).sort();
                if (lk.length) freeMax = fieldLimits[lk[0]];
            }
            var maxSymbols = freeMax || 0;

            if (isMultiple) {
                // Multi-symbol: list of chips + add button
                var itemsHtml = savedSymbols.map(function(sym) {
                    var parts = sym.split(':');
                    var symExchange = parts.length === 2 ? parts[0] : '';
                    var symName = parts.length === 2 ? parts[1] : sym;
                    return '<div class="vnx-sp-item" draggable="true" data-ticker="' + escAttr(sym) + '">' +
                        '<span class="vnx-sp-drag">⠿</span>' +
                        '<span class="vnx-sp-symbol">' + escHtml(symName) + '</span>' +
                        (symExchange ? '<span class="vnx-sp-exchange">' + escHtml(symExchange) + '</span>' : '') +
                        '<button type="button" class="vnx-sp-remove" title="Remove">×</button>' +
                    '</div>';
                }).join('');

                wrap.innerHTML = '<label class="vnx-field-label">' + escHtml(field.label || key) + '</label>' +
                    '<div class="vnx-symbol-picker" data-key="' + escAttr(fieldKey) + '" data-default="' + escAttr(defaultVal) + '"' +
                        ' data-multiple="true"' +
                        (maxSymbols ? ' data-max="' + maxSymbols + '"' : '') +
                        (searchType ? ' data-search-type="' + escAttr(searchType) + '"' : '') +
                        (searchExchange ? ' data-search-exchange="' + escAttr(searchExchange) + '"' : '') + '>' +
                        '<div class="vnx-sp-list">' + itemsHtml + '</div>' +
                        '<button type="button" class="vnx-sp-add-btn" data-field="' + escAttr(fieldKey) + '"' +
                            ((maxSymbols && savedSymbols.length >= maxSymbols) ? ' style="display:none"' : '') + '>+ Add Symbol</button>' +
                    '</div>' +
                    (desc ? '<p class="vnx-field-desc">' + desc + '</p>' : '');
            } else {
                // Single symbol: trigger button
                var firstSym = savedSymbols[0] || '';
                var firstParts = firstSym.split(':');
                var firstName = firstParts.length === 2 ? firstParts[1] : firstSym;
                wrap.innerHTML = '<label class="vnx-field-label">' + escHtml(field.label || key) + '</label>' +
                    '<div class="vnx-symbol-picker vnx-sp-single" data-key="' + escAttr(fieldKey) + '" data-default="' + escAttr(defaultVal) + '"' +
                        (searchType ? ' data-search-type="' + escAttr(searchType) + '"' : '') +
                        (searchExchange ? ' data-search-exchange="' + escAttr(searchExchange) + '"' : '') + '>' +
                        '<div class="vnx-sp-single-trigger" data-field="' + escAttr(fieldKey) + '" data-ticker="' + escAttr(firstSym) + '">' +
                            '<span class="vnx-sp-single-text">' + escHtml(firstName || 'Select symbol...') + '</span>' +
                            '<span class="vnx-search-arrow">▾</span>' +
                        '</div>' +
                    '</div>' +
                    (desc ? '<p class="vnx-field-desc">' + desc + '</p>' : '');
            }

        } else if (type === 'market-data-filters') {
            // Market data filters — dynamic dropdowns based on selected asset type
            var mdfDependsOn = field.depends_on || '';
            wrap.innerHTML = '<label class="vnx-field-label">' + escHtml(field.label || key) + '</label>' +
                '<div class="vnx-market-filters" data-key="' + escAttr(field.key || key) + '" data-default=""' +
                    (mdfDependsOn ? ' data-depends-on="' + escAttr(mdfDependsOn) + '"' : '') + '>' +
                '</div>' +
                (desc ? '<p class="vnx-field-desc">' + desc + '</p>' : '');

            // Wire up: when depends_on select changes, re-render filters
            setTimeout(function() {
                var container = wrap.querySelector('.vnx-market-filters');
                if (!container) return;
                mdfRender(container, '');
                // Find the select this depends on (filter_type)
                if (mdfDependsOn) {
                    var depKey = mdfDependsOn.replace(/-/g, '_');
                    var depEl = settingsEl.querySelector('[data-key="' + depKey + '"]');
                    if (depEl) {
                        var onChange = function() {
                            var val = depEl.value || '';
                            mdfRender(container, val);
                            syncSettingsJson();
                        };
                        depEl.addEventListener('change', onChange);
                        // Initial render with current value
                        mdfRender(container, depEl.value || '');
                    }
                }
            }, 0);

        } else if (type === 'preset-builder') {
            // Preset builder — horizontal tab chips, click to select, items below
            wrap.className += ' vnx-field-full';

            // Detect mode: "column" (screener) vs "currency" (forex)
            var vfSource = (field.value_field || {}).source || '';
            var pbMode = (vfSource === 'screener_metrics') ? 'column' : 'currency';
            var pbAddLabel = pbMode === 'column' ? 'Add column...' : 'Add currency...';

            var parsedPresets = {};
            if (currentVal) {
                try { parsedPresets = JSON.parse(currentVal); } catch(e) {}
            }
            var presetNames = Object.keys(parsedPresets);

            // Parse each preset — screener uses arrays, forex uses "EXCHANGE:CUR1,CUR2"
            var presetData = [];
            presetNames.forEach(function(name) {
                var val = parsedPresets[name] || '';
                var exchange = '';
                var curs = val;
                if (/^[A-Z]{2,5}:/.test(val)) {
                    var ci = val.indexOf(':');
                    exchange = val.substring(0, ci);
                    curs = val.substring(ci + 1);
                }
                presetData.push({ name: name, exchange: exchange, currencies: curs.split(',').filter(Boolean) });
            });

            // Tab chips (horizontal)
            var tabChipsHtml = presetData.map(function(p, i) {
                var isActive = (i === 0) ? ' vnx-pb-tab-active' : '';
                var ex = p.exchange || getExchangeFieldValue();
                return '<div class="vnx-pb-tab-chip' + isActive + '" draggable="true" data-index="' + i + '">' +
                    '<span class="vnx-ss-drag">⠿</span>' +
                    '<span class="vnx-pb-tab-name">' + escHtml(p.name) + '</span>' +
                    '<span class="vnx-pb-tab-exchange">' + escHtml(ex) + '</span>' +
                    '<button type="button" class="vnx-pb-rename" title="Rename">✎</button>' +
                    '<button type="button" class="vnx-pb-tab-remove" title="Remove">×</button>' +
                '</div>';
            }).join('');

            // Active preset currencies
            var activeCurs = presetData.length ? presetData[0].items : [];
            var cursHtml = activeCurs.map(function(c) {
                return '<div class="vnx-pb-chip" draggable="true" data-value="' + escAttr(c) + '">' +
                    '<span class="vnx-ss-drag">⠿</span> ' +
                    escHtml(c) +
                    ' <button type="button" class="vnx-pb-chip-remove">×</button>' +
                '</div>';
            }).join('');

            // Store value_field API config for currency loading
            var vf = field.value_field || {};
            var pbApiAttr = vf.api ? ' data-api="' + escAttr(vf.api) + '"' : '';
            var pbApiParamsAttr = vf.api_params ? ' data-api-params=\'' + JSON.stringify(vf.api_params).replace(/'/g, '&#39;') + '\'' : '';
            var pbValueKeyAttr = vf.value_key ? ' data-value-key="' + escAttr(vf.value_key) + '"' : '';
            var pbLabelKeyAttr = vf.label_key ? ' data-label-key="' + escAttr(vf.label_key) + '"' : '';

            wrap.innerHTML = '<label class="vnx-field-label">' + escHtml(field.label || key) + '</label>' +
                '<div class="vnx-preset-builder" data-key="' + escAttr(field.key || key) + '" data-default="" data-active="0"' +
                    ' data-mode="' + escAttr(pbMode) + '"' +
                    (vfSource ? ' data-source="' + escAttr(vfSource) + '"' : '') +
                    ' data-add-label="' + escAttr(pbAddLabel) + '"' +
                    pbApiAttr + pbApiParamsAttr + pbValueKeyAttr + pbLabelKeyAttr + '>' +
                    '<div class="vnx-pb-tab-bar">' + tabChipsHtml + '</div>' +
                    '<button type="button" class="vnx-pb-add-btn">+ Add Preset</button>' +
                    '<div class="vnx-pb-body">' +
                        (presetData.length ? (
                            '<div class="vnx-pb-chips">' + cursHtml + '</div>' +
                            '<div class="vnx-pb-add-cur">' +
                                '<div class="vnx-search-trigger vnx-pb-cur-trigger"><span>' + escHtml(pbAddLabel) + '</span> <span class="vnx-search-arrow">▾</span></div>' +
                            '</div>'
                        ) : '') +
                    '</div>' +
                '</div>' +
                (desc ? '<p class="vnx-field-desc">' + desc + '</p>' : '');

        } else if (type === 'code-example') {
            // Code example with optional live output
            var code = field.code || '';
            var outputLabel = field.output_label || '';
            wrap.innerHTML = '<label class="vnx-field-label">' + escHtml(field.label || key) + '</label>' +
                '<div class="vnx-code-example">' +
                    '<pre class="vnx-code-block"><code>' + escHtml(code) + '</code></pre>' +
                    (outputLabel ? '<p class="vnx-field-desc" style="margin-top:8px">' + escHtml(outputLabel) + '</p>' +
                        '<div class="vnx-code-output" id="vnx-output-' + escAttr(key) + '"><span style="color:#a7aaad;font-style:italic">Waiting for selection...</span></div>' : '') +
                '</div>';

        } else {
            // Text input — with optional token buttons
            var tokens = field.tokens || [];
            var tokensHtml = '';
            if (tokens.length) {
                tokensHtml = '<div class="vnx-token-btns">' +
                    tokens.map(function(t) {
                        return '<button type="button" class="vnx-token-btn" data-token="' + escAttr(t) + '">' + escHtml(t) + '</button>';
                    }).join('') + '</div>';
            }
            wrap.innerHTML = '<label class="vnx-field-label">' + escHtml(field.label || key) + '</label>' +
                '<input type="text" data-key="' + escAttr(field.key || key) + '" data-default="' + escAttr(defaultVal) + '" value="' + escAttr(currentVal) + '" placeholder="' + escAttr(field.placeholder || '') + '">' +
                tokensHtml +
                (desc ? '<p class="vnx-field-desc">' + desc + '</p>' : '');
        }

        // On change → sync hidden JSON + auto-apply preview
        setTimeout(function() {
            wrap.querySelectorAll('input, select, textarea').forEach(function(input) {
                input.addEventListener(input.type === 'checkbox' ? 'change' : 'input', function() {
                    syncSettingsJson();
                });
            });
        }, 0);

        return wrap;
    }


    // ================================================================
    // SYMBOL PICKER — search modal + add/remove/reorder symbols
    // ================================================================

    var _spActiveField = null;      // Which picker field is currently active
    var _spWidgetEl = null;          // The vunelix-symbol-search custom element
    var _spContainer = null;         // Host container element
    var _spConfigKey = null;         // Serialized config to detect changes
    var _spScriptLoaded = false;     // CDN script loaded flag

    /**
     * Load widget JS from CDN (once)
     */
    function spLoadScript() {
        if (_spScriptLoaded) return;
        _spScriptLoaded = true;
        if (!document.querySelector('script[src*="vunelix-symbol-search.js"]')) {
            var script = document.createElement('script');
            script.type = 'module';
            script.src = cdnUrl + 'widgets/vunelix-symbol-search.js' + cacheBust;
            document.head.appendChild(script);
        }
    }

    /**
     * Read search config from a picker DOM element's data attributes.
     * Maps picker's search_type/search_exchange to widget data-* attributes.
     */
    function spReadPickerConfig(pickerEl) {
        var searchType = pickerEl ? (pickerEl.dataset.searchType || '') : '';
        var searchExchange = pickerEl ? (pickerEl.dataset.searchExchange || '') : '';
        // Hide filter tabs + set default exchange only when search_exchange is specified
        // (e.g. crypto-market-movers has search_exchange="crypto", but forex/stock do not)
        var showFilters = !searchExchange;
        return {
            filterType: searchType,
            defaultExchange: searchExchange,
            showFilters: showFilters
        };
    }

    /**
     * Create/recreate the symbol search widget with correct search config.
     * Called on widget type change — destroys old widget, creates fresh one
     * so MenuSearch initializes with correct filters from the start.
     *
     * e.g. crypto-market-movers → filter_type=crypto, default_exchange=crypto, show_filters=false
     *      ticker (all assets)  → no filter, show_filters=true
     */
    function spLoadWidget(cfg) {
        var key = (cfg.filterType || '') + '|' + (cfg.defaultExchange || '') + '|' + cfg.showFilters;

        // Skip if already loaded with same config
        if (_spWidgetEl && _spConfigKey === key) return;

        // Load CDN script (once)
        spLoadScript();

        // Create host container (once)
        if (!_spContainer) {
            _spContainer = document.createElement('div');
            _spContainer.className = 'vnx-sp-widget-host';
            document.body.appendChild(_spContainer);
        }

        // Destroy old widget — removes shadow DOM, MenuSearch modal, everything
        if (_spWidgetEl) {
            _spWidgetEl.remove();
            _spWidgetEl = null;
        }

        // Create fresh widget with correct config baked in
        _spWidgetEl = document.createElement('vunelix-symbol-search');
        _spWidgetEl.setAttribute('data-enable-return-data', 'true');
        _spWidgetEl.setAttribute('data-enable-overview-link', 'false');
        _spWidgetEl.setAttribute('data-enable-chart-link', 'false');
        _spWidgetEl.setAttribute('data-enable-storage', 'false');
        _spWidgetEl.setAttribute('data-show-filters', cfg.showFilters ? 'true' : 'false');
        if (cfg.filterType) _spWidgetEl.setAttribute('data-filter-type', cfg.filterType);
        if (cfg.defaultExchange) _spWidgetEl.setAttribute('data-default-exchange', cfg.defaultExchange);
        _spContainer.appendChild(_spWidgetEl);
        _spConfigKey = key;

        // Listen for symbol select event on new widget
        _spWidgetEl.addEventListener('vunelix-symbol-select', function(e) {
            if (!_spActiveField) return;
            var detail = e.detail || {};
            var ticker = detail.ticker || '';
            if (!ticker && detail.symbol) {
                ticker = (detail.exchange ? detail.exchange + ':' : '') + detail.symbol;
            }
            if (!ticker) return;
            spAddSymbol(_spActiveField, ticker, detail);
            _spActiveField = null;
        });
    }

    /**
     * Detect search config from first symbol-picker in current widget settings.
     * Called after renderSettings so DOM has the picker elements.
     */
    function spDetectAndLoad() {
        var picker = settingsEl.querySelector('.vnx-symbol-picker');
        if (picker) {
            spLoadWidget(spReadPickerConfig(picker));
        }
    }

    /**
     * Open symbol search — clicks the widget's demo trigger to open MenuSearch modal
     */
    function spOpenModal(fieldKey) {
        _spActiveField = fieldKey;

        // Ensure widget is loaded with correct config for this picker
        var picker = settingsEl.querySelector('.vnx-symbol-picker[data-key="' + fieldKey + '"]');
        spLoadWidget(spReadPickerConfig(picker));

        // Click the widget's demo search bar to open MenuSearch modal
        var tryOpen = function(attempts) {
            if (attempts <= 0) return;
            var shadow = _spWidgetEl.shadowRoot;
            if (shadow) {
                var demoTrigger = shadow.querySelector('.vnx-sw-demo-trigger');
                if (demoTrigger) {
                    demoTrigger.click();
                    return;
                }
            }
            // Widget not ready yet — retry
            setTimeout(function() { tryOpen(attempts - 1); }, 200);
        };
        tryOpen(15);
    }

    /**
     * Add symbol to picker list
     */
    function spAddSymbol(fieldKey, ticker, detail) {
        var picker = settingsEl.querySelector('.vnx-symbol-picker[data-key="' + fieldKey + '"]');
        if (!picker) return;

        // Single mode
        if (!picker.dataset.multiple) {
            var trigger = picker.querySelector('.vnx-sp-single-trigger');
            if (!trigger) return;
            trigger.dataset.ticker = ticker;
            var symbol = detail.symbol || ticker.split(':').pop() || ticker;
            var name = detail.name || '';
            trigger.querySelector('.vnx-sp-single-text').textContent = symbol + (name ? ' — ' + name : '');
            syncSettingsJson();
            return;
        }

        // Multiple mode — duplicate check + max limit
        var list = picker.querySelector('.vnx-sp-list');
        if (!list) return;
        if (list.querySelector('[data-ticker="' + ticker + '"]')) return;

        var maxItems = parseInt(picker.dataset.max) || 0;
        if (maxItems > 0 && list.querySelectorAll('.vnx-sp-item').length >= maxItems) return;

        var parts = ticker.split(':');
        var symExchange = parts.length === 2 ? parts[0] : '';
        var symName = parts.length === 2 ? parts[1] : ticker;

        var logoUrl = detail.logo || '';
        var displaySymbol = detail.symbol || symName;
        var displayExchange = (detail.exchange || symExchange).toUpperCase();

        var div = document.createElement('div');
        div.className = 'vnx-sp-item';
        div.draggable = true;
        div.dataset.ticker = ticker;
        div.innerHTML =
            '<span class="vnx-sp-drag">⠿</span>' +
            (logoUrl ? '<img class="vnx-sp-logo" src="' + escAttr(logoUrl) + '" alt="" onerror="this.style.display=\'none\'">' : '') +
            '<span class="vnx-sp-symbol">' + escHtml(displaySymbol) + '</span>' +
            (displayExchange ? '<span class="vnx-sp-exchange">' + escHtml(displayExchange) + '</span>' : '') +
            '<button type="button" class="vnx-sp-remove" title="Remove">×</button>';

        list.appendChild(div);
        spToggleAddBtn(picker, maxItems);
        syncSettingsJson();
    }

    function spToggleAddBtn(picker, maxItems) {
        if (!maxItems) return;
        var addBtn = picker.querySelector('.vnx-sp-add-btn');
        var count = picker.querySelectorAll('.vnx-sp-item').length;
        if (addBtn) addBtn.style.display = count >= maxItems ? 'none' : '';
    }

    // Open modal — delegated click on add button or single trigger
    document.addEventListener('click', function(e) {
        var btn = e.target.closest('.vnx-sp-add-btn') || e.target.closest('.vnx-sp-single-trigger');
        if (!btn) return;
        var fieldKey = btn.dataset.field;
        if (fieldKey) spOpenModal(fieldKey);
    });

    // Remove symbol — delegated
    document.addEventListener('click', function(e) {
        var removeBtn = e.target.closest('.vnx-sp-remove');
        if (!removeBtn) return;
        var item = removeBtn.closest('.vnx-sp-item');
        if (!item) return;
        var picker = item.closest('.vnx-symbol-picker');
        item.remove();
        if (picker) spToggleAddBtn(picker, parseInt(picker.dataset.max) || 0);
        syncSettingsJson();
    });

    // Drag-to-reorder symbols
    (function() {
        var dragItem = null;
        document.addEventListener('dragstart', function(e) {
            dragItem = e.target.closest('.vnx-sp-item');
            if (!dragItem) return;
            dragItem.classList.add('vnx-sp-dragging');
            e.dataTransfer.effectAllowed = 'move';
        });
        document.addEventListener('dragend', function() {
            if (dragItem) dragItem.classList.remove('vnx-sp-dragging');
            dragItem = null;
            syncSettingsJson();
        });
        document.addEventListener('dragover', function(e) {
            if (!dragItem) return;
            var list = dragItem.closest('.vnx-sp-list');
            if (!list) return;
            var target = e.target.closest('.vnx-sp-item');
            if (!target || target === dragItem || !list.contains(target)) return;
            e.preventDefault();
            e.dataTransfer.dropEffect = 'move';
            var items = Array.from(list.querySelectorAll('.vnx-sp-item'));
            var dragIdx = items.indexOf(dragItem);
            var targetIdx = items.indexOf(target);
            if (dragIdx < targetIdx) target.after(dragItem);
            else target.before(dragItem);
        });
    })();


    // ================================================================
    // MARKET DATA FILTERS — dynamic dropdowns per asset type
    // ================================================================

    var MDF_LABELS = {
        exchange: { all: 'All Exchanges', label: 'Exchange' },
        country:  { all: 'All Countries', label: 'Country' },
        sub_type: { all: 'All Types', label: 'Type' },
        sector:   { all: 'All Sectors', label: 'Sector' },
    };

    /**
     * Render filter dropdowns inside a .vnx-market-filters container
     * based on the selected asset type (crypto, stock, forex, etc.)
     */
    function mdfRender(container, assetType) {
        container.innerHTML = '';
        if (!assetType) return;

        var marketData = MARKET_DATA[assetType];
        if (!marketData) return;

        var html = '';
        Object.keys(marketData).forEach(function(prop) {
            var data = marketData[prop];
            if (!data || typeof data !== 'object') return;

            var info = MDF_LABELS[prop] || { all: 'All', label: prop };
            var isCountry = (prop === 'country');

            // Normalize data to array of {value, label}
            var items = [];
            if (Array.isArray(data)) {
                data.forEach(function(d) { items.push({ value: d.value, label: d.label }); });
            } else {
                Object.keys(data).forEach(function(k) {
                    var v = data[k];
                    if (typeof v === 'string') items.push({ value: k, label: v });
                    else if (v && v.label) items.push({ value: v.value || k, label: v.label });
                    else if (v && v.name) items.push({ value: k, label: v.name });
                });
            }
            if (!items.length) return;

            var isSearchable = items.length > 7;

            if (isSearchable) {
                // Searchable custom dropdown (same pattern as plugin's searchable selects)
                var iconBase = 'https://vunelix.com/assets/icons/';
                var iconType = isCountry ? 'country' : '';

                var searchOptsHtml = '<div class="vnx-search-option selected" data-value="" data-search="all">' + escHtml(info.all) + '</div>';
                items.forEach(function(item) {
                    var flagHtml = iconType ? '<img src="' + iconBase + escAttr(iconType) + '/' + escAttr(item.value) + '.svg" class="vnx-flag-icon" onerror="this.style.display=\'none\'" alt=""> ' : '';
                    searchOptsHtml += '<div class="vnx-search-option" data-value="' + escAttr(item.value) + '" data-search="' + escAttr((item.value + ' ' + item.label).toLowerCase()) + '">' +
                        flagHtml + escHtml(item.label) + '</div>';
                });

                var hiddenOpts = '<option value="" selected>' + escHtml(info.all) + '</option>';
                items.forEach(function(item) {
                    hiddenOpts += '<option value="' + escAttr(item.value) + '">' + escHtml(item.label) + '</option>';
                });

                html += '<div class="vnx-mdf-item">' +
                    '<label class="vnx-mdf-label">' + escHtml(info.label) + '</label>' +
                    '<div class="vnx-searchable-wrap vnx-mdf-searchable" data-filter-prop="' + escAttr(prop) + '">' +
                        '<select data-filter-prop="' + escAttr(prop) + '" style="display:none">' + hiddenOpts + '</select>' +
                        '<div class="vnx-search-trigger" tabindex="0">' +
                            '<span class="vnx-search-label">' + escHtml(info.all) + '</span>' +
                            '<span class="vnx-search-arrow">▾</span>' +
                        '</div>' +
                        '<div class="vnx-search-dropdown">' +
                            '<input type="text" class="vnx-search-input" placeholder="Search..." autocomplete="off">' +
                            '<div class="vnx-search-list">' + searchOptsHtml + '</div>' +
                        '</div>' +
                    '</div>' +
                '</div>';
            } else {
                // Plain select for small lists
                var optionsHtml = '<option value="">' + escHtml(info.all) + '</option>';
                items.forEach(function(item) {
                    optionsHtml += '<option value="' + escAttr(item.value) + '">' + escHtml(item.label) + '</option>';
                });

                html += '<div class="vnx-mdf-item">' +
                    '<label class="vnx-mdf-label">' + escHtml(info.label) + '</label>' +
                    '<select data-filter-prop="' + escAttr(prop) + '">' +
                        optionsHtml +
                    '</select>' +
                '</div>';
            }
        });

        if (!html) return;
        container.innerHTML = '<div class="vnx-mdf-row">' + html + '</div>';

        // On change → sync
        container.querySelectorAll('select').forEach(function(sel) {
            sel.addEventListener('change', function() { syncSettingsJson(); });
        });
    }


    // ================================================================
    // SYNC SETTINGS → HIDDEN JSON FIELD (saved on Publish/Update)
    // ================================================================

    function syncSettingsJson() {
        if (!settingsJson) return;
        var data = {};

        settingsEl.querySelectorAll('[data-key]').forEach(function(el) {
            var key = el.dataset.key;
            var def = el.dataset.default || '';
            var val;

            if (el.classList.contains('vnx-market-filters')) {
                // Each dropdown becomes its own key: default_exchange, default_country, etc.
                el.querySelectorAll('select[data-filter-prop]').forEach(function(sel) {
                    if (sel.value) data['default_' + sel.dataset.filterProp] = sel.value;
                });
                return; // Skip default key assignment
            } else if (el.classList.contains('vnx-symbol-picker')) {
                if (el.dataset.multiple) {
                    var tickers = [];
                    el.querySelectorAll('.vnx-sp-item').forEach(function(item) { tickers.push(item.dataset.ticker); });
                    val = tickers.join(',');
                } else {
                    var trigger = el.querySelector('.vnx-sp-single-trigger');
                    val = trigger ? (trigger.dataset.ticker || '') : '';
                }
            } else if (el.classList.contains('vnx-sortable-select')) {
                var items = [];
                el.querySelectorAll('.vnx-ss-item').forEach(function(item) { items.push(item.dataset.value); });
                val = items.join(',');
            } else if (el.classList.contains('vnx-ajax-select')) {
                val = el.dataset.value || '';
            } else if (el.classList.contains('vnx-filter-builder') || el.classList.contains('vnx-preset-builder')) {
                // Synced via their own functions (fbSyncJson / pbSyncJson)
                var h = el.querySelector('input[type="hidden"]');
                val = h ? h.value : '';
            } else if (el.type === 'checkbox') {
                val = el.checked ? 'true' : 'false';
            } else if (el.classList.contains('vnx-color-pair')) {
                var l = el.querySelector('.vnx-cp-light');
                var d = el.querySelector('.vnx-cp-dark');
                var lv = l ? l.value : '', dv = d ? d.value : '';
                var ld = el.dataset.lightDefault || '', dd = el.dataset.darkDefault || '';
                if (lv !== ld || dv !== dd) val = lv + ',' + dv;
                else return;
            } else {
                val = el.value;
            }

            if (val !== '' && val !== undefined) {
                data[key] = val;
            }
        });

        settingsJson.value = JSON.stringify(data);
    }


    // ================================================================
    // LIVE PREVIEW
    // ================================================================

    var applyBtnBottom = document.querySelector('.vnx-apply-bottom-btn');
    var applyBtnBottomWrap = document.getElementById('vnxApplyBtnBottom');

    function handleApplyClick() {
        applyToPreview();
        // Scroll to preview so user can see changes
        if (previewBox) previewBox.scrollIntoView({ behavior: 'smooth', block: 'start' });
    }

    if (applyBtn) {
        applyBtn.addEventListener('click', handleApplyClick);
    }
    if (applyBtnBottom) {
        applyBtnBottom.addEventListener('click', handleApplyClick);
    }

    function applyToPreview() {
        var slug = typeSelect.value;
        var widget = widgets[slug];
        if (!widget || !previewBox) return;

        if (previewPlaceholder) previewPlaceholder.style.display = 'none';

        var component = widget.component;
        var jsFile = widget.js;

        // Collect ALL current values
        var attrs = {};
        settingsEl.querySelectorAll('[data-key]').forEach(function(el) {
            var key = el.dataset.key;
            var val;
            if (el.classList.contains('vnx-market-filters')) {
                el.querySelectorAll('select[data-filter-prop]').forEach(function(sel) {
                    if (sel.value) attrs['default_' + sel.dataset.filterProp] = sel.value;
                });
                return;
            } else if (el.classList.contains('vnx-symbol-picker')) {
                if (el.dataset.multiple) {
                    var tickers = [];
                    el.querySelectorAll('.vnx-sp-item').forEach(function(item) { tickers.push(item.dataset.ticker); });
                    val = tickers.join(',');
                } else {
                    var trigger = el.querySelector('.vnx-sp-single-trigger');
                    val = trigger ? (trigger.dataset.ticker || '') : '';
                }
            } else if (el.classList.contains('vnx-sortable-select')) {
                var items = [];
                el.querySelectorAll('.vnx-ss-item').forEach(function(item) { items.push(item.dataset.value); });
                val = items.join(',');
            } else if (el.classList.contains('vnx-ajax-select')) {
                val = el.dataset.value || '';
            } else if (el.type === 'checkbox') {
                val = el.checked ? 'true' : 'false';
            } else if (el.classList.contains('vnx-color-pair')) {
                var l = el.querySelector('.vnx-cp-light'), d = el.querySelector('.vnx-cp-dark');
                val = (l ? l.value : '') + ',' + (d ? d.value : '');
            } else {
                val = el.value;
            }
            if (val) attrs[key] = val;
        });

        // Clear preview completely — remove ALL child elements
        previewBox.innerHTML = '';

        var el = document.createElement(component);
        el.id = 'vnx-widget-preview';
        Object.keys(attrs).forEach(function(k) {
            el.setAttribute('data-' + k.replace(/_/g, '-'), attrs[k]);
        });
        previewBox.appendChild(el);

        // Load JS module
        if (!loadedScripts[jsFile]) {
            loadedScripts[jsFile] = true;
            var script = document.createElement('script');
            script.type = 'module';
            script.src = cdnUrl + jsFile + cacheBust;
            document.head.appendChild(script);
        }
    }


    // ================================================================
    // COPY SHORTCODE
    // ================================================================

    if (copyBtn && shortcodeField) {
        copyBtn.addEventListener('click', function() {
            shortcodeField.select();
            navigator.clipboard.writeText(shortcodeField.value).then(function() {
                copyBtn.textContent = 'Copied!';
                setTimeout(function() { copyBtn.textContent = 'Copy'; }, 1500);
            });
        });
    }


    // ================================================================
    // RETURN DATA LIVE OUTPUT — show selected symbol data in code output
    // ================================================================

    document.addEventListener('vunelix-symbol-select', function(e) {
        var output = document.getElementById('vnx-output-return_data_example');
        if (output && e.detail) {
            output.textContent = JSON.stringify(e.detail, null, 2);
            output.style.fontStyle = '';
            output.style.color = '';
        }
    });


    // ================================================================
    // THEME → COLOR PAIR VISIBILITY (same as site)
    // ================================================================

    function updateColorPairVisibility() {
        var themeEl = settingsEl.querySelector('[data-key="theme"]');
        if (!themeEl) return;
        var theme = themeEl.value || 'auto';
        // For searchable-select, check the hidden select
        if (!theme || theme === 'auto') {
            var wrap = themeEl.closest('.vnx-searchable-wrap');
            if (wrap) {
                var sel = wrap.querySelector('select');
                if (sel) theme = sel.value || 'auto';
            }
        }

        settingsEl.querySelectorAll('.vnx-color-pair').forEach(function(pair) {
            var lightItems = pair.querySelectorAll('.vnx-color-item:first-child');
            var darkItems = pair.querySelectorAll('.vnx-color-item:last-child');
            lightItems.forEach(function(el) { el.style.display = (theme === 'dark') ? 'none' : ''; });
            darkItems.forEach(function(el) { el.style.display = (theme === 'light') ? 'none' : ''; });
        });
    }

    // Watch theme changes
    document.addEventListener('input', function(e) {
        var el = e.target.closest('[data-key="theme"]');
        if (el) setTimeout(updateColorPairVisibility, 50);
    });
    document.addEventListener('click', function(e) {
        // For searchable-select theme option click
        var opt = e.target.closest('.vnx-searchable-wrap .vnx-search-option');
        if (opt) {
            var wrap = opt.closest('.vnx-searchable-wrap');
            var sel = wrap ? wrap.querySelector('select[data-key="theme"]') : null;
            if (sel) setTimeout(updateColorPairVisibility, 50);
        }
    });

    // Run on initial load
    setTimeout(updateColorPairVisibility, 200);


    // ================================================================
    // COLOR SYNC
    // ================================================================

    document.addEventListener('input', function(e) {
        if (e.target.classList.contains('vnx-cp-light-picker')) {
            var t = e.target.closest('.vnx-color-item').querySelector('.vnx-cp-light');
            if (t) { t.value = e.target.value; syncSettingsJson(); }
        }
        if (e.target.classList.contains('vnx-cp-dark-picker')) {
            var t = e.target.closest('.vnx-color-item').querySelector('.vnx-cp-dark');
            if (t) { t.value = e.target.value; syncSettingsJson(); }
        }
        if (e.target.classList.contains('vnx-cp-light')) {
            var p = e.target.closest('.vnx-color-item').querySelector('.vnx-cp-light-picker');
            if (p && /^#[0-9a-f]{6}$/i.test(e.target.value)) { p.value = e.target.value; }
            syncSettingsJson();
        }
        if (e.target.classList.contains('vnx-cp-dark')) {
            var p = e.target.closest('.vnx-color-item').querySelector('.vnx-cp-dark-picker');
            if (p && /^#[0-9a-f]{6}$/i.test(e.target.value)) { p.value = e.target.value; }
            syncSettingsJson();
        }
    });


    // ================================================================
    // HELPERS
    // ================================================================

    // ================================================================
    // SORTABLE SELECT — dropdown, add, remove, drag reorder
    // ================================================================

    document.addEventListener('click', function(e) {
        // Toggle dropdown
        var trigger = e.target.closest('.vnx-ss-trigger');
        if (trigger) {
            var menu = trigger.nextElementSibling;
            var isOpen = menu.style.display !== 'none';
            // Close all other menus first
            document.querySelectorAll('.vnx-ss-menu').forEach(function(m) { m.style.display = 'none'; });
            menu.style.display = isOpen ? 'none' : '';
            return;
        }

        // Add item from dropdown
        var option = e.target.closest('.vnx-ss-option:not(.disabled)');
        if (option) {
            var ss = option.closest('.vnx-sortable-select');
            var items = ss.querySelector('.vnx-ss-items');
            var max = parseInt(ss.dataset.max) || 99;
            var opts = JSON.parse(ss.dataset.options || '{}');
            var val = option.dataset.value;

            if (items.querySelectorAll('.vnx-ss-item').length >= max) return;

            var chip = document.createElement('div');
            chip.className = 'vnx-ss-item';
            chip.draggable = true;
            chip.dataset.value = val;
            chip.innerHTML = '<span class="vnx-ss-drag">⠿</span><span class="vnx-ss-label">' + escHtml(opts[val] || val) + '</span><button type="button" class="vnx-ss-remove" title="Remove">×</button>';
            items.appendChild(chip);

            option.classList.add('disabled');
            option.closest('.vnx-ss-menu').style.display = 'none';

            // Hide trigger if max reached
            if (items.querySelectorAll('.vnx-ss-item').length >= max) {
                ss.querySelector('.vnx-ss-dropdown').style.display = 'none';
            }

            syncSettingsJson();
            return;
        }

        // Remove item
        var removeBtn = e.target.closest('.vnx-ss-remove');
        if (removeBtn) {
            var item = removeBtn.closest('.vnx-ss-item');
            var ss = item.closest('.vnx-sortable-select');
            var val = item.dataset.value;
            item.remove();

            // Re-enable in dropdown
            var opt = ss.querySelector('.vnx-ss-option[data-value="' + val + '"]');
            if (opt) opt.classList.remove('disabled');

            // Show trigger again
            ss.querySelector('.vnx-ss-dropdown').style.display = '';

            syncSettingsJson();
            return;
        }

        // Close dropdowns on outside click
        if (!e.target.closest('.vnx-ss-dropdown')) {
            document.querySelectorAll('.vnx-ss-menu').forEach(function(m) { m.style.display = 'none'; });
        }
    });

    // Drag reorder for sortable-select items
    (function() {
        var dragItem = null;
        document.addEventListener('dragstart', function(e) {
            if (!e.target.classList.contains('vnx-ss-item')) return;
            dragItem = e.target;
            dragItem.classList.add('vnx-ss-dragging');
            e.dataTransfer.effectAllowed = 'move';
        });
        document.addEventListener('dragend', function() {
            if (dragItem) dragItem.classList.remove('vnx-ss-dragging');
            dragItem = null;
            syncSettingsJson();
        });
        document.addEventListener('dragover', function(e) {
            if (!dragItem) return;
            e.preventDefault();
            var target = e.target.closest('.vnx-ss-item');
            if (!target || target === dragItem) return;
            var items = Array.from(target.parentNode.querySelectorAll('.vnx-ss-item'));
            if (items.indexOf(dragItem) < items.indexOf(target)) {
                target.after(dragItem);
            } else {
                target.before(dragItem);
            }
        });
    })();

    // ================================================================
    // TOKEN BUTTONS — click to insert {{token}} into text input
    // ================================================================

    document.addEventListener('click', function(e) {
        var tokenBtn = e.target.closest('.vnx-token-btn');
        if (!tokenBtn) return;
        var token = tokenBtn.dataset.token;
        var input = tokenBtn.closest('.vnx-field').querySelector('input[type="text"]');
        if (!input) return;

        var cursorPos = input.selectionStart || input.value.length;
        var before = input.value.substring(0, cursorPos);
        var after = input.value.substring(cursorPos);
        input.value = before + '{{' + token + '}}' + after;
        input.focus();
        var newPos = cursorPos + token.length + 4;
        input.setSelectionRange(newPos, newPos);
        input.dispatchEvent(new Event('input', { bubbles: true }));
    });


    // ================================================================
    // PRESET BUILDER — horizontal tab chips, click to activate, currencies below
    // ================================================================

    // Helper: get current Exchange select value from Data section
    function getExchangeFieldValue() {
        var exEl = settingsEl.querySelector('[data-key="exchange"]');
        if (exEl) return exEl.value || 'FX';
        // Check searchable select hidden select
        var wrap = settingsEl.querySelector('.vnx-searchable-wrap select[data-key="exchange"]');
        return wrap ? wrap.value || 'FX' : 'FX';
    }

    // Rebuild the body (currencies) area for the active tab
    function pbRenderBody(pb) {
        var activeIdx = parseInt(pb.dataset.active) || 0;
        var tabs = pb.querySelectorAll('.vnx-pb-tab-chip');
        var body = pb.querySelector('.vnx-pb-body');

        if (!tabs.length || activeIdx >= tabs.length) {
            body.innerHTML = '';
            return;
        }

        // Get preset data from pbGetData
        var data = pbGetAllData(pb);
        var preset = data[activeIdx];
        if (!preset) { body.innerHTML = ''; return; }

        var addLabel = pb.dataset.addLabel || 'Add currency...';
        var isColumnMode = pb.dataset.mode === 'column';
        var sourceData = isColumnMode ? resolveSource(pb.dataset.source || 'screener_metrics') : {};

        var itemsHtml = (preset.items || []).map(function(c) {
            // For column mode, show label from source data
            var displayText = (isColumnMode && sourceData[c]) ? sourceData[c] : c;
            return '<div class="vnx-pb-chip" draggable="true" data-value="' + escAttr(c) + '">' +
                '<span class="vnx-ss-drag">⠿</span> ' + escHtml(displayText) +
                ' <button type="button" class="vnx-pb-chip-remove">×</button></div>';
        }).join('');

        body.innerHTML =
            '<div class="vnx-pb-chips">' + itemsHtml + '</div>' +
            '<div class="vnx-pb-add-cur">' +
                '<div class="vnx-search-trigger vnx-pb-cur-trigger"><span>' + escHtml(addLabel) + '</span> <span class="vnx-search-arrow">▾</span></div>' +
            '</div>';
    }

    // Get all preset data from tab chips + stored currencies
    // Store currencies in a Map on the pb element
    function pbGetAllData(pb) {
        if (!pb._presets) pb._presets = [];
        return pb._presets;
    }

    function pbInitData(pb) {
        var key = pb.dataset.key;
        var hidden = pb.querySelector('input[type="hidden"][data-key="' + key + '"]');
        if (!hidden) {
            hidden = document.createElement('input');
            hidden.type = 'hidden';
            hidden.dataset.key = key;
            hidden.dataset.default = '';
            pb.appendChild(hidden);
        }
        var val = hidden.value || '';
        var parsed = {};
        if (val) { try { parsed = JSON.parse(val); } catch(e) {} }

        pb._presets = [];
        Object.keys(parsed).forEach(function(name) {
            var v = parsed[name];
            var exchange = '';
            var items = [];
            if (Array.isArray(v)) {
                // Screener format: ["col1","col2"]
                items = v;
            } else {
                // Forex format: "FX:EUR,USD" or "EUR,USD"
                v = String(v || '');
                if (/^[A-Z]{2,5}:/.test(v)) {
                    var ci = v.indexOf(':');
                    exchange = v.substring(0, ci);
                    v = v.substring(ci + 1);
                }
                items = v.split(',').filter(Boolean);
            }
            pb._presets.push({ name: name, exchange: exchange || getExchangeFieldValue(), items: items });
        });
    }

    function pbSyncJson(pb) {
        var data = pbGetAllData(pb);
        var isColumnMode = pb.dataset.mode === 'column';
        var result = {};
        data.forEach(function(p) {
            if (p.name && p.items.length) {
                if (isColumnMode) {
                    // Screener: {"presetName": ["col1","col2"]}
                    result[p.name] = p.items;
                } else {
                    // Forex: {"groupName": "FX:EUR,USD"}
                    result[p.name] = p.exchange ? p.exchange + ':' + p.items.join(',') : p.items.join(',');
                }
            }
        });
        var key = pb.dataset.key;
        var hidden = pb.querySelector('input[type="hidden"][data-key="' + key + '"]');
        if (!hidden) {
            hidden = document.createElement('input');
            hidden.type = 'hidden';
            hidden.dataset.key = key;
            hidden.dataset.default = '';
            pb.appendChild(hidden);
        }
        hidden.value = Object.keys(result).length ? JSON.stringify(result) : '';
        syncSettingsJson();
    }

    function pbRenderTabs(pb) {
        var data = pbGetAllData(pb);
        var activeIdx = parseInt(pb.dataset.active) || 0;
        var isColumnMode = pb.dataset.mode === 'column';
        var bar = pb.querySelector('.vnx-pb-tab-bar');
        bar.innerHTML = data.map(function(p, i) {
            var isActive = (i === activeIdx) ? ' vnx-pb-tab-active' : '';
            var exHtml = (!isColumnMode && p.exchange) ? '<span class="vnx-pb-tab-exchange">' + escHtml(p.exchange) + '</span>' : '';
            return '<div class="vnx-pb-tab-chip' + isActive + '" draggable="true" data-index="' + i + '">' +
                '<span class="vnx-ss-drag">⠿</span>' +
                '<span class="vnx-pb-tab-name">' + escHtml(p.name) + '</span>' +
                exHtml +
                '<button type="button" class="vnx-pb-rename" title="Rename">✎</button>' +
                '<button type="button" class="vnx-pb-tab-remove" title="Remove">×</button>' +
            '</div>';
        }).join('');
    }

    // Init all preset builders on page
    function pbInitAll() {
        document.querySelectorAll('.vnx-preset-builder').forEach(function(pb) {
            if (pb._inited) return;
            pb._inited = true;
            pbInitData(pb);
        });
    }
    setTimeout(pbInitAll, 100);

    document.addEventListener('click', function(e) {
        // Click tab to activate
        var tabChip = e.target.closest('.vnx-pb-tab-chip');
        if (tabChip && !e.target.closest('.vnx-pb-rename') && !e.target.closest('.vnx-pb-tab-remove')) {
            var pb = tabChip.closest('.vnx-preset-builder');
            var idx = parseInt(tabChip.dataset.index);
            pb.dataset.active = idx;
            pb.querySelectorAll('.vnx-pb-tab-chip').forEach(function(t, i) {
                t.classList.toggle('vnx-pb-tab-active', i === idx);
            });
            pbRenderBody(pb);
            return;
        }

        // Rename preset
        var renameBtn = e.target.closest('.vnx-pb-rename');
        if (renameBtn) {
            var tabChip = renameBtn.closest('.vnx-pb-tab-chip');
            var pb = tabChip.closest('.vnx-preset-builder');
            var idx = parseInt(tabChip.dataset.index);
            var data = pbGetAllData(pb);
            var newName = prompt('Preset name:', data[idx].name);
            if (!newName || !newName.trim()) return;
            data[idx].name = newName.trim();
            pbRenderTabs(pb);
            pbSyncJson(pb);
            return;
        }

        // Add new preset
        var addBtn = e.target.closest('.vnx-pb-add-btn');
        if (addBtn) {
            var pb = addBtn.closest('.vnx-preset-builder');
            if (!pb._inited) { pb._inited = true; pbInitData(pb); }
            var name = prompt('Preset name:');
            if (!name || !name.trim()) return;
            var data = pbGetAllData(pb);
            var isCol = pb.dataset.mode === 'column';
            var exchange = isCol ? '' : getExchangeFieldValue();
            data.push({ name: name.trim(), exchange: exchange, items: [] });
            pb.dataset.active = data.length - 1;
            pbRenderTabs(pb);
            pbRenderBody(pb);
            pbSyncJson(pb);
            return;
        }

        // Remove preset tab
        var removeTab = e.target.closest('.vnx-pb-tab-remove');
        if (removeTab) {
            var tabChip = removeTab.closest('.vnx-pb-tab-chip');
            var pb = tabChip.closest('.vnx-preset-builder');
            var idx = parseInt(tabChip.dataset.index);
            var data = pbGetAllData(pb);
            data.splice(idx, 1);
            var active = parseInt(pb.dataset.active) || 0;
            if (active >= data.length) pb.dataset.active = Math.max(0, data.length - 1);
            pbRenderTabs(pb);
            pbRenderBody(pb);
            pbSyncJson(pb);
            return;
        }

        // Remove currency chip
        var removeChip = e.target.closest('.vnx-pb-chip-remove');
        if (removeChip) {
            var chip = removeChip.closest('.vnx-pb-chip');
            var pb = chip.closest('.vnx-preset-builder');
            var activeIdx = parseInt(pb.dataset.active) || 0;
            var data = pbGetAllData(pb);
            var code = chip.dataset.value;
            data[activeIdx].items = data[activeIdx].items.filter(function(c) { return c !== code; });
            pbRenderBody(pb);
            pbSyncJson(pb);
            return;
        }

        // Open add item dropdown (API-based for currency, static for columns)
        var curTrigger = e.target.closest('.vnx-pb-cur-trigger');
        if (curTrigger) {
            e.stopPropagation();
            var pb = curTrigger.closest('.vnx-preset-builder');
            if (!pb._inited) { pb._inited = true; pbInitData(pb); }
            var activeIdx = parseInt(pb.dataset.active) || 0;
            var data = pbGetAllData(pb);
            if (!data[activeIdx]) return;
            var existing = data[activeIdx].items || [];
            var isColumnMode = pb.dataset.mode === 'column';

            var dropdown = pb.querySelector('.vnx-pb-cur-dropdown');
            if (dropdown) { dropdown.remove(); return; }

            var searchPlaceholder = isColumnMode ? 'Search column...' : 'Search currency...';

            // Column mode — use static source directly (no API call)
            if (isColumnMode) {
                var sourceData = resolveSource(pb.dataset.source || 'screener_metrics');
                var html = '<div class="vnx-pb-cur-dropdown vnx-search-dropdown open">' +
                    '<input type="text" class="vnx-search-input vnx-pb-cur-search" placeholder="' + escAttr(searchPlaceholder) + '" autocomplete="off">' +
                    '<div class="vnx-search-list">';
                Object.keys(sourceData).forEach(function(key) {
                    var disabled = existing.indexOf(key) !== -1 ? ' disabled' : '';
                    var searchText = (key + ' ' + sourceData[key]).toLowerCase();
                    html += '<div class="vnx-search-option vnx-pb-cur-option' + disabled + '" data-value="' + escAttr(key) + '" data-search="' + escAttr(searchText) + '">' + escHtml(sourceData[key]) + '</div>';
                });
                html += '</div></div>';
                curTrigger.insertAdjacentHTML('afterend', html);
                var si = pb.querySelector('.vnx-pb-cur-search');
                if (si) si.focus();
                return;
            }

            // Currency mode — load from API
            var exchange = data[activeIdx] ? data[activeIdx].exchange : getExchangeFieldValue();
            var pbApi = pb.dataset.api || 'forex/list';
            var pbParams = {};
            try { pbParams = JSON.parse(pb.dataset.apiParams || '{}'); } catch(e) {}
            pbParams.exchange = exchange;
            var pbValueKey = pb.dataset.valueKey || 'meta.base_currency';
            var pbLabelKey = pb.dataset.labelKey || 'meta.base_currency_desc';

            var loadingHtml = '<div class="vnx-pb-cur-dropdown vnx-search-dropdown open">' +
                '<input type="text" class="vnx-search-input vnx-pb-cur-search" placeholder="' + escAttr(searchPlaceholder) + '" autocomplete="off">' +
                '<div class="vnx-search-list"><div class="vnx-ajax-loading">Loading...</div></div></div>';
            curTrigger.insertAdjacentHTML('afterend', loadingHtml);
            var searchInput = pb.querySelector('.vnx-pb-cur-search');
            if (searchInput) searchInput.focus();

            apiPostCached(pbApi, pbParams).then(function(result) {
                var items = result.response || [];
                var seen = {};
                var uniqueItems = [];

                function addU(val, label) {
                    if (!val || seen[val]) return;
                    seen[val] = true;
                    uniqueItems.push({ value: val, label: label || val });
                }

                items.forEach(function(item) {
                    var baseCurr = getNestedVal(item, 'meta.base_currency');
                    if (baseCurr) addU(String(baseCurr), String(getNestedVal(item, 'meta.base_currency_desc') || baseCurr));
                    var quoteCurr = getNestedVal(item, 'profile.currency');
                    if (quoteCurr) addU(String(quoteCurr), String(getNestedVal(item, 'meta.currency_desc') || quoteCurr));
                    if (!baseCurr && !quoteCurr) {
                        var val = String(getNestedVal(item, pbValueKey));
                        if (val) addU(val, String(getNestedVal(item, pbLabelKey) || val));
                    }
                });
                uniqueItems.sort(function(a, b) { return a.value.localeCompare(b.value); });

                var listEl = pb.querySelector('.vnx-pb-cur-dropdown .vnx-search-list');
                if (!listEl) return;

                var html = '';
                uniqueItems.forEach(function(item) {
                    var disabled = existing.indexOf(item.value) !== -1 ? ' disabled' : '';
                    var searchText = (item.value + ' ' + item.label).toLowerCase();
                    html += '<div class="vnx-search-option vnx-pb-cur-option' + disabled + '" data-value="' + escAttr(item.value) + '" data-search="' + escAttr(searchText) + '">' +
                        escHtml(item.value) + ' — ' + escHtml(item.label) + '</div>';
                });
                listEl.innerHTML = html;
            }).catch(function() {
                var listEl = pb.querySelector('.vnx-pb-cur-dropdown .vnx-search-list');
                if (listEl) listEl.innerHTML = '<div class="vnx-ajax-loading">Failed to load</div>';
            });
            return;
        }

        // Select currency from dropdown
        var curOption = e.target.closest('.vnx-pb-cur-option:not(.disabled)');
        if (curOption) {
            var pb = curOption.closest('.vnx-preset-builder');
            var activeIdx = parseInt(pb.dataset.active) || 0;
            var data = pbGetAllData(pb);
            var code = curOption.dataset.value;
            data[activeIdx].items.push(code);

            var dropdown = pb.querySelector('.vnx-pb-cur-dropdown');
            if (dropdown) dropdown.remove();

            pbRenderBody(pb);
            pbSyncJson(pb);
            return;
        }
    });

    // Search filter for preset currency dropdown
    document.addEventListener('input', function(e) {
        if (!e.target.classList.contains('vnx-pb-cur-search')) return;
        var q = e.target.value.toLowerCase();
        var list = e.target.nextElementSibling;
        list.querySelectorAll('.vnx-pb-cur-option').forEach(function(opt) {
            opt.classList.toggle('hidden', q && (opt.dataset.search || '').indexOf(q) === -1);
        });
    });

    // Close preset currency dropdowns on outside click
    document.addEventListener('click', function(e) {
        if (!e.target.closest('.vnx-pb-add-cur') && !e.target.closest('.vnx-pb-cur-dropdown')) {
            document.querySelectorAll('.vnx-pb-cur-dropdown').forEach(function(d) { d.remove(); });
        }
    });

    // ================================================================
    // ================================================================
    // FILTER BUILDER — interactive metric + condition + value
    // ================================================================

    // Get conditions from imported filter-data.js
    var FB_CONDITIONS_RAW = getFilterConditions();
    // Build operator → condition map (e.g. '_gt' → 'above')
    var FB_OP_TO_KEY = {};
    var FB_CONDITIONS = {};
    Object.keys(FB_CONDITIONS_RAW).forEach(function(k) {
        var c = FB_CONDITIONS_RAW[k];
        var op = c.operator.replace(/^_/, ''); // '_gt' → 'gt'
        FB_OP_TO_KEY[op] = k;
        FB_CONDITIONS[k] = { label: c.label, operator: c.operator, inputs: c.inputs === 'both' ? 2 : 1 };
    });

    function fbSyncJson(fb) {
        var data = {};
        fb.querySelectorAll('.vnx-fb-chip').forEach(function(chip) {
            var path = chip.dataset.metricPath;
            var cond = chip.dataset.condition;
            var val = chip.dataset.value || '';
            if (path && cond && val) {
                data[path + '_' + cond] = val;
            }
        });
        var key = fb.dataset.key;
        var hidden = fb.querySelector('input[type="hidden"][data-key="' + key + '"]');
        if (!hidden) {
            hidden = document.createElement('input');
            hidden.type = 'hidden';
            hidden.dataset.key = key;
            hidden.dataset.default = '';
            fb.appendChild(hidden);
        }
        hidden.value = Object.keys(data).length ? JSON.stringify(data) : '';
        syncSettingsJson();
    }

    // Find metric key from path (e.g. 'active.c' → 'price')
    function fbGetMetricKey(path) {
        var allM = getMetrics();
        var found = '';
        Object.keys(allM).forEach(function(mk) {
            if (allM[mk].path === path) found = mk;
        });
        return found;
    }

    function fbShowDetail(fb, chip) {
        var detail = fb.querySelector('.vnx-fb-detail');
        var condOp = chip.dataset.condition || 'gt'; // stored as operator suffix: 'gt', 'lt', etc.
        var condKey = FB_OP_TO_KEY[condOp] || 'above'; // map to condition key: 'above', 'below', etc.
        var val = chip.dataset.value || '';
        var vals = val.split(',');

        // Get metric key to find allowed conditions and if it's a select filter
        var metricPath = chip.dataset.metricPath;
        var metricKey = fbGetMetricKey(metricPath);
        var allowedConds = getDefaultConditionsForMetric(metricKey);

        // Check if metric has select options (like RSI Signal → Strong Buy, Buy, etc.)
        var allM = getMetrics();
        var metricConfig = allM[metricKey] || {};
        var isSelectFilter = metricConfig.filter_type === 'select';
        var selectOptions = {};
        if (isSelectFilter && metricConfig.load_options) {
            selectOptions = metricConfig.load_options;
        }

        // Condition select — only show allowed conditions for this metric
        var condOpts = '';
        allowedConds.forEach(function(k) {
            if (!FB_CONDITIONS[k]) return;
            var selected = (k === condKey) ? ' selected' : '';
            condOpts += '<option value="' + k + '"' + selected + '>' + FB_CONDITIONS[k].label + '</option>';
        });

        // Inputs — select options or text inputs
        var inputsHtml = '';
        if (isSelectFilter && Object.keys(selectOptions).length) {
            // Select dropdown for predefined values
            var selOpts = '';
            Object.keys(selectOptions).forEach(function(sv) {
                var sl = (typeof selectOptions[sv] === 'object') ? selectOptions[sv].label || sv : selectOptions[sv];
                selOpts += '<option value="' + escAttr(sv) + '"' + (sv === vals[0] ? ' selected' : '') + '>' + escHtml(sl) + '</option>';
            });
            inputsHtml = '<div class="vnx-fb-inputs"><select class="vnx-fb-val vnx-fb-select-val" data-idx="0">' + selOpts + '</select></div>';
            // For select filters, force condition to 'equal'
            if (!chip.dataset.condition || chip.dataset.condition === 'gt') {
                chip.dataset.condition = 'eq';
                condKey = 'equal';
            }
            condOpts = '<option value="equal" selected>' + FB_CONDITIONS['equal'].label + '</option>';
        } else if (condKey === 'between') {
            inputsHtml = '<div class="vnx-fb-inputs"><input type="text" class="vnx-fb-val" data-idx="0" placeholder="from" value="' + escAttr(vals[0] || '') + '"><input type="text" class="vnx-fb-val" data-idx="1" placeholder="to" value="' + escAttr(vals[1] || '') + '"></div>';
        } else {
            inputsHtml = '<div class="vnx-fb-inputs"><input type="text" class="vnx-fb-val" data-idx="0" placeholder="Enter value" value="' + escAttr(vals[0] || '') + '"></div>';
        }

        var chipLabel = chip.querySelector('.vnx-fb-chip-label').textContent;
        var condCount = condOpts.split('</option>').length - 1;
        var condSelectHtml = condCount > 1 ? '<select class="vnx-fb-cond-select">' + condOpts + '</select>' : '';
        detail.innerHTML =
            '<div class="vnx-fb-detail-inner" data-chip-idx="' + Array.from(fb.querySelectorAll('.vnx-fb-chip')).indexOf(chip) + '">' +
                '<div class="vnx-fb-detail-label">' + escHtml(chipLabel) + '</div>' +
                condSelectHtml +
                inputsHtml +
                '<button type="button" class="vnx-fb-remove-btn">Remove</button>' +
            '</div>';
        detail.style.display = '';
        chip.classList.add('vnx-fb-chip-active');
    }

    document.addEventListener('click', function(e) {
        // Add Filter — show metric picker dropdown
        var addTrigger = e.target.closest('.vnx-fb-add-trigger');
        if (addTrigger) {
            var fb = addTrigger.closest('.vnx-filter-builder');
            var existing = fb.querySelector('.vnx-fb-metric-dropdown');
            if (existing) { existing.remove(); return; }

            var metrics = resolveSource('screener_metrics');
            var html = '<div class="vnx-fb-metric-dropdown vnx-search-dropdown open">' +
                '<input type="text" class="vnx-search-input vnx-fb-metric-search" placeholder="Search filter..." autocomplete="off">' +
                '<div class="vnx-search-list">';
            // Get metric paths from METRICS for filter-able ones
            var allM = getMetrics();
            Object.keys(allM).forEach(function(mk) {
                var m = allM[mk];
                if (m.exclude_from_filter) return;
                var assetType = SLUG_TO_ASSET[typeSelect ? typeSelect.value : ''] || 'coin';
                if (m.assets && m.assets.indexOf(assetType) === -1) return;
                var searchText = (mk + ' ' + (m.label || mk)).toLowerCase();
                html += '<div class="vnx-search-option vnx-fb-metric-opt" data-value="' + escAttr(m.path || mk) + '" data-label="' + escAttr(m.label || mk) + '" data-search="' + escAttr(searchText) + '">' + escHtml(m.label || mk) + '</div>';
            });
            html += '</div></div>';
            addTrigger.insertAdjacentHTML('afterend', html);
            var si = fb.querySelector('.vnx-fb-metric-search');
            if (si) si.focus();
            return;
        }

        // Select metric from dropdown
        var metricOpt = e.target.closest('.vnx-fb-metric-opt');
        if (metricOpt) {
            var fb = metricOpt.closest('.vnx-filter-builder');
            var path = metricOpt.dataset.value;
            var label = metricOpt.dataset.label;

            // Check if metric is select-type
            var metricKey = '';
            var allM = getMetrics();
            Object.keys(allM).forEach(function(mk) { if (allM[mk].path === path) metricKey = mk; });
            var mConfig = allM[metricKey] || {};
            var defaultCond = mConfig.filter_type === 'select' ? 'eq' : 'gt';

            var chip = document.createElement('div');
            chip.className = 'vnx-fb-chip';
            chip.dataset.metricPath = path;
            chip.dataset.condition = defaultCond;
            chip.dataset.value = '';
            chip.innerHTML = '<span class="vnx-fb-chip-label">' + escHtml(label) + '</span>' +
                '<button type="button" class="vnx-fb-chip-toggle">▾</button>' +
                '<button type="button" class="vnx-fb-chip-remove" title="Remove">×</button>';
            fb.querySelector('.vnx-fb-chips').appendChild(chip);

            // Close dropdown
            var dd = fb.querySelector('.vnx-fb-metric-dropdown');
            if (dd) dd.remove();

            // Open detail for the new chip
            fbShowDetail(fb, chip);
            return;
        }

        // Toggle chip detail — click anywhere on chip (except remove button)
        var clickedChip = e.target.closest('.vnx-fb-chip');
        if (clickedChip && !e.target.closest('.vnx-fb-chip-remove')) {
            var fb = clickedChip.closest('.vnx-filter-builder');
            var detail = fb.querySelector('.vnx-fb-detail');
            if (clickedChip.classList.contains('vnx-fb-chip-active')) {
                clickedChip.classList.remove('vnx-fb-chip-active');
                detail.style.display = 'none';
            } else {
                fb.querySelectorAll('.vnx-fb-chip-active').forEach(function(c) { c.classList.remove('vnx-fb-chip-active'); });
                fbShowDetail(fb, clickedChip);
            }
            return;
        }

        // Remove filter chip
        var chipRemove = e.target.closest('.vnx-fb-chip-remove');
        if (chipRemove) {
            var chip = chipRemove.closest('.vnx-fb-chip');
            var fb = chip.closest('.vnx-filter-builder');
            chip.remove();
            fb.querySelector('.vnx-fb-detail').style.display = 'none';
            fbSyncJson(fb);
            return;
        }

        // Remove from detail panel
        var removeBtn = e.target.closest('.vnx-fb-remove-btn');
        if (removeBtn) {
            var fb = removeBtn.closest('.vnx-filter-builder');
            var detailInner = removeBtn.closest('.vnx-fb-detail-inner');
            var chipIdx = parseInt(detailInner.dataset.chipIdx);
            var chips = fb.querySelectorAll('.vnx-fb-chip');
            if (chips[chipIdx]) chips[chipIdx].remove();
            fb.querySelector('.vnx-fb-detail').style.display = 'none';
            fbSyncJson(fb);
            return;
        }

        // Close filter metric dropdown on outside click
        if (!e.target.closest('.vnx-fb-add') && !e.target.closest('.vnx-fb-metric-dropdown')) {
            document.querySelectorAll('.vnx-fb-metric-dropdown').forEach(function(d) { d.remove(); });
        }
    });

    // Condition change
    document.addEventListener('change', function(e) {
        if (e.target.classList.contains('vnx-fb-cond-select')) {
            var fb = e.target.closest('.vnx-filter-builder');
            var detailInner = e.target.closest('.vnx-fb-detail-inner');
            var chipIdx = parseInt(detailInner.dataset.chipIdx);
            var chip = fb.querySelectorAll('.vnx-fb-chip')[chipIdx];
            if (!chip) return;

            var condKey = e.target.value; // 'above', 'below', 'between', etc.
            var condConfig = FB_CONDITIONS[condKey] || {};
            var op = (condConfig.operator || '_gt').replace(/^_/, '');
            chip.dataset.condition = op;
            chip.dataset.value = '';

            var inputsDiv = detailInner.querySelector('.vnx-fb-inputs');
            if (condKey === 'between') {
                inputsDiv.innerHTML = '<input type="text" class="vnx-fb-val" data-idx="0" placeholder="from" value=""><input type="text" class="vnx-fb-val" data-idx="1" placeholder="to" value="">';
            } else {
                inputsDiv.innerHTML = '<input type="text" class="vnx-fb-val" data-idx="0" placeholder="Enter value" value="">';
            }
            fbSyncJson(fb);
        }

        // Select filter value change (like RSI Signal dropdown)
        if (e.target.classList.contains('vnx-fb-select-val')) {
            var fb = e.target.closest('.vnx-filter-builder');
            var detailInner = e.target.closest('.vnx-fb-detail-inner');
            var chipIdx = parseInt(detailInner.dataset.chipIdx);
            var chip = fb.querySelectorAll('.vnx-fb-chip')[chipIdx];
            if (chip) {
                chip.dataset.value = e.target.value;
                fbSyncJson(fb);
            }
        }
    });

    // Value input change
    document.addEventListener('input', function(e) {
        if (!e.target.classList.contains('vnx-fb-val')) return;
        var fb = e.target.closest('.vnx-filter-builder');
        var detailInner = e.target.closest('.vnx-fb-detail-inner');
        var chipIdx = parseInt(detailInner.dataset.chipIdx);
        var chip = fb.querySelectorAll('.vnx-fb-chip')[chipIdx];
        if (!chip) return;

        var inputs = detailInner.querySelectorAll('.vnx-fb-val');
        if (inputs.length === 2) {
            chip.dataset.value = (inputs[0].value || '') + ',' + (inputs[1].value || '');
        } else {
            chip.dataset.value = inputs[0].value || '';
        }
        fbSyncJson(fb);
    });

    // Search filter for metric dropdown
    document.addEventListener('input', function(e) {
        if (!e.target.classList.contains('vnx-fb-metric-search')) return;
        var q = e.target.value.toLowerCase();
        var list = e.target.nextElementSibling;
        list.querySelectorAll('.vnx-fb-metric-opt').forEach(function(opt) {
            opt.classList.toggle('hidden', q && (opt.dataset.search || '').indexOf(q) === -1);
        });
    });


    // AJAX-SELECT DEPENDENCY — reload when parent field changes
    // (e.g. Index depends on Country)
    // ================================================================

    function handleDependencyChange(changedKey, newValue) {
        // Find ajax-selects that depend on this field
        var attrName = changedKey.replace(/_/g, '-');
        document.querySelectorAll('.vnx-ajax-select[data-depends-on="' + attrName + '"]').forEach(function(wrap) {
            var paramName = wrap.dataset.dependParam;
            if (!paramName) return;

            // Update the api_params with new value
            var params = {};
            try { params = JSON.parse(wrap.dataset.apiParams || '{}'); } catch(e) {}
            params[paramName] = newValue;
            wrap.dataset.apiParams = JSON.stringify(params);

            // Clear cache for this endpoint
            var api = wrap.dataset.api;
            Object.keys(ajaxCache).forEach(function(k) {
                if (k.indexOf(api) !== -1) delete ajaxCache[k];
            });

            // Reset and reload
            wrap._loaded = false;
            wrap.dataset.value = '';
            var emptyLabel = wrap.dataset.emptyLabel || 'Select...';
            wrap.querySelector('.vnx-search-label').textContent = emptyLabel;

            // If dropdown is open, reload immediately
            var dropdown = wrap.querySelector('.vnx-search-dropdown');
            if (dropdown && dropdown.classList.contains('open')) {
                ajaxSelectLoad(wrap);
            }

            syncSettingsJson();
        });
    }

    // Listen for searchable-select option clicks (country change)
    document.addEventListener('click', function(e) {
        var opt = e.target.closest('.vnx-searchable-wrap .vnx-search-option');
        if (opt) {
            var wrap = opt.closest('.vnx-searchable-wrap');
            var sel = wrap ? wrap.querySelector('select') : null;
            if (sel && sel.dataset.key) {
                setTimeout(function() { handleDependencyChange(sel.dataset.key, sel.value); }, 50);
            }
        }
    });

    // Listen for regular select changes
    document.addEventListener('input', function(e) {
        if (e.target.tagName === 'SELECT' && e.target.dataset.key) {
            handleDependencyChange(e.target.dataset.key, e.target.value);
        }
    });


    // Watch Exchange select → update active preset's exchange + clear cache
    document.addEventListener('input', function(e) {
        var el = e.target.closest('[data-key="exchange"]');
        if (!el) return;
        var newExchange = el.value || 'FX';

        // Clear API cache for forex/list so it reloads with new exchange
        Object.keys(ajaxCache).forEach(function(k) {
            if (k.indexOf('forex/list') !== -1) delete ajaxCache[k];
        });
        // Also clear ajax-select loaded state so they reload
        document.querySelectorAll('.vnx-ajax-select').forEach(function(as) {
            as._loaded = false;
        });

        document.querySelectorAll('.vnx-preset-builder').forEach(function(pb) {
            var activeIdx = parseInt(pb.dataset.active) || 0;
            var data = pbGetAllData(pb);
            if (data[activeIdx]) {
                data[activeIdx].exchange = newExchange;
                pbRenderTabs(pb);
                pbSyncJson(pb);
            }
        });
    });
    // Also handle searchable select change for exchange
    var origSyncSettings = null;

    // Drag reorder for preset currency chips
    (function() {
        var dragChip = null;
        document.addEventListener('dragstart', function(e) {
            if (!e.target.classList.contains('vnx-pb-chip')) return;
            dragChip = e.target;
            dragChip.classList.add('vnx-ss-dragging');
            e.dataTransfer.effectAllowed = 'move';
        });
        document.addEventListener('dragend', function() {
            if (!dragChip) return;
            dragChip.classList.remove('vnx-ss-dragging');
            // Sync order back to data
            var pb = dragChip.closest('.vnx-preset-builder');
            if (pb) {
                var activeIdx = parseInt(pb.dataset.active) || 0;
                var data = pbGetAllData(pb);
                var newOrder = [];
                pb.querySelectorAll('.vnx-pb-chip').forEach(function(c) { newOrder.push(c.dataset.value); });
                if (data[activeIdx]) data[activeIdx].items = newOrder;
                pbSyncJson(pb);
            }
            dragChip = null;
        });
        document.addEventListener('dragover', function(e) {
            if (!dragChip) return;
            e.preventDefault();
            var target = e.target.closest('.vnx-pb-chip');
            if (!target || target === dragChip) return;
            var chips = Array.from(target.parentNode.querySelectorAll('.vnx-pb-chip'));
            if (chips.indexOf(dragChip) < chips.indexOf(target)) {
                target.after(dragChip);
            } else {
                target.before(dragChip);
            }
        });
    })();


    // ================================================================
    // ================================================================
    // AJAX SELECT — load options from API on first open
    // ================================================================

    // Helper: get nested value from object by dot-path (e.g. "meta.base_currency")
    function getNestedVal(obj, path) {
        if (!path) return '';
        var parts = path.split('.');
        var val = obj;
        for (var i = 0; i < parts.length; i++) {
            if (val == null) return '';
            val = val[parts[i]];
        }
        return val != null ? val : '';
    }

    // Load and render ajax-select options
    function ajaxSelectLoad(wrap) {
        if (wrap._loaded) return;
        wrap._loaded = true;

        var api = wrap.dataset.api;
        var params = {};
        try { params = JSON.parse(wrap.dataset.apiParams || '{}'); } catch(e) {}
        var valueKey = wrap.dataset.valueKey;
        var labelKey = wrap.dataset.labelKey;
        var iconKey = wrap.dataset.iconKey;
        var currentVal = wrap.dataset.value || '';
        var list = wrap.querySelector('.vnx-search-list');

        list.innerHTML = '<div class="vnx-ajax-loading">Loading...</div>';

        apiPostCached(api, params).then(function(result) {
            var items = result.response || [];
            // Extract unique currencies — base + quote from each forex pair
            // Same as site docs.js extractCurrencyOptions()
            var iconBaseUrl = 'https://vunelix.com/assets/icons/';
            var seen = {};
            var uniqueItems = [];

            function addUnique(val, label, icon) {
                if (!val || seen[val]) return;
                seen[val] = true;
                uniqueItems.push({ value: val, label: label || val, icon: icon || '' });
            }

            items.forEach(function(item) {
                // Base currency (e.g. AED from FX:AEDAUD) + base_logo
                var baseCurr = getNestedVal(item, 'meta.base_currency');
                if (baseCurr) addUnique(String(baseCurr), String(getNestedVal(item, 'meta.base_currency_desc') || baseCurr), String(getNestedVal(item, 'profile.base_logo') || ''));

                // Quote currency (e.g. AUD from FX:AEDAUD) + curr_logo
                var quoteCurr = getNestedVal(item, 'profile.currency');
                if (quoteCurr) addUnique(String(quoteCurr), String(getNestedVal(item, 'meta.currency_desc') || quoteCurr), String(getNestedVal(item, 'profile.curr_logo') || ''));

                // Fallback for non-forex APIs
                if (!baseCurr && !quoteCurr) {
                    var val = String(getNestedVal(item, valueKey));
                    if (val) addUnique(val, String(getNestedVal(item, labelKey) || val), String(getNestedVal(item, iconKey) || ''));
                }
            });

            // Sort alphabetically by value
            uniqueItems.sort(function(a, b) { return a.value.localeCompare(b.value); });

            // Render options with flag icons
            var html = '';
            uniqueItems.forEach(function(item) {
                var selected = item.value === currentVal ? ' selected' : '';
                var searchText = (item.value + ' ' + item.label).toLowerCase();
                var flagHtml = item.icon ? '<img src="' + iconBaseUrl + escAttr(item.icon) + '.svg" class="vnx-flag-icon" onerror="this.style.display=\'none\'" alt=""> ' : '';
                html += '<div class="vnx-search-option' + selected + '" data-value="' + escAttr(item.value) + '" data-label="' + escAttr(item.label) + '" data-icon="' + escAttr(item.icon) + '" data-search="' + escAttr(searchText) + '">' +
                    flagHtml + escHtml(item.value) + ' — ' + escHtml(item.label) +
                '</div>';
            });

            if (!html) html = '<div class="vnx-ajax-loading">No results</div>';
            list.innerHTML = html;

            // Update trigger label with flag if we have a match
            if (currentVal) {
                var match = uniqueItems.find(function(i) { return i.value === currentVal; });
                if (match) {
                    var triggerFlag = match.icon ? '<img src="' + iconBaseUrl + escAttr(match.icon) + '.svg" class="vnx-flag-icon" onerror="this.style.display=\'none\'" alt=""> ' : '';
                    wrap.querySelector('.vnx-search-label').innerHTML = triggerFlag + escHtml(match.value) + ' — ' + escHtml(match.label);
                }
            }
        }).catch(function(err) {
            list.innerHTML = '<div class="vnx-ajax-loading">Failed to load: ' + escHtml(err.message) + '</div>';
        });
    }

    // Open/close ajax-select dropdown
    document.addEventListener('click', function(e) {
        var trigger = e.target.closest('.vnx-ajax-select .vnx-search-trigger');
        if (trigger) {
            var wrap = trigger.closest('.vnx-ajax-select');
            var dropdown = wrap.querySelector('.vnx-search-dropdown');
            var wasOpen = dropdown.classList.contains('open');

            // Close all dropdowns
            document.querySelectorAll('.vnx-ajax-select .vnx-search-dropdown.open').forEach(function(d) { d.classList.remove('open'); });

            if (!wasOpen) {
                dropdown.classList.add('open');
                ajaxSelectLoad(wrap); // Load on first open
                var input = dropdown.querySelector('.vnx-search-input');
                input.value = '';
                input.focus();
                dropdown.querySelectorAll('.vnx-search-option').forEach(function(o) { o.classList.remove('hidden'); });
            }
            return;
        }

        // Select option
        var opt = e.target.closest('.vnx-ajax-select .vnx-search-option');
        if (opt) {
            var wrap = opt.closest('.vnx-ajax-select');
            var val = opt.dataset.value;
            var label = opt.dataset.label || val;
            var icon = opt.dataset.icon || '';

            wrap.dataset.value = val;
            var flagHtml = icon ? '<img src="https://vunelix.com/assets/icons/' + escAttr(icon) + '.svg" class="vnx-flag-icon" onerror="this.style.display=\'none\'" alt=""> ' : '';
            wrap.querySelector('.vnx-search-label').innerHTML = flagHtml + escHtml(val) + ' — ' + escHtml(label);

            wrap.querySelectorAll('.vnx-search-option').forEach(function(o) { o.classList.remove('selected'); });
            opt.classList.add('selected');
            wrap.querySelector('.vnx-search-dropdown').classList.remove('open');

            syncSettingsJson();
            return;
        }

        // Close on outside click
        if (!e.target.closest('.vnx-ajax-select')) {
            document.querySelectorAll('.vnx-ajax-select .vnx-search-dropdown.open').forEach(function(d) { d.classList.remove('open'); });
        }
    });

    // Search filter for ajax-select
    document.addEventListener('input', function(e) {
        if (!e.target.closest('.vnx-ajax-select')) return;
        if (!e.target.classList.contains('vnx-search-input')) return;
        var q = e.target.value.toLowerCase();
        var list = e.target.nextElementSibling;
        list.querySelectorAll('.vnx-search-option').forEach(function(opt) {
            opt.classList.toggle('hidden', q && (opt.dataset.search || '').indexOf(q) === -1);
        });
    });


    // SEARCHABLE SELECT — custom dropdown with search filter
    // ================================================================

    document.addEventListener('click', function(e) {
        // Toggle dropdown (only for searchable-select, NOT ajax-select or preset-builder)
        var trigger = e.target.closest('.vnx-search-trigger');
        if (trigger && !trigger.closest('.vnx-ajax-select') && !trigger.closest('.vnx-pb-add-cur') && !trigger.closest('.vnx-pb-body')) {
            var wrap = trigger.closest('.vnx-searchable-wrap');
            if (!wrap) return;
            var dropdown = wrap.querySelector('.vnx-search-dropdown');
            var wasOpen = dropdown.classList.contains('open');

            // Close all other searchable-select dropdowns (not ajax-select or preset-builder)
            document.querySelectorAll('.vnx-searchable-wrap .vnx-search-dropdown.open').forEach(function(d) { d.classList.remove('open'); });

            if (!wasOpen) {
                dropdown.classList.add('open');
                var input = dropdown.querySelector('.vnx-search-input');
                input.value = '';
                input.focus();
                // Show all options
                dropdown.querySelectorAll('.vnx-search-option').forEach(function(o) { o.classList.remove('hidden'); });
            }
            return;
        }

        // Select an option
        var option = e.target.closest('.vnx-search-option');
        if (option) {
            var wrap = option.closest('.vnx-searchable-wrap');
            if (!wrap) return;
            var val = option.dataset.value;
            var label = option.textContent.trim();

            // Update hidden select
            var select = wrap.querySelector('select');
            select.value = val;

            // Update trigger label (preserve flag icon if present)
            var flagImg = option.querySelector('.vnx-flag-icon');
            var labelEl = wrap.querySelector('.vnx-search-label');
            if (flagImg) {
                labelEl.innerHTML = '<img src="' + flagImg.src + '" class="vnx-flag-icon" alt=""> ' + escHtml(label);
            } else {
                labelEl.textContent = label;
            }

            // Update selected state
            wrap.querySelectorAll('.vnx-search-option').forEach(function(o) { o.classList.remove('selected'); });
            option.classList.add('selected');

            // Close dropdown
            wrap.querySelector('.vnx-search-dropdown').classList.remove('open');

            // Trigger change
            select.dispatchEvent(new Event('input', { bubbles: true }));
            syncSettingsJson();
            return;
        }

        // Close dropdowns on outside click (exclude ajax-select and preset-builder dropdowns)
        if (!e.target.closest('.vnx-searchable-wrap') && !e.target.closest('.vnx-ajax-select') && !e.target.closest('.vnx-pb-add-cur') && !e.target.closest('.vnx-pb-cur-dropdown')) {
            document.querySelectorAll('.vnx-searchable-wrap .vnx-search-dropdown.open').forEach(function(d) { d.classList.remove('open'); });
        }
    });

    // Search filter input
    document.addEventListener('input', function(e) {
        if (!e.target.classList.contains('vnx-search-input')) return;
        var q = e.target.value.toLowerCase();
        var list = e.target.nextElementSibling;
        list.querySelectorAll('.vnx-search-option').forEach(function(opt) {
            var text = opt.dataset.search || opt.textContent.toLowerCase();
            opt.classList.toggle('hidden', q && text.indexOf(q) === -1);
        });
    });


    function escHtml(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
    function escAttr(s) { return String(s).replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }

    // ================================================================
    // DASHBOARD SLIDER
    // ================================================================

    function initDashboardSlider() {
        var track = document.querySelector('.vnx-slider-track');
        if (!track) return;

        var slides = track.querySelectorAll('.vnx-slide');
        var prevBtn = document.querySelector('.vnx-slider-prev');
        var nextBtn = document.querySelector('.vnx-slider-next');
        var dotsWrap = document.querySelector('.vnx-slider-dots');
        var current = 0;
        var total = slides.length;

        // Build dots
        for (var i = 0; i < total; i++) {
            var dot = document.createElement('button');
            dot.type = 'button';
            dot.className = 'vnx-slider-dot' + (i === 0 ? ' active' : '');
            dot.dataset.index = i;
            dotsWrap.appendChild(dot);
        }
        var dots = dotsWrap.querySelectorAll('.vnx-slider-dot');

        function goTo(idx) {
            if (idx < 0) idx = total - 1;
            if (idx >= total) idx = 0;
            current = idx;
            track.style.transform = 'translateX(-' + (current * 100) + '%)';
            dots.forEach(function(d) { d.classList.toggle('active', +d.dataset.index === current); });
        }

        prevBtn.addEventListener('click', function() { goTo(current - 1); });
        nextBtn.addEventListener('click', function() { goTo(current + 1); });
        dotsWrap.addEventListener('click', function(e) {
            if (e.target.classList.contains('vnx-slider-dot')) goTo(+e.target.dataset.index);
        });

        // Auto-advance every 5s
        var autoTimer = setInterval(function() { goTo(current + 1); }, 5000);
        track.closest('.vnx-slider').addEventListener('mouseenter', function() { clearInterval(autoTimer); });
        track.closest('.vnx-slider').addEventListener('mouseleave', function() {
            autoTimer = setInterval(function() { goTo(current + 1); }, 5000);
        });
    }

})();
