// ── ElevenLabs Voice Library Browser ──────────────────────────────────────

(function initElevenLabsBrowser() {
  if (!$('el-browser-card')) return;

  let _elPage = 0, _elHasMore = false, _elTotal = 0;
  let _elAudio = null, _elAudioBtn = null;
  let _elFilters = {};
  let _elSearchTimer;

  const _elHue = str => {
    let h = 0;
    for (let i = 0; i < str.length; i++) h = (h * 31 + str.charCodeAt(i)) & 0xffffffff;
    return Math.abs(h) % 360;
  };
  const _elEsc = s => String(s || '')
    .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');

  // Load key state from settings
  function elLoadKey() {
    fetch('/api/settings').then(r => r.json()).then(s => {
      const key = (s.elevenlabs_api_key || '').trim();
      const inp = $('el-api-key'), st = $('el-key-status');
      if (inp) inp.placeholder = key ? '••••••••••••••• (key saved)' : 'ElevenLabs API key (free — unlocks full library)…';
      if (st) { st.innerHTML = key ? '<span class="mdi mdi-check-circle-outline"></span> Key active' : 'No key — max 3 results'; st.className = 'el-key-status ' + (key ? 'el-key-ok' : 'el-key-none'); }
      elFetch();
    }).catch(() => elFetch());
  }

  $('el-key-save').addEventListener('click', () => {
    const inp = $('el-api-key');
    const val = (inp?.value || '').trim();
    if (!val || val.startsWith('•')) { _elPage = 0; elFetch(); return; }
    fetch('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ elevenlabs_api_key: val }) })
      .then(() => {
        const st = $('el-key-status');
        if (st) { st.innerHTML = '<span class="mdi mdi-check-circle-outline"></span> Key saved'; st.className = 'el-key-status el-key-ok'; }
        if (inp) { inp.value = ''; inp.placeholder = '••••••••••••••• (key saved)'; }
        _elPage = 0; elFetch();
      });
  });
  $('el-api-key')?.addEventListener('keydown', e => { if (e.key === 'Enter') $('el-key-save')?.click(); });

  $('el-cats')?.querySelectorAll('.el-cat').forEach(pill => {
    pill.addEventListener('click', () => {
      $('el-cats').querySelectorAll('.el-cat').forEach(p => p.classList.remove('active'));
      pill.classList.add('active');
      _elFilters = {};
      for (const attr of pill.getAttributeNames()) {
        if (attr.startsWith('data-el-') && pill.getAttribute(attr))
          _elFilters[attr.slice('data-el-'.length)] = pill.getAttribute(attr);
      }
      _elPage = 0; elFetch();
    });
  });

  ['el-lang', 'el-gender', 'el-age'].forEach(id => {
    $(id)?.addEventListener('change', () => { _elPage = 0; elFetch(); });
  });
  $('el-search')?.addEventListener('input', () => {
    clearTimeout(_elSearchTimer);
    _elSearchTimer = setTimeout(() => { _elPage = 0; elFetch(); }, 420);
  });
  $('el-fetch')?.addEventListener('click', () => { _elPage = 0; elFetch(); });
  $('el-prev')?.addEventListener('click', () => { if (_elPage > 0) { _elPage--; elFetch(); } });
  $('el-next')?.addEventListener('click', () => { if (_elHasMore) { _elPage++; elFetch(); } });

  function elParams() {
    const p = new URLSearchParams({ page_size: '24', page: String(_elPage) });
    const lang = $('el-lang')?.value, gender = $('el-gender')?.value, age = $('el-age')?.value;
    const search = ($('el-search')?.value || '').trim();
    if (lang) p.set('language', lang);
    if (gender) p.set('gender', gender);
    if (age) p.set('age', age);
    if (search) p.set('search', search);
    Object.entries(_elFilters).forEach(([k, v]) => { if (v) p.set(k, v); });
    return p.toString();
  }

  async function elFetch() {
    const grid = $('el-grid'), sb = $('el-status-bar'), st = $('el-status-text');
    if (grid) grid.innerHTML = '<div class="el-loading"><span class="el-spinner"></span> Loading voices…</div>';
    try {
      const res = await fetch('/api/elevenlabs/voices?' + elParams());
      const data = await res.json();
      if (data.detail) {
        if (grid) grid.innerHTML = `<div class="el-error"><span class="mdi mdi-alert-outline"></span> ${_elEsc(data.detail?.message || JSON.stringify(data.detail))}</div>`;
        if (sb) sb.hidden = true;
        return;
      }
      _elHasMore = !!data.has_more;
      _elTotal = data.total_count || 0;
      const voices = data.voices || [];
      if (st) {
        const cap = voices.length < 24 && _elTotal > 3 ? ' · \u{1F511} Add your free API key for full access' : '';
        st.innerHTML = `<strong>${_elTotal.toLocaleString()}</strong> voices &middot; showing ${voices.length} &middot; page ${_elPage + 1}${cap}`;
      }
      if (sb) sb.hidden = false;
      if (!voices.length) {
        if (grid) grid.innerHTML = '<div class="el-empty">No voices found — try different filters.</div>';
      } else {
        if (grid) { grid.innerHTML = voices.map(elCard).join(''); }
        grid?.querySelectorAll('.el-play').forEach(b => b.addEventListener('click', () => elPlay(b)));
        grid?.querySelectorAll('.el-clone').forEach(b => b.addEventListener('click', () => elImport(b)));
      }
      const pager = $('el-pager');
      if (pager) {
        pager.hidden = _elPage === 0 && !_elHasMore;
        const pb = $('el-prev'), nb = $('el-next'), pi = $('el-pager-info');
        if (pb) pb.disabled = _elPage === 0;
        if (nb) nb.disabled = !_elHasMore;
        if (pi) pi.textContent = `Page ${_elPage + 1}${_elTotal > 24 ? ' of ~' + Math.ceil(_elTotal / 24) : ''}`;
      }
    } catch (e) {
      if (grid) grid.innerHTML = `<div class="el-error"><span class="mdi mdi-alert-outline"></span> ${_elEsc(e.message)}</div>`;
    }
  }

  function elCard(v) {
    const hue = _elHue(v.voice_id || v.name || '');
    const letter = _elEsc((v.name || '?')[0].toUpperCase());
    const lang  = (v.language || '').toUpperCase();
    const g     = v.gender === 'female' ? 'F' : v.gender === 'male' ? 'M' : (v.gender || '');
    const age   = (v.age || '').replace(/_/g, ' ');
    const uc    = (v.use_case || '').replace(/_/g, ' ');
    const clones = v.cloned_by_count || 0;
    const cl    = clones >= 1000 ? (clones / 1000).toFixed(1) + 'k' : clones > 0 ? String(clones) : '';
    const desc  = (v.description || '').slice(0, 88) + ((v.description || '').length > 88 ? '…' : '');
    const prev  = _elEsc(v.preview_url || '');
    const nm    = _elEsc(v.name || '');
    const tags  = [
      lang  ? `<span class="el-tag el-lang">${lang}</span>` : '',
      g     ? `<span class="el-tag el-tag-g">${g}</span>` : '',
      age   ? `<span class="el-tag">${_elEsc(age)}</span>` : '',
      uc    ? `<span class="el-tag el-uc">${_elEsc(uc)}</span>` : '',
      (v.accent && v.accent !== 'standard') ? `<span class="el-tag">${_elEsc(v.accent)}</span>` : '',
      v.free_users_allowed ? '<span class="el-tag el-free">Free</span>' : '',
      v.featured ? '<span class="el-tag el-feat"><span class="mdi mdi-star"></span></span>' : '',
      cl ? `<span class="el-tag el-clones"><span class="mdi mdi-download-outline"></span> ${cl}</span>` : '',
    ].join('');
    return `<div class="el-card-voice">
  <div class="el-vc-av" style="--elh:${hue}">${letter}</div>
  <div class="el-vc-body">
    <div class="el-vc-name">${nm}</div>
    <div class="el-vc-tags">${tags}</div>
    ${desc ? `<div class="el-vc-desc">${_elEsc(desc)}</div>` : ''}
    <div class="el-vc-meta">
      <button class="el-play" title="Play preview" data-preview="${prev}">▶ Play</button>
    </div>
  </div>
  ${prev ? `<div class="el-vc-footer"><button class="el-clone" title="Import to Clone a Voice" data-preview="${prev}" data-name="${nm}"><span class="mdi mdi-download"></span> Clone</button></div>` : ''}
</div>`;
  }

  function elPlay(btn) {
    const url = btn.dataset.preview;
    if (!url) return;
    if (_elAudio && _elAudioBtn === btn) {
      _elAudio.paused ? _elAudio.play() : _elAudio.pause();
      btn.textContent = _elAudio.paused ? '▶' : '⏸';
      return;
    }
    if (_elAudio) { _elAudio.pause(); if (_elAudioBtn) _elAudioBtn.textContent = '▶'; }
    _elAudio = new Audio(url);
    _elAudioBtn = btn;
    btn.textContent = '⏸';
    _elAudio.play().catch(() => { btn.textContent = '▶'; _elAudio = null; _elAudioBtn = null; });
    _elAudio.addEventListener('ended', () => { btn.textContent = '▶'; _elAudio = null; _elAudioBtn = null; });
  }

  function elImport(btn) {
    const url = btn.dataset.preview;
    if (!url) return;
    if (typeof navTo === 'function') navTo('s-clone');
    setTimeout(() => {
      const inp = $('lib-add-url'), btn2 = $('lib-add-url-btn');
      if (inp) { inp.value = url; inp.dispatchEvent(new Event('input')); }
      if (btn2) btn2.click();
    }, 120);
  }

  elLoadKey();
})();
loadVoiceLibrary().then(renderIntegrationSnippets).catch(e => status('Voice library load failed: ' + e.message));

// ── Engines section: load containers on startup ────────────────────────────
loadLocalContainers();

// ── Custom engine card storage helpers ────────────────────────────────────
function loadCustomEngineCards() {
  if (_appSettings && Array.isArray(_appSettings.custom_engine_cards) && _appSettings.custom_engine_cards.length)
    return _appSettings.custom_engine_cards;
  try { return JSON.parse(localStorage.getItem('engines-custom-cards') || '[]'); }
  catch (e) { return []; }
}
function saveCustomEngineCards(cards) {
  localStorage.setItem('engines-custom-cards', JSON.stringify(cards));
  if (_appSettings) _appSettings.custom_engine_cards = cards;
  _patchSettings({ custom_engine_cards: cards });
}

// Settings key per docker container name; role fallback for custom cards
const DC_USE_MAP = {
  'tts-faster-qwen3-tts-voiceclone':  { settingKey: 'tts_url',          label: 'Use as TTS' },
  'tts-faster-qwen3-tts-voicedesign': { settingKey: 'voice_design_url', label: 'Use as Voice Design' },
  'tts-faster-qwen3-tts-customvoice': { settingKey: 'customvoice_url',  label: 'Use as CustomVoice' },
  'tts-faster-qwen3-tts-streaming':   { settingKey: 'tts_stream_url',   label: 'Use as Streaming TTS' },
  'faster-whisper':               { settingKey: 'faster_whisper_url', label: 'Use as STT' },
  'fish-speech-api':              { settingKey: 'fishspeech_url',   label: 'Use as Fish-Speech' },
  'nemotron-asr-streaming':       { settingKey: 'faster_whisper_url', label: 'Use as STT' },
  'parakeet-asr':                 { settingKey: 'nvidia_asr_url',   label: 'Use as STT' },
  'parakeet-stt-multi-9011':      { settingKey: 'nvidia_asr_url',   label: 'Use as STT' },
  'magpie-tts':                   { settingKey: 'nvidia_tts_url',   label: 'Use as TTS' },
  'nvidia-magpie-tts':            { settingKey: 'nvidia_tts_url',   label: 'Use as TTS' },
  'nvidia-parakeet-asr':          { settingKey: 'nvidia_router_url', label: 'Use as router' },
  'parakeet-rnnt-nim':            { settingKey: 'nvidia_asr_url',   label: 'Use as STT' },
  'nvidia-parakeet-rnnt-nim':     { settingKey: 'nvidia_asr_url',   label: 'Use as STT' },
  'kokoro-tts':                   { settingKey: 'kokoro_url',       label: 'Use as Kokoro TTS' },
  'nvidia-zeroshot-nim':          { settingKey: 'nvidia_zeroshot_url', label: 'Use as Zeroshot' },
  'nvidia-flow-nim':              { settingKey: 'nvidia_flow_url',  label: 'Use as Flow NIM' },
  'vibevoice-msft-tts':           { settingKey: 'vibevoice_url',    label: 'Use as VibeVoice' },
  'xtts-dgx-spark':               { settingKey: 'xtts_url',         label: 'Use as XTTS' },
  'whisperx-blackwell':           { settingKey: 'faster_whisper_url', label: 'Use as STT' },
};
const DC_ROLE_USE = {
  tts: { settingKey: 'tts_url',              label: 'Use as TTS' },
  stt: { settingKey: 'faster_whisper_url',   label: 'Use as STT' },
  llm: { settingKey: 'llm_url',              label: 'Use as LLM' },
};

window.openAddEngineDialog = function(role) {
  const dlg = document.getElementById('add-engine-dlg');
  if (!dlg) return;
  delete dlg.dataset.editId;
  const titleEl = dlg.querySelector('.add-engine-title');
  if (titleEl) titleEl.textContent = ' Add Custom Engine';
  const saveBtn = document.getElementById('aed-save');
  if (saveBtn) saveBtn.textContent = 'Add Card';
  const roleEl = document.getElementById('aed-role');
  if (roleEl && role) roleEl.value = role;
  ['aed-name', 'aed-url', 'aed-container', 'aed-desc'].forEach(id => {
    const el = document.getElementById(id); if (el) el.value = '';
  });
  dlg.showModal();
};

function editCustomEngineCard(card) {
  const dlg = document.getElementById('add-engine-dlg');
  if (!dlg) return;
  dlg.dataset.editId = String(card.id || card.name);
  const titleEl = dlg.querySelector('.add-engine-title');
  if (titleEl) titleEl.textContent = ' Edit Engine';
  const saveBtn = document.getElementById('aed-save');
  if (saveBtn) saveBtn.textContent = 'Save Changes';
  const roleEl = document.getElementById('aed-role');
  if (roleEl) roleEl.value = card.role || 'tts';
  const setVal = (id, val) => { const el = document.getElementById(id); if (el) el.value = val || ''; };
  setVal('aed-name',      card.label || card.name);
  setVal('aed-url',       card.url);
  setVal('aed-container', card.containerName);
  setVal('aed-desc',      card.description);
  dlg.showModal();
}

(function initCustomEngineDialog() {
  const dlg = document.getElementById('add-engine-dlg');
  if (!dlg) return;
  document.getElementById('aed-cancel')?.addEventListener('click', () => { delete dlg.dataset.editId; dlg.close(); });
  dlg.addEventListener('click', e => { if (e.target === dlg) { delete dlg.dataset.editId; dlg.close(); } });
  document.getElementById('aed-save')?.addEventListener('click', () => {
    const name          = document.getElementById('aed-name')?.value.trim();
    const role          = document.getElementById('aed-role')?.value;
    const url           = document.getElementById('aed-url')?.value.trim();
    const containerName = document.getElementById('aed-container')?.value.trim() || '';
    const desc          = document.getElementById('aed-desc')?.value.trim() || '';
    if (!name) { toast('Name is required', 'error'); return; }
    if (!url)  { toast('URL is required', 'error'); return; }
    const editId = dlg.dataset.editId;
    if (editId) {
      const cards = loadCustomEngineCards().map(c =>
        String(c.id || c.name) === editId
          ? { ...c, name, label: name, role, url, containerName, description: desc }
          : c
      );
      saveCustomEngineCards(cards);
      delete dlg.dataset.editId;
      dlg.close();
      loadLocalContainers();
      toast(`"${name}" updated`, 'success');
    } else {
      const card = { id: 'custom-' + Date.now(), name, label: name, role, url, containerName, description: desc, isCustom: true };
      const cards = loadCustomEngineCards();
      cards.push(card);
      saveCustomEngineCards(cards);
      dlg.close();
      loadLocalContainers();
      toast(`"${name}" added`, 'success');
    }
  });
})();

// ── Local Docker container management ─────────────────────────────────────

function cardType(card) {
  const page = card?.closest('[data-page]');
  return page ? page.dataset.page : '';
}

function _dcSkel(n = 2) {
  const card = () => `<div class="dc-skel-card">
    <div class="dc-skel-line tall"></div>
    <div class="dc-skel-line wide"></div>
    <div class="dc-skel-line medium"></div>
    <div style="display:flex;gap:6px">
      <div class="dc-skel-line short" style="flex:1"></div>
      <div class="dc-skel-line short" style="flex:1"></div>
    </div>
  </div>`;
  return Array.from({ length: n }, card).join('');
}

async function loadLocalContainers() {
  const gridTts = $('dc-grid-tts');
  const gridStt = $('dc-grid-stt');
  const gridLlm = $('dc-grid-llm');
  if (!gridTts && !gridStt && !gridLlm) return;
  const skel = _dcSkel(2);
  if (gridTts) gridTts.innerHTML = skel;
  if (gridStt) gridStt.innerHTML = skel;
  if (gridLlm) gridLlm.innerHTML = skel;
  const custom = loadCustomEngineCards();
  try {
    const r = await fetch('/api/local-containers');
    const d = await r.json();
    renderLocalContainers([...(d.containers || []), ...custom]);
  } catch (e) {
    const err = `<div class="dc-loading dc-load-err">Could not reach server: ${escHtml(e.message)}</div>`;
    if (gridTts) gridTts.innerHTML = err;
    if (gridStt) gridStt.innerHTML = err;
    if (custom.length) renderLocalContainers(custom);
  }
}

function renderLocalContainers(containers) {
  const gridTts = $('dc-grid-tts');
  const gridStt = $('dc-grid-stt');
  const gridLlm = $('dc-grid-llm');
  if (!gridTts && !gridStt && !gridLlm) return;

  const ROLE_LABEL = { tts: 'TTS', stt: 'STT', 'stt+tts': 'STT · TTS', llm: 'LLM' };
  const DC_ICONS = {
    'tts-faster-qwen3-tts-voiceclone':  '<span class="mdi mdi-speaker-outline"></span>',
    'tts-faster-qwen3-tts-voicedesign': '<span class="mdi mdi-auto-fix"></span>',
    'tts-faster-qwen3-tts-customvoice': '🎭',
    'tts-faster-qwen3-tts-streaming':   '<span class="mdi mdi-lightning-bolt"></span>',
    'parakeet-asr':                 '🦜',
    'magpie-tts':                   '🐦',
    'parakeet-rnnt-nim':            '🦜',
    'faster-whisper':                '<span class="mdi mdi-waveform"></span>',
    'fish-speech-api':               '<span class="mdi mdi-emoticon-outline"></span>',
    'nvidia-magpie-tts':             '<span class="mdi mdi-speaker-wireless"></span>',
    'nvidia-parakeet-asr':           '<span class="mdi mdi-microphone-variant"></span>',
    'nvidia-parakeet-rnnt-nim':      '<span class="mdi mdi-microphone-variant"></span>',
    'kokoro-tts':                    '<span class="mdi mdi-music-note"></span>',
    'nvidia-zeroshot-nim':           '<span class="mdi mdi-star-circle-outline"></span>',
    'nvidia-flow-nim':               '<span class="mdi mdi-weather-windy"></span>',
    'nemotron-asr-streaming':        '<span class="mdi mdi-waveform"></span>',
    'vibevoice-msft-tts':            '<span class="mdi mdi-account-voice"></span>',
    'xtts-dgx-spark':                '<span class="mdi mdi-account-music-outline"></span>',
    'litellm':                       '<span class="mdi mdi-router-network"></span>',
    'llama.cpp':                     '<span class="mdi mdi-chip"></span>',
    'ollama':                        '<span class="mdi mdi-circle-outline"></span>',
    'llama-swap':                    '<span class="mdi mdi-swap-horizontal"></span>',
  };
  const _t = (v) => `<span class="mdi mdi-lightning-bolt"></span>`;
  const _l = (v) => `<span class="mdi mdi-clock-outline"></span>`;
  const _q = (v) => `<span class="mdi mdi-star-outline"></span>`;
  const _m = (v) => `<span class="mdi mdi-memory"></span>`;
  const DC_METRICS = {
    'tts-faster-qwen3-tts-voiceclone':  [[_t(),'~0.3× GPU'],[_l(),'1–3 s'],[_q(),'Premium clone'],[_m(),'6–8 GB VRAM']],
    'tts-faster-qwen3-tts-voicedesign': [[_t(),'~0.4× GPU'],[_l(),'1–3 s'],[_q(),'Premium'],      [_m(),'6–8 GB VRAM']],
    'tts-faster-qwen3-tts-customvoice': [[_t(),'~0.3× GPU'],[_l(),'1–3 s'],[_q(),'Premium'],      [_m(),'6–8 GB VRAM']],
    'tts-faster-qwen3-tts-streaming':   [[_t(),'~0.1× GPU'],[_l(),'0.5–1 s'],[_q(),'Premium'],    [_m(),'6–8 GB VRAM']],
    'magpie-tts':                   [[_t(),'~0.05× GPU'],[_l(),'0.3–0.8 s'],[_q(),'High'],    [_m(),'4–6 GB VRAM']],
    'parakeet-asr':                 [[_t(),'~200× RT GPU'],[_l(),'<0.3 s'],[_q(),'Parakeet-TDT'],[_m(),'2 GB VRAM']],
    'parakeet-rnnt-nim':            [[_t(),'~200× RT GPU'],[_l(),'<0.3 s'],[_q(),'Parakeet-1B'], [_m(),'2 GB VRAM']],
    'faster-whisper':                [[_t(),'~70× RT GPU'],[_l(),'0.5–2 s'],[_q(),'Whisper large-v3'],[_m(),'~1.5 GB VRAM']],
    'fish-speech-api':               [[_t(),'~0.4× GPU'],[_l(),'1–4 s'],[_q(),'Tone + clone'],[_m(),'~20 GB VRAM']],
    'vibevoice-msft-tts':            [[_t(),'fast'],[_l(),'0.2–1 s'],[_q(),'TTS'],[_m(),'~2 GB']],
    'xtts-dgx-spark':                [[_t(),'~0.3× GPU'],[_l(),'1–3 s'],[_q(),'XTTS v2'],[_m(),'~3 GB']],
    'litellm':                       [[_t(),'proxy'],[_l(),'route'],[_q(),'OpenAI API'],[_m(),'~600 MB']],
    'llama.cpp':                     [[_t(),'local'],[_l(),'model dependent'],[_q(),'OpenAI API'],[_m(),'model dependent']],
    'ollama':                        [[_t(),'local'],[_l(),'model dependent'],[_q(),'OpenAI API'],[_m(),'model dependent']],
    'llama-swap':                    [[_t(),'swap'],[_l(),'warm after load'],[_q(),'OpenAI API'],[_m(),'model dependent']],
  };
  const roleIcon = { tts: '<span class="mdi mdi-speaker-outline"></span>', stt: '<span class="mdi mdi-microphone-variant"></span>', 'stt+tts': '<span class="mdi mdi-swap-horizontal"></span>', llm: '<span class="mdi mdi-robot-outline"></span>' };

  function buildCardHtml(c) {
    const isCustom = !!c.isCustom;
    const st = isCustom ? 'custom' : (c.status || 'not_found');
    const running  = st === 'running';
    const stopped  = st === 'exited' || st === 'stopped';
    const absent   = st === 'not_found';
    const stLabel  = isCustom ? 'Custom' : running ? 'Running' : stopped ? 'Stopped' : 'Not installed';
    const stCls    = isCustom ? '' : running ? 'llm-compat-running' : stopped ? 'llm-compat-stopped' : '';
    const cardCls  = running ? ' llm-local-card-running' : stopped ? ' llm-local-card-offline' : '';
    const roleBadge = ROLE_LABEL[c.role] || c.role || '';
    const portStr   = c.port ? ` :${c.port}` : '';
    const installed = !absent;
    const icon = DC_ICONS[c.name] || roleIcon[c.role] || '📦';
    const metricChips = (DC_METRICS[c.name] || [])
      .map(([em, txt]) => `<span class="llm-metric-chip">${em} ${escHtml(txt)}</span>`).join('');
    const metricsHtml = metricChips ? `<div class="llm-local-metrics">${metricChips}</div>` : '';
    const n = escHtml(c.name);

    // URL row — input only, no inline Connect button
    const defaultUrl = isCustom
      ? (c.url || '')
      : (c.port ? `http://host.docker.internal:${c.port}${c.role === 'llm' ? '/v1' : ''}` : '');
    const urlRowHtml = `
  <div class="llm-local-url">
    <span class="llm-local-url-label">URL</span>
    <input class="llm-local-url-inp dc-url-inp" type="text" spellcheck="false"
      placeholder="${escHtml(defaultUrl)}"
      data-dc-url-key="${n}"
      data-dc-url-default="${escHtml(defaultUrl)}">
  </div>`;

    // Container name row — always shown so users can pre-configure even absent containers
    const savedContainerName = isCustom
      ? (c.containerName || '')
      : ((_appSettings?.engine_container_names?.[c.name]) || (installed ? c.name : ''));
    const containerNameRowHtml = `
  <div class="llm-local-url">
    <span class="llm-local-url-label" title="Docker container name — enables Stop / Start / Restart"><span class="mdi mdi-docker"></span></span>
    <input class="llm-local-url-inp dc-container-name-inp" type="text"
      placeholder="container name (optional)" spellcheck="false"
      value="${escHtml(savedContainerName)}"
      data-dc-initial-running="${running}"
      data-cn-key="${n}">
  </div>`;

    // API key row — only for LLMs
    const savedApiKey = isCustom
      ? (c.apiKey || '')
      : ((_appSettings?.engine_api_keys?.[c.name]) || localStorage.getItem('dc-apikey-' + n) || '');
    const apiKeyRowHtml = (c.role === 'llm' || c.name === 'litellm') ? `
  <div class="llm-local-url">
    <span class="llm-local-url-label" title="API Key"><span class="mdi mdi-key-outline"></span></span>
    <input class="llm-local-url-inp dc-apikey-inp" type="password"
      placeholder="API key (optional)" spellcheck="false"
      value="${escHtml(savedApiKey)}"
      data-dc-apikey-key="${n}">
  </div>` : '';

    // Use-as button — right-aligned, clearly labelled as the activation action
    const useEntry = DC_USE_MAP[c.name] || DC_ROLE_USE[c.role];
    const useBtn = useEntry
      ? `<button class="llm-use-btn dc-use-btn"
           data-dc-use-key="${escHtml(useEntry.settingKey)}"
           data-dc-name="${n}"
           title="Saves this URL to Settings and makes it available in the ${escHtml(useEntry.label.replace('Use as ', ''))} dropdown. Also applied automatically when you Connect.">
           <span class="mdi mdi-arrow-right-circle-outline"></span> ${escHtml(useEntry.label)}
         </button>`
      : '';

    // Docker action buttons — dynamic; renderDockerBtns fills them in via container name input
    const dockerBtnsSpan = (isCustom || installed)
      ? '<span class="dc-docker-btns-span"></span>'
      : '';
    const githubBtn = (!isCustom && absent && c.repo)
      ? `<a class="llm-use-btn" href="${escHtml(c.repo)}" target="_blank" rel="noopener"><span class="mdi mdi-github"></span> View on GitHub</a>`
      : '';

    // Edit / Delete for custom cards
    const customId  = escHtml(String(c.id || c.name));
    const editBtn   = isCustom
      ? `<button class="llm-use-btn dc-edit-btn" data-dc-custom-id="${customId}"><span class="mdi mdi-pencil-outline"></span> Edit</button>`
      : '';
    const deleteBtn = isCustom
      ? `<button class="llm-use-btn dc-delete-btn" data-dc-custom-id="${customId}"><span class="mdi mdi-delete-outline"></span> Remove</button>`
      : '';

    // Unified controls row: [Connect] [Stop/Start/Restart] [Edit/Delete] [Use as →]
    const controlsRowHtml = `
  <div class="dc-controls-row">
    <button class="llm-local-ping dc-connect-btn" data-dc-url-key="${n}"><span class="mdi mdi-lan-connect"></span> Connect</button>
    <div class="dc-ctrl-group">${dockerBtnsSpan}${githubBtn}${editBtn}${deleteBtn}</div>
    ${useBtn}
  </div>`;

    const portNum = c.port || '';
    const snippetBaseUrl = isCustom ? (c.url || 'http://localhost') : `http://localhost:${portNum}`;
    let snippetCode = '';
    if (c.role === 'tts') {
      snippetCode = `curl -s "${snippetBaseUrl}/v1/audio/speech" \\\n  -H "Authorization: Bearer dummy" \\\n  -H "Content-Type: application/json" \\\n  -d '{"model":"tts-1","voice":"default","input":"Hello world","response_format":"wav"}' \\\n  --output test.wav`;
    } else if (c.role === 'stt') {
      snippetCode = `curl -s "${snippetBaseUrl}/v1/audio/transcriptions" \\\n  -F "file=@audio.wav" \\\n  -F "model=whisper-1"`;
    } else {
      snippetCode = `curl -s "${snippetBaseUrl}/v1/chat/completions" \\\n  -H "Authorization: Bearer dummy" \\\n  -H "Content-Type: application/json" \\\n  -d '{"model":"default","messages":[{"role":"user","content":"Hello!"}]}'`;
    }
    const snippetCopy = escHtml(snippetCode);
    const snippetHtml = (portNum || isCustom) ? `
  <div class="llm-local-snippet">
    <div class="llm-snippet-bar">
      <span>API test</span>
      <button class="llm-copy-btn dc-copy-btn" data-copy="${snippetCopy}">Copy</button>
    </div>
    <pre>${escHtml(snippetCode)}</pre>
  </div>` : '';

    return `<div class="llm-local-card${cardCls}">
  <div class="llm-local-head">
    <span class="llm-local-icon">${icon}</span>
    <span class="llm-local-name">${escHtml(c.label || c.name)}</span>
    ${roleBadge ? `<span class="llm-local-compat">${escHtml(roleBadge)}${escHtml(portStr)}</span>` : ''}
    <span class="llm-local-compat ${stCls}">${escHtml(stLabel)}</span>
  </div>
  ${metricsHtml}
  ${c.description ? `<p class="llm-local-desc">${escHtml(c.description)}</p>` : ''}${urlRowHtml}${apiKeyRowHtml}${containerNameRowHtml}${controlsRowHtml}${snippetHtml}
</div>`;
  }

  const isTts = c => c.role === 'tts';
  const isStt = c => c.role === 'stt' || c.role === 'stt+tts';
  const isLlm = c => c.role === 'llm';

  if (gridTts) gridTts.innerHTML = containers.filter(isTts).map(buildCardHtml).join('');
  if (gridStt) gridStt.innerHTML = containers.filter(isStt).map(buildCardHtml).join('');
  if (gridLlm) gridLlm.innerHTML = containers.filter(isLlm).map(buildCardHtml).join('');

  [gridTts, gridStt, gridLlm].forEach(grid => {
    if (!grid) return;

    // Stop / Start / Restart docker containers
    grid.querySelectorAll('.dc-btn[data-dc-action]').forEach(btn => {
      btn.addEventListener('click', async () => {
        const action = btn.dataset.dcAction;
        const name   = btn.dataset.dcName;
        btn.disabled = true;
        btn.textContent = action === 'start' ? 'Starting…' : 'Stopping…';
        try {
          const r = await fetch(`/api/local-containers/${encodeURIComponent(name)}/${action}`, { method: 'POST' });
          const d = await r.json();
          if (!d.ok) toast(d.error || `${action} failed`, 'error');
        } catch (e) {
          toast(`${action} failed: ${e.message}`, 'error');
        }
        await loadLocalContainers();
      });
    });

    // Copy snippet buttons
    grid.querySelectorAll('.dc-copy-btn').forEach(btn => {
      btn.addEventListener('click', async () => {
        const text = (btn.dataset.copy || '').replace(/&#10;/g, '\n').replace(/&quot;/g, '"').replace(/&amp;/g, '&');
        await copyText(text);
        toast('Copied', 'success');
      });
    });

    // Helpers — set connected / disconnected visual state on a card
    function setConnected(btn, card, useBtn, connected) {
      if (connected) {
        btn.innerHTML = '<span class="mdi mdi-check-network"></span> Connected';
        btn.className = 'llm-local-ping dc-connect-btn ok';
        btn.title = 'Reachable — click to disconnect';
        if (card) card.classList.add('llm-local-card-online');
        if (useBtn) useBtn.classList.add('active');
      } else {
        btn.innerHTML = '<span class="mdi mdi-lan-connect"></span> Connect';
        btn.className = 'llm-local-ping dc-connect-btn';
        btn.title = 'Test connection to this service';
        if (card) card.classList.remove('llm-local-card-online');
        if (useBtn) useBtn.classList.remove('active');
      }
    }

    // URL input — server settings first, localStorage fallback; write to both on change
    grid.querySelectorAll('.dc-url-inp[data-dc-url-key]').forEach(inp => {
      const key       = inp.dataset.dcUrlKey;
      const fromSrv   = _appSettings?.engine_local_urls?.['dc-' + key];
      const fromLS    = localStorage.getItem('dc-url-' + key);
      if (fromSrv || fromLS) inp.value = fromSrv || fromLS;
      inp.addEventListener('input', () => {
        localStorage.setItem('dc-url-' + key, inp.value);
        if (window._saveEngineLocalUrls) window._saveEngineLocalUrls();
      });
      if (localStorage.getItem('dc-con-' + key) === '1') {
        const card   = inp.closest('.llm-local-card');
        const btn    = card?.querySelector(`.dc-connect-btn[data-dc-url-key="${CSS.escape(key)}"]`);
        const useBtn = card?.querySelector('.dc-use-btn');
        if (btn) setConnected(btn, card, useBtn, true);
      }
    });

    // Connect / Disconnect — probe URL, auto-apply setting, show state
    grid.querySelectorAll('.dc-connect-btn[data-dc-url-key]').forEach(btn => {
      btn.addEventListener('click', async () => {
        // Disconnect if already connected
        if (btn.classList.contains('ok')) {
          const key  = btn.dataset.dcUrlKey;
          const card = btn.closest('.llm-local-card');
          const useBtn = card?.querySelector('.dc-use-btn');
          setConnected(btn, card, useBtn, false);
          localStorage.removeItem('dc-con-' + key);
          return;
        }
        const key    = btn.dataset.dcUrlKey;
        const card   = btn.closest('.llm-local-card');
        const inp    = card?.querySelector(`.dc-url-inp[data-dc-url-key="${CSS.escape(key)}"]`);
        const apikeyInp = card?.querySelector(`.dc-apikey-inp[data-dc-apikey-key="${CSS.escape(key)}"]`);
        const useBtn = card?.querySelector('.dc-use-btn');
        const rawUrl = inp?.value.trim() || inp?.placeholder;
        const apiKey = apikeyInp?.value.trim() || '';
        if (!rawUrl) { toast('Enter a URL first', 'error'); return; }
        btn.disabled = true;
        btn.innerHTML = '<span class="mdi mdi-loading mdi-spin"></span> Connecting…';
        try {
          if (window.probeUrl) {
            const type = card ? cardType(card) : '';
            const d    = await window.probeUrl(rawUrl, type, apiKey);
            if (d.ok) {
              setConnected(btn, card, useBtn, true);
              localStorage.setItem('dc-con-' + key, '1');
              // Auto-apply URL to settings so it appears in dropdowns immediately
              if (useBtn && window.applyAndSaveSettings) {
                const settingKey = useBtn.dataset.dcUseKey;
                const url = inp?.value.trim() || inp?.placeholder || '';
                if (settingKey && url) {
                  const patch = { [settingKey]: url };
                  if (apiKey && settingKey === 'llm_url') patch.llm_api_key = apiKey;
                  await window.applyAndSaveSettings(patch);
                  toast(`✓ Connected and set as active ${useBtn.textContent.replace('Use as ', '').trim()}`, 'success');
                } else {
                  toast('✓ Service reachable', 'success');
                }
              } else {
                toast('✓ Service reachable', 'success');
              }
            } else {
              setConnected(btn, card, useBtn, false);
              localStorage.removeItem('dc-con-' + key);
              toast('Cannot reach ' + rawUrl + ': ' + (d.error || 'No response'), 'error');
            }
          } else {
            setConnected(btn, card, useBtn, false);
          }
        } catch (e) {
          setConnected(btn, card, useBtn, false);
          localStorage.removeItem('dc-con-' + key);
        } finally {
          btn.disabled = false;
        }
      });
    });

    // "Use as TTS / STT / LLM" — manual override to apply URL to settings
    grid.querySelectorAll('.dc-use-btn[data-dc-use-key]').forEach(btn => {
      btn.addEventListener('click', async () => {
        const settingKey = btn.dataset.dcUseKey;
        const urlKey     = btn.dataset.dcName;
        const card = btn.closest('.llm-local-card');
        const inp = card?.querySelector(`.dc-url-inp[data-dc-url-key="${CSS.escape(urlKey)}"]`);
        const apikeyInp = card?.querySelector(`.dc-apikey-inp[data-dc-apikey-key="${CSS.escape(urlKey)}"]`);
        const url = inp?.value.trim() || inp?.placeholder || '';
        const apiKey = apikeyInp?.value.trim() || '';
        if (!url) { toast('Enter a URL first', 'error'); return; }
        if (window.applyAndSaveSettings) {
          const patch = { [settingKey]: url };
          if (apiKey && settingKey === 'llm_url') patch.llm_api_key = apiKey;
          await window.applyAndSaveSettings(patch);
          if (settingKey === 'llm_url' && window.syncActiveLlmPanel) window.syncActiveLlmPanel(url, apiKey);
          btn.classList.add('active');
          toast(`✓ ${btn.textContent.trim()} — now available in the backend dropdown`, 'success');
        }
      });
    });

    // Edit custom card
    grid.querySelectorAll('.dc-edit-btn[data-dc-custom-id]').forEach(btn => {
      btn.addEventListener('click', () => {
        const id   = btn.dataset.dcCustomId;
        const card = loadCustomEngineCards().find(c => String(c.id || c.name) === id);
        if (card) editCustomEngineCard(card);
      });
    });

    // Delete custom card
    grid.querySelectorAll('.dc-delete-btn[data-dc-custom-id]').forEach(btn => {
      btn.addEventListener('click', () => {
        const id = btn.dataset.dcCustomId;
        const cards = loadCustomEngineCards().filter(c => String(c.id || c.name) !== id);
        saveCustomEngineCards(cards);
        loadLocalContainers();
      });
    });

    // Container name input — wires docker buttons dynamically for custom and Docker stack cards
    grid.querySelectorAll('.dc-container-name-inp').forEach(inp => {
      const card     = inp.closest('.llm-local-card');
      const btnsSpan = card?.querySelector('.dc-docker-btns-span');
      const customId = card?.querySelector('.dc-edit-btn')?.dataset.dcCustomId;

      function renderDockerBtns(name, initialRunning) {
        if (!btnsSpan) return;
        if (!name) { btnsSpan.innerHTML = ''; return; }
        const esc      = name.replace(/&/g, '&amp;').replace(/"/g, '&quot;');
        const startBtn = `<button class="llm-use-btn dc-btn" data-dc-action="start"   data-dc-name="${esc}"><span class="mdi mdi-play"></span> Start</button>`;
        const stopBtn  = `<button class="llm-use-btn dc-btn" data-dc-action="stop"    data-dc-name="${esc}"><span class="mdi mdi-stop"></span> Stop</button>`;
        const rstBtn   = `<button class="llm-use-btn dc-btn" data-dc-action="restart" data-dc-name="${esc}"><span class="mdi mdi-restart"></span> Restart</button>`;
        if (initialRunning === true)       btnsSpan.innerHTML = stopBtn + rstBtn;
        else if (initialRunning === false) btnsSpan.innerHTML = startBtn + rstBtn;
        else                               btnsSpan.innerHTML = startBtn + stopBtn + rstBtn;
        btnsSpan.querySelectorAll('.dc-btn').forEach(btn => {
          btn.addEventListener('click', async () => {
            const action = btn.dataset.dcAction;
            const cname  = btn.dataset.dcName;
            const orig   = btn.innerHTML;
            btn.disabled = true;
            btn.textContent = action === 'start' ? 'Starting…' : action === 'stop' ? 'Stopping…' : 'Restarting…';
            try {
              const r = await fetch(`/api/local-containers/${encodeURIComponent(cname)}/${action}`, { method: 'POST' });
              const d = await r.json();
              if (d.ok) toast(`${cname}: ${action} OK`, 'success');
              else toast(d.error || `${action} failed`, 'error');
            } catch (e) { toast(`${action} failed: ${e.message}`, 'error'); }
            btn.disabled = false; btn.innerHTML = orig;
          });
        });
      }

      inp.addEventListener('input', () => {
        const name = inp.value.trim();
        if (window._saveEngineContainerNames) window._saveEngineContainerNames();
        renderDockerBtns(name); // no initial state after user edits → show all 3 buttons
        if (customId) {
          const updated = loadCustomEngineCards().map(c =>
            String(c.id || c.name) === customId ? { ...c, containerName: name } : c
          );
          saveCustomEngineCards(updated);
        }
      });

      const initRunning = inp.dataset.dcInitialRunning === 'true'  ? true
                        : inp.dataset.dcInitialRunning === 'false' ? false : undefined;
      renderDockerBtns(inp.value.trim(), initRunning);
    });

    initLlmSnippets(grid);
  });
}

document.querySelectorAll('.dc-refresh-btn').forEach(b => b.addEventListener('click', loadLocalContainers));

