"use strict";

const $ = (sel) => document.querySelector(sel);

const messagesEl = $("#messages");
const statusEl = $("#status");
const promptEl = $("#prompt");
const formEl = $("#prompt-form");
const micBtn = $("#mic");
const ttsSel = $("#tts");
const langSel = $("#lang-sel");

// "Flex" ist im Sprachmenü nur ein weiterer Wert: keine feste Sprache, sondern
// automatische Erkennung. Eine konkrete Sprache = Fix-Modus (Eingabe wird übersetzt).
// Beide internen Felder (language, language_mode) werden aus dieser einen Auswahl abgeleitet.
function langOverrides() {
  const sel = langSel && langSel.value;
  if (!sel || sel === "flex") return { language_mode: "flex" };
  return { language: sel, language_mode: "fix" };
}

// Nutzer-Präferenzen als Request-Overrides für jeden Turn.
function overrides() {
  const out = { ...langOverrides() };
  if (isDeviceMode()) {
    out.text_only = true;                 // kein Server-Audio -> Gerät spricht selbst
  } else if (ttsSel && ttsSel.value) {
    out.tts_provider = ttsSel.value;
  }
  return out;
}

async function savePrefs(obj) {
  try {
    await fetch("/api/me/prefs", {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(obj),
    });
  } catch (e) { console.error("savePrefs", e); }
}

// ---------- Geräte-TTS (Web Speech API) ----------
// "Gerät" liest die Antwort lokal vor -> der Server schickt kein Audio (spart Bytes/Kosten).
const TTS_SUPPORTED = typeof window !== "undefined" && "speechSynthesis" in window;
const LANG_BCP47 = {
  de: "de-DE", en: "en-US", fr: "fr-FR", es: "es-ES",
  it: "it-IT", nl: "nl-NL", ru: "ru-RU", zh: "zh-CN",
};

function isMobile() {
  const ua = navigator.userAgent || "";
  return /Android|iPhone|iPad|iPod/i.test(ua) ||
    (navigator.maxTouchPoints > 1 && /Macintosh/.test(ua));  // iPad ab iOS 13
}

function isDeviceMode() { return !!ttsSel && ttsSel.value === "device"; }

let _voices = [];
function loadVoices() { _voices = TTS_SUPPORTED ? speechSynthesis.getVoices() : []; }
if (TTS_SUPPORTED) {
  loadVoices();
  // Stimmen laden asynchron -> bei Eintreffen Preset-Zustand neu bewerten (entgrauen).
  speechSynthesis.addEventListener("voiceschanged", () => { loadVoices(); refreshDevicePreset(); });
  setTimeout(() => refreshDevicePreset(), 1500);  // Backstop, falls kein voiceschanged kommt
}

function pickVoice(bcp) {
  if (!_voices.length) loadVoices();
  const lc = bcp.toLowerCase();
  const two = lc.slice(0, 2);
  return _voices.find((v) => v.lang && v.lang.toLowerCase() === lc) ||
         _voices.find((v) => v.lang && v.lang.toLowerCase().startsWith(two)) || null;
}

function _makeUtterance(text, lang) {
  const u = new SpeechSynthesisUtterance(text);
  u.lang = LANG_BCP47[lang] || lang || "de-DE";
  const v = pickVoice(u.lang);
  if (v) u.voice = v;
  return u;
}

// Hat der Browser nutzbare Sprachstimmen? (Linux-Desktops haben oft KEINE -> stumm.)
function deviceVoicesReady() {
  if (!TTS_SUPPORTED) return false;
  if (!_voices.length) loadVoices();
  return _voices.length > 0;
}

// Server-Fallback: holt Audio via /api/speak (ohne tts_provider -> Server-Default,
// niemals "device") und spielt es ab. Greift, wenn Geräte-TTS keine Stimme hat.
async function serverSpeakFallback(text, lang, btn) {
  try {
    const body = { text };
    if (lang) body.language = lang;
    const r = await fetch("/api/speak", {
      method: "POST", headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    });
    if (!r.ok) throw new Error("HTTP " + r.status);
    startReplay([await r.arrayBuffer()], btn);
  } catch (e) {
    setReplayPlaying(btn, false);
    statusEl.textContent = "Vorlesen fehlgeschlagen";
  }
}

// Vorlesen ohne Button-Status (automatisches Vorlesen der Antwort).
function deviceSpeak(text, lang) {
  if (!text) return;
  if (deviceVoicesReady()) {
    speechSynthesis.cancel();
    speechSynthesis.speak(_makeUtterance(text, lang));
  } else {
    serverSpeakFallback(text, lang, null);   // keine Stimmen -> Server-Audio
  }
}

// Vorlesen mit Button-Status (Replay): Icon -> ⏹ während des Sprechens, danach zurück.
function deviceSpeakBtn(text, lang, btn) {
  if (!text) { setReplayPlaying(btn, false); return; }
  if (!deviceVoicesReady()) { serverSpeakFallback(text, lang, btn); return; }
  speechSynthesis.cancel();
  const u = _makeUtterance(text, lang);
  u.onend = u.onerror = () => setReplayPlaying(btn, false);
  setReplayPlaying(btn, true);
  speechSynthesis.speak(u);
}

// iOS: speechSynthesis darf nur nach einer Nutzergeste starten -> beim ersten Tap freischalten.
let _ttsUnlocked = false;
function unlockTTS() {
  if (_ttsUnlocked || !TTS_SUPPORTED) return;
  try { speechSynthesis.speak(new SpeechSynthesisUtterance("")); _ttsUnlocked = true; } catch (e) {}
}

const PLAYBACK_KEY = "va-playback";  // geräte-lokal: "device" wenn Geräte-TTS gewählt

// Setzt das verborgene Quell-Select (#tts) beim Laden + spiegelt es in die Presets.
// Vorrang: 1. lokale Wahl (localStorage), 2. Server-Pref (tts_provider), 3. Mobil-Default = Gerät.
function applyPlaybackChoice(serverProvider) {
  if (!ttsSel) return;
  const local = localStorage.getItem(PLAYBACK_KEY);
  if (local === "device" && TTS_SUPPORTED) {
    ttsSel.value = "device";
  } else if (serverProvider) {
    ttsSel.value = serverProvider;
  } else if (TTS_SUPPORTED && isMobile() && local === null) {
    ttsSel.value = "device";              // Mobil-Default, solange nichts gewählt wurde
    localStorage.setItem(PLAYBACK_KEY, "device");
  } else if (!ttsSel.value) {
    ttsSel.value = "piper";               // Fallback: Server
  }
  refreshDevicePreset();                  // graut „Im Gerät" aus, wenn keine Stimmen + syncPresetUI
}

// Graut „Im Gerät" aus, wenn keine Sprachstimmen vorhanden sind; entgraut automatisch,
// sobald welche da sind. Kein Aufzwingen: device wird nur (wieder) gewählt, wenn es die
// gespeicherte Präferenz ist. Ohne Stimmen NICHT auf der deaktivierten Option sitzen bleiben.
function refreshDevicePreset() {
  const btn = document.querySelector('.tts-preset[data-tts="device"]');
  if (!btn || !ttsSel) return;
  const ready = deviceVoicesReady();
  btn.disabled = !ready;
  btn.classList.toggle("opacity-50", !ready);
  btn.classList.toggle("cursor-not-allowed", !ready);
  const sub = btn.querySelector(".tts-sub");
  if (sub) sub.textContent = ready
    ? "Das Gerät liest vor · spart Daten"
    : "keine Sprachstimmen im Browser";
  if (ready) {
    if (localStorage.getItem(PLAYBACK_KEY) === "device" && ttsSel.value !== "device") {
      ttsSel.value = "device";            // gespeicherte Präferenz wieder aktivieren
    }
  } else if (ttsSel.value === "device") {
    ttsSel.value = "piper";               // Server statt deaktivierter Option (Präferenz bleibt)
  }
  syncPresetUI();
}

// Markiert das aktive Ton-Preset (Häkchen + Rahmen) passend zu #tts.
function syncPresetUI() {
  const val = ttsSel ? ttsSel.value : "";
  document.querySelectorAll(".tts-preset").forEach((btn) => {
    const active = btn.dataset.tts === val;
    btn.classList.toggle("ring-2", active);
    btn.classList.toggle("ring-blue-500", active);
    btn.classList.toggle("border-blue-500", active);
    const chk = btn.querySelector(".check");
    if (chk) chk.style.opacity = active ? "1" : "0";
  });
}

// ---------- Einstellungs-Menü (⋮) ----------
function openMenu() {
  $("#settings-sheet").classList.remove("hidden");
  $("#settings-backdrop").classList.remove("hidden");
}
function closeMenu() {
  $("#settings-sheet").classList.add("hidden");
  $("#settings-backdrop").classList.add("hidden");
}

let busy = false;
let activeWs = null;     // aktive WS-Verbindung (fuer Barge-in von aussen erreichbar)
let activeSources = [];  // laufende AudioBufferSourceNodes (stoppbar per Barge-in)

// Session-ID pro Nutzer (sonst "gehoert einem anderen Nutzer"-Konflikt). Wird aus
// /api/me abgeleitet; bis dahin null -> ensureSession() wartet auf loadMe().
let sessionId = null;
let mePromise = null;
let currentUserId = null;
const sessionKey = (uid) => "va-session-" + uid;

// ---------- Identitaet / Menue ----------
function loadMe() {
  mePromise = (async () => {
    try {
      const res = await fetch("/api/me", { headers: { Accept: "application/json" } });
      if (!res.ok) {
        $("#identity").textContent = "Nicht angemeldet";
        return null;
      }
      const me = await res.json();
      currentUserId = me.user_id;
      // Zuletzt aktive (ggf. frische) Session bevorzugen, sonst die Basis-Session.
      sessionId = localStorage.getItem(sessionKey(me.user_id)) || ("web-" + me.user_id);
      $("#identity").textContent = "Angemeldet als " + (me.display_name || me.external_id || "Gast");
      if (me.sso_logout_url) {
        const logout = $("#logout");
        logout.href = me.sso_logout_url;
        logout.classList.remove("hidden");
      }
      if (me.is_admin) {
        $("#admin-toggle").classList.remove("hidden");
      }
      // Gespeicherte Nutzer-Präferenzen in die Dropdowns laden.
      const prefs = me.prefs || {};
      if (langSel) {
        // Flex-Modus -> "flex", sonst die feste Sprache (Fallback: Flex).
        langSel.value = prefs.language_mode === "flex"
          ? "flex"
          : (prefs.language || "flex");
      }
      applyPlaybackChoice(prefs.tts_provider);
      return me;
    } catch (e) {
      $("#identity").textContent = "Verbindung fehlgeschlagen";
      return null;
    }
  })();
  return mePromise;
}

// Stellt sicher, dass eine nutzerspezifische Session-ID feststeht, bevor ein Turn startet.
async function ensureSession() {
  if (!sessionId && mePromise) {
    try { await mePromise; } catch (e) { /* ignore */ }
  }
  if (!sessionId) sessionId = "web-anon";
}

// loadUsers() ersetzt durch Admin-Panel (siehe unten)

// ---------- Nachrichten-UI ----------
// Nach dem naechsten Layout ans Ende scrollen -> die neueste Antwort ist sichtbar.
function scrollToBottom() {
  requestAnimationFrame(() => { messagesEl.scrollTop = messagesEl.scrollHeight; });
}

const BUBBLE = {
  user: "ml-auto max-w-[80%] rounded-2xl rounded-br-sm bg-blue-600 px-4 py-2 text-white whitespace-pre-wrap",
  assistant: "mr-auto max-w-[80%] rounded-2xl rounded-bl-sm border border-slate-200 bg-white px-4 py-2 whitespace-pre-wrap dark:border-slate-700 dark:bg-slate-800",
  system: "mx-auto max-w-[90%] rounded-lg bg-amber-100 px-3 py-1.5 text-xs text-amber-800 dark:bg-amber-900/40 dark:text-amber-200",
  emergency: "mx-auto max-w-[90%] rounded-lg bg-red-100 px-3 py-1.5 text-sm font-semibold text-red-800 dark:bg-red-900/40 dark:text-red-200",
};

// Lautsprecher-/Stop-Icons als SVG (currentColor → per CSS einfärbbar, anders als Emoji).
const ICON_SPEAKER =
  '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" ' +
  'class="w-4 h-4"><path stroke-linecap="round" stroke-linejoin="round" ' +
  'd="M19.114 5.636a9 9 0 0 1 0 12.728M16.463 8.288a5.25 5.25 0 0 1 0 7.424M6.75 8.25l4.72-4.72a.75.75 0 0 1 1.28.53v15.88a.75.75 0 0 1-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 0 1 2.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75Z" /></svg>';
const ICON_STOP =
  '<svg viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4">' +
  '<rect x="6" y="6" width="12" height="12" rx="2" /></svg>';

// meta.lang: Sprache, in der diese Bubble vorgelesen werden soll (für Replay).
function addMessage(role, text, meta = {}) {
  const div = document.createElement("div");
  div.className = BUBBLE[role] || BUBBLE.assistant;
  const span = document.createElement("span");
  span.className = "bubble-text";
  span.textContent = text;
  div.appendChild(span);
  div._textEl = span;
  // Vorlese-Symbol nur an Nutzer-/Assistenten-Bubbles (nicht System/Notfall).
  if (role === "user" || role === "assistant") {
    div.classList.add("relative", "pr-7");
    div._replay = { lang: meta.lang || null, chunks: null };  // chunks: gecachtes PCM (Assistent)
    const btn = document.createElement("button");
    btn.type = "button";
    btn.innerHTML = ICON_SPEAKER;
    btn.title = "Vorlesen";
    btn.setAttribute("aria-label", "Vorlesen");
    // Kontrastreiche Farbe je Bubble: weiß auf Blau; im Dark-Mode hell auf Dunkelgrau.
    const replayColor = role === "user"
      ? "text-white/80 hover:text-white"
      : "text-slate-500 hover:text-slate-700 dark:text-slate-300 dark:hover:text-white";
    btn.className = "replay-btn absolute top-1.5 right-1.5 transition-colors " + replayColor;
    btn.addEventListener("click", () => replayBubble(div));
    div._replayBtn = btn;
    div.appendChild(btn);
  }
  messagesEl.appendChild(div);
  scrollToBottom();
  return div;
}

// ---------- Vorlesen / Replay ----------
function setReplayPlaying(btn, on) {
  if (!btn) return;
  btn.innerHTML = on ? ICON_STOP : ICON_SPEAKER;
  btn.dataset.playing = on ? "1" : "";
}

function startReplay(chunks, btn) {
  stopAudio();                       // Barge-in: laufendes Audio (auch andere Replays) stoppen
  setReplayPlaying(btn, true);
  playPcm(chunks, 24000, () => setReplayPlaying(btn, false));
}

async function speakBubble(text, lang, btn) {
  try {
    const body = { text };
    if (lang) body.language = lang;
    if (ttsSel && ttsSel.value) body.tts_provider = ttsSel.value;
    const r = await fetch("/api/speak", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    });
    if (!r.ok) throw new Error("HTTP " + r.status);
    startReplay([await r.arrayBuffer()], btn);
  } catch (e) {
    setReplayPlaying(btn, false);
    statusEl.textContent = "Vorlesen fehlgeschlagen";
  }
}

function replayBubble(div) {
  const btn = div._replayBtn;
  if (btn && btn.dataset.playing) { stopAudio(); setReplayPlaying(btn, false); return; }  // Toggle
  const info = div._replay || {};
  const text = (div._textEl && div._textEl.textContent) || "";
  if (info.chunks && info.chunks.length) {
    startReplay(info.chunks, btn);                 // Assistent: gecachtes PCM, sofort & gratis
  } else if (isDeviceMode()) {
    stopAudio();
    // Geräte-TTS: bereinigtes "spoken" bevorzugen (ohne Markdown/Emojis), sonst Rohtext.
    deviceSpeakBtn(info.spoken || text, info.lang, btn);
  } else {
    speakBubble(text, info.lang, btn);             // Server-TTS via /api/speak (bereinigt selbst)
  }
}

// ---------- Barge-in-Hilfsfunktionen ----------
function stopAudio() {
  activeSources.forEach((s) => { try { s.stop(); } catch (e) {} });
  activeSources = [];
  if (TTS_SUPPORTED) { try { speechSynthesis.cancel(); } catch (e) {} }  // Geräte-TTS stoppen
}

function setMicIdle() {
  micBtn.textContent = "🎤";
  micBtn.title = "Mikrofon";
  micBtn.classList.remove("bg-red-600", "animate-pulse", "bg-amber-500");
  micBtn.classList.add("bg-emerald-600");
}

function setMicRecording() {
  micBtn.classList.remove("bg-emerald-600", "bg-amber-500");
  micBtn.classList.add("bg-red-600", "animate-pulse");
  micBtn.title = "Aufnahme stoppen";
}

function setMicBusy() {
  micBtn.textContent = "⏹";
  micBtn.title = "KI unterbrechen (Barge-in)";
  micBtn.classList.remove("bg-emerald-600", "bg-red-600", "animate-pulse");
  micBtn.classList.add("bg-amber-500");
}

// ---------- Audio-Wiedergabe (PCM s16le) ----------
let audioCtx = null;
function playPcm(chunks, sampleRate, onended) {
  if (!chunks.length) { if (onended) onended(); return; }
  const total = chunks.reduce((n, c) => n + c.byteLength, 0);
  const merged = new Uint8Array(total);
  let off = 0;
  for (const c of chunks) { merged.set(new Uint8Array(c), off); off += c.byteLength; }
  const view = new DataView(merged.buffer);
  const n = Math.floor(merged.byteLength / 2);
  audioCtx = audioCtx || new (window.AudioContext || window.webkitAudioContext)();
  if (audioCtx.state === "suspended") audioCtx.resume();
  const buf = audioCtx.createBuffer(1, n, sampleRate || 24000);
  const ch = buf.getChannelData(0);
  for (let i = 0; i < n; i++) ch[i] = view.getInt16(i * 2, true) / 32768;
  const src = audioCtx.createBufferSource();
  src.buffer = buf;
  src.connect(audioCtx.destination);
  activeSources.push(src);
  src.onended = () => {
    activeSources = activeSources.filter((s) => s !== src);
    if (onended) onended();
  };
  src.start();
}

// ---------- WS-Turn (Text und Sprache teilen die Event-Logik) ----------
function wsUrl(path) {
  const proto = location.protocol === "https:" ? "wss" : "ws";
  return `${proto}://${location.host}${path}?session_id=${encodeURIComponent(sessionId)}`;
}

// onopen: (ws) => sendet die Eingabe. Liefert ein Promise, das beim done-Event endet.
function runTurn(path, onopen) {
  return new Promise((resolve) => {
    const ws = new WebSocket(wsUrl(path));
    activeWs = ws;
    ws.binaryType = "arraybuffer";
    const pcm = [];
    let answerEl = null;
    let sampleRate = 24000;
    // Sprache dieses Turns (für den Replay je Bubble): Flex folgt erkannter Sprache.
    let routeLang = null, routeMode = null, detectedLang = null;
    const turnLang = () =>
      (routeMode === "flex" ? (detectedLang || routeLang) : routeLang) || null;

    ws.onopen = () => onopen(ws);
    ws.onerror = () => { statusEl.textContent = "Verbindungsfehler"; resolve(); };
    ws.onclose = () => { activeWs = null; resolve(); };

    ws.onmessage = (event) => {
      if (typeof event.data !== "string") { pcm.push(event.data); return; }
      let msg;
      try { msg = JSON.parse(event.data); } catch { return; }
      switch (msg.type) {
        case "ack":
          routeLang = (msg.route && msg.route.language) || null;
          routeMode = (msg.route && msg.route.language_mode) || null;
          break;
        case "transcript":
          detectedLang = msg.detected_language || null;
          if (msg.text) addMessage("user", msg.text, { lang: turnLang() });
          break;
        case "token":
          if (!answerEl) answerEl = addMessage("assistant", "", { lang: turnLang() });
          answerEl._textEl.textContent += msg.text || "";
          scrollToBottom();
          break;
        case "semantic":
          if (!answerEl) answerEl = addMessage("assistant", "", { lang: turnLang() });
          if (msg.text) answerEl._textEl.textContent = msg.text;
          // Sprache final festhalten (für Replay) und im Geräte-Modus lokal vorlesen.
          // Für TTS das bereinigte "spoken" (ohne Markdown/Emojis) bevorzugen, anzeigen
          // bleibt der Originaltext.
          if (answerEl._replay) {
            answerEl._replay.lang = turnLang();
            answerEl._replay.spoken = msg.spoken || msg.text || "";
          }
          if (isDeviceMode()) {
            const spoken = msg.spoken || msg.text;
            if (spoken) deviceSpeak(spoken, turnLang());
          }
          scrollToBottom();
          break;
        case "emergency":
          addMessage("emergency", "⚠ Notfall erkannt (" + (msg.category || "?") + ")");
          break;
        case "done":
          sampleRate = msg.sample_rate || 24000;
          // Geräte-Modus: kein Server-Audio -> nichts abspielen/cachen (Gerät hat schon gesprochen).
          if (!isDeviceMode()) {
            if (answerEl && answerEl._replay) answerEl._replay.chunks = pcm.slice();  // Replay-Cache
            playPcm(pcm, sampleRate);
          }
          statusEl.textContent = "";
          scrollToBottom();
          ws.close();
          break;
        case "error":
          addMessage("system", "Fehler: " + (msg.detail || msg.status || "unbekannt"));
          ws.close();
          break;
        case "interrupted":
          statusEl.textContent = "";
          ws.close();  // → ws.onclose → resolve() → busy=false → setMicIdle()
          break;
      }
    };
  });
}

// ---------- Text senden ----------
async function sendText(text) {
  if (busy || !text.trim()) return;
  busy = true;
  setMicBusy();
  await ensureSession();
  // Bei fester Sprache wird der eigene Text in dieser Sprache vorgelesen; bei Flex offen.
  const lo = langOverrides();
  addMessage("user", text, { lang: lo.language_mode === "fix" ? lo.language : null });
  statusEl.textContent = "denkt …";
  const audioStream = !isDeviceMode();  // Geräte-Modus: kein Server-Audio anfordern
  await runTurn("/ws/chat", (ws) => {
    ws.send(JSON.stringify({ text, stream: true, audio_stream: audioStream, ...overrides() }));
  });
  busy = false;
  setMicIdle();
}

formEl.addEventListener("submit", (e) => {
  e.preventDefault();
  unlockTTS();  // iOS: Geräte-TTS bei der Nutzergeste freischalten
  const text = promptEl.value;
  promptEl.value = "";
  sendText(text);
});

// ---------- Mikrofon (Push-to-Talk) ----------
let mediaRecorder = null;
let recChunks = [];

async function startRecording(statusText) {
  if (busy) return;
  const md = navigator.mediaDevices;
  if (!md || !md.getUserMedia) {
    addMessage("system", "Mikrofon nicht freigegeben: Bitte die Seite direkt in Safari öffnen (nicht im In-App-Browser) und keinen Privat-Modus verwenden.");
    return;
  }
  if (typeof MediaRecorder === "undefined") {
    addMessage("system", "Aufnahme nicht unterstützt (iOS < 14.3?). Bitte iOS aktualisieren.");
    return;
  }
  let stream;
  try {
    stream = await md.getUserMedia({ audio: true });
  } catch (e) {
    addMessage("system", "Mikrofon-Zugriff fehlgeschlagen: " + (e.name || "") +
      ". Tipp: iPhone → Einstellungen → Safari → Mikrofon = Erlauben/Fragen.");
    return;
  }
  recChunks = [];
  try {
    mediaRecorder = new MediaRecorder(stream);
  } catch (e) {
    stream.getTracks().forEach((t) => t.stop());
    addMessage("system", "Aufnahme nicht möglich: " + (e.name || e.message));
    return;
  }
  mediaRecorder.ondataavailable = (e) => { if (e.data.size) recChunks.push(e.data); };
  mediaRecorder.onstop = async () => {
    stream.getTracks().forEach((t) => t.stop());
    const mime = mediaRecorder.mimeType || "audio/mp4";
    const blob = new Blob(recChunks, { type: mime });
    const bytes = await blob.arrayBuffer();
    // Safari -> audio/mp4, andere -> webm; der Server erkennt das Format am Inhalt.
    sendVoice(bytes, mime.includes("mp4") || mime.includes("mpeg") ? "mp4" : "webm");
  };
  mediaRecorder.start();
  setMicRecording();
  statusEl.textContent = statusText || "Aufnahme … (zum Stoppen erneut tippen)";
}

function stopRecording() {
  if (mediaRecorder && mediaRecorder.state !== "inactive") mediaRecorder.stop();
  setMicIdle();
}

async function sendVoice(bytes, fmt = "webm") {
  if (busy) return;
  busy = true;
  setMicBusy();
  await ensureSession();
  statusEl.textContent = "verarbeite Sprache …";
  const audioStream = !isDeviceMode();  // Geräte-Modus: kein Server-Audio anfordern
  await runTurn("/ws/voice", (ws) => {
    ws.send(JSON.stringify({ type: "start", format: fmt, stream: true, audio_stream: audioStream, ...overrides() }));
    ws.send(bytes);
    ws.send(JSON.stringify({ type: "end" }));
  });
  busy = false;
  setMicIdle();
}

micBtn.addEventListener("click", () => {
  unlockTTS();  // iOS: Geräte-TTS bei der Nutzergeste freischalten
  if (busy) {
    // Barge-in: laufende KI-Antwort sofort unterbrechen
    if (activeWs) activeWs.send(JSON.stringify({ type: "interrupt" }));
    stopAudio();
    statusEl.textContent = "Unterbrochen";
    // busy + Button-Reset erfolgen sobald die WS schliesst (-> runTurn resolve -> sendVoice/sendText)
  } else if (mediaRecorder && mediaRecorder.state === "recording") {
    stopRecording();
  } else {
    startRecording();         // Server-STT (Upload) — überall gleich
  }
});

$("#admin-toggle").addEventListener("click", () => { closeMenu(); openAdminPanel(); });

// ---------- Neues Gespräch ----------
// Startet eine frische Session (neuer session_id) -> der bisherige Verlauf wird nicht
// mehr als LLM-Kontext genutzt. Behebt "Sprache springt nicht um", wenn die History
// in einer anderen Sprache dominiert. Nicht-destruktiv: alte Session bleibt in der DB.
function newConversation() {
  const base = currentUserId || "anon";
  sessionId = "web-" + base + "-" + Date.now();
  if (currentUserId) localStorage.setItem(sessionKey(currentUserId), sessionId);
  stopAudio();
  messagesEl.innerHTML = "";
  statusEl.textContent = "Neues Gespräch begonnen";
  setTimeout(() => {
    if (statusEl.textContent === "Neues Gespräch begonnen") statusEl.textContent = "";
  }, 2500);
}
$("#new-chat").addEventListener("click", () => { closeMenu(); newConversation(); });

// ---------- Menü (⋮) verdrahten ----------
$("#menu-toggle").addEventListener("click", () =>
  $("#settings-sheet").classList.contains("hidden") ? openMenu() : closeMenu());
$("#settings-backdrop").addEventListener("click", closeMenu);
document.addEventListener("keydown", (e) => { if (e.key === "Escape") closeMenu(); });
// Ton-Presets: setzen das verborgene #tts + lösen die bestehende Persistenz-Logik aus.
document.querySelectorAll(".tts-preset").forEach((btn) =>
  btn.addEventListener("click", () => {
    unlockTTS();
    ttsSel.value = btn.dataset.tts;
    ttsSel.dispatchEvent(new Event("change"));
    syncPresetUI();
  })
);

// Tag/Nacht: Umschalter (manuell) + automatisch dem System folgen, solange nichts gewählt.
const themeBtn = $("#theme-toggle");
const setThemeIcon = () => {
  const icon = $("#theme-icon");
  if (icon) icon.textContent = document.documentElement.classList.contains("dark") ? "☀️" : "🌙";
};
themeBtn.addEventListener("click", () => {
  const dark = document.documentElement.classList.toggle("dark");
  localStorage.setItem("theme", dark ? "dark" : "light");
  setThemeIcon();
});
matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
  if (!localStorage.getItem("theme")) {
    document.documentElement.classList.toggle("dark", e.matches);
    setThemeIcon();
  }
});
setThemeIcon();

// Wenn die Bildschirmtastatur die sichtbare Hoehe aendert (iOS), unten bleiben.
if (window.visualViewport) {
  window.visualViewport.addEventListener("resize", scrollToBottom);
}
window.addEventListener("resize", scrollToBottom);
promptEl.addEventListener("focus", () => setTimeout(scrollToBottom, 300));

// Dropdown-Änderungen dauerhaft als Nutzer-Präferenz speichern.
if (langSel) langSel.addEventListener("change", () => savePrefs(langOverrides()));
if (ttsSel) {
  ttsSel.addEventListener("change", () => {
    unlockTTS();  // Auswahl ist eine Nutzergeste -> Geräte-TTS gleich freischalten
    if (ttsSel.value === "device") {
      // Geräte-TTS ist geräte-lokal (nicht server-seitig) -> nur lokal merken.
      localStorage.setItem(PLAYBACK_KEY, "device");
    } else {
      localStorage.removeItem(PLAYBACK_KEY);
      savePrefs({ tts_provider: ttsSel.value || null });  // echter Provider -> Server-Pref
    }
  });
}

loadMe();

// ═══════════════════════════════════════════════════════════════
// ADMIN PANEL
// ═══════════════════════════════════════════════════════════════

// ---------- Hilfsfunktionen ----------
function escHtml(str) {
  if (str === null || str === undefined || str === "") return "";
  return String(str).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}

function fmtDate(iso) {
  if (!iso) return "—";
  try {
    return new Date(iso).toLocaleString("de-DE", {
      day: "2-digit", month: "2-digit", year: "2-digit",
      hour: "2-digit", minute: "2-digit",
    });
  } catch { return iso; }
}

async function adminFetch(url, method = "GET", body = null) {
  try {
    const opts = { method, headers: { "Content-Type": "application/json" } };
    if (body) opts.body = JSON.stringify(body);
    const res = await fetch(url, opts);
    if (method === "DELETE" && res.status === 204) return {};
    const data = await res.json().catch(() => null);
    if (!res.ok) { console.error("Admin API", res.status, data); return null; }
    return data;
  } catch (e) { console.error("adminFetch", e); return null; }
}

// ---------- Panel öffnen / schließen ----------
function openAdminPanel() {
  $("#admin-panel").classList.remove("hidden");
  switchAdminTab("overview");
}

$("#admin-close").addEventListener("click", () => $("#admin-panel").classList.add("hidden"));

// ---------- Tab-Struktur: 5 Bereiche, je mit 1+ Abschnitten ----------
// section = vorhandener Content-Block (#admin-tab-<section>); Bereiche mit mehreren
// Abschnitten bekommen eine Sub-Navigation.
const ADMIN_TABS = {
  overview:  [["overview", "Übersicht"]],
  users:     [["users", "Verwalten"], ["chat", "Gespräche"]],
  emergency: [["emergency", "Notfälle"]],
  system:    [["status", "Status"], ["metrics", "Metriken"], ["log", "Log"]],
  config:    [["settings", "Einstellungen"], ["words", "Wörterbuch"]],
};
const SECTION_LOADER = {
  overview: () => loadOverview(),
  users: () => loadAdminUsers(),
  chat: () => loadChatUsers(),
  emergency: () => loadEmergencyEvents(),
  status: () => loadStatus(),
  metrics: () => loadMetrics(),
  words: () => loadWoerterbuch(),
  settings: () => loadSettings(),
  log: () => {},  // Log: Nutzer verbindet manuell
};

function _setTabActive(btn, active) {
  btn.classList.toggle("border-blue-600", active);
  btn.classList.toggle("text-blue-600", active);
  btn.classList.toggle("dark:border-blue-400", active);
  btn.classList.toggle("dark:text-blue-400", active);
  btn.classList.toggle("border-transparent", !active);
  btn.classList.toggle("text-slate-500", !active);
}

function showAdminSection(section, top) {
  document.querySelectorAll('[id^="admin-tab-"]').forEach((el) => el.classList.add("hidden"));
  const panel = $("#admin-tab-" + section);
  if (panel) panel.classList.remove("hidden");
  // Sub-Tab markieren
  document.querySelectorAll(".admin-subtab").forEach((b) => {
    const active = b.dataset.sec === section;
    b.classList.toggle("bg-blue-600", active);
    b.classList.toggle("text-white", active);
    b.classList.toggle("border-blue-600", active);
    b.classList.toggle("text-slate-600", !active);
    b.classList.toggle("dark:text-slate-300", !active);
    b.classList.toggle("border-slate-300", !active);
    b.classList.toggle("dark:border-slate-600", !active);
  });
  const loader = SECTION_LOADER[section];
  if (loader) loader();
}

function switchAdminTab(top) {
  const sections = ADMIN_TABS[top] || [[top, top]];
  document.querySelectorAll(".admin-tab").forEach((btn) => _setTabActive(btn, btn.dataset.tab === top));

  const subnav = $("#admin-subnav");
  if (sections.length <= 1) {
    subnav.classList.add("hidden");
    subnav.innerHTML = "";
    showAdminSection(sections[0][0], top);
  } else {
    subnav.classList.remove("hidden");
    subnav.innerHTML = sections.map(([sec, label]) =>
      `<button class="admin-subtab text-xs rounded-lg border px-3 py-1.5 whitespace-nowrap transition-colors border-slate-300 dark:border-slate-600 text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700" data-sec="${sec}">${escHtml(label)}</button>`
    ).join("");
    subnav.querySelectorAll(".admin-subtab").forEach((b) =>
      b.addEventListener("click", () => showAdminSection(b.dataset.sec, top))
    );
    showAdminSection(sections[0][0], top);
  }
}

document.querySelectorAll(".admin-tab").forEach((btn) =>
  btn.addEventListener("click", () => switchAdminTab(btn.dataset.tab))
);
$("#load-overview").addEventListener("click", loadOverview);
$("#load-emergency").addEventListener("click", loadEmergencyEvents);
$("#load-status").addEventListener("click", loadStatus);
$("#load-metrics").addEventListener("click", loadMetrics);

// ════════════════════════════════════════
// TAB: ÜBERSICHT (Dashboard)
// ════════════════════════════════════════
async function loadOverview() {
  const c = $("#overview-content");
  c.innerHTML = '<p class="text-sm text-slate-400">lade …</p>';
  const [users, usage, emergencies, llm] = await Promise.all([
    adminFetch("/api/admin/users"),
    adminFetch("/api/admin/usage"),
    adminFetch("/api/admin/emergency-events"),
    adminFetch("/api/admin/llm/status"),
  ]);
  const userCount = users ? users.length : "—";
  const reqTotal = usage ? usage.reduce((s, u) => s + (u.total_requests || 0), 0) : "—";
  const emCount = emergencies ? emergencies.length : "—";
  const backend = llm ? ({ ollama: "Ollama", llamacpp: "llama.cpp" }[llm.backend] || llm.backend) : "—";
  const gpu = (llm && llm.gpus && llm.gpus.length)
    ? llm.gpus.map((g) => `GPU ${g.index}: ${g.percent}%`).join(" · ") : "—";

  const hasEmergencies = typeof emCount === "number" && emCount > 0;
  // Kachel; mit data-jump wird sie anklickbar (springt in den Bereich).
  const stat = (label, value, sub, opts = {}) => `
    <${opts.jump ? `button data-jump="${opts.jump}"` : "div"}
      class="ov-stat text-left bg-white dark:bg-slate-800 rounded-xl border ${opts.border || "border-slate-200 dark:border-slate-700"} p-4 ${opts.jump ? "hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors" : ""}">
      <p class="text-xs text-slate-500 dark:text-slate-400">${escHtml(label)}</p>
      <p class="text-2xl font-semibold mt-1 ${opts.accent || ""}">${escHtml(value)}</p>
      ${sub ? `<p class="text-xs text-slate-400 mt-0.5 truncate">${escHtml(sub)}</p>` : ""}
    </${opts.jump ? "button" : "div"}>`;

  const jump = (to, label) =>
    `<button class="ov-jump text-sm rounded-lg border border-slate-300 dark:border-slate-600 px-3 py-1.5 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors" data-jump="${to}">${escHtml(label)}</button>`;

  c.innerHTML = `
    <div class="grid grid-cols-2 gap-3">
      ${stat("Nutzer", userCount, "", { jump: "users" })}
      ${stat("Anfragen gesamt", reqTotal, "", { jump: "system" })}
      ${stat("Notfälle", emCount, hasEmergencies ? "ansehen" : "keine",
             { jump: "emergency",
               accent: hasEmergencies ? "text-red-600 dark:text-red-400" : "",
               border: hasEmergencies ? "border-red-300 dark:border-red-800" : "" })}
      ${stat("LLM-Backend", backend, llm ? llm.model : "", { jump: "system" })}
    </div>
    <div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4">
      <h4 class="font-semibold text-sm mb-2">GPU-Auslastung</h4>
      <p class="text-xs font-mono text-slate-600 dark:text-slate-300">${escHtml(gpu)}</p>
    </div>
    <div class="flex flex-wrap gap-2">
      ${jump("users", "→ Nutzer")} ${jump("system", "→ System")} ${jump("emergency", "→ Notfälle")}
    </div>`;
  c.querySelectorAll(".ov-jump, .ov-stat[data-jump]").forEach((b) =>
    b.addEventListener("click", () => switchAdminTab(b.dataset.jump)));
}

// ════════════════════════════════════════
// TAB: NUTZER
// ════════════════════════════════════════
async function loadAdminUsers() {
  const container = $("#admin-user-cards");
  container.innerHTML = '<p class="text-sm text-slate-400 text-center py-4">lade …</p>';
  const users = await adminFetch("/api/admin/users");
  if (!users) { container.innerHTML = '<p class="text-sm text-red-500 text-center py-4">Fehler beim Laden.</p>'; return; }
  if (!users.length) { container.innerHTML = '<p class="text-sm text-slate-400 text-center py-4">Noch keine Nutzer.</p>'; return; }
  container.innerHTML = "";
  users.forEach((u) => container.appendChild(buildUserCard(u)));
}

$("#create-user-form").addEventListener("submit", async (e) => {
  e.preventDefault();
  const nameEl = $("#new-user-name");
  const name = nameEl.value.trim();
  if (!name) return;
  const data = await adminFetch("/api/admin/users", "POST", { display_name: name });
  if (data?.token) {
    $("#new-token-value").textContent = data.token;
    $("#new-token-box").classList.remove("hidden");
    nameEl.value = "";
    loadAdminUsers();
  }
});

function buildUserCard(u) {
  const card = document.createElement("div");
  card.className = "bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4";

  const badgeSSO  = u.external_id
    ? `<span class="rounded-full bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300 text-xs px-2 py-0.5">SSO</span>` : "";

  card.innerHTML = `
    <div class="flex items-start gap-3 flex-wrap">
      <div class="flex-1 min-w-0">
        <div class="flex items-center gap-2 flex-wrap mb-0.5">
          <span class="font-semibold text-slate-900 dark:text-slate-100 user-dn">${escHtml(u.display_name)}</span>
          ${badgeSSO}
        </div>
        <p class="text-xs text-slate-400 font-mono">${u.user_id.slice(0, 8)}…${u.external_id ? " · " + escHtml(u.external_id) : ""}</p>
        <p class="text-xs text-slate-400">seit ${fmtDate(u.created_at)}</p>
      </div>
      <div class="flex flex-wrap gap-1.5">
        <button class="btn-rename text-xs rounded-lg border border-slate-300 dark:border-slate-600 px-2.5 py-1.5 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors">Umbenennen</button>
        <button class="btn-token  text-xs rounded-lg border border-amber-300 dark:border-amber-700 text-amber-700 dark:text-amber-300 px-2.5 py-1.5 hover:bg-amber-50 dark:hover:bg-amber-900/20 transition-colors">Token-Reset</button>
        <button class="btn-delete text-xs rounded-lg border border-red-300 dark:border-red-800 text-red-600 dark:text-red-400 px-2.5 py-1.5 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors">Löschen</button>
      </div>
    </div>

    <!-- Token-Anzeige nach Reset -->
    <div class="token-box hidden mt-3 rounded-lg bg-amber-50 dark:bg-amber-900/30 border border-amber-200 dark:border-amber-700 p-3">
      <p class="text-xs text-amber-700 dark:text-amber-300 font-medium mb-1">Neuer Token (einmalig — jetzt kopieren!):</p>
      <code class="token-val text-xs font-mono break-all text-amber-900 dark:text-amber-100 select-all"></code>
    </div>

    <!-- Umbenenn-Formular -->
    <div class="rename-box hidden mt-3">
      <form class="flex gap-2">
        <input type="text" class="rename-inp flex-1 rounded-lg border border-slate-300 dark:border-slate-600 bg-transparent px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
               value="${escHtml(u.display_name)}" />
        <button type="submit" class="rounded-lg bg-blue-600 hover:bg-blue-700 text-white text-sm px-3 py-1.5 transition-colors">Speichern</button>
        <button type="button" class="rename-cancel rounded-lg border border-slate-300 dark:border-slate-600 text-sm px-3 py-1.5 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors">Abbrechen</button>
      </form>
    </div>

    <!-- Erinnerungen -->
    <div class="mt-3 pt-3 border-t border-slate-100 dark:border-slate-700">
      <button class="btn-mem text-xs text-slate-500 hover:text-slate-700 dark:hover:text-slate-300 flex items-center gap-1 transition-colors">
        <span class="mem-arrow">▶</span> Erinnerungen
      </button>
      <div class="mem-section hidden mt-2 space-y-1.5">
        <div class="mem-list"></div>
        <form class="mem-add-form flex gap-2 mt-2">
          <input type="text" placeholder="Neue Erinnerung …"
            class="flex-1 rounded-lg border border-slate-300 dark:border-slate-600 bg-transparent px-3 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-blue-500" />
          <button type="submit" class="rounded-lg bg-blue-600 hover:bg-blue-700 text-white text-xs px-3 py-1.5 transition-colors">Hinzufügen</button>
        </form>
      </div>
    </div>
  `;

  const uid = u.user_id;

  // Umbenennen
  card.querySelector(".btn-rename").addEventListener("click", () =>
    card.querySelector(".rename-box").classList.toggle("hidden")
  );
  card.querySelector(".rename-cancel").addEventListener("click", () =>
    card.querySelector(".rename-box").classList.add("hidden")
  );
  card.querySelector(".rename-box form").addEventListener("submit", async (e) => {
    e.preventDefault();
    const name = card.querySelector(".rename-inp").value.trim();
    if (!name) return;
    const res = await adminFetch(`/api/admin/users/${uid}`, "PUT", { display_name: name });
    if (res) {
      card.querySelector(".user-dn").textContent = name;
      card.querySelector(".rename-box").classList.add("hidden");
    }
  });

  // Token-Reset
  card.querySelector(".btn-token").addEventListener("click", async () => {
    if (!confirm(`Token für „${u.display_name}" zurücksetzen?\nDer alte Token wird sofort ungültig.`)) return;
    const data = await adminFetch(`/api/admin/users/${uid}/token`, "POST");
    if (data?.token) {
      card.querySelector(".token-val").textContent = data.token;
      card.querySelector(".token-box").classList.remove("hidden");
    }
  });

  // Löschen
  card.querySelector(".btn-delete").addEventListener("click", async () => {
    if (!confirm(`Nutzer „${u.display_name}" und alle Daten unwiderruflich löschen?`)) return;
    const res = await adminFetch(`/api/admin/users/${uid}`, "DELETE");
    if (res !== null) card.remove();
  });

  // Erinnerungen auf-/zuklappen
  const memBtn     = card.querySelector(".btn-mem");
  const memSection = card.querySelector(".mem-section");
  const memArrow   = card.querySelector(".mem-arrow");
  memBtn.addEventListener("click", async () => {
    const open = !memSection.classList.contains("hidden");
    memSection.classList.toggle("hidden", open);
    memArrow.textContent = open ? "▶" : "▼";
    if (!open) await renderMemories(uid, card.querySelector(".mem-list"));
  });

  // Erinnerung hinzufügen
  card.querySelector(".mem-add-form").addEventListener("submit", async (e) => {
    e.preventDefault();
    const inp = e.target.querySelector("input");
    const content = inp.value.trim();
    if (!content) return;
    const res = await adminFetch(`/api/admin/users/${uid}/memories`, "POST", { content });
    if (res) { inp.value = ""; await renderMemories(uid, card.querySelector(".mem-list")); }
  });

  return card;
}

async function renderMemories(uid, container) {
  container.innerHTML = '<p class="text-xs text-slate-400">lade …</p>';
  const mems = await adminFetch(`/api/admin/users/${uid}/memories`);
  if (!mems?.length) {
    container.innerHTML = '<p class="text-xs text-slate-400 italic">Keine Erinnerungen vorhanden.</p>';
    return;
  }
  container.innerHTML = "";
  mems.forEach((m) => {
    const row = document.createElement("div");
    row.className = "flex items-start gap-2 rounded-lg bg-slate-50 dark:bg-slate-700/50 px-3 py-2 text-xs";
    row.innerHTML = `
      <span class="flex-1 text-slate-700 dark:text-slate-300 leading-relaxed">${escHtml(m.content)}</span>
      <button class="shrink-0 text-slate-400 hover:text-red-500 dark:hover:text-red-400 transition-colors font-bold" title="Löschen">✕</button>
    `;
    row.querySelector("button").addEventListener("click", async () => {
      await adminFetch(`/api/admin/users/${uid}/memories/${m.id}`, "DELETE");
      row.remove();
    });
    container.appendChild(row);
  });
}

// ════════════════════════════════════════
// TAB: GESPRÄCHE
// ════════════════════════════════════════
let _chatUser = null;

async function loadChatUsers() {
  const list = $("#chat-user-list");
  list.innerHTML = '<li class="px-3 py-3 text-sm text-slate-400">lade …</li>';
  const users = await adminFetch("/api/admin/users");
  if (!users) return;
  list.innerHTML = "";
  users.forEach((u) => {
    const li = document.createElement("li");
    li.className = "px-3 py-2.5 text-sm cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors flex flex-col";
    li.innerHTML = `<span class="font-medium truncate">${escHtml(u.display_name)}</span>
                    <span class="text-xs text-slate-400 font-mono truncate">${u.user_id.slice(0,8)}…</span>`;
    li.addEventListener("click", () => {
      document.querySelectorAll("#chat-user-list li").forEach((el) =>
        el.classList.remove("bg-blue-50", "dark:bg-blue-900/20", "text-blue-700", "dark:text-blue-300")
      );
      li.classList.add("bg-blue-50", "dark:bg-blue-900/20");
      _chatUser = u;
      loadChatSessions(u);
    });
    list.appendChild(li);
  });
}

async function loadChatSessions(u) {
  const panel = $("#chat-right-panel");
  panel.innerHTML = `<p class="p-4 text-sm text-slate-400">lade Sessions für ${escHtml(u.display_name)} …</p>`;
  const sessions = await adminFetch(`/api/admin/users/${u.user_id}/sessions`);
  if (!sessions?.length) {
    panel.innerHTML = '<p class="p-4 text-sm text-slate-400">Keine Sessions gefunden.</p>';
    return;
  }
  panel.innerHTML = `
    <div class="px-4 py-3 border-b border-slate-200 dark:border-slate-700 bg-slate-50/80 dark:bg-slate-800/50">
      <p class="text-xs font-semibold text-slate-500 uppercase tracking-wide">Sessions · ${escHtml(u.display_name)}</p>
    </div>
    <div id="session-list" class="divide-y divide-slate-100 dark:divide-slate-800/60"></div>
  `;
  const list = panel.querySelector("#session-list");
  sessions.forEach((s) => {
    const row = document.createElement("div");
    row.className = "px-4 py-3 cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors";
    row.innerHTML = `
      <p class="text-sm font-mono text-slate-700 dark:text-slate-300 truncate">${escHtml(s.id)}</p>
      <p class="text-xs text-slate-400 mt-0.5">${fmtDate(s.updated_at)} · ${s.msg_count} Nachrichten</p>
    `;
    row.addEventListener("click", () => loadTranscript(s.id));
    list.appendChild(row);
  });
}

async function loadTranscript(sessionId) {
  const panel = $("#chat-right-panel");
  panel.innerHTML = `<p class="p-4 text-sm text-slate-400">lade Transkript …</p>`;
  const msgs = await adminFetch(`/api/admin/sessions/${sessionId}/messages`);
  if (!msgs?.length) {
    panel.innerHTML = `<div class="p-4">
      <button class="back-btn text-xs text-blue-600 hover:underline mb-3 block">← zurück zu Sessions</button>
      <p class="text-sm text-slate-400">Keine Nachrichten.</p></div>`;
    panel.querySelector(".back-btn").addEventListener("click", () => _chatUser && loadChatSessions(_chatUser));
    return;
  }

  const bubbles = msgs.map((m) => {
    const isUser = m.role === "user";
    return `<div class="flex ${isUser ? "justify-end" : "justify-start"}">
      <div class="max-w-[80%] rounded-2xl px-4 py-2 text-sm whitespace-pre-wrap leading-relaxed
        ${isUser
          ? "rounded-br-sm bg-blue-600 text-white"
          : "rounded-bl-sm border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-800 dark:text-slate-100"}">
        ${escHtml(m.content)}</div></div>`;
  }).join("");

  panel.innerHTML = `
    <div class="sticky top-0 px-4 py-2.5 border-b border-slate-200 dark:border-slate-700 bg-white/90 dark:bg-slate-800/90 backdrop-blur flex items-center gap-3">
      <button class="back-btn text-xs text-blue-600 hover:underline">← Sessions</button>
      <span class="text-xs text-slate-400 font-mono truncate">${escHtml(sessionId)}</span>
    </div>
    <div class="p-4 space-y-3">${bubbles}</div>
  `;
  panel.querySelector(".back-btn").addEventListener("click", () => _chatUser && loadChatSessions(_chatUser));
}

// ════════════════════════════════════════
// TAB: NOTFÄLLE
// ════════════════════════════════════════
async function loadEmergencyEvents() {
  const container = $("#emergency-content");
  container.innerHTML = '<p class="text-sm text-slate-400">lade …</p>';
  const events = await adminFetch("/api/admin/emergency-events");
  if (!events) { container.innerHTML = '<p class="text-sm text-red-500">Fehler beim Laden.</p>'; return; }
  if (!events.length) { container.innerHTML = '<p class="text-sm text-slate-400 py-4 text-center">Keine Notfall-Ereignisse protokolliert.</p>'; return; }
  container.innerHTML = `
    <div class="overflow-x-auto rounded-xl border border-slate-200 dark:border-slate-700">
      <table class="w-full text-sm">
        <thead class="bg-slate-50 dark:bg-slate-800/60">
          <tr class="text-xs text-slate-500 dark:text-slate-400 border-b border-slate-200 dark:border-slate-700">
            <th class="text-left px-4 py-2.5">Zeitpunkt</th>
            <th class="text-left px-4 py-2.5">Nutzer</th>
            <th class="text-left px-4 py-2.5">Kategorie</th>
            <th class="text-left px-4 py-2.5">Textausschnitt</th>
          </tr>
        </thead>
        <tbody class="divide-y divide-slate-100 dark:divide-slate-800 bg-white dark:bg-slate-800">
          ${events.map((ev) => `
            <tr class="hover:bg-slate-50 dark:hover:bg-slate-700/40 transition-colors">
              <td class="px-4 py-2.5 text-xs text-slate-400 whitespace-nowrap">${fmtDate(ev.created_at)}</td>
              <td class="px-4 py-2.5 text-xs font-medium">${escHtml(ev.display_name)}</td>
              <td class="px-4 py-2.5">
                <span class="rounded-full bg-red-100 dark:bg-red-900/40 text-red-700 dark:text-red-300 text-xs px-2.5 py-0.5 font-medium">${escHtml(ev.category)}</span>
              </td>
              <td class="px-4 py-2.5 text-xs text-slate-600 dark:text-slate-300 max-w-[16rem] break-words">${escHtml(ev.snippet)}</td>
            </tr>`).join("")}
        </tbody>
      </table>
    </div>`;
}

// ════════════════════════════════════════
// TAB: STATUS
// ════════════════════════════════════════
// LLM-/GPU-Status-Karte (read-only) für den Status-Tab.
function renderLlmCard(llm) {
  if (!llm) return "";
  const backendLabel = { ollama: "Ollama", llamacpp: "llama.cpp", unknown: "—" }[llm.backend] || llm.backend;
  const dot = (on) => `<span class="h-2.5 w-2.5 rounded-full ${on ? "bg-emerald-500" : "bg-slate-400"}"></span>`;
  const row = (label, val) =>
    `<dt class="text-slate-500 dark:text-slate-400">${label}</dt>
     <dd class="font-mono text-xs text-slate-700 dark:text-slate-300">${escHtml(val ?? "—")}</dd>`;

  const loaded = (llm.ollama_loaded || []).map((m) =>
    `${escHtml(m.name)} <span class="text-slate-400">(${escHtml(m.processor || "")})</span>`
  ).join(", ") || "—";

  const gpuBars = (llm.gpus || []).map((g) => `
    <div class="flex items-center gap-3">
      <span class="w-10 shrink-0 text-xs text-slate-500 dark:text-slate-400">GPU ${g.index}</span>
      <div class="flex-1 h-3 bg-slate-100 dark:bg-slate-700 rounded-full overflow-hidden">
        <div class="h-full ${g.percent > 85 ? "bg-red-500" : "bg-blue-500 dark:bg-blue-600"} rounded-full" style="width:${g.percent}%"></div>
      </div>
      <span class="w-28 text-right text-xs font-mono text-slate-500 dark:text-slate-400">${g.used_mib} / ${g.total_mib} MiB</span>
    </div>`).join("") || '<p class="text-xs text-slate-400">keine GPU-Daten (nvidia-smi)</p>';

  return `
    <div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4">
      <div class="flex items-center gap-2 mb-3">
        ${dot(true)}<h3 class="font-semibold text-sm">LLM-Backend</h3>
        <span class="text-xs rounded-full bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300 px-2 py-0.5 font-medium">${escHtml(backendLabel)}</span>
      </div>
      <dl class="grid grid-cols-2 gap-x-8 gap-y-1.5 text-sm mb-3">
        ${row("Modell", llm.model)}
        ${row("URL", llm.base_url)}
        ${row("Ollama erreichbar", llm.ollama_reachable ? "ja" : "nein")}
        ${row("llama.cpp läuft", llm.llamacpp_running ? "ja" : "nein")}
        ${row("Geladene Modelle", loaded)}
        ${row("Gateway-Dienst", llm.gateway_service_active ? "aktiv (systemd)" : "Vordergrund/aus")}
      </dl>
      <div class="space-y-2 pt-2 border-t border-slate-100 dark:border-slate-700">
        ${gpuBars}
      </div>

      <!-- Steuerung (schreibend) -->
      <div class="mt-4 pt-3 border-t border-slate-100 dark:border-slate-700 flex flex-wrap items-center gap-2">
        <select id="llm-backend-sel" class="text-sm rounded-lg border border-slate-300 dark:border-slate-600 bg-transparent px-2 py-1.5">
          <option value="ollama" ${llm.backend === "ollama" ? "selected" : ""}>Ollama</option>
          <option value="llamacpp" ${llm.backend === "llamacpp" ? "selected" : ""}>llama.cpp</option>
        </select>
        <input id="llm-model-inp" type="text" placeholder="Modell (Ollama)" value="${escHtml(llm.backend === "ollama" ? llm.model : "")}"
          class="text-sm rounded-lg border border-slate-300 dark:border-slate-600 bg-transparent px-2 py-1.5 flex-1 min-w-32 font-mono" />
        <button id="llm-switch-btn" class="rounded-lg bg-blue-600 hover:bg-blue-700 text-white text-sm px-3 py-1.5 font-medium transition-colors">Backend wechseln</button>
        <button id="gw-restart-btn" class="rounded-lg border border-amber-300 dark:border-amber-700 text-amber-700 dark:text-amber-300 text-sm px-3 py-1.5 hover:bg-amber-50 dark:hover:bg-amber-900/20 transition-colors">Gateway neu starten</button>
        <span id="llm-ctrl-msg" class="text-xs text-slate-400"></span>
      </div>
      <p class="mt-2 text-xs text-slate-400">Hinweis: Wirkt vollständig nur, wenn das Gateway als systemd-Dienst läuft (sonst .env/Backend umgestellt, aber Gateway manuell neu starten).</p>
    </div>`;
}

// Wartet, bis das Gateway nach einem Neustart wieder antwortet, dann Status neu laden.
async function pollGatewayBack(msgEl, label) {
  for (let i = 0; i < 40; i++) {
    await new Promise((r) => setTimeout(r, 1500));
    try {
      const r = await fetch("/api/config", { cache: "no-store" });
      if (r.ok) { if (msgEl) msgEl.textContent = label + " ✓"; loadStatus(); return; }
    } catch (e) { /* noch nicht oben */ }
    if (msgEl) msgEl.textContent = label + " … (" + (i + 1) + ")";
  }
  if (msgEl) msgEl.textContent = label + " — Zeitüberschreitung, bitte Status manuell prüfen.";
}

function wireLlmControls() {
  const sel = $("#llm-backend-sel");
  const switchBtn = $("#llm-switch-btn");
  const restartBtn = $("#gw-restart-btn");
  const msg = $("#llm-ctrl-msg");
  if (!switchBtn) return;

  switchBtn.addEventListener("click", async () => {
    const backend = sel.value;
    const model = ($("#llm-model-inp").value || "").trim();
    if (!confirm(`Backend auf „${backend}"${backend === "ollama" && model ? " (" + model + ")" : ""} umstellen?\nDie GPU des anderen Backends wird freigegeben.`)) return;
    switchBtn.disabled = true;
    msg.textContent = "stelle um …";
    const body = { backend };
    if (backend === "ollama" && model) body.model = model;
    const res = await adminFetch("/api/admin/llm/backend", "POST", body);
    if (res === null) { msg.textContent = "Fehler (Modell/Backend ungültig?)"; switchBtn.disabled = false; return; }
    pollGatewayBack(msg, "Backend gewechselt");
  });

  restartBtn.addEventListener("click", async () => {
    if (!confirm("Gateway-Dienst jetzt neu starten?")) return;
    restartBtn.disabled = true;
    msg.textContent = "starte neu …";
    const res = await adminFetch("/api/admin/gateway/restart", "POST");
    if (res === null) { msg.textContent = "Fehler beim Neustart"; restartBtn.disabled = false; return; }
    pollGatewayBack(msg, "Neu gestartet");
  });
}

async function loadStatus() {
  const container = $("#status-content");
  container.innerHTML = '<p class="text-sm text-slate-400">lade …</p>';
  try {
    const [cfg, met, llm] = await Promise.all([
      fetch("/api/config").then((r) => r.json()),
      fetch("/api/metrics").then((r) => r.json()),
      adminFetch("/api/admin/llm/status"),
    ]);
    const route    = cfg.default_route || {};
    const counters = met.counters      || {};
    const turnsTotal = counters["turns_total"] || 0;
    const httpTotal  = Object.entries(counters)
      .filter(([k]) => k.startsWith("http_requests_total"))
      .reduce((s, [, v]) => s + v, 0);

    const providerRow = (label, val) =>
      `<dt class="text-slate-500 dark:text-slate-400">${label}</dt>
       <dd class="font-mono text-xs text-slate-700 dark:text-slate-300">${escHtml(val || "—")}</dd>`;

    container.innerHTML = `
      <!-- Gateway-Status -->
      <div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4">
        <div class="flex items-center gap-2 mb-3">
          <span class="h-2.5 w-2.5 rounded-full bg-emerald-500 shadow shadow-emerald-300/60"></span>
          <h3 class="font-semibold text-sm">Gateway</h3>
          <span class="text-xs text-emerald-600 dark:text-emerald-400 font-medium">läuft</span>
        </div>
        <dl class="grid grid-cols-2 gap-x-8 gap-y-1.5 text-sm">
          ${providerRow("Profil",    cfg.profile)}
          ${providerRow("Umgebung",  cfg.app_env)}
        </dl>
      </div>

      <!-- Provider -->
      <div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4">
        <h3 class="font-semibold text-sm mb-3">Provider (Standard-Route)</h3>
        <dl class="grid grid-cols-2 gap-x-8 gap-y-1.5 text-sm">
          ${providerRow("STT",     route.stt_provider)}
          ${providerRow("LLM",     route.llm_provider)}
          ${providerRow("TTS",     route.tts_provider)}
          ${providerRow("Sprache", route.language)}
        </dl>
      </div>

      ${renderLlmCard(llm)}

      <!-- Laufzeit-Metriken -->
      <div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4">
        <h3 class="font-semibold text-sm mb-3">Laufzeit-Metriken</h3>
        <dl class="grid grid-cols-2 gap-x-8 gap-y-1.5 text-sm">
          ${providerRow("Turns gesamt",   turnsTotal)}
          ${providerRow("HTTP-Anfragen",  httpTotal)}
        </dl>
      </div>

      <!-- Verfügbare Provider -->
      <div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4">
        <h3 class="font-semibold text-sm mb-3">Verfügbare Provider</h3>
        <dl class="grid grid-cols-2 gap-x-8 gap-y-1.5 text-sm">
          ${providerRow("STT", (cfg.available?.stt_providers || []).join(", "))}
          ${providerRow("LLM", (cfg.available?.llm_providers || []).join(", "))}
          ${providerRow("TTS", (cfg.available?.tts_providers || []).join(", "))}
        </dl>
      </div>

      <!-- Datenbank-Export -->
      <div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4">
        <h3 class="font-semibold text-sm mb-1">Datenbank-Backup</h3>
        <p class="text-xs text-slate-400 mb-3">SQLite-Datenbank als Datei herunterladen (Backup / Migration).</p>
        <a href="/api/admin/db-export" download="voice-assistant.db"
          class="inline-flex items-center gap-2 rounded-lg bg-slate-700 hover:bg-slate-600 text-white text-xs px-4 py-2 font-medium transition-colors">
          ⬇ voice-assistant.db herunterladen
        </a>
      </div>
    `;
    wireLlmControls();  // Buttons der LLM-Karte verdrahten (nach dem Rendern)
  } catch (e) {
    console.error("loadStatus error:", e);
    container.innerHTML = '<p class="text-sm text-red-500">Fehler beim Laden.</p>';
  }
}

// ════════════════════════════════════════
// TAB: METRIKEN
// ════════════════════════════════════════
async function loadMetrics() {
  const container = $("#metrics-content");
  container.innerHTML = '<p class="text-sm text-slate-400">lade …</p>';
  const data = await adminFetch("/api/admin/usage");
  if (!data) { container.innerHTML = '<p class="text-sm text-red-500">Fehler beim Laden.</p>'; return; }
  if (!data.length) { container.innerHTML = '<p class="text-sm text-slate-400 py-4 text-center">Noch keine Nutzungsdaten vorhanden.</p>'; return; }

  const totalReq = data.reduce((s, u) => s + (u.total_requests || 0), 0);
  const maxReq   = Math.max(...data.map((u) => u.total_requests || 0), 1);

  const bars = data.map((u) => {
    const pct = Math.round(((u.total_requests || 0) / maxReq) * 100);
    return `
      <div class="flex items-center gap-3">
        <span class="w-28 shrink-0 text-xs text-slate-600 dark:text-slate-300 truncate">${escHtml(u.display_name)}</span>
        <div class="flex-1 h-4 bg-slate-100 dark:bg-slate-700 rounded-full overflow-hidden">
          <div class="h-full bg-blue-500 dark:bg-blue-600 rounded-full" style="width:${pct}%"></div>
        </div>
        <span class="w-10 text-right text-xs font-mono text-slate-500 dark:text-slate-400">${u.total_requests ?? 0}</span>
      </div>`;
  }).join("");

  container.innerHTML = `
    <div class="overflow-x-auto rounded-xl border border-slate-200 dark:border-slate-700 mb-4">
      <table class="w-full text-sm">
        <thead class="bg-slate-50 dark:bg-slate-800/60">
          <tr class="text-xs text-slate-500 dark:text-slate-400 border-b border-slate-200 dark:border-slate-700">
            <th class="text-left px-4 py-2.5">Nutzer</th>
            <th class="text-right px-4 py-2.5">Anfragen gesamt</th>
            <th class="text-right px-4 py-2.5">Einheiten</th>
            <th class="text-right px-4 py-2.5">Letzte Aktivität</th>
          </tr>
        </thead>
        <tbody class="divide-y divide-slate-100 dark:divide-slate-800 bg-white dark:bg-slate-800">
          ${data.map((u) => `
            <tr class="hover:bg-slate-50 dark:hover:bg-slate-700/40 transition-colors">
              <td class="px-4 py-2.5 font-medium">${escHtml(u.display_name)}</td>
              <td class="px-4 py-2.5 text-right font-mono text-xs">${u.total_requests ?? 0}</td>
              <td class="px-4 py-2.5 text-right font-mono text-xs">${u.total_units ?? 0}</td>
              <td class="px-4 py-2.5 text-right text-xs text-slate-400">${u.last_active || "—"}</td>
            </tr>`).join("")}
        </tbody>
        <tfoot class="border-t-2 border-slate-200 dark:border-slate-600 bg-slate-50 dark:bg-slate-800/60">
          <tr class="text-xs font-semibold text-slate-600 dark:text-slate-300">
            <td class="px-4 py-2">Gesamt</td>
            <td class="px-4 py-2 text-right font-mono">${totalReq}</td>
            <td class="px-4 py-2"></td>
            <td class="px-4 py-2"></td>
          </tr>
        </tfoot>
      </table>
    </div>

    <div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4">
      <h3 class="font-semibold text-sm mb-4">Anfragen je Nutzer</h3>
      <div class="space-y-2.5">${bars}</div>
    </div>
  `;
}

// ════════════════════════════════════════
// TAB: WÖRTERBUCH
// ════════════════════════════════════════
async function loadWoerterbuch() {
  const lang = ($("#words-lang") || {}).value || "de";
  const container = $("#words-content");
  container.innerHTML = '<p class="text-sm text-slate-400">lade …</p>';
  const data = await adminFetch(`/api/admin/pronunciation/${lang}`);
  if (!data) { container.innerHTML = '<p class="text-sm text-red-500">Fehler beim Laden.</p>'; return; }
  container.innerHTML = "";
  const sections = [
    ["abbreviations", "Abkürzungen"],
    ["units",         "Einheiten"],
    ["terms",         "Begriffe / Aussprache"],
  ];
  for (const [section, label] of sections) {
    container.appendChild(buildWortSection(label, section, data[section] || {}, lang));
  }
}

function buildWortSection(title, section, entries, lang) {
  const wrap = document.createElement("div");
  wrap.className = "bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4";

  const rows = Object.entries(entries).map(([k, v]) => `
    <tr class="row-edit cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-700/40 transition-colors group"
        data-key="${escHtml(k)}" data-val="${escHtml(v)}" title="Zum Bearbeiten anklicken">
      <td class="px-3 py-2 text-xs font-mono text-slate-700 dark:text-slate-300">${escHtml(k)}</td>
      <td class="px-3 py-2 text-xs text-slate-400">→</td>
      <td class="px-3 py-2 text-xs text-slate-700 dark:text-slate-300 font-medium">${escHtml(v)}</td>
      <td class="px-3 py-2 text-right">
        <button class="del-btn text-slate-300 dark:text-slate-600 hover:text-red-500 dark:hover:text-red-400 transition-colors text-xs opacity-0 group-hover:opacity-100"
                data-key="${escHtml(k)}" title="Löschen">✕</button>
      </td>
    </tr>`).join("");

  wrap.innerHTML = `
    <h4 class="font-semibold text-sm mb-3 flex items-center justify-between">
      ${escHtml(title)}
      <span class="text-xs font-normal text-slate-400">${Object.keys(entries).length} Einträge</span>
    </h4>
    ${rows ? `
    <div class="overflow-x-auto mb-4">
      <table class="w-full">
        <tbody class="divide-y divide-slate-100 dark:divide-slate-700/60">${rows}</tbody>
      </table>
    </div>` : '<p class="text-xs text-slate-400 italic mb-4">Keine Einträge.</p>'}
    <form class="add-form flex gap-2 flex-wrap">
      <input type="text" placeholder="Schlüssel" required
        class="flex-1 min-w-24 rounded-lg border border-slate-300 dark:border-slate-600 bg-transparent px-3 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-blue-500" />
      <input type="text" placeholder="Ersetzung" required
        class="flex-1 min-w-24 rounded-lg border border-slate-300 dark:border-slate-600 bg-transparent px-3 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-blue-500" />
      <button type="submit"
        class="rounded-lg bg-blue-600 hover:bg-blue-700 text-white text-xs px-3 py-1.5 font-medium transition-colors whitespace-nowrap">+ Hinzufügen</button>
    </form>
  `;

  const form = wrap.querySelector(".add-form");
  const [keyInp, valInp] = form.querySelectorAll("input");
  const submitBtn = form.querySelector('button[type="submit"]');

  // Zeile anklicken -> Begriffspaar in die Editier-Felder laden, Button wird zu „Speichern".
  wrap.querySelectorAll("tr.row-edit").forEach((tr) =>
    tr.addEventListener("click", () => {
      keyInp.value = tr.dataset.key;
      valInp.value = tr.dataset.val;
      form.dataset.editKey = tr.dataset.key;  // Originalschlüssel merken (für Umbenennen)
      submitBtn.textContent = "Speichern";
      valInp.focus();
    })
  );

  wrap.querySelectorAll(".del-btn").forEach((btn) =>
    btn.addEventListener("click", async (e) => {
      e.stopPropagation();  // nicht zugleich die Zeile zum Bearbeiten laden
      const key = btn.dataset.key;
      if (!confirm(`Eintrag „${key}" löschen?`)) return;
      const res = await adminFetch(
        `/api/admin/pronunciation/${lang}/${section}/${encodeURIComponent(key)}`, "DELETE"
      );
      if (res !== null) btn.closest("tr").remove();
    })
  );

  form.addEventListener("submit", async (e) => {
    e.preventDefault();
    const key = keyInp.value.trim();
    const value = valInp.value.trim();
    if (!key || !value) return;
    const editKey = form.dataset.editKey || "";
    // Hinzufügen ODER Aktualisieren (gleicher Schlüssel wird überschrieben; Backend sortiert).
    const res = await adminFetch(`/api/admin/pronunciation/${lang}`, "POST", { section, key, value });
    if (res === null) return;
    // Umbenannt -> alten Schlüssel entfernen (neuer wurde oben bereits angelegt).
    if (editKey && editKey !== key) {
      await adminFetch(
        `/api/admin/pronunciation/${lang}/${section}/${encodeURIComponent(editKey)}`, "DELETE"
      );
    }
    loadWoerterbuch();  // neu laden -> sortierte Liste, Formular zurück auf „+ Hinzufügen"
  });

  return wrap;
}

$("#load-words").addEventListener("click", loadWoerterbuch);
$("#words-lang").addEventListener("change", loadWoerterbuch);

// ════════════════════════════════════════
// TAB: LOG
// ════════════════════════════════════════
let _logWs = null;

function connectLog() {
  if (_logWs) return;
  const proto = location.protocol === "https:" ? "wss" : "ws";
  _logWs = new WebSocket(`${proto}://${location.host}/api/admin/log`);
  const output   = $("#log-output");
  const logStat  = $("#log-status");
  const btnConn  = $("#log-connect");
  const btnDisc  = $("#log-disconnect");

  btnConn.disabled = true;
  btnDisc.disabled = false;
  btnDisc.classList.remove("opacity-40");
  logStat.textContent = "verbinde …";
  logStat.className = "text-xs text-amber-400";

  _logWs.onopen = () => {
    logStat.textContent = "verbunden";
    logStat.className = "text-xs text-emerald-400";
    output.innerHTML = "";
  };

  _logWs.onmessage = (e) => {
    const line = document.createElement("div");
    line.textContent = e.data;
    output.appendChild(line);
    output.scrollTop = output.scrollHeight;
  };

  _logWs.onerror = () => {
    logStat.textContent = "Verbindungsfehler";
    logStat.className = "text-xs text-red-400";
  };

  _logWs.onclose = () => {
    _logWs = null;
    btnConn.disabled = false;
    btnDisc.disabled = true;
    btnDisc.classList.add("opacity-40");
    if (!logStat.className.includes("red")) {
      logStat.textContent = "getrennt";
      logStat.className = "text-xs text-slate-400";
    }
  };
}

function disconnectLog() {
  if (_logWs) { _logWs.close(); }
}

$("#log-connect").addEventListener("click", connectLog);
$("#log-disconnect").addEventListener("click", disconnectLog);
$("#log-clear").addEventListener("click", () => { $("#log-output").innerHTML = ""; });

// ════════════════════════════════════════
// TAB: EINSTELLUNGEN
// ════════════════════════════════════════

// Metadaten für jedes Feld: UI-Typ, Optionen, Test-Typ
const FIELD_META = {
  default_stt_provider:            { ui: "combo",   opts: ["openrouter","faster-whisper"], test: "stt" },
  default_llm_provider:            { ui: "combo",   opts: ["openrouter","local-openai-compatible"], test: "llm" },
  default_tts_provider:            { ui: "combo",   opts: ["openrouter","piper","chatterbox"], test: "tts" },
  default_language:                { ui: "combo",   opts: ["de","en","fr","es","it","nl"],
                                     labels: { de:"Deutsch",en:"Englisch",fr:"Französisch",es:"Spanisch",it:"Italienisch",nl:"Niederländisch" } },
  openrouter_llm_model:            { ui: "combo",   opts: ["google/gemini-3.1-flash-lite","openai/gpt-4.1-mini","google/gemini-2.5-flash","anthropic/claude-haiku-4-5-20251001","meta-llama/llama-3.3-70b-instruct"], test: "llm" },
  openrouter_tts_model:            { ui: "combo",   opts: ["google/gemini-3.1-flash-tts-preview","openai/gpt-4o-mini-tts"], test: "tts", testProvider: "openrouter" },
  openrouter_tts_voice:            { ui: "combo",   opts: ["Zephyr","Puck","Charon","Kore","Fenrir","Leda","Orus","Aoede","Callirrhoe","Enceladus","Algieba","Despina","Achernar"], test: "tts", testProvider: "openrouter" },
  piper_voice:                     { ui: "combo",   opts: ["de_DE-thorsten-high","en_US-ryan-high","en_US-lessac-high","en_GB-cori-high","es_ES-sharvard-medium","fr_FR-siwis-medium","it_IT-paola-medium","nl_NL-mls-medium","ru_RU-irina-medium","zh_CN-huayan-medium"], test: "tts", testProvider: "piper" },
  local_llm_system_prompt:         { ui: "textarea", test: "llm" },
  local_llm_temperature:           { ui: "range",   min: 0, max: 2, step: 0.1, test: "llm" },
  local_llm_top_p:                 { ui: "range",   min: 0, max: 1, step: 0.05, test: "llm" },
  local_llm_max_tokens:            { ui: "number",  min: 0, test: "llm" },
  tts_normalize_level:             { ui: "select",  opts: ["auto","full","light","off"], test: "tts" },
  audio_stream_default:            { ui: "select",  opts: ["true","false"],
                                     labels: { "true":"Aktiviert (satzweises Streaming)","false":"Deaktiviert (alles auf einmal)" }, test: "tts" },
  memory_extraction_enabled:       { ui: "select",  opts: ["true","false"],
                                     labels: { "true":"Aktiviert","false":"Deaktiviert" } },
  memory_extraction_every_n_turns: { ui: "number",  min: 1, max: 20 },
  daily_request_limit:             { ui: "number",  min: 0 },
};

const FIELD_GROUPS = [
  { label: "Provider & Sprache",        keys: ["default_stt_provider","default_llm_provider","default_tts_provider","default_language"] },
  { label: "OpenRouter-Modelle & Stimmen", keys: ["openrouter_llm_model","openrouter_tts_model","openrouter_tts_voice"] },
  { label: "Piper TTS",                 keys: ["piper_voice"] },
  { label: "Lokales LLM",              keys: ["local_llm_system_prompt","local_llm_temperature","local_llm_top_p","local_llm_max_tokens"] },
  { label: "TTS-Verarbeitung",         keys: ["tts_normalize_level","audio_stream_default"] },
  { label: "Gedächtnis",               keys: ["memory_extraction_enabled","memory_extraction_every_n_turns"] },
  { label: "Limits",                    keys: ["daily_request_limit"] },
];

let _settingsData = [];

async function loadSettings() {
  const container = $("#settings-content");
  container.innerHTML = '<p class="text-sm text-slate-400">lade …</p>';
  const data = await adminFetch("/api/admin/config");
  if (!data) { container.innerHTML = '<p class="text-sm text-red-500">Fehler beim Laden.</p>'; return; }
  _settingsData = data;
  container.innerHTML = "";
  const byKey = Object.fromEntries(data.map((d) => [d.key, d]));
  FIELD_GROUPS.forEach((group) => {
    const items = group.keys.map((k) => byKey[k]).filter(Boolean);
    if (!items.length) return;
    const section = document.createElement("div");
    section.className = "bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden";
    const header = document.createElement("div");
    header.className = "px-4 py-2.5 border-b border-slate-100 dark:border-slate-700 bg-slate-50/80 dark:bg-slate-800/60";
    header.innerHTML = `<h4 class="text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide">${escHtml(group.label)}</h4>`;
    section.appendChild(header);
    items.forEach((item, idx) => {
      const row = buildSettingRow(item);
      if (idx > 0) row.classList.add("border-t", "border-slate-100", "dark:border-slate-700/60");
      section.appendChild(row);
    });
    container.appendChild(section);
  });
}

function _buildControl(item, meta) {
  const val = item.effective_value;
  const id = "cfg-" + item.key;
  const cls = "rounded-lg border border-slate-300 dark:border-slate-600 bg-transparent px-3 py-2 text-xs font-mono focus:outline-none focus:ring-2 focus:ring-blue-500";

  if (meta.ui === "select") {
    const labels = meta.labels || {};
    const options = meta.opts.map((o) =>
      `<option value="${escHtml(o)}" ${o === val ? "selected" : ""}>${escHtml(labels[o] || o)}</option>`
    ).join("");
    return `<select id="${id}" class="${cls} flex-1">${options}</select>`;
  }

  if (meta.ui === "combo") {
    const inList = meta.opts.includes(val);
    const labels = meta.labels || {};
    const options = meta.opts.map((o) =>
      `<option value="${escHtml(o)}" ${o === val ? "selected" : ""}>${escHtml(labels[o] || o)}</option>`
    ).join("");
    const customSel = !inList ? 'selected' : '';
    return `
      <div class="flex-1 space-y-1.5">
        <select class="combo-sel w-full ${cls}">
          ${options}
          <option value="__custom__" ${customSel}>✏ Eigener Wert …</option>
        </select>
        <input id="${id}" type="text" value="${escHtml(val)}" placeholder="Eigenen Wert eingeben"
          class="combo-inp w-full ${cls} ${inList ? "hidden" : ""}" />
      </div>`;
  }

  if (meta.ui === "range") {
    return `
      <div class="flex-1 flex items-center gap-3">
        <input id="${id}" type="range" min="${meta.min}" max="${meta.max}" step="${meta.step}" value="${escHtml(val)}"
          class="flex-1 accent-blue-600" />
        <span class="range-val text-xs font-mono w-8 text-right">${escHtml(val)}</span>
      </div>`;
  }

  if (meta.ui === "number") {
    const extra = meta.max !== undefined ? `max="${meta.max}"` : "";
    return `<input id="${id}" type="number" min="${meta.min ?? 0}" ${extra} value="${escHtml(val)}" class="${cls} flex-1" />`;
  }

  if (meta.ui === "textarea") {
    return `<textarea id="${id}" rows="3" class="${cls} flex-1 resize-y">${escHtml(val)}</textarea>`;
  }

  return `<input id="${id}" type="text" value="${escHtml(val)}" class="${cls} flex-1" />`;
}

function buildSettingRow(item) {
  const meta = FIELD_META[item.key] || { ui: "text" };
  const overridden = item.is_overridden;
  const envName = item.key.toUpperCase();
  const testType = meta.test;

  const wrap = document.createElement("div");
  wrap.className = "px-4 py-3" + (overridden ? " bg-amber-50/40 dark:bg-amber-900/10" : "");

  const testBtn = testType
    ? `<button class="test-btn text-xs rounded-lg border border-slate-300 dark:border-slate-600 px-2.5 py-1.5 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors whitespace-nowrap">${testType === "tts" ? "🔊 Test" : testType === "llm" ? "💬 Test" : "🎤 Test"}</button>`
    : "";

  const resetBtn = overridden
    ? `<button class="reset-btn text-xs text-red-500 hover:text-red-700 dark:hover:text-red-400 transition-colors whitespace-nowrap">↩ Reset</button>`
    : "";

  wrap.innerHTML = `
    <div class="flex items-center gap-2 mb-2 flex-wrap">
      <span class="font-medium text-sm flex-1">${escHtml(item.label)}</span>
      ${overridden ? '<span class="text-xs rounded-full bg-amber-200 dark:bg-amber-800/60 text-amber-800 dark:text-amber-200 px-2 py-0.5 font-medium">überschrieben</span>' : ""}
      ${resetBtn}
    </div>
    <div class="flex gap-2 items-start flex-wrap">
      ${_buildControl(item, meta)}
      <div class="flex flex-col gap-1.5 shrink-0">
        <button class="save-btn rounded-lg bg-blue-600 hover:bg-blue-700 text-white text-xs px-3 py-2 font-medium transition-colors">Speichern</button>
        ${testBtn}
      </div>
    </div>
    <div class="flex items-baseline gap-2 mt-1.5 text-xs text-slate-400 flex-wrap">
      <span class="font-mono font-medium text-slate-500 dark:text-slate-400">${envName}</span>
      <span class="text-slate-300 dark:text-slate-600">·</span>
      <span>.env: <span class="font-mono">${escHtml(item.base_value) || "—"}</span></span>
      <span class="text-slate-300 dark:text-slate-600">·</span>
      <span>${escHtml(item.hint)}</span>
    </div>
    <div class="feedback-msg text-xs mt-1 hidden"></div>
    <div class="test-result text-xs mt-2 hidden rounded-lg bg-slate-100 dark:bg-slate-700/60 px-3 py-2 font-mono whitespace-pre-wrap max-h-28 overflow-y-auto"></div>
  `;

  // Combo-Synchronisation: Dropdown ↔ Textfeld
  const comboSel = wrap.querySelector(".combo-sel");
  const comboInp = wrap.querySelector(".combo-inp");
  if (comboSel && comboInp) {
    comboSel.addEventListener("change", () => {
      if (comboSel.value === "__custom__") {
        comboInp.classList.remove("hidden");
        comboInp.focus();
      } else {
        comboInp.value = comboSel.value;
        comboInp.classList.add("hidden");
      }
    });
    comboInp.addEventListener("input", () => {
      const match = Array.from(comboSel.options).find((o) => o.value === comboInp.value);
      comboSel.value = match ? match.value : "__custom__";
    });
  }

  // Range-Slider: Wert live anzeigen
  const rangeInp = wrap.querySelector('input[type="range"]');
  if (rangeInp) {
    rangeInp.addEventListener("input", () => {
      wrap.querySelector(".range-val").textContent = rangeInp.value;
    });
  }

  // Aktuellen Wert holen (aus Combo-Textfeld oder normalem Input)
  function getCurrentValue() {
    if (comboInp) return comboInp.value.trim();
    const inp = wrap.querySelector(`#cfg-${item.key}`);
    return inp ? (inp.value ?? inp.textContent ?? "").trim() : "";
  }

  // Feedback-Anzeige
  function showFeedback(ok, msg) {
    const fb = wrap.querySelector(".feedback-msg");
    fb.textContent = msg;
    fb.className = "feedback-msg text-xs mt-1 " + (ok ? "text-emerald-600 dark:text-emerald-400" : "text-red-500");
    setTimeout(() => { fb.className = "feedback-msg text-xs mt-1 hidden"; }, 3000);
  }

  // Speichern
  wrap.querySelector(".save-btn").addEventListener("click", async () => {
    const val = getCurrentValue();
    const res = await adminFetch(`/api/admin/config/${item.key}`, "PUT", { value: val });
    if (res !== null) {
      showFeedback(true, "✓ Gespeichert");
      item.is_overridden = true;
      item.effective_value = val;
      // Overrides-Badge nachrüsten falls noch nicht da
      if (!wrap.querySelector(".reset-btn")) {
        wrap.querySelector(".flex.items-center.gap-2").insertAdjacentHTML("beforeend",
          '<button class="reset-btn text-xs text-red-500 hover:text-red-700 dark:hover:text-red-400 transition-colors whitespace-nowrap">↩ Reset</button>');
        wrap.querySelector(".reset-btn").addEventListener("click", doReset);
      }
    } else {
      showFeedback(false, "✗ Fehler beim Speichern");
    }
  });

  // Reset
  async function doReset() {
    if (!confirm(`Override für „${item.label}" entfernen?\nDer .env-Wert gilt danach wieder.`)) return;
    const res = await adminFetch(`/api/admin/config/${item.key}`, "DELETE");
    if (res !== null) loadSettings();
  }
  wrap.querySelector(".reset-btn")?.addEventListener("click", doReset);

  // Test
  const testBtnEl = wrap.querySelector(".test-btn");
  if (testBtnEl) {
    testBtnEl.addEventListener("click", async () => {
      const resultEl = wrap.querySelector(".test-result");
      resultEl.textContent = "…";
      resultEl.classList.remove("hidden");
      testBtnEl.disabled = true;
      try {
        if (testType === "tts") {
          const body = { text: "Hallo, ich bin der Sprachassistent." };
          if (meta.testProvider) body.tts_provider = meta.testProvider;
          const r = await fetch("/api/speak", {
            method: "POST", headers: { "Content-Type": "application/json" },
            body: JSON.stringify(body),
          });
          if (!r.ok) throw new Error("HTTP " + r.status);
          const buf = await r.arrayBuffer();
          const view = new DataView(buf);
          const n = Math.floor(buf.byteLength / 2);
          audioCtx = audioCtx || new (window.AudioContext || window.webkitAudioContext)();
          const abuf = audioCtx.createBuffer(1, n, 24000);
          const ch = abuf.getChannelData(0);
          for (let i = 0; i < n; i++) ch[i] = view.getInt16(i * 2, true) / 32768;
          const src = audioCtx.createBufferSource();
          src.buffer = abuf; src.connect(audioCtx.destination); src.start();
          resultEl.textContent = "▶ Audio spielt …";
          src.onended = () => { resultEl.classList.add("hidden"); };
        } else if (testType === "llm") {
          const r = await fetch("/api/chat?debug=true", {
            method: "POST", headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ text: "Sag kurz Hallo.", tts_provider: "piper" }),
          });
          const d = await r.json();
          resultEl.textContent = d.response || d.semantic || d.trace?.response || JSON.stringify(d).slice(0, 200);
        } else if (testType === "stt") {
          const cfg = await fetch("/api/config").then((r) => r.json());
          const available = cfg.available?.stt_providers || [];
          const current = item.effective_value;
          resultEl.textContent = available.includes(current)
            ? `✓ Provider „${current}" ist verfügbar.`
            : `✗ Provider „${current}" nicht in verfügbaren: ${available.join(", ")}`;
        }
      } catch (e) {
        resultEl.textContent = "Fehler: " + e.message;
      }
      testBtnEl.disabled = false;
    });
  }

  return wrap;
}

$("#load-settings").addEventListener("click", loadSettings);
