// KI-Chat client: renders messages (markdown for assistant), sends messages,
// streams assistant tokens over SSE, and handles "new chat" reset.
// Tailwind CDN is loaded via the head partial; this file only handles logic.
(function () {
  const boot = window.__CHAT_BOOT__ || {};
  let activeId = boot.activeId || null;
  const log = document.getElementById('chatLog');
  const form = document.getElementById('chatForm');
  const input = document.getElementById('chatInput');
  const sendBtn = document.getElementById('sendBtn');
  const newThreadBtn = document.getElementById('newThreadBtn');
  const threadList = document.getElementById('threadList');
  let sending = false;

  // ----- helpers -----
  function escapeHtml(s) {
    const d = document.createElement('div');
    d.textContent = s == null ? '' : String(s);
    return d.innerHTML;
  }

  function scrollBottom() {
    if (log) log.scrollTop = log.scrollHeight;
  }

  // Minimal markdown → HTML. HTML is escaped first, so model output can never
  // inject markup; only the markdown tokens below are turned into tags. Link
  // targets are restricted to http(s)/mailto/relative so a `javascript:` URL
  // from the model cannot become a clickable XSS vector, and the URL is stripped
  // of characters that could break out of the href attribute.
  function link(url, label) {
    const safe = String(url).trim().replace(/["'<>`\\]/g, '');
    return `<a href="${safe}" target="_blank" rel="noopener noreferrer">${label}</a>`;
  }

  function inlineFmt(t) {
    // Fertige <a>-Tags werden geparkt, damit die Autolink-Regel unten nicht in
    // eine bereits gebaute href hineinläuft und den Tag zerlegt.
    const parked = [];
    const park = (html) => `\u0000${parked.push(html) - 1}\u0000`;

    let s = t
      .replace(/`([^`]+)`/g, '<code>$1</code>')
      .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
      .replace(/(^|[^*])\*([^*]+)\*/g, '$1<em>$2</em>')
      .replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (m, label, url) => {
        const u = String(url).trim();
        if (u && !/^(https?:|mailto:|\/|#)/i.test(u)) return m;
        return park(link(u, label));
      });

    // Nackte Quellen-URLs klickbar machen — bei Web-Recherchen nennt der
    // Assistent Belege oft als reine URL, nicht als Markdown-Link. Nur http(s),
    // und Satzzeichen am Ende gehören zum Satz, nicht zur Adresse.
    s = s.replace(/https?:\/\/[^\s<>"'`\u0000]+/g, (u) => {
      const m = u.match(/[.,;:!?)\]]+$/);
      const rein = m ? u.slice(0, -m[0].length) : u;
      return park(link(rein, rein)) + (m ? m[0] : '');
    });

    return s.replace(/\u0000(\d+)\u0000/g, (m, i) => parked[Number(i)]);
  }

  // Trennzeile einer Markdown-Tabelle: |---|:---:|---:| (mind. zwei Striche je
  // Spalte, Doppelpunkte für die Ausrichtung).
  function isTableSeparator(line) {
    return /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)*\|?\s*$/.test(line);
  }

  function tableCells(line) {
    return line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => c.trim());
  }

  function alignOf(sep) {
    const links = sep.startsWith(':');
    const rechts = sep.endsWith(':');
    if (links && rechts) return 'center';
    if (rechts) return 'right';
    return null; // linksbündig ist der Standard, kein style nötig
  }

  function renderMarkdown(src) {
    const esc = escapeHtml(src == null ? '' : String(src)).replace(/\r\n/g, '\n');
    const lines = esc.split('\n');
    let html = '';
    let i = 0;
    let para = [];
    const flushPara = () => {
      if (para.length) html += '<p>' + inlineFmt(para.join(' ')) + '</p>';
      para = [];
    };
    while (i < lines.length) {
      const line = lines[i];
      if (/^```/.test(line)) { // fenced code block
        flushPara();
        const code = [];
        i++;
        while (i < lines.length && !/^```/.test(lines[i])) { code.push(lines[i]); i++; }
        i++; // closing fence
        html += '<pre><code>' + code.join('\n') + '</code></pre>';
        continue;
      }
      const hm = line.match(/^(#{1,6})\s+(.*)$/);
      if (hm) {
        flushPara();
        const lvl = Math.min(hm[1].length, 3);
        html += '<h' + lvl + '>' + inlineFmt(hm[2].trim()) + '</h' + lvl + '>';
        i++; continue;
      }
      if (/^\s*[-*]\s+/.test(line)) { // bullet list
        flushPara();
        const items = [];
        while (i < lines.length && /^\s*[-*]\s+/.test(lines[i])) {
          items.push('<li>' + inlineFmt(lines[i].replace(/^\s*[-*]\s+/, '')) + '</li>');
          i++;
        }
        html += '<ul>' + items.join('') + '</ul>';
        continue;
      }
      if (/^\s*\d+\.\s+/.test(line)) { // numbered list
        flushPara();
        const items = [];
        while (i < lines.length && /^\s*\d+\.\s+/.test(lines[i])) {
          items.push('<li>' + inlineFmt(lines[i].replace(/^\s*\d+\.\s+/, '')) + '</li>');
          i++;
        }
        html += '<ol>' + items.join('') + '</ol>';
        continue;
      }
      // Tabelle (GFM): Kopfzeile + Trennzeile (|---|:--:|), dann Datenzeilen.
      // Das Modell nutzt Tabellen gern für Anforderung/Erfahrung-Vergleiche;
      // ohne diesen Zweig landeten sie als Pipe-Wüste in einem Absatz.
      if (line.indexOf('|') >= 0 && i + 1 < lines.length && isTableSeparator(lines[i + 1])) {
        flushPara();
        const kopf = tableCells(line);
        const ausricht = tableCells(lines[i + 1]).map(alignOf);
        i += 2;
        const zeilen = [];
        while (i < lines.length && lines[i].indexOf('|') >= 0 && !/^\s*$/.test(lines[i])) {
          zeilen.push(tableCells(lines[i]));
          i++;
        }
        const zelle = (tag, inhalt, idx) => {
          const a = ausricht[idx];
          return '<' + tag + (a ? ' style="text-align:' + a + '"' : '') + '>' + inlineFmt(inhalt || '') + '</' + tag + '>';
        };
        let t = '<div class="chat-md-table"><table><thead><tr>';
        kopf.forEach((c, idx) => { t += zelle('th', c, idx); });
        t += '</tr></thead><tbody>';
        for (const z of zeilen) {
          t += '<tr>';
          // An der Kopfbreite ausrichten: fehlende Zellen leer, überzählige raus —
          // sonst zerreißt eine krumme Zeile das Tabellenraster.
          for (let c = 0; c < kopf.length; c++) t += zelle('td', z[c], c);
          t += '</tr>';
        }
        html += t + '</tbody></table></div>';
        continue;
      }
      if (/^\s*([-*_])\1{2,}\s*$/.test(line)) { // Trennlinie
        flushPara();
        html += '<hr>';
        i++; continue;
      }
      if (/^\s*$/.test(line)) { flushPara(); i++; continue; }
      para.push(line.trim());
      i++;
    }
    flushPara();
    return html;
  }

  function clearLogToEmpty(msg) {
    if (!log) return;
    log.innerHTML =
      '<div class="h-full flex items-center justify-center text-gray-400 text-sm px-6">' +
      escapeHtml(msg) + '</div>';
  }

  // Build a message row. Assistant content is markdown-rendered + left aligned;
  // user content is escaped plain text with preserved line breaks.
  function buildRow(role, contentHtml) {
    const wrap = document.createElement('div');
    wrap.className = 'flex ' + (role === 'user' ? 'justify-end' : 'justify-start');
    const bubble = document.createElement('div');
    bubble.className = 'max-w-[85%] rounded-lg px-3 py-2 text-sm text-left ' +
      (role === 'user'
        ? 'bg-blue-600 text-white whitespace-pre-wrap break-words'
        : 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-100 chat-md');
    bubble.innerHTML = contentHtml;
    wrap.appendChild(bubble);
    return { wrap, bubble };
  }

  function addMessage(role, content) {
    if (log && log.querySelector('.h-full')) log.innerHTML = '';
    const html = role === 'user' ? escapeHtml(content) : renderMarkdown(content);
    const { wrap, bubble } = buildRow(role, html);
    log.appendChild(wrap);
    scrollBottom();
    return bubble;
  }

  function typingIndicator() {
    if (log && log.querySelector('.h-full')) log.innerHTML = '';
    const wrap = document.createElement('div');
    wrap.className = 'flex justify-start';
    wrap.id = 'typing';
    wrap.innerHTML = '<div class="rounded-lg px-3 py-2 bg-gray-100 dark:bg-gray-700 text-gray-500 flex gap-1">' +
      '<span class="typing-dot"></span><span class="typing-dot"></span><span class="typing-dot"></span></div>';
    log.appendChild(wrap);
    scrollBottom();
    return wrap;
  }

  // Titel des aktiven Threads in der Seitenleiste setzen (kein innerHTML: der
  // Titel kommt aus einer Modellantwort).
  function setThreadTitle(titel) {
    if (!titel || !activeId) return;
    const li = threadList && threadList.querySelector('li[data-thread-li="' + activeId + '"]');
    const titleEl = li && li.querySelector('.threadLink .font-medium');
    if (titleEl && titel !== titleEl.textContent) titleEl.textContent = titel;
  }

  function prependThread(id, titel) {
    if (!threadList) return;
    const empty = threadList.querySelector('li.text-center');
    if (empty) empty.remove();
    const li = document.createElement('li');
    li.setAttribute('data-thread-li', id);
    li.innerHTML =
      '<div class="flex items-center group">' +
        '<a href="/chat?thread=' + encodeURIComponent(id) + '" data-thread="' + id +
          '" class="threadLink flex-1 min-w-0 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700/40">' +
          '<div class="truncate font-medium">' + escapeHtml(titel || '(neuer Chat)') + '</div>' +
          '<div class="text-[11px] text-gray-400">gerade</div>' +
        '</a>' +
        '<button type="button" data-rename-thread="' + id +
          '" class="threadRename shrink-0 p-1.5 rounded text-gray-400 hover:text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/30 opacity-0 group-hover:opacity-100 focus:opacity-100" title="Namen ändern" aria-label="Namen ändern">' +
          '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">' +
            '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>' +
          '</svg>' +
        '</button>' +
        '<button type="button" data-delete-thread="' + id +
          '" class="threadDelete shrink-0 mr-2 p-1.5 rounded text-gray-400 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/30 opacity-0 group-hover:opacity-100 focus:opacity-100" title="Verlauf löschen" aria-label="Verlauf löschen">' +
          '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">' +
            '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6M1 7h22M9 7V4a1 1 0 011-1h4a1 1 0 011 1v3"></path>' +
          '</svg>' +
        '</button>' +
      '</div>';
    threadList.insertBefore(li, threadList.firstChild);
  }

  // ----- "Neuer Chat": reset client-side to a fresh empty conversation -----
  function resetToNewChat() {
    activeId = null;
    document.getElementById('activeThread').value = '';
    clearLogToEmpty('Stelle eine Frage zu deinen Bewerbungen.');
    if (input) { input.value = ''; input.disabled = false; input.style.height = 'auto'; input.focus(); }
    if (sendBtn) sendBtn.disabled = false;
    // deselect sidebar rows
    threadList && threadList.querySelectorAll('.bg-blue-50, .dark\\:bg-gray-700').forEach((el) => {
      el.classList.remove('bg-blue-50', 'dark:bg-gray-700');
    });
    history.replaceState(null, '', '/chat');
  }

  async function createThread() {
    const res = await fetch('/chat/api/threads', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ titel: '' }),
    });
    if (!res.ok) throw new Error('Thread konnte nicht angelegt werden.');
    const data = await res.json();
    activeId = data.id;
    document.getElementById('activeThread').value = activeId;
    prependThread(activeId, data.titel);
    history.replaceState(null, '', '/chat?thread=' + activeId);
    return activeId;
  }

  // Parse the SSE response body stream; call onEvent with each parsed data object.
  async function consumeSSE(res, onEvent) {
    const reader = res.body.getReader();
    const decoder = new TextDecoder();
    let buffer = '';
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      buffer += decoder.decode(value, { stream: true });
      let sep;
      while ((sep = buffer.indexOf('\n\n')) >= 0) {
        const block = buffer.slice(0, sep);
        buffer = buffer.slice(sep + 2);
        for (const line of block.split('\n')) {
          if (!line.startsWith('data:')) continue;
          const payload = line.slice(5).trim();
          if (!payload) continue;
          try { onEvent(JSON.parse(payload)); } catch (e) { /* ignore */ }
        }
      }
    }
  }

  async function sendMessage(text) {
    if (sending || !text.trim()) return;
    sending = true;
    sendBtn.disabled = true;

    if (!activeId) {
      try { await createThread(); }
      catch (e) { sending = false; sendBtn.disabled = false; alert(e.message); return; }
    }

    addMessage('user', text);
    input.value = '';
    input.style.height = 'auto';
    const typing = typingIndicator();
    let assistantBubble = null;
    let assistantText = '';

    try {
      const res = await fetch('/chat/api/threads/' + activeId + '/messages', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ content: text }),
      });
      if (!res.ok || !res.body) {
        let msg = 'Senden fehlgeschlagen (' + res.status + ')';
        try { const j = await res.json(); msg = j.error || msg; } catch (e) {}
        throw new Error(msg);
      }

      await consumeSSE(res, (ev) => {
        if (ev.type === 'tool') {
          // Tool lookups run before the first text token; surface a muted hint
          // inside the typing indicator so the user sees activity.
          const typingEl = document.getElementById('typing');
          if (typingEl && !assistantBubble) {
            const label = ev.label || ev.name || 'nachschlagen…';
            typingEl.innerHTML = '<div class="rounded-lg px-3 py-2 bg-gray-100 dark:bg-gray-700 text-gray-400 text-sm">' +
              '🔍 ' + escapeHtml(label) + '</div>';
          }
          scrollBottom();
        } else if (ev.type === 'token') {
          if (!assistantBubble) {
            if (typing) typing.remove();
            assistantBubble = addMessage('assistant', '');
          }
          assistantText += ev.content;
          assistantBubble.innerHTML = renderMarkdown(assistantText);
          scrollBottom();
        } else if (ev.type === 'done') {
          if (assistantBubble && ev.content) {
            assistantBubble.innerHTML = renderMarkdown(ev.content);
          } else if (!assistantBubble) {
            if (typing) typing.remove();
            addMessage('assistant', ev.content || '(keine Antwort)');
          }
          // Live-update the auto-generated thread title in the sidebar.
          setThreadTitle(ev.titel);
          scrollBottom();
        } else if (ev.type === 'titel') {
          // Kommt nach 'done' nach: der vom Modell formulierte Titel löst den
          // vorläufigen (abgeschnittene erste Nachricht) in der Seitenleiste ab.
          setThreadTitle(ev.titel);
        } else if (ev.type === 'error') {
          if (typing) typing.remove();
          if (!assistantBubble) addMessage('assistant', 'Fehler: ' + ev.message);
          else assistantBubble.innerHTML = renderMarkdown(assistantText + '\n\n_Fehler: ' + ev.message + '_');
        }
      });
    } catch (err) {
      if (typing) typing.remove();
      addMessage('assistant', 'Fehler: ' + (err.message || 'unbekannt'));
    } finally {
      if (typing && typing.parentNode) typing.remove();
      sending = false;
      sendBtn.disabled = false;
      input.focus();
    }
  }

  // ----- wiring -----
  if (input) {
    input.addEventListener('input', () => {
      input.style.height = 'auto';
      input.style.height = Math.min(input.scrollHeight, 160) + 'px';
    });
    input.addEventListener('keydown', (e) => {
      if (e.key === 'Enter' && !e.shiftKey) {
        e.preventDefault();
        sendMessage(input.value);
      }
    });
  }

  if (form) {
    form.addEventListener('submit', (e) => {
      e.preventDefault();
      sendMessage(input.value);
    });
  }

  if (newThreadBtn) {
    newThreadBtn.addEventListener('click', resetToNewChat);
  }

  // Rename a thread inline: replace the title with an input, save on
  // Enter/blur (PATCH), cancel on Escape.
  async function saveRename(input, link, id, original) {
    const val = input.value.trim();
    if (!val) { link.querySelector('.font-medium').textContent = original; return; }
    try {
      const res = await fetch('/chat/api/threads/' + id, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ titel: val }),
      });
      if (!res.ok) throw new Error('Speichern fehlgeschlagen');
      link.querySelector('.font-medium').textContent = val;
    } catch (err) {
      link.querySelector('.font-medium').textContent = original;
      alert(err.message);
    }
  }

  function startRename(li, id) {
    const link = li.querySelector('.threadLink');
    const titleEl = link.querySelector('.font-medium');
    const original = titleEl.textContent;
    const input = document.createElement('input');
    input.type = 'text';
    input.value = original;
    input.className = 'font-medium w-full bg-transparent border border-blue-400 rounded px-1 py-0.5 text-sm text-gray-800 dark:text-gray-100 focus:outline-none focus:ring-1 focus:ring-blue-500';
    titleEl.replaceWith(input);
    input.focus();
    input.select();
    let done = false;
    const commit = () => {
      if (done) return; done = true;
      const title = document.createElement('div');
      title.className = 'truncate font-medium';
      title.textContent = input.value.trim() || original;
      input.replaceWith(title);
      if (input.value.trim() && input.value.trim() !== original) {
        saveRename(input, link, id, original);
      } else {
        title.textContent = original;
      }
    };
    input.addEventListener('keydown', (e) => {
      if (e.key === 'Enter') { e.preventDefault(); commit(); }
      else if (e.key === 'Escape') { e.preventDefault(); done = true; const title = document.createElement('div'); title.className = 'truncate font-medium'; title.textContent = original; input.replaceWith(title); }
    });
    input.addEventListener('blur', commit);
  }

  if (threadList) {
    threadList.addEventListener('click', (e) => {
      const renameBtn = e.target.closest('[data-rename-thread]');
      if (renameBtn) {
        e.preventDefault();
        e.stopPropagation();
        const id = Number(renameBtn.getAttribute('data-rename-thread'));
        const li = threadList.querySelector('li[data-thread-li="' + id + '"]');
        if (li) startRename(li, id);
      }
    });
  }

  // Delete a thread from the sidebar. Event delegation: works for server-rendered
  // rows and ones added client-side (prependThread).
  if (threadList) {
    threadList.addEventListener('click', async (e) => {
      const btn = e.target.closest('[data-delete-thread]');
      if (!btn) return;
      e.preventDefault();
      e.stopPropagation();
      const id = Number(btn.getAttribute('data-delete-thread'));
      if (!id || !confirm('Diesen Chat-Verlauf wirklich löschen?')) return;
      try {
        const res = await fetch('/chat/api/threads/' + id, { method: 'DELETE' });
        if (!res.ok) throw new Error('Löschen fehlgeschlagen (' + res.status + ')');
      } catch (err) {
        alert(err.message);
        return;
      }
      const li = threadList.querySelector('li[data-thread-li="' + id + '"]');
      if (li) li.remove();
      // If the deleted thread was active, start a fresh chat.
      if (activeId === id) resetToNewChat();
      // If no threads remain, show the empty placeholder.
      if (!threadList.querySelector('li[data-thread-li]')) {
        const empty = document.createElement('li');
        empty.className = 'px-3 py-4 text-sm text-gray-400 text-center';
        empty.textContent = 'Noch keine Verläufe.';
        threadList.appendChild(empty);
      }
    });
  }

  // Render any pre-loaded messages (existing thread) with the same renderer.
  function renderInitial() {
    const blob = document.getElementById('initialMessages');
    if (!blob) return;
    let msgs = [];
    try { msgs = JSON.parse(blob.textContent); } catch (e) { return; }
    if (!Array.isArray(msgs) || !msgs.length) return;
    if (log.querySelector('.h-full')) log.innerHTML = '';
    msgs.forEach((m) => addMessage(m.role, m.content));
    scrollBottom();
  }

  renderInitial();
  scrollBottom();
  if (input && !input.disabled) input.focus();
})();