/**
 * Settings — tabbed modal (General / Backup / MQTT) + full-screen Log overlay.
 */

import { apiKey } from '../core/state.ts';
import { API_BASE, fetchWithAuth } from '../core/api.ts';
import { Modal } from '../core/modal.ts';
import { showToast, showConfirm } from '../core/ui.ts';
import { t } from '../core/i18n.ts';
import { ICON_UNDO, ICON_DOWNLOAD, ICON_SQUARE, ICON_CIRCLE } from '../core/icons.ts';
import { IconSelect } from '../core/icon-select.ts';
import { openAuthedWs } from '../core/ws-auth.ts';
import {
    NotificationChannel, NotificationPreferences,
    refreshNotificationPreferences, saveNotificationPreferences,
    requestOsNotificationPermission, fireTestNotification, getOsPermission,
} from './notifications-watcher.ts';

// ─── External URL (used by other modules for user-visible URLs) ──

let _externalUrl = '';
let _externalUrlInputBound = false;

/** Get the configured external base URL (empty string = not set). */
export function getExternalUrl(): string {
    return _externalUrl;
}

/**
 * Return the base origin for user-visible URLs (webhook, WS).
 * If an external URL is configured, use that; otherwise fall back to window.location.origin.
 */
export function getBaseOrigin(): string {
    return _externalUrl || window.location.origin;
}

/** Show or hide the save-bar that sits below the External URL field. */
function _updateExternalUrlSaveBar(): void {
    const input = document.getElementById('settings-external-url') as HTMLInputElement | null;
    const bar = document.getElementById('settings-external-url-save-bar');
    if (!input || !bar) return;
    const dirty = input.value.trim().replace(/\/+$/, '') !== _externalUrl;
    bar.hidden = !dirty;
}

/** Wire input listener once so editing the External URL toggles the save-bar. */
function _bindExternalUrlInput(): void {
    if (_externalUrlInputBound) return;
    const input = document.getElementById('settings-external-url') as HTMLInputElement | null;
    if (!input) return;
    input.addEventListener('input', _updateExternalUrlSaveBar);
    _externalUrlInputBound = true;
}

export async function loadExternalUrl(): Promise<void> {
    try {
        const resp = await fetchWithAuth('/system/external-url');
        if (!resp.ok) return;
        const data = await resp.json();
        _externalUrl = data.external_url || '';
        const input = document.getElementById('settings-external-url') as HTMLInputElement | null;
        if (input) input.value = _externalUrl;
        _bindExternalUrlInput();
        _updateExternalUrlSaveBar();
    } catch (err) {
        console.error('Failed to load external URL:', err);
    }
}

export async function saveExternalUrl(): Promise<void> {
    const input = document.getElementById('settings-external-url') as HTMLInputElement | null;
    if (!input) return;
    const url = input.value.trim().replace(/\/+$/, '');
    try {
        const resp = await fetchWithAuth('/system/external-url', {
            method: 'PUT',
            body: JSON.stringify({ external_url: url }),
        });
        if (!resp.ok) {
            const err = await resp.json().catch(() => ({}));
            throw new Error(err.detail || `HTTP ${resp.status}`);
        }
        const data = await resp.json();
        _externalUrl = data.external_url || '';
        input.value = _externalUrl;
        _updateExternalUrlSaveBar();
        showToast(t('settings.external_url.saved'), 'success');
    } catch (err) {
        console.error('Failed to save external URL:', err);
        showToast(t('settings.external_url.save_error') + ': ' + err.message, 'error');
    }
}

/** Discard pending edits and restore the persisted External URL value. */
export function revertExternalUrl(): void {
    const input = document.getElementById('settings-external-url') as HTMLInputElement | null;
    if (!input) return;
    input.value = _externalUrl;
    _updateExternalUrlSaveBar();
}

// ─── Settings-modal tab switching ───────────────────────────

const SETTINGS_ACTIVE_TAB_KEY = 'settings_active_tab';

export function switchSettingsTab(tabId: string): void {
    let activeBtn: HTMLElement | null = null;
    // Both selectors are queried so older cached templates with the legacy
    // top tab strip continue to work alongside the new left rail.
    document.querySelectorAll('.settings-tab-btn, .settings-rail-btn').forEach(btn => {
        const isActive = (btn as HTMLElement).dataset.settingsTab === tabId;
        btn.classList.toggle('active', isActive);
        if (isActive) activeBtn = btn as HTMLElement;
    });
    document.querySelectorAll('.settings-panel').forEach(panel => {
        panel.classList.toggle('active', panel.id === `settings-panel-${tabId}`);
    });
    // Keep the active tab visible inside the (possibly scrolling) tab bar / rail.
    if (activeBtn) {
        (activeBtn as HTMLElement).scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
    }
    // Swap the modal channel stripe color to match the tab's rail accent.
    const modalContent = document.querySelector('#settings-modal .modal-content') as HTMLElement | null;
    const railCh = (activeBtn as HTMLElement | null)?.dataset.railCh;
    if (modalContent && railCh) {
        modalContent.style.setProperty('--modal-ch', `var(--ch-${railCh}, var(--ch-amber))`);
    } else if (modalContent) {
        modalContent.style.removeProperty('--modal-ch');
    }
    // Remember so the next openSettingsModal() re-opens this tab.
    try { localStorage.setItem(SETTINGS_ACTIVE_TAB_KEY, tabId); } catch { /* storage blocked */ }
    // Lazy-render the appearance tab content
    if (tabId === 'appearance' && typeof window.renderAppearanceTab === 'function') {
        window.renderAppearanceTab();
    }
    // Lazy-load update settings
    if (tabId === 'updates' && typeof (window as any).loadUpdateSettings === 'function') {
        (window as any).initUpdateSettingsPanel();
        (window as any).loadUpdateSettings();
    }
    // Lazy-render the about panel content
    if (tabId === 'about' && typeof (window as any).renderAboutPanel === 'function') {
        (window as any).renderAboutPanel();
    }
    // Lazy-render the notifications panel (build IconSelects + load prefs)
    if (tabId === 'notifications') {
        initNotificationsPanel();
    }
}

// ─── Log Viewer ────────────────────────────────────────────

/** @type {WebSocket|null} */
let _logWs: WebSocket | null = null;

/** Connection state — drives LED cluster, patch indicator, and the
 *  signal-flow strip on the console surface. */
type LogConnectionState = 'idle' | 'connecting' | 'live' | 'error';

/** Live tally of streamed lines, by severity. Reset on Clear. */
const _logStats = { total: 0, warn: 0, err: 0 };

/** Level ordering for filter comparisons */
const _LOG_LEVELS = { DEBUG: 10, INFO: 20, WARNING: 30, ERROR: 40, CRITICAL: 50 };

function _detectLevel(line: string): string {
    for (const lvl of ['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG']) {
        if (line.includes(lvl)) return lvl;
    }
    return 'DEBUG';
}

function _levelClass(level: string): string {
    if (level === 'ERROR' || level === 'CRITICAL') return 'log-line-error';
    if (level === 'WARNING') return 'log-line-warning';
    if (level === 'DEBUG') return 'log-line-debug';
    return '';
}

function _filterLevel(): string {
    const sel = document.getElementById('log-viewer-filter') as HTMLSelectElement | null;
    return sel ? sel.value : 'all';
}

function _linePassesFilter(line: string): boolean {
    const filter = _filterLevel();
    if (filter === 'all') return true;
    const lineLvl = _detectLevel(line);
    return (_LOG_LEVELS[lineLvl] ?? 10) >= (_LOG_LEVELS[filter] ?? 0);
}

/** Mirror the live tally into the .mod-metric cells and toggle the
 *  channel-tinted "has-warn"/"has-errors" classes for non-zero counts. */
function _renderLogStats(): void {
    const total = document.getElementById('log-stat-total');
    const warn = document.getElementById('log-stat-warn');
    const err = document.getElementById('log-stat-err');
    if (total) total.textContent = String(_logStats.total);
    if (warn) warn.textContent = String(_logStats.warn);
    if (err) err.textContent = String(_logStats.err);

    document.getElementById('log-stat-warn-cell')?.classList.toggle('has-warn', _logStats.warn > 0);
    document.getElementById('log-stat-err-cell')?.classList.toggle('has-errors', _logStats.err > 0);
}

function _resetLogStats(): void {
    _logStats.total = 0;
    _logStats.warn = 0;
    _logStats.err = 0;
    _renderLogStats();
}

/** Map the live connection state onto the header LED cluster, the
 *  patch indicator dot/label, and the signal-flow class. */
function _setLogConnectionState(state: LogConnectionState): void {
    const overlay = document.getElementById('log-overlay');
    overlay?.classList.toggle('is-streaming', state === 'live');

    const leds = Array.from(document.querySelectorAll<HTMLElement>('#log-viewer-leds .led'));
    leds.forEach((led, idx) => {
        led.classList.remove('on', 'blink', 'fault');
        if (state === 'live') {
            led.classList.add('on');
            if (idx > 0) led.classList.add('blink');
        } else if (state === 'connecting') {
            if (idx === 0) led.classList.add('on', 'blink');
        } else if (state === 'error') {
            if (idx === 0) led.classList.add('fault');
        }
    });

    const patchDot = document.querySelector<HTMLElement>('#log-patch-indicator .patch-dot');
    patchDot?.classList.toggle('is-live', state === 'live');

    const patchLabel = document.getElementById('log-patch-label');
    if (patchLabel) {
        const key =
            state === 'live' ? 'settings.logs.patch.live'
            : state === 'connecting' ? 'settings.logs.patch.connecting'
            : state === 'error' ? 'settings.logs.patch.error'
            : 'settings.logs.patch.idle';
        const fallback =
            state === 'live' ? 'STREAMING'
            : state === 'connecting' ? 'CONNECTING'
            : state === 'error' ? 'OFFLINE'
            : 'STANDBY';
        const translated = t(key);
        // t() returns the key itself when a translation is missing —
        // detect that and fall through to the English label.
        patchLabel.textContent = translated === key ? fallback : translated;
        patchLabel.dataset.i18n = key;
    }

    const btn = document.getElementById('log-viewer-connect-btn');
    if (btn) {
        const isConnected = state === 'live' || state === 'connecting';
        const labelKey = isConnected ? 'settings.logs.disconnect' : 'settings.logs.connect';
        const labelEl = btn.querySelector('span') ?? btn;
        labelEl.textContent = t(labelKey);
        if (labelEl instanceof HTMLElement) labelEl.dataset.i18n = labelKey;
        btn.classList.toggle('mod-btn-go', !isConnected);
        btn.classList.toggle('mod-btn-stop', isConnected);
    }
}

function _appendLine(line: string): void {
    // Skip keepalive empty pings
    if (!line) return;

    const level = _detectLevel(line);

    // Tally is independent of the active filter — counts always reflect
    // the unfiltered stream so the user can see the full picture.
    _logStats.total += 1;
    if (level === 'WARNING') _logStats.warn += 1;
    if (level === 'ERROR' || level === 'CRITICAL') _logStats.err += 1;
    _renderLogStats();

    document.getElementById('log-overlay')?.classList.add('has-data');

    if (!_linePassesFilter(line)) return;

    const output = document.getElementById('log-viewer-output');
    if (!output) return;

    const cls = _levelClass(level);
    const span = document.createElement('span');
    if (cls) span.className = cls;
    span.textContent = line + '\n';
    output.appendChild(span);

    // Auto-scroll to bottom
    output.scrollTop = output.scrollHeight;
}

export function connectLogViewer(): void {
    if (_logWs && (_logWs.readyState === WebSocket.OPEN || _logWs.readyState === WebSocket.CONNECTING)) {
        // Disconnect
        _logWs.close();
        _logWs = null;
        _setLogConnectionState('idle');
        return;
    }

    const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
    const url = `${proto}//${location.host}/api/v1/system/logs/ws`;

    _setLogConnectionState('connecting');

    openAuthedWs(url).then((ws) => {
        _logWs = ws;
        _setLogConnectionState('live');

        ws.onmessage = (evt) => {
            _appendLine(evt.data);
        };

        ws.onerror = () => {
            showToast(t('settings.logs.error'), 'error');
            _setLogConnectionState('error');
        };

        ws.onclose = () => {
            _logWs = null;
            _setLogConnectionState('idle');
        };
    }).catch(() => {
        showToast(t('settings.logs.error'), 'error');
        _setLogConnectionState('error');
    });
}

export function disconnectLogViewer(): void {
    if (_logWs) {
        _logWs.close();
        _logWs = null;
    }
    _setLogConnectionState('idle');
}

export function clearLogViewer(): void {
    const output = document.getElementById('log-viewer-output');
    if (output) output.innerHTML = '';
    _resetLogStats();
    document.getElementById('log-overlay')?.classList.remove('has-data');
}

/** Re-render the log output according to the current filter selection. */
export function applyLogFilter(): void {
    const output = document.getElementById('log-viewer-output');
    if (!output) return;
    const filter = _filterLevel();
    for (const span of Array.from(output.children)) {
        const line = span.textContent;
        const lineLvl = _detectLevel(line);
        const passes = filter === 'all' || (_LOG_LEVELS[lineLvl] ?? 10) >= (_LOG_LEVELS[filter] ?? 0);
        (span as HTMLElement).style.display = passes ? '' : 'none';
    }
}

// ─── Log Overlay (full-screen) ──────────────────────────────

let _logFilterIconSelect: IconSelect | null = null;

/** Build filter items lazily so t() has locale data loaded. */
function _getLogFilterItems(): { value: string; icon: string; label: string; desc: string }[] {
    return [
        { value: 'all',     icon: '<span style="color:#9e9e9e;font-weight:700">*</span>', label: t('settings.logs.filter.all'),     desc: t('settings.logs.filter.all_desc') },
        { value: 'INFO',    icon: '<span style="color:#4fc3f7;font-weight:700">I</span>', label: t('settings.logs.filter.info'),    desc: t('settings.logs.filter.info_desc') },
        { value: 'WARNING', icon: '<span style="color:#ffb74d;font-weight:700">W</span>', label: t('settings.logs.filter.warning'), desc: t('settings.logs.filter.warning_desc') },
        { value: 'ERROR',   icon: '<span style="color:#ef5350;font-weight:700">E</span>', label: t('settings.logs.filter.error'),   desc: t('settings.logs.filter.error_desc') },
    ];
}

export function openLogOverlay(): void {
    const overlay = document.getElementById('log-overlay');
    if (overlay) {
        overlay.style.display = 'flex';

        // Initialize log filter icon select (once)
        if (!_logFilterIconSelect) {
            const filterSel = document.getElementById('log-viewer-filter') as HTMLSelectElement | null;
            if (filterSel) {
                _logFilterIconSelect = new IconSelect({
                    target: filterSel,
                    items: _getLogFilterItems(),
                    columns: 2,
                    onChange: () => applyLogFilter(),
                });
            }
        }

        // Auto-connect when opening
        if (!_logWs || _logWs.readyState !== WebSocket.OPEN) {
            connectLogViewer();
        }
    }
}

export function closeLogOverlay(): void {
    const overlay = document.getElementById('log-overlay');
    if (overlay) overlay.style.display = 'none';
    disconnectLogViewer();
}

// ─── Settings Modal ─────────────────────────────────────────

// Simple modal (no form / no dirty check needed)
const settingsModal = new Modal('settings-modal');

let _logLevelIconSelect: IconSelect | null = null;
let _autoBackupIntervalIconSelect: IconSelect | null = null;
let _shutdownActionIconSelect: IconSelect | null = null;

// Notifications: the visual matrix is now the source of truth — see
// initNotificationsPanel() / _setNotifMatrixSelection() below.

type ShutdownAction = 'stop_targets' | 'nothing';
const _SHUTDOWN_ACTIONS: readonly ShutdownAction[] = ['stop_targets', 'nothing'] as const;
function _isShutdownAction(v: string): v is ShutdownAction {
    return (_SHUTDOWN_ACTIONS as readonly string[]).includes(v);
}

/** Build interval items (hour-tiles) for auto-backup and update check pickers.
 *  Labels match the existing native-<option> text verbatim so no new i18n keys are needed.
 */
export function _getHourIntervalItems(): { value: string; icon: string; label: string }[] {
    return [
        { value: '1',   icon: '<span style="font-weight:700">1h</span>',  label: '1h' },
        { value: '6',   icon: '<span style="font-weight:700">6h</span>',  label: '6h' },
        { value: '12',  icon: '<span style="font-weight:700">12h</span>', label: '12h' },
        { value: '24',  icon: '<span style="font-weight:700">24h</span>', label: '24h' },
        { value: '48',  icon: '<span style="font-weight:700">48h</span>', label: '48h' },
        { value: '168', icon: '<span style="font-weight:700">7d</span>',  label: '7d' },
    ];
}

/** Build shutdown-action items lazily so t() has locale data loaded. */
function _getShutdownActionItems(): { value: string; icon: string; label: string; desc: string }[] {
    return [
        {
            value: 'stop_targets',
            icon: ICON_SQUARE,
            label: t('settings.shutdown_action.opt.stop'),
            desc: t('settings.shutdown_action.opt.stop_desc'),
        },
        {
            value: 'nothing',
            icon: ICON_CIRCLE,
            label: t('settings.shutdown_action.opt.nothing'),
            desc: t('settings.shutdown_action.opt.nothing_desc'),
        },
    ];
}

/** Build log-level items lazily so t() has locale data loaded. */
function _getLogLevelItems(): { value: string; icon: string; label: string; desc: string }[] {
    return [
        { value: 'DEBUG',    icon: '<span style="color:#9e9e9e;font-weight:700">D</span>', label: 'DEBUG',    desc: t('settings.log_level.desc.debug') },
        { value: 'INFO',     icon: '<span style="color:#4fc3f7;font-weight:700">I</span>', label: 'INFO',     desc: t('settings.log_level.desc.info') },
        { value: 'WARNING',  icon: '<span style="color:#ffb74d;font-weight:700">W</span>', label: 'WARNING',  desc: t('settings.log_level.desc.warning') },
        { value: 'ERROR',    icon: '<span style="color:#ef5350;font-weight:700">E</span>', label: 'ERROR',    desc: t('settings.log_level.desc.error') },
        { value: 'CRITICAL', icon: '<span style="color:#ff1744;font-weight:700">!</span>', label: 'CRITICAL', desc: t('settings.log_level.desc.critical') },
    ];
}

export function openSettingsModal(): void {
    (document.getElementById('settings-error') as HTMLElement).style.display = 'none';

    // Restore last-opened tab (from localStorage) if the tab still exists;
    // fall back to 'general' otherwise. Callers that want a specific tab
    // (e.g. donation link → about, update badge → updates) call
    // switchSettingsTab() themselves *after* opening.
    let saved = 'general';
    try { saved = localStorage.getItem(SETTINGS_ACTIVE_TAB_KEY) || 'general'; } catch { /* ignore */ }
    if (!document.getElementById(`settings-panel-${saved}`)) saved = 'general';
    switchSettingsTab(saved);

    settingsModal.open();

    // Initialize log level icon select
    if (!_logLevelIconSelect) {
        const levelSel = document.getElementById('settings-log-level') as HTMLSelectElement | null;
        if (levelSel) {
            _logLevelIconSelect = new IconSelect({
                target: levelSel,
                items: _getLogLevelItems(),
                columns: 3,
                onChange: () => setLogLevel(),
            });
        }
    }

    // Initialize auto-backup interval icon select. The onChange callback
    // auto-persists the section — there is no longer a manual Save button.
    if (!_autoBackupIntervalIconSelect) {
        const sel = document.getElementById('auto-backup-interval') as HTMLSelectElement | null;
        if (sel) {
            _autoBackupIntervalIconSelect = new IconSelect({
                target: sel,
                items: _getHourIntervalItems(),
                columns: 3,
                onChange: () => saveAutoBackupSettings(),
            });
        }
    }

    // Initialize shutdown-action icon select
    if (!_shutdownActionIconSelect) {
        const sel = document.getElementById('settings-shutdown-action') as HTMLSelectElement | null;
        if (sel) {
            _shutdownActionIconSelect = new IconSelect({
                target: sel,
                items: _getShutdownActionItems(),
                columns: 2,
                onChange: () => setShutdownAction(),
            });
        }
    }

    loadApiKeysList();
    loadExternalUrl();
    loadAutoBackupSettings();
    _bindAutoBackupListeners();
    loadBackupList();
    loadLogLevel();
    loadShutdownAction();
    loadDaylightTimezone();
    _seedRailFooter();
    // Refresh the update status so the rail badge ("update available" pill
    // on the Updates tab) is current when the modal opens — it would
    // otherwise reflect whatever state the app loaded with.
    if (typeof (window as any).loadUpdateStatus === 'function') {
        (window as any).loadUpdateStatus();
    }
}

/** Populate the rail footer with version info, mirroring the page header
 *  badge so the modal feels grounded. Idempotent — safe to call repeatedly. */
function _seedRailFooter(): void {
    const footer = document.getElementById('settings-rail-build');
    if (!footer) return;
    const version = document.getElementById('version-number')?.textContent?.trim() || '';
    footer.textContent = version ? version : '';
}

export function closeSettingsModal(): void {
    settingsModal.forceClose();
}

// ─── Backup ────────────────────────────────────────────────

export async function downloadBackup(): Promise<void> {
    try {
        const resp = await fetchWithAuth('/system/backup', { timeout: 30000 });
        if (!resp.ok) {
            const err = await resp.json().catch(() => ({}));
            throw new Error(err.detail || `HTTP ${resp.status}`);
        }
        const blob = await resp.blob();
        const disposition = resp.headers.get('Content-Disposition') || '';
        const match = disposition.match(/filename="(.+?)"/);
        const filename = match ? match[1] : 'ledgrab-backup.json';

        const a = document.createElement('a');
        a.href = URL.createObjectURL(blob);
        a.download = filename;
        document.body.appendChild(a);
        a.click();
        a.remove();
        URL.revokeObjectURL(a.href);

        showToast(t('settings.backup.success'), 'success');
    } catch (err) {
        console.error('Backup download failed:', err);
        showToast(t('settings.backup.error') + ': ' + err.message, 'error');
    }
}

// ─── Restore ───────────────────────────────────────────────

export async function handleRestoreFileSelected(input: HTMLInputElement): Promise<void> {
    const file = input.files![0];
    input.value = '';
    if (!file) return;

    const confirmed = await showConfirm(t('settings.restore.confirm'));
    if (!confirmed) return;

    try {
        const formData = new FormData();
        formData.append('file', file);

        const resp = await fetch(`${API_BASE}/system/restore`, {
            method: 'POST',
            headers: { 'Authorization': `Bearer ${apiKey}` },
            body: formData,
        });

        if (!resp.ok) {
            const err = await resp.json().catch(() => ({}));
            throw new Error(err.detail || `HTTP ${resp.status}`);
        }

        const data = await resp.json();
        showToast(data.message || t('settings.restore.success'), 'success');
        settingsModal.forceClose();

    } catch (err) {
        console.error('Restore failed:', err);
        showToast(t('settings.restore.error') + ': ' + err.message, 'error');
    }
}

// ─── Server restart ────────────────────────────────────────

export async function restartServer(): Promise<void> {
    const confirmed = await showConfirm(t('settings.restart_confirm'));
    if (!confirmed) return;

    try {
        const resp = await fetchWithAuth('/system/restart', { method: 'POST' });
        if (!resp.ok) {
            const err = await resp.json().catch(() => ({}));
            throw new Error(err.detail || `HTTP ${resp.status}`);
        }
        settingsModal.forceClose();
    } catch (err) {
        console.error('Server restart failed:', err);
        showToast(t('settings.restore.error') + ': ' + err.message, 'error');
    }
}

// ─── Auto-Backup settings ─────────────────────────────────

export async function loadAutoBackupSettings(): Promise<void> {
    try {
        const resp = await fetchWithAuth('/system/auto-backup/settings');
        if (!resp.ok) return;
        const data = await resp.json();

        (document.getElementById('auto-backup-enabled') as HTMLInputElement).checked = data.enabled;
        const intervalSel = document.getElementById('auto-backup-interval') as HTMLSelectElement;
        intervalSel.value = String(data.interval_hours);
        if (_autoBackupIntervalIconSelect) _autoBackupIntervalIconSelect.setValue(String(data.interval_hours));
        (document.getElementById('auto-backup-max') as HTMLInputElement).value = data.max_backups;

        const statusEl = document.getElementById('auto-backup-status')!;
        if (data.last_backup_time) {
            const d = new Date(data.last_backup_time);
            statusEl.textContent = t('settings.auto_backup.last_backup') + ': ' + d.toLocaleString();
        } else {
            statusEl.textContent = t('settings.auto_backup.last_backup') + ': ' + t('settings.auto_backup.never');
        }

        const pill = document.getElementById('auto-backup-status-pill');
        if (pill) {
            if (data.enabled) {
                pill.textContent = t('settings.auto_backup.pill.running');
                pill.hidden = false;
            } else {
                pill.hidden = true;
            }
        }
    } catch (err) {
        console.error('Failed to load auto-backup settings:', err);
    }
}

/** Persist auto-backup settings. The Save button has been removed —
 *  this is invoked silently from change-listeners on the three fields
 *  (enabled checkbox, interval IconSelect, max-backups input).
 *  Errors still surface as toasts; success is silent because the
 *  Auto-Backup section's own pill/status text reflects the new state. */
export async function saveAutoBackupSettings(): Promise<void> {
    const enabledEl = document.getElementById('auto-backup-enabled') as HTMLInputElement | null;
    const intervalEl = document.getElementById('auto-backup-interval') as HTMLInputElement | null;
    const maxEl = document.getElementById('auto-backup-max') as HTMLInputElement | null;
    if (!enabledEl || !intervalEl || !maxEl) return;

    const enabled = enabledEl.checked;
    const interval_hours = parseFloat(intervalEl.value);
    const max_backups = parseInt(maxEl.value, 10);
    if (Number.isNaN(interval_hours) || Number.isNaN(max_backups)) return;

    try {
        const resp = await fetchWithAuth('/system/auto-backup/settings', {
            method: 'PUT',
            body: JSON.stringify({ enabled, interval_hours, max_backups }),
        });
        if (!resp.ok) {
            const err = await resp.json().catch(() => ({}));
            throw new Error(err.detail || `HTTP ${resp.status}`);
        }
        loadAutoBackupSettings();
        loadBackupList();
    } catch (err) {
        console.error('Failed to save auto-backup settings:', err);
        showToast(t('settings.auto_backup.save_error') + ': ' + err.message, 'error');
    }
}

/** Wire change listeners on the three auto-backup fields so any edit
 *  auto-saves. Called once per modal open; idempotent. */
let _autoBackupListenersBound = false;
function _bindAutoBackupListeners(): void {
    if (_autoBackupListenersBound) return;
    const enabledEl = document.getElementById('auto-backup-enabled') as HTMLInputElement | null;
    const maxEl = document.getElementById('auto-backup-max') as HTMLInputElement | null;
    if (!enabledEl || !maxEl) return;
    // Toggle: persist on every flip.
    enabledEl.addEventListener('change', () => { saveAutoBackupSettings(); });
    // Number input: fires on blur / Enter — avoids saving on every keystroke
    // while the user is still typing a multi-digit value.
    maxEl.addEventListener('change', () => { saveAutoBackupSettings(); });
    _autoBackupListenersBound = true;
}

export async function triggerBackupNow(): Promise<void> {
    try {
        const resp = await fetchWithAuth('/system/auto-backup/trigger', { method: 'POST' });
        if (!resp.ok) {
            const err = await resp.json().catch(() => ({}));
            throw new Error(err.detail || `HTTP ${resp.status}`);
        }
        showToast(t('settings.auto_backup.backup_created'), 'success');
        loadBackupList();
        loadAutoBackupSettings();
    } catch (err) {
        console.error('Backup failed:', err);
        showToast(t('settings.auto_backup.backup_error') + ': ' + err.message, 'error');
    }
}

// ─── Saved backup list ────────────────────────────────────

export async function loadBackupList(): Promise<void> {
    const container = document.getElementById('saved-backups-list')!;
    const meta = document.getElementById('saved-backups-meta');
    container.setAttribute('data-empty', t('settings.saved_backups.empty'));
    try {
        const resp = await fetchWithAuth('/system/backups');
        if (!resp.ok) return;
        const data = await resp.json();

        if (data.count === 0) {
            container.innerHTML = '';
            if (meta) meta.hidden = true;
            return;
        }

        let totalBytes = 0;
        container.innerHTML = data.backups.map(b => {
            const sizeBytes = b.size_bytes || 0;
            totalBytes += sizeBytes;
            const sizeStr = sizeBytes >= 1024 * 1024
                ? (sizeBytes / (1024 * 1024)).toFixed(1) + ' MB'
                : (sizeBytes / 1024).toFixed(1) + ' KB';
            const date = new Date(b.created_at).toLocaleString();
            const isAuto = b.filename.startsWith('ledgrab-autobackup-');
            const typeKey = isAuto ? 'settings.saved_backups.type.auto' : 'settings.saved_backups.type.manual';
            return `<div class="backup-row">
                <div>
                    <div class="backup-name" title="${b.filename}">${date}</div>
                    <div class="backup-meta">${sizeStr} · ${t(typeKey)}</div>
                </div>
                <button class="icon-btn" onclick="restoreSavedBackup('${b.filename}')" title="${t('settings.saved_backups.restore')}">${ICON_UNDO}</button>
                <button class="icon-btn" onclick="downloadSavedBackup('${b.filename}')" title="${t('settings.saved_backups.download')}">${ICON_DOWNLOAD}</button>
                <button class="icon-btn danger" onclick="deleteSavedBackup('${b.filename}')" title="${t('settings.saved_backups.delete')}"><svg class="icon" viewBox="0 0 24 24"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></button>
            </div>`;
        }).join('');
        if (meta) {
            meta.hidden = false;
            const totalStr = totalBytes >= 1024 * 1024
                ? (totalBytes / (1024 * 1024)).toFixed(1) + ' MB'
                : (totalBytes / 1024).toFixed(1) + ' KB';
            meta.textContent = `${data.count} · ${totalStr}`;
        }
    } catch (err) {
        console.error('Failed to load backup list:', err);
        container.innerHTML = '';
        if (meta) meta.hidden = true;
    }
}

export async function downloadSavedBackup(filename: string): Promise<void> {
    try {
        const resp = await fetchWithAuth(`/system/backups/${encodeURIComponent(filename)}`, { timeout: 30000 });
        if (!resp.ok) {
            const err = await resp.json().catch(() => ({}));
            throw new Error(err.detail || `HTTP ${resp.status}`);
        }
        const blob = await resp.blob();
        const a = document.createElement('a');
        a.href = URL.createObjectURL(blob);
        a.download = filename;
        document.body.appendChild(a);
        a.click();
        a.remove();
        URL.revokeObjectURL(a.href);
    } catch (err) {
        console.error('Backup download failed:', err);
        showToast(t('settings.backup.error') + ': ' + err.message, 'error');
    }
}

export async function restoreSavedBackup(filename: string): Promise<void> {
    const confirmed = await showConfirm(t('settings.restore.confirm'));
    if (!confirmed) return;

    try {
        // Download the backup file from the server
        const dlResp = await fetchWithAuth(`/system/backups/${encodeURIComponent(filename)}`, { timeout: 30000 });
        if (!dlResp.ok) {
            const err = await dlResp.json().catch(() => ({}));
            throw new Error(err.detail || `HTTP ${dlResp.status}`);
        }
        const blob = await dlResp.blob();

        // POST it to the restore endpoint
        const formData = new FormData();
        formData.append('file', blob, filename);

        const resp = await fetch(`${API_BASE}/system/restore`, {
            method: 'POST',
            headers: { 'Authorization': `Bearer ${apiKey}` },
            body: formData,
        });

        if (!resp.ok) {
            const err = await resp.json().catch(() => ({}));
            throw new Error(err.detail || `HTTP ${resp.status}`);
        }

        const data = await resp.json();
        showToast(data.message || t('settings.restore.success'), 'success');
        settingsModal.forceClose();

    } catch (err) {
        console.error('Restore from saved backup failed:', err);
        showToast(t('settings.restore.error') + ': ' + err.message, 'error');
    }
}

export async function deleteSavedBackup(filename: string): Promise<void> {
    const confirmed = await showConfirm(t('settings.saved_backups.delete_confirm'));
    if (!confirmed) return;

    try {
        const resp = await fetchWithAuth(`/system/backups/${encodeURIComponent(filename)}`, {
            method: 'DELETE',
        });
        if (!resp.ok) {
            const err = await resp.json().catch(() => ({}));
            throw new Error(err.detail || `HTTP ${resp.status}`);
        }
        loadBackupList();
    } catch (err) {
        console.error('Backup delete failed:', err);
        showToast(t('settings.saved_backups.delete_error') + ': ' + err.message, 'error');
    }
}

// ─── API Keys (read-only display) ─────────────────────────────

export async function loadApiKeysList(): Promise<void> {
    const container = document.getElementById('settings-api-keys-list');
    if (!container) return;
    const meta = document.getElementById('settings-api-keys-meta');
    try {
        const resp = await fetchWithAuth('/system/api-keys');
        if (!resp.ok) {
            container.innerHTML = `<div class="api-key-empty-note">${t('settings.api_keys.load_error')}</div>`;
            if (meta) meta.hidden = true;
            return;
        }
        const data = await resp.json();
        if (data.count === 0) {
            container.innerHTML = `<div class="api-key-empty-note">${t('settings.api_keys.empty')}</div>`;
            if (meta) meta.hidden = true;
            return;
        }
        container.innerHTML = data.keys.map(k =>
            `<div class="api-key-row">
                <span class="api-key-name">${k.label}</span>
                <span class="api-key-mask">${k.masked}</span>
                <span class="api-key-tag">${t('settings.api_keys.read_only')}</span>
            </div>`
        ).join('');
        if (meta) {
            meta.hidden = false;
            meta.textContent = `${data.count} ${data.count === 1 ? t('settings.api_keys.meta.one') : t('settings.api_keys.meta.many')}`;
        }
    } catch (err) {
        console.error('Failed to load API keys:', err);
        if (container) container.innerHTML = '';
        if (meta) meta.hidden = true;
    }
}

// ─── Log Level ────────────────────────────────────────────────

export async function loadLogLevel(): Promise<void> {
    try {
        const resp = await fetchWithAuth('/system/log-level');
        if (!resp.ok) return;
        const data = await resp.json();
        if (_logLevelIconSelect) {
            _logLevelIconSelect.setValue(data.level);
        } else {
            const select = document.getElementById('settings-log-level') as HTMLSelectElement | null;
            if (select) select.value = data.level;
        }
    } catch (err) {
        console.error('Failed to load log level:', err);
    }
}

export async function setLogLevel(): Promise<void> {
    const select = document.getElementById('settings-log-level') as HTMLSelectElement | null;
    if (!select) return;
    const level = select.value;
    try {
        const resp = await fetchWithAuth('/system/log-level', {
            method: 'PUT',
            body: JSON.stringify({ level }),
        });
        if (!resp.ok) {
            const err = await resp.json().catch(() => ({}));
            throw new Error(err.detail || `HTTP ${resp.status}`);
        }
        showToast(t('settings.log_level.saved'), 'success');
    } catch (err) {
        console.error('Failed to set log level:', err);
        showToast(t('settings.log_level.save_error') + ': ' + err.message, 'error');
    }
}

// ─── Shutdown action ──────────────────────────────────────────

export async function loadShutdownAction(): Promise<void> {
    try {
        const resp = await fetchWithAuth('/system/shutdown-action');
        if (!resp.ok) return;
        const data = await resp.json();
        const action: ShutdownAction = _isShutdownAction(data.action) ? data.action : 'stop_targets';
        if (_shutdownActionIconSelect) {
            _shutdownActionIconSelect.setValue(action);
        } else {
            const select = document.getElementById('settings-shutdown-action') as HTMLSelectElement | null;
            if (select) select.value = action;
        }
    } catch (err) {
        console.error('Failed to load shutdown action:', err);
    }
}

export async function setShutdownAction(): Promise<void> {
    const select = document.getElementById('settings-shutdown-action') as HTMLSelectElement | null;
    if (!select) return;
    const value = select.value;
    if (!_isShutdownAction(value)) return;
    try {
        const resp = await fetchWithAuth('/system/shutdown-action', {
            method: 'PUT',
            body: JSON.stringify({ action: value }),
        });
        if (!resp.ok) {
            const err = await resp.json().catch(() => ({}));
            throw new Error(err.detail || `HTTP ${resp.status}`);
        }
        showToast(t('settings.shutdown_action.saved'), 'success');
    } catch (err) {
        console.error('Failed to set shutdown action:', err);
        showToast(t('settings.shutdown_action.save_error') + ': ' + err.message, 'error');
    }
}

// ─── Daylight timezone (global) ───────────────────────────────

export async function loadDaylightTimezone(): Promise<void> {
    const select = document.getElementById('settings-daylight-timezone') as HTMLSelectElement | null;
    if (!select) return;
    let current = '';
    try {
        const resp = await fetchWithAuth('/preferences/daylight-timezone');
        if (resp.ok) {
            const data = await resp.json();
            current = typeof data?.timezone === 'string' ? data.timezone : '';
        }
    } catch (err) {
        console.error('Failed to load daylight timezone:', err);
    }
    const { enhanceTimezoneSelect } = await import('../core/daylight-tz.ts');
    enhanceTimezoneSelect(select, current, () => { saveDaylightTimezone(); });
}

export async function saveDaylightTimezone(): Promise<void> {
    const select = document.getElementById('settings-daylight-timezone') as HTMLSelectElement | null;
    if (!select) return;
    const tz = select.value;
    try {
        const resp = await fetchWithAuth('/preferences/daylight-timezone', {
            method: 'PUT',
            body: JSON.stringify({ timezone: tz }),
        });
        if (!resp.ok) {
            const err = await resp.json().catch(() => ({}));
            throw new Error(err.detail || `HTTP ${resp.status}`);
        }
        showToast(t('settings.daylight_timezone.saved'), 'success');
    } catch (err) {
        console.error('Failed to save daylight timezone:', err);
        showToast(t('settings.daylight_timezone.save_error') + ': ' + err.message, 'error');
    }
}

// ─── Notifications tab ────────────────────────────────────────

const _NOTIF_EVENT_KEYS = [
    'device_online', 'device_offline', 'device_discovered', 'device_lost',
] as const;
type NotifEventKey = typeof _NOTIF_EVENT_KEYS[number];

function _isNotifChannel(v: string): v is NotificationChannel {
    return v === 'none' || v === 'snack' || v === 'os' || v === 'both';
}

let _notifPrefsLoaded = false;
let _notifMatrixWired = false;

const _NOTIF_CHANNELS: NotificationChannel[] = ['snack', 'os', 'both', 'none'];

/** Reflect a (event, channel) selection in the visual matrix and the
 *  hidden underlying <select> that notifications-watcher.ts reads from. */
function _setNotifMatrixSelection(event: NotifEventKey, channel: NotificationChannel): void {
    // 1. Visual cells
    document.querySelectorAll(
        `#settings-notif-matrix .notif-matrix-opt[data-event="${event}"]`,
    ).forEach((cell) => {
        const c = cell as HTMLElement;
        c.classList.toggle('selected', c.dataset.channel === channel);
        c.setAttribute('aria-checked', c.dataset.channel === channel ? 'true' : 'false');
    });
    // 2. Underlying hidden <select> (must already contain a matching <option>).
    const sel = document.getElementById(
        `settings-notif-${event.replace(/_/g, '-')}`,
    ) as HTMLSelectElement | null;
    if (sel) {
        // Ensure the option exists — the matrix is the source of truth, so we
        // populate the select on demand instead of relying on IconSelect.
        if (!sel.querySelector(`option[value="${channel}"]`)) {
            for (const c of _NOTIF_CHANNELS) {
                if (sel.querySelector(`option[value="${c}"]`)) continue;
                const opt = document.createElement('option');
                opt.value = c;
                opt.textContent = c;
                sel.appendChild(opt);
            }
        }
        sel.value = channel;
    }
}

export async function initNotificationsPanel(): Promise<void> {
    // Wire matrix cells (idempotent).
    if (!_notifMatrixWired) {
        const matrix = document.getElementById('settings-notif-matrix');
        if (matrix) {
            // Pre-populate hidden selects with all channel options so
            // saveNotifPreferencesFromUi() can read .value back without
            // depending on IconSelect.
            for (const event of _NOTIF_EVENT_KEYS) {
                const sel = document.getElementById(
                    `settings-notif-${event.replace(/_/g, '-')}`,
                ) as HTMLSelectElement | null;
                if (!sel) continue;
                for (const c of _NOTIF_CHANNELS) {
                    if (sel.querySelector(`option[value="${c}"]`)) continue;
                    const opt = document.createElement('option');
                    opt.value = c;
                    opt.textContent = c;
                    sel.appendChild(opt);
                }
            }

            matrix.addEventListener('click', (e) => {
                const cell = (e.target as HTMLElement).closest('.notif-matrix-opt') as HTMLElement | null;
                if (!cell) return;
                const event = cell.dataset.event;
                const channel = cell.dataset.channel;
                if (!event || !channel || !_isNotifChannel(channel)) return;
                if (!_NOTIF_EVENT_KEYS.includes(event as NotifEventKey)) return;
                _setNotifMatrixSelection(event as NotifEventKey, channel);
                saveNotifPreferencesFromUi();
            });
            matrix.addEventListener('keydown', (e) => {
                const ke = e as KeyboardEvent;
                if (ke.key !== 'Enter' && ke.key !== ' ') return;
                const cell = (ke.target as HTMLElement).closest('.notif-matrix-opt') as HTMLElement | null;
                if (!cell) return;
                ke.preventDefault();
                cell.click();
            });
            _notifMatrixWired = true;
        }
    }

    const bgInput = document.getElementById('settings-notif-background') as HTMLInputElement | null;
    if (bgInput && !bgInput.dataset.wired) {
        bgInput.dataset.wired = '1';
        bgInput.addEventListener('change', () => saveNotifPreferencesFromUi());
    }

    if (!_notifPrefsLoaded) {
        try {
            const prefs = await refreshNotificationPreferences();
            for (const key of _NOTIF_EVENT_KEYS) {
                _setNotifMatrixSelection(key, prefs.channels[key]);
            }
            if (bgInput) bgInput.checked = prefs.background_discovery_enabled;
            _notifPrefsLoaded = true;
        } catch (err) {
            console.error('Failed to load notification preferences:', err);
        }
    }

    _refreshNotifPermissionState();
}

function _refreshNotifPermissionState(): void {
    const stateEl = document.getElementById('settings-notif-permission-state');
    const perm = getOsPermission();
    const key = perm === 'granted'
        ? 'settings.notifications.permission.state.granted'
        : perm === 'denied'
            ? 'settings.notifications.permission.state.denied'
            : 'settings.notifications.permission.state.default';
    if (stateEl) stateEl.textContent = t(key);
    // Target the Grant button specifically — not the bare `button` selector,
    // which would also match the `.hint-toggle` ? button now sitting in the
    // same row and disable it whenever permission is denied/granted.
    const grantBtn = document.querySelector(
        '#settings-notif-permission-row .btn',
    ) as HTMLButtonElement | null;
    if (grantBtn) grantBtn.disabled = perm === 'granted' || perm === 'denied';

    // Update the rack-panel meta pill so the section header reflects the
    // current OS permission state at a glance.
    const pill = document.getElementById('settings-notif-permission-pill');
    if (pill) {
        if (perm === 'granted') {
            pill.textContent = t('settings.notifications.permission.pill.granted');
            pill.hidden = false;
        } else if (perm === 'denied') {
            pill.textContent = t('settings.notifications.permission.pill.denied');
            pill.hidden = false;
        } else {
            pill.hidden = true;
        }
    }
}

async function saveNotifPreferencesFromUi(): Promise<void> {
    const bgInput = document.getElementById('settings-notif-background') as HTMLInputElement | null;
    const channels: Partial<NotificationPreferences['channels']> = {};
    for (const key of _NOTIF_EVENT_KEYS) {
        const sel = document.getElementById(`settings-notif-${key.replace(/_/g, '-')}`) as HTMLSelectElement | null;
        const value = sel?.value;
        if (value && _isNotifChannel(value)) {
            channels[key] = value;
        }
    }
    const next: NotificationPreferences = {
        channels: channels as NotificationPreferences['channels'],
        background_discovery_enabled: bgInput ? bgInput.checked : true,
        startup_grace_sec: 10,
        flap_debounce_sec: 5,
    };
    try {
        await saveNotificationPreferences(next);
        showToast(t('settings.notifications.saved'), 'success');
    } catch (err) {
        console.error('Failed to save notification preferences:', err);
        showToast(t('settings.notifications.save_error') + ': ' + (err as Error).message, 'error');
    }
}

export async function requestNotifPermissionFromSettings(): Promise<void> {
    const perm = await requestOsNotificationPermission();
    _refreshNotifPermissionState();
    if (perm === 'granted') {
        showToast(t('settings.notifications.permission.granted'), 'success');
    } else if (perm === 'denied') {
        showToast(t('settings.notifications.permission.denied'), 'error');
    }
}

export function testNotifFromSettings(): void {
    fireTestNotification();
}

