/**
 * Z2M Light Targets — editor/cards/CRUD for Zigbee2MQTT light output targets.
 *
 * Mirrors the HA Light Target editor visually and structurally:
 *  - BindableScalarWidget for all numeric fields
 *  - EntitySelect for the unified colour source picker (CSS + color VS)
 *  - IconSelect for the stop-action picker
 *  - Per-source-kind mapping rows (LED ranges in CSS mode, brightness-only in color_vs mode)
 *
 * Friendly names are typed text (no Z2M device catalog yet) — the row chrome
 * is otherwise identical to HA mappings so the modals feel like siblings.
 */

import {
    _cachedValueSources, _cachedMQTTSources,
    colorStripSourcesCache, mqttSourcesCache,
    outputTargetsCache, valueSourcesCache,
} from '../core/state.ts';
import { fetchWithAuth, escapeHtml } from '../core/api.ts';
import { logError } from '../core/log.ts';
import { t } from '../core/i18n.ts';
import { Modal } from '../core/modal.ts';
import { showToast, showConfirm, formatUptime } from '../core/ui.ts';
import {
    ICON_EDIT, ICON_START, ICON_STOP, ICON_STOP_PLAIN, ICON_TRASH,
    ICON_OK, ICON_WARNING, ICON_CLOCK, ICON_SUN,
    getColorStripIcon, getValueSourceIcon,
} from '../core/icons.ts';
import * as P from '../core/icon-paths.ts';
import { EntitySelect } from '../core/entity-palette.ts';
import { IconSelect } from '../core/icon-select.ts';
import {
    LightTargetWidgets,
    buildColorSourcePickerItems,
    decodeColorSourceValue,
    encodeColorSourceValue,
    ensureStopActionIconSelect,
} from '../core/light-target-editor.ts';
import { wrapCard } from '../core/card-colors.ts';
import type { ModCardOpts, ModChipOpts, ModMetricOpts, ModBtnOpts, ModMenuItemOpts, LedState } from '../core/mod-card.ts';
import { TagInput, renderTagChips } from '../core/tag-input.ts';
import { openAuthedWs } from '../core/ws-auth.ts';
import { bindableSourceId, bindableValue } from '../types.ts';

const ICON_Z2M = `<svg class="icon" viewBox="0 0 24 24">${P.lightbulb}</svg>`;
const _icon = (d: string) => `<svg class="icon" viewBox="0 0 24 24">${d}</svg>`;

// ── Modal state ──

type Z2MLightSourceKind = 'css' | 'color_vs';

let _z2mLightTagsInput: TagInput | null = null;
let _colorSourceEntitySelect: EntitySelect | null = null;
let _mqttSourceEntitySelect: EntitySelect | null = null;
let _stopActionIconSelect: IconSelect | null = null;
let _editorCssSources: any[] = [];
let _editorColorValueSources: any[] = []; // value sources where return_type === 'color'
let _editorSourceKind: Z2MLightSourceKind = 'css';
let _editorColorVsId: string = '';

// Shared bundle of the 5 BindableScalarWidget knobs. Z2M overrides
// updateRate (0.5-10 Hz vs HA's 0.5-5) and transition (default 0.3s vs HA's 0.5s).
const _widgets = new LightTargetWidgets({
    idPrefix: 'z2m-light-editor',
    valueSources: () => _cachedValueSources,
    overrides: {
        updateRate: { max: 10.0, step: 0.5, default: 5.0 },
        transition: { default: 0.3 },
    },
});

class Z2MLightEditorModal extends Modal {
    constructor() { super('z2m-light-editor-modal'); }

    onForceClose() {
        if (_z2mLightTagsInput) { _z2mLightTagsInput.destroy(); _z2mLightTagsInput = null; }
        if (_colorSourceEntitySelect) { _colorSourceEntitySelect.destroy(); _colorSourceEntitySelect = null; }
        if (_mqttSourceEntitySelect) { _mqttSourceEntitySelect.destroy(); _mqttSourceEntitySelect = null; }
        if (_stopActionIconSelect) { _stopActionIconSelect.destroy(); _stopActionIconSelect = null; }
        _widgets.destroyAll();
    }

    snapshotValues() {
        return {
            name: (document.getElementById('z2m-light-editor-name') as HTMLInputElement).value,
            mqtt_source: (document.getElementById('z2m-light-editor-mqtt-source') as HTMLSelectElement).value,
            base_topic: (document.getElementById('z2m-light-editor-base-topic') as HTMLInputElement).value,
            color_source: (document.getElementById('z2m-light-editor-color-source') as HTMLSelectElement).value,
            source_kind: _editorSourceKind,
            brightness: JSON.stringify(_widgets.getValue('brightness')),
            update_rate: JSON.stringify(_widgets.getValue('updateRate')),
            transition: JSON.stringify(_widgets.getValue('transition')),
            color_tolerance: JSON.stringify(_widgets.getValue('colorTolerance')),
            min_brightness_threshold: JSON.stringify(_widgets.getValue('minBrightnessThreshold')),
            stop_action: (document.getElementById('z2m-light-editor-stop-action') as HTMLSelectElement).value,
            mappings: _getMappingsJSON(),
            tags: JSON.stringify(_z2mLightTagsInput ? _z2mLightTagsInput.getValue() : []),
        };
    }
}

const z2mLightEditorModal = new Z2MLightEditorModal();

// ── Mappings ──

function _getMappingsJSON(): string {
    const rows = document.querySelectorAll('#z2m-light-mappings-list .ha-light-mapping-row');
    const mappings: any[] = [];
    rows.forEach(row => {
        const friendlyEl = row.querySelector('.z2m-mapping-friendly') as HTMLInputElement;
        const ledStartEl = row.querySelector('.z2m-mapping-led-start') as HTMLInputElement | null;
        const ledEndEl = row.querySelector('.z2m-mapping-led-end') as HTMLInputElement | null;
        const brightnessEl = row.querySelector('.z2m-mapping-brightness') as HTMLInputElement;
        // In color_vs mode LED range inputs aren't rendered — fall back to
        // safe defaults so mappings round-trip if the user toggles back to CSS.
        mappings.push({
            friendly_name: friendlyEl ? friendlyEl.value.trim() : '',
            led_start: ledStartEl ? (parseInt(ledStartEl.value) || 0) : 0,
            led_end: ledEndEl ? (parseInt(ledEndEl.value) || -1) : -1,
            brightness_scale: parseFloat(brightnessEl.value) || 1.0,
        });
    });
    return JSON.stringify(mappings);
}

function _isColorValueSource(vs: any): boolean {
    return !!vs && vs.return_type === 'color';
}

/** Build the unified Color Source picker items via the shared helper. */
function _buildColorSourcePickerItems(): any[] {
    return buildColorSourcePickerItems(
        _editorCssSources,
        _editorColorValueSources,
        {
            cssHeader: t('z2m_light.color_source.css'),
            colorVsHeader: t('z2m_light.color_source.color_vs'),
        },
    );
}

function _setMappingsModeHint(): void {
    const hintColorVs = document.getElementById('z2m-light-mappings-mode-hint');
    const hintCss = document.querySelectorAll('#z2m-light-editor-modal small.input-hint[data-i18n="z2m_light.mappings.hint"]')[0] as HTMLElement | undefined;
    const visibleCss = _editorSourceKind === 'css';
    if (hintColorVs) hintColorVs.style.display = visibleCss ? 'none' : '';
    // CSS hint is only revealed by the toggle; do not auto-show.
    if (hintCss && !visibleCss) hintCss.style.display = 'none';
}

export function addZ2MLightMapping(data: any = null): void {
    const list = document.getElementById('z2m-light-mappings-list');
    if (!list) return;

    const row = document.createElement('div');
    row.className = 'ha-light-mapping-row';

    const friendlyVal = data?.friendly_name || '';
    const previewIsEmpty = !friendlyVal;
    const previewText = friendlyVal || (t('z2m_light.mapping.unassigned') || '—');

    // Per-source-kind row layout: CSS shows LED ranges; color_vs hides them
    // and promotes the brightness scale to a single inline field.
    const rangeBlock = _editorSourceKind === 'color_vs'
        ? `<div class="ha-mapping-range-row">
                <div>
                    <label>${t('z2m_light.mapping.brightness')}</label>
                    <input type="number" class="z2m-mapping-brightness" value="${data?.brightness_scale ?? 1.0}" min="0" max="1" step="0.1">
                </div>
            </div>`
        : `<div class="ha-mapping-range-row">
                <div>
                    <label>${t('z2m_light.mapping.led_start')}</label>
                    <input type="number" class="z2m-mapping-led-start" value="${data?.led_start ?? 0}" min="0" step="1">
                </div>
                <div>
                    <label>${t('z2m_light.mapping.led_end')}</label>
                    <input type="number" class="z2m-mapping-led-end" value="${data?.led_end ?? -1}" min="-1" step="1">
                </div>
                <div>
                    <label>${t('z2m_light.mapping.brightness')}</label>
                    <input type="number" class="z2m-mapping-brightness" value="${data?.brightness_scale ?? 1.0}" min="0" max="1" step="0.1">
                </div>
            </div>`;

    const removeLabel = t('common.delete') || 'Remove';
    row.innerHTML = `
        <div class="ha-mapping-header">
            <div class="ha-mapping-icon" aria-hidden="true">${_icon(P.lightbulb)}</div>
            <span class="ha-mapping-name${previewIsEmpty ? ' is-empty' : ''}">${escapeHtml(previewText)}</span>
            <button type="button" class="ha-mapping-remove-btn" onclick="removeZ2MLightMapping(this)" title="${escapeHtml(removeLabel)}" aria-label="${escapeHtml(removeLabel)}">${ICON_TRASH}</button>
        </div>
        <div class="ha-mapping-fields">
            <div class="ha-mapping-field">
                <label>${t('z2m_light.mapping.friendly_name')}</label>
                <input type="text" class="z2m-mapping-friendly" value="${escapeHtml(friendlyVal)}" placeholder="living_room_bulb_1" autocomplete="off" spellcheck="false">
            </div>
            ${rangeBlock}
        </div>
    `;
    list.appendChild(row);

    // Live-update the header preview when the user retypes the friendly name.
    const friendlyInput = row.querySelector('.z2m-mapping-friendly') as HTMLInputElement;
    const previewEl = row.querySelector('.ha-mapping-name') as HTMLElement;
    const placeholder = t('z2m_light.mapping.unassigned') || '—';
    friendlyInput.addEventListener('input', () => {
        const val = friendlyInput.value.trim();
        previewEl.textContent = val || placeholder;
        previewEl.classList.toggle('is-empty', !val);
    });
}

export function removeZ2MLightMapping(btn: HTMLElement): void {
    const row = btn.closest('.ha-light-mapping-row');
    if (row) row.remove();
}

/**
 * Re-render every mapping row using the current `_editorSourceKind` layout.
 * Snapshots existing values (friendly_name, ranges, brightness) so the user
 * does not lose data when toggling between CSS and color_vs modes.
 */
function _rerenderMappingsForMode(): void {
    const list = document.getElementById('z2m-light-mappings-list');
    if (!list) return;
    const snapshot = JSON.parse(_getMappingsJSON());
    list.innerHTML = '';
    snapshot.forEach((m: any) => addZ2MLightMapping(m));
    _setMappingsModeHint();
}

// ── Stop-action IconSelect items ──

function _stopActionItems() {
    return [
        { value: 'none',     icon: _icon(P.circleOff), label: t('z2m_light.stop_action.none'),     desc: t('z2m_light.stop_action.none.desc') },
        { value: 'turn_off', icon: _icon(P.power),     label: t('z2m_light.stop_action.turn_off'), desc: t('z2m_light.stop_action.turn_off.desc') },
    ];
}

function _ensureStopActionIconSelect(): void {
    _stopActionIconSelect = ensureStopActionIconSelect(
        'z2m-light-editor-stop-action',
        _stopActionItems(),
        2,
        _stopActionIconSelect,
    );
}

// ── Show / Close ──

export async function showZ2MLightEditor(targetId: string | null = null, cloneData: any = null): Promise<void> {
    const [cssSources, mqttSources] = await Promise.all([
        colorStripSourcesCache.fetch().catch((): any[] => []),
        mqttSourcesCache.fetch().catch((): any[] => []),
        valueSourcesCache.fetch().catch(() => {}),
    ]);
    _editorCssSources = cssSources;
    _editorColorValueSources = (_cachedValueSources || []).filter(_isColorValueSource);

    const isEdit = !!targetId;
    const isClone = !!cloneData;
    const titleKey = isEdit ? 'z2m_light.edit' : 'z2m_light.add';
    document.getElementById('z2m-light-editor-title')!.innerHTML = `${ICON_Z2M} ${t(titleKey)}`;
    (document.getElementById('z2m-light-editor-id') as HTMLInputElement).value = '';
    (document.getElementById('z2m-light-editor-error') as HTMLElement).style.display = 'none';

    // MQTT broker picker — populates from MQTTSourceStore.
    const mqttSelect = document.getElementById('z2m-light-editor-mqtt-source') as HTMLSelectElement;
    mqttSelect.innerHTML = `<option value="">${t('z2m_light.mqtt_source.none')}</option>` +
        mqttSources.map((s: any) =>
            `<option value="${escapeHtml(s.id)}">${escapeHtml(s.name)}</option>`
        ).join('');

    // Unified Color Source picker — combines CSS sources + colour-returning value sources.
    const colorSelect = document.getElementById('z2m-light-editor-color-source') as HTMLSelectElement;
    const cssOptions = cssSources.map((s: any) =>
        `<option value="css:${escapeHtml(s.id)}">${escapeHtml(s.name)}</option>`
    ).join('');
    const colorVsOptions = _editorColorValueSources.map((s: any) =>
        `<option value="cvs:${escapeHtml(s.id)}">${escapeHtml(s.name)}</option>`
    ).join('');
    colorSelect.innerHTML = `<option value="">—</option>${cssOptions}${colorVsOptions}`;

    // Clear mappings
    document.getElementById('z2m-light-mappings-list')!.innerHTML = '';

    let editData: any = null;
    if (isEdit) {
        try {
            const resp = await fetchWithAuth(`/output-targets/${targetId}`);
            if (!resp.ok) throw new Error('Failed to load target');
            editData = await resp.json();
        } catch (e: any) {
            if (e.isAuth) return;
            showToast(e.message, 'error');
            return;
        }
    } else if (isClone) {
        editData = cloneData;
    }

    if (editData) {
        if (isEdit) (document.getElementById('z2m-light-editor-id') as HTMLInputElement).value = editData.id;
        (document.getElementById('z2m-light-editor-name') as HTMLInputElement).value = editData.name || '';
        mqttSelect.value = editData.mqtt_source_id || '';
        (document.getElementById('z2m-light-editor-base-topic') as HTMLInputElement).value = editData.base_topic || 'zigbee2mqtt';
        _editorSourceKind = (editData.source_kind === 'color_vs') ? 'color_vs' : 'css';
        _editorColorVsId = editData.color_value_source_id || '';
        const editCssId = editData.color_strip_source_id || '';
        colorSelect.value = encodeColorSourceValue(
            _editorSourceKind,
            _editorSourceKind === 'color_vs' ? _editorColorVsId : editCssId,
        );
        _widgets.applyValues(editData);
        (document.getElementById('z2m-light-editor-stop-action') as HTMLSelectElement).value = editData.stop_action ?? 'none';
        _ensureStopActionIconSelect();
        if (_stopActionIconSelect) _stopActionIconSelect.setValue(editData.stop_action ?? 'none', false);
        (document.getElementById('z2m-light-editor-description') as HTMLInputElement).value = editData.description || '';

        const mappings = editData.z2m_light_mappings || [];
        mappings.forEach((m: any) => addZ2MLightMapping(m));
    } else {
        (document.getElementById('z2m-light-editor-name') as HTMLInputElement).value = '';
        // Default to the only configured MQTT source if there's just one; otherwise leave blank.
        mqttSelect.value = mqttSources.length === 1 ? mqttSources[0].id : '';
        (document.getElementById('z2m-light-editor-base-topic') as HTMLInputElement).value = 'zigbee2mqtt';
        _editorSourceKind = 'css';
        _editorColorVsId = '';
        _widgets.applyDefaults();
        (document.getElementById('z2m-light-editor-stop-action') as HTMLSelectElement).value = 'none';
        _ensureStopActionIconSelect();
        if (_stopActionIconSelect) _stopActionIconSelect.setValue('none', false);
        (document.getElementById('z2m-light-editor-description') as HTMLInputElement).value = '';

        addZ2MLightMapping();
    }
    _setMappingsModeHint();

    // EntitySelect on the MQTT broker picker
    if (_mqttSourceEntitySelect) { _mqttSourceEntitySelect.destroy(); _mqttSourceEntitySelect = null; }
    _mqttSourceEntitySelect = new EntitySelect({
        target: mqttSelect,
        getItems: () => (_cachedMQTTSources || []).map((s: any) => ({
            value: s.id,
            label: s.name,
            icon: _icon(P.palette),
            desc: `${s.broker_host}:${s.broker_port}`,
        })),
        placeholder: t('palette.search'),
        allowNone: true,
        noneLabel: t('z2m_light.mqtt_source.none'),
    });

    // EntitySelect on the unified colour picker
    if (_colorSourceEntitySelect) { _colorSourceEntitySelect.destroy(); _colorSourceEntitySelect = null; }
    _colorSourceEntitySelect = new EntitySelect({
        target: colorSelect,
        getItems: () => _buildColorSourcePickerItems(),
        placeholder: t('palette.search'),
        onChange: (rawValue: string) => {
            // Decode "css:<id>" or "cvs:<id>" into source kind + id, then re-render
            // mapping rows so the LED-range fields appear/disappear in step.
            const { kind: newKind, id } = decodeColorSourceValue(rawValue);
            _editorColorVsId = newKind === 'color_vs' ? id : '';
            if (newKind !== _editorSourceKind) {
                _editorSourceKind = newKind;
                _rerenderMappingsForMode();
            } else {
                _setMappingsModeHint();
            }
        },
    });

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

    z2mLightEditorModal.open();
    z2mLightEditorModal.snapshot();
}

export async function closeZ2MLightEditor(): Promise<void> {
    await z2mLightEditorModal.close();
}

// ── Save ──

export async function saveZ2MLightEditor(): Promise<void> {
    const targetId = (document.getElementById('z2m-light-editor-id') as HTMLInputElement).value;
    const name = (document.getElementById('z2m-light-editor-name') as HTMLInputElement).value.trim();
    const mqttSourceId = (document.getElementById('z2m-light-editor-mqtt-source') as HTMLSelectElement).value;
    const baseTopic = (document.getElementById('z2m-light-editor-base-topic') as HTMLInputElement).value.trim() || 'zigbee2mqtt';
    const colorSourceRaw = (document.getElementById('z2m-light-editor-color-source') as HTMLSelectElement).value;
    const { kind: sourceKind, id: colorSourceId } = decodeColorSourceValue(colorSourceRaw);
    const cssSourceId = sourceKind === 'css' ? colorSourceId : '';
    const colorValueSourceId = sourceKind === 'color_vs' ? colorSourceId : '';

    const brightness = _widgets.getValue('brightness');
    const updateRate = _widgets.getValue('updateRate');
    const transition = _widgets.getValue('transition');
    const colorTolerance = _widgets.getValue('colorTolerance');
    const minBrightnessThreshold = _widgets.getValue('minBrightnessThreshold');
    const stopActionRaw = (document.getElementById('z2m-light-editor-stop-action') as HTMLSelectElement).value;
    const stopAction: 'none' | 'turn_off' = stopActionRaw === 'turn_off' ? 'turn_off' : 'none';
    const description = (document.getElementById('z2m-light-editor-description') as HTMLInputElement).value.trim() || null;

    if (!name) {
        z2mLightEditorModal.showError(t('z2m_light.error.name_required'));
        return;
    }
    if (!mqttSourceId) {
        z2mLightEditorModal.showError(t('z2m_light.error.mqtt_source_required') || 'MQTT broker is required');
        return;
    }
    if (sourceKind === 'color_vs' && !colorValueSourceId) {
        z2mLightEditorModal.showError(t('z2m_light.error.color_source_required'));
        return;
    }

    const mappings = JSON.parse(_getMappingsJSON()).filter((m: any) => m.friendly_name);
    if (mappings.length === 0) {
        z2mLightEditorModal.showError(t('z2m_light.error.mapping_required') || 'At least one bulb mapping is required');
        return;
    }

    const payload: any = {
        target_type: 'z2m_light',
        name,
        mqtt_source_id: mqttSourceId,
        source_kind: sourceKind,
        color_strip_source_id: cssSourceId,
        color_value_source_id: colorValueSourceId,
        brightness,
        z2m_light_mappings: mappings,
        base_topic: baseTopic,
        update_rate: updateRate,
        transition,
        color_tolerance: colorTolerance,
        min_brightness_threshold: minBrightnessThreshold,
        stop_action: stopAction,
        description,
        tags: _z2mLightTagsInput ? _z2mLightTagsInput.getValue() : [],
    };

    try {
        const response = targetId
            ? await fetchWithAuth(`/output-targets/${targetId}`, { method: 'PUT', body: JSON.stringify(payload) })
            : await fetchWithAuth('/output-targets', { method: 'POST', body: JSON.stringify(payload) });

        if (!response.ok) {
            const err = await response.json().catch(() => ({}));
            throw new Error(err.detail || `HTTP ${response.status}`);
        }
        showToast(targetId ? t('z2m_light.updated') : t('z2m_light.created'), 'success');
        outputTargetsCache.invalidate();
        z2mLightEditorModal.forceClose();
        if (window.loadTargetsTab) await window.loadTargetsTab();
    } catch (e: any) {
        if (e.isAuth) return;
        z2mLightEditorModal.showError(e.message);
    }
}

// ── Edit / Clone ──

export async function editZ2MLightTarget(targetId: string): Promise<void> {
    await showZ2MLightEditor(targetId);
}

export async function cloneZ2MLightTarget(targetId: string): Promise<void> {
    try {
        const resp = await fetchWithAuth(`/output-targets/${targetId}`);
        if (!resp.ok) throw new Error('Failed to load target');
        const data = await resp.json();
        delete data.id;
        data.name = data.name + ' (copy)';
        await showZ2MLightEditor(null, data);
    } catch (e: any) {
        if (e.isAuth) return;
        showToast(e.message, 'error');
    }
}

// ── Card rendering ──

export function createZ2MLightTargetCard(target: any, cssSourceMap: Record<string, any> = {}, valueSourceMap: Record<string, any> = {}): string {
    const cssSource = cssSourceMap[target.color_strip_source_id];
    const cssId = target.color_strip_source_id;
    const mappingCount = target.z2m_light_mappings?.length || 0;
    const isRunning = !!target.state?.processing;
    const state = target.state || {};

    // Brightness value source
    const bvsId = bindableSourceId(target.brightness);
    const bvs = bvsId && valueSourceMap[bvsId] ? valueSourceMap[bvsId] : null;

    // ── Badge / meta ──
    const badgeText = 'Z2M · LIGHT';
    const topicLabel = target.base_topic || 'zigbee2mqtt';
    const bulbsLabel = t('z2m_light.bulbs', { count: mappingCount });
    const rateLabel = `${target.update_rate ?? 5.0} Hz`;
    const metaHtml = [escapeHtml(topicLabel), bulbsLabel, rateLabel].join(' · ');

    // ── LEDs ──
    const leds: LedState[] = isRunning
        ? (state.mqtt_connected === false ? ['fault'] : ['on', 'blink'])
        : ['off'];

    // ── Chips ──
    const chips: ModChipOpts[] = [];
    const colorVsId: string = target.color_value_source_id || '';
    const colorVs = colorVsId && valueSourceMap[colorVsId] ? valueSourceMap[colorVsId] : null;
    if (target.source_kind === 'color_vs') {
        if (colorVs) {
            chips.push({
                icon: getValueSourceIcon(colorVs.source_type),
                text: colorVs.name,
                title: t('z2m_light.color_source'),
                onclick: `event.stopPropagation(); navigateToCard('streams','value','value-sources','data-id','${colorVsId}')`,
            });
        } else if (colorVsId) {
            chips.push({ icon: _icon(P.palette), text: colorVsId, title: t('z2m_light.color_source') });
        }
    } else if (cssSource) {
        chips.push({
            icon: getColorStripIcon(cssSource.source_type),
            text: cssSource.name,
            title: t('targets.color_strip_source'),
            onclick: `event.stopPropagation(); navigateToCard('streams','color_strip','color-strips','data-css-id','${cssId}')`,
        });
    } else if (cssId) {
        chips.push({ icon: _icon(P.palette), text: cssId, title: t('targets.color_strip_source') });
    }
    if (bvs) {
        chips.push({
            icon: getValueSourceIcon(bvs.source_type),
            text: bvs.name,
            title: t('targets.brightness_vs'),
            onclick: `event.stopPropagation(); navigateToCard('streams','value','value-sources','data-id','${bvsId}')`,
        });
    } else if (bindableValue(target.brightness, 1.0) < 1.0) {
        chips.push({
            icon: ICON_SUN,
            text: `${Math.round(bindableValue(target.brightness, 1.0) * 100)}%`,
            title: t('targets.brightness'),
        });
    }

    // ── Metrics (running only) ──
    const metrics: ModMetricOpts[] = [];
    if (isRunning) {
        metrics.push({ k: 'Hz', v: '<span data-tm="fps">---</span>', accent: true, title: t('targets.fps') });
        metrics.push({ k: t('device.metrics.uptime') || 'Uptime', icon: ICON_CLOCK, v: '<span data-tm="uptime">---</span>', title: t('device.metrics.uptime') });
        metrics.push({
            k: 'MQTT',
            icon: state.mqtt_connected ? ICON_OK : ICON_WARNING,
            v: '<span data-tm="mqtt-status">---</span>',
            title: t('z2m_light.connection_metric'),
        });
    }

    const extraHtml = isRunning ? `<div class="ha-light-swatches" data-z2m-swatches="${target.id}"></div>` : '';

    const patchState: 'live' | 'standby' | 'offline' | 'idle' =
        isRunning ? (state.mqtt_connected === false ? 'offline' : 'live') :
        cssId || colorVsId ? 'standby' : 'idle';
    const patchLabel = isRunning
        ? (state.mqtt_connected === false ? t('patch.disconnected') : t('patch.streaming'))
        : (cssId || colorVsId) ? t('patch.standby') : t('patch.not_configured');

    const primaryAction: ModBtnOpts = isRunning
        ? { label: t('common.stop'), icon: ICON_STOP, onclick: 'void 0', title: t('device.button.stop'), variant: 'stop', dataAttrs: { 'data-action': 'stop' } }
        : { label: t('common.start'), icon: ICON_START, onclick: 'void 0', title: t('device.button.start'), variant: 'go', dataAttrs: { 'data-action': 'start' } };

    const iconActions: ModBtnOpts[] = [];
    if (mappingCount > 0) {
        iconActions.push({ icon: ICON_STOP_PLAIN, onclick: 'void 0', title: t('z2m_light.button.turn_off') || 'Turn Off Bulbs', variant: 'stop', dataAttrs: { 'data-action': 'turn-off' } });
    }
    iconActions.push({ icon: ICON_EDIT, onclick: 'void 0', title: t('common.edit'), dataAttrs: { 'data-action': 'edit' } });

    const targetMenuExtraItems: ModMenuItemOpts[] = [];

    const mod: ModCardOpts = {
        head: {
            badge: { text: badgeText },
            name: target.name,
            metaHtml,
            leds,
            menu: {
                extraItems: targetMenuExtraItems,
                duplicateOnclick: `cloneZ2MLightTarget('${target.id}')`,
                hideOnclick: `toggleCardHidden('z2m-light-targets','${target.id}')`,
                deleteOnclick: `deleteTarget('${target.id}')`,
            },
        },
        body: {
            desc: target.description || undefined,
            metrics: metrics.length ? metrics : undefined,
            chips: chips.length ? chips : undefined,
            extraHtml: extraHtml || undefined,
        },
        foot: { patchState, patchLabel, primaryAction, iconActions },
        running: isRunning,
    };

    const cardHtml = wrapCard({ type: 'card', dataAttr: 'data-z2m-target-id', id: target.id, mod });
    const tags = renderTagChips(target.tags || []);
    return tags ? cardHtml.replace(/<\/div>\s*$/, `${tags}</div>`) : cardHtml;
}

export function patchZ2MLightTargetMetrics(target: any): void {
    const card = document.querySelector(`[data-z2m-target-id="${target.id}"]`);
    if (!card) return;
    const state = target.state || {};
    const metrics = target.metrics || {};

    const fpsEl = card.querySelector('[data-tm="fps"]') as HTMLElement | null;
    if (fpsEl) fpsEl.textContent = `${(state.fps_actual ?? 0).toFixed(1)}`;

    const uptimeEl = card.querySelector('[data-tm="uptime"]') as HTMLElement | null;
    if (uptimeEl) uptimeEl.textContent = metrics.uptime_seconds ? formatUptime(metrics.uptime_seconds) : '---';

    const mqttEl = card.querySelector('[data-tm="mqtt-status"]') as HTMLElement | null;
    if (mqttEl) mqttEl.innerHTML = state.mqtt_connected ? ICON_OK : ICON_WARNING;

    const swatchesEl = card.querySelector(`[data-z2m-swatches="${target.id}"]`) as HTMLElement | null;
    if (swatchesEl) swatchesEl.innerHTML = _renderEntitySwatches(state.entity_colors || {}, target.z2m_light_mappings || []);
}

// ── Event delegation ──

const _z2mLightActions: Record<string, (id: string) => void> = {
    start: (id) => _startStop(id, 'start'),
    stop: (id) => _startStop(id, 'stop'),
    clone: cloneZ2MLightTarget,
    edit: editZ2MLightTarget,
    'turn-off': (id) => { void turnOffZ2MLightTarget(id); },
};

async function _startStop(targetId: string, action: 'start' | 'stop'): Promise<void> {
    try {
        const resp = await fetchWithAuth(`/output-targets/${targetId}/${action}`, { method: 'POST' });
        if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
        outputTargetsCache.invalidate();
        if (window.loadTargetsTab) await window.loadTargetsTab();
    } catch (e: any) {
        if (e.isAuth) return;
        showToast(e.message, 'error');
    }
}

export async function turnOffZ2MLightTarget(targetId: string): Promise<void> {
    const confirmed = await showConfirm(t('confirm.turn_off_z2m_light') || 'Turn off mapped bulbs?');
    if (!confirmed) return;
    try {
        const resp = await fetchWithAuth(`/output-targets/${targetId}/z2m-light/turn-off`, { method: 'POST' });
        if (resp.ok) {
            showToast(t('z2m_light.turn_off.success') || 'Bulbs turned off', 'success');
        } else {
            const err = await resp.json().catch(() => ({}));
            showToast(err.detail || t('z2m_light.turn_off.failed') || 'Failed to turn off bulbs', 'error');
        }
    } catch (e: any) {
        if (e.isAuth) return;
        showToast(t('z2m_light.turn_off.failed') || 'Failed to turn off bulbs', 'error');
    }
}

export function initZ2MLightTargetDelegation(container: HTMLElement): void {
    container.addEventListener('click', (e: MouseEvent) => {
        const btn = (e.target as HTMLElement).closest<HTMLElement>('[data-action]');
        if (!btn) return;
        const section = btn.closest<HTMLElement>('[data-card-section="z2m-light-targets"]');
        if (!section) return;
        const card = btn.closest<HTMLElement>('[data-z2m-target-id]');
        if (!card) return;
        const action = btn.dataset.action;
        const id = card.getAttribute('data-z2m-target-id');
        if (!action || !id) return;
        const handler = _z2mLightActions[action];
        if (handler) {
            e.stopPropagation();
            handler(id);
        }
    });
}

function _renderEntitySwatches(entityColors: Record<string, any>, mappings: any[]): string {
    if (!mappings.length) return '';
    return mappings.map(m => {
        const c = entityColors[m.friendly_name];
        const bg = c ? c.hex : '#333';
        return `<div class="ha-light-swatch" data-entity="${escapeHtml(m.friendly_name)}" title="${escapeHtml(m.friendly_name)}">
            <span class="swatch-color" style="background:${bg}"></span>
            <span class="swatch-label">${escapeHtml(m.friendly_name)}</span>
        </div>`;
    }).join('');
}

// ── WebSocket color preview ──

const _z2mLightWS: Record<string, WebSocket> = {};

export function connectZ2MLightWS(targetId: string): void {
    if (_z2mLightWS[targetId]) return;
    const loc = window.location;
    const wsProto = loc.protocol === 'https:' ? 'wss:' : 'ws:';
    const url = `${wsProto}//${loc.host}/api/v1/output-targets/${targetId}/z2m-light/ws`;

    openAuthedWs(url).then((ws) => {
        _z2mLightWS[targetId] = ws;
        ws.onmessage = (ev) => {
            try {
                const data = JSON.parse(ev.data);
                if (data.type === 'colors_update') _updateSwatchColors(targetId, data.colors);
            } catch (err) { logError('z2m-light-targets.ws.message', err); }
        };
        ws.onclose = () => { delete _z2mLightWS[targetId]; };
        ws.onerror = () => { delete _z2mLightWS[targetId]; };
    }).catch(() => { delete _z2mLightWS[targetId]; });
}

export function disconnectZ2MLightWS(targetId: string): void {
    const ws = _z2mLightWS[targetId];
    if (ws) { ws.close(); delete _z2mLightWS[targetId]; }
}

function _updateSwatchColors(targetId: string, colors: Record<string, any>): void {
    const container = document.querySelector(`[data-z2m-swatches="${targetId}"]`);
    if (!container) return;
    for (const [friendly, c] of Object.entries(colors)) {
        const swatch = container.querySelector(`[data-entity="${friendly}"] .swatch-color`) as HTMLElement | null;
        if (swatch) swatch.style.background = (c as any).hex;
    }
}

// ── Expose to global scope ──

window.showZ2MLightEditor = showZ2MLightEditor;
window.closeZ2MLightEditor = closeZ2MLightEditor;
window.saveZ2MLightEditor = saveZ2MLightEditor;
window.editZ2MLightTarget = editZ2MLightTarget;
window.cloneZ2MLightTarget = cloneZ2MLightTarget;
window.addZ2MLightMapping = addZ2MLightMapping;
window.removeZ2MLightMapping = removeZ2MLightMapping;
