/**
 * ChiliColectivo Diagnose-Log – Fehler & wichtige Ereignisse sammeln, als Textdatei exportieren.
 */
(function (global) {
  'use strict';

  const STORAGE_KEY = 'chillicollectivoDiagnosticLog';
  const MAX_ENTRIES = 2500;

  const DETAIL_STRIP_KEYS = [
    'lastCustomerMessage',
    'customerSnippet',
    'profileTextSnippet',
    'text',
    'body',
    'message',
    'dictationText',
    'markedText',
    'composed',
    'visible',
    'preview',
    'customerMessage',
    'submitBody',
    'lastSubmitBody',
  ];

  const DETAIL_TEXT_KEY_PATTERN = /(snippet|text|body|message|content|preview|composed|visible|marked|dictation|inserted|removed|added|fieldhead|fieldtail|customer|tail|head|plain|clean|raw|corrected|committed|interim|rendered|before|after|result)/i;

  function detailKeyStoresText(key) {
    const k = String(key || '');
    if (!k || /len$/i.test(k) || /count$/i.test(k)) return false;
    if (DETAIL_STRIP_KEYS.includes(k)) return true;
    return DETAIL_TEXT_KEY_PATTERN.test(k);
  }

  function sanitizeDetailObject(detail, depth = 0) {
    if (!detail || typeof detail !== 'object' || detail instanceof Error) return detail;
    if (depth > 6) return { truncated: true };
    const out = {};
    for (const [key, value] of Object.entries(detail)) {
      if (value == null || value === '') continue;
      if (detailKeyStoresText(key)) {
        if (typeof value === 'string') {
          out[`${key}Len`] = value.length;
        } else if (typeof value === 'number') {
          out[key] = value;
        }
        continue;
      }
      if (typeof value === 'object' && !(value instanceof Error)) {
        out[key] = sanitizeDetailObject(value, depth + 1);
        continue;
      }
      out[key] = value;
    }
    return out;
  }

  let cache = null;
  let cacheLoaded = false;
  let saveTimer = null;

  function safeString(value) {
    if (value == null) return '';
    if (typeof value === 'string') return value;
    if (value instanceof Error) {
      return `${value.name}: ${value.message}${value.stack ? `\n${value.stack}` : ''}`;
    }
    try {
      return JSON.stringify(value);
    } catch (_) {
      return String(value);
    }
  }

  function getExtensionVersion() {
    try {
      return chrome?.runtime?.getManifest?.()?.version || 'unknown';
    } catch (_) {
      return 'unknown';
    }
  }

  function normalizeEntry(level, source, message, detail) {
    const rawDetail = detail && typeof detail === 'object' && !(detail instanceof Error)
      ? sanitizeDetailObject(detail)
      : null;

    return {
      ts: new Date().toISOString(),
      level: String(level || 'info').toLowerCase(),
      source: String(source || 'app').slice(0, 80),
      message: String(message || '').slice(0, 2000),
      detail: rawDetail != null ? safeString(rawDetail).slice(0, 3000) : (detail != null ? safeString(detail).slice(0, 3000) : ''),
      url: typeof location !== 'undefined' && location.href ? location.href.slice(0, 300) : '',
    };
  }

  async function ensureLoaded() {
    if (cacheLoaded) return cache || [];
    cacheLoaded = true;
    try {
      const data = await chrome.storage.local.get(STORAGE_KEY);
      cache = Array.isArray(data[STORAGE_KEY]) ? data[STORAGE_KEY] : [];
      let migrated = false;
      cache = cache.map((entry) => {
        if (!entry?.detail) return entry;
        const scrubbed = scrubStoredDetail(entry.detail);
        if (scrubbed !== entry.detail) {
          migrated = true;
          return { ...entry, detail: scrubbed };
        }
        return entry;
      });
      if (migrated) {
        await chrome.storage.local.set({ [STORAGE_KEY]: cache.slice(-MAX_ENTRIES) });
      }
    } catch (_) {
      cache = [];
    }
    return cache;
  }

  function schedulePersist() {
    if (saveTimer) return;
    saveTimer = setTimeout(async () => {
      saveTimer = null;
      try {
        if (cache) {
          await chrome.storage.local.set({ [STORAGE_KEY]: cache.slice(-MAX_ENTRIES) });
        }
      } catch (_) {}
    }, 300);
  }

  async function append(level, source, message, detail) {
    await ensureLoaded();
    if (!Array.isArray(cache)) cache = [];
    const entry = normalizeEntry(level, source, message, detail);
    cache.push(entry);
    if (cache.length > MAX_ENTRIES) cache = cache.slice(-MAX_ENTRIES);
    schedulePersist();
    return entry;
  }

  async function getAll() {
    await ensureLoaded();
    return cache.slice();
  }

  async function getCount() {
    await ensureLoaded();
    return cache.length;
  }

  async function clear() {
    cache = [];
    cacheLoaded = true;
    try {
      await chrome.storage.local.set({ [STORAGE_KEY]: [] });
    } catch (_) {}
  }

  function countBySource(entries) {
    const counts = {};
    for (const e of entries) {
      const src = String(e.source || 'unknown');
      counts[src] = (counts[src] || 0) + 1;
    }
    return counts;
  }

  function parseDetailObject(detailStr) {
    if (!detailStr) return null;
    try {
      const obj = JSON.parse(detailStr);
      return obj && typeof obj === 'object' ? obj : null;
    } catch (_) {
      return null;
    }
  }

  function scrubStoredDetail(detailStr) {
    if (!detailStr) return '';
    const obj = parseDetailObject(detailStr);
    if (!obj) return detailStr;
    return safeString(sanitizeDetailObject(obj)).slice(0, 3000);
  }

  function analyzeEntries(entries) {
    const counts = countBySource(entries);
    const warnEntries = entries.filter((e) => e.level === 'warn' || e.level === 'error');
    const anomalies = [];

    const replaceSpanMiss = counts['voice.replace-span-miss'] || 0;
    const fieldSpanLost = counts['voice.field-span-lost'] || 0;
    const fieldSpanCaptured = counts['voice.field-span-captured'] || 0;
    const fieldSpanReplaced = counts['voice.field-span-replaced'] || 0;
    if (fieldSpanLost > 0) {
      anomalies.push({
        severity: 'warn',
        code: 'field-span-lost',
        message: `${fieldSpanLost}× Feld-Markierung verloren – Text evtl. am Ende angehängt statt ersetzt`,
      });
    }
    if (fieldSpanCaptured > 0 && fieldSpanReplaced === 0 && fieldSpanLost === 0) {
      anomalies.push({
        severity: 'warn',
        code: 'field-span-not-applied',
        message: `${fieldSpanCaptured}× Feld-Markierung erfasst, aber 0× ersetzt – Ersetzung schlug fehl`,
      });
    }
    if (replaceSpanMiss >= 20) {
      const noCommitted = entries.filter((e) => e.source === 'voice.replace-span-miss'
        && String(e.detail || '').includes('no-committed-el')).length;
      anomalies.push({
        severity: 'info',
        code: 'replace-span-noise',
        message: `${replaceSpanMiss}× replace-span-miss (${noCommitted}× ohne Vorschau-Element) – in Direktmodus normal, verdeckt aber echte Fehler`,
      });
    }

    const recOnend = counts['voice.rec-onend'] || 0;
    if (recOnend >= 12) {
      anomalies.push({
        severity: 'warn',
        code: 'rec-onend-spam',
        message: `${recOnend}× STT-Neustart (rec-onend) – Erkennungs-Loop oder Mikro ohne Zielfeld`,
      });
    }

    const fieldLost = counts['voice.field-lost'] || 0;
    if (fieldLost > 0) {
      anomalies.push({
        severity: 'warn',
        code: 'field-lost',
        message: `${fieldLost}× Zielfeld verloren – Diktat ging ins Leere (Navigation während Aufnahme?)`,
      });
    }

    const interimRegression = counts['voice.preview-interim-regression'] || 0;
    if (interimRegression > 0) {
      anomalies.push({
        severity: 'warn',
        code: 'interim-regression',
        message: `${interimRegression}× STT-Text wurde kürzer (Pause/Regression) – Textverlust möglich`,
      });
    }

    const displayShrank = counts['voice.preview-display-shrank'] || 0;
    if (displayShrank > 0) {
      anomalies.push({
        severity: 'warn',
        code: 'display-shrank',
        message: `${displayShrank}× Vorschau-Anzeige schrumpfte – vorheriger Text evtl. gelöscht`,
      });
    }

    const stackOverflow = entries.filter((e) => e.source === 'window.onerror'
      && String(e.message || e.detail || '').includes('Maximum call stack size exceeded')).length;
    if (stackOverflow > 0) {
      anomalies.push({
        severity: 'error',
        code: 'stack-overflow',
        message: `${stackOverflow}× Stack-Overflow (Vorschau/Logging-Rekursion) – Extension-UI eingefroren, Mic-Toggle blockiert`,
      });
    }

    const recLoop = counts['voice.rec-onend-loop'] || 0;
    if (recLoop > 0) {
      anomalies.push({
        severity: 'warn',
        code: 'rec-onend-loop',
        message: `${recLoop}× STT-Neustart-Loop erkannt (≥8× in 60s)`,
      });
    }

    const flushSkipInflight = counts['voice.preview-flush-skip'] || 0;
    const tailDeduped = counts['voice.preview-tail-deduped'] || 0;
    const liveSkipped = counts['voice.preview-live-skipped'] || 0;
    const duplicateInsert = counts['voice.duplicate-insert'] || 0;
    const duplicateRoute = (counts['voice.duplicate-text-route'] || 0) + (counts['voice.duplicate-text-route-suppressed'] || 0);
    const flushedBlocked = counts['voice.flushed-preview-blocked'] || 0;
    const insertCommits = counts['voice.insert-commit-done'] || 0;
    const fieldClickFlush = entries.filter((e) => e.source === 'voice.preview-flush-start'
      && String(e.detail || '').includes('field-click')).length;

    if (tailDeduped > 0 || liveSkipped > 0) {
      anomalies.push({
        severity: 'warn',
        code: 'preview-tail-duplicate',
        message: `Doppelter Satz in Vorschau erkannt (${tailDeduped}× bereinigt, ${liveSkipped}× Live-Text übersprungen) – evtl. Klick/Taste doppelt oder STT-Wiederholung`,
      });
    }
    if (duplicateInsert > 0 || duplicateRoute > 0) {
      anomalies.push({
        severity: 'warn',
        code: 'duplicate-insert',
        message: `${duplicateInsert + duplicateRoute}× Text-Duplikat erkannt (Feld oder Route)`,
      });
    }
    if (flushSkipInflight > 0) {
      anomalies.push({
        severity: 'info',
        code: 'flush-inflight',
        message: `${flushSkipInflight}× paralleler Flush abgebrochen (Doppel-Klick/Taste)`,
      });
    }
    if (flushedBlocked > 0) {
      anomalies.push({
        severity: 'info',
        code: 'flushed-blocked',
        message: `${flushedBlocked}× spätes STT nach Flush blockiert`,
      });
    }
    if (fieldClickFlush > 0 && insertCommits === 0) {
      anomalies.push({
        severity: 'warn',
        code: 'click-flush-no-insert',
        message: `${fieldClickFlush}× Klick-Flush ohne insert-commit-done – kein Textfeld oder leer?`,
      });
    }

    const flushNoField = counts['voice.preview-flush-no-field'] || 0;
    if (flushNoField > 0) {
      anomalies.push({
        severity: 'warn',
        code: 'flush-no-field',
        message: `${flushNoField}× Vorschau-Flush ohne Chatfeld`,
      });
    }

    const previewCorrectedSkipped = counts['voice.preview-corrected-skipped'] || 0;
    if (previewCorrectedSkipped > 0) {
      anomalies.push({
        severity: 'info',
        code: 'preview-correct-blocked',
        message: `${previewCorrectedSkipped}× STT-Korrektur blockiert (wäre kürzer geworden)`,
      });
    }

    const respondMiss = counts['voice.customer-respond-miss'] || 0;
    if (respondMiss > 0) {
      anomalies.push({
        severity: 'warn',
        code: 'customer-respond-miss',
        message: `${respondMiss}× Antwort-Pfeil nicht gefunden — Chat offen? Kundennachricht sichtbar?`,
      });
    }

    const respondClicked = counts['voice.customer-respond-clicked'] || 0;
    const respondArmed = counts['voice.customer-respond-armed'] || 0;
    if (respondArmed > 0 && respondClicked === 0 && respondMiss === 0) {
      anomalies.push({
        severity: 'info',
        code: 'customer-respond-no-click',
        message: 'Antwort-Pfeil-Auto war aktiv, aber kein Klick — evtl. schon beantwortet oder keine Kundennachricht',
      });
    }

    const postSendSkipped = counts['voice.post-send-restart-skipped'] || 0;
    if (postSendSkipped > 0) {
      anomalies.push({
        severity: 'warn',
        code: 'post-send-restart-skipped',
        message: `${postSendSkipped}× Mic-Neustart nach Senden übersprungen – war Aufnahme aus?`,
      });
    }

    const outCounted = counts['voice.out-counted'] || 0;
    const sendPressed = counts['voice.send-button-pressed'] || 0;
    if (sendPressed > 0 && outCounted === 0 && postSendSkipped === 0) {
      anomalies.push({
        severity: 'info',
        code: 'send-without-out-log',
        message: `${sendPressed}× Sendeknopf, aber kein voice.out-counted – Zähler aus oder dedupliziert?`,
      });
    }

    const enterModeEntries = entries.filter((e) => {
      const d = parseDetailObject(e.detail);
      return d && d.enterToInsert === true;
    }).length;
    const directModeEntries = entries.filter((e) => {
      const d = parseDetailObject(e.detail);
      return d && d.enterToInsert === false;
    }).length;

    const topEvents = Object.entries(counts)
      .sort((a, b) => b[1] - a[1])
      .slice(0, 12);

    return {
      total: entries.length,
      warnCount: warnEntries.length,
      errorCount: entries.filter((e) => e.level === 'error').length,
      counts,
      anomalies,
      topEvents,
      insertCommits,
      enterModeEntries,
      directModeEntries,
      warnEntries: warnEntries.slice(-20),
    };
  }

  const DETAIL_PRIORITY_KEYS = [
    'enterToInsert', 'clickToInsert', 'insertMode', 'mode', 'reason', 'trigger', 'route',
    'kind', 'textLen', 'committedLen', 'interimLen', 'composedLen', 'visibleLen',
    'beforeLen', 'afterLen', 'markedTextLen', 'dictationTextLen',
    'previewEditing', 'recSessionAgeMs', 'rapidDeathCount', 'delayMs',
    'sharedPrefixWords', 'prefixStripped', 'count', 'totalMisses',
    'outCount', 'outTarget', 'outCounted', 'restartId',
    'micIndependent', 'conversationKey', 'customerKey', 'openDelayMs', 'postSendDelayMs',
    'hadCustomerMessage', 'matchedInCustomerMessage',
  ];

  function formatDetailForExport(detailStr) {
    const obj = parseDetailObject(detailStr);
    if (!obj) return detailStr ? [`  Detail: ${detailStr}`] : [];
    const lines = [];
    const shown = new Set();
    for (const key of DETAIL_PRIORITY_KEYS) {
      if (obj[key] == null || obj[key] === '') continue;
      const val = typeof obj[key] === 'object' ? JSON.stringify(obj[key]) : String(obj[key]);
      lines.push(`  ${key}: ${val.slice(0, 400)}`);
      shown.add(key);
    }
    const rest = {};
    Object.keys(obj).forEach((key) => {
      if (!shown.has(key)) rest[key] = obj[key];
    });
    if (Object.keys(rest).length) {
      lines.push(`  …rest: ${JSON.stringify(rest).slice(0, 3500)}`);
    }
    return lines;
  }

  function buildExportText(entries, meta = {}) {
    const lines = [
      'ChiliColectivo – Diagnose-Log',
      `Erstellt: ${new Date().toISOString()}`,
      `Extension: ${meta.version || getExtensionVersion()}`,
    ];
    if (meta.url) lines.push(`Seite: ${meta.url}`);
    if (meta.userAgent) lines.push(`Browser: ${meta.userAgent}`);
    lines.push(`Einträge: ${entries.length}`);
    lines.push('Hinweis: Keine Chat- oder Diktattexte — nur technische Metadaten.');
    lines.push('='.repeat(72));
    lines.push('');
    lines.push('Hinweise zur Fehlersuche (Mic / Diktat):');
    lines.push('');
    lines.push('── Chatfeld markieren / übersprechen (ohne Vorschau) ──');
    lines.push('  voice.field-span-captured       Markierung im Chatfeld erfasst');
    lines.push('  voice.field-span-restored       Markierung nach Pause wiederhergestellt');
    lines.push('  voice.field-span-replaced       Markierung durch Diktat ersetzt');
    lines.push('  voice.field-span-lost           Markierung verloren → evtl. Append am Ende (WARN)');
    lines.push('  voice.field-span-invalid/clamped Anker nach Textänderung angepasst');
    lines.push('');
    lines.push('── Vorschau → Chatfeld (Enter/Klick/Flush) ──');
    lines.push('  voice.preview-flush-start/done     Vorschau ins Feld (trigger: hotkey | field-click)');
    lines.push('  voice.preview-collect         Text für Flush zusammengestellt (committed+interim+DOM)');
    lines.push('  voice.preview-live-skipped    Live-Text nicht angehängt (steckt schon in Vorschau)');
    lines.push('  voice.preview-tail-deduped    Doppelter Satz am Ende entfernt vor Einfügen');
    lines.push('  voice.field-click-armed       Klick-Flush vorbereitet (Mousedown)');
    lines.push('  voice.preview-flush-start     Flush beginnt (trigger: field-click | hotkey | enter-while-recording)');
    lines.push('  voice.preview-flush-field-before  Chatfeld-Zustand VOR Einfügen');
    lines.push('  voice.preview-flush-done      Flush abgeschlossen (Feld NACH Einfügen)');
    lines.push('  voice.preview-flushed-marked  15s-Duplikat-Schutz nach Flush aktiv');
    lines.push('  voice.preview-display-cleared Grüne Vorschau geleert');
    lines.push('  voice.flushed-preview-blocked Text nach Flush blockiert');
    lines.push('  voice.post-flush-ignore-*     Späte STT nach Flush ignoriert/freigegeben');
    lines.push('');
    lines.push('── Texteinfügen ins Chatfeld ──');
    lines.push('  voice.insert-commit-start     Einfügen gestartet');
    lines.push('  voice.insert-commit-done       Einfügen fertig');
    lines.push('  voice.insert-commit-skip       Einfügen übersprungen (Prefix leer)');
    lines.push('  voice.insert-field-prefix-*   Feld hatte schon Anfang → nur Rest eingefügt');
    lines.push('  voice.insert-field-lead-*     Feldanfang wurde ersetzt');
    lines.push('  voice.duplicate-insert         Ganzes Stück schon im Feld');
    lines.push('');
    lines.push('── Voice-Final / STT-Pipeline ──');
    lines.push('  voice.insert-text-final        Final in insertText angekommen');
    lines.push('  voice.insert-final-to-preview  Final nur in grüne Vorschau (Enter-Modus)');
    lines.push('  voice.insert-final-prefix-*    Prefix gegen committed/lastInserted abgezogen');
    lines.push('  voice.final-emit-sent/skipped  Background-Übertragung');
    lines.push('  voice.silence-final-fired      Stille-Timer → Final');
    lines.push('  voice.rec-final-ignored        Final nach Flush blockiert');
    lines.push('  voice.duplicate-text-*         Spätes Final unterdrückt');
    lines.push('');
    lines.push('── Vorschau-Sync / Schutz-Status ──');
    lines.push('  voice.preview-sync-empty       Leere Vorschau-Sync (Flush-Schutz?)');
    lines.push('  voice.preview-segment-append   Neues Segment nach Pause angehängt');
    lines.push('  voice.preview-snapshot-committed Sichtbare Vorschau vor Pause/Neustart gesichert');
    lines.push('  voice.preview-interim-regression STT-Interim wurde kürzer (WARN)');
    lines.push('  voice.preview-display-shrank   Vorschau-Anzeige schrumpfte (WARN)');
    lines.push('  voice.preview-stack-detected   Möglicher Doppel-Stack in Vorschau (WARN)');
    lines.push('  voice.rec-onend-loop           STT-Neustart-Loop ≥8×/60s (WARN)');
    lines.push('  voice.rec-health-restart       STT war aus – Selbstheilung');
    lines.push('  voice.dictation-session-start  Aufnahme gestartet (Modus + Feld)');
    lines.push('  voice.dictation-session-end    Aufnahme beendet (Zusammenfassung)');
    lines.push('  voice.replace-span-miss-summary Ersetzungsbereich fehlte (aggregiert)');
    lines.push('  voice.preview-redo-reset       Redo-Status zurückgesetzt');
    lines.push('  voice.flush-guard-cleared      15s-Schutz bewusst gelöscht');
    lines.push('  voice.field-lost / field-rebound  Zielfeld verloren / wieder angebunden');
    lines.push('  voice.post-send-restart-start/done  Mic aus→an nach Senden (Out)');
    lines.push('  voice.post-send-restart-skipped   Mic-Neustart nach Senden übersprungen (WARN)');
    lines.push('  voice.out-counted                 Out gezählt + Mic-Neustart');
    lines.push('  voice.out-count-deduped           Out doppelt ignoriert, Mic-Neustart trotzdem');
    lines.push('  voice.out-count-disabled          Out-Zähler aus, Mic-Neustart nach Senden');
    lines.push('  voice.toggle-start/stop          Nutzer schaltet Mic an/aus');
    lines.push('  voice.send-button-pressed         Sendeknopf gedrückt');
    lines.push('');
    lines.push('── Sonstiges ──');
    lines.push('  voice.replace-span-*      Markierung/Ersetzung in Vorschau');
    lines.push('  voice.preview-*           Puffern, Anhängen, Duplikate in Vorschau');
    lines.push('  voice.rec-*               Spracherkennung Start/Ende/Neustart/Fehler');
    lines.push('  voice.mic-*               Mikro-Glimmen, Erkennung, Anomalien');
    lines.push('  voice.route-changed       Neuer Chat / URL-Wechsel');
    lines.push('  voice.customer-respond-armed          Antwort-Pfeil-Auto aktiv (einmal pro Profil)');
    lines.push('  voice.customer-respond-clicked        Antwort-Pfeil geklickt (trigger: profile-open)');
    lines.push('  voice.customer-respond-skipped        Antwort-Pfeil übersprungen (no-customer, bereits geklickt, …)');
    lines.push('  voice.customer-respond-miss           Antwort-Pfeil nicht gefunden (WARN)');
    lines.push('  voice.customer-respond-stopped        Antwort-Pfeil-Auto aus');
    lines.push('  voice.speech-corrected    STT-Korrektur inhaltlich');
    lines.push('  voice.speech-punct-corrected  Gesprochene Satzzeichen → , . ! ? " ...');
    lines.push('  voice.send-*              Senden, Sperren, manuelles Senden');
    lines.push('');
    lines.push('── Profilpflege (PP) ──');
    lines.push('  profile.customer-to-field   Kunde/Profil/Diktat → PP-Feld (explizite Zuordnung)');
    lines.push('  profile.insert              Wert in PP-Feld eingetragen');
    lines.push('  profile.insert-skipped        PP-Eintrag übersprungen (z.B. Duplikat)');
    lines.push('  profile.auto-fill-done        Auto-Fill nach Kundennachricht (mehrere Felder)');
    lines.push('  profile.command               Sprachbefehl (kunde/fake + Feld)');
    lines.push('  profile.focus                 PP-Feld fokussiert / Reclaim / Markierung');
    lines.push('  event: stick|reclaim|reclaim-skipped|selection-captured');
    lines.push('');
    lines.push('  PP Detail-Felder:');
    lines.push('    profile.customer-to-field        → explizit: was wohin floss');
    lines.push('    matchedInCustomerMessage       → true wenn Wert aus Kundennachricht stammt (ohne Nachrichtentext)');
    lines.push('    hadCustomerMessage               → ob Kundennachricht im Chat vorhanden war');
    lines.push('    extractedValue / extractionSource → was eingetragen wurde / woher (Profil|Text|Diktat|…)');
    lines.push('    fieldId / fieldLabel / side      → welches PP-Feld (Kunde/Fake)');
    lines.push('    valueBefore / valueAfter         → Feldinhalt vor/nach Eintrag');
    lines.push('    insertedSnippet / dictationText  → was eingetragen wurde');
    lines.push('    markedText                       → was im PP-Feld markiert war (Diktat-Ersetzung)');
    lines.push('    source                           → voice-dictation | voice-command | pp-auto-fill | pp-undo');
    lines.push('');
    lines.push('── Vorschau bearbeiten / markieren / STT-Fehler lernen ──');
    lines.push('  voice.preview-edit-selection-captured  Text markiert (siehe markedText)');
    lines.push('  voice.preview-edit-selection-cleared   Markierung aufgehoben');
    lines.push('  voice.preview-edit-text-changed        Manuell korrigiert (WARN: removedSnippet/addedSnippet)');
    lines.push('  voice.preview-edit-mouse-down/focus-*  Bearbeitung gestartet/beendet');
    lines.push('  voice.replace-span-captured            Ersetzungsbereich für Diktat erfasst');
    lines.push('  voice.preview-span-replaced            Markierung durch Diktat ersetzt');
    lines.push('  voice.preview-dom-replace              DOM-Ersetzung markierter Stelle');
    lines.push('  voice.preview-append / preview-buffered  Diktat ohne Markierung angehängt');
    lines.push('  voice.preview-corrected                STT hat ganze Vorschau korrigiert');
    lines.push('  voice.speech-corrected                 Roh-STT → korrigiert (raw/corrected)');
    lines.push('');
    lines.push('  Wichtige Detail-Felder im Log:');
    lines.push('    markedText / markedTextSnippet  → was du markiert hast');
    lines.push('    dictationText                   → was STT stattdessen schrieb');
    lines.push('    previewBefore / previewAfter    → Vorschau vor/nach Änderung');
    lines.push('    removedSnippet / addedSnippet     → was du manuell gelöscht/hinzugefügt hast');
    lines.push('    contextBefore / contextAfter      → Text links/rechts der Markierung');
    lines.push('    raw / corrected (speech-corrected)→ STT-Rohform vs. Korrektur');
    lines.push('');
    lines.push('Snapshot-Felder in Detail: fieldHead/fieldTail, recordingStartPos,');
    lines.push('  hasFlushedNorm, flushedPreviewAgeMs, lastInsertedSnippet, ignoreRestOfCurrentRec');
    lines.push('');
    lines.push('Bei Problemen: Log direkt nach dem Fehler exportieren und mitschicken.');
    lines.push('Hinweis: Kundennachrichten werden aus Datenschutzgründen nicht im Log gespeichert.');
    lines.push(`Speicher-Limit: ${MAX_ENTRIES} Einträge (älteste werden automatisch verworfen).`);
    lines.push('='.repeat(72));
    lines.push('');

    const analysis = analyzeEntries(entries);
    lines.push('── ZUSAMMENFASSUNG (automatisch) ──');
    lines.push(`Einträge: ${analysis.total} | WARN: ${analysis.warnCount} | ERROR: ${analysis.errorCount}`);
    lines.push(`Erfolgreiche Einfügungen (insert-commit-done): ${analysis.insertCommits}`);
    if (analysis.directModeEntries || analysis.enterModeEntries) {
      lines.push(`Modus-Hinweise im Log: enterToInsert=true: ~${analysis.enterModeEntries} Snapshots | false (Direkt): ~${analysis.directModeEntries}`);
    }
    lines.push('');
    if (analysis.anomalies.length) {
      lines.push('Erkannte Auffälligkeiten:');
      analysis.anomalies.forEach((a) => {
        const icon = a.severity === 'warn' ? '⚠' : a.severity === 'error' ? '✖' : 'ℹ';
        lines.push(`  ${icon} [${a.code}] ${a.message}`);
      });
      lines.push('');
    } else {
      lines.push('Keine automatisch erkannten Auffälligkeiten.');
      lines.push('');
    }
    lines.push('Top-Events:');
    analysis.topEvents.forEach(([src, n]) => {
      lines.push(`  ${String(n).padStart(4)}× ${src}`);
    });
    lines.push('');
    if (analysis.warnEntries.length) {
      lines.push('Letzte WARN/ERROR:');
      analysis.warnEntries.forEach((e) => {
        lines.push(`  [${e.ts}] ${String(e.level).toUpperCase()} · ${e.source}: ${String(e.message || '').slice(0, 120)}`);
      });
      lines.push('');
    }
    lines.push('='.repeat(72));
    lines.push('');

    for (const e of entries) {
      lines.push(`[${e.ts}] ${String(e.level || 'info').toUpperCase()} · ${e.source}`);
      lines.push(`  ${String(e.message || '').replace(/\n/g, '\n  ')}`);
      const detailLines = formatDetailForExport(e.detail);
      detailLines.forEach((line) => lines.push(line));
      if (e.url) lines.push(`  URL: ${e.url}`);
      lines.push('');
    }

    return lines.join('\n');
  }

  async function exportTextFile() {
    const entries = await getAll();
    const meta = {
      version: getExtensionVersion(),
      url: typeof location !== 'undefined' ? location.href : '',
      userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : '',
    };
    const text = buildExportText(entries, meta);
    const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
    const filename = `chillicollectivo-log-${stamp}.txt`;

    if (typeof document === 'undefined') {
      return { ok: false, reason: 'no-dom', text };
    }

    const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
    const blobUrl = URL.createObjectURL(blob);
    const link = document.createElement('a');
    link.href = blobUrl;
    link.download = filename;
    link.style.display = 'none';
    document.documentElement.appendChild(link);
    link.click();
    setTimeout(() => {
      URL.revokeObjectURL(blobUrl);
      link.remove();
    }, 500);

    await append('info', 'diagnostic-log', 'Log-Datei exportiert', { filename, entries: entries.length });
    return { ok: true, filename, entries: entries.length };
  }

  function shouldCaptureConsole(args) {
    const msg = args.map((a) => safeString(a)).join(' ');
    return /\[CHILLICOLLECTIVO\]|ChilliColectivo|Chillicollectivo|tfx-|Chilli/i.test(msg);
  }

  function installConsoleHooks() {
    if (typeof console === 'undefined' || console.__ccDiagPatched) return;
    console.__ccDiagPatched = true;

    ['warn', 'error'].forEach((level) => {
      const original = console[level]?.bind(console);
      if (!original) return;
      console[level] = (...args) => {
        original(...args);
        if (shouldCaptureConsole(args)) {
          append(level, 'console', args.map((a) => safeString(a)).join(' ')).catch(() => {});
        }
      };
    });
  }

  function installGlobalErrorHooks() {
    if (typeof window === 'undefined' || window.__ccDiagErrors) return;
    window.__ccDiagErrors = true;

    window.addEventListener('error', (ev) => {
      append('error', 'window.onerror', ev.message || 'Unbekannter Fehler', {
        file: ev.filename,
        line: ev.lineno,
        col: ev.colno,
        stack: ev.error?.stack,
      }).catch(() => {});
    });

    window.addEventListener('unhandledrejection', (ev) => {
      append('error', 'unhandledrejection', safeString(ev.reason), ev.reason?.stack).catch(() => {});
    });
  }

  function initContent() {
    installConsoleHooks();
    installGlobalErrorHooks();
    ensureLoaded().then(() => {
      try {
        if (typeof sessionStorage !== 'undefined' && sessionStorage.getItem('cc-diag-inited')) return;
        if (typeof sessionStorage !== 'undefined') sessionStorage.setItem('cc-diag-inited', '1');
      } catch (_) {}
      append('info', 'diagnostic-log', 'Diagnose-Log aktiv', {
        version: getExtensionVersion(),
      }).catch(() => {});
    });
  }

  function initBackground() {
    ensureLoaded().then(() => {
      append('info', 'background', 'Service Worker gestartet', {
        version: getExtensionVersion(),
      }).catch(() => {});
    });
  }

  const api = {
    append,
    error: (source, message, detail) => append('error', source, message, detail),
    warn: (source, message, detail) => append('warn', source, message, detail),
    info: (source, message, detail) => append('info', source, message, detail),
    debug: (source, message, detail) => append('debug', source, message, detail),
    getAll,
    getCount,
    clear,
    exportTextFile,
    buildExportText,
    analyzeEntries,
    initContent,
    initBackground,
    installConsoleHooks,
    installGlobalErrorHooks,
  };

  global.ChillicollectivoDiagnosticLog = api;

  if (typeof document !== 'undefined') {
    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', initContent, { once: true });
    } else {
      initContent();
    }
  }
})(typeof globalThis !== 'undefined' ? globalThis : (typeof self !== 'undefined' ? self : window));
