// app-core.js — Web UI part 1/3 (globals, init, utils, WS, chat, render, nav, models, memory, sessions, workspaces, plugins).
// Split from the former monolithic app.js (v5.32.0). Loaded as a classic global-scope script BEFORE app-panels.js + app-views.js — order matters.

/**
 * Alvin Bot Web UI — Client-side application
 * Professional redesign with Lucide icons, i18n, command palette
 */

const API = '';
let ws = null;
let currentAssistantMsg = null;
let chatMessages = [];
let isTyping = false;
let notifySound = true;
const CHAT_STORAGE_KEY = 'alvinbot_chat_history';

// ── UI Init (icons, i18n, static elements) ──────────────
function initUI() {
  // Sidebar title
  document.getElementById('sidebar-title').innerHTML = `${icon('bot', 20)} <span>${t('app.title')}</span>`;
  document.getElementById('bot-status').innerHTML = `<span class="status-dot offline"></span> ${t('connecting')}`;

  // Nav
  const NAV = [
    { section: 'nav.main', items: [
      { page: 'chat', icon: 'message-square', label: 'nav.chat' },
      { page: 'dashboard', icon: 'layout-dashboard', label: 'nav.dashboard' },
    ]},
    { section: 'nav.ai', items: [
      { page: 'models', icon: 'bot', label: 'nav.models' },
      { page: 'personality', icon: 'palette', label: 'nav.personality' },
    ]},
    { section: 'nav.data', items: [
      { page: 'memory', icon: 'brain', label: 'nav.memory' },
      { page: 'sessions', icon: 'clipboard', label: 'nav.sessions' },
      { page: 'workspaces', icon: 'compass', label: 'nav.workspaces' },
      { page: 'files', icon: 'folder', label: 'nav.files' },
    ]},
    { section: 'nav.system', items: [
      { page: 'cron', icon: 'timer', label: 'nav.cron' },
      { page: 'tools', icon: 'hammer', label: 'nav.tools' },
      { page: 'plugins', icon: 'plug', label: 'nav.plugins' },
      { page: 'platforms', icon: 'smartphone', label: 'nav.platforms' },
      { page: 'users', icon: 'users', label: 'nav.users' },
      { page: 'terminal', icon: 'terminal', label: 'nav.terminal' },
      { page: 'reliability', icon: 'shield-check', label: 'nav.reliability' },
      { page: 'maintenance', icon: 'stethoscope', label: 'nav.maintenance' },
      { page: 'settings', icon: 'settings', label: 'nav.settings' },
    ]},
  ];

  let navHtml = '';
  for (const section of NAV) {
    navHtml += `<div class="nav-section">${t(section.section)}</div>`;
    for (const item of section.items) {
      const active = item.page === 'chat' ? ' active' : '';
      navHtml += `<div class="nav-item${active}" data-page="${item.page}" data-icon="${item.icon}" data-label="${item.label}">${icon(item.icon, 16)} <span>${t(item.label)}</span></div>`;
    }
  }
  document.getElementById('nav-container').innerHTML = navHtml;

  // Sidebar footer
  const langDe = getLang() === 'de' ? 'lang-active' : '';
  const langEn = getLang() === 'en' ? 'lang-active' : '';
  document.getElementById('sidebar-footer').innerHTML = `
    <button onclick="toggleTheme()" title="${t('sidebar.theme')}">${icon('sun', 14)} <span>${t('sidebar.theme')}</span></button>
    <button onclick="resetChat()" title="${t('chat.new.session')}">${icon('refresh-cw', 14)} <span>${t('sidebar.reset')}</span></button>
    <button class="lang-toggle" onclick="toggleLang()" title="${t('sidebar.lang')}">
      ${icon('languages', 14)}
      <span><span class="${langDe}">DE</span> | <span class="${langEn}">EN</span></span>
    </button>
  `;

  // Page title
  document.getElementById('page-title').innerHTML = `${icon('message-square', 18)} ${t('nav.chat')}`;

  // Cmd+K hint
  document.getElementById('cmd-k-hint').textContent = navigator.platform?.includes('Mac') ? '⌘K' : 'Ctrl+K';

  // Chat header
  document.getElementById('chat-header').innerHTML = `
    <label>${t('chat.model')}:</label>
    <select id="chat-model" onchange="switchModel(this.value)"></select>
    <label style="margin-left:8px">${t('chat.effort')}:</label>
    <select id="chat-effort">
      <option value="low">${t('chat.effort.low')}</option>
      <option value="medium">${t('chat.effort.medium')}</option>
      <option value="high" selected>${t('chat.effort.high')}</option>
      <option value="max">${t('chat.effort.max')}</option>
    </select>
    <div style="flex:1"></div>
    <button class="btn btn-sm btn-outline" onclick="exportChat('markdown')" title="⌘⇧E">${icon('download', 14)} ${t('chat.export')}</button>
    <button class="btn btn-sm btn-outline" onclick="exportChat('json')">JSON</button>
  `;

  // Chat welcome
  document.getElementById('chat-welcome').textContent = t('chat.welcome');

  // Chat input area
  document.getElementById('chat-input-area').innerHTML = `
    <label for="file-upload" style="cursor:pointer;padding:4px 8px;opacity:0.6;transition:opacity 0.2s;display:flex;align-items:center" title="${t('chat.file.attach')}">
      ${icon('paperclip', 20)}
    </label>
    <input type="file" id="file-upload" style="display:none" onchange="handleFileSelect(this.files)">
    <textarea id="chat-input" placeholder="${t('chat.placeholder')}" rows="1"></textarea>
    <button class="btn-send" id="send-btn" onclick="sendMessage()">${icon('send', 16)} ${t('chat.send')}</button>
  `;

  // Reply/file close buttons
  document.getElementById('reply-close-btn').innerHTML = icon('x', 16);
  document.getElementById('file-close-btn').innerHTML = icon('x', 16);

  // Drop overlay
  document.getElementById('drop-overlay').innerHTML = `${icon('paperclip', 24)} ${t('chat.file.drop')}`;

  // Memory save button
  document.getElementById('memory-save-btn').innerHTML = `${icon('save', 14)} ${t('save')}`;

  // File buttons
  document.getElementById('file-new-btn').innerHTML = `${icon('file-text', 14)} ${t('files.new')}`;
  document.getElementById('file-up-btn').innerHTML = `${icon('arrow-up', 14)} ${t('files.up')}`;
  document.getElementById('file-save-btn').innerHTML = `${icon('save', 14)} ${t('save')}`;
  document.getElementById('file-delete-btn').innerHTML = `${icon('trash-2', 14)}`;
  document.getElementById('file-close-editor-btn').innerHTML = icon('x', 14);
  document.getElementById('file-breadcrumb').innerHTML = `${icon('folder', 14)} /`;

  // Cron header
  document.getElementById('cron-header').innerHTML = `
    <div style="flex:1">
      <h3 style="font-size:1em;margin-bottom:4px;display:flex;align-items:center;gap:6px">${icon('timer', 18)} ${t('cron.title')}</h3>
      <div class="sub">${t('cron.desc')}</div>
    </div>
    <button class="btn btn-sm" onclick="showCreateCron()">${icon('plus', 14)} ${t('cron.create')}</button>
  `;

  // Cron type selector
  document.getElementById('cron-name').placeholder = t('cron.name.placeholder');
  const cronType = document.getElementById('cron-type');
  cronType.innerHTML = `
    <option value="reminder">${icon('timer', 14)} ${t('cron.type.reminder')}</option>
    <option value="shell">${icon('zap', 14)} ${t('cron.type.shell')}</option>
    <option value="http">${icon('globe', 14)} ${t('cron.type.http')}</option>
    <option value="message">${icon('message-square', 14)} ${t('cron.type.message')}</option>
    <option value="ai-query">${icon('bot', 14)} ${t('cron.type.ai')}</option>
  `;
  document.getElementById('cron-payload').placeholder = t('cron.payload.placeholder');
  document.getElementById('cron-form-buttons').innerHTML = `
    <button class="btn btn-sm" onclick="createCronJob()">${icon('save', 14)} ${t('cron.create')}</button>
    <button class="btn btn-sm btn-outline" onclick="document.getElementById('cron-create-form').style.display='none'">${t('cancel')}</button>
  `;

  // Tools search
  document.getElementById('tools-search').placeholder = `${t('tools.search.placeholder')}`;

  // Terminal
  document.getElementById('terminal-input').placeholder = t('terminal.placeholder');
  document.getElementById('terminal-run-btn').innerHTML = `${icon('play', 14)} ${t('terminal.run')}`;

  // Maintenance loading
  document.getElementById('maint-loading').textContent = t('loading');

  // Command palette
  document.getElementById('cmd-palette-input').placeholder = t('cmd.placeholder');

  // Personality card
  document.getElementById('personality-card').innerHTML = `
    <h3 style="display:flex;align-items:center;gap:8px">${icon('palette', 18)} ${t('personality.title')}</h3>
    <div class="sub" style="margin-bottom:12px">${t('personality.desc')}</div>
    <textarea class="editor" id="soul-editor" style="min-height:300px" placeholder="${t('personality.placeholder')}"></textarea>
    <div style="margin-top:12px;display:flex;gap:8px">
      <button class="btn" onclick="saveSoul()">${icon('save', 14)} ${t('personality.save')}</button>
      <button class="btn btn-outline" onclick="loadPersonality()">${icon('refresh-cw', 14)} ${t('personality.reload')}</button>
    </div>
  `;

  // Rebind nav events
  bindNavEvents();
  bindChatEvents();
}

// Called by i18n when language changes
function refreshUI() {
  // Save current page
  const activePage = document.querySelector('.nav-item.active')?.dataset?.page || 'chat';
  initUI();
  // Restore active page
  const navItem = document.querySelector(`.nav-item[data-page="${activePage}"]`);
  if (navItem) navItem.click();
}

// ── Chat Persistence ────────────────────────────────────
function saveChatToStorage() {
  try { localStorage.setItem(CHAT_STORAGE_KEY, JSON.stringify(chatMessages)); } catch { }
}

function restoreChatFromStorage() {
  try {
    const stored = localStorage.getItem(CHAT_STORAGE_KEY);
    if (!stored) return;
    const messages = JSON.parse(stored);
    if (!Array.isArray(messages) || messages.length === 0) return;
    chatMessages = messages;
    for (const msg of messages) {
      addMessage(msg.role, msg.text, msg.time, true);
    }
  } catch { }
}

function clearChatStorage() { localStorage.removeItem(CHAT_STORAGE_KEY); }

// ── Toast Notifications ─────────────────────────────────
function toast(message, type = 'success') {
  const el = document.createElement('div');
  el.className = `toast ${type}`;
  const ic = type === 'success' ? icon('circle-check', 16) : icon('circle-alert', 16);
  el.innerHTML = `${ic} <span>${escapeHtml(message)}</span>`;
  document.body.appendChild(el);
  setTimeout(() => el.remove(), 3000);
}

// ── Navigation ──────────────────────────────────────────
function bindNavEvents() {
  document.querySelectorAll('.nav-item').forEach(item => {
    item.addEventListener('click', () => {
      document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
      item.classList.add('active');
      const page = item.dataset.page;
      document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
      const pageEl = document.getElementById('page-' + page);
      if (pageEl) pageEl.classList.add('active');
      document.getElementById('page-title').innerHTML = `${icon(item.dataset.icon, 18)} ${t(item.dataset.label)}`;

      const loaders = { dashboard: loadDashboard, memory: loadMemory, models: loadModels,
        sessions: loadSessions, plugins: loadPlugins, tools: loadTools, cron: loadCron,
        files: () => navigateFiles('.'), users: loadUsers, settings: loadSettings,
        platforms: loadPlatforms, personality: loadPersonality, maintenance: loadMaintenance,
        workspaces: loadWorkspaces, reliability: loadReliability };
      if (loaders[page]) loaders[page]();
    });
  });
}

// ── WebSocket ───────────────────────────────────────────
function connectWS() {
  const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
  ws = new WebSocket(`${proto}//${location.host}`);
  ws.onopen = () => {
    document.getElementById('bot-status').innerHTML = `<span class="status-dot online"></span> ${t('connected')}`;
  };
  ws.onclose = () => {
    document.getElementById('bot-status').innerHTML = `<span class="status-dot offline"></span> ${t('reconnecting')}`;
    setTimeout(connectWS, 3000);
  };
  ws.onmessage = (e) => handleWSMessage(JSON.parse(e.data));
}

let pendingTools = [];
let currentToolGroup = null;

function handleWSMessage(msg) {
  const typing = document.getElementById('typing-indicator');
  switch (msg.type) {
    case 'text':
      typing.classList.remove('visible');
      flushToolGroup();
      if (!currentAssistantMsg) currentAssistantMsg = addMessage('assistant', '');
      currentAssistantMsg.querySelector('.msg-text').innerHTML = renderMarkdown(msg.text || '');
      scrollToBottom();
      break;
    case 'tool':
      typing.classList.add('visible');
      pendingTools.push({ name: msg.name, input: msg.input });
      updateToolIndicator();
      break;
    case 'done':
      flushToolGroup();
      if (currentAssistantMsg && (msg.cost || msg.inputTokens || msg.outputTokens)) {
        const costEl = document.createElement('span');
        costEl.className = 'time';
        const parts = [];
        if (msg.inputTokens || msg.outputTokens) parts.push(`${(msg.inputTokens||0)+(msg.outputTokens||0)} tokens`);
        if (msg.cost) parts.push(`$${msg.cost.toFixed(4)}`);
        costEl.textContent = parts.join(' · ');
        currentAssistantMsg.querySelector('.msg-text').appendChild(costEl);
      }
      if (currentAssistantMsg) {
        chatMessages.push({ role: 'assistant', text: currentAssistantMsg.querySelector('.msg-text').textContent, time: timeStr() });
        saveChatToStorage();
      }
      currentAssistantMsg = null;
      document.getElementById('send-btn').disabled = false;
      typing.classList.remove('visible');
      if (notifySound) playNotifySound();
      break;
    case 'error':
      flushToolGroup();
      addMessage('system', `${msg.error}`);
      currentAssistantMsg = null;
      document.getElementById('send-btn').disabled = false;
      typing.classList.remove('visible');
      break;
    case 'fallback':
      addMessage('system', `${icon('zap', 14)} ${t('chat.fallback')}: ${msg.from} → ${msg.to}`);
      break;
    case 'reset':
      document.getElementById('messages').innerHTML = `<div class="msg system">${t('chat.welcome')}</div><div class="typing-indicator" id="typing-indicator"><div class="typing-dot"></div><div class="typing-dot"></div><div class="typing-dot"></div></div>`;
      addMessage('system', t('chat.session.reset'));
      chatMessages = [];
      pendingTools = [];
      currentToolGroup = null;
      clearChatStorage();
      break;
  }
}

function updateToolIndicator() {
  const typing = document.getElementById('typing-indicator');
  if (pendingTools.length > 0) {
    typing.classList.add('visible');
    typing.innerHTML = `<span style="font-size:0.75em;color:var(--fg2);display:flex;align-items:center;gap:4px">${icon('wrench', 14)} ${pendingTools[pendingTools.length - 1].name}...</span>`;
  }
}

function flushToolGroup() {
  if (pendingTools.length === 0) return;
  const typing = document.getElementById('typing-indicator');
  typing.innerHTML = '<div class="typing-dot"></div><div class="typing-dot"></div><div class="typing-dot"></div>';

  const group = document.createElement('div');
  group.className = 'msg tool-group';
  const count = pendingTools.length;
  const names = [...new Set(pendingTools.map(t => t.name))];
  const summary = names.length <= 3 ? names.join(', ') : names.slice(0, 3).join(', ') + ` +${names.length - 3}`;

  group.innerHTML = `
    <div class="tool-group-header" onclick="this.parentElement.classList.toggle('expanded')">
      <span class="tool-group-icon">${icon('wrench', 14)}</span>
      <span class="tool-group-label">${t('chat.tools.used', { count })}</span>
      <span class="tool-group-names">${summary}</span>
      <span class="tool-group-chevron">${icon('chevron-right', 14)}</span>
    </div>
    <div class="tool-group-details">
      ${pendingTools.map(t => `<div class="tool-group-item"><span class="tool-item-name">${escapeHtml(t.name)}</span>${t.input ? `<pre class="tool-item-input">${escapeHtml(typeof t.input === 'string' ? t.input : JSON.stringify(t.input, null, 2))}</pre>` : ''}</div>`).join('')}
    </div>
  `;

  const container = document.getElementById('messages');
  container.insertBefore(group, typing);
  pendingTools = [];
  scrollToBottom();
}

// ── Sound ───────────────────────────────────────────────
function playNotifySound() {
  try {
    const ctx = new (window.AudioContext || window.webkitAudioContext)();
    const osc = ctx.createOscillator();
    const gain = ctx.createGain();
    osc.connect(gain); gain.connect(ctx.destination);
    osc.frequency.value = 800; gain.gain.value = 0.1;
    osc.start(); osc.stop(ctx.currentTime + 0.1);
  } catch { }
}

// ── Markdown Rendering ──────────────────────────────────
function renderMarkdown(text) {
  return text
    .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
    .replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>')
    .replace(/`([^`]+)`/g, '<code>$1</code>')
    .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
    .replace(/\*(.+?)\*/g, '<em>$1</em>')
    .replace(/^- (.+)$/gm, '• $1')
    .replace(/\n/g, '<br>');
}

function escapeHtml(s) {
  if (!s) return '';
  const div = document.createElement('div');
  div.textContent = s;
  return div.innerHTML;
}

function timeStr() {
  return new Date().toLocaleTimeString(getLang() === 'de' ? 'de-DE' : 'en-US', { hour: '2-digit', minute: '2-digit' });
}

// ── Reply State ─────────────────────────────────────────
let _replyTo = null;

function setReply(msgIndex, text, role) {
  _replyTo = { msgIndex, text, role };
  const preview = document.getElementById('reply-preview');
  const previewText = document.getElementById('reply-preview-text');
  const sender = role === 'user' ? t('chat.reply.you') : t('chat.reply.bot');
  previewText.textContent = `↩ ${sender}: ${text.substring(0, 120)}${text.length > 120 ? '…' : ''}`;
  preview.style.display = 'flex';
  document.getElementById('chat-input').focus();
}

function clearReply() {
  _replyTo = null;
  document.getElementById('reply-preview').style.display = 'none';
}

// ── File Upload State ───────────────────────────────────
let _pendingFile = null;

function handleFileSelect(files) {
  if (!files || !files.length) return;
  const file = files[0];
  const reader = new FileReader();
  reader.onload = () => {
    _pendingFile = { name: file.name, type: file.type, size: file.size, dataUrl: reader.result };
    const preview = document.getElementById('file-preview');
    const previewText = document.getElementById('file-preview-text');
    const sizeKb = (file.size / 1024).toFixed(1);
    previewText.innerHTML = `${icon('paperclip', 14)} ${file.name} (${sizeKb} KB)`;
    preview.style.display = 'flex';
  };
  reader.readAsDataURL(file);
}

function clearFileUpload() {
  _pendingFile = null;
  document.getElementById('file-preview').style.display = 'none';
  document.getElementById('file-upload').value = '';
}

// ── Drag & Drop ─────────────────────────────────────────
function initDragDrop() {
  const msgs = document.getElementById('messages');
  if (!msgs) return;
  let dragCounter = 0;
  msgs.setAttribute('data-drop-text', `${t('chat.file.drop')}`);
  msgs.addEventListener('dragenter', (e) => { e.preventDefault(); dragCounter++; msgs.classList.add('drag-over'); });
  msgs.addEventListener('dragleave', (e) => { e.preventDefault(); dragCounter--; if (dragCounter <= 0) { dragCounter = 0; msgs.classList.remove('drag-over'); } });
  msgs.addEventListener('dragover', (e) => { e.preventDefault(); });
  msgs.addEventListener('drop', (e) => {
    e.preventDefault(); dragCounter = 0; msgs.classList.remove('drag-over');
    if (e.dataTransfer?.files?.length) handleFileSelect(e.dataTransfer.files);
  });
}

// ── Chat ────────────────────────────────────────────────
let _msgCounter = 0;

function addMessage(role, text, customTime, skipSave) {
  const msgIdx = _msgCounter++;
  const el = document.createElement('div');
  el.className = 'msg ' + role;
  el.dataset.msgIndex = msgIdx;

  if (role === 'user' && _replyTo && !skipSave) {
    const quote = document.createElement('div');
    quote.className = 'reply-quote';
    const sender = _replyTo.role === 'user' ? t('chat.reply.you') : t('chat.reply.bot');
    quote.textContent = `${sender}: ${_replyTo.text.substring(0, 100)}`;
    el.appendChild(quote);
  }

  if (role === 'user' && _pendingFile && !skipSave) {
    const badge = document.createElement('div');
    badge.className = 'file-badge';
    badge.innerHTML = `${icon('paperclip', 12)} ${escapeHtml(_pendingFile.name)}`;
    el.appendChild(badge);
  }

  const textEl = document.createElement('span');
  textEl.className = 'msg-text';
  if (role === 'assistant') {
    textEl.innerHTML = renderMarkdown(text);
  } else {
    textEl.textContent = text;
  }
  el.appendChild(textEl);

  if (role !== 'system') {
    const time = document.createElement('span');
    time.className = 'time';
    time.textContent = customTime || timeStr();
    el.appendChild(time);

    const replyBtn = document.createElement('button');
    replyBtn.className = 'msg-reply-btn';
    replyBtn.innerHTML = `${icon('chevron-right', 12)} ${t('chat.reply')}`;
    replyBtn.title = t('chat.reply');
    replyBtn.onclick = () => setReply(msgIdx, text, role);
    el.appendChild(replyBtn);
  }

  const container = document.getElementById('messages');
  container.insertBefore(el, document.getElementById('typing-indicator'));
  if (!skipSave) scrollToBottom();
  return el;
}

function scrollToBottom() {
  const msgs = document.getElementById('messages');
  msgs.scrollTop = msgs.scrollHeight;
}

function sendMessage() {
  const input = document.getElementById('chat-input');
  const text = input.value.trim();
  if ((!text && !_pendingFile) || !ws || ws.readyState !== 1) return;

  const effort = document.getElementById('chat-effort')?.value;
  let fullText = text || '';
  let replyContext = null;
  if (_replyTo) {
    replyContext = { role: _replyTo.role, text: _replyTo.text };
    const replyLabel = _replyTo.role === 'user' ? 'my' : 'your';
    fullText = `[Reply to ${replyLabel} message: "${_replyTo.text.substring(0, 300)}"]\n\n${fullText}`;
  }
  let fileInfo = null;
  if (_pendingFile) {
    fileInfo = { name: _pendingFile.name, type: _pendingFile.type, size: _pendingFile.size };
    const fileRef = `[File attached: ${_pendingFile.name} (${_pendingFile.type}, ${(_pendingFile.size/1024).toFixed(1)} KB)]`;
    fullText = fullText ? `${fileRef}\n\n${fullText}` : fileRef;
  }

  const tm = timeStr();
  addMessage('user', text || `${_pendingFile?.name || 'File'}`, tm);
  chatMessages.push({ role: 'user', text: fullText, time: tm, replyTo: replyContext, file: fileInfo });
  saveChatToStorage();

  const payload = { type: 'chat', text: fullText, effort };
  if (_pendingFile) payload.file = { name: _pendingFile.name, type: _pendingFile.type, dataUrl: _pendingFile.dataUrl };
  ws.send(JSON.stringify(payload));

  input.value = '';
  input.style.height = 'auto';
  clearReply();
  clearFileUpload();
  document.getElementById('send-btn').disabled = true;
  document.getElementById('typing-indicator').classList.add('visible');
  scrollToBottom();
}

function resetChat() {
  if (ws && ws.readyState === 1) ws.send(JSON.stringify({ type: 'reset' }));
  chatMessages = [];
  clearChatStorage();
}

function exportChat(format = 'markdown') {
  if (chatMessages.length === 0) { toast(t('chat.no.export'), 'error'); return; }
  fetch(API + '/api/chat/export', {
    method: 'POST', headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ messages: chatMessages, format }),
  }).then(res => res.text()).then(text => {
    const ext = format === 'json' ? 'json' : 'md';
    const blob = new Blob([text], { type: 'text/plain' });
    const a = document.createElement('a');
    a.href = URL.createObjectURL(blob);
    a.download = `alvin-bot-chat-${new Date().toISOString().slice(0,10)}.${ext}`;
    a.click();
    toast(t('chat.exported'));
  });
}

function bindChatEvents() {
  const chatInput = document.getElementById('chat-input');
  if (!chatInput) return;

  // Remove existing listeners by replacing element (clean slate)
  const newInput = chatInput.cloneNode(true);
  chatInput.parentNode.replaceChild(newInput, chatInput);

  newInput.addEventListener('keydown', (e) => {
    if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
  });
  newInput.addEventListener('input', function() {
    this.style.height = 'auto';
    this.style.height = Math.min(this.scrollHeight, 140) + 'px';
  });
}

// Global keyboard shortcuts
document.addEventListener('keydown', (e) => {
  if (e.key === 'n' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); resetChat(); }
  if (e.key === 'e' && (e.metaKey || e.ctrlKey) && e.shiftKey) { e.preventDefault(); exportChat(); }
  if (e.key === 'k' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); openCommandPalette(); }
});

// ── Dashboard ───────────────────────────────────────────
async function loadDashboard() {
  const res = await fetch(API + '/api/status');
  const data = await res.json();
  const tok = data.tokens || {};
  const fmtTokens = (n) => n >= 1000000 ? (n/1000000).toFixed(1) + 'M' : n >= 1000 ? (n/1000).toFixed(1) + 'k' : String(n || 0);
  // Setup status cards (only show if something is unconfigured)
  const setup = data.setup || {};
  let setupHtml = '';
  if (!setup.telegram || !setup.provider) {
    const tgStatus = setup.telegram
      ? `<span style="color:var(--green)">✅ Connected</span>`
      : `<span style="color:var(--yellow)">⚠️ Not configured</span> — <a href="#" onclick="navigateTo('platforms');return false" style="color:var(--accent)">Set up</a>`;
    const aiStatus = setup.provider
      ? `<span style="color:var(--green)">✅ ${data.model.name}</span>`
      : `<span style="color:var(--yellow)">⚠️ Not configured</span> — <a href="#" onclick="navigateTo('models');return false" style="color:var(--accent)">Set up</a>`;
    setupHtml = `
      <div class="card" style="grid-column:1/-1;border:1px solid var(--yellow);background:var(--bg2)">
        <h3>${icon('alert-triangle', 14)} Setup Status</h3>
        <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:8px">
          <div><strong>Telegram:</strong> ${tgStatus}</div>
          <div><strong>AI Provider:</strong> ${aiStatus}</div>
        </div>
      </div>
    `;
  }

  document.getElementById('dashboard-cards').innerHTML = `
    ${setupHtml}
    <div class="card"><h3>${icon('bot', 14)} ${t('dashboard.model')}</h3><div class="value">${data.model.name}</div><div class="sub">${data.model.model}</div></div>
    <div class="card"><h3>${icon('clock', 14)} ${t('dashboard.uptime')}</h3><div class="value">${Math.floor(data.bot.uptime/3600)}h ${Math.floor(data.bot.uptime%3600/60)}m</div><div class="sub">v${data.bot.version}</div></div>
    <div class="card"><h3>${icon('zap', 14)} ${t('dashboard.tokens')}</h3><div class="value">${fmtTokens(tok.total)}</div><div class="sub">${fmtTokens(tok.totalInput)} ${t('dashboard.tokens.in')} · ${fmtTokens(tok.totalOutput)} ${t('dashboard.tokens.out')} · $${(tok.totalCost || 0).toFixed(4)}</div></div>
    <div class="card"><h3>${icon('brain', 14)} ${t('dashboard.memory')}</h3><div class="value">${data.memory.dailyLogs} ${t('dashboard.memory.days')}</div><div class="sub">${data.memory.vectors} ${t('dashboard.memory.vectors')} · ${data.memory.todayEntries} ${t('dashboard.memory.today')}</div></div>
    <div class="card"><h3>${icon('plug', 14)} ${t('dashboard.plugins')}</h3><div class="value">${data.plugins}</div><div class="sub">${t('dashboard.plugins.loaded')}</div></div>
    <div class="card"><h3>${icon('wrench', 14)} ${t('dashboard.mcp')}</h3><div class="value">${data.mcp}</div><div class="sub">${t('dashboard.mcp.servers')}</div></div>
    <div class="card"><h3>${icon('users', 14)} ${t('dashboard.users')}</h3><div class="value">${data.users}</div><div class="sub">${t('dashboard.users.profiles')}</div></div>
  `;
  document.getElementById('model-badge').textContent = data.model.name;
}

// ── Models / Providers ──────────────────────────────────
async function loadModels() {
  const [modelsRes, setupRes] = await Promise.all([
    fetch(API + '/api/models'),
    fetch(API + '/api/providers/setup'),
  ]);
  const modelsData = await modelsRes.json();
  const setupData = await setupRes.json();

  const sel = document.getElementById('chat-model');
  if (sel) {
    sel.innerHTML = modelsData.models.map(m =>
      `<option value="${m.key}" ${m.active ? 'selected' : ''}>${m.name}</option>`
    ).join('');
  }

  let html = `<div style="margin-bottom:20px"><h3 style="font-size:1em;margin-bottom:4px;display:flex;align-items:center;gap:8px">${icon('bot', 20)} ${t('models.title')}</h3><div class="sub">${t('models.desc')}</div></div>`;

  for (const p of setupData.providers) {
    const statusBadge = p.hasKey
      ? `<span class="badge badge-green">${icon('circle-check', 12)} ${t('models.key.set')}</span>`
      : (p.free ? `<span class="badge badge-yellow">${icon('zap', 12)} ${t('models.free')}</span>` : `<span class="badge badge-red">${icon('circle-x', 12)} ${t('models.key.none')}</span>`);

    html += `<div class="card setup-card" style="margin-bottom:16px">
      <div style="display:flex;align-items:center;gap:10px;margin-bottom:12px">
        <span style="font-size:1.5em">${p.icon}</span>
        <div style="flex:1">
          <h3 style="font-size:0.95em;text-transform:none;letter-spacing:0">${p.name}</h3>
          <div class="sub">${p.description}</div>
        </div>
        ${statusBadge}
      </div>`;

    html += `<details style="margin-bottom:12px"><summary style="cursor:pointer;color:var(--accent2);font-size:0.82em;font-weight:500;display:flex;align-items:center;gap:4px">${icon('clipboard', 14)} ${t('models.setup.guide')}</summary><ol style="margin:8px 0 0 16px;color:var(--fg2);font-size:0.82em;line-height:1.6">`;
    for (const step of p.setupSteps) html += `<li>${step}</li>`;
    if (p.signupUrl) html += `<li><a href="${p.signupUrl}" target="_blank" style="color:var(--accent2)">${p.signupUrl}</a></li>`;
    html += `</ol></details>`;

    if (p.envKey) {
      html += `<div style="display:flex;gap:8px;align-items:center;margin-bottom:8px">
        <input type="password" id="key-${p.id}" placeholder="${t('models.key.placeholder')}" value="${p.keyPreview}" style="flex:1;background:var(--bg3);border:1px solid var(--glass-border);border-radius:6px;padding:8px 12px;color:var(--fg);font:inherit;font-size:0.85em;font-family:monospace;outline:none">
        <button class="btn btn-sm" onclick="saveProviderKey('${p.id}')">${icon('save', 12)} ${t('models.key.save')}</button>
        <button class="btn btn-sm btn-outline" onclick="testProviderKey('${p.id}')">${icon('test-tube', 12)} ${t('models.key.test')}</button>
      </div>
      <div id="key-result-${p.id}" style="font-size:0.78em;margin-bottom:8px"></div>`;
    }

    if (p.hasKey && p.id !== 'claude-sdk' && p.id !== 'ollama') {
      html += `<details id="live-models-${p.id}" style="margin-bottom:8px">
        <summary onclick="loadLiveModels('${p.id}')" style="cursor:pointer;color:var(--accent2);font-size:0.82em;font-weight:500;display:flex;align-items:center;gap:4px">${icon('search', 14)} ${t('models.live.title')}</summary>
        <div id="live-models-list-${p.id}" style="margin-top:8px;max-height:300px;overflow-y:auto;font-size:0.82em">${t('models.live.loading')}</div>
      </details>`;
    }

    html += `<div style="border-top:1px solid var(--glass-border);padding-top:8px;margin-top:4px">`;
    for (const m of p.modelsActive) {
      const isActive = m.active;
      html += `<div style="display:flex;align-items:center;gap:8px;padding:6px 0;font-size:0.85em">
        <span style="width:20px;text-align:center">${isActive ? icon('circle-check', 14, 'style="color:var(--green)"') : (m.registered ? icon('circle-dot', 14) : icon('circle-dot', 14, 'style="opacity:0.3"'))}</span>
        <span style="flex:1;font-family:monospace">${m.name} <span style="color:var(--fg2)">(${m.model})</span></span>
        ${isActive ? `<span class="badge badge-green">${t('active')}</span>` : `<button class="btn btn-sm btn-outline" onclick="switchModel('${m.key}')">${t('models.activate')}</button>`}
      </div>`;
    }
    html += `</div></div>`;
  }

  // Custom Models section
  html += `<div class="card" style="margin-bottom:16px">
    <div style="display:flex;align-items:center;gap:10px;margin-bottom:12px">
      <span style="display:flex">${icon('wrench', 24)}</span>
      <div style="flex:1">
        <h3 style="font-size:0.95em;text-transform:none;letter-spacing:0">${t('models.custom')}</h3>
        <div class="sub">${t('models.custom.desc')}</div>
      </div>
    </div>`;

  if (setupData.customModels.length > 0) {
    for (const cm of setupData.customModels) {
      html += `<div style="display:flex;align-items:center;gap:8px;padding:6px 0;font-size:0.85em;border-bottom:1px solid var(--glass-border)">
        <span style="font-family:monospace;flex:1">${cm.name} <span style="color:var(--fg2)">(${cm.model})</span></span>
        <span class="badge">${cm.baseUrl}</span>
        <button class="btn btn-sm btn-outline" onclick="switchModel('${cm.key}')">${t('models.activate')}</button>
        <button class="btn btn-sm btn-outline" style="color:var(--red)" onclick="removeCustomModel('${cm.key}')">${icon('x', 12)}</button>
      </div>`;
    }
  }

  html += `<button class="btn btn-sm" style="margin-top:12px" onclick="showAddCustomModel()">${icon('plus', 14)} ${t('models.custom.add')}</button>
    <div id="custom-model-form" style="display:none;margin-top:12px;padding:12px;background:var(--bg3);border-radius:8px">
      <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px">
        <input id="cm-key" placeholder="${t('models.custom.key')}" style="background:var(--bg);border:1px solid var(--bg4);border-radius:6px;padding:8px;color:var(--fg);font:inherit;font-size:0.82em">
        <input id="cm-name" placeholder="${t('models.custom.name')}" style="background:var(--bg);border:1px solid var(--bg4);border-radius:6px;padding:8px;color:var(--fg);font:inherit;font-size:0.82em">
        <input id="cm-model" placeholder="${t('models.custom.model')}" style="background:var(--bg);border:1px solid var(--bg4);border-radius:6px;padding:8px;color:var(--fg);font:inherit;font-size:0.82em">
        <input id="cm-url" placeholder="${t('models.custom.url')}" style="background:var(--bg);border:1px solid var(--bg4);border-radius:6px;padding:8px;color:var(--fg);font:inherit;font-size:0.82em">
        <input id="cm-apikey-env" placeholder="${t('models.custom.envkey')}" style="background:var(--bg);border:1px solid var(--bg4);border-radius:6px;padding:8px;color:var(--fg);font:inherit;font-size:0.82em">
        <input id="cm-apikey" type="password" placeholder="${t('models.custom.apikey')}" style="background:var(--bg);border:1px solid var(--bg4);border-radius:6px;padding:8px;color:var(--fg);font:inherit;font-size:0.82em">
      </div>
      <div style="display:flex;gap:8px">
        <button class="btn btn-sm" onclick="addCustomModel()">${icon('save', 12)} ${t('save')}</button>
        <button class="btn btn-sm btn-outline" onclick="document.getElementById('custom-model-form').style.display='none'">${t('cancel')}</button>
      </div>
    </div>
  </div>`;

  // Fallback chain
  html += `<div class="card">
    <h3 style="font-size:0.85em;text-transform:none;margin-bottom:8px;display:flex;align-items:center;gap:6px">${icon('layers', 16)} ${t('models.fallback')}</h3>
    <div class="sub" style="margin-bottom:12px">${t('models.fallback.desc')}</div>
    <div id="fallback-list" style="font-size:0.85em">${t('loading')}</div>
  </div>`;

  // Append Primary Provider manager card
  html += `<section class="card" style="max-width:680px;margin-top:16px">
    <h3 style="margin:0 0 4px">Primary provider</h3>
    <p class="hint" style="margin:0 0 12px">Switch the active provider, add a key, or log in — takes effect immediately.</p>
    <div id="provider-manager" role="list"></div>
  </section>`;

  document.getElementById('models-setup').innerHTML = html;
  loadFallbackOrder();
  renderProviderManager();
}

async function renderProviderManager() {
  const host = document.getElementById('provider-manager');
  if (!host) return;
  let data;
  try {
    const res = await fetch(API + '/api/providers/setup-status');
    data = await res.json();
  } catch { return; }
  host.innerHTML = '';
  const COLORS = { ready: '#16a34a', 'needs-key': '#d97706', 'needs-login': '#d97706', offline: '#6b7280' };
  const LABELS = { ready: 'Ready', 'needs-key': 'Needs key', 'needs-login': 'Needs login', offline: 'Offline' };
  for (const p of data.providers) {
    const row = document.createElement('div');
    row.setAttribute('role', 'listitem');
    row.style.cssText = 'display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:10px;border-bottom:1px solid var(--border,#e5e7eb)';
    row.innerHTML =
      `<span aria-hidden="true" style="color:${COLORS[p.readiness]}">●</span>` +
      `<span style="flex:1;min-width:160px"><strong>${p.name}</strong>${p.isPrimary ? ' <em>(primary)</em>' : ''}` +
      `<br><small style="color:#475569">${LABELS[p.readiness]} · ${p.modelName}</small></span>`;
    const action = document.createElement('button');
    action.style.cssText = 'cursor:pointer;min-height:44px;padding:8px 14px';
    if (p.readiness === 'ready' && !p.isPrimary) {
      action.textContent = 'Make primary';
      action.onclick = () => makePrimary(p.key, action);
    } else if (p.readiness === 'needs-key') {
      action.textContent = 'Add key';
      action.onclick = () => promptKey(p, row);
    } else if (p.readiness === 'needs-login') {
      action.textContent = 'Log in';
      action.onclick = () => startClaudeLogin(p, row);
    } else {
      action.disabled = true;
      action.textContent = p.isPrimary ? 'Primary' : '—';
    }
    row.appendChild(action);
    host.appendChild(row);
  }
}

async function makePrimary(key, btn) {
  btn.disabled = true;
  const old = btn.textContent;
  btn.textContent = 'Switching…';
  try {
    const r = await fetch(API + '/api/providers/set-primary', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ key }),
    });
    if (r.ok) {
      renderProviderManager();
    } else {
      const e = await r.json().catch(() => ({}));
      toast(e.hint || 'Switch failed', 'error');
      btn.disabled = false;
      btn.textContent = old;
    }
  } catch (err) {
    toast(err.message || 'Switch failed', 'error');
    btn.disabled = false;
    btn.textContent = old;
  }
}

function promptKey(p, row) {
  const f = document.createElement('div');
  f.style.cssText = 'display:flex;gap:8px;width:100%;margin-top:8px';
  f.innerHTML = `<input type="password" placeholder="${p.keyEnv}" style="flex:1;min-height:44px;padding:8px"><button style="cursor:pointer;min-height:44px">Save</button>`;
  const input = f.querySelector('input');
  const save = f.querySelector('button');
  save.onclick = async () => {
    save.disabled = true;
    save.textContent = 'Saving…';
    try {
      const r = await fetch(API + '/api/providers/set-key', {
        method: 'POST', headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ providerId: p.key, apiKey: input.value }),
      });
      if (r.ok) {
        renderProviderManager();
      } else {
        toast('Save failed', 'error');
        save.disabled = false;
        save.textContent = 'Save';
      }
    } catch (err) {
      toast(err.message || 'Save failed', 'error');
      save.disabled = false;
      save.textContent = 'Save';
    }
  };
  row.appendChild(f);
  input.focus();
}

async function startClaudeLogin(p, row) {
  let r;
  try {
    const res = await fetch(API + '/api/providers/login/start', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ family: p.family }),
    });
    r = await res.json();
  } catch (err) {
    toast(err.message || 'Login start failed', 'error');
    return;
  }
  if (!r.ok) { toast(r.error || 'Login not available', 'error'); return; }
  // Guided: `claude setup-token` must run in a terminal on the host — it opens the
  // browser itself and uses a localhost callback, so it can't be driven from a
  // remote browser. Show the instruction inline and poll until the provider
  // becomes available, then refresh so the user can switch to it.
  const box = document.createElement('div');
  box.style.cssText = 'width:100%;margin-top:8px;font-size:0.9em';
  box.innerHTML = `<p style="margin:0 0 6px">${r.instructions || 'Run <code>claude setup-token</code> in a terminal on the machine running Alvin.'}</p><p style="margin:0;color:#475569">Waiting for login to complete…</p>`;
  row.appendChild(box);
  const poll = setInterval(async () => {
    try {
      const s = await (await fetch(API + '/api/providers/login/poll', {
        method: 'POST', headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({}),
      })).json();
      if (s.done) { clearInterval(poll); toast('Claude is ready', 'success'); renderProviderManager(); }
    } catch { /* ignore poll errors */ }
  }, 3000);
  setTimeout(() => clearInterval(poll), 600000); // stop polling after 10 min
}

async function loadFallbackOrder() {
  try {
    const res = await fetch(API + '/api/fallback');
    const data = await res.json();
    const container = document.getElementById('fallback-list');
    if (!container) return;

    const primary = data.order?.primary || data.activeProvider || '(not set)';
    const chain = data.order?.fallbacks || [];
    const healthArr = data.health || [];
    const health = {};
    healthArr.forEach(h => { health[h.key] = h; });

    let html = `<div style="display:flex;align-items:center;gap:8px;padding:8px 12px;background:var(--bg3);border-radius:6px;margin-bottom:6px">
      ${icon('crown', 16)}
      <span style="flex:1;font-family:monospace;font-weight:600">${primary}</span>
      <span class="badge badge-green">${t('models.fallback.primary')}</span>
      ${health[primary] ? `<span style="font-size:0.78em;color:${health[primary].healthy ? 'var(--green)' : 'var(--red)'}">● ${health[primary].healthy ? t('models.fallback.healthy') : t('models.fallback.unhealthy')}</span>` : ''}
    </div>`;

    if (chain.length === 0) {
      html += `<div style="padding:8px 12px;color:var(--fg2);font-style:italic">${t('models.fallback.none')}</div>`;
    }

    chain.forEach((key, i) => {
      const h = health[key];
      const healthDot = h ? `<span style="font-size:0.78em;color:${h.healthy ? 'var(--green)' : 'var(--red)'}">● ${h.healthy ? t('models.fallback.healthy') : t('models.fallback.unhealthy')}</span>` : '';
      html += `<div style="display:flex;align-items:center;gap:8px;padding:6px 12px;background:var(--bg2);border-radius:6px;margin-bottom:4px">
        <span style="color:var(--fg2);min-width:20px;text-align:center">${i + 1}.</span>
        <span style="flex:1;font-family:monospace">${key}</span>
        ${healthDot}
        <button class="btn btn-sm btn-outline" style="padding:2px 8px;font-size:0.8em" onclick="moveFallback('${key}','up')" ${i === 0 ? 'disabled style="opacity:0.3;padding:2px 8px;font-size:0.8em"' : ''}>${icon('arrow-up', 12)}</button>
        <button class="btn btn-sm btn-outline" style="padding:2px 8px;font-size:0.8em" onclick="moveFallback('${key}','down')" ${i === chain.length - 1 ? 'disabled style="opacity:0.3;padding:2px 8px;font-size:0.8em"' : ''}>${icon('arrow-down', 12)}</button>
        <button class="btn btn-sm btn-outline" style="padding:2px 8px;font-size:0.8em;color:var(--red)" onclick="removeFallback('${key}')">${icon('x', 12)}</button>
      </div>`;
    });

    const allProviders = ['claude-sdk','groq','openai','google','nvidia-llama-3.3-70b','nvidia-kimi-k2.5','openrouter'];
    const available = allProviders.filter(p => p !== primary && !chain.includes(p));
    if (available.length > 0) {
      html += `<div style="margin-top:8px;display:flex;gap:8px;align-items:center">
        <select id="fallback-add-select" style="flex:1;background:var(--bg3);border:1px solid var(--glass-border);border-radius:6px;padding:6px 10px;color:var(--fg);font:inherit;font-size:0.85em">
          ${available.map(p => `<option value="${p}">${p}</option>`).join('')}
        </select>
        <button class="btn btn-sm" onclick="addFallback()">${icon('plus', 12)} ${t('models.fallback.add')}</button>
      </div>`;
    }

    container.innerHTML = html;
  } catch (err) {
    const container = document.getElementById('fallback-list');
    if (container) container.innerHTML = `<span style="color:var(--red)">${t('error')}: ${err.message}</span>`;
  }
}

async function moveFallback(key, direction) {
  await fetch(API + '/api/fallback/move', {
    method: 'POST', headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ key, direction }),
  });
  loadFallbackOrder();
}

async function removeFallback(key) {
  const res = await fetch(API + '/api/fallback');
  const data = await res.json();
  const primary = data.order?.primary || data.activeProvider;
  const newChain = (data.order?.fallbacks || []).filter(k => k !== key);
  await fetch(API + '/api/fallback', {
    method: 'POST', headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ primary, fallbacks: newChain }),
  });
  toast(t('models.fallback.removed', { key }));
  loadFallbackOrder();
}

async function addFallback() {
  const sel = document.getElementById('fallback-add-select');
  if (!sel) return;
  const key = sel.value;
  const res = await fetch(API + '/api/fallback');
  const data = await res.json();
  const primary = data.order?.primary || data.activeProvider;
  const newChain = [...(data.order?.fallbacks || []), key];
  await fetch(API + '/api/fallback', {
    method: 'POST', headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ primary, fallbacks: newChain }),
  });
  toast(t('models.fallback.added', { key }));
  loadFallbackOrder();
}

const _liveModelsCache = {};
async function loadLiveModels(providerId) {
  const container = document.getElementById('live-models-list-' + providerId);
  if (!container) return;
  if (_liveModelsCache[providerId] && Date.now() - _liveModelsCache[providerId].ts < 60000) {
    renderLiveModels(providerId, _liveModelsCache[providerId].models, _liveModelsCache[providerId].recommendedId, container);
    return;
  }
  container.innerHTML = `<span style="color:var(--fg2)">${t('models.live.loading')}</span>`;
  try {
    const res = await fetch(API + '/api/providers/live-models?id=' + providerId);
    const data = await res.json();
    if (!data.ok || !data.models?.length) {
      container.innerHTML = `<span style="color:var(--fg2)">${t('models.live.none')}</span>`;
      return;
    }
    // Prefer the newest-first, tier-annotated `ranked` list (parity with the
    // terminal wizard); fall back to the plain alphabetical list on old servers.
    const list = (data.ranked && data.ranked.length) ? data.ranked : data.models;
    _liveModelsCache[providerId] = { models: list, recommendedId: data.recommendedId, ts: Date.now() };
    renderLiveModels(providerId, list, data.recommendedId, container);
  } catch (err) {
    container.innerHTML = `<span style="color:var(--red)">${t('error')}: ${err.message}</span>`;
  }
}

// Small colored chip for a ranked model's tier/state badges.
function _modelBadgeChips(m) {
  const chips = [];
  if (m.recommended) chips.push(`<span class="badge badge-yellow" style="font-size:0.72em">★ ${t('wizard.step2.recommended')}</span>`);
  if (m.newest) chips.push(`<span class="badge badge-green" style="font-size:0.72em">🆕 newest</span>`);
  if (m.tier === 'flagship') chips.push(`<span class="badge badge-purple" style="font-size:0.72em">💎 flagship</span>`);
  else if (m.tier === 'fast') chips.push(`<span class="badge badge-blue" style="font-size:0.72em">⚡ fast</span>`);
  return chips.join(' ');
}

function renderLiveModels(providerId, models, recommendedId, container) {
  const countInfo = models.length > 20 ? ` <span style="color:var(--fg2)">(${t('models.live.count', { count: models.length })})</span>` : '';
  let html = `<div style="margin-bottom:6px;font-weight:500">${t('models.live.available')}${countInfo}:</div>`;
  html += '<div style="display:flex;flex-direction:column;gap:2px">';
  for (const m of models) {
    // `ranked` rows carry a friendly name + tier/badges; plain rows fall back.
    const isRanked = typeof m.tier === 'string';
    const friendly = (m.name && m.name !== m.id) ? m.name : m.id;
    const badges = isRanked ? _modelBadgeChips(m) : (m.recommended || m.id === recommendedId ? `<span class="badge badge-yellow" style="font-size:0.72em">★ ${t('wizard.step2.recommended')}</span>` : '');
    html += `<div style="display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:4px;background:var(--bg2)">
      <div style="flex:1;min-width:0">
        <div style="font-weight:500;font-size:0.9em">${friendly}</div>
        <div style="font-family:monospace;font-size:0.76em;color:var(--fg2)">${m.id}</div>
      </div>
      ${badges}
      <button class="btn btn-sm btn-outline" style="padding:1px 8px;font-size:0.78em" onclick="activateLiveModel('${providerId}','${m.id}','${(friendly).replace(/'/g,"\\'")}')"> ${t('models.activate')}</button>
    </div>`;
  }
  html += '</div>';
  container.innerHTML = html;
}

async function activateLiveModel(providerId, modelId, modelName) {
  const baseUrls = {
    anthropic: 'https://api.anthropic.com/v1/', openai: 'https://api.openai.com/v1',
    google: 'https://generativelanguage.googleapis.com/v1beta/openai', groq: 'https://api.groq.com/openai/v1',
    nvidia: 'https://integrate.api.nvidia.com/v1', openrouter: 'https://openrouter.ai/api/v1',
  };
  const apiKeyEnvs = {
    anthropic: 'ANTHROPIC_API_KEY', openai: 'OPENAI_API_KEY', google: 'GOOGLE_API_KEY',
    groq: 'GROQ_API_KEY', nvidia: 'NVIDIA_API_KEY', openrouter: 'OPENROUTER_API_KEY',
  };
  const key = modelId.replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-');
  const res = await fetch(API + '/api/providers/add-custom', {
    method: 'POST', headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ key, name: modelName || modelId, model: modelId, baseUrl: baseUrls[providerId] || '', apiKeyEnv: apiKeyEnvs[providerId] || '', type: 'openai-compatible' }),
  });
  const data = await res.json();
  if (data.ok) {
    await fetch(API + '/api/models/switch', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key }) });
    toast(`${modelName || modelId} ${t('models.activated')}`);
    loadModels();
  } else {
    toast(data.error || t('models.activate.error'), 'error');
  }
}

async function switchModel(key) {
  await fetch(API + '/api/models/switch', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key }) });
  loadModels(); loadDashboard();
  toast(t('models.switched'));
}

async function saveProviderKey(providerId) {
  const input = document.getElementById('key-' + providerId);
  const apiKey = input.value.trim();
  if (!apiKey || apiKey.includes('...')) { toast(t('models.key.fill'), 'error'); return; }
  const res = await fetch(API + '/api/providers/set-key', {
    method: 'POST', headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ providerId, apiKey }),
  });
  const data = await res.json();
  toast(data.ok ? t('models.key.saved') : data.error, data.ok ? 'success' : 'error');
}

async function testProviderKey(providerId) {
  const input = document.getElementById('key-' + providerId);
  const apiKey = input?.value?.trim() || '';
  const useStored = !apiKey || apiKey.includes('...');
  const resultDiv = document.getElementById('key-result-' + providerId);
  resultDiv.innerHTML = `<span style="color:var(--fg2)">${t('models.key.testing')}</span>`;
  const res = await fetch(API + '/api/providers/test-key', {
    method: 'POST', headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ providerId, apiKey: useStored ? '__USE_STORED__' : apiKey }),
  });
  const data = await res.json();
  resultDiv.innerHTML = data.ok
    ? `<span style="color:var(--green)">${icon('circle-check', 12)} ${t('models.key.works')}</span>`
    : `<span style="color:var(--red)">${icon('circle-x', 12)} ${data.error}</span>`;
}

function showAddCustomModel() { document.getElementById('custom-model-form').style.display = ''; }

async function addCustomModel() {
  const model = {
    key: document.getElementById('cm-key').value.trim(),
    name: document.getElementById('cm-name').value.trim(),
    model: document.getElementById('cm-model').value.trim(),
    baseUrl: document.getElementById('cm-url').value.trim(),
    apiKeyEnv: document.getElementById('cm-apikey-env').value.trim(),
    apiKey: document.getElementById('cm-apikey').value.trim(),
    type: 'openai-compatible', supportsStreaming: true,
  };
  if (!model.key || !model.name || !model.model || !model.baseUrl) {
    toast(t('models.custom.fill'), 'error'); return;
  }
  const res = await fetch(API + '/api/providers/add-custom', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(model) });
  const data = await res.json();
  if (data.ok) {
    toast(t('models.custom.added'));
    document.getElementById('custom-model-form').style.display = 'none';
    loadModels();
  } else {
    toast(data.error, 'error');
  }
}

async function removeCustomModel(key) {
  if (!confirm(t('models.custom.remove', { key }))) return;
  await fetch(API + '/api/providers/remove-custom', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key }) });
  toast(t('models.custom.removed'));
  loadModels();
}

// ── Memory ──────────────────────────────────────────────
async function loadMemory() {
  const res = await fetch(API + '/api/memory');
  const data = await res.json();
  const sel = document.getElementById('memory-file');
  sel.innerHTML = `<option value="MEMORY.md">${icon('file-text', 14)} ${t('memory.longterm')}</option>`;
  data.dailyFiles.forEach(f => { sel.innerHTML += `<option value="${f}">${icon('calendar', 14)} ${f}</option>`; });
  document.getElementById('memory-editor').value = data.longTermMemory;
}

async function loadMemoryFile() {
  const file = document.getElementById('memory-file').value;
  const url = file === 'MEMORY.md' ? '/api/memory' : '/api/memory/' + file;
  const res = await fetch(API + url);
  const data = await res.json();
  document.getElementById('memory-editor').value = data.content || data.longTermMemory || '';
}

async function saveMemoryFile() {
  const file = document.getElementById('memory-file').value;
  const content = document.getElementById('memory-editor').value;
  const res = await fetch(API + '/api/memory/save', {
    method: 'POST', headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ file, content }),
  });
  const data = await res.json();
  toast(data.ok ? t('memory.saved') : data.error, data.ok ? 'success' : 'error');
}

// ── Sessions ────────────────────────────────────────────
async function loadSessions() {
  const res = await fetch(API + '/api/sessions');
  const data = await res.json();
  const list = document.getElementById('sessions-list');
  if (data.sessions.length === 0) {
    list.innerHTML = `<div class="card"><h3>${t('sessions.none')}</h3><div class="sub">${t('sessions.none.desc')}</div></div>`;
    return;
  }
  list.innerHTML = data.sessions.map(s => {
    const dur = Math.floor((s.lastActivity - s.startedAt) / 60000);
    return `<div class="list-item"><div class="icon">${icon('message-circle', 18)}</div><div class="info">
      <div class="name">${s.name}${s.username ? ' @'+s.username : ''}</div>
      <div class="desc">${s.messageCount} ${t('sessions.msgs')} · ${s.toolUseCount} ${t('sessions.tools')} · $${s.totalCost.toFixed(4)} · ${dur}min</div>
    </div><span class="badge ${s.isProcessing ? 'badge-yellow' : 'badge-green'}">${s.isProcessing ? t('sessions.active') : t('sessions.idle')}</span></div>`;
  }).join('');
}

// ── Workspaces (v4.12.0) ────────────────────────────────
async function loadWorkspaces() {
  try {
    const res = await fetch(API + '/api/workspaces');
    const data = await res.json();
    const list = document.getElementById('workspaces-list');
    if (!list) return;
    if (!data.workspaces || data.workspaces.length === 0) {
      list.innerHTML = `<div class="hint" style="color:var(--fg2)">No workspaces yet. Create one at <code>~/.alvin-bot/workspaces/&lt;name&gt;.md</code> with YAML frontmatter (purpose, cwd, emoji, color, channels). See <code>docs/install/slack-setup.md</code>.</div>`;
      return;
    }
    list.innerHTML = data.workspaces.map(ws => {
      const emoji = ws.emoji || '🧭';
      const color = ws.color || '#6366f1';
      const purpose = ws.purpose || '<em style="opacity:0.6">(no purpose set)</em>';
      const cost = (ws.stats.totalCost || 0).toFixed(4);
      const sessionCount = ws.stats.sessionCount || 0;
      const messageCount = ws.stats.messageCount || 0;
      const channelsList = (ws.channels && ws.channels.length > 0)
        ? `<div class="sub" style="font-size:0.75em;opacity:0.6;margin-top:4px">channels: ${ws.channels.join(', ')}</div>`
        : '';
      return `
        <div class="list-item" style="border-left:4px solid ${color};padding-left:12px">
          <div class="info" style="flex:1">
            <div class="name" style="font-weight:600">${emoji} ${ws.name}</div>
            <div class="desc">${purpose}</div>
            <div class="sub" style="font-size:0.78em;opacity:0.7;margin-top:2px">cwd: <code>${ws.cwd}</code></div>
            ${channelsList}
          </div>
          <div style="text-align:right;font-family:monospace;font-size:0.85em;opacity:0.8;line-height:1.5">
            <div>${sessionCount} session${sessionCount === 1 ? '' : 's'}</div>
            <div>${messageCount} msg</div>
            <div>$${cost}</div>
          </div>
        </div>`;
    }).join('');
  } catch (err) {
    console.error('loadWorkspaces failed:', err);
  }
}

// ── Plugins ─────────────────────────────────────────────
async function loadPlugins() {
  const [pluginsRes, mcpRes, skillsRes] = await Promise.all([
    fetch(API + '/api/plugins'),
    fetch(API + '/api/mcp').catch(() => ({ json: () => ({ servers: [], tools: [], config: { servers: {} } }) })),
    fetch(API + '/api/skills').catch(() => ({ json: () => ({ skills: [] }) })),
  ]);
  const pluginsData = await pluginsRes.json();
  const mcpData = await mcpRes.json();
  const skillsData = await skillsRes.json();

  let html = '';

  // ── Plugins Section ──
  html += `<div style="margin-bottom:24px">
    <h3 style="font-size:1em;margin-bottom:8px;display:flex;align-items:center;gap:8px">${icon('plug', 18)} ${t('plugins.none').replace('Keine ','').replace('No ','') || 'Plugins'}</h3>`;
  if (pluginsData.plugins.length === 0) {
    html += `<div class="card"><div class="sub">${t('plugins.none.desc')}</div></div>`;
  } else {
    html += pluginsData.plugins.map(p => `<div class="list-item"><div class="icon">${icon('plug', 18)}</div><div class="info">
      <div class="name">${p.name} <span class="badge">${p.version}</span></div>
      <div class="desc">${p.description}${p.commands.length ? ' · '+p.commands.join(', ') : ''}</div>
    </div></div>`).join('');
  }
  html += '</div>';

  // ── MCP Servers Section ──
  html += `<div style="margin-bottom:24px">
    <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
      <h3 style="font-size:1em;display:flex;align-items:center;gap:8px;flex:1">${icon('server', 18)} MCP Servers</h3>
      <button class="btn btn-sm" onclick="showAddMCP()">${icon('plus', 14)} ${t('models.fallback.add')}</button>
      <button class="btn btn-sm btn-outline" onclick="discoverMCP()">${icon('search', 14)} Auto-Discover</button>
    </div>`;

  if (mcpData.servers.length === 0) {
    html += `<div class="card"><div class="sub">No MCP servers configured. Add one or use Auto-Discover.</div></div>`;
  } else {
    for (const s of mcpData.servers) {
      const statusBadge = s.connected
        ? `<span class="badge badge-green">${icon('check', 12)} Connected · ${s.tools} tools</span>`
        : `<span class="badge badge-red">${icon('x', 12)} Disconnected</span>`;
      html += `<div class="list-item">
        <div class="icon">${icon('server', 18)}</div>
        <div class="info"><div class="name">${escapeHtml(s.name)}</div></div>
        ${statusBadge}
        <button class="btn btn-sm btn-outline" style="color:var(--red);padding:2px 8px" onclick="removeMCP('${escapeHtml(s.name)}')">${icon('trash-2', 14)}</button>
      </div>`;
    }
  }

  // Add MCP form (hidden by default)
  html += `<div id="mcp-add-form" style="display:none;margin-top:12px;padding:16px;background:var(--bg2);border:1px solid var(--glass-border);border-radius:var(--radius)">
    <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px">
      <input id="mcp-name" placeholder="Server name (e.g. filesystem)" class="input">
      <input id="mcp-command" placeholder="Command (e.g. npx)" class="input">
      <input id="mcp-args" placeholder="Args (comma-separated, e.g. -y,@modelcontextprotocol/server-filesystem,/tmp)" class="input" style="grid-column:1/-1">
      <input id="mcp-url" placeholder="Or HTTP URL (for remote servers)" class="input" style="grid-column:1/-1">
    </div>
    <div style="display:flex;gap:8px">
      <button class="btn btn-sm" onclick="addMCP()">${icon('save', 14)} ${t('save')}</button>
      <button class="btn btn-sm btn-outline" onclick="document.getElementById('mcp-add-form').style.display='none'">${t('cancel')}</button>
    </div>
  </div>`;

  // Discovery results area
  html += `<div id="mcp-discover-results" style="margin-top:8px"></div>`;
  html += '</div>';

  // ── Skills Section ──
  html += `<div style="margin-bottom:24px">
    <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
      <h3 style="font-size:1em;display:flex;align-items:center;gap:8px;flex:1">${icon('sparkles', 18)} Skills</h3>
      <button class="btn btn-sm" onclick="showAddSkill()">${icon('plus', 14)} ${t('cron.create')}</button>
    </div>`;

  if (skillsData.skills.length === 0) {
    html += `<div class="card"><div class="sub">No skills installed. Create one to add specialized knowledge.</div></div>`;
  } else {
    for (const s of skillsData.skills) {
      html += `<div class="list-item">
        <div class="icon">${icon('sparkles', 18)}</div>
        <div class="info">
          <div class="name">${escapeHtml(s.name)} <span class="badge">${s.category}</span></div>
          <div class="desc">${escapeHtml(s.description || '')} · Triggers: ${s.triggers.join(', ')}</div>
        </div>
        <button class="btn btn-sm btn-outline" onclick="editSkill('${escapeHtml(s.id)}')">${icon('edit', 14)}</button>
        <button class="btn btn-sm btn-outline" style="color:var(--red);padding:2px 8px" onclick="deleteSkill('${escapeHtml(s.id)}')">${icon('trash-2', 14)}</button>
      </div>`;
    }
  }

  // Add Skill form (hidden)
  html += `<div id="skill-add-form" style="display:none;margin-top:12px;padding:16px;background:var(--bg2);border:1px solid var(--glass-border);border-radius:var(--radius)">
    <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px">
      <input id="skill-id" placeholder="Unique ID (e.g. video-creation)" class="input">
      <input id="skill-name" placeholder="Display name" class="input">
      <input id="skill-desc" placeholder="Short description" class="input">
      <input id="skill-triggers" placeholder="Trigger keywords (comma-separated)" class="input">
      <input id="skill-category" placeholder="Category (e.g. media, code, data)" class="input">
      <select id="skill-priority" class="input"><option value="3">Priority: Normal (3)</option><option value="5">High (5)</option><option value="1">Low (1)</option></select>
    </div>
    <textarea id="skill-content" class="editor" style="min-height:200px" placeholder="Skill content (instructions, workflows, best practices)..."></textarea>
    <div style="display:flex;gap:8px;margin-top:8px">
      <button class="btn btn-sm" onclick="createSkill()">${icon('save', 14)} ${t('save')}</button>
      <button class="btn btn-sm btn-outline" onclick="document.getElementById('skill-add-form').style.display='none'">${t('cancel')}</button>
    </div>
  </div>`;
  html += '</div>';

  document.getElementById('plugins-list').innerHTML = html;
}

// ── MCP Management Functions ────────────────────────────

