// ============================================================
// Core: Shared state, constants, utilities, i18n, API commands
// ============================================================

// SVG path constants (avoid rebuilding innerHTML on every state update)
export const SVG_PLAY = '<path d="M8 5v14l11-7z"/>';
export const SVG_PAUSE = '<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>';
export const SVG_STOP = '<path d="M6 6h12v12H6z"/>';
export const SVG_IDLE = '<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14.5v-9l6 4.5-6 4.5z"/>';
export const SVG_MUTED = '<path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/>';
export const SVG_UNMUTED = '<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/>';

// Empty state illustration SVGs
export const EMPTY_SVG_FOLDER = '<svg viewBox="0 0 64 64"><path d="M8 16h20l4-6h20a4 4 0 014 4v36a4 4 0 01-4 4H8a4 4 0 01-4-4V20a4 4 0 014-4z"/><path d="M4 24h56" stroke-dasharray="4 3" opacity="0.4"/></svg>';
export const EMPTY_SVG_FILE = '<svg viewBox="0 0 64 64"><path d="M16 4h22l14 14v38a4 4 0 01-4 4H16a4 4 0 01-4-4V8a4 4 0 014-4z"/><path d="M38 4v14h14"/><path d="M22 32h20M22 40h14" opacity="0.5"/></svg>';
export function emptyStateHtml(svgStr, text) {
    return `<div class="empty-state-illustration">${svgStr}<p>${text}</p></div>`;
}

// Media source registry: substring key → { name, icon }
export const MEDIA_SOURCES = {
    'spotify': {
        name: 'Spotify',
        icon: '<svg viewBox="0 0 24 24"><path fill="#1DB954" d="M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.66 0 12 0zm5.521 17.34c-.24.359-.66.48-1.021.24-2.82-1.74-6.36-2.101-10.561-1.141-.418.122-.779-.179-.899-.539-.12-.421.18-.78.54-.9 4.56-1.021 8.52-.6 11.64 1.32.42.18.479.659.301 1.02zm1.44-3.3c-.301.42-.841.6-1.262.3-3.239-1.98-8.159-2.58-11.939-1.38-.479.12-1.02-.12-1.14-.6-.12-.48.12-1.021.6-1.141C9.6 9.9 15 10.561 18.72 12.84c.361.181.54.78.241 1.2zm.12-3.36C15.24 8.4 8.82 8.16 5.16 9.301c-.6.179-1.2-.181-1.38-.721-.18-.601.18-1.2.72-1.381 4.26-1.26 11.28-1.02 15.721 1.621.539.3.719 1.02.419 1.56-.299.421-1.02.599-1.559.3z"/></svg>'
    },
    'yandex music': {
        name: 'Yandex Music',
        icon: '<svg viewBox="0 0 24 24"><path fill="#FFCC00" d="M12 0C5.376 0 0 5.376 0 12s5.376 12 12 12 12-5.376 12-12S18.624 0 12 0zm0 2.4a9.6 9.6 0 110 19.2 9.6 9.6 0 010-19.2z"/><path fill="#FFCC00" d="M13.2 6h-2.4v7.2L7.2 6H4.8l5.4 12h1.2l.6-1.35V6z"/></svg>'
    },
    'яндекс музыка': {
        name: 'Яндекс Музыка',
        icon: '<svg viewBox="0 0 24 24"><path fill="#FFCC00" d="M12 0C5.376 0 0 5.376 0 12s5.376 12 12 12 12-5.376 12-12S18.624 0 12 0zm0 2.4a9.6 9.6 0 110 19.2 9.6 9.6 0 010-19.2z"/><path fill="#FFCC00" d="M13.2 6h-2.4v7.2L7.2 6H4.8l5.4 12h1.2l.6-1.35V6z"/></svg>'
    },
    'chrome': {
        name: 'Google Chrome',
        icon: '<svg viewBox="0 0 24 24"><circle fill="#4587F3" cx="12" cy="12" r="11"/><path fill="#DB4437" d="M12 1C7.2 1 3.1 3.8 1.3 7.9L7.7 12l1.8-3.1c.7-1.1 1.9-1.9 3.3-1.9h9.7C21 3.5 16.9 1 12 1z"/><path fill="#0F9D58" d="M7.7 12L1.3 7.9C.5 9.2 0 10.6 0 12c0 4.5 2.8 8.4 6.8 10l3.8-6.6L7.7 12z"/><path fill="#FFCD40" d="M6.8 22c2.7 1.5 6.4 1.7 9.4.2 2.8-1.4 4.9-3.9 5.8-6.8l-6.5-3.4-1.8 3.1c-.7 1.1-1.9 1.9-3.3 1.9-.9 0-1.7-.3-2.4-.7L6.8 22z"/><circle fill="#F1F1F1" cx="12" cy="12" r="4.8"/><circle fill="#4587F3" cx="12" cy="12" r="3.8"/></svg>'
    },
    'msedge': {
        name: 'Microsoft Edge',
        icon: '<svg viewBox="0 0 24 24"><path fill="#0078D4" d="M21.86 17.86q.14 0 .25-.12.1-.13.1-.25 0-.06 0-.13-.12-.76-.39-1.49-.26-.72-.65-1.39-.4-.66-.92-1.25-.53-.58-1.15-1.06-.61-.48-1.3-.85-.69-.37-1.44-.6-.75-.22-1.53-.3-.8-.07-1.6 0h-.04q-.51.03-1.03.14-.5.12-1 .31-.49.2-.95.46-.46.27-.89.6-.42.32-.8.7-.37.4-.69.83-.31.44-.57.92-.25.49-.44 1 .09-.14.21-.28.12-.14.26-.27.14-.12.3-.23.16-.1.33-.18.18-.08.37-.14.18-.06.38-.08.2-.02.4-.01.21.01.41.06.28.07.53.2.25.12.47.3.21.18.39.4.18.21.32.45.14.25.23.52.1.26.14.54.04.28.02.56-.02.36-.12.72-.1.35-.27.68-.17.33-.4.62-.24.3-.52.56-.28.25-.6.46-.32.2-.67.35.44.1.9.14.44.03.89-.02.45-.05.88-.17.44-.12.85-.3.41-.2.79-.44.37-.25.71-.55.34-.3.63-.65.3-.35.54-.73.24-.39.42-.8.18-.42.3-.86.12-.43.18-.88.06-.45.06-.9 0-.48-.07-.95-.07-.47-.22-.93z"/><path fill="#50E6FF" d="M11.89.03Q10.03.17 8.3.88 6.57 1.59 5.1 2.77 3.65 3.94 2.55 5.5 1.44 7.06.79 8.88.14 10.7 0 12.65q.01.22.02.45 0 .22.03.44.04.42.12.83.08.42.2.83.12.4.28.79.16.39.36.76.2.37.43.72.24.34.51.66.27.32.57.6.3.29.63.54.33.25.68.46.35.21.72.38.38.17.77.28.39.12.79.18.41.06.82.05.41 0 .82-.07.41-.08.79-.22.39-.14.74-.34.36-.2.68-.44.33-.25.6-.54.28-.3.5-.63.23-.33.4-.7.17-.36.27-.75-1.1.9-2.44 1.36-1.33.46-2.77.46-1.26 0-2.44-.39-1.18-.39-2.17-1.08-1-1.08-1.6-2.02-.6-.94-.87-2-.27-1.07-.25-2.2.02-.55.12-1.08.1-.54.29-1.05.18-.52.44-1 .27-.49.6-.94.34-.44.74-.83.4-.38.85-.71.45-.32.94-.57.49-.25 1.02-.42.52-.16 1.07-.24.55-.07 1.1-.05.81.04 1.57.25.77.2 1.46.56.7.36 1.29.85.6.5 1.07 1.1.48.6.82 1.29.34.69.54 1.44.2.76.24 1.55.04.79-.08 1.57-.11.78-.37 1.52-.26.74-.66 1.4-.39.67-.91 1.24-.52.57-1.14 1.02-.62.44-1.32.76-.7.32-1.45.49-.75.16-1.52.18z"/></svg>'
    },
    'firefox': {
        name: 'Firefox',
        icon: '<svg viewBox="0 0 24 24"><path fill="#FF7139" d="M12 0C5.37 0 0 5.37 0 12s5.37 12 12 12 12-5.37 12-12S18.63 0 12 0zm6.73 7.27c-.47-.77-1.22-1.6-1.7-1.87.54.97.86 2.07.93 3.15 0 0-.02.03-.02.05-.38-1.34-1.14-2.15-1.78-3.05-.03-.05-.06-.1-.1-.15-.03-.05-.05-.1-.06-.15 0-.02-.01-.04-.02-.05l-.01.02c-.02.03-.03.05-.04.08 0 0 0 .01-.01.02l.01-.02c-.64 1.07-1.72 2.2-2.1 3.56-.46.01-.9.09-1.32.23l-.06.03c-.03-.2-.04-.4-.04-.6 0-.67.15-1.3.4-1.87-1.08.4-1.93 1.12-2.53 1.72-.33-.36-.36-1.56-.34-1.8-.01 0-.03.02-.04.02-.27.2-.52.42-.75.66-.28.3-.53.62-.76.96-.12.2-.24.4-.34.6-.15.32-.27.66-.36 1-.02.07-.03.14-.05.21v.03c-.06.3-.1.6-.12.9v.1c0 .07 0 .14-.01.21C7.3 13.8 7.52 16.37 9 18.26l.04.05c-1.55-1-2.57-2.64-2.87-4.42-.04.2-.06.4-.07.6-.01.2-.02.4-.01.6.02.6.13 1.2.3 1.77.2.57.46 1.12.8 1.62.17.25.36.48.56.7.2.22.42.43.66.62 1.83 1.47 4.17 1.87 6.34 1.21.26-.08.5-.17.74-.28 1.1-.5 2.06-1.27 2.78-2.23.03-.03.05-.07.07-.1.08-.1.15-.2.22-.32.5-.77.84-1.62 1.02-2.5.02-.1.04-.2.05-.3.1-.57.14-1.15.12-1.73 0-.1-.01-.19-.02-.29.06-1.2-.15-2.42-.63-3.53-.1-.23-.2-.45-.32-.67z"/></svg>'
    },
    'opera': {
        name: 'Opera',
        icon: '<svg viewBox="0 0 24 24"><path fill="#FF1B2D" d="M12 0C5.37 0 0 5.37 0 12s5.37 12 12 12c2.75 0 5.28-.93 7.3-2.49-1.24.77-2.68 1.22-4.22 1.22-2.2 0-4.17-1.1-5.55-2.83C8.1 18.1 7.2 15.22 7.2 12s.9-6.1 2.33-7.9C10.91 2.37 12.88 1.27 15.08 1.27c1.54 0 2.98.45 4.22 1.22C17.28.93 14.75 0 12 0z"/><path fill="#FF1B2D" d="M15.08 1.27c-2.2 0-4.17 1.1-5.55 2.83C8.1 5.9 7.2 8.78 7.2 12s.9 6.1 2.33 7.9c1.38 1.73 3.35 2.83 5.55 2.83 2.2 0 4.17-1.1 5.55-2.83C22.06 18.1 22.96 15.22 22.96 12s-.9-6.1-2.33-7.9c-1.38-1.73-3.35-2.83-5.55-2.83z" opacity=".75"/></svg>'
    },
    'brave': {
        name: 'Brave',
        icon: '<svg viewBox="0 0 24 24"><path fill="#FB542B" d="M12 0L3.6 4.8v9.6L12 24l8.4-9.6V4.8L12 0zm5.7 14.1l-1.2 1.8c-.3.3-.6.6-.9.9l-2.1 1.5-1.5.9-1.5-.9-2.1-1.5c-.3-.3-.6-.6-.9-.9l-1.2-1.8c-.3-.6-.3-1.2 0-1.5l.6-1.5.6-1.2.6-1.2.3-.6c.15-.3.45-.3.6 0l.6.9c.15.3.45.3.6 0l.6-.9.6-.9c.15-.3.45-.3.6 0l.6.9.6.9c.15.3.45.3.6 0l.6-.9c.15-.3.45-.3.6 0l.3.6.6 1.2.6 1.2.6 1.5c.3.3.3.9 0 1.5z"/></svg>'
    },
    'yandex': {
        name: 'Yandex Browser',
        icon: '<svg viewBox="0 0 24 24"><path fill="#FF0000" d="M12 0C5.37 0 0 5.37 0 12s5.37 12 12 12 12-5.37 12-12S18.63 0 12 0z"/><path fill="#FFF" d="M13.5 5h-2.1l-3.9 8.1V5H5.4v14h2.1l4.05-8.55V19h2.1V5z"/></svg>'
    },
    'vlc': {
        name: 'VLC',
        icon: '<svg viewBox="0 0 24 24"><path fill="#FF8800" d="M12 1.5L7.5 16h9L12 1.5z"/><path fill="#FF5722" d="M6 18.5c-1.5 0-2.5.5-2.5 1.5s2.5 2.5 8.5 2.5 8.5-1.5 8.5-2.5-1-1.5-2.5-1.5H6z"/><path fill="#FF8800" d="M6 18.5h12l-1.5-2.5h-9L6 18.5z"/></svg>'
    },
    'aimp': {
        name: 'AIMP',
        icon: '<svg viewBox="0 0 24 24"><path fill="#F7A600" d="M12 0C5.37 0 0 5.37 0 12s5.37 12 12 12 12-5.37 12-12S18.63 0 12 0z"/><path fill="#FFF" d="M12 4l-7 14h3l1.5-3h5l1.5 3h3L12 4zm0 5l1.75 3.5h-3.5L12 9z"/></svg>'
    },
    'foobar': {
        name: 'foobar2000',
        icon: '<svg viewBox="0 0 24 24"><rect fill="#1F1A17" width="24" height="24" rx="4"/><path fill="#D89B2B" d="M6 6h3v12H6V6zm4.5 0H13v12h-2.5V6zm4 0H17v12h-2.5V6z"/></svg>'
    },
    'music.ui': {
        name: 'Groove Music',
        icon: '<svg viewBox="0 0 24 24"><circle fill="#7B83EB" cx="12" cy="12" r="11"/><path fill="#FFF" d="M15 7v7a3 3 0 11-2-2.83V7h2z"/></svg>'
    },
    'itunes': {
        name: 'iTunes',
        icon: '<svg viewBox="0 0 24 24"><path fill="#EA4CC0" d="M12 0C5.37 0 0 5.37 0 12s5.37 12 12 12 12-5.37 12-12S18.63 0 12 0z"/><path fill="#FFF" d="M16.5 6.5l-7 1.75v7.25a2.5 2.5 0 11-1.5-2.29V9.5l7-1.75v4.75a2.5 2.5 0 11-1.5-2.29V6.5z" opacity=".9"/></svg>'
    },
    'apple music': {
        name: 'Apple Music',
        icon: '<svg viewBox="0 0 24 24"><path fill="#FC3C44" d="M12 0C5.37 0 0 5.37 0 12s5.37 12 12 12 12-5.37 12-12S18.63 0 12 0z"/><path fill="#FFF" d="M16.5 6.5l-7 1.75v7.25a2.5 2.5 0 11-1.5-2.29V9.5l7-1.75v4.75a2.5 2.5 0 11-1.5-2.29V6.5z" opacity=".9"/></svg>'
    },
    'deezer': {
        name: 'Deezer',
        icon: '<svg viewBox="0 0 24 24"><rect fill="#000" width="24" height="24" rx="4"/><g fill="#A238FF"><rect x="2" y="16" width="3" height="2" rx=".5"/><rect x="6.5" y="14" width="3" height="4" rx=".5"/><rect x="11" y="10" width="3" height="8" rx=".5"/><rect x="15.5" y="12" width="3" height="6" rx=".5"/><rect x="19" y="8" width="3" height="10" rx=".5"/></g></svg>'
    },
    'tidal': {
        name: 'TIDAL',
        icon: '<svg viewBox="0 0 24 24"><path fill="#000" d="M12 4.8L8 8.8l4 4-4 4-4-4 4-4-4-4 4-4 4 4zm4 0l4 4-4 4-4-4 4-4z"/></svg>'
    },
};

export function resolveMediaSource(raw) {
    if (!raw) return null;
    const lower = raw.toLowerCase();
    for (const [key, info] of Object.entries(MEDIA_SOURCES)) {
        if (lower.includes(key)) return info;
    }
    return { name: raw.replace(/\.exe$/i, ''), icon: null };
}

// Cached DOM references (populated once after DOMContentLoaded)
export const dom = {};
export function cacheDom() {
    dom.trackTitle = document.getElementById('track-title');
    dom.artist = document.getElementById('artist');
    dom.album = document.getElementById('album');
    dom.miniTrackTitle = document.getElementById('mini-track-title');
    dom.miniArtist = document.getElementById('mini-artist');
    dom.albumArt = document.getElementById('album-art');
    dom.albumArtGlow = document.getElementById('album-art-glow');
    dom.miniAlbumArt = document.getElementById('mini-album-art');
    dom.volumeSlider = document.getElementById('volume-slider');
    dom.volumeDisplay = document.getElementById('volume-display');
    dom.miniVolumeSlider = document.getElementById('mini-volume-slider');
    dom.miniVolumeDisplay = document.getElementById('mini-volume-display');
    dom.progressFill = document.getElementById('progress-fill');
    dom.currentTime = document.getElementById('current-time');
    dom.totalTime = document.getElementById('total-time');
    dom.metaElapsed = document.getElementById('meta-elapsed');
    dom.metaLength = document.getElementById('meta-length');
    dom.progressBar = document.getElementById('progress-bar');
    dom.miniProgressFill = document.getElementById('mini-progress-fill');
    dom.miniCurrentTime = document.getElementById('mini-current-time');
    dom.miniTotalTime = document.getElementById('mini-total-time');
    dom.playbackState = document.getElementById('playback-state');
    dom.stateIcon = document.getElementById('state-icon');
    dom.playPauseIcon = document.getElementById('play-pause-icon');
    dom.miniPlayPauseIcon = document.getElementById('mini-play-pause-icon');
    dom.muteIcon = document.getElementById('mute-icon');
    dom.miniMuteIcon = document.getElementById('mini-mute-icon');
    dom.statusDot = document.getElementById('status-dot');
    dom.source = document.getElementById('source');
    dom.sourceIcon = document.getElementById('sourceIcon');
    dom.btnPlayPause = document.getElementById('btn-play-pause');
    dom.btnNext = document.getElementById('btn-next');
    dom.btnPrevious = document.getElementById('btn-previous');
    dom.miniBtnPlayPause = document.getElementById('mini-btn-play-pause');
    dom.miniPlayer = document.getElementById('mini-player');
}

// Timing constants
export const VOLUME_THROTTLE_MS = 16;
// 250ms is plenty for sub-second progress; the inline updateProgress
// also short-circuits when the rounded second hasn't moved, so there's
// no visible difference for the user.
export const POSITION_INTERPOLATION_MS = 250;
export const SEARCH_DEBOUNCE_MS = 200;
export const TOAST_DURATION_MS = 3000;
export const WS_BACKOFF_BASE_MS = 3000;
export const WS_BACKOFF_MAX_MS = 30000;
export const WS_MAX_RECONNECT_ATTEMPTS = 20;
export const WS_PING_INTERVAL_MS = 30000;
export const VOLUME_RELEASE_DELAY_MS = 500;

// Shared state (accessed across multiple modules)
export let ws = null;
export function setWs(value) { ws = value; }
export function getWs() { return ws; }
export let currentState = 'idle';
export function setCurrentState(value) { currentState = value; }
export let currentDuration = 0;
export function setCurrentDuration(value) { currentDuration = value; }
export let currentPosition = 0;
export function setCurrentPosition(value) { currentPosition = value; }
export let isUserAdjustingVolume = false;
export function setIsUserAdjustingVolume(value) { isUserAdjustingVolume = value; }
export let volumeUpdateTimer = null;
export function setVolumeUpdateTimer(value) { volumeUpdateTimer = value; }
export let scripts = [];
export function setScripts(value) { scripts = value; }
export let lastStatus = null;
export function setLastStatus(value) { lastStatus = value; }
export let currentPlayState = 'idle';
export function setCurrentPlayState(value) { currentPlayState = value; }

// ============================================================
// Internationalization (i18n)
// ============================================================

let currentLocale = 'en';
let translations = {};
const supportedLocales = {
    'en': 'English',
    'ru': 'Русский'
};

// Minimal inline fallback for critical UI elements
const fallbackTranslations = {
    'app.title': 'Media Server',
    'auth.connect': 'Connect',
    'auth.placeholder': 'Enter API Token',
    'player.status.connected': 'Connected',
    'player.status.disconnected': 'Disconnected'
};

export function t(key, params = {}) {
    let text = translations[key] || fallbackTranslations[key] || key;
    Object.keys(params).forEach(param => {
        text = text.replace(new RegExp(`\\{${param}\\}`, 'g'), params[param]);
    });
    return text;
}

async function loadTranslations(locale) {
    try {
        const response = await fetch(`/static/locales/${locale}.json`);
        if (!response.ok) {
            throw new Error(`Failed to load ${locale}.json`);
        }
        return await response.json();
    } catch (error) {
        console.error(`Error loading translations for ${locale}:`, error);
        if (locale !== 'en') {
            return await loadTranslations('en');
        }
        return {};
    }
}

function detectBrowserLocale() {
    const browserLang = navigator.language || navigator.languages?.[0] || 'en';
    const langCode = browserLang.split('-')[0];
    return supportedLocales[langCode] ? langCode : 'en';
}

export async function initLocale() {
    const savedLocale = localStorage.getItem('locale') || detectBrowserLocale();
    await setLocale(savedLocale);
}

async function setLocale(locale) {
    if (!supportedLocales[locale]) {
        locale = 'en';
    }
    translations = await loadTranslations(locale);
    currentLocale = locale;
    document.documentElement.setAttribute('data-locale', locale);
    document.documentElement.setAttribute('lang', locale);
    localStorage.setItem('locale', locale);
    updateAllText();
    updateLocaleSelect();
    document.body.classList.remove('loading-translations');
    document.body.classList.add('translations-loaded');
}

export function changeLocale() {
    const select = document.getElementById('locale-select');
    const newLocale = select.value;
    if (newLocale && newLocale !== currentLocale) {
        localStorage.setItem('locale', newLocale);
        setLocale(newLocale);
    }
}

function updateLocaleSelect() {
    const select = document.getElementById('locale-select');
    if (select) {
        select.value = currentLocale;
    }
}

// Note: updateAllText calls functions from other modules via late-bound references.
// These are set from app.js after all modules are loaded.
let _updatePlaybackState = null;
let _updateConnectionStatus = null;
let _loadScriptsTable = null;
let _loadCallbacksTable = null;
let _loadLinksTable = null;
let _displayQuickAccess = null;
let _renderAccentSwatches = null;

export function registerUpdateCallbacks(callbacks) {
    _updatePlaybackState = callbacks.updatePlaybackState;
    _updateConnectionStatus = callbacks.updateConnectionStatus;
    _loadScriptsTable = callbacks.loadScriptsTable;
    _loadCallbacksTable = callbacks.loadCallbacksTable;
    _loadLinksTable = callbacks.loadLinksTable;
    _displayQuickAccess = callbacks.displayQuickAccess;
    _renderAccentSwatches = callbacks.renderAccentSwatches;
}

function updateAllText() {
    document.querySelectorAll('[data-i18n]').forEach(el => {
        const key = el.getAttribute('data-i18n');
        el.textContent = t(key);
    });
    document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
        const key = el.getAttribute('data-i18n-placeholder');
        el.placeholder = t(key);
    });
    document.querySelectorAll('[data-i18n-title]').forEach(el => {
        const key = el.getAttribute('data-i18n-title');
        el.title = t(key);
    });

    // Re-apply dynamic content with new translations
    if (_updatePlaybackState) _updatePlaybackState(currentState);
    const connected = ws && ws.readyState === WebSocket.OPEN;
    if (_updateConnectionStatus) _updateConnectionStatus(connected);

    if (lastStatus) {
        const fallbackTitle = lastStatus.state === 'idle' ? t('player.no_media') : t('player.title_unavailable');
        document.getElementById('track-title').textContent = lastStatus.title || fallbackTitle;
        const initSrc = resolveMediaSource(lastStatus.source);
        document.getElementById('source').textContent = initSrc ? initSrc.name : t('player.unknown_source');
        document.getElementById('sourceIcon').innerHTML = initSrc?.icon || '';
    }

    if (hasCredentials()) {
        if (_loadScriptsTable) _loadScriptsTable();
        if (_loadCallbacksTable) _loadCallbacksTable();
        if (_loadLinksTable) _loadLinksTable();
        if (_displayQuickAccess) _displayQuickAccess();
    }
    if (_renderAccentSwatches) _renderAccentSwatches();
}

export async function fetchVersion() {
    try {
        const response = await fetch('/api/health');
        if (response.ok) {
            const data = await response.json();
            const label = document.getElementById('version-label');
            if (data.version) {
                label.textContent = `v${data.version}`;
                const folioVersion = document.getElementById('folio-version');
                if (folioVersion) folioVersion.textContent = `v${data.version}`;
            }
            if (data.update_available) {
                showUpdateBanner(data.update_available);
            }
        }
    } catch (error) {
        console.error('Error fetching version:', error);
    }
}

export function showUpdateBanner(update) {
    const dismissed = sessionStorage.getItem('update_dismissed');
    if (dismissed === update.latest) return;

    const banner = document.getElementById('updateBanner');
    const text = document.getElementById('updateBannerText');
    const link = document.getElementById('updateBannerLink');
    const closeBtn = document.getElementById('updateBannerClose');

    text.textContent = t('update.available', { version: update.latest });
    link.href = update.url;
    link.textContent = t('update.view_release');
    banner.classList.remove('hidden');

    closeBtn.onclick = () => {
        banner.classList.add('hidden');
        sessionStorage.setItem('update_dismissed', update.latest);
    };
}

// ============================================================
// Shared Utilities
// ============================================================

export function formatTime(seconds) {
    if (!seconds || seconds < 0) return '0:00';
    const mins = Math.floor(seconds / 60);
    const secs = Math.floor(seconds % 60);
    return `${mins}:${secs.toString().padStart(2, '0')}`;
}

export function escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}

export function showToast(message, type = 'success') {
    const container = document.getElementById('toast-container');
    const toast = document.createElement('div');
    toast.className = `toast ${type}`;
    toast.textContent = message;
    container.appendChild(toast);

    requestAnimationFrame(() => {
        toast.classList.add('show');
    });

    setTimeout(() => {
        toast.classList.remove('show');
        toast.addEventListener('transitionend', () => toast.remove(), { once: true });
        setTimeout(() => { if (toast.parentNode) toast.remove(); }, 500);
    }, TOAST_DURATION_MS);
}

export function closeDialog(dialog) {
    dialog.classList.add('dialog-closing');
    dialog.addEventListener('animationend', () => {
        dialog.classList.remove('dialog-closing');
        dialog.close();
    }, { once: true });
}

export function showAboutDialog() {
    const dialog = document.getElementById('aboutDialog');
    if (dialog) dialog.showModal();
}

export function closeAboutDialog() {
    const dialog = document.getElementById('aboutDialog');
    if (dialog) closeDialog(dialog);
}

export function showConfirm(message) {
    return new Promise((resolve) => {
        const dialog = document.getElementById('confirmDialog');
        const msg = document.getElementById('confirmDialogMessage');
        const btnCancel = document.getElementById('confirmDialogCancel');
        const btnConfirm = document.getElementById('confirmDialogConfirm');

        msg.textContent = message;

        function cleanup() {
            btnCancel.removeEventListener('click', onCancel);
            btnConfirm.removeEventListener('click', onConfirm);
            dialog.removeEventListener('close', onClose);
            closeDialog(dialog);
        }

        function onCancel() { cleanup(); resolve(false); }
        function onConfirm() { cleanup(); resolve(true); }
        function onClose() { cleanup(); resolve(false); }

        btnCancel.addEventListener('click', onCancel);
        btnConfirm.addEventListener('click', onConfirm);
        dialog.addEventListener('close', onClose);

        dialog.showModal();
    });
}

// ============================================================
// Auth Helpers
// ============================================================

// Set to false when server reports auth_required: false
export let authRequired = true;
export function setAuthRequired(value) { authRequired = value; }

/**
 * Build Authorization headers for API requests.
 * Returns empty object when auth is disabled or no token is stored.
 */
export function getAuthHeaders() {
    const token = localStorage.getItem('media_server_token');
    return token ? { 'Authorization': `Bearer ${token}` } : {};
}

/**
 * Check if we have sufficient credentials to call the API.
 * True when auth is disabled OR a token is stored.
 */
export function hasCredentials() {
    return !authRequired || !!localStorage.getItem('media_server_token');
}

// ============================================================
// API Commands
// ============================================================

export async function sendCommand(endpoint, body = null) {
    const options = {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
    };

    if (body) {
        options.body = JSON.stringify(body);
    }

    try {
        const response = await fetch(`/api/media/${endpoint}`, options);
        if (!response.ok) {
            const data = await response.json().catch(() => ({}));
            console.error(`Command ${endpoint} failed:`, response.status);
            showToast(data.detail || `Command failed: ${endpoint}`, 'error');
        }
    } catch (error) {
        console.error(`Error sending command ${endpoint}:`, error);
        showToast(`Connection error: ${endpoint}`, 'error');
    }
}

export function togglePlayPause() {
    if (currentState === 'playing') {
        sendCommand('pause');
    } else {
        sendCommand('play');
    }
}

export function nextTrack() {
    sendCommand('next');
}

export function previousTrack() {
    sendCommand('previous');
}

let lastSentVolume = -1;
export function setVolume(volume) {
    if (volume === lastSentVolume) return;
    lastSentVolume = volume;
    if (ws && ws.readyState === WebSocket.OPEN) {
        ws.send(JSON.stringify({ type: 'volume', volume: volume }));
    } else {
        sendCommand('volume', { volume: volume });
    }
}
// Reset the de-dupe cache whenever the server reports a fresh volume value
// (e.g., another client moved the slider). Otherwise the user can end up
// unable to "set volume back to the value we last sent" after a remote change.
export function notifyRemoteVolume(volume) {
    lastSentVolume = volume;
}

export function toggleMute() {
    sendCommand('mute');
}

export function seek(position) {
    sendCommand('seek', { position: position });
}

// ============================================================
// MDI Icon System
// ============================================================

const mdiIconCache = (() => {
    try {
        return JSON.parse(localStorage.getItem('mdiIconCache') || '{}');
    } catch { return {}; }
})();

function _persistMdiCache() {
    try { localStorage.setItem('mdiIconCache', JSON.stringify(mdiIconCache)); } catch {}
}

// Strict iconify MDI slug — used to reject anything that could be path-traversal
// or query injection before we even hit the network.
const MDI_SLUG_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;

function sanitizeSvg(rawSvg) {
    // Parse the SVG and strip anything that could execute script. Anything
    // unparseable returns null so callers fall back to the placeholder.
    try {
        const doc = new DOMParser().parseFromString(rawSvg, 'image/svg+xml');
        const root = doc.documentElement;
        if (!root || root.tagName.toLowerCase() !== 'svg' || root.querySelector('parsererror')) {
            return null;
        }
        const walker = doc.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
        const toRemove = [];
        let node = walker.currentNode;
        while (node) {
            const tag = node.tagName?.toLowerCase();
            if (tag === 'script' || tag === 'foreignobject') {
                toRemove.push(node);
            } else if (node.attributes) {
                for (const attr of Array.from(node.attributes)) {
                    const name = attr.name.toLowerCase();
                    if (name.startsWith('on') ||
                        ((name === 'href' || name === 'xlink:href') &&
                            /^\s*(javascript|data):/i.test(attr.value))) {
                        node.removeAttribute(attr.name);
                    }
                }
            }
            node = walker.nextNode();
        }
        toRemove.forEach((el) => el.remove());
        return root.outerHTML;
    } catch {
        return null;
    }
}

const PLACEHOLDER_SVG = '<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"/></svg>';

export async function fetchMdiIcon(iconName) {
    const name = String(iconName || '').replace(/^mdi:/, '');
    if (!MDI_SLUG_RE.test(name)) return PLACEHOLDER_SVG;
    if (mdiIconCache[name]) return mdiIconCache[name];

    try {
        const response = await fetch(`https://api.iconify.design/mdi/${encodeURIComponent(name)}.svg?width=16&height=16`);
        if (response.ok) {
            const raw = await response.text();
            const safe = sanitizeSvg(raw);
            if (safe) {
                mdiIconCache[name] = safe;
                _persistMdiCache();
                return safe;
            }
        }
    } catch (e) {
        console.warn('Failed to fetch MDI icon:', name, e);
    }

    return PLACEHOLDER_SVG;
}

export async function resolveMdiIcons(container) {
    const els = container.querySelectorAll('[data-mdi-icon]');
    await Promise.all(Array.from(els).map(async (el) => {
        const icon = el.dataset.mdiIcon;
        if (icon) {
            el.innerHTML = await fetchMdiIcon(icon);
        }
    }));
}

export function setupIconPreview(inputId, previewId) {
    const input = document.getElementById(inputId);
    const preview = document.getElementById(previewId);
    if (!input || !preview) return;

    let debounceTimer = null;

    input.addEventListener('input', () => {
        clearTimeout(debounceTimer);
        const value = input.value.trim();

        if (!value) {
            preview.innerHTML = '';
            return;
        }

        debounceTimer = setTimeout(async () => {
            const svg = await fetchMdiIcon(value);
            if (input.value.trim() === value) {
                preview.innerHTML = svg;
            }
        }, 400);
    });
}
