/**
 * 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 } from '../core/api.ts';
import { devicesCache } from '../core/state.ts';
import { _fetchOpenrgbZones, _getCheckedZones, _splitOpenrgbZone, _getZoneMode, ensureDmxProtocolIconSelect, destroyDmxProtocolIconSelect, ensureSpiLedTypeIconSelect, destroySpiLedTypeIconSelect, ensureGameSenseDeviceTypeIconSelect, destroyGameSenseDeviceTypeIconSelect } 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_PLUG, ICON_REFRESH, ICON_TEMPLATE, ICON_CLONE } from '../core/icons.ts';
import { wrapCard } from '../core/card-colors.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';

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

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',
            csptId: (document.getElementById('settings-css-processing-template') as HTMLSelectElement | null)?.value || '',
        };
    }

    _getUrl() {
        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;

    let healthClass, healthTitle, healthLabel;
    if (devLastChecked === null || devLastChecked === undefined) {
        healthClass = 'health-unknown';
        healthTitle = t('device.health.checking');
        healthLabel = '';
    } 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)`;
        healthLabel = '';
    } else {
        healthClass = 'health-offline';
        healthTitle = t('device.health.offline');
        if (state.device_error) healthTitle += `: ${state.device_error}`;
        healthLabel = '';
    }

    const ledCount = state.device_led_count || device.led_count;

    // Parse zone names from OpenRGB URL for badge display
    const openrgbZones = isOpenrgbDevice(device.device_type)
        ? _splitOpenrgbZone(device.url).zones : [];

    return wrapCard({
        dataAttr: 'data-device-id',
        id: device.id,
        topButtons: (device.capabilities || []).includes('power_control') ? `<button class="card-top-btn card-power-btn" onclick="turnOffDevice('${device.id}')" title="${t('device.button.power_off')}">${ICON_STOP_PLAIN}</button>` : '',
        removeOnclick: `removeDevice('${device.id}')`,
        removeTitle: t('device.button.remove'),
        content: `
            <div class="card-header">
                <div class="card-title" title="${escapeHtml(device.name || device.id)}">
                    <span class="health-dot ${healthClass}" title="${healthTitle}" role="status" aria-label="${healthTitle}"></span>
                    <span class="card-title-text">${device.name || device.id}</span>
                    ${healthLabel}
                </div>
            </div>
            <div class="card-subtitle">
                <span class="card-meta device-type-badge">${(device.device_type || 'wled').toUpperCase()}</span>
                ${device.url && device.url.startsWith('http') ? `<a class="device-url-badge" href="${device.url}" target="_blank" rel="noopener" title="${t('device.button.webui')}"><span class="device-url-text">${escapeHtml(device.url.replace(/^https?:\/\//, ''))}</span><span class="device-url-icon">${ICON_WEB}</span></a>` : (device.url && !device.url.startsWith('mock://') && !device.url.startsWith('ws://') && !device.url.startsWith('openrgb://') && !device.url.startsWith('http') ? `<span class="device-url-badge"><span class="device-url-text">${escapeHtml(device.url)}</span></span>` : '')}
                ${openrgbZones.length
                    ? openrgbZones.map((z: any) => `<span class="card-meta zone-badge" data-zone-name="${escapeHtml(z)}">${ICON_LED} ${escapeHtml(z)}</span>`).join('')
                    : (ledCount ? `<span class="card-meta" title="${t('device.led_count')}">${ICON_LED} ${ledCount}</span>` : '')}
                ${state.device_led_type ? `<span class="card-meta">${ICON_PLUG} ${state.device_led_type.replace(/ RGBW$/, '')}</span>` : ''}
                <span class="card-meta" title="${state.device_rgbw ? 'RGBW' : 'RGB'}"><span class="channel-indicator"><span class="ch" style="background:#e53935"></span><span class="ch" style="background:#43a047"></span><span class="ch" style="background:#1e88e5"></span>${state.device_rgbw ? '<span class="ch" style="background:#eee"></span>' : ''}</span></span>
            </div>
            <div class="stream-card-props"><span class="stream-card-prop" style="opacity:0.65;" data-last-seen="${device.id}"></span></div>
            ${(device.capabilities || []).includes('brightness_control') ? `
            <div class="brightness-control${_deviceBrightnessCache[device.id] == null ? ' brightness-loading' : ''}" data-brightness-wrap="${device.id}">
                <input type="range" class="brightness-slider" min="0" max="255"
                       value="${_deviceBrightnessCache[device.id] ?? 0}" data-device-brightness="${device.id}"
                       oninput="updateBrightnessLabel('${device.id}', this.value)"
                       onchange="saveCardBrightness('${device.id}', this.value)"
                       title="${_deviceBrightnessCache[device.id] != null ? Math.round(_deviceBrightnessCache[device.id] / 255 * 100) + '%' : '...'}"
                       ${_deviceBrightnessCache[device.id] == null ? 'disabled' : ''}>
            </div>` : ''}
            ${renderTagChips(device.tags)}`,
        actions: `
            <button class="btn btn-icon btn-secondary card-ping-btn" onclick="event.stopPropagation(); pingDevice('${device.id}')" title="${t('device.button.ping')}">
                ${ICON_REFRESH}
            </button>
            <button class="btn btn-icon btn-secondary" onclick="cloneDevice('${device.id}')" title="${t('common.clone')}">
                ${ICON_CLONE}
            </button>
            <button class="btn btn-icon btn-secondary" onclick="showSettings('${device.id}')" title="${t('device.button.settings')}">
                ${ICON_SETTINGS}
            </button>`,
    });
}

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 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) {
            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 apiKey = localStorage.getItem('wled_api_key') || '';
                const wsUrl = `${wsProto}//${hostPart}/api/v1/devices/${device.id}/ws?token=${encodeURIComponent(apiKey)}`;
                (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';
        }

        // 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 || '';

        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; } settingsModal.forceClose(); }
export function closeDeviceSettingsModal() { settingsModal.close(); }

export async function saveDeviceSettings() {
    const deviceId = (document.getElementById('settings-device-id') as HTMLInputElement).value;
    const name = (document.getElementById('settings-device-name') as HTMLInputElement).value.trim();
    const url = settingsModal._getUrl();

    if (!name || !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);
        }
        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) slider.title = Math.round(parseInt(value) / 255 * 100) + '%';
}

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.title = Math.round(data.brightness / 255 * 100) + '%';
            slider.disabled = false;
        }
        const wrap = document.querySelector(`[data-brightness-wrap="${CSS.escape(deviceId)}"]`) as HTMLElement | null;
        if (wrap) wrap.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}`;
        }
    }
}
