// ── Settings ──────────────────────────────────────────────────────────────

const SETTINGS_SEEN_KEY = 'vcf-settings-seen';
let _appSettings = {};
let _ttsBackends = [];

function availableTtsBackends() {
  return (_ttsBackends || []).filter(b => b.available);
}

function ttsBackendOptions(selected = '') {
  const backends = availableTtsBackends();
  if (!backends.length) return '<option value="">No TTS backend available</option>';
  const current = selected || backends[0].id;
  return backends.map(b => `<option value="${escHtml(b.id)}" ${b.id === current ? 'selected' : ''}>${escHtml(b.label)}</option>`).join('');
}

function styleBackendOptions(selected = 'customvoice', preferStyleAware = false) {
  const backends = availableTtsBackends();
  if (!backends.length) return '<option value="">No backend available</option>';
  const preferred = backends.some(b => b.id === selected) ? selected
    : preferStyleAware
      ? (backends.find(b => b.style_aware)?.id || backends[0].id)
      : backends[0].id;
  return backends.map(b => `<option value="${escHtml(b.id)}" ${b.id === preferred ? 'selected' : ''}>${escHtml(b.label)}</option>`).join('');
}

function backendById(id) {
  return availableTtsBackends().find(b => b.id === id) || availableTtsBackends()[0] || null;
}

function backendComputeDevice(id) {
  const b = backendById(id);
  const text = [id, b?.label, b?.speed, b?.latency, b?.quality, b?.ram, b?.purpose, b?.best_for]
    .filter(Boolean).join(' ').toLowerCase();
  if (/\b(cloud|api|elevenlabs|groq)\b/.test(text)) return 'Cloud';
  if (/\b(cpu|metal)\b/.test(text) && !/\b(cuda|gpu|vram|rtx|dgx)\b/.test(text)) return 'CPU';
  if (/\b(cuda|gpu|vram|rtx|dgx)\b/.test(text)) return 'CUDA/GPU';
  return 'Unknown';
}

function backendComputeDeviceClass(id) {
  const label = backendComputeDevice(id).toLowerCase();
  if (label.includes('cuda') || label.includes('gpu')) return 'gpu';
  if (label.includes('cpu')) return 'cpu';
  if (label.includes('cloud')) return 'cloud';
  return '';
}

function backendHelpHtml(b, compact = false) {
  if (!b) return '<strong>No TTS backend is reachable.</strong><div>Start at least one TTS service or check Settings URLs.</div>';
  const tags = [
    b.uses_wav ? ['good', 'uses WAV identity'] : ['warn', 'prompt/model voice'],
    b.style_aware ? ['good', 'style-aware'] : ['warn', 'weak style'],
    b.true_streaming ? ['good', 'true streaming'] : ['', 'buffered/normal'],
  ].map(([cls, text]) => `<span class="backend-tag ${cls}">${escHtml(text)}</span>`).join('');
  const metricParts = [];
  if (b.speed)   metricParts.push(`<span class="backend-metric-tag"><span class="mdi mdi-lightning-bolt"></span> ${escHtml(b.speed)}</span>`);
  if (b.latency) metricParts.push(`<span class="backend-metric-tag"><span class="mdi mdi-clock-outline"></span> ${escHtml(b.latency)}</span>`);
  if (b.quality) metricParts.push(`<span class="backend-metric-tag"><span class="mdi mdi-star-circle-outline"></span> ${escHtml(b.quality)}</span>`);
  if (b.ram)     metricParts.push(`<span class="backend-metric-tag"><span class="mdi mdi-memory"></span> ${escHtml(b.ram)}</span>`);
  const metrics = metricParts.length ? `<div class="backend-metrics-row">${metricParts.join('')}</div>` : '';
  const detail = compact ? escHtml(b.best_for || '') : `${escHtml(b.purpose || '')}<br><strong>Identity:</strong> ${escHtml(b.identity || '')}<br><strong>Style:</strong> ${escHtml(b.style || '')}<br><strong>Best for:</strong> ${escHtml(b.best_for || '')}`;
  return `<strong>${escHtml(b.label)}</strong><div class="backend-help-tags">${tags}</div>${metrics}<div>${detail}</div>`;
}

function sttBackendHelpHtml(b) {
  if (!b) return 'No STT engine selected.';
  const m = b.metrics || {};
  const metricParts = [];
  if (m.speed)   metricParts.push(`<span class="backend-metric-tag"><span class="mdi mdi-lightning-bolt"></span> ${escHtml(m.speed)}</span>`);
  if (m.latency) metricParts.push(`<span class="backend-metric-tag"><span class="mdi mdi-clock-outline"></span> ${escHtml(m.latency)}</span>`);
  if (m.quality) metricParts.push(`<span class="backend-metric-tag"><span class="mdi mdi-star-circle-outline"></span> ${escHtml(m.quality)}</span>`);
  if (m.ram)     metricParts.push(`<span class="backend-metric-tag"><span class="mdi mdi-memory"></span> ${escHtml(m.ram)}</span>`);
  const metrics = metricParts.length ? `<div class="backend-metrics-row">${metricParts.join('')}</div>` : '';
  const modelList = Array.isArray(b.models) && b.models.length ? ' Models: ' + b.models.slice(0, 4).join(', ') + '.' : '';
  const avail = b.available ? `<span class="backend-tag good">ready</span>` : `<span class="backend-tag warn">unavailable</span>`;
  return `<strong>${escHtml(b.label)}</strong> ${avail}${metrics}<div style="margin-top:4px;font-size:0.85em;opacity:.8">${escHtml(b.url)}${escHtml(modelList)}</div>`;
}

function updateBackendHelp() {
  const b = backendById($('tts-backend-select')?.value || '');
  const help = $('tts-backend-help');
  if (help) help.innerHTML = backendHelpHtml(b);
  // Dynamic style-support badge + warning in Try It Out
  const styleSupport = $('preview-style-support');
  const styleWarn = $('preview-style-warn');
  const styleInput = $('preview-style-instruction');
  if (b && styleSupport) {
    if (b.style_aware) {
      styleSupport.textContent = 'style-aware ✓';
      styleSupport.style.cssText = 'font-size:11px;display:inline-block;background:rgba(166,227,161,.2);color:var(--green);border-radius:4px;padding:1px 6px;margin-left:4px';
    } else {
      styleSupport.textContent = 'weak style';
      styleSupport.style.cssText = 'font-size:11px;display:inline-block;background:rgba(249,226,175,.2);color:var(--yellow);border-radius:4px;padding:1px 6px;margin-left:4px';
    }
  }
  if (styleWarn) {
    const hasInstruct = (styleInput?.value || '').trim().length > 0;
    styleWarn.style.display = (b && !b.style_aware && hasInstruct) ? 'block' : 'none';
  }
  const sttB = backendById($('stt-tts-backend-select')?.value || '');
  const sttHelp = $('stt-tts-backend-help');
  if (sttHelp) sttHelp.innerHTML = backendHelpHtml(sttB);
}

function updateStyleBackendHelp(scope = document) {
  scope.querySelectorAll('.opt-style-backend').forEach(sel => {
    const box = sel.closest('.opt-style-panel')?.querySelector('.opt-style-backend-help');
    if (!box) return;
    const b = backendById(sel.value);
    box.innerHTML = backendHelpHtml(b, true);
    if (b && !b.style_aware) {
      const styleAwareBacks = availableTtsBackends().filter(x => x.style_aware);
      const suggestion = styleAwareBacks.length
        ? ` Try <strong>${escHtml(styleAwareBacks[0].label)}</strong> instead.`
        : ' No style-aware backend is currently reachable.';
      box.innerHTML += `<div class="style-backend-warn"><span class="mdi mdi-alert-outline"></span> This backend ignores the style instruction — output will sound the same regardless of what you type.${suggestion}</div>`;
    }
  });
}

function updateBackendDependentTabs() {
  const availableBackends = availableTtsBackends();
  const available = new Set(availableBackends.map(b => b.id));
  document.querySelectorAll('.tab[data-backend-required]').forEach(tab => {
    const originalSubtitle = tab.dataset.originalSubtitle || tab.querySelector('.tab-subtitle')?.textContent || '';
    const originalTooltip = tab.dataset.originalTooltip || tab.querySelector('.tab-tooltip')?.textContent || '';
    tab.dataset.originalSubtitle = originalSubtitle;
    tab.dataset.originalTooltip = originalTooltip;
    const required = tab.dataset.backendRequired;
    const ok = required === 'any_tts' ? availableBackends.length > 0 : available.has(required);
    tab.hidden = false;
    tab.classList.toggle('backend-unavailable', !ok);
    tab.setAttribute('aria-disabled', ok ? 'false' : 'true');
    tab.tabIndex = ok ? 0 : -1;
    const subtitle = tab.querySelector('.tab-subtitle');
    const tooltip = tab.querySelector('.tab-tooltip');
    if (subtitle) subtitle.textContent = ok ? originalSubtitle : 'not running/configured';
    if (tooltip) tooltip.textContent = ok ? originalTooltip : `${originalTooltip}\n\n${disabledBackendTabMessage(tab)}`;
  });
  const active = document.querySelector('.tab.active');
  if (!active || active.classList.contains('backend-unavailable')) {
    const first = document.querySelector('.tab:not(.backend-unavailable)');
    if (first) switchTab(first.dataset.tab);
  }
}

async function refreshTtsBackendAvailability(selected = '') {
  try {
    const d = await fetch('/api/tts-backends').then(r => r.json());
    _ttsBackends = (d.backends || []).filter(b => b && b.id);
  } catch (_) {
    _ttsBackends = [];
  }
  const preview = $('tts-backend-select');
  if (preview) {
    const prev = selected || preview.value;
    preview.innerHTML = ttsBackendOptions(prev);
    preview.disabled = !availableTtsBackends().length;
  }
  const sttTtsBackend = $('stt-tts-backend-select');
  if (sttTtsBackend) {
    const prev = selected || sttTtsBackend.value;
    sttTtsBackend.innerHTML = ttsBackendOptions(prev);
    sttTtsBackend.disabled = !availableTtsBackends().length;
  }
  const libraryTts = $('library-tts-backend-select');
  if (libraryTts) {
    const prev = libraryTts.value || 'voice_clone';
    libraryTts.innerHTML = ttsBackendOptions(prev);
    libraryTts.disabled = !availableTtsBackends().length;
  }
  const readerBackend = $('reader-backend-select');
  if (readerBackend) {
    const prev = selected || readerBackend.value;
    readerBackend.innerHTML = ttsBackendOptions(prev);
    readerBackend.disabled = !availableTtsBackends().length;
    if (typeof readerUpdateBackendHint === 'function') readerUpdateBackendHint();
  }
  document.querySelectorAll('.opt-style-backend').forEach(sel => {
    const prev = sel.value;
    sel.innerHTML = styleBackendOptions(prev);
    sel.disabled = !availableTtsBackends().length;
  });
  document.querySelectorAll('.opt-compare-backend').forEach(sel => {
    const prev = sel.value && sel.value !== '' ? sel.value : 'voice_clone';
    sel.innerHTML = styleBackendOptions(prev);
    sel.disabled = !availableTtsBackends().length;
  });
  const perfSel = $('perf-backend-select');
  if (perfSel) {
    const prev = perfSel.value;
    perfSel.innerHTML = ttsBackendOptions(prev);
    perfSel.disabled = !availableTtsBackends().length;
  }
  const batchSel = $('batch-backend-select');
  if (batchSel) {
    const prev = batchSel.value;
    batchSel.innerHTML = ttsBackendOptions(prev);
    batchSel.disabled = !availableTtsBackends().length;
  }
  updateBackendHelp();
  updateStyleBackendHelp();
  updateBackendDependentTabs();
  // Call any post-refresh hooks registered by sub-sections (e.g. conversation panel)
  (window._ttsRefreshHooks || []).forEach(fn => { try { fn(); } catch(_) {} });
  return _ttsBackends;
}

async function _patchSettings(patch) {
  try {
    await fetch('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(patch) });
  } catch (e) { console.warn('[settings] patch failed', e); }
}

let _engineUrlSaveTimer = null;
function _saveEngineLocalUrls() {
  clearTimeout(_engineUrlSaveTimer);
  _engineUrlSaveTimer = setTimeout(() => {
    const urls = {};
    // Static cards (data-llm-local-key)
    document.querySelectorAll('[data-llm-local-key]').forEach(i => { if (i.value) urls[i.dataset.llmLocalKey] = i.value; });
    // Dynamic Docker stack card URL overrides (data-dc-url-key)
    document.querySelectorAll('.dc-url-inp[data-dc-url-key]').forEach(i => {
      const v = i.value.trim();
      if (v && v !== i.placeholder) urls['dc-' + i.dataset.dcUrlKey] = v;
    });
    _patchSettings({ engine_local_urls: urls });
    if (_appSettings) _appSettings.engine_local_urls = urls;
  }, 800);
}

let _containerNameSaveTimer = null;
function _saveEngineContainerNames() {
  clearTimeout(_containerNameSaveTimer);
  _containerNameSaveTimer = setTimeout(() => {
    const names = {};
    document.querySelectorAll('[data-cn-key]').forEach(inp => {
      const v = inp.value.trim();
      if (v) names[inp.dataset.cnKey] = v;
    });
    _patchSettings({ engine_container_names: names });
    if (_appSettings) _appSettings.engine_container_names = names;
  }, 800);
}
window._saveEngineLocalUrls      = _saveEngineLocalUrls;
window._saveEngineContainerNames = _saveEngineContainerNames;


// ── Settings guidance / tooltips ─────────────────────────────────────────
const SETTINGS_PAGE_GUIDES = {
  general: {
    icon: 'mdi-palette-outline',
    title: 'Start here',
    text: 'Choose the theme you prefer. Everything else can stay as-is until you connect or change an engine.'
  },
  connections: {
    icon: 'mdi-lan-connect',
    title: 'Connect engines first',
    text: 'Most users only need the Docker Engines page: click Connect, then Use as TTS or Use as STT. Edit URLs here only when a service moved or runs on another host.',
    steps: ['Use host.docker.internal for containers on the same Docker host.', 'Use 192.168.x.x when calling another machine on your LAN.', 'After changing URLs, Save settings, then refresh the target page.']
  },
  playback: {
    icon: 'mdi-play-circle-outline',
    title: 'Recommended playback',
    text: 'Auto is the safest default: it streams when the backend supports it and falls back to normal WAV playback when saving or compatibility matters.'
  },
  captures: {
    icon: 'mdi-record-circle-outline',
    title: 'Voice capture defaults',
    text: 'These choices affect microphone transcription and STT-to-TTS workflows. Pick a language only if auto-detect makes mistakes often.'
  },
  payloads: {
    icon: 'mdi-code-json',
    title: 'Advanced tuning',
    text: 'Leave these as defaults unless a backend needs special parameters. Invalid JSON prevents settings from saving, so change one box at a time.'
  },
  storage: {
    icon: 'mdi-folder-outline',
    title: 'Match your volume mounts',
    text: 'These are container paths, not host paths. In your Portainer stack, /voices should point to your real voice folder.'
  },
  apikeys: {
    icon: 'mdi-key-outline',
    title: 'Local-first defaults',
    text: 'Local containers usually do not need real keys. Use sk-local only when a compatible server insists on an Authorization header.'
  },
  backup: {
    icon: 'mdi-backup-restore',
    title: 'Back up before big changes',
    text: 'Export voices before moving folders, changing stacks, or testing new voice libraries. Keys are intentionally left out.'
  },
  logs: {
    icon: 'mdi-text-box-outline',
    title: 'Use logs when something feels stuck',
    text: 'Refresh after a failed request. Error rows usually say which URL, model, or payload needs attention.'
  },
  about: {
    icon: 'mdi-information-outline',
    title: 'Version and support info',
    text: 'Use this page to confirm the running app version and open release notes when behavior changed after an update.'
  }
};

const SETTINGS_FIELD_HELP = {
  's-theme-select': ['Theme', 'Changes only your browser UI. It is saved immediately and does not affect generated audio.'],
  's-tts-url': ['Voice Clone / Base URL', 'Use this for normal cloned WAV voices. Your 8020 Qwen3 voice clone container usually belongs here.'],
  's-voice-design-url': ['Voice Design URL', 'Use this for creating prompt-designed voices. Your 8021 Qwen3 Voice Design container usually belongs here.'],
  's-customvoice-url': ['CustomVoice URL', 'Use this for Qwen3 CustomVoice speaker/style presets. Your 8022 container usually belongs here.'],
  's-tts-stream-url': ['Streaming URL', 'Use this for lower-latency playback. Your 8023 streaming container usually belongs here.'],
  's-kokoro-url': ['Kokoro URL', 'Small, fast OpenAI-compatible TTS. Good as a lightweight fallback, but it does not clone your WAV identities.'],
  's-vibevoice-url': ['VibeVoice URL', 'Simple local TTS service. Good for experiments; not every Voice Creator feature maps to it.'],
  's-xtts-url': ['XTTS v2 URL', 'Use only if an XTTS API server is running. It can clone from short references but has a different voice model than Qwen3.'],
  's-nvidia-router-url': ['NVIDIA router URL', 'Router endpoint that can expose NVIDIA speech services through one URL. Handy when Parakeet and Magpie share a gateway.'],
  's-nvidia-tts-url': ['NVIDIA Magpie TTS URL', 'Direct Magpie TTS endpoint with fixed speakers. Good quality, but not your cloned WAV voice library.'],
  's-nvidia-asr-url': ['NVIDIA Parakeet ASR URL', 'Direct speech-to-text endpoint. Use this for fast local transcription when Parakeet is running.'],
  's-nvidia-zeroshot-url': ['NVIDIA Zeroshot NIM URL', 'Experimental clone endpoint that uses an audio prompt. Leave empty unless that NIM is running.'],
  's-nvidia-flow-url': ['NVIDIA Flow NIM URL', 'Experimental clone endpoint that uses an audio prompt plus transcript. Leave empty unless that NIM is running.'],
  's-whisper-url': ['Active STT URL', 'Main transcription URL used by the app. The quick buttons below copy known engine URLs into this field.'],
  's-faster-whisper-url': ['faster-whisper URL', 'Fast local Whisper endpoint, often the best general-purpose STT choice when GPU acceleration is available.'],
  's-whisper-cpp-url': ['whisper.cpp URL', 'Lightweight Whisper server for CPU or small CUDA setups. Useful fallback when heavier STT is offline.'],
  's-groq-api-key': ['Groq API key', 'Only needed for Groq cloud STT or LLM. Keep empty if you use only local services.'],
  's-tts-stream-mode': ['Playback mode', 'Auto is recommended. Streaming feels faster; buffered is more compatible and better for saving files.'],
  's-tts-backend': ['Request style', 'Choose the payload format expected by the backend connected to the normal TTS URL. Qwen3/OpenAI is the usual local stack default.'],
  's-stt-language': ['Default language', 'Auto-detect is usually fine. Set this when transcription keeps choosing the wrong language.'],
  's-stt-preferred-backend': ['Preferred STT backend', 'Overrides the active STT URL only for capture workflows. Leave default if you want one global STT setting.'],
  's-llm-url': ['LLM base URL', 'OpenAI-compatible chat endpoint used for rewrites, casting, refinement, and voice-design assistance.'],
  's-auto-refine': ['Auto-refine', 'When enabled, the app sends transcripts to the LLM for cleanup automatically. Leave off if you want raw transcripts.'],
  's-refine-model': ['Refinement model', 'Optional model override for transcript cleanup. Leave empty to use the current LLM default.'],
  's-audiobook-prompt': ['Audiobook prompt', 'Custom LLM instructions for character detection.'],
  's-captures-default-voice': ['Default playback voice', 'Preselects a TTS voice in capture workflows so you do not have to choose it every time.'],
  's-tts-extra-voice-clone': ['Voice Clone params', 'Advanced JSON sent to the 8020 backend. Temperature/top_p/seed are common controls.'],
  's-tts-extra-streaming': ['Streaming params', 'Advanced JSON sent to the streaming backend. Keep close to Voice Clone params for comparable sound.'],
  's-tts-extra-customvoice': ['CustomVoice params', 'Advanced JSON sent to CustomVoice. Use only backend-supported fields.'],
  's-tts-extra-voice-design': ['Voice Design params', 'Advanced JSON sent when generating prompt-designed voices.'],
  's-tts-extra-nvidia-magpie': ['Magpie params', 'Usually empty. Magpie uses fixed speakers and may reject unknown fields.'],
  's-tts-extra-nvidia-zeroshot': ['Zeroshot params', 'Optional multipart fields for the clone NIM. The app already sends text, language, and audio prompt.'],
  's-tts-extra-nvidia-flow': ['Flow params', 'Optional multipart fields for Flow. The app also sends reference transcript when available.'],
  's-tts-extra-kokoro': ['Kokoro params', 'Usually empty. The selected Kokoro voice carries most of the useful information.'],
  's-tts-extra-vibevoice': ['VibeVoice params', 'Usually empty. VibeVoice commonly only needs text.'],
  's-voices-scan-dir': ['Voice scan directory', 'Container path that contains active_voices and hidden_voices. Usually /voices.'],
  's-output-dir': ['Active voices directory', 'Container path where new cloned or exported voices are saved. Usually /voices/active_voices.'],
  's-tts-key': ['TTS API key', 'Optional. For local OpenAI-compatible servers, sk-local or empty usually works.'],
  's-vd-key': ['Voice Design API key', 'Optional. Use only if your Voice Design backend requires Authorization.'],
  's-whisper-key': ['Whisper API key', 'Optional. Needed for cloud STT, usually empty for local Whisper-compatible containers.'],
  's-seed-finder-text': ['Seed Finder text', 'Custom test sentence used when opening the Seed Finder. Leave empty to use language defaults.'],
  's-seed-finder-dir': ['Seed Finder Sample Files', 'Container path for seed finder sample storage.'],
  's-pt-dir': ['PT Files', 'Container path for .pt embedding storage.']
};


function enhanceSettingsHelp(root = document) {
  const scope = root && root.querySelectorAll ? root : document;
  scope.querySelectorAll('.s-settings-page').forEach(page => {
    const key = page.dataset.page;
    const guide = SETTINGS_PAGE_GUIDES[key];
    const card = page.querySelector('.card');
    const head = card?.querySelector('.s-page-head');
    if (!guide || !card || !head || card.querySelector('.settings-guide')) return;
    const box = document.createElement('div');
    box.className = 'settings-guide';
    box.innerHTML = `
      <div class="settings-guide-icon"><span class="mdi ${escHtml(guide.icon)}"></span></div>
      <div class="settings-guide-body">
        <strong>${escHtml(guide.title)}</strong>
        <p>${escHtml(guide.text)}</p>
        ${guide.steps ? `<ul>${guide.steps.map(s => `<li>${escHtml(s)}</li>`).join('')}</ul>` : ''}
      </div>`;
    head.insertAdjacentElement('afterend', box);
  });

  Object.entries(SETTINGS_FIELD_HELP).forEach(([id, help]) => {
    const control = scope.getElementById ? scope.getElementById(id) : document.getElementById(id);
    if (!control) return;
    const field = control.closest('.s-field');
    const label = field?.querySelector('label');
    if (!field || !label) return;
    const [title, detail] = help;
    if (!label.querySelector('.s-help-tip')) {
      const tipId = `${id}-tip`;
      const wrap = document.createElement('span');
      wrap.className = 's-help-tip';
      wrap.innerHTML = `<button type="button" aria-label="Help for ${escHtml(title)}" aria-describedby="${escHtml(tipId)}"><span class="mdi mdi-information-outline" aria-hidden="true"></span></button><span class="s-tooltip" id="${escHtml(tipId)}" role="tooltip">${escHtml(detail)}</span>`;
      label.appendChild(wrap);
    }
    if (!field.querySelector('.s-field-tip')) {
      const tip = document.createElement('span');
      tip.className = 's-field-tip';
      tip.textContent = detail;
      const hint = field.querySelector('.s-hint');
      if (hint) hint.insertAdjacentElement('afterend', tip);
      else field.appendChild(tip);
    }
    if (!control.getAttribute('aria-label')) control.setAttribute('aria-label', title);
    if (!control.getAttribute('title')) control.setAttribute('title', detail);
  });
}

async function loadSettings() {
  const s = await fetch('/api/settings').then(r => r.json());
  const sv = (id, val) => { const el = $(id); if (el) el.value = val; };
  sv('s-whisper-url',        s.whisper_url          || '');
  sv('s-whisper-key',        s.whisper_api_key      || '');
  sv('s-tts-url',            s.tts_url              || '');
  sv('s-faster-whisper-url', s.faster_whisper_url   || '');
  sv('s-whisper-cpp-url',    s.whisper_cpp_url      || '');
  sv('s-groq-api-key',       s.groq_api_key         || '');
  sv('s-kokoro-url',         s.kokoro_url           || '');
  sv('s-vibevoice-url',      s.vibevoice_url        || '');
  sv('s-xtts-url',           s.xtts_url             || '');
  sv('s-llm-url',            s.llm_url              || '');
  _appSettings = s;

  // Restore engine URL inputs from server (overrides localStorage fallback)
  const savedEngineUrls = s.engine_local_urls || {};
  document.querySelectorAll('[data-llm-local-key]').forEach(inp => {
    const v = savedEngineUrls[inp.dataset.llmLocalKey];
    if (v) inp.value = v;
  });
  document.querySelectorAll('.dc-url-inp[data-dc-url-key]').forEach(inp => {
    const v = savedEngineUrls['dc-' + inp.dataset.dcUrlKey];
    if (v) inp.value = v;
  });

  // Restore container names from server — uses data-cn-key tags set by engines.js / ai-backends.js
  const savedContainerNames = s.engine_container_names || {};
  document.querySelectorAll('[data-cn-key]').forEach(inp => {
    const name = savedContainerNames[inp.dataset.cnKey];
    if (name && !inp.value) {
      inp.value = name;
      inp.dispatchEvent(new Event('input', { bubbles: true }));
    }
  });

  // Restore LLM URLs for refinement and conversation panels
  const refineInp = $('refine-llm-url');
  if (refineInp && s.refine_llm_url) refineInp.value = s.refine_llm_url;
  const convInp = $('conv-llm-url');
  if (convInp && s.conv_llm_url) convInp.value = s.conv_llm_url;
  sv('s-tts-stream-url',         s.tts_stream_url       || '');
  sv('s-customvoice-url',        s.customvoice_url       || 'http://host.docker.internal:8022');
  sv('s-nvidia-router-url',      s.nvidia_router_url     || 'http://host.docker.internal:8090');
  sv('s-nvidia-tts-url',         s.nvidia_tts_url        || 'http://host.docker.internal:8091');
  sv('s-nvidia-asr-url',         s.nvidia_asr_url        || 'http://host.docker.internal:8092');
  sv('s-nvidia-zeroshot-url',    s.nvidia_zeroshot_url || s.nvidia_clone_url || 'http://host.docker.internal:8093');
  sv('s-nvidia-flow-url',        s.nvidia_flow_url     || 'http://host.docker.internal:8094');
  sv('s-tts-stream-mode',        s.tts_stream_mode      || 'auto');
  sv('s-tts-key',                s.tts_api_key          || '');
  sv('s-tts-backend',            s.tts_backend          || 'openai');
  const defaultTtsParams = {temperature:0.1, top_p:0.8, seed:0};
  const byBackend = s.tts_extra_params_by_backend || {};
  sv('s-tts-extra-voice-clone',    JSON.stringify(byBackend.voice_clone    || s.tts_extra_params || defaultTtsParams, null, 2));
  sv('s-tts-extra-streaming',      JSON.stringify(byBackend.streaming      || s.tts_extra_params || defaultTtsParams, null, 2));
  sv('s-tts-extra-customvoice',    JSON.stringify(byBackend.customvoice    || s.tts_extra_params || defaultTtsParams, null, 2));
  sv('s-tts-extra-voice-design',   JSON.stringify(byBackend.voice_design   || s.tts_extra_params || defaultTtsParams, null, 2));
  sv('s-tts-extra-nvidia-magpie',  JSON.stringify(byBackend.nvidia_magpie  || {}, null, 2));
  sv('s-tts-extra-nvidia-zeroshot',JSON.stringify(byBackend.nvidia_zeroshot|| {}, null, 2));
  sv('s-tts-extra-nvidia-flow',    JSON.stringify(byBackend.nvidia_flow    || {}, null, 2));
  sv('s-tts-extra-kokoro',         JSON.stringify(byBackend.kokoro         || {}, null, 2));
  sv('s-tts-extra-vibevoice',      JSON.stringify(byBackend.vibevoice      || {}, null, 2));
  sv('s-voice-design-url',  s.voice_design_url     || 'http://host.docker.internal:8021');
  sv('s-vd-key',            s.voice_design_api_key || '');
  sv('s-voices-scan-dir',   s.voices_scan_dir      || '');
  sv('s-output-dir',        s.output_dir           || '');
  const seedFinderDirEl = $('s-seed-finder-dir');
  if (seedFinderDirEl) seedFinderDirEl.value = s.seed_finder_dir || '';
  const ptDirEl = $('s-pt-dir');
  if (ptDirEl) ptDirEl.value = s.pt_dir || '';
  const seedTextEl = $('s-seed-finder-text');
  if (seedTextEl) seedTextEl.value = s.seed_finder_text || '';
  const themeEl = $('s-theme-select');
  if (themeEl) themeEl.value = document.documentElement.dataset.theme || 'dark';
  if (window.populateLangSelect) window.populateLangSelect();
  // Captures settings
  const sttLang = $('s-stt-language'); if (sttLang) sttLang.value = s.stt_language || '';
  const sttPref = $('s-stt-preferred-backend'); if (sttPref) sttPref.value = s.stt_preferred_backend || '';
  const autoRef = $('s-auto-refine'); if (autoRef) autoRef.value = s.auto_refine || 'off';
  const refModel = $('s-refine-model'); if (refModel) refModel.value = s.refine_model || '';
  const rfFill = $('s-refine-fillers'); if (rfFill) rfFill.checked = s.refine_fillers !== false;
  const rfRep = $('s-refine-repetitions'); if (rfRep) rfRep.checked = s.refine_repetitions !== false;
  const rfCorr = $('s-refine-corrections'); if (rfCorr) rfCorr.checked = s.refine_corrections !== false;
  const rfPunc = $('s-refine-punctuation'); if (rfPunc) rfPunc.checked = s.refine_punctuation !== false;
  // Default capture voice dropdown
  const cvSel = $('s-captures-default-voice');
  if (cvSel && window._voices) {
    const cur = s.captures_default_voice || '';
    cvSel.innerHTML = '<option value="">None — select manually</option>' +
      (window._voices || []).map(v => `<option value="${escHtml(v.name)}" ${v.name === cur ? 'selected' : ''}>${escHtml(v.display_name || v.name)}</option>`).join('');
  }
  await refreshTtsBackendAvailability();
  enhanceSettingsHelp(document);
  if (typeof renderSettingsAbout === 'function') renderSettingsAbout();
}

function markSettingsSeen() {
  localStorage.setItem(SETTINGS_SEEN_KEY, '1');
}
function openSettings(firstRun = false) {
  if (firstRun) markSettingsSeen();
  switchTab('settings');
}
function closeSettings(markSeen = true) {
  if (markSeen) markSettingsSeen();
}

document.querySelectorAll('.s-eye-btn').forEach(btn => {
  btn.addEventListener('click', () => {
    const inp = $(btn.dataset.target);
    inp.type = inp.type === 'password' ? 'text' : 'password';
  });
});
$('settings-btn')?.addEventListener('click', async () => { await loadSettings(); openSettings(false); });
document.addEventListener('click', async e => {
  if (e.target.closest('.s-reload-btn')) { await loadSettings(); toast('Settings reloaded', 'success'); }
});
$('s-use-parakeet-asr')?.addEventListener('click', () => { $('s-whisper-url').value = $('s-nvidia-asr-url').value || 'http://host.docker.internal:8092'; });
$('s-use-nvidia-router')?.addEventListener('click', () => { const url = $('s-nvidia-router-url').value || 'http://host.docker.internal:8090'; $('s-whisper-url').value = url; $('s-nvidia-tts-url').value = url; });
$('s-use-faster-whisper')?.addEventListener('click', () => { $('s-whisper-url').value = $('s-faster-whisper-url').value || 'http://host.docker.internal:8000'; });
$('s-use-whisper-cpp')?.addEventListener('click', () => { $('s-whisper-url').value = $('s-whisper-cpp-url').value || 'http://host.docker.internal:8080'; });
document.addEventListener('click', async e => { if (!e.target.closest('.s-save-btn')) return;
{
  let ttsExtraParamsByBackend = {};
  const paramFields = [
    ['voice_clone', 's-tts-extra-voice-clone', 'Voice Clone/Base'],
    ['streaming', 's-tts-extra-streaming', 'Streaming'],
    ['customvoice', 's-tts-extra-customvoice', 'CustomVoice'],
    ['voice_design', 's-tts-extra-voice-design', 'Voice Design'],
    ['nvidia_magpie', 's-tts-extra-nvidia-magpie', 'NVIDIA Magpie'],
    ['nvidia_zeroshot', 's-tts-extra-nvidia-zeroshot', 'NVIDIA Zeroshot'],
    ['nvidia_flow', 's-tts-extra-nvidia-flow', 'NVIDIA Flow'],
    ['kokoro', 's-tts-extra-kokoro', 'Kokoro'],
    ['vibevoice', 's-tts-extra-vibevoice', 'VibeVoice'],
  ];
  try {
    for (const [key, id, label] of paramFields) {
      ttsExtraParamsByBackend[key] = JSON.parse($(id).value || '{}');
    }
  } catch (e) {
    toast('TTS params JSON is invalid: ' + e.message, 'error');
    return;
  }
  await fetch('/api/settings', { method:'POST', headers:{'Content-Type':'application/json'},
    body: JSON.stringify({
      whisper_url:          $('s-whisper-url').value,
      whisper_api_key:      $('s-whisper-key').value,
      faster_whisper_url:   $('s-faster-whisper-url').value,
      whisper_cpp_url:      $('s-whisper-cpp-url').value,
      groq_api_key:         $('s-groq-api-key').value,
      tts_url:              $('s-tts-url').value,
      kokoro_url:           $('s-kokoro-url').value,
      vibevoice_url:        $('s-vibevoice-url').value,
      xtts_url:             $('s-xtts-url')?.value || '',
      llm_url:              $('s-llm-url')?.value || '',
      tts_stream_url:       $('s-tts-stream-url').value,
      customvoice_url:     $('s-customvoice-url').value,
      nvidia_router_url:   $('s-nvidia-router-url').value,
      nvidia_tts_url:      $('s-nvidia-tts-url').value,
      nvidia_asr_url:      $('s-nvidia-asr-url').value,
      nvidia_clone_url:    $('s-nvidia-zeroshot-url').value,
      nvidia_zeroshot_url: $('s-nvidia-zeroshot-url').value,
      nvidia_flow_url:     $('s-nvidia-flow-url').value,
      tts_stream_mode:      $('s-tts-stream-mode').value,
      tts_api_key:          $('s-tts-key').value,
      tts_backend:          $('s-tts-backend').value,
      tts_extra_params_by_backend: ttsExtraParamsByBackend,
      voice_design_url:     $('s-voice-design-url').value,
      voice_design_api_key: $('s-vd-key').value,
      voices_scan_dir:      $('s-voices-scan-dir').value,
      output_dir:           $('s-output-dir').value,
      seed_finder_dir:      $('s-seed-finder-dir')?.value || '',
      pt_dir:               $('s-pt-dir')?.value || '',
      stt_language:             $('s-stt-language')?.value || '',
      stt_preferred_backend:    $('s-stt-preferred-backend')?.value || '',
      auto_refine:              $('s-auto-refine')?.value || 'off',
      refine_model:             $('s-refine-model')?.value || '',
      refine_fillers:           $('s-refine-fillers')?.checked ?? true,
      refine_repetitions:       $('s-refine-repetitions')?.checked ?? true,
      refine_corrections:       $('s-refine-corrections')?.checked ?? true,
      refine_punctuation:       $('s-refine-punctuation')?.checked ?? true,
      captures_default_voice:   $('s-captures-default-voice')?.value || '',
      seed_finder_text:         $('s-seed-finder-text')?.value || '',
    }) });
  _appSettings.tts_stream_url = $('s-tts-stream-url').value;
  _appSettings.customvoice_url = $('s-customvoice-url').value;
  _appSettings.nvidia_router_url = $('s-nvidia-router-url').value;
  _appSettings.nvidia_tts_url = $('s-nvidia-tts-url').value;
  _appSettings.nvidia_asr_url = $('s-nvidia-asr-url').value;
  _appSettings.nvidia_clone_url = $('s-nvidia-zeroshot-url').value;
  _appSettings.nvidia_zeroshot_url = $('s-nvidia-zeroshot-url').value;
  _appSettings.nvidia_flow_url = $('s-nvidia-flow-url').value;
  _appSettings.voice_design_url = $('s-voice-design-url').value;
  _appSettings.vibevoice_url = $('s-vibevoice-url').value;
  _appSettings.xtts_url = $('s-xtts-url')?.value || '';
  _appSettings.tts_stream_mode = $('s-tts-stream-mode').value;
  _appSettings.seed_finder_text = $('s-seed-finder-text')?.value || '';
  _ttsStreamHealth = null;
  await refreshTtsBackendAvailability($('tts-backend-select')?.value || '');
  markSettingsSeen();
  renderIntegrationSnippets();
  toast('Settings saved', 'success');
}});

// Theme select in General settings
document.addEventListener('change', e => {
  if (e.target.id === 's-theme-select') applyTheme(e.target.value);
  if (e.target.id === 's-lang-select' && window.setAppLang) window.setAppLang(e.target.value);
});


window.enhanceSettingsHelp = enhanceSettingsHelp;
setTimeout(() => enhanceSettingsHelp(document), 0);
const _settingsHelpObserver = new MutationObserver((mutations) => {
  if (mutations.some(m => Array.from(m.addedNodes || []).some(n => n.nodeType === 1 && (n.matches?.('.s-settings-page') || n.querySelector?.('.s-settings-page'))))) {
    enhanceSettingsHelp(document);
  }
});
if (document.body) _settingsHelpObserver.observe(document.body, { childList: true, subtree: true });

// ── Voice ID field (tab 3) ────────────────────────────────────────────────

function validateVoiceId(v) { return /^[A-Za-z0-9_\-\.]+$/.test(v); }

$('voice-id-input')?.addEventListener('input', () => {
  const val = $('voice-id-input').value;
  const ok = val && validateVoiceId(val);
  $('voice-id-input').className = val ? (ok ? 'id-valid' : 'id-invalid') : '';
  if ($('voice-id-hint')) $('voice-id-hint').textContent = val && !ok ? 'Only A-Z, a-z, 0-9, _, -, . allowed' : '';
});
$('helper-apply-btn')?.addEventListener('click', () => {
  const name = $('name-input').value.trim();
  if (!name) { toast('Enter a name first', 'error'); return; }
  $('voice-id-input').value = `${$('lang-select').value}_${$('gender-select').value}_${name}`;
  $('voice-id-input').dispatchEvent(new Event('input'));
});

// ── Global Directory Browser ──────────────────────────────────────────────────
(function initGlobalDirBrowser() {
  const modal = document.getElementById('global-dir-browser-modal');
  const crumb = document.getElementById('global-dir-browser-crumb');
  const list  = document.getElementById('global-dir-browser-list');
  const pathEl = document.getElementById('global-dir-browser-path');
  const selectBtn = document.getElementById('global-dir-browser-select');
  if (!modal || !crumb || !list || !pathEl || !selectBtn) return;

  let currentTargetInput = null;
  let currentPath = '/';

  async function navigateTo(path) {
    currentPath = path;
    list.innerHTML = '<span class="vef-loading" style="padding:8px; color:var(--text-muted);">Loading…</span>';
    pathEl.textContent = path;
    try {
      const data = await fetch('/api/browse-dirs?path=' + encodeURIComponent(path)).then(r => r.json());
      const parts = data.path.split('/').filter(Boolean);
      const crumbs = [{ label: '/', path: '/' }];
      parts.forEach((p, i) => crumbs.push({ label: p, path: '/' + parts.slice(0, i + 1).join('/') }));
      crumb.innerHTML = crumbs.map((c, i) =>
        i < crumbs.length - 1
          ? `<button class="vef-crumb-btn" data-path="${escHtml(c.path)}" style="background:none; border:none; color:var(--text-link); cursor:pointer; font:inherit; padding:2px;">${escHtml(c.label)}</button><span class="vef-sep" style="color:var(--text-muted); margin:0 4px;">/</span>`
          : `<span class="vef-crumb-cur" style="padding:2px;">${escHtml(c.label)}</span>`
      ).join('');
      crumb.querySelectorAll('.vef-crumb-btn').forEach(b => b.addEventListener('click', () => navigateTo(b.dataset.path)));
      
      if (!data.dirs || !data.dirs.length) {
        list.innerHTML = '<span class="vef-empty" style="padding:8px; color:var(--text-muted); display:block;">No subdirectories here.</span>';
      } else {
        list.innerHTML = data.dirs.map(d =>
          `<button class="vef-dir-item" data-path="${escHtml(data.path === '/' ? '/' + d : data.path + '/' + d)}" style="display:flex; width:100%; text-align:left; background:none; border:none; padding:6px 8px; border-radius:4px; cursor:pointer; color:var(--text); align-items:center; gap:8px;">
             <span class="mdi mdi-folder-outline" style="color:var(--text-muted);"></span> ${escHtml(d)}
           </button>`
        ).join('');
        list.querySelectorAll('.vef-dir-item').forEach(b => {
          b.addEventListener('click', () => navigateTo(b.dataset.path));
          b.addEventListener('mouseover', () => b.style.background = 'var(--bg-hover)');
          b.addEventListener('mouseout', () => b.style.background = 'none');
        });
      }
      pathEl.textContent = data.path;
      currentPath = data.path;
    } catch (e) {
      list.innerHTML = `<span class="vef-error" style="padding:8px; color:var(--color-danger); display:block;">Error: ${escHtml(e.message)}</span>`;
    }
  }

  document.addEventListener('click', e => {
    const btn = e.target.closest('.s-browse-dir-btn');
    if (!btn) return;
    currentTargetInput = document.getElementById(btn.dataset.target);
    if (!currentTargetInput) return;
    const startPath = currentTargetInput.value.trim() || currentTargetInput.placeholder || '/';
    modal.hidden = false;
    navigateTo(startPath);
  });

  selectBtn.addEventListener('click', () => {
    if (currentTargetInput && currentPath) {
      currentTargetInput.value = currentPath;
      currentTargetInput.dispatchEvent(new Event('input', { bubbles: true }));
    }
    modal.hidden = true;
  });
})();
