/**
 * HA Light Targets — editor, cards, CRUD for Home Assistant light output targets.
 */

import {
    _cachedHASources, _cachedValueSources, haSourcesCache,
    colorStripSourcesCache, outputTargetsCache, valueSourcesCache,
    getHAEntityFriendlyName, setHAEntityNames,
} 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, getHAEntityIcon } 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 { renderDeviceIconSvg } from '../core/device-icons.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_HA = `<svg class="icon" viewBox="0 0 24 24">${P.home}</svg>`;
const _icon = (d: string) => `<svg class="icon" viewBox="0 0 24 24">${d}</svg>`;

// ── Modal ──

type HALightSourceKind = 'css' | 'color_vs';

let _haLightTagsInput: TagInput | null = null;
let _haSourceEntitySelect: EntitySelect | null = null;
let _cssSourceEntitySelect: EntitySelect | null = null;
let _mappingEntitySelects: EntitySelect[] = [];
let _editorCssSources: any[] = [];
let _editorColorValueSources: any[] = [];  // value sources with return_type='color'
let _cachedHAEntities: any[] = [];  // fetched from selected HA source
let _stopActionIconSelect: IconSelect | null = null;
// Shared bundle of brightness/update_rate/transition/color_tolerance/min_brightness_threshold widgets.
// Defaults match the previous per-widget config — HA-light uses every default unchanged.
const _widgets = new LightTargetWidgets({
    idPrefix: 'ha-light-editor',
    valueSources: () => _cachedValueSources,
});
// Active mode + selected colour value source id (only used in color_vs mode).
let _editorSourceKind: HALightSourceKind = 'css';
let _editorColorVsId: string = '';

class HALightEditorModal extends Modal {
    constructor() { super('ha-light-editor-modal'); }

    onForceClose() {
        if (_haLightTagsInput) { _haLightTagsInput.destroy(); _haLightTagsInput = null; }
        if (_haSourceEntitySelect) { _haSourceEntitySelect.destroy(); _haSourceEntitySelect = null; }
        if (_cssSourceEntitySelect) { _cssSourceEntitySelect.destroy(); _cssSourceEntitySelect = null; }
        if (_stopActionIconSelect) { _stopActionIconSelect.destroy(); _stopActionIconSelect = null; }
        _widgets.destroyAll();
        _destroyMappingEntitySelects();
    }

    snapshotValues() {
        return {
            name: (document.getElementById('ha-light-editor-name') as HTMLInputElement).value,
            ha_source: (document.getElementById('ha-light-editor-ha-source') as HTMLSelectElement).value,
            color_source: (document.getElementById('ha-light-editor-css-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('ha-light-editor-stop-action') as HTMLSelectElement).value,
            mappings: _getMappingsJSON(),
            tags: JSON.stringify(_haLightTagsInput ? _haLightTagsInput.getValue() : []),
        };
    }
}

const haLightEditorModal = new HALightEditorModal();

function _destroyMappingEntitySelects(): void {
    for (const es of _mappingEntitySelects) es.destroy();
    _mappingEntitySelects = [];
}

function _getMappingsJSON(): string {
    const rows = document.querySelectorAll('#ha-light-mappings-list .ha-light-mapping-row');
    const mappings: any[] = [];
    rows.forEach(row => {
        const ledStartEl = row.querySelector('.ha-mapping-led-start') as HTMLInputElement | null;
        const ledEndEl = row.querySelector('.ha-mapping-led-end') as HTMLInputElement | null;
        // In color_vs mode the LED range inputs are not rendered; fall back to
        // safe defaults so mappings round-trip if the user toggles back to CSS.
        mappings.push({
            entity_id: (row.querySelector('.ha-mapping-entity') as HTMLSelectElement).value.trim(),
            led_start: ledStartEl ? (parseInt(ledStartEl.value) || 0) : 0,
            led_end: ledEndEl ? (parseInt(ledEndEl.value) || -1) : -1,
            brightness_scale: parseFloat((row.querySelector('.ha-mapping-brightness') as HTMLInputElement).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('ha_light.color_source.css'),
            colorVsHeader: t('ha_light.color_source.color_vs'),
        },
    );
}

function _setMappingsModeHint(): void {
    const hintColorVs = document.getElementById('ha-light-mappings-mode-hint');
    const hintCss = document.querySelectorAll('#ha-light-editor-modal small.input-hint[data-i18n="ha_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';
}

function _getEntityItems() {
    return _cachedHAEntities
        .filter((e: any) => e.domain === 'light')
        .map((e: any) => ({
            value: e.entity_id,
            label: e.friendly_name || e.entity_id,
            icon: getHAEntityIcon(e),
            desc: e.state || '',
        }));
}

async function _fetchHAEntities(haSourceId: string): Promise<void> {
    if (!haSourceId) { _cachedHAEntities = []; return; }
    try {
        const resp = await fetchWithAuth(`/home-assistant/sources/${haSourceId}/entities`);
        if (!resp.ok) { _cachedHAEntities = []; return; }
        const data = await resp.json();
        _cachedHAEntities = data.entities || [];
        // Mirror into the shared cache so card chips/swatches across the
        // app pick up friendly names on the next render.
        setHAEntityNames(haSourceId, _cachedHAEntities);
    } catch {
        _cachedHAEntities = [];
    }
}

function _rebuildMappingEntityOptions(): void {
    // Update all existing mapping entity <select> elements with new options
    const lightEntities = _cachedHAEntities.filter((e: any) => e.domain === 'light');
    const rows = document.querySelectorAll('#ha-light-mappings-list .ha-light-mapping-row');
    rows.forEach(row => {
        const sel = row.querySelector('.ha-mapping-entity') as HTMLSelectElement;
        if (!sel) return;
        const currentVal = sel.value;
        sel.innerHTML = `<option value="">${t('ha_light.mapping.select_entity')}</option>` +
            lightEntities.map((e: any) =>
                `<option value="${e.entity_id}" ${e.entity_id === currentVal ? 'selected' : ''}>${escapeHtml(e.friendly_name || e.entity_id)}</option>`
            ).join('');
        // Restore value if it was set but not in list
        if (currentVal && !lightEntities.some((e: any) => e.entity_id === currentVal)) {
            sel.innerHTML += `<option value="${escapeHtml(currentVal)}" selected>${escapeHtml(currentVal)}</option>`;
        }
    });
    // Rebuild EntitySelect instances (destroy old, create new)
    _destroyMappingEntitySelects();
    rows.forEach(row => {
        const sel = row.querySelector('.ha-mapping-entity') as HTMLSelectElement;
        if (!sel) return;
        const es = new EntitySelect({
            target: sel,
            getItems: _getEntityItems,
            placeholder: t('ha_light.mapping.search_entity'),
        });
        _mappingEntitySelects.push(es);
    });
}

// ── Mapping rows ──

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

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

    // Build options from cached entities (light domain only)
    const lightEntities = _cachedHAEntities.filter((e: any) => e.domain === 'light');
    const selectedId = data?.entity_id || '';
    const entityOptions = `<option value="">${t('ha_light.mapping.select_entity')}</option>` +
        lightEntities.map((e: any) =>
            `<option value="${e.entity_id}" ${e.entity_id === selectedId ? 'selected' : ''}>${escapeHtml(e.friendly_name || e.entity_id)}</option>`
        ).join('');
    // If editing and the entity isn't in the list (HA disconnected), add it manually
    const hasSelected = !selectedId || lightEntities.some((e: any) => e.entity_id === selectedId);
    const extraOption = (!hasSelected && selectedId)
        ? `<option value="${escapeHtml(selectedId)}" selected>${escapeHtml(selectedId)}</option>`
        : '';

    // Header preview: show the resolved friendly name when available, fall
    // back to raw entity_id (HA disconnected case), or a placeholder dash
    // when no entity is selected yet.
    const previewText = (() => {
        if (!selectedId) return t('ha_light.mapping.unassigned') || '—';
        const ent = lightEntities.find((e: any) => e.entity_id === selectedId);
        return ent ? (ent.friendly_name || ent.entity_id) : selectedId;
    })();
    const previewIsEmpty = !selectedId;

    // 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('ha_light.mapping.brightness')}</label>
                    <input type="number" class="ha-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('ha_light.mapping.led_start')}</label>
                    <input type="number" class="ha-mapping-led-start" value="${data?.led_start ?? 0}" min="0" step="1">
                </div>
                <div>
                    <label>${t('ha_light.mapping.led_end')}</label>
                    <input type="number" class="ha-mapping-led-end" value="${data?.led_end ?? -1}" min="-1" step="1">
                </div>
                <div>
                    <label>${t('ha_light.mapping.brightness')}</label>
                    <input type="number" class="ha-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="removeHALightMapping(this)" title="${escapeHtml(removeLabel)}" aria-label="${escapeHtml(removeLabel)}">${ICON_TRASH}</button>
        </div>
        <div class="ha-mapping-fields">
            <div class="ha-mapping-field">
                <label>${t('ha_light.mapping.entity_id')}</label>
                <select class="ha-mapping-entity">${entityOptions}${extraOption}</select>
            </div>
            ${rangeBlock}
        </div>
    `;
    list.appendChild(row);

    // Attach EntitySelect to the entity dropdown
    const entitySelect = row.querySelector('.ha-mapping-entity') as HTMLSelectElement;
    const es = new EntitySelect({
        target: entitySelect,
        getItems: _getEntityItems,
        placeholder: t('ha_light.mapping.search_entity'),
    });
    _mappingEntitySelects.push(es);

    // Live-update the header preview when the user picks a different entity.
    // EntitySelect mutates the underlying <select>'s value/selectedIndex and
    // dispatches `change` on it, so listening here is enough.
    const previewEl = row.querySelector('.ha-mapping-name') as HTMLElement | null;
    if (previewEl) {
        const placeholder = t('ha_light.mapping.unassigned') || '—';
        entitySelect.addEventListener('change', () => {
            const opt = entitySelect.options[entitySelect.selectedIndex];
            const hasValue = !!(opt && opt.value);
            previewEl.textContent = hasValue ? opt.text : placeholder;
            previewEl.classList.toggle('is-empty', !hasValue);
        });
    }
}

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

export function removeHALightMapping(btn: HTMLElement): void {
    const row = btn.closest('.ha-light-mapping-row');
    if (!row) return;
    // Find and destroy the EntitySelect for this row
    const select = row.querySelector('.ha-mapping-entity') as HTMLSelectElement;
    if (select) {
        const idx = _mappingEntitySelects.findIndex(es => (es as any)._target === select || (es as any).target === select);
        if (idx >= 0) {
            _mappingEntitySelects[idx].destroy();
            _mappingEntitySelects.splice(idx, 1);
        }
    }
    row.remove();
}

// ── Stop-action IconSelect items ──

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

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

// ── Show / Close ──

export async function showHALightEditor(targetId: string | null = null, cloneData: any = null): Promise<void> {
    // Load data for dropdowns
    const [haSources, cssSources] = await Promise.all([
        haSourcesCache.fetch().catch((): any[] => []),
        colorStripSourcesCache.fetch().catch((): any[] => []),
        valueSourcesCache.fetch().catch(() => {}),
    ]);
    _editorCssSources = cssSources;
    _editorColorValueSources = (_cachedValueSources || []).filter(_isColorValueSource);

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

    // Populate HA source dropdown
    const haSelect = document.getElementById('ha-light-editor-ha-source') as HTMLSelectElement;
    haSelect.innerHTML = haSources.map((s: any) =>
        `<option value="${s.id}">${escapeHtml(s.name)}</option>`
    ).join('');

    // Unified Color Source picker — combines CSS sources + colour-returning value sources.
    const colorSelect = document.getElementById('ha-light-editor-css-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
    _destroyMappingEntitySelects();
    document.getElementById('ha-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('ha-light-editor-id') as HTMLInputElement).value = editData.id;
        (document.getElementById('ha-light-editor-name') as HTMLInputElement).value = editData.name || '';
        haSelect.value = editData.ha_source_id || '';
        _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('ha-light-editor-stop-action') as HTMLSelectElement).value = editData.stop_action ?? 'none';
        _ensureStopActionIconSelect();
        if (_stopActionIconSelect) _stopActionIconSelect.setValue(editData.stop_action ?? 'none', false);
        (document.getElementById('ha-light-editor-description') as HTMLInputElement).value = editData.description || '';

        // Fetch entities from the selected HA source before loading mappings
        await _fetchHAEntities(editData.ha_source_id || '');

        // Load mappings (with entity picker populated)
        const mappings = editData.ha_light_mappings || [];
        mappings.forEach((m: any) => addHALightMapping(m));
    } else {
        (document.getElementById('ha-light-editor-name') as HTMLInputElement).value = '';
        _editorSourceKind = 'css';
        _editorColorVsId = '';
        _widgets.applyDefaults();
        (document.getElementById('ha-light-editor-stop-action') as HTMLSelectElement).value = 'none';
        _ensureStopActionIconSelect();
        if (_stopActionIconSelect) _stopActionIconSelect.setValue('none', false);
        (document.getElementById('ha-light-editor-description') as HTMLInputElement).value = '';

        // Fetch entities from the first HA source
        const firstSource = haSources[0]?.id || '';
        await _fetchHAEntities(firstSource);

        // Add one empty mapping by default
        addHALightMapping();
    }
    _setMappingsModeHint();

    // EntitySelects
    if (_haSourceEntitySelect) { _haSourceEntitySelect.destroy(); _haSourceEntitySelect = null; }
    _haSourceEntitySelect = new EntitySelect({
        target: haSelect,
        getItems: () => haSources.map((s: any) => ({
            value: s.id, label: s.name, icon: ICON_HA,
            desc: s.connected ? t('ha_source.connected') : t('ha_source.disconnected'),
        })),
        placeholder: t('palette.search'),
        onChange: async (newSourceId: string) => {
            // Refetch entities when HA source changes, then rebuild mapping selects
            await _fetchHAEntities(newSourceId);
            _rebuildMappingEntityOptions();
        },
    });

    if (_cssSourceEntitySelect) { _cssSourceEntitySelect.destroy(); _cssSourceEntitySelect = null; }
    _cssSourceEntitySelect = 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 (_haLightTagsInput) { _haLightTagsInput.destroy(); _haLightTagsInput = null; }
    _haLightTagsInput = new TagInput(document.getElementById('ha-light-tags-container'), { placeholder: t('tags.placeholder') });
    _haLightTagsInput.setValue(editData?.tags || []);

    haLightEditorModal.open();
    haLightEditorModal.snapshot();
}

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

// ── Save ──

export async function saveHALightEditor(): Promise<void> {
    const targetId = (document.getElementById('ha-light-editor-id') as HTMLInputElement).value;
    const name = (document.getElementById('ha-light-editor-name') as HTMLInputElement).value.trim();
    const haSourceId = (document.getElementById('ha-light-editor-ha-source') as HTMLSelectElement).value;
    const colorSourceRaw = (document.getElementById('ha-light-editor-css-source') as HTMLSelectElement).value;
    // Decode the unified picker value: "css:<id>" or "cvs:<id>" (or "" for none).
    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('ha-light-editor-stop-action') as HTMLSelectElement).value;
    const stopAction: 'none' | 'turn_off' | 'restore' =
        stopActionRaw === 'turn_off' || stopActionRaw === 'restore' ? stopActionRaw : 'none';
    const description = (document.getElementById('ha-light-editor-description') as HTMLInputElement).value.trim() || null;

    if (!name) {
        haLightEditorModal.showError(t('ha_light.error.name_required'));
        return;
    }
    if (!haSourceId) {
        haLightEditorModal.showError(t('ha_light.error.ha_source_required'));
        return;
    }
    if (sourceKind === 'color_vs' && !colorValueSourceId) {
        haLightEditorModal.showError(t('ha_light.error.color_source_required'));
        return;
    }

    // Collect mappings
    const mappings = JSON.parse(_getMappingsJSON()).filter((m: any) => m.entity_id);

    const payload: any = {
        name,
        ha_source_id: haSourceId,
        source_kind: sourceKind,
        color_strip_source_id: cssSourceId,
        color_value_source_id: colorValueSourceId,
        brightness,
        ha_light_mappings: mappings,
        update_rate: updateRate,
        transition,
        color_tolerance: colorTolerance,
        min_brightness_threshold: minBrightnessThreshold,
        stop_action: stopAction,
        description,
        tags: _haLightTagsInput ? _haLightTagsInput.getValue() : [],
    };

    payload.target_type = 'ha_light';

    try {
        let response;
        if (targetId) {
            response = await fetchWithAuth(`/output-targets/${targetId}`, {
                method: 'PUT',
                body: JSON.stringify(payload),
            });
        } else {
            response = 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('ha_light.updated') : t('ha_light.created'), 'success');
        outputTargetsCache.invalidate();
        haLightEditorModal.forceClose();
        // Reload targets tab
        if (window.loadTargetsTab) await window.loadTargetsTab();
    } catch (e: any) {
        if (e.isAuth) return;
        haLightEditorModal.showError(e.message);
    }
}

// ── Edit / Clone / Delete ──

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

export async function cloneHALightTarget(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 showHALightEditor(null, data);
    } catch (e: any) {
        if (e.isAuth) return;
        showToast(e.message, 'error');
    }
}

// ── Card rendering ──

export function createHALightTargetCard(target: any, haSourceMap: Record<string, any> = {}, cssSourceMap: Record<string, any> = {}, valueSourceMap: Record<string, any> = {}): string {
    const haSource = haSourceMap[target.ha_source_id];
    const cssSource = cssSourceMap[target.color_strip_source_id];
    const cssId = target.color_strip_source_id;
    const mappingCount = target.ha_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 = 'HA · LIGHT';
    const haLabel = haSource ? haSource.name : (target.ha_source_id || '—');
    const haConnectionTitle = t('ha_light.connection_tooltip');
    const haLink = haSource
        ? `<a class="mod-meta__link" role="button" tabindex="0" onclick="event.stopPropagation(); navigateToCard('integrations','home_assistant','ha-sources','data-id','${target.ha_source_id}')" title="${escapeHtml(haConnectionTitle)}">${escapeHtml(haLabel)}</a>`
        : escapeHtml(haLabel);
    const lightsLabel = t('ha_light.lights', { count: mappingCount });
    const rateLabel = `${target.update_rate ?? 2.0} Hz`;
    const metaHtml = [haLink, lightsLabel, rateLabel].join(' · ');

    // ── LEDs ──
    const leds: LedState[] = isRunning
        ? (state.ha_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('ha_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('ha_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: 'FPS',
            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: 'HA',
            icon: state.ha_connected ? ICON_OK : ICON_WARNING,
            v: '<span data-tm="ha-status">---</span>',
            title: t('ha_light.connection_metric'),
        });
    }

    // ── Live entity color swatches (running) ──
    const extraHtml = isRunning
        ? `<div class="ha-light-swatches" data-ha-swatches="${target.id}"></div>`
        : '';

    // ── Patch indicator ──
    const patchState: 'live' | 'standby' | 'offline' | 'idle' =
        isRunning ? (state.ha_connected === false ? 'offline' : 'live') :
        haSource ? 'standby' : 'idle';
    const patchLabel = isRunning
        ? (state.ha_connected === false ? t('patch.disconnected') : t('patch.streaming'))
        : haSource
            ? t('patch.standby')
            : t('patch.not_configured');

    // ── Foot actions — uses event delegation via [data-action] ──
    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[] = [];

    // Power-off shortcut: only useful when at least one entity is mapped.
    // Mirrors the LED-device card's stop-style power button.
    if (mappingCount > 0) {
        iconActions.push({
            icon: ICON_STOP_PLAIN,
            onclick: 'void 0',
            title: t('ha_light.button.turn_off') || 'Turn Off Lights',
            variant: 'stop',
            dataAttrs: { 'data-action': 'turn-off' },
        });
    }

    iconActions.push({
        icon: ICON_EDIT,
        onclick: 'void 0',
        title: t('common.edit'),
        dataAttrs: { 'data-action': 'edit' },
    });

    // ── Custom icon plate (HA-light targets have no inheritance source) ──
    const targetIconId = (target.icon as string | undefined) || '';
    const targetIconColor = (target.icon_color as string | undefined) || '';
    const iconHtml = targetIconId ? renderDeviceIconSvg(targetIconId, { size: 24 }) : '';
    const iconTitle = targetIconId
        ? (t('device.icon.change') || 'Change icon…')
        : (t('device.icon.choose') || 'Choose icon…');

    const targetMenuExtraItems: ModMenuItemOpts[] = [
        {
            label: targetIconId
                ? (t('device.icon.change') || 'Change icon…')
                : (t('device.icon.choose') || 'Choose icon…'),
            icon: ICON_EDIT,
            dataAttrs: { 'data-icon-picker-trigger': `target:${target.id}` },
        },
    ];

    const mod: ModCardOpts = {
        head: {
            badge: { text: badgeText },
            name: target.name,
            metaHtml,
            leds,
            iconHtml,
            iconColor: targetIconColor,
            iconAttrs: { 'data-icon-picker-trigger': `target:${target.id}` },
            iconTitle,
            menu: {
                extraItems: targetMenuExtraItems,
                duplicateOnclick: `cloneHALightTarget('${target.id}')`,
                hideOnclick: `toggleCardHidden('ha-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-ha-target-id',
        id: target.id,
        mod,
    });
    const tags = renderTagChips(target.tags || []);
    return tags
        ? cardHtml.replace(/<\/div>\s*$/, `${tags}</div>`)
        : cardHtml;
}

// ── Metrics patching ──

export function patchHALightTargetMetrics(target: any): void {
    const card = document.querySelector(`[data-ha-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)} Hz`;

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

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

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

// ── Event delegation ──

const _haLightActions: Record<string, (id: string) => void> = {
    start: (id) => _startStop(id, 'start'),
    stop: (id) => _startStop(id, 'stop'),
    clone: cloneHALightTarget,
    edit: editHALightTarget,
    'turn-off': (id) => { void turnOffHALightTarget(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 turnOffHALightTarget(targetId: string): Promise<void> {
    const confirmed = await showConfirm(t('confirm.turn_off_ha_light') || 'Turn off mapped lights?');
    if (!confirmed) return;
    try {
        const resp = await fetchWithAuth(
            `/output-targets/${targetId}/ha-light/turn-off`,
            { method: 'POST' },
        );
        if (resp.ok) {
            showToast(t('ha_light.turn_off.success') || 'Lights turned off', 'success');
        } else {
            const err = await resp.json().catch(() => ({}));
            showToast(err.detail || t('ha_light.turn_off.failed') || 'Failed to turn off lights', 'error');
        }
    } catch (e: any) {
        if (e.isAuth) return;
        showToast(t('ha_light.turn_off.failed') || 'Failed to turn off lights', 'error');
    }
}

export function initHALightTargetDelegation(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="ha-light-targets"]');
        if (!section) return;
        const card = btn.closest<HTMLElement>('[data-ha-target-id]');
        if (!card) return;

        const action = btn.dataset.action;
        const id = card.getAttribute('data-ha-target-id');
        if (!action || !id) return;

        const handler = _haLightActions[action];
        if (handler) {
            e.stopPropagation();
            handler(id);
        }
    });
}

// ── Entity color swatches ──

function _renderEntitySwatches(
    entityColors: Record<string, any>,
    mappings: any[],
    haSourceId: string = '',
): string {
    if (!mappings.length) return '';
    return mappings.map(m => {
        const c = entityColors[m.entity_id];
        const bg = c ? c.hex : '#333';
        // Prefer the cached friendly name; fall back to the raw entity_id
        // (minus the `light.` domain prefix) when no name is cached yet.
        const friendly = getHAEntityFriendlyName(haSourceId, m.entity_id);
        const label = friendly !== m.entity_id ? friendly : m.entity_id.replace('light.', '');
        return `<div class="ha-light-swatch" data-entity="${escapeHtml(m.entity_id)}" title="${escapeHtml(m.entity_id)}">
            <span class="swatch-color" style="background:${bg}"></span>
            <span class="swatch-label">${escapeHtml(label)}</span>
        </div>`;
    }).join('');
}

// ── WebSocket color preview ──

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

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

    openAuthedWs(url).then((ws) => {
        _haLightWS[targetId] = ws;

        ws.onmessage = (ev) => {
            try {
                const data = JSON.parse(ev.data);
                if (data.type === 'colors_update') {
                    _updateSwatchColors(targetId, data.colors);
                }
            } catch (err) { logError('ha-light-targets.ws.message', err); }
        };

        ws.onclose = () => {
            delete _haLightWS[targetId];
        };

        ws.onerror = () => {
            delete _haLightWS[targetId];
        };
    }).catch(() => {
        delete _haLightWS[targetId];
    });
}

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

export function disconnectAllHALightWS(): void {
    for (const id of Object.keys(_haLightWS)) {
        disconnectHALightWS(id);
    }
}

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

// ── Expose to global scope ──

window.showHALightEditor = showHALightEditor;
window.closeHALightEditor = closeHALightEditor;
window.saveHALightEditor = saveHALightEditor;
window.editHALightTarget = editHALightTarget;
window.cloneHALightTarget = cloneHALightTarget;
window.addHALightMapping = addHALightMapping;
window.removeHALightMapping = removeHALightMapping;
