/**
 * Value Sources — CRUD for scalar value sources (static, animated, audio, adaptive_time, adaptive_scene).
 *
 * Value sources produce a float 0.0-1.0 used for dynamic brightness control
 * on LED targets. Five subtypes: static (constant), animated (waveform),
 * audio (audio-reactive), adaptive_time (time-of-day schedule),
 * adaptive_scene (scene brightness analysis).
 *
 * Card rendering is handled by streams.js (Value tab).
 * This module manages the editor modal and API operations.
 */

import { _cachedValueSources, _cachedAudioSources, _cachedStreams, apiKey, valueSourcesCache } from '../core/state.ts';
import { API_BASE, fetchWithAuth, escapeHtml } from '../core/api.ts';
import { t } from '../core/i18n.ts';
import { showToast, showConfirm } from '../core/ui.ts';
import { Modal } from '../core/modal.ts';
import {
    getValueSourceIcon, getAudioSourceIcon, getPictureSourceIcon,
    ICON_CLONE, ICON_EDIT, ICON_TEST,
    ICON_LED_PREVIEW, ICON_ACTIVITY, ICON_TIMER, ICON_MOVE_VERTICAL, ICON_CLOCK,
    ICON_MUSIC, ICON_TRENDING_UP, ICON_MAP_PIN, ICON_MONITOR, ICON_REFRESH,
} from '../core/icons.ts';
import { wrapCard } from '../core/card-colors.ts';
import { TagInput, renderTagChips } from '../core/tag-input.ts';
import { IconSelect, showTypePicker } from '../core/icon-select.ts';
import { EntitySelect } from '../core/entity-palette.ts';
import { loadPictureSources } from './streams.ts';
import type { ValueSource } from '../types.ts';

export { getValueSourceIcon };

// ── EntitySelect instances for value source editor ──
let _vsAudioSourceEntitySelect: EntitySelect | null = null;
let _vsPictureSourceEntitySelect: EntitySelect | null = null;
let _vsTagsInput: TagInput | null = null;

class ValueSourceModal extends Modal {
    constructor() { super('value-source-modal'); }

    onForceClose() {
        if (_vsTagsInput) { _vsTagsInput.destroy(); _vsTagsInput = null; }
    }

    snapshotValues() {
        const type = (document.getElementById('value-source-type') as HTMLSelectElement).value;
        return {
            name: (document.getElementById('value-source-name') as HTMLInputElement).value,
            description: (document.getElementById('value-source-description') as HTMLInputElement).value,
            type,
            value: (document.getElementById('value-source-value') as HTMLInputElement).value,
            waveform: (document.getElementById('value-source-waveform') as HTMLSelectElement).value,
            speed: (document.getElementById('value-source-speed') as HTMLInputElement).value,
            minValue: (document.getElementById('value-source-min-value') as HTMLInputElement).value,
            maxValue: (document.getElementById('value-source-max-value') as HTMLInputElement).value,
            audioSource: (document.getElementById('value-source-audio-source') as HTMLSelectElement).value,
            mode: (document.getElementById('value-source-mode') as HTMLSelectElement).value,
            sensitivity: (document.getElementById('value-source-sensitivity') as HTMLInputElement).value,
            smoothing: (document.getElementById('value-source-smoothing') as HTMLInputElement).value,
            autoGain: (document.getElementById('value-source-auto-gain') as HTMLInputElement).checked,
            adaptiveMin: (document.getElementById('value-source-adaptive-min-value') as HTMLInputElement).value,
            adaptiveMax: (document.getElementById('value-source-adaptive-max-value') as HTMLInputElement).value,
            pictureSource: (document.getElementById('value-source-picture-source') as HTMLSelectElement).value,
            sceneBehavior: (document.getElementById('value-source-scene-behavior') as HTMLSelectElement).value,
            sceneSensitivity: (document.getElementById('value-source-scene-sensitivity') as HTMLInputElement).value,
            sceneSmoothing: (document.getElementById('value-source-scene-smoothing') as HTMLInputElement).value,
            schedule: JSON.stringify(_getScheduleFromUI()),
            daylightSpeed: (document.getElementById('value-source-daylight-speed') as HTMLInputElement).value,
            daylightRealTime: (document.getElementById('value-source-daylight-real-time') as HTMLInputElement).checked,
            daylightLatitude: (document.getElementById('value-source-daylight-latitude') as HTMLInputElement).value,
            tags: JSON.stringify(_vsTagsInput ? _vsTagsInput.getValue() : []),
        };
    }
}

const valueSourceModal = new ValueSourceModal();

/* ── Name auto-generation ────────────────────────────────────── */

let _vsNameManuallyEdited = false;

function _autoGenerateVSName() {
    if (_vsNameManuallyEdited) return;
    if ((document.getElementById('value-source-id') as HTMLInputElement).value) return;
    const type = (document.getElementById('value-source-type') as HTMLSelectElement).value;
    const typeLabel = t(`value_source.type.${type}`);
    let detail = '';
    if (type === 'animated') {
        const wf = (document.getElementById('value-source-waveform') as HTMLSelectElement).value;
        detail = t(`value_source.waveform.${wf}`);
    } else if (type === 'audio') {
        const mode = (document.getElementById('value-source-mode') as HTMLSelectElement).value;
        detail = t(`value_source.mode.${mode}`);
    } else if (type === 'adaptive_scene') {
        const sel = document.getElementById('value-source-picture-source') as HTMLSelectElement;
        const name = sel?.selectedOptions[0]?.textContent?.trim();
        if (name) detail = name;
    }
    (document.getElementById('value-source-name') as HTMLInputElement).value = detail ? `${typeLabel} · ${detail}` : typeLabel;
}

/* ── Icon-grid type selector ──────────────────────────────────── */

const VS_TYPE_KEYS = ['static', 'animated', 'audio', 'adaptive_time', 'adaptive_scene', 'daylight'];

function _buildVSTypeItems() {
    return VS_TYPE_KEYS.map(key => ({
        value: key,
        icon: getValueSourceIcon(key),
        label: t(`value_source.type.${key}`),
        desc: t(`value_source.type.${key}.desc`),
    }));
}

let _vsTypeIconSelect: IconSelect | null = null;
let _waveformIconSelect: IconSelect | null = null;

const _WAVEFORM_SVG = {
    sine:     '<svg viewBox="0 0 60 24" width="60" height="24" fill="none" stroke="currentColor" stroke-width="2"><path d="M0 12 Q15 -4 30 12 Q45 28 60 12"/></svg>',
    triangle: '<svg viewBox="0 0 60 24" width="60" height="24" fill="none" stroke="currentColor" stroke-width="2"><path d="M0 12 L15 2 L45 22 L60 12"/></svg>',
    square:   '<svg viewBox="0 0 60 24" width="60" height="24" fill="none" stroke="currentColor" stroke-width="2"><path d="M0 20 L0 4 L30 4 L30 20 L60 20 L60 4"/></svg>',
    sawtooth: '<svg viewBox="0 0 60 24" width="60" height="24" fill="none" stroke="currentColor" stroke-width="2"><path d="M0 20 L30 4 L30 20 L60 4"/></svg>',
};

function _ensureWaveformIconSelect() {
    const sel = document.getElementById('value-source-waveform');
    if (!sel) return;
    const items = [
        { value: 'sine',     icon: _WAVEFORM_SVG.sine,     label: t('value_source.waveform.sine') },
        { value: 'triangle', icon: _WAVEFORM_SVG.triangle, label: t('value_source.waveform.triangle') },
        { value: 'square',   icon: _WAVEFORM_SVG.square,   label: t('value_source.waveform.square') },
        { value: 'sawtooth', icon: _WAVEFORM_SVG.sawtooth, label: t('value_source.waveform.sawtooth') },
    ];
    if (_waveformIconSelect) { _waveformIconSelect.updateItems(items); return; }
    _waveformIconSelect = new IconSelect({ target: sel, items, columns: 4 } as any);
}

/* ── Waveform canvas preview ──────────────────────────────────── */

/**
 * Draw a waveform preview on the canvas element #value-source-waveform-preview.
 * Shows one full cycle of the selected waveform shape.
 */
function _drawWaveformPreview(waveformType: any) {
    const canvas = document.getElementById('value-source-waveform-preview') as HTMLCanvasElement;
    if (!canvas) return;

    const dpr = window.devicePixelRatio || 1;
    const cssW = canvas.offsetWidth || 200;
    const cssH = 60;
    canvas.width = cssW * dpr;
    canvas.height = cssH * dpr;
    canvas.style.height = cssH + 'px';

    const ctx = canvas.getContext('2d')!;
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    ctx.clearRect(0, 0, cssW, cssH);

    const W = cssW;
    const H = cssH;
    const padX = 8;
    const padY = 8;
    const drawW = W - padX * 2;
    const drawH = H - padY * 2;
    const midY = padY + drawH / 2;

    // Draw zero line
    ctx.strokeStyle = 'rgba(255,255,255,0.12)';
    ctx.lineWidth = 1;
    ctx.setLineDash([3, 4]);
    ctx.beginPath();
    ctx.moveTo(padX, midY);
    ctx.lineTo(padX + drawW, midY);
    ctx.stroke();
    ctx.setLineDash([]);

    // Draw waveform
    const N = 120;
    ctx.beginPath();
    for (let i = 0; i <= N; i++) {
        const t = i / N;  // 0..1 over one cycle
        let v; // -1..1
        switch (waveformType) {
            case 'triangle':
                v = t < 0.5 ? (4 * t - 1) : (3 - 4 * t);
                break;
            case 'square':
                v = t < 0.5 ? 1 : -1;
                break;
            case 'sawtooth':
                v = 2 * t - 1;
                break;
            case 'sine':
            default:
                v = Math.sin(2 * Math.PI * t);
                break;
        }
        const x = padX + t * drawW;
        const y = midY - v * (drawH / 2);
        if (i === 0) ctx.moveTo(x, y);
        else ctx.lineTo(x, y);
    }

    // Glow effect: draw thick translucent line first
    ctx.strokeStyle = 'rgba(99,179,237,0.25)';
    ctx.lineWidth = 4;
    ctx.stroke();

    // Crisp line on top
    ctx.strokeStyle = '#63b3ed';
    ctx.lineWidth = 1.5;
    ctx.stroke();
}

export function updateWaveformPreview() {
    const wf = (document.getElementById('value-source-waveform') as HTMLSelectElement)?.value || 'sine';
    _drawWaveformPreview(wf);
}

/* ── Audio mode icon-grid selector ────────────────────────────── */

const _AUDIO_MODE_SVG = {
    rms:  '<svg viewBox="0 0 60 24" width="60" height="24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="8" width="12" height="12" rx="2" fill="currentColor" opacity="0.3"/><rect x="24" y="4" width="12" height="16" rx="2" fill="currentColor" opacity="0.5"/><rect x="44" y="6" width="12" height="14" rx="2" fill="currentColor" opacity="0.4"/></svg>',
    peak: '<svg viewBox="0 0 60 24" width="60" height="24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 20 L10 14 L18 16 L26 4 L34 12 L42 18 L50 10 L58 20"/><circle cx="26" cy="4" r="2" fill="currentColor"/></svg>',
    beat: '<svg viewBox="0 0 60 24" width="60" height="24" fill="none" stroke="currentColor" stroke-width="2"><path d="M0 12 L12 12 L16 2 L22 22 L28 6 L32 12 L60 12"/></svg>',
};

let _audioModeIconSelect: IconSelect | null = null;

function _ensureAudioModeIconSelect() {
    const sel = document.getElementById('value-source-mode');
    if (!sel) return;
    const items = [
        { value: 'rms',  icon: _AUDIO_MODE_SVG.rms,  label: t('value_source.mode.rms'),  desc: t('value_source.mode.rms.desc') },
        { value: 'peak', icon: _AUDIO_MODE_SVG.peak, label: t('value_source.mode.peak'), desc: t('value_source.mode.peak.desc') },
        { value: 'beat', icon: _AUDIO_MODE_SVG.beat, label: t('value_source.mode.beat'), desc: t('value_source.mode.beat.desc') },
    ];
    if (_audioModeIconSelect) { _audioModeIconSelect.updateItems(items); return; }
    _audioModeIconSelect = new IconSelect({ target: sel, items, columns: 3 } as any);
}

function _ensureVSTypeIconSelect() {
    const sel = document.getElementById('value-source-type');
    if (!sel) return;
    if (_vsTypeIconSelect) { _vsTypeIconSelect.updateItems(_buildVSTypeItems()); return; }
    _vsTypeIconSelect = new IconSelect({ target: sel, items: _buildVSTypeItems(), columns: 2 } as any);
}

// ── Modal ─────────────────────────────────────────────────────

export async function showValueSourceModal(editData: any, presetType: any = null) {
    // When creating new: show type picker first, then re-enter with presetType
    if (!editData && !presetType) {
        showTypePicker({
            title: t('value_source.select_type'),
            items: _buildVSTypeItems(),
            onPick: (type) => showValueSourceModal(null, type),
        });
        return;
    }

    const hasId = editData?.id;
    const isEdit = !!hasId;

    const sourceType = editData?.source_type || presetType || 'static';
    const titleIcon = getValueSourceIcon(sourceType);
    const titleKey = isEdit ? 'value_source.edit' : 'value_source.add';
    const typeName = t(`value_source.type.${sourceType}`);
    (document.getElementById('value-source-modal-title') as HTMLElement).innerHTML = isEdit
        ? `${titleIcon} ${t(titleKey)}`
        : `${titleIcon} ${t(titleKey)}: ${typeName}`;
    (document.getElementById('value-source-id') as HTMLInputElement).value = isEdit ? editData.id : '';
    (document.getElementById('value-source-error') as HTMLElement).style.display = 'none';

    _vsNameManuallyEdited = !!(isEdit || editData);
    (document.getElementById('value-source-name') as HTMLInputElement).oninput = () => { _vsNameManuallyEdited = true; };

    _ensureVSTypeIconSelect();
    const typeSelect = document.getElementById('value-source-type') as HTMLSelectElement;
    // Type is chosen before the modal opens — always hide selector
    (document.getElementById('value-source-type-group') as HTMLElement).style.display = 'none';

    if (editData) {
        (document.getElementById('value-source-name') as HTMLInputElement).value = editData.name || '';
        (document.getElementById('value-source-description') as HTMLInputElement).value = editData.description || '';
        typeSelect.value = editData.source_type || 'static';
        onValueSourceTypeChange();

        if (editData.source_type === 'static') {
            _setSlider('value-source-value', editData.value ?? 1.0);
        } else if (editData.source_type === 'animated') {
            (document.getElementById('value-source-waveform') as HTMLSelectElement).value = editData.waveform || 'sine';
            if (_waveformIconSelect) _waveformIconSelect.setValue(editData.waveform || 'sine');
            _drawWaveformPreview(editData.waveform || 'sine');
            _setSlider('value-source-speed', editData.speed ?? 10);
            _setSlider('value-source-min-value', editData.min_value ?? 0);
            _setSlider('value-source-max-value', editData.max_value ?? 1);
        } else if (editData.source_type === 'audio') {
            _populateAudioSourceDropdown(editData.audio_source_id || '');
            (document.getElementById('value-source-mode') as HTMLSelectElement).value = editData.mode || 'rms';
            if (_audioModeIconSelect) _audioModeIconSelect.setValue(editData.mode || 'rms');
            (document.getElementById('value-source-auto-gain') as HTMLInputElement).checked = !!editData.auto_gain;
            _setSlider('value-source-sensitivity', editData.sensitivity ?? 1.0);
            _setSlider('value-source-smoothing', editData.smoothing ?? 0.3);
            _setSlider('value-source-audio-min-value', editData.min_value ?? 0);
            _setSlider('value-source-audio-max-value', editData.max_value ?? 1);
        } else if (editData.source_type === 'adaptive_time') {
            _populateScheduleUI(editData.schedule);
            _setSlider('value-source-adaptive-min-value', editData.min_value ?? 0);
            _setSlider('value-source-adaptive-max-value', editData.max_value ?? 1);
        } else if (editData.source_type === 'adaptive_scene') {
            _populatePictureSourceDropdown(editData.picture_source_id || '');
            (document.getElementById('value-source-scene-behavior') as HTMLSelectElement).value = editData.scene_behavior || 'complement';
            _setSlider('value-source-scene-sensitivity', editData.sensitivity ?? 1.0);
            _setSlider('value-source-scene-smoothing', editData.smoothing ?? 0.3);
            _setSlider('value-source-adaptive-min-value', editData.min_value ?? 0);
            _setSlider('value-source-adaptive-max-value', editData.max_value ?? 1);
        } else if (editData.source_type === 'daylight') {
            _setSlider('value-source-daylight-speed', editData.speed ?? 1.0);
            (document.getElementById('value-source-daylight-real-time') as HTMLInputElement).checked = !!editData.use_real_time;
            _setSlider('value-source-daylight-latitude', editData.latitude ?? 50);
            _syncDaylightVSSpeedVisibility();
            _setSlider('value-source-adaptive-min-value', editData.min_value ?? 0);
            _setSlider('value-source-adaptive-max-value', editData.max_value ?? 1);
        }
    } else {
        (document.getElementById('value-source-name') as HTMLInputElement).value = '';
        (document.getElementById('value-source-description') as HTMLInputElement).value = '';
        typeSelect.value = presetType || 'static';
        onValueSourceTypeChange();
        _setSlider('value-source-value', 1.0);
        _setSlider('value-source-speed', 10);
        _setSlider('value-source-min-value', 0);
        _setSlider('value-source-max-value', 1);
        (document.getElementById('value-source-waveform') as HTMLSelectElement).value = 'sine';
        _drawWaveformPreview('sine');
        _populateAudioSourceDropdown('');
        (document.getElementById('value-source-mode') as HTMLSelectElement).value = 'rms';
        if (_audioModeIconSelect) _audioModeIconSelect.setValue('rms');
        (document.getElementById('value-source-auto-gain') as HTMLInputElement).checked = false;
        _setSlider('value-source-sensitivity', 1.0);
        _setSlider('value-source-smoothing', 0.3);
        _setSlider('value-source-audio-min-value', 0);
        _setSlider('value-source-audio-max-value', 1);
        // Adaptive defaults
        _populateScheduleUI([]);
        _populatePictureSourceDropdown('');
        (document.getElementById('value-source-scene-behavior') as HTMLSelectElement).value = 'complement';
        _setSlider('value-source-scene-sensitivity', 1.0);
        _setSlider('value-source-scene-smoothing', 0.3);
        _setSlider('value-source-adaptive-min-value', 0);
        _setSlider('value-source-adaptive-max-value', 1);
        // Daylight defaults
        _setSlider('value-source-daylight-speed', 1.0);
        (document.getElementById('value-source-daylight-real-time') as HTMLInputElement).checked = false;
        _setSlider('value-source-daylight-latitude', 50);
        _syncDaylightVSSpeedVisibility();
        _autoGenerateVSName();
    }

    // Wire up auto-name triggers
    (document.getElementById('value-source-waveform') as HTMLSelectElement).onchange = () => { _autoGenerateVSName(); _drawWaveformPreview((document.getElementById('value-source-waveform') as HTMLSelectElement).value); };
    (document.getElementById('value-source-mode') as HTMLSelectElement).onchange = () => _autoGenerateVSName();
    (document.getElementById('value-source-picture-source') as HTMLSelectElement).onchange = () => _autoGenerateVSName();

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

    valueSourceModal.open();
    valueSourceModal.snapshot();
}

export async function closeValueSourceModal() {
    await valueSourceModal.close();
}

export function onValueSourceTypeChange() {
    const type = (document.getElementById('value-source-type') as HTMLSelectElement).value;
    if (_vsTypeIconSelect) _vsTypeIconSelect.setValue(type);
    (document.getElementById('value-source-static-section') as HTMLElement).style.display = type === 'static' ? '' : 'none';
    (document.getElementById('value-source-animated-section') as HTMLElement).style.display = type === 'animated' ? '' : 'none';
    if (type === 'animated') { _ensureWaveformIconSelect(); _drawWaveformPreview((document.getElementById('value-source-waveform') as HTMLSelectElement).value); }
    (document.getElementById('value-source-audio-section') as HTMLElement).style.display = type === 'audio' ? '' : 'none';
    if (type === 'audio') _ensureAudioModeIconSelect();
    (document.getElementById('value-source-adaptive-time-section') as HTMLElement).style.display = type === 'adaptive_time' ? '' : 'none';
    (document.getElementById('value-source-adaptive-scene-section') as HTMLElement).style.display = type === 'adaptive_scene' ? '' : 'none';
    (document.getElementById('value-source-daylight-section') as HTMLElement).style.display = type === 'daylight' ? '' : 'none';
    (document.getElementById('value-source-adaptive-range-section') as HTMLElement).style.display =
        (type === 'adaptive_time' || type === 'adaptive_scene' || type === 'daylight') ? '' : 'none';

    // Populate audio dropdown when switching to audio type
    if (type === 'audio') {
        const select = document.getElementById('value-source-audio-source') as HTMLSelectElement;
        if (select && select.options.length === 0) {
            _populateAudioSourceDropdown('');
        }
    }

    // Populate picture source dropdown when switching to scene type
    if (type === 'adaptive_scene') {
        _populatePictureSourceDropdown('');
    }

    _autoGenerateVSName();
}

// ── Daylight helpers ──────────────────────────────────────────

export function onDaylightVSRealTimeChange() {
    _syncDaylightVSSpeedVisibility();
}

function _syncDaylightVSSpeedVisibility() {
    const rt = (document.getElementById('value-source-daylight-real-time') as HTMLInputElement).checked;
    (document.getElementById('value-source-daylight-speed-group') as HTMLElement).style.display = rt ? 'none' : '';
}

// ── Save ──────────────────────────────────────────────────────

export async function saveValueSource() {
    const id = (document.getElementById('value-source-id') as HTMLInputElement).value;
    const name = (document.getElementById('value-source-name') as HTMLInputElement).value.trim();
    const sourceType = (document.getElementById('value-source-type') as HTMLSelectElement).value;
    const description = (document.getElementById('value-source-description') as HTMLInputElement).value.trim() || null;
    const errorEl = document.getElementById('value-source-error') as HTMLElement;

    if (!name) {
        errorEl.textContent = t('value_source.error.name_required');
        errorEl.style.display = '';
        return;
    }

    const payload: any = { name, source_type: sourceType, description, tags: _vsTagsInput ? _vsTagsInput.getValue() : [] };

    if (sourceType === 'static') {
        payload.value = parseFloat((document.getElementById('value-source-value') as HTMLInputElement).value);
    } else if (sourceType === 'animated') {
        payload.waveform = (document.getElementById('value-source-waveform') as HTMLSelectElement).value;
        payload.speed = parseFloat((document.getElementById('value-source-speed') as HTMLInputElement).value);
        payload.min_value = parseFloat((document.getElementById('value-source-min-value') as HTMLInputElement).value);
        payload.max_value = parseFloat((document.getElementById('value-source-max-value') as HTMLInputElement).value);
    } else if (sourceType === 'audio') {
        payload.audio_source_id = (document.getElementById('value-source-audio-source') as HTMLSelectElement).value;
        payload.mode = (document.getElementById('value-source-mode') as HTMLSelectElement).value;
        payload.auto_gain = (document.getElementById('value-source-auto-gain') as HTMLInputElement).checked;
        payload.sensitivity = parseFloat((document.getElementById('value-source-sensitivity') as HTMLInputElement).value);
        payload.smoothing = parseFloat((document.getElementById('value-source-smoothing') as HTMLInputElement).value);
        payload.min_value = parseFloat((document.getElementById('value-source-audio-min-value') as HTMLInputElement).value);
        payload.max_value = parseFloat((document.getElementById('value-source-audio-max-value') as HTMLInputElement).value);
    } else if (sourceType === 'adaptive_time') {
        payload.schedule = _getScheduleFromUI();
        if (payload.schedule.length < 2) {
            errorEl.textContent = t('value_source.error.schedule_min');
            errorEl.style.display = '';
            return;
        }
        payload.min_value = parseFloat((document.getElementById('value-source-adaptive-min-value') as HTMLInputElement).value);
        payload.max_value = parseFloat((document.getElementById('value-source-adaptive-max-value') as HTMLInputElement).value);
    } else if (sourceType === 'adaptive_scene') {
        payload.picture_source_id = (document.getElementById('value-source-picture-source') as HTMLSelectElement).value;
        payload.scene_behavior = (document.getElementById('value-source-scene-behavior') as HTMLSelectElement).value;
        payload.sensitivity = parseFloat((document.getElementById('value-source-scene-sensitivity') as HTMLInputElement).value);
        payload.smoothing = parseFloat((document.getElementById('value-source-scene-smoothing') as HTMLInputElement).value);
        payload.min_value = parseFloat((document.getElementById('value-source-adaptive-min-value') as HTMLInputElement).value);
        payload.max_value = parseFloat((document.getElementById('value-source-adaptive-max-value') as HTMLInputElement).value);
    } else if (sourceType === 'daylight') {
        payload.speed = parseFloat((document.getElementById('value-source-daylight-speed') as HTMLInputElement).value);
        payload.use_real_time = (document.getElementById('value-source-daylight-real-time') as HTMLInputElement).checked;
        payload.latitude = parseFloat((document.getElementById('value-source-daylight-latitude') as HTMLInputElement).value);
        payload.min_value = parseFloat((document.getElementById('value-source-adaptive-min-value') as HTMLInputElement).value);
        payload.max_value = parseFloat((document.getElementById('value-source-adaptive-max-value') as HTMLInputElement).value);
    }

    try {
        const method = id ? 'PUT' : 'POST';
        const url = id ? `/value-sources/${id}` : '/value-sources';
        const resp = await fetchWithAuth(url, {
            method,
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(payload),
        });
        if (!resp.ok) {
            const err = await resp.json().catch(() => ({}));
            throw new Error(err.detail || `HTTP ${resp.status}`);
        }
        showToast(t(id ? 'value_source.updated' : 'value_source.created'), 'success');
        valueSourceModal.forceClose();
        valueSourcesCache.invalidate();
        await loadPictureSources();
    } catch (e: any) {
        errorEl.textContent = e.message;
        errorEl.style.display = '';
    }
}

// ── Edit ──────────────────────────────────────────────────────

export async function editValueSource(sourceId: any) {
    try {
        const resp = await fetchWithAuth(`/value-sources/${sourceId}`);
        if (!resp.ok) throw new Error(t('value_source.error.load'));
        const data = await resp.json();
        await showValueSourceModal(data);
    } catch (e: any) {
        if (e.isAuth) return;
        showToast(e.message, 'error');
    }
}

// ── Clone ─────────────────────────────────────────────────────

export async function cloneValueSource(sourceId: any) {
    try {
        const resp = await fetchWithAuth(`/value-sources/${sourceId}`);
        if (!resp.ok) throw new Error(t('value_source.error.load'));
        const data = await resp.json();
        delete data.id;
        data.name = data.name + ' (copy)';
        await showValueSourceModal(data);
    } catch (e: any) {
        if (e.isAuth) return;
        showToast(e.message, 'error');
    }
}

// ── Delete ────────────────────────────────────────────────────

export async function deleteValueSource(sourceId: any) {
    const confirmed = await showConfirm(t('value_source.delete.confirm'));
    if (!confirmed) return;

    try {
        const resp = await fetchWithAuth(`/value-sources/${sourceId}`, { method: 'DELETE' });
        if (!resp.ok) {
            const err = await resp.json().catch(() => ({}));
            throw new Error(err.detail || `HTTP ${resp.status}`);
        }
        showToast(t('value_source.deleted'), 'success');
        valueSourcesCache.invalidate();
        await loadPictureSources();
    } catch (e: any) {
        showToast(e.message, 'error');
    }
}

// ── Value Source Test (real-time output chart) ────────────────

const VS_HISTORY_SIZE = 200;

let _testVsWs: WebSocket | null = null;
let _testVsAnimFrame: number | null = null;
let _testVsLatest: any = null;
let _testVsHistory: number[] = [];
let _testVsMinObserved = Infinity;
let _testVsMaxObserved = -Infinity;

const testVsModal = new Modal('test-value-source-modal', { backdrop: true, lock: true });

export function testValueSource(sourceId: any) {
    const statusEl = document.getElementById('vs-test-status');
    if (statusEl) {
        statusEl.textContent = t('value_source.test.connecting');
        statusEl.style.display = '';
    }

    // Reset state
    _testVsLatest = null;
    _testVsHistory = [];
    _testVsMinObserved = Infinity;
    _testVsMaxObserved = -Infinity;

    const currentEl = document.getElementById('vs-test-current');
    const minEl = document.getElementById('vs-test-min');
    const maxEl = document.getElementById('vs-test-max');
    if (currentEl) currentEl.textContent = '---';
    if (minEl) minEl.textContent = '---';
    if (maxEl) maxEl.textContent = '---';

    testVsModal.open();

    // Size canvas to container
    const canvas = document.getElementById('vs-test-canvas') as HTMLCanvasElement;
    _sizeVsCanvas(canvas);

    // Connect WebSocket
    const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
    const wsUrl = `${protocol}//${window.location.host}${API_BASE}/value-sources/${sourceId}/test/ws?token=${encodeURIComponent(apiKey || '')}`;

    try {
        _testVsWs = new WebSocket(wsUrl);

        _testVsWs.onopen = () => {
            if (statusEl) statusEl.style.display = 'none';
        };

        _testVsWs.onmessage = (event) => {
            try {
                const data = JSON.parse(event.data);
                _testVsLatest = data.value;
                _testVsHistory.push(data.value);
                if (_testVsHistory.length > VS_HISTORY_SIZE) {
                    _testVsHistory.shift();
                }
                if (data.value < _testVsMinObserved) _testVsMinObserved = data.value;
                if (data.value > _testVsMaxObserved) _testVsMaxObserved = data.value;
            } catch (e) {
                console.error('Value source test WS parse error:', e);
                return;
            }
        };

        _testVsWs.onclose = () => {
            _testVsWs = null;
        };

        _testVsWs.onerror = () => {
            showToast(t('value_source.test.error'), 'error');
            _cleanupVsTest();
        };
    } catch {
        showToast(t('value_source.test.error'), 'error');
        _cleanupVsTest();
        return;
    }

    // Start render loop
    _testVsAnimFrame = requestAnimationFrame(_renderVsTestLoop);
}

export function closeTestValueSourceModal() {
    _cleanupVsTest();
    testVsModal.forceClose();
}

function _cleanupVsTest() {
    if (_testVsAnimFrame) {
        cancelAnimationFrame(_testVsAnimFrame);
        _testVsAnimFrame = null;
    }
    if (_testVsWs) {
        _testVsWs.onclose = null;
        _testVsWs.close();
        _testVsWs = null;
    }
    _testVsLatest = null;
}

function _sizeVsCanvas(canvas: HTMLCanvasElement) {
    const rect = canvas.parentElement!.getBoundingClientRect();
    const dpr = window.devicePixelRatio || 1;
    canvas.width = rect.width * dpr;
    canvas.height = 200 * dpr;
    canvas.style.height = '200px';
    canvas.getContext('2d')!.scale(dpr, dpr);
}

function _renderVsTestLoop() {
    _renderVsChart();
    if (testVsModal.isOpen) {
        _testVsAnimFrame = requestAnimationFrame(_renderVsTestLoop);
    }
}

function _renderVsChart() {
    const canvas = document.getElementById('vs-test-canvas') as HTMLCanvasElement;
    if (!canvas) return;

    const ctx = canvas.getContext('2d')!;
    const dpr = window.devicePixelRatio || 1;
    const w = canvas.width / dpr;
    const h = canvas.height / dpr;

    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    ctx.clearRect(0, 0, w, h);

    // Draw horizontal guide lines at 0.0, 0.5, 1.0
    ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
    ctx.setLineDash([4, 4]);
    ctx.lineWidth = 1;
    for (const frac of [0, 0.5, 1.0]) {
        const y = h - frac * h;
        ctx.beginPath();
        ctx.moveTo(0, y);
        ctx.lineTo(w, y);
        ctx.stroke();
    }
    ctx.setLineDash([]);

    // Draw Y-axis labels
    ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
    ctx.font = '10px monospace';
    ctx.textAlign = 'left';
    ctx.fillText('1.0', 4, 12);
    ctx.fillText('0.5', 4, h / 2 - 2);
    ctx.fillText('0.0', 4, h - 4);

    const history = _testVsHistory;
    if (history.length < 2) return;

    // Draw filled area under the line
    ctx.beginPath();
    const stepX = w / (VS_HISTORY_SIZE - 1);
    const startOffset = VS_HISTORY_SIZE - history.length;

    ctx.moveTo(startOffset * stepX, h);
    for (let i = 0; i < history.length; i++) {
        const x = (startOffset + i) * stepX;
        const y = h - history[i] * h;
        ctx.lineTo(x, y);
    }
    ctx.lineTo((startOffset + history.length - 1) * stepX, h);
    ctx.closePath();
    ctx.fillStyle = 'rgba(76, 175, 80, 0.15)';
    ctx.fill();

    // Draw the line
    ctx.beginPath();
    for (let i = 0; i < history.length; i++) {
        const x = (startOffset + i) * stepX;
        const y = h - history[i] * h;
        if (i === 0) ctx.moveTo(x, y);
        else ctx.lineTo(x, y);
    }
    ctx.strokeStyle = '#4caf50';
    ctx.lineWidth = 2;
    ctx.stroke();

    // Update stats
    const curEl = document.getElementById('vs-test-current');
    const mnEl = document.getElementById('vs-test-min');
    const mxEl = document.getElementById('vs-test-max');
    if (_testVsLatest !== null && curEl) {
        curEl.textContent = (_testVsLatest * 100).toFixed(1) + '%';
    }
    if (_testVsMinObserved !== Infinity && mnEl) {
        mnEl.textContent = (_testVsMinObserved * 100).toFixed(1) + '%';
    }
    if (_testVsMaxObserved !== -Infinity && mxEl) {
        mxEl.textContent = (_testVsMaxObserved * 100).toFixed(1) + '%';
    }
}

// ── Card rendering (used by streams.js) ───────────────────────

export function createValueSourceCard(src: ValueSource) {
    const icon = getValueSourceIcon(src.source_type);

    let propsHtml = '';
    if (src.source_type === 'static') {
        propsHtml = `<span class="stream-card-prop">${ICON_LED_PREVIEW} ${t('value_source.type.static')}: ${src.value ?? 1.0}</span>`;
    } else if (src.source_type === 'animated') {
        const waveLabel = src.waveform || 'sine';
        propsHtml = `
            <span class="stream-card-prop">${ICON_ACTIVITY} ${escapeHtml(waveLabel)}</span>
            <span class="stream-card-prop">${ICON_TIMER} ${src.speed ?? 10} cpm</span>
            <span class="stream-card-prop">${ICON_MOVE_VERTICAL} ${src.min_value ?? 0}–${src.max_value ?? 1}</span>
        `;
    } else if (src.source_type === 'audio') {
        const audioSrc = _cachedAudioSources.find(a => a.id === src.audio_source_id);
        const audioName = audioSrc ? audioSrc.name : (src.audio_source_id || '-');
        const audioSection = audioSrc ? (audioSrc.source_type === 'mono' ? 'audio-mono' : audioSrc.source_type === 'band_extract' ? 'audio-band-extract' : 'audio-multi') : 'audio-multi';
        const audioTab = audioSrc ? (audioSrc.source_type === 'mono' ? 'audio_mono' : audioSrc.source_type === 'band_extract' ? 'audio_band_extract' : 'audio_multi') : 'audio_multi';
        const modeLabel = src.mode || 'rms';
        const audioBadge = audioSrc
            ? `<span class="stream-card-prop stream-card-link" title="${escapeHtml(t('value_source.audio_source'))}" onclick="event.stopPropagation(); navigateToCard('streams','${audioTab}','${audioSection}','data-id','${src.audio_source_id}')">${ICON_MUSIC} ${escapeHtml(audioName)}</span>`
            : `<span class="stream-card-prop" title="${escapeHtml(t('value_source.audio_source'))}">${ICON_MUSIC} ${escapeHtml(audioName)}</span>`;
        propsHtml = `
            ${audioBadge}
            <span class="stream-card-prop">${ICON_TRENDING_UP} ${modeLabel.toUpperCase()}</span>
            <span class="stream-card-prop">${ICON_MOVE_VERTICAL} ${src.min_value ?? 0}–${src.max_value ?? 1}</span>
        `;
    } else if (src.source_type === 'adaptive_time') {
        const pts = (src.schedule || []).length;
        propsHtml = `
            <span class="stream-card-prop">${ICON_MAP_PIN} ${pts} ${t('value_source.schedule.points')}</span>
            <span class="stream-card-prop">${ICON_MOVE_VERTICAL} ${src.min_value ?? 0}–${src.max_value ?? 1}</span>
        `;
    } else if (src.source_type === 'daylight') {
        if (src.use_real_time) {
            propsHtml = `<span class="stream-card-prop">${ICON_CLOCK} ${t('value_source.daylight.real_time')}</span>`;
        } else {
            propsHtml = `<span class="stream-card-prop">${ICON_TIMER} ${t('value_source.daylight.speed_label')} ${src.speed ?? 1.0}x</span>`;
        }
        propsHtml += `<span class="stream-card-prop">${ICON_MOVE_VERTICAL} ${src.min_value ?? 0}\u2013${src.max_value ?? 1}</span>`;
    } else if (src.source_type === 'adaptive_scene') {
        const ps = _cachedStreams.find(s => s.id === src.picture_source_id);
        const psName = ps ? ps.name : (src.picture_source_id || '-');
        let psSubTab = 'raw', psSection = 'raw-streams';
        if (ps) {
            if (ps.stream_type === 'static_image') { psSubTab = 'static_image'; psSection = 'static-streams'; }
            else if (ps.stream_type === 'processed') { psSubTab = 'processed'; psSection = 'proc-streams'; }
        }
        const psBadge = ps
            ? `<span class="stream-card-prop stream-card-link" onclick="event.stopPropagation(); navigateToCard('streams','${psSubTab}','${psSection}','data-stream-id','${src.picture_source_id}')" title="${escapeHtml(t('value_source.picture_source'))}">${ICON_MONITOR} ${escapeHtml(psName)}</span>`
            : `<span class="stream-card-prop">${ICON_MONITOR} ${escapeHtml(psName)}</span>`;
        propsHtml = `
            ${psBadge}
            <span class="stream-card-prop">${ICON_REFRESH} ${src.scene_behavior || 'complement'}</span>
        `;
    }

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

// ── Helpers ───────────────────────────────────────────────────

function _setSlider(id: string, value: any) {
    const slider = document.getElementById(id) as HTMLInputElement;
    if (slider) {
        slider.value = value;
        const display = document.getElementById(id + '-display');
        if (display) display.textContent = value;
    }
}

function _populateAudioSourceDropdown(selectedId: any) {
    const select = document.getElementById('value-source-audio-source') as HTMLSelectElement;
    if (!select) return;
    select.innerHTML = _cachedAudioSources.map((s: any) => {
        const badge = s.source_type === 'multichannel' ? ' [multichannel]' : s.source_type === 'band_extract' ? ' [band]' : ' [mono]';
        return `<option value="${s.id}"${s.id === selectedId ? ' selected' : ''}>${escapeHtml(s.name)}${badge}</option>`;
    }).join('');

    if (_vsAudioSourceEntitySelect) _vsAudioSourceEntitySelect.destroy();
    if (_cachedAudioSources.length > 0) {
        _vsAudioSourceEntitySelect = new EntitySelect({
            target: select,
            getItems: () => _cachedAudioSources.map(s => ({
                value: s.id,
                label: s.name,
                icon: getAudioSourceIcon(s.source_type),
                desc: s.source_type,
            })),
            placeholder: t('palette.search'),
        });
    }
}

// ── Adaptive helpers ──────────────────────────────────────────

function _populatePictureSourceDropdown(selectedId: any) {
    const select = document.getElementById('value-source-picture-source') as HTMLSelectElement;
    if (!select) return;
    select.innerHTML = _cachedStreams.map((s: any) =>
        `<option value="${s.id}"${s.id === selectedId ? ' selected' : ''}>${escapeHtml(s.name)}</option>`
    ).join('');

    if (_vsPictureSourceEntitySelect) _vsPictureSourceEntitySelect.destroy();
    if (_cachedStreams.length > 0) {
        _vsPictureSourceEntitySelect = new EntitySelect({
            target: select,
            getItems: () => _cachedStreams.map(s => ({
                value: s.id,
                label: s.name,
                icon: getPictureSourceIcon(s.stream_type),
            })),
            placeholder: t('palette.search'),
        });
    }
}

export function addSchedulePoint(time: string = '', value: number = 1.0) {
    const list = document.getElementById('value-source-schedule-list');
    if (!list) return;
    const timeStr = time || '12:00';
    const [h, m] = timeStr.split(':').map(Number);
    const pad = (n: number) => String(n).padStart(2, '0');

    const row = document.createElement('div');
    row.className = 'schedule-row';
    row.innerHTML = `
        <div class="schedule-time-wrap">
            <input type="number" class="schedule-h" min="0" max="23" value="${pad(h)}" data-role="hour">
            <span class="schedule-time-colon">:</span>
            <input type="number" class="schedule-m" min="0" max="59" value="${pad(m)}" data-role="minute">
        </div>
        <input type="hidden" class="schedule-time" value="${timeStr}">
        <input type="range" class="schedule-value" min="0" max="1" step="0.01" value="${value}"
            oninput="this.nextElementSibling.textContent = this.value">
        <span class="schedule-value-display">${value}</span>
        <button type="button" class="btn btn-icon btn-danger btn-sm" onclick="this.parentElement.remove()">&#x2715;</button>
    `;
    list.appendChild(row);
    _wireScheduleTimePicker(row);
}

function _wireScheduleTimePicker(row: HTMLElement) {
    const hInput = row.querySelector('.schedule-h') as HTMLInputElement;
    const mInput = row.querySelector('.schedule-m') as HTMLInputElement;
    const hidden = row.querySelector('.schedule-time') as HTMLInputElement;
    if (!hInput || !mInput || !hidden) return;

    const pad = (n: number) => String(n).padStart(2, '0');

    function clamp(input: HTMLInputElement, min: number, max: number) {
        let v = parseInt(input.value, 10);
        if (isNaN(v)) v = min;
        if (v < min) v = min;
        if (v > max) v = max;
        input.value = pad(v);
        return v;
    }

    function sync() {
        const hv = clamp(hInput, 0, 23);
        const mv = clamp(mInput, 0, 59);
        hidden.value = `${pad(hv)}:${pad(mv)}`;
    }

    [hInput, mInput].forEach(inp => {
        inp.addEventListener('focus', () => inp.select());
        inp.addEventListener('input', sync);
        inp.addEventListener('blur', sync);
        inp.addEventListener('keydown', (e) => {
            const isHour = inp.dataset.role === 'hour';
            const max = isHour ? 23 : 59;
            if (e.key === 'ArrowUp') {
                e.preventDefault();
                let v = parseInt(inp.value, 10) || 0;
                inp.value = pad(v >= max ? 0 : v + 1);
                sync();
            } else if (e.key === 'ArrowDown') {
                e.preventDefault();
                let v = parseInt(inp.value, 10) || 0;
                inp.value = pad(v <= 0 ? max : v - 1);
                sync();
            }
        });
    });

    sync();
}

function _getScheduleFromUI() {
    const rows = document.querySelectorAll('#value-source-schedule-list .schedule-row');
    const schedule: { time: string; value: number }[] = [];
    rows.forEach(row => {
        const time = (row.querySelector('.schedule-time') as HTMLInputElement).value;
        const value = parseFloat((row.querySelector('.schedule-value') as HTMLInputElement).value);
        if (time) schedule.push({ time, value });
    });
    return schedule;
}

function _populateScheduleUI(schedule: any) {
    const list = document.getElementById('value-source-schedule-list');
    if (!list) return;
    list.innerHTML = '';
    if (!schedule || schedule.length === 0) {
        // Default: morning bright, night dim
        addSchedulePoint('08:00', 1.0);
        addSchedulePoint('22:00', 0.3);
    } else {
        schedule.forEach(p => addSchedulePoint(p.time, p.value));
    }
}
