/**
 * Device cards — settings modal, brightness, power, color.
 */

import {
    _deviceBrightnessCache, updateDeviceBrightness,
    csptCache,
} from '../core/state.ts';
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml, isSerialDevice, isMockDevice, isMqttDevice, isWsDevice, isOpenrgbDevice, isDmxDevice, isDdpDevice, isOpcDevice, isYeelightDevice, isWizDevice, isLifxDevice, isGoveeDevice, isNanoleafDevice, isBleDevice, isGroupDevice } from '../core/api.ts';
import { devicesCache } from '../core/state.ts';
import { _fetchOpenrgbZones, _getCheckedZones, _splitOpenrgbZone, _getZoneMode, ensureDmxProtocolIconSelect, destroyDmxProtocolIconSelect, ensureDdpColorOrderIconSelect, destroyDdpColorOrderIconSelect, ensureSpiLedTypeIconSelect, destroySpiLedTypeIconSelect, ensureGameSenseDeviceTypeIconSelect, destroyGameSenseDeviceTypeIconSelect, addGroupChildSettingsWithId as _addGroupChildSettingsWithId, ensureGroupModeIconSelect, destroyGroupModeIconSelect, ensureBleFamilyIconSelect, destroyBleFamilyIconSelect } from './device-discovery.ts';
import { t } from '../core/i18n.ts';
import { showToast, showConfirm, desktopFocus } from '../core/ui.ts';
import { Modal } from '../core/modal.ts';
import { ICON_SETTINGS, ICON_STOP_PLAIN, ICON_LED, ICON_WEB, ICON_REFRESH, ICON_TEMPLATE } from '../core/icons.ts';
import { wrapCard } from '../core/card-colors.ts';
import type { ModCardOpts, LedState, ModMetricOpts, ModChipOpts, ModBtnOpts, ModMenuItemOpts } from '../core/mod-card.ts';
import { TagInput, renderTagChips } from '../core/tag-input.ts';
import { EntitySelect } from '../core/entity-palette.ts';
import { getBaseOrigin } from './settings.ts';
import type { Device } from '../types.ts';
import { renderDeviceIconSvg } from '../core/device-icons.ts';
import { ICON_EDIT } from '../core/icons.ts';

let _deviceTagsInput: any = null;
let _settingsCsptEntitySelect: any = null;

/* The General Settings modal groups its many conditional fields into
   four `.ds-section` panels (Identity / Connection / Hardware / Behavior).
   showSettings() toggles individual `.form-group` visibility by device
   type and capability — this helper then collapses any section whose
   form-groups have all ended up `display: none`, so the user never
   sees a section header with nothing underneath it. */
function _updateSettingsSectionVisibility() {
    const root = document.getElementById('device-settings-modal');
    if (!root) return;
    const sections = root.querySelectorAll<HTMLElement>('.ds-section');
    sections.forEach((sec) => {
        if (sec.dataset.dsKey === 'identity') {
            sec.dataset.dsEmpty = 'false';
            return;
        }
        const groups = sec.querySelectorAll<HTMLElement>('.form-group');
        let anyVisible = false;
        groups.forEach((g) => {
            if (g.style.display !== 'none') anyVisible = true;
        });
        sec.dataset.dsEmpty = anyVisible ? 'false' : 'true';
    });
}

function _ensureSettingsCsptSelect() {
    const sel = document.getElementById('settings-css-processing-template') as HTMLSelectElement | null;
    if (!sel) return;
    const templates = csptCache.data || [];
    sel.innerHTML = `<option value="">${t('common.none_no_cspt')}</option>` +
        templates.map((tp: any) => `<option value="${tp.id}">${tp.name}</option>`).join('');
    if (_settingsCsptEntitySelect) _settingsCsptEntitySelect.destroy();
    if (templates.length > 0) {
        _settingsCsptEntitySelect = new EntitySelect({
            target: sel,
            getItems: () => (csptCache.data || []).map((tp: any) => ({
                value: tp.id,
                label: tp.name,
                icon: ICON_TEMPLATE,
                desc: '',
            })),
            placeholder: t('palette.search'),
            allowNone: true,
            noneLabel: t('common.none_no_cspt'),
        } as any);
    }
}

class DeviceSettingsModal extends Modal {
    constructor() { super('device-settings-modal'); }

    deviceType = '';
    capabilities: string[] = [];

    snapshotValues() {
        return {
            name: (this.$('settings-device-name') as HTMLInputElement).value,
            url: this._getUrl(),
            state_check_interval: (this.$('settings-health-interval') as HTMLInputElement).value,
            auto_shutdown: (this.$('settings-auto-shutdown') as HTMLInputElement).checked,
            led_count: (this.$('settings-led-count') as HTMLInputElement).value,
            led_type: (document.getElementById('settings-led-type') as HTMLSelectElement | null)?.value || 'rgb',
            send_latency: (document.getElementById('settings-send-latency') as HTMLInputElement | null)?.value || '0',
            zones: JSON.stringify(_getCheckedZones('settings-zone-list')),
            zoneMode: _getZoneMode('settings-zone-mode'),
            tags: JSON.stringify(_deviceTagsInput ? _deviceTagsInput.getValue() : []),
            dmxProtocol: (document.getElementById('settings-dmx-protocol') as HTMLSelectElement | null)?.value || 'artnet',
            dmxStartUniverse: (document.getElementById('settings-dmx-start-universe') as HTMLInputElement | null)?.value || '0',
            dmxStartChannel: (document.getElementById('settings-dmx-start-channel') as HTMLInputElement | null)?.value || '1',
            opcChannel: (document.getElementById('settings-opc-channel') as HTMLInputElement | null)?.value || '0',
            bleFamily: (document.getElementById('settings-ble-family') as HTMLSelectElement | null)?.value || '',
            bleGoveeKey: (document.getElementById('settings-ble-govee-key') as HTMLInputElement | null)?.value || '',
            yeelightMinInterval: (document.getElementById('settings-yeelight-min-interval') as HTMLInputElement | null)?.value || '500',
            wizMinInterval: (document.getElementById('settings-wiz-min-interval') as HTMLInputElement | null)?.value || '50',
            lifxMinInterval: (document.getElementById('settings-lifx-min-interval') as HTMLInputElement | null)?.value || '50',
            goveeMinInterval: (document.getElementById('settings-govee-min-interval') as HTMLInputElement | null)?.value || '50',
            nanoleafMinInterval: (document.getElementById('settings-nanoleaf-min-interval') as HTMLInputElement | null)?.value || '100',
            csptId: (document.getElementById('settings-css-processing-template') as HTMLSelectElement | null)?.value || '',
        };
    }

    _getUrl() {
        if (isGroupDevice(this.deviceType)) {
            return 'group://virtual';
        }
        if (isMockDevice(this.deviceType)) {
            const deviceId = (this.$('settings-device-id') as HTMLInputElement | null)?.value || '';
            return `mock://${deviceId}`;
        }
        if (isWsDevice(this.deviceType)) {
            const deviceId = (this.$('settings-device-id') as HTMLInputElement | null)?.value || '';
            return `ws://${deviceId}`;
        }
        if (isSerialDevice(this.deviceType)) {
            return (this.$('settings-serial-port') as HTMLSelectElement).value;
        }
        let url = (this.$('settings-device-url') as HTMLInputElement).value.trim();
        // Append selected zones for OpenRGB
        if (isOpenrgbDevice(this.deviceType)) {
            const zones = _getCheckedZones('settings-zone-list');
            if (zones.length > 0) {
                const { baseUrl } = _splitOpenrgbZone(url);
                url = baseUrl + '/' + zones.join('+');
            }
        }
        return url;
    }
}

const settingsModal = new DeviceSettingsModal();

export function formatRelativeTime(isoString: any) {
    if (!isoString) return null;
    const then = new Date(isoString);
    const diffMs = Date.now() - then.getTime();
    if (diffMs < 0) return null;
    const diffSec = Math.floor(diffMs / 1000);
    if (diffSec < 5) return t('device.last_seen.just_now');
    if (diffSec < 60) return t('device.last_seen.seconds').replace('%d', diffSec);
    const diffMin = Math.floor(diffSec / 60);
    if (diffMin < 60) return t('device.last_seen.minutes').replace('%d', diffMin);
    const diffHr = Math.floor(diffMin / 60);
    if (diffHr < 24) return t('device.last_seen.hours').replace('%d', diffHr);
    const diffDay = Math.floor(diffHr / 24);
    return t('device.last_seen.days').replace('%d', diffDay);
}

export function createDeviceCard(device: Device & { state?: any }) {
    const state = device.state || {};

    const devOnline = state.device_online || false;
    const devLatency = state.device_latency_ms;
    const devName = state.device_name;
    const devVersion = state.device_version;
    const devLastChecked = state.device_last_checked;

    // Health dot — kept inline beside the name so the existing
    // event-driven status updates (.health-dot[data-device-id=…])
    // continue to work without needing new selectors.
    let healthClass: string, healthTitle: string;
    if (devLastChecked === null || devLastChecked === undefined) {
        healthClass = 'health-unknown';
        healthTitle = t('device.health.checking');
    } else if (devOnline) {
        healthClass = 'health-online';
        healthTitle = t('device.health.online');
        if (devName) healthTitle += ` - ${devName}`;
        if (devVersion) healthTitle += ` v${devVersion}`;
        if (devLatency !== null && devLatency !== undefined) healthTitle += ` (${Math.round(devLatency)}ms)`;
    } else {
        healthClass = 'health-offline';
        healthTitle = t('device.health.offline');
        if (state.device_error) healthTitle += `: ${state.device_error}`;
    }
    const healthDot = `<span class="health-dot ${healthClass}" title="${escapeHtml(healthTitle)}" role="status" aria-label="${escapeHtml(healthTitle)}"></span>`;

    // ── LED bezel — 1-3 dots reflecting connection state ──────────
    let leds: LedState[];
    if (devLastChecked === null || devLastChecked === undefined) {
        leds = ['off'];
    } else if (devOnline) {
        leds = ['on', 'blink', 'blink'];
    } else {
        leds = ['fault'];
    }

    // ── Type label for the badge ──
    const badgeText = `${(device.device_type || 'wled').toUpperCase()} · OUT`;

    // ── Metadata sub-line — URL becomes a clickable anchor when http(s),
    //    plain text otherwise. Built as HTML so the link is real, not a
    //    chip. Each part is HTML-escaped individually. ──
    const metaPartsHtml: string[] = [];
    if (device.url && device.url.startsWith('http')) {
        const safeUrl = escapeHtml(device.url);
        const display = escapeHtml(device.url.replace(/^https?:\/\//, ''));
        metaPartsHtml.push(
            `<a class="mod-meta__link" href="${safeUrl}" target="_blank" rel="noopener" title="${t('device.button.webui') || 'Open device web UI'}" onclick="event.stopPropagation();">${display} ${ICON_WEB}</a>`
        );
    } else if (device.url && !device.url.startsWith('mock://') && !device.url.startsWith('ws://') && !device.url.startsWith('openrgb://')) {
        metaPartsHtml.push(escapeHtml(device.url));
    }
    if (devVersion) metaPartsHtml.push(escapeHtml(`v${devVersion}`));

    // ── Metric strip — three blocks: LED count, latency, and chip type
    //    (which carries both the LED chip identifier and RGB/RGBW colour
    //    channel layout, stacked in two rows). ──
    const ledCount = state.device_led_count || device.led_count;
    const metrics: ModMetricOpts[] = [];
    if (ledCount) {
        metrics.push({ k: t('device.led_count') || 'PIXELS', v: String(ledCount), title: t('device.led_count') });
    }
    if (devOnline && devLatency !== null && devLatency !== undefined) {
        metrics.push({ k: 'LAT', v: `${Math.round(devLatency)}<small>ms</small>`, accent: true });
    }
    const ledTypeName = state.device_led_type
        ? state.device_led_type.replace(/ RGBW$/, '')
        : null;
    const colorChannels = state.device_rgbw === true ? 'RGBW'
        : state.device_rgbw === false ? 'RGB'
        : null;
    if (ledTypeName || colorChannels) {
        const primary = ledTypeName ?? colorChannels!;
        const secondary = ledTypeName && colorChannels ? colorChannels : null;
        const subHtml = secondary
            ? `<span class="v-sub">${escapeHtml(secondary)}</span>`
            : '';
        metrics.push({
            k: t('dashboard.device.chip') || 'CHIP',
            v: `${escapeHtml(primary)}${subHtml}`,
            variant: 'text-stack',
            title: [state.device_led_type, colorChannels].filter(Boolean).join(' · ') || undefined,
        });
    }

    // ── Chips: OpenRGB zones (LED type/RGBW now live in the metric strip) ──
    const openrgbZones = isOpenrgbDevice(device.device_type)
        ? _splitOpenrgbZone(device.url).zones : [];
    const chips: ModChipOpts[] = [];
    if (openrgbZones.length) {
        for (const z of openrgbZones) {
            chips.push({ icon: ICON_LED, text: String(z), title: t('device.openrgb.zone') });
        }
    }

    // ── Patch indicator state ──
    const isOnline = devOnline && devLastChecked != null;
    const patchState: 'live' | 'standby' | 'offline' | 'idle' =
        devLastChecked == null ? 'idle' :
        isOnline ? 'live' :
        'offline';
    const patchLabel = devLastChecked == null ? t('patch.checking')
                     : isOnline ? t('patch.online')
                     : t('patch.offline');

    // ── Brightness fader ──
    const hasBrightness = (device.capabilities || []).includes('brightness_control');
    const brightnessVal = _deviceBrightnessCache[device.id] ?? 0;
    const brightnessLoaded = _deviceBrightnessCache[device.id] != null;

    // ── Foot actions ──
    const hasPower = (device.capabilities || []).includes('power_control');
    const iconActions: ModBtnOpts[] = [];
    if (hasPower && isOnline) {
        // Power-off as a stop-style icon button (was in topButtons before)
        iconActions.push({
            icon: ICON_STOP_PLAIN,
            onclick: `turnOffDevice('${device.id}')`,
            title: t('device.button.power_off'),
            variant: 'stop',
        });
    }
    iconActions.push({
        icon: ICON_REFRESH,
        onclick: `event.stopPropagation(); pingDevice('${device.id}')`,
        title: t('device.button.ping'),
    });
    iconActions.push({
        icon: ICON_SETTINGS,
        onclick: `showSettings('${device.id}')`,
        title: t('device.button.settings'),
    });

    // ── Custom icon plate (optional, set via the picker) ──
    const iconId = (device as Device).icon;
    const iconColor = (device as Device).icon_color;
    const iconHtml = iconId ? renderDeviceIconSvg(iconId, { size: 24 }) : '';
    const iconTitle = iconId ? t('device.icon.change') : t('device.icon.choose');

    // Kebab menu — add "Change icon…" as the first extra item so the
    // picker is reachable from anywhere on the card, not only the plate.
    // Wired via document-level delegation (data-icon-picker-trigger),
    // not an inline onclick string — see features/icon-picker.ts.
    const menuExtraItems: ModMenuItemOpts[] = [
        {
            label: iconId ? t('device.icon.change') : t('device.icon.choose'),
            icon: ICON_EDIT,
            dataAttrs: { 'data-icon-picker-trigger': device.id },
        },
    ];

    const mod: ModCardOpts = {
        head: {
            badge: { text: badgeText },
            name: device.name || device.id,
            metaHtml: metaPartsHtml.length ? metaPartsHtml.join(' · ') : undefined,
            healthDot,
            leds,
            iconHtml,
            iconColor,
            iconAttrs: { 'data-icon-picker-trigger': device.id },
            iconTitle,
            menu: {
                extraItems: menuExtraItems,
                duplicateOnclick: `cloneDevice('${device.id}')`,
                hideOnclick: `toggleCardHidden('led-devices','${device.id}')`,
                deleteOnclick: `removeDevice('${device.id}')`,
            },
        },
        body: {
            metrics: metrics.length ? metrics : undefined,
            chips: chips.length ? chips : undefined,
            fader: hasBrightness ? {
                label: t('device.brightness') || 'Bright',
                value: brightnessVal,
                max: 255,
                sliderId: undefined,
                oninput: `updateBrightnessLabel('${device.id}', this.value)`,
                onchange: `saveCardBrightness('${device.id}', this.value)`,
                dataAttrs: { 'data-device-brightness': device.id },
                disabled: !brightnessLoaded,
            } : undefined,
        },
        foot: {
            patchState,
            patchLabel,
            iconActions,
        },
        running: isOnline && hasBrightness && brightnessVal > 0,
    };

    // Tag chips render via existing renderTagChips() — append after wrapCard
    // returns, since the modular API doesn't yet have a "tags" slot.
    const cardHtml = wrapCard({
        dataAttr: 'data-device-id',
        id: device.id,
        classes: hasBrightness && !brightnessLoaded ? 'brightness-loading' : '',
        mod,
    });
    const tags = renderTagChips(device.tags);
    if (!tags) return cardHtml;
    // Insert tags before the closing </div> of the wrapper card.
    return cardHtml.replace(/<\/div>\s*$/, `${tags}</div>`);
}

export async function turnOffDevice(deviceId: any) {
    const confirmed = await showConfirm(t('confirm.turn_off_device'));
    if (!confirmed) return;
    try {
        const setResp = await fetchWithAuth(`/devices/${deviceId}/power`, {
            method: 'PUT',
            body: JSON.stringify({ power: false })
        });
        if (setResp.ok) {
            showToast(t('device.power.off_success'), 'success');
        } else {
            const error = await setResp.json();
            showToast(error.detail || 'Failed', 'error');
        }
    } catch (error: any) {
        if (error.isAuth) return;
        showToast(t('device.error.power_off_failed'), 'error');
    }
}

export async function pingDevice(deviceId: any) {
    const btn = document.querySelector(`[data-device-id="${CSS.escape(deviceId)}"] .card-ping-btn`) as HTMLElement | null;
    if (btn) btn.classList.add('spinning');
    try {
        const resp = await fetchWithAuth(`/devices/${deviceId}/ping`, { method: 'POST' });
        if (resp.ok) {
            const data = await resp.json();
            const ms = data.device_latency_ms != null ? data.device_latency_ms.toFixed(0) : '?';
            showToast(data.device_online
                ? t('device.ping.online', { ms })
                : t('device.ping.offline'), data.device_online ? 'success' : 'error');
            // Refresh device cards to update health dot
            devicesCache.invalidate();
            await window.loadDevices();
        } else {
            const err = await resp.json();
            showToast(err.detail || 'Ping failed', 'error');
        }
    } catch (error: any) {
        if (error.isAuth) return;
        showToast(t('device.ping.error'), 'error');
    } finally {
        if (btn) btn.classList.remove('spinning');
    }
}

export function attachDeviceListeners(deviceId: any) {
    // Add any specific event listeners here if needed
}

export async function removeDevice(deviceId: any) {
    const confirmed = await showConfirm(t('device.remove.confirm'));
    if (!confirmed) return;

    try {
        const response = await fetchWithAuth(`/devices/${deviceId}`, {
            method: 'DELETE',
        });
        if (response.ok) {
            showToast(t('device.removed'), 'success');
            devicesCache.invalidate();
            window.loadDevices();
        } else {
            const error = await response.json();
            showToast(error.detail || t('device.error.remove_failed'), 'error');
        }
    } catch (error: any) {
        if (error.isAuth) return;
        console.error('Failed to remove device:', error);
        showToast(t('device.error.remove_failed'), 'error');
    }
}

export async function showSettings(deviceId: any) {
    try {
        const deviceResponse = await fetchWithAuth(`/devices/${deviceId}`);
        if (!deviceResponse.ok) { showToast(t('device.error.settings_load_failed'), 'error'); return; }

        const device = await deviceResponse.json();
        const isAdalight = isSerialDevice(device.device_type);
        const caps = device.capabilities || [];

        // Set modal state before populating fields (so async helpers read correct type)
        settingsModal.deviceType = device.device_type;
        settingsModal.capabilities = caps;

        (document.getElementById('settings-device-id') as HTMLInputElement).value = device.id;
        (document.getElementById('settings-device-name') as HTMLInputElement).value = device.name;
        (document.getElementById('settings-health-interval') as HTMLInputElement).value = device.state_check_interval ?? 30;

        const isMock = isMockDevice(device.device_type);
        const isWs = isWsDevice(device.device_type);
        const isMqtt = isMqttDevice(device.device_type);
        const isGroup = isGroupDevice(device.device_type);
        const urlGroup = document.getElementById('settings-url-group') as HTMLElement;
        const serialGroup = document.getElementById('settings-serial-port-group') as HTMLElement;
        const urlLabel = urlGroup.querySelector('label[for="settings-device-url"]') as HTMLElement | null;
        const urlHint = urlGroup.querySelector('.input-hint') as HTMLElement | null;
        const urlInput = document.getElementById('settings-device-url') as HTMLInputElement;
        if (isMock || isWs || isGroup) {
            urlGroup.style.display = 'none';
            urlInput.removeAttribute('required');
            serialGroup.style.display = 'none';
        } else if (isAdalight) {
            urlGroup.style.display = 'none';
            urlInput.removeAttribute('required');
            serialGroup.style.display = '';
            _populateSettingsSerialPorts(device.url);
        } else {
            urlGroup.style.display = '';
            urlInput.setAttribute('required', '');
            urlInput.value = device.url;
            serialGroup.style.display = 'none';
            // Relabel for MQTT
            if (isMqtt) {
                if (urlLabel) urlLabel.textContent = t('device.mqtt_topic');
                if (urlHint) urlHint.textContent = t('device.mqtt_topic.hint');
                urlInput.placeholder = t('device.mqtt_topic.placeholder') || 'mqtt://ledgrab/device/living-room';
            } else if (isOpenrgbDevice(device.device_type)) {
                if (urlLabel) urlLabel.textContent = t('device.openrgb.url');
                if (urlHint) urlHint.textContent = t('device.openrgb.url.hint');
                urlInput.placeholder = 'openrgb://localhost:6742/0';
                // Parse zone from URL and show base URL only
                const { baseUrl, zones: currentZones } = _splitOpenrgbZone(device.url);
                urlInput.value = baseUrl;
            } else {
                if (urlLabel) urlLabel.textContent = t('device.url');
                if (urlHint) urlHint.textContent = t('settings.url.hint');
                urlInput.placeholder = t('device.url.placeholder') || 'http://192.168.1.100';
            }
        }

        const ledCountGroup = document.getElementById('settings-led-count-group') as HTMLElement;
        if (caps.includes('manual_led_count')) {
            ledCountGroup.style.display = '';
            (document.getElementById('settings-led-count') as HTMLInputElement).value = device.led_count || '';
        } else {
            ledCountGroup.style.display = 'none';
        }

        const baudRateGroup = document.getElementById('settings-baud-rate-group') as HTMLElement;
        if (isAdalight) {
            baudRateGroup.style.display = '';
            const baudSelect = document.getElementById('settings-baud-rate') as HTMLSelectElement;
            if (device.baud_rate) {
                baudSelect.value = String(device.baud_rate);
            } else {
                baudSelect.value = '115200';
            }
            updateSettingsBaudFpsHint();
        } else {
            baudRateGroup.style.display = 'none';
        }

        // Mock-specific fields
        const ledTypeGroup = document.getElementById('settings-led-type-group');
        const sendLatencyGroup = document.getElementById('settings-send-latency-group');
        if (isMock) {
            if (ledTypeGroup) {
                (ledTypeGroup as HTMLElement).style.display = '';
                (document.getElementById('settings-led-type') as HTMLSelectElement).value = device.rgbw ? 'rgbw' : 'rgb';
            }
            if (sendLatencyGroup) {
                (sendLatencyGroup as HTMLElement).style.display = '';
                (document.getElementById('settings-send-latency') as HTMLInputElement).value = device.send_latency_ms || 0;
            }
        } else {
            if (ledTypeGroup) (ledTypeGroup as HTMLElement).style.display = 'none';
            if (sendLatencyGroup) (sendLatencyGroup as HTMLElement).style.display = 'none';
        }

        // WS connection URL
        const wsUrlGroup = document.getElementById('settings-ws-url-group');
        if (wsUrlGroup) {
            if (isWs) {
                const origin = getBaseOrigin();
                const wsProto = origin.startsWith('https') ? 'wss:' : 'ws:';
                const hostPart = origin.replace(/^https?:\/\//, '');
                const wsUrl = `${wsProto}//${hostPart}/api/v1/devices/${device.id}/ws`;
                (document.getElementById('settings-ws-url') as HTMLInputElement).value = wsUrl;
                (wsUrlGroup as HTMLElement).style.display = '';
            } else {
                (wsUrlGroup as HTMLElement).style.display = 'none';
            }
        }

        // Hide health check for devices without health_check capability
        const healthIntervalGroup = document.getElementById('settings-health-interval-group');
        if (healthIntervalGroup) {
            (healthIntervalGroup as HTMLElement).style.display = caps.includes('health_check') ? '' : 'none';
        }

        // Hide auto-restore for devices without auto_restore capability
        const autoShutdownGroup = document.getElementById('settings-auto-shutdown-group');
        if (autoShutdownGroup) {
            (autoShutdownGroup as HTMLElement).style.display = caps.includes('auto_restore') ? '' : 'none';
        }
        (document.getElementById('settings-auto-shutdown') as HTMLInputElement).checked = !!device.auto_shutdown;

        // OpenRGB zone picker + mode toggle
        const settingsZoneGroup = document.getElementById('settings-zone-group');
        const settingsZoneModeGroup = document.getElementById('settings-zone-mode-group');
        if (settingsZoneModeGroup) (settingsZoneModeGroup as HTMLElement).style.display = 'none';
        if (settingsZoneGroup) {
            if (isOpenrgbDevice(device.device_type)) {
                (settingsZoneGroup as HTMLElement).style.display = '';
                const { baseUrl, zones: currentZones } = _splitOpenrgbZone(device.url);
                // Set zone mode radio from device
                const savedMode = device.zone_mode || 'combined';
                const modeRadio = document.querySelector(`input[name="settings-zone-mode"][value="${CSS.escape(savedMode)}"]`) as HTMLInputElement | null;
                if (modeRadio) modeRadio.checked = true;
                _fetchOpenrgbZones(baseUrl, 'settings-zone-list', currentZones).then(() => {
                    // Re-snapshot after zones are loaded so dirty-check baseline includes them
                    settingsModal.snapshot();
                });
            } else {
                (settingsZoneGroup as HTMLElement).style.display = 'none';
                (document.getElementById('settings-zone-list') as HTMLElement).innerHTML = '';
            }
        }

        // DMX-specific fields
        const dmxProtocolGroup = document.getElementById('settings-dmx-protocol-group');
        const dmxStartUniverseGroup = document.getElementById('settings-dmx-start-universe-group');
        const dmxStartChannelGroup = document.getElementById('settings-dmx-start-channel-group');
        if (isDmxDevice(device.device_type)) {
            if (dmxProtocolGroup) (dmxProtocolGroup as HTMLElement).style.display = '';
            if (dmxStartUniverseGroup) (dmxStartUniverseGroup as HTMLElement).style.display = '';
            if (dmxStartChannelGroup) (dmxStartChannelGroup as HTMLElement).style.display = '';
            (document.getElementById('settings-dmx-protocol') as HTMLSelectElement).value = device.dmx_protocol || 'artnet';
            ensureDmxProtocolIconSelect('settings-dmx-protocol');
            (document.getElementById('settings-dmx-start-universe') as HTMLInputElement).value = device.dmx_start_universe ?? 0;
            (document.getElementById('settings-dmx-start-channel') as HTMLInputElement).value = device.dmx_start_channel ?? 1;
            // Relabel URL field as IP Address
            const urlLabel2 = urlGroup.querySelector('label[for="settings-device-url"]') as HTMLElement | null;
            const urlHint2 = urlGroup.querySelector('.input-hint') as HTMLElement | null;
            if (urlLabel2) urlLabel2.textContent = t('device.dmx.url');
            if (urlHint2) urlHint2.textContent = t('device.dmx.url.hint');
            urlInput.placeholder = t('device.dmx.url.placeholder') || '192.168.1.50';
        } else {
            destroyDmxProtocolIconSelect('settings-dmx-protocol');
            if (dmxProtocolGroup) (dmxProtocolGroup as HTMLElement).style.display = 'none';
            if (dmxStartUniverseGroup) (dmxStartUniverseGroup as HTMLElement).style.display = 'none';
            if (dmxStartChannelGroup) (dmxStartChannelGroup as HTMLElement).style.display = 'none';
        }

        // DDP-specific fields
        const ddpPortGroup = document.getElementById('settings-ddp-port-group');
        const ddpDestinationIdGroup = document.getElementById('settings-ddp-destination-id-group');
        const ddpColorOrderGroup = document.getElementById('settings-ddp-color-order-group');
        if (isDdpDevice(device.device_type)) {
            if (ddpPortGroup) (ddpPortGroup as HTMLElement).style.display = '';
            if (ddpDestinationIdGroup) (ddpDestinationIdGroup as HTMLElement).style.display = '';
            if (ddpColorOrderGroup) (ddpColorOrderGroup as HTMLElement).style.display = '';
            (document.getElementById('settings-ddp-port') as HTMLInputElement).value = device.ddp_port ?? 0;
            (document.getElementById('settings-ddp-destination-id') as HTMLInputElement).value = device.ddp_destination_id ?? 1;
            const colorOrderSel = document.getElementById('settings-ddp-color-order') as HTMLSelectElement;
            const colorOrderVal = String(device.ddp_color_order ?? 1);
            if (colorOrderSel) colorOrderSel.value = colorOrderVal;
            ensureDdpColorOrderIconSelect('settings-ddp-color-order');
            // Relabel URL field as IP Address
            const urlLabel3 = urlGroup.querySelector('label[for="settings-device-url"]') as HTMLElement | null;
            const urlHint3 = urlGroup.querySelector('.input-hint') as HTMLElement | null;
            if (urlLabel3) urlLabel3.textContent = t('device.ddp.url');
            if (urlHint3) urlHint3.textContent = t('device.ddp.url.hint');
            urlInput.placeholder = t('device.ddp.url.placeholder') || '192.168.1.50';
        } else {
            destroyDdpColorOrderIconSelect('settings-ddp-color-order');
            if (ddpPortGroup) (ddpPortGroup as HTMLElement).style.display = 'none';
            if (ddpDestinationIdGroup) (ddpDestinationIdGroup as HTMLElement).style.display = 'none';
            if (ddpColorOrderGroup) (ddpColorOrderGroup as HTMLElement).style.display = 'none';
        }

        // OPC-specific fields — single optional channel input (0 = broadcast
        // to every channel on the server, 1-255 = specific output). No
        // discovery; user enters IP manually. Same URL-relabel pattern as DDP.
        const opcChannelGroup = document.getElementById('settings-opc-channel-group');
        if (isOpcDevice(device.device_type)) {
            if (opcChannelGroup) (opcChannelGroup as HTMLElement).style.display = '';
            (document.getElementById('settings-opc-channel') as HTMLInputElement).value = String(device.opc_channel ?? 0);
            // Relabel URL field as IP Address
            const urlLabelOpc = urlGroup.querySelector('label[for="settings-device-url"]') as HTMLElement | null;
            const urlHintOpc = urlGroup.querySelector('.input-hint') as HTMLElement | null;
            if (urlLabelOpc) urlLabelOpc.textContent = t('device.opc.url');
            if (urlHintOpc) urlHintOpc.textContent = t('device.opc.url.hint');
            urlInput.placeholder = t('device.opc.url.placeholder') || '192.168.1.50';
        } else {
            if (opcChannelGroup) (opcChannelGroup as HTMLElement).style.display = 'none';
        }

        // Yeelight-specific fields — exposed in the settings modal so the
        // user can tune the per-bulb client-side rate gate without recreating
        // the device. The bulb runs a ~1 cmd/sec cap on the wire; values
        // below 500 risk being throttled. LED count is intentionally still
        // shown — it controls the source-side strip mapping even though
        // Yeelight averages to a single color before sending.
        const yeelightMinIntervalGroup = document.getElementById('settings-yeelight-min-interval-group');
        if (isYeelightDevice(device.device_type)) {
            if (yeelightMinIntervalGroup) (yeelightMinIntervalGroup as HTMLElement).style.display = '';
            const ymi = device.yeelight_min_interval_ms ?? 500;
            (document.getElementById('settings-yeelight-min-interval') as HTMLInputElement).value = String(ymi);
            // Relabel URL field as IP Address (same pattern as DMX/DDP)
            const urlLabel4 = urlGroup.querySelector('label[for="settings-device-url"]') as HTMLElement | null;
            const urlHint4 = urlGroup.querySelector('.input-hint') as HTMLElement | null;
            if (urlLabel4) urlLabel4.textContent = t('device.yeelight.url');
            if (urlHint4) urlHint4.textContent = t('device.yeelight.url.hint');
            urlInput.placeholder = t('device.yeelight.url.placeholder') || '192.168.1.50';
        } else {
            if (yeelightMinIntervalGroup) (yeelightMinIntervalGroup as HTMLElement).style.display = 'none';
        }

        // WiZ-specific fields — UDP fire-and-forget, no wire-level rate cap
        // beyond what the bulb's MCU can keep up with. Default 50 ms ≈ 20 Hz
        // is comfortable; users can push lower if they want to experiment.
        const wizMinIntervalGroup = document.getElementById('settings-wiz-min-interval-group');
        if (isWizDevice(device.device_type)) {
            if (wizMinIntervalGroup) (wizMinIntervalGroup as HTMLElement).style.display = '';
            const wmi = device.wiz_min_interval_ms ?? 50;
            (document.getElementById('settings-wiz-min-interval') as HTMLInputElement).value = String(wmi);
            // Relabel URL field as IP Address (same pattern as Yeelight/DMX/DDP)
            const urlLabel5 = urlGroup.querySelector('label[for="settings-device-url"]') as HTMLElement | null;
            const urlHint5 = urlGroup.querySelector('.input-hint') as HTMLElement | null;
            if (urlLabel5) urlLabel5.textContent = t('device.wiz.url');
            if (urlHint5) urlHint5.textContent = t('device.wiz.url.hint');
            urlInput.placeholder = t('device.wiz.url.placeholder') || '192.168.1.50';
        } else {
            if (wizMinIntervalGroup) (wizMinIntervalGroup as HTMLElement).style.display = 'none';
        }

        // LIFX-specific fields — binary UDP on port 56700, single-pixel
        // (HSBK averaged from the strip). LIFX recommends ≤20 cmd/sec per
        // device; default 50 ms matches that ceiling.
        const lifxMinIntervalGroup = document.getElementById('settings-lifx-min-interval-group');
        if (isLifxDevice(device.device_type)) {
            if (lifxMinIntervalGroup) (lifxMinIntervalGroup as HTMLElement).style.display = '';
            const lmi = device.lifx_min_interval_ms ?? 50;
            (document.getElementById('settings-lifx-min-interval') as HTMLInputElement).value = String(lmi);
            // Relabel URL field as IP Address (same pattern as WiZ/Yeelight/DMX/DDP)
            const urlLabel6 = urlGroup.querySelector('label[for="settings-device-url"]') as HTMLElement | null;
            const urlHint6 = urlGroup.querySelector('.input-hint') as HTMLElement | null;
            if (urlLabel6) urlLabel6.textContent = t('device.lifx.url');
            if (urlHint6) urlHint6.textContent = t('device.lifx.url.hint');
            urlInput.placeholder = t('device.lifx.url.placeholder') || '192.168.1.50';
        } else {
            if (lifxMinIntervalGroup) (lifxMinIntervalGroup as HTMLElement).style.display = 'none';
        }

        // Govee-specific fields — 2023+ LAN API over UDP fire-and-forget
        // on port 4003. Critical UX note: the hint reminds users to enable
        // "LAN Control" in the Govee Home app or the device won't respond.
        const goveeMinIntervalGroup = document.getElementById('settings-govee-min-interval-group');
        if (isGoveeDevice(device.device_type)) {
            if (goveeMinIntervalGroup) (goveeMinIntervalGroup as HTMLElement).style.display = '';
            const gmi = device.govee_min_interval_ms ?? 50;
            (document.getElementById('settings-govee-min-interval') as HTMLInputElement).value = String(gmi);
            // Relabel URL field as IP Address (same pattern as LIFX/WiZ/Yeelight/DMX/DDP)
            const urlLabel7 = urlGroup.querySelector('label[for="settings-device-url"]') as HTMLElement | null;
            const urlHint7 = urlGroup.querySelector('.input-hint') as HTMLElement | null;
            if (urlLabel7) urlLabel7.textContent = t('device.govee.url');
            if (urlHint7) urlHint7.textContent = t('device.govee.url.hint');
            urlInput.placeholder = t('device.govee.url.placeholder') || '192.168.1.50';
        } else {
            if (goveeMinIntervalGroup) (goveeMinIntervalGroup as HTMLElement).style.display = 'none';
        }

        // Nanoleaf-specific fields — HTTP REST on fixed port 16021. Same
        // URL-relabel pattern as the rest of the LAN-bulb family. The
        // `nanoleaf_token` is set once at pair time and is encrypted at
        // rest; we show a read-only "Paired ✓" indicator (no input) so the
        // user can confirm the device is authenticated without ever seeing
        // or being able to overwrite the secret. To re-pair, delete and
        // re-add the device.
        const nanoleafMinIntervalGroup = document.getElementById('settings-nanoleaf-min-interval-group');
        const nanoleafPairedGroup = document.getElementById('settings-nanoleaf-paired-group');
        if (isNanoleafDevice(device.device_type)) {
            if (nanoleafMinIntervalGroup) (nanoleafMinIntervalGroup as HTMLElement).style.display = '';
            const nmi = device.nanoleaf_min_interval_ms ?? 100;
            (document.getElementById('settings-nanoleaf-min-interval') as HTMLInputElement).value = String(nmi);
            if (nanoleafPairedGroup) {
                (nanoleafPairedGroup as HTMLElement).style.display = device.nanoleaf_paired ? '' : 'none';
            }
            const urlLabel8 = urlGroup.querySelector('label[for="settings-device-url"]') as HTMLElement | null;
            const urlHint8 = urlGroup.querySelector('.input-hint') as HTMLElement | null;
            if (urlLabel8) urlLabel8.textContent = t('device.nanoleaf.url');
            if (urlHint8) urlHint8.textContent = t('device.nanoleaf.url.hint');
            urlInput.placeholder = t('device.nanoleaf.url.placeholder') || '192.168.1.50';
        } else {
            if (nanoleafMinIntervalGroup) (nanoleafMinIntervalGroup as HTMLElement).style.display = 'none';
            if (nanoleafPairedGroup) (nanoleafPairedGroup as HTMLElement).style.display = 'none';
        }

        // BLE-specific fields — exposed in the settings modal so the user
        // can fix a wrong protocol family pick without deleting+recreating
        // the device. Uses the shared IconSelect grid (project rule bans
        // plain <select> pickers).
        const bleFamilyGroup = document.getElementById('settings-ble-family-group');
        const bleGoveeKeyGroup = document.getElementById('settings-ble-govee-key-group');
        const bleFamilySelect = document.getElementById('settings-ble-family') as HTMLSelectElement | null;
        const bleGoveeKeyInput = document.getElementById('settings-ble-govee-key') as HTMLInputElement | null;
        const _toggleSettingsGoveeKeyVisibility = () => {
            const family = bleFamilySelect?.value || '';
            if (bleGoveeKeyGroup) {
                (bleGoveeKeyGroup as HTMLElement).style.display = family === 'govee' ? '' : 'none';
            }
        };
        if (isBleDevice(device.device_type)) {
            if (bleFamilyGroup) (bleFamilyGroup as HTMLElement).style.display = '';
            const currentFamily = device.ble_family || 'sp110e';
            if (bleFamilySelect) bleFamilySelect.value = currentFamily;
            if (bleGoveeKeyInput) bleGoveeKeyInput.value = device.ble_govee_key || '';
            const iconSelect = ensureBleFamilyIconSelect(
                'settings-ble-family',
                _toggleSettingsGoveeKeyVisibility,
            );
            if (iconSelect) iconSelect.setValue(currentFamily);
            _toggleSettingsGoveeKeyVisibility();
        } else {
            destroyBleFamilyIconSelect('settings-ble-family');
            if (bleFamilyGroup) (bleFamilyGroup as HTMLElement).style.display = 'none';
            if (bleGoveeKeyGroup) (bleGoveeKeyGroup as HTMLElement).style.display = 'none';
        }

        // Group device fields
        const groupChildrenGroup = document.getElementById('settings-group-children-group');
        const groupModeGroup = document.getElementById('settings-group-mode-group');
        if (isGroup) {
            if (groupChildrenGroup) (groupChildrenGroup as HTMLElement).style.display = '';
            if (groupModeGroup) (groupModeGroup as HTMLElement).style.display = '';
            // Set mode via select + IconSelect
            const savedMode = device.group_mode || 'sequence';
            const modeSelect = document.getElementById('settings-group-mode-select') as HTMLSelectElement;
            if (modeSelect) modeSelect.value = savedMode;
            ensureGroupModeIconSelect('settings-group-mode-select');
            // Show/hide LED count based on mode
            const settingsLedCountGroup = document.getElementById('settings-led-count-group') as HTMLElement;
            if (settingsLedCountGroup) settingsLedCountGroup.style.display = savedMode === 'independent' ? '' : 'none';
            // Populate children list
            await devicesCache.fetch();
            const childrenList = document.getElementById('settings-group-children-list') as HTMLElement;
            if (childrenList) {
                childrenList.innerHTML = '';
                const childIds = device.group_device_ids || [];
                childIds.forEach((childId: string) => {
                    _addGroupChildSettingsWithId(childId);
                });
            }
        } else {
            if (groupChildrenGroup) (groupChildrenGroup as HTMLElement).style.display = 'none';
            if (groupModeGroup) (groupModeGroup as HTMLElement).style.display = 'none';
            destroyGroupModeIconSelect('settings-group-mode-select');
        }

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

        // CSPT template selector
        await csptCache.fetch();
        _ensureSettingsCsptSelect();
        const csptSel = document.getElementById('settings-css-processing-template') as HTMLSelectElement | null;
        if (csptSel) csptSel.value = device.default_css_processing_template_id || '';

        _updateSettingsSectionVisibility();
        settingsModal.snapshot();
        settingsModal.open();

        setTimeout(() => desktopFocus(document.getElementById('settings-device-name')), 100);

    } catch (error: any) {
        if (error.isAuth) return;
        console.error('Failed to load device settings:', error);
        showToast(t('device.error.settings_load_failed'), 'error');
    }
}

export function isSettingsDirty() { return settingsModal.isDirty(); }
export function forceCloseDeviceSettingsModal() { if (_deviceTagsInput) { _deviceTagsInput.destroy(); _deviceTagsInput = null; } if (_settingsCsptEntitySelect) { _settingsCsptEntitySelect.destroy(); _settingsCsptEntitySelect = null; } destroyBleFamilyIconSelect('settings-ble-family'); destroyDdpColorOrderIconSelect('settings-ddp-color-order'); destroyDmxProtocolIconSelect('settings-dmx-protocol'); settingsModal.forceClose(); }
export function closeDeviceSettingsModal() { settingsModal.close(); }

export async function saveDeviceSettings() {
    const deviceId = (document.getElementById('settings-device-id') as HTMLInputElement).value;
    if (settingsModal.closeIfPristine(deviceId)) return;

    const name = (document.getElementById('settings-device-name') as HTMLInputElement).value.trim();
    const url = settingsModal._getUrl();

    const isGroup = isGroupDevice(settingsModal.deviceType);
    if (!name || (!isGroup && !url)) {
        settingsModal.showError(t('device.error.required'));
        return;
    }

    try {
        const body: any = {
            name, url,
            auto_shutdown: (document.getElementById('settings-auto-shutdown') as HTMLInputElement).checked,
            state_check_interval: parseInt((document.getElementById('settings-health-interval') as HTMLInputElement).value, 10) || 30,
            tags: _deviceTagsInput ? _deviceTagsInput.getValue() : [],
        };
        const ledCountInput = document.getElementById('settings-led-count') as HTMLInputElement;
        if (settingsModal.capabilities.includes('manual_led_count') && ledCountInput.value) {
            body.led_count = parseInt(ledCountInput.value, 10);
        }
        if (isSerialDevice(settingsModal.deviceType)) {
            const baudVal = (document.getElementById('settings-baud-rate') as HTMLSelectElement).value;
            if (baudVal) body.baud_rate = parseInt(baudVal, 10);
        }
        if (isMockDevice(settingsModal.deviceType)) {
            const sendLatency = (document.getElementById('settings-send-latency') as HTMLInputElement | null)?.value;
            if (sendLatency !== undefined) body.send_latency_ms = parseInt(sendLatency, 10);
            const ledType = (document.getElementById('settings-led-type') as HTMLSelectElement | null)?.value;
            body.rgbw = ledType === 'rgbw';
        }
        if (isOpenrgbDevice(settingsModal.deviceType)) {
            body.zone_mode = _getZoneMode('settings-zone-mode');
        }
        if (isDmxDevice(settingsModal.deviceType)) {
            body.dmx_protocol = (document.getElementById('settings-dmx-protocol') as HTMLSelectElement | null)?.value || 'artnet';
            body.dmx_start_universe = parseInt((document.getElementById('settings-dmx-start-universe') as HTMLInputElement | null)?.value || '0', 10);
            body.dmx_start_channel = parseInt((document.getElementById('settings-dmx-start-channel') as HTMLInputElement | null)?.value || '1', 10);
        }
        if (isDdpDevice(settingsModal.deviceType)) {
            body.ddp_port = parseInt((document.getElementById('settings-ddp-port') as HTMLInputElement | null)?.value || '0', 10);
            body.ddp_destination_id = parseInt((document.getElementById('settings-ddp-destination-id') as HTMLInputElement | null)?.value || '1', 10);
            body.ddp_color_order = parseInt((document.getElementById('settings-ddp-color-order') as HTMLSelectElement | null)?.value || '1', 10);
        }
        if (isOpcDevice(settingsModal.deviceType)) {
            const raw = (document.getElementById('settings-opc-channel') as HTMLInputElement | null)?.value;
            const parsed = parseInt(raw || '0', 10);
            body.opc_channel = Number.isFinite(parsed) ? parsed : 0;
        }
        if (isYeelightDevice(settingsModal.deviceType)) {
            const raw = (document.getElementById('settings-yeelight-min-interval') as HTMLInputElement | null)?.value;
            const parsed = parseInt(raw || '500', 10);
            body.yeelight_min_interval_ms = Number.isFinite(parsed) ? parsed : 500;
        }
        if (isWizDevice(settingsModal.deviceType)) {
            const raw = (document.getElementById('settings-wiz-min-interval') as HTMLInputElement | null)?.value;
            const parsed = parseInt(raw || '50', 10);
            body.wiz_min_interval_ms = Number.isFinite(parsed) ? parsed : 50;
        }
        if (isLifxDevice(settingsModal.deviceType)) {
            const raw = (document.getElementById('settings-lifx-min-interval') as HTMLInputElement | null)?.value;
            const parsed = parseInt(raw || '50', 10);
            body.lifx_min_interval_ms = Number.isFinite(parsed) ? parsed : 50;
        }
        if (isGoveeDevice(settingsModal.deviceType)) {
            const raw = (document.getElementById('settings-govee-min-interval') as HTMLInputElement | null)?.value;
            const parsed = parseInt(raw || '50', 10);
            body.govee_min_interval_ms = Number.isFinite(parsed) ? parsed : 50;
        }
        if (isNanoleafDevice(settingsModal.deviceType)) {
            const raw = (document.getElementById('settings-nanoleaf-min-interval') as HTMLInputElement | null)?.value;
            const parsed = parseInt(raw || '100', 10);
            body.nanoleaf_min_interval_ms = Number.isFinite(parsed) ? parsed : 100;
            // Intentionally do NOT include nanoleaf_token here — the token
            // is set once at pair time, encrypted at rest, and never
            // re-emitted from the settings modal. Re-pairing means
            // delete + re-add.
        }
        if (isBleDevice(settingsModal.deviceType)) {
            body.ble_family = (document.getElementById('settings-ble-family') as HTMLSelectElement | null)?.value || 'sp110e';
            const goveeKey = (document.getElementById('settings-ble-govee-key') as HTMLInputElement | null)?.value?.trim() || '';
            body.ble_govee_key = goveeKey;
        }
        if (isGroup) {
            const childRows = document.querySelectorAll('#settings-group-children-list .group-child-row') as NodeListOf<HTMLElement>;
            body.group_device_ids = Array.from(childRows).map(r => r.dataset.deviceId || '').filter(v => v !== '');
            body.group_mode = (document.getElementById('settings-group-mode-select') as HTMLSelectElement)?.value || 'sequence';
        }
        const csptId = (document.getElementById('settings-css-processing-template') as HTMLSelectElement | null)?.value || '';
        body.default_css_processing_template_id = csptId;
        const deviceResponse = await fetchWithAuth(`/devices/${deviceId}`, {
            method: 'PUT',
            body: JSON.stringify(body)
        });

        if (!deviceResponse.ok) {
            const errorData = await deviceResponse.json();
            const detail = errorData.detail || errorData.message || '';
            const detailStr = Array.isArray(detail) ? detail.map((d: any) => d.msg || d).join('; ') : String(detail);
            settingsModal.showError(detailStr || t('device.error.update'));
            return;
        }

        showToast(t('settings.saved'), 'success');
        devicesCache.invalidate();
        settingsModal.forceClose();
        window.loadDevices();
    } catch (err: any) {
        if (err.isAuth) return;
        console.error('Failed to save device settings:', err);
        settingsModal.showError(err.message || t('device.error.save'));
    }
}

// Brightness
export function updateBrightnessLabel(deviceId: any, value: any) {
    const slider = document.querySelector(`[data-device-brightness="${CSS.escape(deviceId)}"]`) as HTMLElement | null;
    if (!slider) return;
    const v = parseInt(value);
    slider.title = Math.round(v / 255 * 100) + '%';
    // mod-card fader visuals — fill width + numeric readout
    const fader = slider.closest('.mod-fader');
    if (fader) {
        const fill = fader.querySelector('.mod-fader__fill') as HTMLElement | null;
        if (fill) fill.style.width = `${(v / 255) * 100}%`;
        const valEl = fader.querySelector('.mod-fader__v') as HTMLElement | null;
        if (valEl) valEl.textContent = String(v);
    }
}

export async function saveCardBrightness(deviceId: any, value: any) {
    const bri = parseInt(value);
    updateDeviceBrightness(deviceId, bri);
    try {
        const resp = await fetchWithAuth(`/devices/${deviceId}/brightness`, {
            method: 'PUT',
            body: JSON.stringify({ brightness: bri })
        });
        if (!resp.ok) {
            const errData = await resp.json().catch(() => ({}));
            const detail = errData.detail || errData.message || '';
            const detailStr = Array.isArray(detail) ? detail.map((d: any) => d.msg || d).join('; ') : String(detail);
            showToast(detailStr || t('device.error.brightness'), 'error');
        }
    } catch (err: any) {
        if (err.isAuth) return;
        showToast(err.message || t('device.error.brightness'), 'error');
    }
}

const _brightnessFetchInFlight = new Set();
export async function fetchDeviceBrightness(deviceId: any) {
    if (_brightnessFetchInFlight.has(deviceId)) return;
    _brightnessFetchInFlight.add(deviceId);
    try {
        const resp = await fetchWithAuth(`/devices/${deviceId}/brightness`);
        if (!resp.ok) return;
        const data = await resp.json();
        updateDeviceBrightness(deviceId, data.brightness);
        const slider = document.querySelector(`[data-device-brightness="${CSS.escape(deviceId)}"]`) as HTMLInputElement | null;
        if (slider) {
            slider.value = data.brightness;
            slider.disabled = false;
            // Sync the mod-fader's fill + numeric readout via the
            // shared visual-update helper.
            updateBrightnessLabel(deviceId, data.brightness);
        }
        // Legacy wrapper (pre-migration cards)
        const wrap = document.querySelector(`[data-brightness-wrap="${CSS.escape(deviceId)}"]`) as HTMLElement | null;
        if (wrap) wrap.classList.remove('brightness-loading');
        // mod-card variant — loading flag lives on the card itself
        const card = slider?.closest('.card.mod-card') as HTMLElement | null;
        if (card) card.classList.remove('brightness-loading');
    } catch (err) {
        // Silently fail — device may be offline
    } finally {
        _brightnessFetchInFlight.delete(deviceId);
    }
}

// LED protocol timing constants
const LED_US_PER_BIT = 1.25;              // SK6812/WS2812B bit time (μs)
const LED_BITS_PER_PIXEL = 32;            // RGBW worst case (4 channels × 8 bits)
const LED_US_PER_PIXEL = LED_BITS_PER_PIXEL * LED_US_PER_BIT;  // 40μs
const LED_RESET_US = 80;                  // reset/latch pulse (μs)
const US_PER_SECOND = 1_000_000;

// Serial protocol constants
const SERIAL_BITS_PER_BYTE = 10;          // 8N1: 1 start + 8 data + 1 stop
const SERIAL_RGB_BYTES_PER_LED = 3;
const ADALIGHT_HEADER_BYTES = 6;          // 'Ada' + count_hi + count_lo + checksum
const AMBILED_HEADER_BYTES = 1;

// FPS hint helpers (shared with device-discovery, targets)
export function _computeMaxFps(baudRate: any, ledCount: any, deviceType: any) {
    if (!ledCount || ledCount < 1) return null;
    if (deviceType === 'wled') {
        const frameUs = ledCount * LED_US_PER_PIXEL + LED_RESET_US;
        return Math.floor(US_PER_SECOND / frameUs);
    }
    if (!baudRate) return null;
    const overhead = deviceType === 'ambiled' ? AMBILED_HEADER_BYTES : ADALIGHT_HEADER_BYTES;
    const bitsPerFrame = (ledCount * SERIAL_RGB_BYTES_PER_LED + overhead) * SERIAL_BITS_PER_BYTE;
    return Math.floor(baudRate / bitsPerFrame);
}

export function _renderFpsHint(hintEl: any, baudRate: any, ledCount: any, deviceType: any) {
    const fps = _computeMaxFps(baudRate, ledCount, deviceType);
    if (fps !== null) {
        hintEl.textContent = `Max FPS ≈ ${fps}`;
        hintEl.style.display = '';
    } else {
        hintEl.style.display = 'none';
    }
}

export function updateSettingsBaudFpsHint() {
    const hintEl = document.getElementById('settings-baud-fps-hint');
    const baudRate = parseInt((document.getElementById('settings-baud-rate') as HTMLSelectElement).value, 10);
    const ledCount = parseInt((document.getElementById('settings-led-count') as HTMLInputElement).value, 10);
    _renderFpsHint(hintEl, baudRate, ledCount, settingsModal.deviceType);
}

// Settings serial port population (used from showSettings)
async function _populateSettingsSerialPorts(currentUrl: any) {
    const select = document.getElementById('settings-serial-port') as HTMLSelectElement;
    select.innerHTML = '';
    const loadingOpt = document.createElement('option');
    loadingOpt.value = currentUrl;
    loadingOpt.textContent = currentUrl + ' ⏳';
    select.appendChild(loadingOpt);

    try {
        const discoverType = settingsModal.deviceType || 'adalight';
        const resp = await fetchWithAuth(`/devices/discover?timeout=2&device_type=${encodeURIComponent(discoverType)}`);
        if (!resp.ok) return;
        const data = await resp.json();
        const devices = data.devices || [];

        select.innerHTML = '';
        let currentFound = false;
        devices.forEach((device: any) => {
            const opt = document.createElement('option');
            opt.value = device.url;
            opt.textContent = device.name;
            if (device.url === currentUrl) currentFound = true;
            select.appendChild(opt);
        });
        if (!currentFound) {
            const opt = document.createElement('option');
            opt.value = currentUrl;
            opt.textContent = currentUrl;
            select.insertBefore(opt, select.firstChild);
        }
        select.value = currentUrl;
    } catch (err) {
        console.error('Failed to discover serial ports:', err);
    }
}

export function copyWsUrl() {
    const input = document.getElementById('settings-ws-url') as HTMLInputElement | null;
    if (!input || !input.value) return;
    if (navigator.clipboard && window.isSecureContext) {
        navigator.clipboard.writeText(input.value).then(() => {
            showToast(t('settings.copied') || 'Copied!', 'success');
        });
    } else {
        input.select();
        document.execCommand('copy');
        showToast(t('settings.copied') || 'Copied!', 'success');
    }
}

export async function loadDevices() {
    await window.loadTargetsTab();
}

document.addEventListener('auth:keyChanged', () => loadDevices());

// ===== OpenRGB zone count enrichment =====

// Cache: baseUrl → { zoneName: ledCount, ... }
const _zoneCountCache: any = {};

/** Return cached zone LED counts for a base URL, or null if not cached. */
export function getZoneCountCache(baseUrl: any) {
    return _zoneCountCache[baseUrl] || null;
}
const _zoneCountInFlight = new Set();

/**
 * Fetch zone LED counts for an OpenRGB device and update zone badges on the card.
 * Called after cards are rendered (same pattern as fetchDeviceBrightness).
 */
export async function enrichOpenrgbZoneBadges(deviceId: any, deviceUrl: any) {
    const { baseUrl, zones } = _splitOpenrgbZone(deviceUrl);
    if (!zones.length) return;

    // Use cache if available
    if (_zoneCountCache[baseUrl]) {
        _applyZoneCounts(deviceId, zones, _zoneCountCache[baseUrl]);
        return;
    }

    // Deduplicate in-flight requests per base URL
    if (_zoneCountInFlight.has(baseUrl)) return;
    _zoneCountInFlight.add(baseUrl);

    try {
        const resp = await fetchWithAuth(`/devices/openrgb-zones?url=${encodeURIComponent(baseUrl)}`);
        if (!resp.ok) return;
        const data = await resp.json();
        const counts: any = {};
        for (const z of data.zones) {
            counts[z.name.toLowerCase()] = z.led_count;
        }
        _zoneCountCache[baseUrl] = counts;
        _applyZoneCounts(deviceId, zones, counts);
    } catch {
        // Silently fail — device may be offline
    } finally {
        _zoneCountInFlight.delete(baseUrl);
    }
}

function _applyZoneCounts(deviceId: any, zones: any, counts: any) {
    const card = document.querySelector(`[data-device-id="${CSS.escape(deviceId)}"]`);
    if (!card) return;
    for (const zoneName of zones) {
        const badge = card.querySelector(`[data-zone-name="${CSS.escape(zoneName)}"]`);
        if (!badge) continue;
        const ledCount = counts[zoneName.toLowerCase()];
        if (ledCount != null) {
            badge.innerHTML = `${ICON_LED} ${escapeHtml(zoneName)} · ${ledCount}`;
        }
    }
}
