/* Documents hub v2 — NAS, labels, Docling */
(function () {
  const D = window.__DOC_DATA || {};
  const topWords = D.topWords || [];
  const summary = D.summary || {};
  const allDocuments = D.documents || [];

  let wordsChartInst = null;
  let sentimentChartInst = null;

  window.documentsDash = function documentsDash() {
    return {
      activeTab: 'overview',
      filterQuery: '',
      filterSentiment: '',
      filterType: '',
      filterLabel: '',
      filteredDocs: allDocuments,
      docTypes: [...new Set(allDocuments.map((d) => d.doc_type).filter(Boolean))],
      labelVocabulary: [],
      photoTaxonomy: [],
      documentTaxonomy: [],
      photoFilterLabel: '',
      activeDoc: null,
      docSelectedLabels: [],
      docLabelsInput: '',
      docNotesInput: '',
      photos: [],
      activePhoto: null,
      photoModalOpen: false,
      modalPhotoLabels: [],
      activeOcr: '',
      photoLabelsInput: '',
      photoNotesInput: '',
      activeItems: [],
      detections: [],
      liveRefresh: false,
      liveTimer: null,
      nasOcrBusy: false,
      doclingStatus: 'Docling laden…',
      doclingFiles: [],
      doclingHistory: [],
      doclingFilter: '',
      doclingExtFilter: '',
      doclingSelected: '',
      doclingOpts: { ocr: true, tables: true, page_images: false, picture_images: true, force_full_page_ocr: false },
      doclingFormats: ['markdown', 'html', 'json', 'text', 'doctags', 'yaml'],
      doclingExportFormats: ['markdown', 'html', 'json', 'text', 'doctags', 'yaml'],
      doclingBusy: false,
      doclingResult: null,
      doclingExports: {},
      doclingTables: [],
      doclingPictures: [],
      doclingMeta: {},
      doclingExportTab: 'markdown',
      doclingPreviewSearch: '',
      doclingFileStatus: {},
      nasFileTotal: 0,
      nasLoading: false,
      doclingEditMode: true,
      doclingEditContent: '',
      doclingAutoConvert: true,
      dlOptionsOpen: false,
      doclingFileLoading: false,
      doclingFileKind: '',
      doclingSupportedSuffixes: [],
      dlExtGroups: [
        { ext: '', label: 'Alles', icon: '' },
        { ext: '.pdf', label: 'PDF', icon: '📕' },
        { ext: '.docx', label: 'Word', icon: '📝' },
        { ext: '.doc', label: 'DOC', icon: '📝' },
        { ext: '.xlsx', label: 'Excel', icon: '📊' },
        { ext: '.xls', label: 'XLS', icon: '📊' },
        { ext: '.pptx', label: 'PPT', icon: '📽️' },
        { ext: '.ppt', label: 'PPT', icon: '📽️' },
        { ext: '.txt', label: 'Tekst', icon: '📄' },
        { ext: '.md', label: 'MD', icon: '📄' },
        { ext: '.csv', label: 'CSV', icon: '📋' },
        { ext: '.html', label: 'HTML', icon: '🌐' },
        { ext: '.json', label: 'JSON', icon: '{}' },
        { ext: '.png', label: 'IMG', icon: '🖼️' },
      ],
      pptTitle: 'Foodlinkk Presentatie',
      pptSubtitle: 'Foodlinkk',
      pptFilename: '',
      pptSlides: [],
      pptSourcePdf: '',
      pptLastOutput: '',
      pptBusy: false,
      pptDownloadPath: '',
      chatMessages: [],
      chatInput: '',
      chatBusy: false,
      chatStatus: 'RAG laden…',
      chatSearchQ: '',
      chatSearchHits: [],
      chatScopeFile: false,
      chatScopeClient: true,
      chatUseHerman: false,
      chatReindexing: false,
      llmProviders: [],
      chatLlmProviderId: '',
      nasDiagHint: '',
      autoSyncBusy: false,
      autoSyncLabel: 'Second brain: —',
      autoSyncTimer: null,
      linkClients: [],
      linkClientId: '',
      linkProjectId: '',
      linkPathInput: '',
      linkProjects: [],
      client360: { stats: {}, links: [], sentiment_breakdown: [], monthly_trends: [], top_words: [], stores: [], deals: [] },
      shareBrowsePath: '',
      shareBrowseEntries: [],
      shareBrowseLoading: false,
      selectedSharePaths: [],
      docVersions: [],
      doclingFileMtime: '',

      init() {
        try {
          document.querySelectorAll('.doc-row-fallback').forEach(function (el) { el.remove(); });
          window.__docActiveTab = this.activeTab;
          this.applyFilters();
          this.renderCharts(window.__vizMode || 'neo-bars');
          this.loadPhotos();
          this.loadLabelVocabulary();
          this.loadTaxonomy();
          this.loadShareFiles(false);
          this.browseShare('', false);
          this.loadLinkClients();
          this.loadLlmProviders();
          this.refreshAutoSyncStatus();
          document.addEventListener('foodlinkk:vizmode', (ev) => this.renderCharts(ev.detail.mode));
        } catch (e) {
          console.error('documentsDash init:', e);
          Cockpit.toast('Documenten UI gedeeltelijk geladen — ververs de pagina (Ctrl+F5)', 'error');
        }
      },

      switchTab(tab) {
        this.activeTab = tab;
        window.__docActiveTab = tab;
        if (window.DocTabs && window.DocTabs.show) window.DocTabs.show(tab);
        this.onTabShown(tab);
      },

      onTabShown(tab) {
        const runners = {
          labels: () => this.loadPhotos(),
          docling: () => this.initDocling(),
          powerpoint: () => this.initPowerpoint(),
          chat: () => this.initDocChat(),
          analytics360: () => this.initAnalytics360(),
        };
        const fn = runners[tab];
        if (fn) {
          try {
            const out = fn();
            if (out && typeof out.catch === 'function') out.catch(() => {});
          } catch (err) {
            console.error('switchTab', tab, err);
          }
        }
      },

      formatTs(iso) {
        if (!iso) return '—';
        try {
          const d = new Date(iso);
          if (Number.isNaN(d.getTime())) return String(iso).slice(0, 16);
          return d.toLocaleString('nl-NL', {
            day: '2-digit', month: 'short', year: 'numeric',
            hour: '2-digit', minute: '2-digit',
          });
        } catch (e) {
          return String(iso).slice(0, 16);
        }
      },

      async loadVersions(path) {
        if (!path) { this.docVersions = []; return; }
        try {
          const data = await Cockpit.api('/docling/versions?path=' + encodeURIComponent(path));
          this.docVersions = data.versions || [];
        } catch (e) {
          this.docVersions = [];
        }
      },

      async loadVersion(versionId) {
        if (!versionId || !this.doclingSelected) return;
        try {
          const data = await Cockpit.api('/docling/versions/' + versionId);
          if (data.version && data.version.content != null) {
            this.doclingEditContent = data.version.content;
            Cockpit.toast('Versie geladen: ' + this.formatTs(data.version.created_at), 'info');
          }
        } catch (e) {
          Cockpit.toast(e.message || 'Versie laden mislukt', 'error');
        }
      },

      async recordVersion(label) {
        if (!this.doclingSelected || !this.doclingEditContent) return;
        try {
          await Cockpit.api('/docling/versions', {
            method: 'POST',
            body: JSON.stringify({
              storage_path: this.doclingSelected,
              content: this.doclingEditContent,
              source_format: 'markdown',
              version_label: label || 'bewerking',
              nas_mtime: this.doclingFileMtime || null,
            }),
          });
          await this.loadVersions(this.doclingSelected);
        } catch (e) {        }
      },

      formatTs(iso) {
        if (!iso) return '—';
        try {
          const d = new Date(iso);
          if (Number.isNaN(d.getTime())) return String(iso).slice(0, 16);
          return d.toLocaleString('nl-NL', {
            day: '2-digit', month: 'short', year: 'numeric',
            hour: '2-digit', minute: '2-digit',
          });
        } catch (e) {
          return String(iso).slice(0, 16);
        }
      },

      async loadVersions(path) {
        if (!path) { this.docVersions = []; return; }
        try {
          const data = await Cockpit.api('/docling/versions?path=' + encodeURIComponent(path));
          this.docVersions = data.versions || [];
        } catch (e) {
          this.docVersions = [];
        }
      },

      async loadVersion(versionId) {
        if (!versionId || !this.doclingSelected) return;
        try {
          const data = await Cockpit.api('/docling/versions/' + versionId);
          if (data.version && data.version.content != null) {
            this.doclingEditContent = data.version.content;
            Cockpit.toast('Versie geladen: ' + this.formatTs(data.version.created_at), 'info');
          }
        } catch (e) {
          Cockpit.toast(e.message || 'Versie laden mislukt', 'error');
        }
      },

      async recordVersion(label) {
        if (!this.doclingSelected || !this.doclingEditContent) return;
        try {
          await Cockpit.api('/docling/versions', {
            method: 'POST',
            body: JSON.stringify({
              storage_path: this.doclingSelected,
              content: this.doclingEditContent,
              source_format: 'markdown',
              version_label: label || 'bewerking',
              nas_mtime: this.doclingFileMtime || null,
            }),
          });
          await this.loadVersions(this.doclingSelected);
        } catch (e) {}
      },

      async loadLlmProviders() {
        try {
          const data = await Cockpit.api('/api/settings/llm');
          this.llmProviders = data.providers || [];
          const def = data.default || this.llmProviders.find((p) => p.is_default);
          if (def && !this.chatLlmProviderId) this.chatLlmProviderId = def.id;
          const banner = document.getElementById('doc-hub-status');
          if (banner && def && def.provider_type === 'ollama') {
            banner.style.display = 'block';
            banner.textContent = 'Tip: Ollama is traag op CPU (~2 min). Voeg DeepSeek/Gemini toe via Instellingen → AI / LLM voor snelle chat & RAG.';
          }
          const cloud = this.llmProviders.find((p) => p.provider_type !== 'ollama' && p.api_key_set);
          if (cloud && def && def.provider_type === 'ollama') {
            this.chatLlmProviderId = cloud.id;
          }
        } catch (e) {}
      },

      onChatProviderChange() {
        const p = this.llmProviders.find((row) => Number(row.id) === Number(this.chatLlmProviderId));
        this.chatStatus = p ? (p.label + ' · ' + p.model) : 'Standaard LLM';
      },

      filteredPhotos() {
        if (!this.photoFilterLabel) return this.photos;
        return this.photos.filter((p) => (p.user_labels || []).includes(this.photoFilterLabel));
      },

      mergeShareItems(items) {
        const byPath = new Map(allDocuments.map((d) => [d.storage_path, d]));
        (items || []).forEach((f) => {
          const path = f.path || f.storage_path;
          if (!path) return;
          if (byPath.has(path)) {
            const ex = byPath.get(path);
            if (f.modified_at) ex.modified_at = f.modified_at;
            if (!ex.analyzed_at && f.modified_at) ex.modified_at = f.modified_at;
            return;
          }
          const row = {
            filename: f.filename || path.split('/').pop(),
            storage_path: path,
            doc_type: f.doc_type || 'general',
            word_count: f.word_count || 0,
            sentiment_label: f.sentiment_label || 'neutral',
            sentiment_compound: f.sentiment_compound || 0,
            analyzed_at: f.analyzed_at || '',
            modified_at: f.modified_at || '',
            user_labels: f.user_labels || [],
            label_notes: f.label_notes || '',
            from_share: !f.analyzed_at,
          };
          allDocuments.push(row);
          byPath.set(path, row);
        });
        this.docTypes = [...new Set(allDocuments.map((d) => d.doc_type).filter(Boolean))];
        this.applyFilters();
      },

      async loadShareFiles(showToast) {
        try {
          const r = await fetch('/api/admin/documents/share-files?limit=300');
          const data = await r.json();
          if (data.ok !== false) {
            this.mergeShareItems(data.items || []);
            this.nasFileTotal = data.total || (data.items || []).length;
            if (showToast) Cockpit.toast((this.nasFileTotal || 0) + ' bestanden op NAS', 'success');
          } else if (showToast) {
            Cockpit.toast(data.error || 'NAS laden mislukt', 'error');
          }
        } catch (e) {
          if (showToast) Cockpit.toast('NAS bestanden laden mislukt', 'error');
        }
      },

      async refreshShare(showToast) {
        try {
          if (showToast !== false) Cockpit.toast('NAS scan gestart (~30s)…', 'info');
          const scanCtrl = new AbortController();
          const scanTimer = setTimeout(() => scanCtrl.abort(), 90000);
          try {
            await fetch('/api/admin/documents/trigger-scan', { method: 'POST', signal: scanCtrl.signal });
          } finally {
            clearTimeout(scanTimer);
          }
          await this.loadShareFiles(showToast !== false);
        } catch (e) {
          if (showToast !== false) Cockpit.toast('Share scan mislukt — toon cache', 'error');
          await this.loadShareFiles(false);
        }
      },

      applyFilters() {
        const q = this.filterQuery.toLowerCase();
        this.filteredDocs = allDocuments.filter((d) => {
          if (this.filterSentiment && d.sentiment_label !== this.filterSentiment) return false;
          if (this.filterType && d.doc_type !== this.filterType) return false;
          if (this.filterLabel && !(d.user_labels || []).includes(this.filterLabel)) return false;
          if (q) {
            const hay = ((d.filename || '') + ' ' + (d.storage_path || '') + ' ' + (d.user_labels || []).join(' ')).toLowerCase();
            if (!hay.includes(q)) return false;
          }
          return true;
        });
      },

      async loadLabelVocabulary() {
        try {
          const r = await fetch('/api/admin/labels/vocabulary?limit=60');
          const data = await r.json();
          if (data.ok) this.labelVocabulary = data.labels || [];
        } catch (e) {}
      },

      async loadTaxonomy() {
        try {
          const r = await fetch('/api/admin/labels/taxonomy');
          const data = await r.json();
          if (data.ok) {
            this.photoTaxonomy = data.photo || [];
            this.documentTaxonomy = data.document || [];
          }
        } catch (e) {        }
      },

      formatTs(iso) {
        if (!iso) return '—';
        try {
          const d = new Date(iso);
          if (Number.isNaN(d.getTime())) return String(iso).slice(0, 16);
          return d.toLocaleString('nl-NL', {
            day: '2-digit', month: 'short', year: 'numeric',
            hour: '2-digit', minute: '2-digit',
          });
        } catch (e) {
          return String(iso).slice(0, 16);
        }
      },

      async loadVersions(path) {
        if (!path) { this.docVersions = []; return; }
        try {
          const data = await Cockpit.api('/docling/versions?path=' + encodeURIComponent(path));
          this.docVersions = data.versions || [];
        } catch (e) {
          this.docVersions = [];
        }
      },

      async loadVersion(versionId) {
        if (!versionId || !this.doclingSelected) return;
        try {
          const data = await Cockpit.api('/docling/versions/' + versionId);
          if (data.version && data.version.content != null) {
            this.doclingEditContent = data.version.content;
            Cockpit.toast('Versie geladen: ' + this.formatTs(data.version.created_at), 'info');
          }
        } catch (e) {
          Cockpit.toast(e.message || 'Versie laden mislukt', 'error');
        }
      },

      async recordVersion(label) {
        if (!this.doclingSelected || !this.doclingEditContent) return;
        try {
          await Cockpit.api('/docling/versions', {
            method: 'POST',
            body: JSON.stringify({
              storage_path: this.doclingSelected,
              content: this.doclingEditContent,
              source_format: 'markdown',
              version_label: label || 'bewerking',
              nas_mtime: this.doclingFileMtime || null,
            }),
          });
          await this.loadVersions(this.doclingSelected);
        } catch (e) {}
      },

      openDocLabel(doc) {
        this.activeDoc = doc;
        this.docSelectedLabels = [...(doc.user_labels || [])];
        this.docLabelsInput = '';
        this.docNotesInput = doc.label_notes || '';
      },

      isShareSelected(path) {
        return this.selectedSharePaths.includes(path);
      },

      toggleShareSelect(path) {
        const idx = this.selectedSharePaths.indexOf(path);
        if (idx >= 0) this.selectedSharePaths.splice(idx, 1);
        else this.selectedSharePaths.push(path);
      },

      openInWorkbench(path) {
        if (!path) return;
        const folder = path.includes('/') ? path.replace(/\/[^/]+$/, '') : '';
        const open = async () => {
          if (folder !== this.shareBrowsePath) {
            await this.browseShare(folder, false);
          }
          this.switchTab('docling');
          await this.openDoclingFile(path);
        };
        open().catch((e) => Cockpit.toast(e.message || 'Openen mislukt', 'error'));
      },

      openSelectedInWorkbench() {
        const paths = [...this.selectedSharePaths];
        if (!paths.length) return;
        this.openInWorkbench(paths[0]);
        if (paths.length > 1) {
          Cockpit.toast(paths.length + ' geselecteerd — eerste geopend in Workbench', 'info');
        }
      },

      sharePathParts() {
        return (this.shareBrowsePath || '').split('/').filter(Boolean);
      },

      sharePathPrefix(idx) {
        return this.sharePathParts().slice(0, idx + 1).join('/');
      },

      shareBrowseFolders() {
        return (this.shareBrowseEntries || []).filter((e) => e.type === 'folder');
      },

      async browseShare(path, showToast) {
        const rel = (path || '').replace(/^\/+/, '').replace(/\/+$/, '');
        this.shareBrowsePath = rel;
        this.shareBrowseLoading = true;
        try {
          const r = await fetch('/api/admin/documents/share-browse?path=' + encodeURIComponent(rel) + '&limit=800');
          const data = await r.json();
          if (data.ok !== false && Array.isArray(data.entries)) {
            this.shareBrowseEntries = data.entries;
            const files = data.entries.filter((e) => e.type === 'file');
            files.forEach((f) => {
              const ex = this.doclingFiles.find((x) => x.path === f.path);
              if (!ex) {
                this.doclingFiles.push({
                  path: f.path,
                  filename: f.filename || f.name,
                  size: f.size,
                  modified_at: f.modified_at,
                });
              }
            });
            if (showToast) Cockpit.toast((data.entries.length || 0) + ' items in ' + (rel || 'Share'), 'success');
          } else {
            await this.loadAllNasFiles(false);
            this.buildShareEntriesFromFiles();
            if (showToast) Cockpit.toast('Share map geladen (flat)', 'info');
          }
        } catch (e) {
          await this.loadAllNasFiles(false);
          this.buildShareEntriesFromFiles();
        } finally {
          this.shareBrowseLoading = false;
        }
      },

      browseShareParent() {
        const parts = this.sharePathParts();
        parts.pop();
        this.browseShare(parts.join('/'), false);
      },

      buildShareEntriesFromFiles() {
        const prefix = this.shareBrowsePath ? this.shareBrowsePath + '/' : '';
        const folders = new Set();
        const files = [];
        this.doclingFiles.forEach((f) => {
          const path = f.path || '';
          if (prefix && !path.startsWith(prefix)) return;
          if (!prefix && path.includes('/')) {
            folders.add(path.split('/')[0]);
            return;
          }
          const rest = prefix ? path.slice(prefix.length) : path;
          if (!rest) return;
          const slash = rest.indexOf('/');
          if (slash >= 0) folders.add(rest.slice(0, slash));
          else files.push({ type: 'file', path, filename: f.filename, name: f.filename, size: f.size });
        });
        this.shareBrowseEntries = [
          ...[...folders].sort().map((name) => ({
            type: 'folder',
            name,
            path: prefix ? prefix.replace(/\/$/, '') + (prefix.endsWith('/') ? '' : '/') + name : name,
          })),
          ...files,
        ];
      },

      toggleDocLabel(id) {
        const idx = this.docSelectedLabels.indexOf(id);
        if (idx >= 0) this.docSelectedLabels.splice(idx, 1);
        else this.docSelectedLabels.push(id);
      },

      async saveDocLabels() {
        if (!this.activeDoc) return;
        const labels = [...this.docSelectedLabels];
        this.docLabelsInput.split(',').map((s) => s.trim()).filter(Boolean).forEach((t) => {
          if (!labels.includes(t)) labels.push(t);
        });
        try {
          const r = await Cockpit.api('/documents/labels', {
            method: 'PATCH',
            body: JSON.stringify({
              storage_path: this.activeDoc.storage_path,
              labels,
              notes: this.docNotesInput || null,
            }),
          });
          const saved = r.document || {};
          this.activeDoc.user_labels = saved.user_labels || labels;
          this.activeDoc.label_notes = saved.label_notes || this.docNotesInput;
          const idx = allDocuments.findIndex((d) => d.storage_path === this.activeDoc.storage_path);
          if (idx >= 0) {
            allDocuments[idx].user_labels = this.activeDoc.user_labels;
            allDocuments[idx].label_notes = this.activeDoc.label_notes;
          }
          this.applyFilters();
          await this.loadLabelVocabulary();
          Cockpit.toast('Document gelabeld', 'success');
        } catch (e) {
          Cockpit.toast(e.message, 'error');
        }
      },

      toggleModalLabel(id) {
        const idx = this.modalPhotoLabels.indexOf(id);
        if (idx >= 0) this.modalPhotoLabels.splice(idx, 1);
        else this.modalPhotoLabels.push(id);
      },

      async openPhotoModal(id) {
        this.activePhoto = id;
        this.photoModalOpen = true;
        document.body.style.overflow = 'hidden';
        this.modalPhotoLabels = [];
        const p = this.photos.find((x) => x.id === id);
        if (p && p.user_labels) this.modalPhotoLabels = [...p.user_labels];
        this.photoLabelsInput = '';
        this.photoNotesInput = p ? (p.label_notes || '') : '';
        this.activeOcr = p ? (p.ocr_preview || '') : '';
        try {
          const r = await Cockpit.api('/photos/' + id + '/detections');
          this.detections = r.detections || [];
          this.activeItems = r.extracted_items || [];
          this.activeOcr = r.ocr_text || this.activeOcr;
          this.modalPhotoLabels = [...(r.user_labels || this.modalPhotoLabels)];
          this.photoNotesInput = r.label_notes || this.photoNotesInput;
        } catch (e) {
          Cockpit.toast(e.message, 'error');
        }
      },

      closePhotoModal() {
        this.photoModalOpen = false;
        document.body.style.overflow = '';
      },

      async saveModalPhotoLabels() {
        if (!this.activePhoto) return;
        const labels = [...this.modalPhotoLabels];
        this.photoLabelsInput.split(',').map((s) => s.trim()).filter(Boolean).forEach((t) => {
          if (!labels.includes(t)) labels.push(t);
        });
        if (!labels.length) return Cockpit.toast('Kies minimaal één label-type', 'error');
        try {
          const r = await Cockpit.api('/photos/' + this.activePhoto + '/labels', {
            method: 'PATCH',
            body: JSON.stringify({ labels, notes: this.photoNotesInput || null }),
          });
          const saved = r.photo || {};
          const p = this.photos.find((x) => x.id === this.activePhoto);
          if (p) {
            p.user_labels = saved.user_labels || labels;
            p.label_notes = saved.label_notes || this.photoNotesInput;
          }
          await this.loadLabelVocabulary();
          this.closePhotoModal();
          Cockpit.toast('Foto gelabeld', 'success');
        } catch (e) {
          Cockpit.toast(e.message, 'error');
        }
      },

      labelDisplayName(id) {
        const t = this.photoTaxonomy.find((x) => x.id === id);
        return t ? t.label : id;
      },

      async batchNasOcr(force) {
        if (this.nasOcrBusy) return;
        this.nasOcrBusy = true;
        Cockpit.toast('NAS OCR gestart…', 'info');
        try {
          const url = '/api/admin/documents/nas-ocr' + (force ? '?force=true' : '');
          const r = await fetch(url, { method: 'POST' });
          const data = await r.json();
          if (!r.ok || !data.ok) throw new Error(data.detail || data.error || 'OCR mislukt');
          Cockpit.toast((data.images_extracted || 0) + ' afbeeldingen ge-OCR\'d', 'success');
          await this.loadPhotos();
        } catch (e) {
          Cockpit.toast(e.message || 'NAS OCR mislukt', 'error');
        } finally {
          this.nasOcrBusy = false;
        }
      },

      renderCharts(mode) {
        mode = mode || window.__vizMode || 'neo-bars';
        if (typeof Chart === 'undefined') return;
        if (wordsChartInst) { wordsChartInst.destroy(); wordsChartInst = null; }
        if (sentimentChartInst) { sentimentChartInst.destroy(); sentimentChartInst = null; }
        const dash = this;
        const wc = document.getElementById('wordsChart');
        const sc = document.getElementById('sentimentChart');
        if (!wc || !sc) return;
        wordsChartInst = new Chart(wc, {
          type: 'bar',
          data: {
            labels: topWords.slice(0, 10).map((w) => w.lemma),
            datasets: [{
              data: topWords.slice(0, 10).map((w) => w.total_count),
              backgroundColor: 'rgba(0,229,255,0.75)',
              borderWidth: 0,
              borderRadius: 6,
            }],
          },
          options: {
            responsive: true,
            maintainAspectRatio: false,
            plugins: { legend: { display: false } },
            onClick: (_, els) => {
              if (els[0]) {
                dash.filterQuery = topWords[els[0].index].lemma;
                dash.applyFilters();
              }
            },
            scales: {
              y: { beginAtZero: true, ticks: { color: '#64748b', font: { size: 9 } } },
              x: { ticks: { color: '#64748b', font: { size: 9 } } },
            },
          },
        });
        sentimentChartInst = new Chart(sc, {
          type: 'doughnut',
          data: {
            labels: ['Positief', 'Neutraal', 'Negatief'],
            datasets: [{
              data: [summary.positive, summary.neutral, summary.negative],
              backgroundColor: ['#4ade80', '#64748b', '#fb7185'],
              borderWidth: 0,
            }],
          },
          options: {
            responsive: true,
            maintainAspectRatio: false,
            plugins: { legend: { labels: { color: '#94a3b8', font: { size: 9 } } } },
            onClick: (_, els) => {
              if (!els[0]) return;
              dash.filterSentiment = ['positive', 'neutral', 'negative'][els[0].index];
              dash.applyFilters();
            },
          },
        });
      },

      async loadPhotos() {
        try {
          const r = await Cockpit.api('/photos?limit=80');
          this.photos = r.photos || [];
        } catch (e) {}
      },

      toggleLive() {
        if (this.liveTimer) clearInterval(this.liveTimer);
        if (this.liveRefresh) {
          this.liveTimer = setInterval(() => {
            this.loadPhotos();
            this.refreshShare(false);
          }, 15000);
        }
      },

      async uploadPhoto(ev) {
        const file = ev.target.files && ev.target.files[0];
        if (!file) return;
        const b64 = await new Promise((res, rej) => {
          const r = new FileReader();
          r.onload = () => res(r.result.split(',')[1]);
          r.onerror = rej;
          r.readAsDataURL(file);
        });
        try {
          await Cockpit.api('/photos/analyze', {
            method: 'POST',
            body: JSON.stringify({ image_b64: b64, source: 'upload', filename: file.name }),
          });
          Cockpit.toast('Foto geanalyseerd', 'success');
          await this.loadPhotos();
        } catch (e) {
          Cockpit.toast(e.message, 'error');
        }
        ev.target.value = '';
      },

      /* ── Docling ── */
      doclingFilteredFiles() {
        const q = (this.doclingFilter || '').toLowerCase();
        const fromBrowse = (this.shareBrowseEntries || [])
          .filter((e) => e.type === 'file')
          .map((e) => ({
            path: e.path,
            filename: e.filename || e.name,
            size: e.size,
            modified_at: e.modified_at,
          }));
        const pool = fromBrowse.length ? fromBrowse : this.doclingFiles.filter((f) => {
          const path = f.path || '';
          const prefix = this.shareBrowsePath ? this.shareBrowsePath.replace(/\/?$/, '/') : '';
          if (prefix) {
            if (!path.startsWith(prefix)) return false;
            if (path.slice(prefix.length).includes('/')) return false;
          } else if (path.includes('/')) {
            return false;
          }
          return true;
        });
        return pool.filter((f) => {
          if (this.doclingExtFilter) {
            const ext = this.doclingExtFilter.startsWith('.') ? this.doclingExtFilter : '.' + this.doclingExtFilter;
            const pathExt = this.fileExtension(f.path);
            if (ext === '.png') {
              if (!['.png', '.jpg', '.jpeg', '.webp', '.gif', '.bmp', '.tiff', '.tif'].includes(pathExt)) return false;
            } else if (pathExt !== ext.toLowerCase()) return false;
          }
          if (!q) return true;
          return ((f.path || '') + ' ' + (f.filename || '')).toLowerCase().includes(q);
        });
      },

      fileExtension(path) {
        const p = (path || '').toLowerCase();
        const i = p.lastIndexOf('.');
        return i >= 0 ? p.slice(i) : '';
      },

      fileIcon(path) {
        const ext = this.fileExtension(path);
        const map = {
          '.pdf': '📕', '.docx': '📝', '.doc': '📝', '.xlsx': '📊', '.xls': '📊',
          '.pptx': '📽️', '.ppt': '📽️', '.txt': '📄', '.md': '📄', '.csv': '📋',
          '.html': '🌐', '.htm': '🌐', '.json': '{}', '.xml': '📰', '.yaml': '⚙️',
          '.png': '🖼️', '.jpg': '🖼️', '.jpeg': '🖼️', '.webp': '🖼️', '.gif': '🖼️',
        };
        return map[ext] || '📁';
      },

      fileKind(path) {
        const ext = this.fileExtension(path);
        if (this.isPlainTextExt(ext)) return 'text';
        if (this.isImageExt(ext)) return 'image';
        if (ext === '.pdf') return 'pdf';
        if (['.docx', '.doc', '.xlsx', '.xls', '.pptx', '.ppt'].includes(ext)) return 'office';
        return 'other';
      },

      fileKindLabel(path) {
        const k = this.fileKind(path);
        return { text: 'Tekst', image: 'Afbeelding', pdf: 'PDF', office: 'Office', other: 'Bestand' }[k] || 'Bestand';
      },

      isPlainTextExt(ext) {
        return ['.txt', '.md', '.csv', '.json', '.xml', '.html', '.htm', '.yaml', '.yml', '.log', '.rtf'].includes(ext);
      },

      isImageExt(ext) {
        return ['.png', '.jpg', '.jpeg', '.webp', '.gif', '.bmp', '.tiff', '.tif'].includes(ext);
      },

      isDoclingSupported(path) {
        const ext = this.fileExtension(path);
        if (!ext) return false;
        const supported = this.doclingSupportedSuffixes.length
          ? this.doclingSupportedSuffixes
          : ['.pdf', '.docx', '.pptx', '.xlsx', '.html', '.htm', '.md', '.txt', '.png', '.jpg', '.jpeg', '.webp', '.tiff', '.tif', '.bmp', '.gif'];
        return supported.includes(ext);
      },

      pptPdfFiles() {
        return this.doclingFiles.filter((f) => (f.path || '').toLowerCase().endsWith('.pdf'));
      },

      pptPptxFiles() {
        return this.doclingFiles.filter((f) => (f.path || '').toLowerCase().endsWith('.pptx'));
      },

      async loadAllNasFiles(showToast) {
        this.nasLoading = true;
        try {
          if (showToast) Cockpit.toast('NAS bestanden laden…', 'info');
          const r = await fetch('/api/admin/documents/share-files?limit=500');
          const data = await r.json();
          const byPath = new Map();
          allDocuments.forEach((d) => {
            byPath.set(d.storage_path, {
              path: d.storage_path,
              filename: d.filename,
              word_count: d.word_count,
              sentiment_label: d.sentiment_label,
              user_labels: d.user_labels || [],
            });
          });
          (data.items || []).forEach((f) => {
            const path = f.path || f.storage_path;
            if (!path) return;
            const ex = byPath.get(path) || {};
            byPath.set(path, {
              ...ex,
              path,
              filename: f.filename || path.split('/').pop(),
              size: f.size,
              modified_at: f.modified_at,
            });
          });
          this.doclingFiles = Array.from(byPath.values()).sort((a, b) =>
            (a.filename || '').localeCompare(b.filename || ''),
          );
          this.nasFileTotal = data.total || this.doclingFiles.length;
          this.buildShareEntriesFromFiles();
          if (showToast) Cockpit.toast(this.nasFileTotal + ' bestanden op NAS', 'success');
        } catch (e) {
          if (showToast) Cockpit.toast('NAS laden mislukt', 'error');
        } finally {
          this.nasLoading = false;
        }
      },

      fileDoclingStatus(path) {
        return this.doclingFileStatus[path] || '';
      },

      selectDoclingFile(path) {
        this.openInWorkbench(path);
      },

      async openDoclingFile(path) {
        if (!path || (this.doclingFileLoading && this.doclingSelected === path)) return;
        this.doclingSelected = path;
        const fileMeta = this.doclingFiles.find((f) => f.path === path) || {};
        this.doclingFileMtime = fileMeta.modified_at || '';
        this.doclingFileKind = this.fileKind(path);
        this.doclingEditMode = true;
        this.doclingFileLoading = true;
        this.doclingResult = null;
        this.doclingExports = {};
        this.doclingTables = [];
        this.doclingPictures = [];
        this.doclingEditContent = '';
        await this.loadVersions(path);

        const ext = this.fileExtension(path);
        if (this.doclingFileKind === 'pdf') this.doclingExportTab = 'original';
        else if (this.doclingFileKind === 'image') this.doclingExportTab = 'original';
        else this.doclingExportTab = 'rendered';

        try {
          const hasDraft = await this.loadWorkspace(path);
          if (hasDraft) {
            this.doclingFileLoading = false;
            return;
          }

          if (this.isPlainTextExt(ext)) {
            await this.loadNasRawText(path);
            this.doclingFileLoading = false;
            return;
          }

          if (this.doclingFileKind === 'image') {
            this.doclingFileLoading = false;
            if (this.doclingAutoConvert && this.isDoclingSupported(path)) {
              await this.runDoclingConvert(true);
            }
            return;
          }

          if (this.doclingFileKind === 'pdf') {
            const cached = await this.loadDoclingCached();
            this.doclingFileLoading = false;
            if (!cached && this.doclingAutoConvert) await this.runDoclingConvert(true);
            return;
          }

          const cached = await this.loadDoclingCached();
          if (cached) {
            this.doclingFileLoading = false;
            return;
          }

          if (this.doclingAutoConvert && this.isDoclingSupported(path)) {
            this.doclingFileLoading = false;
            await this.runDoclingConvert(true);
          } else if (!this.isDoclingSupported(path)) {
            this.doclingEditContent = '# ' + path.split('/').pop() + '\n\nDit bestandstype wordt niet ondersteund voor Docling-conversie.\nOpen het origineel via de knop rechtsboven.';
            Cockpit.toast('Geen Docling-support voor ' + ext, 'info');
          }
        } catch (e) {
          Cockpit.toast(e.message || 'Laden mislukt', 'error');
        } finally {
          this.doclingFileLoading = false;
        }
      },

      async loadNasRawText(path) {
        const data = await Cockpit.api('/docling/nas-read?path=' + encodeURIComponent(path));
        this.doclingEditContent = data.content || '';
        this.doclingExports = { markdown: this.doclingEditContent, text: this.doclingEditContent };
        this.doclingResult = { status: 'raw' };
        this.doclingExportTab = 'rendered';
      },

      async loadWorkspace(path) {
        if (!path) return false;
        try {
          const data = await Cockpit.api('/docling/workspace?path=' + encodeURIComponent(path));
          if (data.workspace && data.workspace.content) {
            this.doclingEditContent = data.workspace.content;
            return true;
          }
        } catch (e) {}
        const md = this.doclingExports.markdown;
        if (typeof md === 'string') this.doclingEditContent = md;
        return false;
      },

      async saveWorkspace() {
        if (!this.doclingSelected) return;
        try {
          await Cockpit.api('/docling/workspace', {
            method: 'PUT',
            body: JSON.stringify({
              storage_path: this.doclingSelected,
              content: this.doclingEditContent,
              source_format: 'markdown',
            }),
          });
          if (this.doclingExports) this.doclingExports.markdown = this.doclingEditContent;
          await this.recordVersion('concept opgeslagen');
          Cockpit.toast('Concept opgeslagen', 'success');
        } catch (e) {
          Cockpit.toast(e.message, 'error');
        }
      },

      async saveToNas() {
        if (!this.doclingSelected || !this.doclingEditContent) return;
        try {
          const data = await Cockpit.api('/docling/save-nas', {
            method: 'POST',
            body: JSON.stringify({
              path: this.doclingSelected,
              content: this.doclingEditContent,
              subdir: 'Telegram/Exports',
            }),
          });
          Cockpit.toast('Opgeslagen op NAS: ' + (data.path || data.filename), 'success');
          await this.recordVersion('NAS export');
          await this.loadAllNasFiles(false);
        } catch (e) {
          Cockpit.toast(e.message, 'error');
        }
      },

      sendToPowerpoint() {
        this.switchTab('powerpoint');
        this.importMarkdownToSlides(this.doclingEditContent || this.doclingExports.markdown || '');
      },

      async initPowerpoint() {
        await this.loadAllNasFiles(false);
        if (!this.pptSlides.length) {
          this.pptSlides = [{ title: 'Intro', bullets: [], bulletsText: '' }];
        }
      },

      markdownToSlides(md) {
        const slides = [];
        let current = { title: '', bullets: [], bulletsText: '' };
        (md || '').split('\n').forEach((line) => {
          const t = line.trim();
          if (t.startsWith('## ')) {
            if (current.title || current.bullets.length) slides.push({ ...current });
            current = { title: t.slice(3).trim(), bullets: [], bulletsText: '' };
          } else if (t.startsWith('# ') && !current.title) {
            current.title = t.slice(2).trim();
          } else if (/^[-*]\s/.test(t)) {
            current.bullets.push(t.replace(/^[-*]\s+/, ''));
            current.bulletsText = current.bullets.join('\n');
          } else if (t.length > 20 && !t.startsWith('|')) {
            current.bullets.push(t);
            current.bulletsText = current.bullets.join('\n');
          }
        });
        if (current.title || current.bullets.length) slides.push(current);
        return slides.length ? slides : [{ title: 'Inhoud', bullets: [(md || '').slice(0, 500)], bulletsText: (md || '').slice(0, 500) }];
      },

      importMarkdownToSlides(md) {
        const text = md || this.doclingEditContent || this.doclingExports.markdown || '';
        if (!text) return Cockpit.toast('Geen markdown — eerst Docling convert', 'info');
        this.pptSlides = this.markdownToSlides(text);
        const firstLine = (text.split('\n').find((l) => l.startsWith('# ')) || '').replace(/^#\s+/, '');
        if (firstLine) this.pptTitle = firstLine.slice(0, 200);
        Cockpit.toast(this.pptSlides.length + ' slides gegenereerd', 'success');
      },

      addPptSlide() {
        this.pptSlides.push({ title: 'Nieuwe slide', bullets: [], bulletsText: '' });
      },

      removePptSlide(idx) {
        this.pptSlides.splice(idx, 1);
      },

      syncSlideBullets(slide) {
        slide.bullets = (slide.bulletsText || '').split('\n').map((s) => s.trim()).filter(Boolean);
      },

      async createPptx() {
        if (!this.pptSlides.length) return;
        this.pptSlides.forEach((s) => this.syncSlideBullets(s));
        this.pptBusy = true;
        try {
          const data = await Cockpit.api('/pptx/create', {
            method: 'POST',
            body: JSON.stringify({
              title: this.pptTitle || 'Presentatie',
              subtitle: this.pptSubtitle || 'Foodlinkk',
              filename: this.pptFilename || this.pptTitle,
              slides: this.pptSlides.map((s) => ({ title: s.title, bullets: s.bullets })),
            }),
          });
          this.pptLastOutput = data.relative_output || data.output || data.path || 'klaar';
          Cockpit.toast('PowerPoint aangemaakt', 'success');
          await this.loadAllNasFiles(false);
        } catch (e) {
          Cockpit.toast(e.message, 'error');
        } finally {
          this.pptBusy = false;
        }
      },

      async convertPdfToPptx() {
        if (!this.pptSourcePdf) return;
        this.pptBusy = true;
        Cockpit.toast('PDF → PPTX conversie…', 'info');
        try {
          const data = await Cockpit.api('/pptx/pdf-to-pptx?path=' + encodeURIComponent(this.pptSourcePdf) + '&mode=smart');
          this.pptLastOutput = data.relative_output || data.output || '';
          Cockpit.toast('PPTX gegenereerd', 'success');
          await this.loadAllNasFiles(false);
        } catch (e) {
          Cockpit.toast(e.message, 'error');
        } finally {
          this.pptBusy = false;
        }
      },

      async loadDoclingHistory() {
        try {
          const data = await Cockpit.api('/docling/history?limit=40');
          this.doclingHistory = data.items || [];
          const statusMap = {};
          (this.doclingHistory || []).forEach((h) => {
            if (!statusMap[h.storage_path]) statusMap[h.storage_path] = h.status;
          });
          this.doclingFileStatus = statusMap;
        } catch (e) {}
      },

      async initDocling() {
        if (!this.doclingFiles.length) await this.loadAllNasFiles(false);
        else if (!this.shareBrowseEntries.length) await this.browseShare(this.shareBrowsePath || '', false);
        else this.nasLoading = false;
        try {
          const caps = await Cockpit.api('/docling/capabilities');
          if (caps.installed) {
            this.doclingStatus = 'Docling actief · ' + (caps.supported_suffixes || []).length + ' formaten · klik = open + convert';
            if (caps.export_formats) this.doclingExportFormats = caps.export_formats;
            if (caps.supported_suffixes) this.doclingSupportedSuffixes = caps.supported_suffixes;
          } else {
            this.doclingStatus = 'Docling niet geïnstalleerd';
          }
        } catch (e) {
          this.doclingStatus = 'Docling offline: ' + e.message;
        }
        await this.loadDoclingHistory();
      },

      doclingPayload() {
        return {
          path: this.doclingSelected,
          ocr: this.doclingOpts.ocr,
          tables: this.doclingOpts.tables,
          page_images: this.doclingOpts.page_images,
          picture_images: this.doclingOpts.picture_images,
          force_full_page_ocr: this.doclingOpts.force_full_page_ocr,
          formats: this.doclingFormats.length ? this.doclingFormats : ['markdown', 'json'],
        };
      },

      applyDoclingResult(data) {
        const src = data.result || data;
        this.doclingResult = src;
        this.doclingExports = src.exports || data.exports || {};
        this.doclingTables = src.tables_data || data.tables || [];
        this.doclingPictures = src.pictures || data.pictures || [];
        this.doclingMeta = {
          page_count: src.page_count || data.page_count || 0,
          metadata: src.metadata || data.metadata || {},
          status: src.status || 'done',
        };
        const keys = this.doclingAvailableExports();
        this.doclingExportTab = keys.includes('markdown') ? 'rendered' : (keys[0] || 'rendered');
        if (typeof this.doclingExports.markdown === 'string') {
          this.doclingEditContent = this.doclingExports.markdown;
        }
      },

      doclingAvailableExports() {
        return Object.keys(this.doclingExports || {}).filter((k) => this.doclingExports[k] != null);
      },

      doclingPreviewText() {
        let val = this.doclingExports[this.doclingExportTab];
        if (val == null) return '';
        if (typeof val === 'string') val = val;
        else if (typeof val === 'object' && val.error) return 'Error: ' + val.error;
        else val = JSON.stringify(val, null, 2);
        const q = (this.doclingPreviewSearch || '').trim().toLowerCase();
        if (!q || typeof val !== 'string') return val;
        const lines = val.split('\n');
        const hits = lines.filter((l) => l.toLowerCase().includes(q));
        return hits.length ? hits.join('\n') : '(geen matches voor "' + q + '")';
      },

      doclingPreviewHtml() {
        const val = this.doclingExports.html;
        return typeof val === 'string' ? val : '';
      },

      doclingRenderedMarkdown() {
        const md = this.doclingExports.markdown;
        if (typeof md !== 'string') return '';
        return md
          .replace(/^### (.*)$/gm, '<h3>$1</h3>')
          .replace(/^## (.*)$/gm, '<h2>$1</h2>')
          .replace(/^# (.*)$/gm, '<h1>$1</h1>')
          .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
          .replace(/\n\n/g, '</p><p>')
          .replace(/^/, '<p>')
          .replace(/$/, '</p>');
      },

      async copyDoclingExport() {
        const text = this.doclingExportTab === 'html'
          ? this.doclingPreviewHtml()
          : this.doclingPreviewText();
        try {
          await navigator.clipboard.writeText(text);
          Cockpit.toast('Gekopieerd', 'success');
        } catch (e) {
          Cockpit.toast('Kopiëren mislukt', 'error');
        }
      },

      downloadDoclingExport() {
        const tab = this.doclingExportTab;
        let content = tab === 'html' ? this.doclingPreviewHtml() : this.doclingPreviewText();
        if (!content) return Cockpit.toast('Geen export', 'info');
        const ext = tab === 'json' ? 'json' : tab === 'html' ? 'html' : tab === 'yaml' ? 'yaml' : 'md';
        const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
        const a = document.createElement('a');
        a.href = URL.createObjectURL(blob);
        a.download = (this.doclingSelected || 'export').split('/').pop().replace(/\.[^.]+$/, '') + '.' + ext;
        a.click();
        URL.revokeObjectURL(a.href);
      },

      async runDoclingConvert(silent) {
        if (!this.doclingSelected || this.doclingBusy) return;
        this.doclingBusy = true;
        if (!silent) Cockpit.toast('Docling converteert…', 'info');
        try {
          const data = await Cockpit.api('/docling/convert', {
            method: 'POST',
            body: JSON.stringify(this.doclingPayload()),
          });
          this.applyDoclingResult(data);
          await this.loadDoclingHistory();
          if (!silent) Cockpit.toast('Klaar · ' + (data.page_count || 0) + ' pagina\'s · ' + (this.doclingTables.length || 0) + ' tabellen', 'success');
        } catch (e) {
          if (!silent) Cockpit.toast(e.message, 'error');
          else Cockpit.toast('Convert mislukt: ' + e.message, 'error');
        } finally {
          this.doclingBusy = false;
        }
      },

      async runDoclingBatch() {
        if (this.doclingBusy) return;
        this.doclingBusy = true;
        try {
          const data = await Cockpit.api('/docling/batch', {
            method: 'POST',
            body: JSON.stringify({
              limit: 10,
              ext: this.doclingExtFilter || null,
              formats: this.doclingFormats,
              ...this.doclingOpts,
            }),
          });
          await this.loadDoclingHistory();
          Cockpit.toast((data.converted || 0) + ' / ' + (data.total || 0) + ' geconverteerd', 'success');
          if (this.doclingSelected && data.results) {
            const hit = (data.results || []).find((r) => r.storage_path === this.doclingSelected && r.ok);
            if (hit) this.applyDoclingResult(hit);
          }
        } catch (e) {
          Cockpit.toast(e.message, 'error');
        } finally {
          this.doclingBusy = false;
        }
      },

      async loadDoclingCached() {
        if (!this.doclingSelected) return false;
        try {
          const data = await Cockpit.api('/docling/result?path=' + encodeURIComponent(this.doclingSelected));
          if (data.ok && data.result) {
            this.applyDoclingResult(data.result);
            return true;
          }
        } catch (e) {}
        return false;
      },

      openHistoryItem(item) {
        if (!item || !item.storage_path) return;
        this.openDoclingFile(item.storage_path);
      },

      stat360(key) {
        const stats = this.client360 && this.client360.stats;
        if (!stats || stats[key] == null) return 0;
        return stats[key];
      },

      linkedClientName() {
        const id = Number(this.linkClientId);
        if (!id) return '';
        const c = (this.linkClients || []).find((row) => Number(row.id) === id);
        return 'Klant: ' + (c && c.name ? c.name : '—');
      },

      async initDocChat() {
        await this.loadLlmProviders();
        const p = this.llmProviders.find((row) => Number(row.id) === Number(this.chatLlmProviderId));
        this.chatStatus = p ? (p.label + ' · ' + p.model) : 'LLM laden…';
        await this.loadLinkClients();
        try {
          const d = await Cockpit.api('/documents/nas-diagnostics');
          const vis = d.visible_files || 0;
          this.chatStatus = vis + ' bestanden zichtbaar · Chroma RAG actief';
          if (vis < 30) {
            this.nasDiagHint = d.hint || 'Weinig bestanden zichtbaar op NAS — controleer Synology rechten voor map CUCINA/Foodlinkk.';
          } else {
            this.nasDiagHint = '';
          }
        } catch (e) {
          this.chatStatus = 'Diagnostics offline';
        }
      },

      async reindexNas() {
        this.chatReindexing = true;
        Cockpit.toast('NAS indexeren voor RAG…', 'info');
        try {
          await Cockpit.api('/documents/trigger-scan', { method: 'POST' });
          Cockpit.toast('Index scan gestart — Herman kan zo meer documenten zien', 'success');
        } catch (e) {
          Cockpit.toast(e.message, 'error');
        } finally {
          this.chatReindexing = false;
        }
      },

      async runChatSearch() {
        const q = (this.chatSearchQ || '').trim();
        if (!q) return;
        try {
          const data = await Cockpit.api('/documents/search?q=' + encodeURIComponent(q) + '&limit=8');
          this.chatSearchHits = data.results || [];
        } catch (e) {
          Cockpit.toast('Zoeken mislukt', 'error');
        }
      },

      async sendDocChat() {
        const msg = (this.chatInput || '').trim();
        if (!msg || this.chatBusy) return;
        this.chatBusy = true;
        this.chatStatus = 'Bezig met denken… (kan ~1–2 min duren op CPU)';
        this.chatMessages.push({ role: 'user', content: msg });
        const savedInput = this.chatInput;
        this.chatInput = '';
        this.$nextTick(() => {
          const log = document.getElementById('doc-chat-log');
          if (log) log.scrollTop = log.scrollHeight;
        });
        const ctrl = new AbortController();
        const timer = setTimeout(() => ctrl.abort(), 110000);
        try {
          const history = this.chatMessages.slice(-8, -1).map((m) => ({ role: m.role, content: m.content }));
          const payload = {
            message: msg,
            use_herman: !!this.chatUseHerman,
            llm_provider_id: this.chatLlmProviderId ? Number(this.chatLlmProviderId) : null,
            history,
            path_prefix: this.chatScopeFile && this.doclingSelected ? this.doclingSelected : '',
            client_id: this.chatScopeClient && this.linkClientId ? Number(this.linkClientId) : null,
            project_id: this.linkProjectId ? Number(this.linkProjectId) : null,
          };
          const data = await Cockpit.api('/documents/chat', {
            method: 'POST',
            body: JSON.stringify(payload),
            signal: ctrl.signal,
          });
          if (!data.ok && !data.reply) throw new Error(data.detail || data.error || 'Chat mislukt');
          this.chatMessages.push({
            role: 'assistant',
            content: data.reply || '(geen antwoord)',
            agent: data.agent_label || 'Herman',
            sources: data.rag_sources || [],
          });
          this.chatStatus = (data.rag_sources && data.rag_sources.length)
            ? data.rag_sources.length + ' bronnen gebruikt'
            : 'Antwoord ontvangen';
        } catch (e) {
          const errMsg = e.name === 'AbortError'
            ? 'Timeout na 3 min — probeer een kortere vraag of zet Herman orchestrator uit'
            : (e.message || e);
          this.chatMessages.push({ role: 'assistant', content: 'Fout: ' + errMsg, agent: 'Systeem', sources: [] });
          this.chatInput = savedInput;
          this.chatStatus = 'Fout bij chat';
        } finally {
          clearTimeout(timer);
          this.chatBusy = false;
          this.$nextTick(() => {
            const log = document.getElementById('doc-chat-log');
            if (log) log.scrollTop = log.scrollHeight;
          });
        }
      },

      async loadLinkClients() {
        try {
          const data = await Cockpit.api('/clients');
          this.linkClients = data.items || [];
        } catch (e) {}
      },

      linkProjectsForClient() {
        if (!this.linkClientId) return [];
        return (this.client360.projects || []).length
          ? this.client360.projects
          : this.linkProjects.filter((p) => p.client_id === Number(this.linkClientId));
      },

      async initAnalytics360() {
        await this.loadLinkClients();
        if (this.doclingSelected) this.linkPathInput = this.doclingSelected;
        if (this.linkClientId) await this.loadClient360();
        await this.refreshAutoSyncStatus();
      },

      async loadClient360() {
        if (!this.linkClientId) return;
        try {
          const data = await Cockpit.api('/clients/' + this.linkClientId + '/360');
          if (!data.ok) throw new Error(data.error || '360 laden mislukt');
          this.client360 = data;
          this.linkProjects = data.projects || [];
        } catch (e) {
          Cockpit.toast('360: ' + (e.message || e), 'error');
        }
      },

      async linkSelectedFile() {
        const path = (this.linkPathInput || this.doclingSelected || '').trim();
        if (!path || !this.linkClientId) return;
        try {
          await Cockpit.api('/documents/links', {
            method: 'POST',
            body: JSON.stringify({
              storage_path: path,
              client_id: Number(this.linkClientId),
              project_id: this.linkProjectId ? Number(this.linkProjectId) : null,
              is_folder: false,
            }),
          });
          Cockpit.toast('Bestand gekoppeld', 'success');
          await this.loadClient360();
        } catch (e) {
          Cockpit.toast(e.message, 'error');
        }
      },

      async linkSelectedFolder() {
        let path = (this.linkPathInput || this.doclingSelected || '').trim();
        if (!path || !this.linkClientId) return;
        if (!path.endsWith('/')) path = path.replace(/\/[^/]+$/, '') || path;
        try {
          await Cockpit.api('/documents/links', {
            method: 'POST',
            body: JSON.stringify({
              storage_path: path,
              client_id: Number(this.linkClientId),
              project_id: this.linkProjectId ? Number(this.linkProjectId) : null,
              is_folder: true,
            }),
          });
          Cockpit.toast('Map gekoppeld: ' + path, 'success');
          await this.loadClient360();
        } catch (e) {
          Cockpit.toast(e.message, 'error');
        }
      },

      async unlinkDocument(linkId) {
        try {
          await Cockpit.api('/documents/links/' + linkId, { method: 'DELETE' });
          await this.loadClient360();
        } catch (e) {
          Cockpit.toast(e.message, 'error');
        }
      },

      async runAutoSync(force) {
        this.autoSyncBusy = true;
        try {
          const data = await Cockpit.api('/brain/auto-sync' + (force ? '?force=true' : ''), { method: 'POST' });
          const brain = (data.steps || {}).brain_sync || {};
          Cockpit.toast('Sync: ' + (brain.synced || 0) + ' docs → second brain', 'success');
          await this.refreshAutoSyncStatus();
        } catch (e) {
          Cockpit.toast(e.message, 'error');
        } finally {
          this.autoSyncBusy = false;
        }
      },

      async refreshAutoSyncStatus() {
        try {
          const data = await Cockpit.api('/brain/auto-sync/status');
          const last = data.last;
          if (last && last.created_at) {
            this.autoSyncLabel = 'Laatste sync: ' + last.created_at.slice(0, 16) + ' (' + last.status + ')';
          }
        } catch (e) {}
      },

      startAutoSyncLoop() {
        if (this.autoSyncTimer) clearInterval(this.autoSyncTimer);
        this.autoSyncTimer = setInterval(() => this.runAutoSync(false), 10 * 60 * 1000);
      },
    };
  };

  window.wordSearch = function wordSearch() {
    return {
      query: '',
      includeStopwords: false,
      results: topWords.slice(0, 20),
      loading: false,
      async search() {
        this.loading = true;
        try {
          const params = new URLSearchParams({ limit: '40', stopwords: this.includeStopwords ? 'true' : 'false' });
          if (this.query.trim()) params.set('q', this.query.trim());
          const res = await fetch('/api/admin/documents/words?' + params);
          this.results = (await res.json()).items || [];
        } catch (e) {
          Cockpit.toast('Woord zoeken mislukt', 'error');
        } finally {
          this.loading = false;
        }
      },
      init() {
        this.search();
      },
    };
  };

  function registerDocumentsDash() {
    if (window.Alpine && window.documentsDash) {
      Alpine.data('documentsDash', window.documentsDash);
    }
  }
  document.addEventListener('alpine:init', registerDocumentsDash);
  if (window.Alpine) registerDocumentsDash();
})();
