// frontend/src/pages/crm/customers/tabs/FilesTab.jsx

import { useState, useEffect, useCallback, useRef } from 'react'
import { createPortal } from 'react-dom'
import api from '@/lib/api'
import { useToast } from '@/components/ui/Toast'
import Button from '@/components/ui/Button'
import SearchBar from '@/components/ui/SearchBar'
import Spinner from '@/components/ui/Spinner'
import ConfirmDialog from '@/components/ui/ConfirmDialog'
import { fmtDateMedium } from '@/lib/formatters'
import Icon from '@/components/ui/Icon'

// ─── Category colour system ────────────────────────────────────────────────────

const CATEGORIES = [
  { value: 'all',            label: 'All',                color: null },
  { value: 'sent_media',     label: 'Sent',               color: { bg: 'rgba(201,168,76,0.12)',  text: '#c9a84c', border: 'rgba(107,90,42,0.6)',   dot: '#c9a84c' } },
  { value: 'received_media', label: 'Received',           color: { bg: 'rgba(184,146,46,0.10)',  text: '#b8922e', border: 'rgba(92,75,26,0.6)',    dot: '#b8922e' } },
  { value: 'media',          label: 'General',            color: { bg: 'rgba(212,168,67,0.10)',  text: '#d4a843', border: 'rgba(107,90,42,0.5)',   dot: '#d4a843' } },
  { value: 'invoices',       label: 'Invoices',           color: { bg: 'rgba(196,112,112,0.10)', text: '#c47070', border: 'rgba(107,46,46,0.5)',   dot: '#c47070' } },
  { value: 'quotations',     label: 'Quotations',         color: { bg: 'rgba(106,171,122,0.10)', text: '#6aab7a', border: 'rgba(46,94,56,0.5)',    dot: '#6aab7a' } },
  { value: 'documents',      label: 'Documents',          color: { bg: 'rgba(167,139,250,0.10)', text: '#a78bfa', border: 'rgba(91,33,182,0.5)',   dot: '#a78bfa' } },
]

const SORT_OPTIONS = [
  { value: 'date_desc', label: 'Date ↓' },
  { value: 'date_asc',  label: 'Date ↑' },
  { value: 'name_asc',  label: 'Name A–Z' },
  { value: 'name_desc', label: 'Name Z–A' },
  { value: 'size_desc', label: 'Size ↓' },
  { value: 'size_asc',  label: 'Size ↑' },
]

const PAGE_SIZES = [20, 50, 100]

const SUBFOLDER_TO_CATEGORY = {
  sent_media: 'sent_media', received_media: 'received_media',
  media: 'media', invoices: 'invoices', quotations: 'quotations', documents: 'documents',
  sent: 'sent_media', received: 'received_media',
}

// ─── Helpers ───────────────────────────────────────────────────────────────────

function getCatMeta(value) {
  return CATEGORIES.find(c => c.value === value) || CATEGORIES.find(c => c.value === 'documents')
}

function formatFileSize(bytes) {
  if (!bytes && bytes !== 0) return '—'
  if (bytes === 0) return '0 B'
  const units = ['B', 'KB', 'MB', 'GB']
  let i = 0, val = bytes
  while (val >= 1024 && i < units.length - 1) { val /= 1024; i++ }
  return `${val < 10 ? val.toFixed(1) : Math.round(val)} ${units[i]}`
}

function formatDate(d) { return fmtDateMedium(d) }

function getExt(filename = '') {
  const p = filename.split('.')
  return p.length > 1 ? p[p.length - 1].toUpperCase() : 'FILE'
}

function getMimeGroup(mime = '', filename = '') {
  if (!mime && filename) {
    const ext = filename.split('.').pop().toLowerCase()
    if (['jpg','jpeg','png','gif','webp','svg','bmp'].includes(ext)) return 'image'
    if (['mp4','mov','avi','mkv','webm'].includes(ext)) return 'video'
    if (['mp3','wav','ogg','flac','aac','m4a'].includes(ext)) return 'audio'
    if (ext === 'pdf') return 'pdf'
    if (['zip','tar','gz','rar','7z'].includes(ext)) return 'archive'
    if (['txt','md','csv','json','xml','yaml','yml'].includes(ext)) return 'text'
  }
  if (mime.startsWith('image/')) return 'image'
  if (mime.startsWith('video/')) return 'video'
  if (mime.startsWith('audio/')) return 'audio'
  if (mime === 'application/pdf') return 'pdf'
  if (mime.startsWith('text/')) return 'text'
  return 'generic'
}

function detectCategory(path = '') {
  const parts = path.split('/')
  for (let i = parts.length - 2; i >= 0; i--) {
    if (SUBFOLDER_TO_CATEGORY[parts[i]]) return SUBFOLDER_TO_CATEGORY[parts[i]]
  }
  return 'media'
}

function isPreviewable(mime, filename) {
  return !!(mime && (
    mime.startsWith('image/') || mime.startsWith('video/') || mime.startsWith('audio/') ||
    mime === 'application/pdf' || mime === 'text/plain' ||
    (filename || '').endsWith('.txt')
  ))
}

// ─── Shared toolbar button style ───────────────────────────────────────────────

const TB = {
  height: 34,
  display: 'flex',
  alignItems: 'center',
  gap: 6,
  padding: '0 10px',
  fontSize: 'var(--font-size-sm)',
  fontFamily: 'var(--font-family-base)',
  backgroundColor: 'var(--color-bg-surface)',
  border: '1px solid var(--color-border-strong)',
  borderRadius: 'var(--radius-md)',
  color: 'var(--color-text-secondary)',
  cursor: 'pointer',
  whiteSpace: 'nowrap',
  transition: 'background-color 0.12s, color 0.12s, border-color 0.12s',
  flexShrink: 0,
}

// ─── PDF Thumbnail (PDF.js CDN) ────────────────────────────────────────────────

function PdfThumbnail({ url }) {
  const canvasRef = useRef(null)
  const [failed, setFailed] = useState(false)
  useEffect(() => {
    let cancelled = false
    ;(async () => {
      try {
        if (!window.pdfjsLib) {
          await new Promise((res, rej) => {
            if (document.getElementById('__pdfjs_ft__')) { res(); return }
            const s = document.createElement('script')
            s.id = '__pdfjs_ft__'
            s.src = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js'
            s.onload = res; s.onerror = rej
            document.head.appendChild(s)
          })
          window.pdfjsLib.GlobalWorkerOptions.workerSrc =
            'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'
        }
        const pdf  = await window.pdfjsLib.getDocument(url).promise
        if (cancelled) return
        const page = await pdf.getPage(1)
        if (cancelled) return
        const canvas = canvasRef.current
        if (!canvas) return
        const vp    = page.getViewport({ scale: 1 })
        const scale = Math.min(200 / vp.width, 140 / vp.height)
        const scaled = page.getViewport({ scale })
        canvas.width = scaled.width; canvas.height = scaled.height
        await page.render({ canvasContext: canvas.getContext('2d'), viewport: scaled }).promise
      } catch { if (!cancelled) setFailed(true) }
    })()
    return () => { cancelled = true }
  }, [url])
  if (failed) return <FileTypeIcon group="pdf" size={40} />
  return <canvas ref={canvasRef} style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' }} />
}

// ─── File type icon ────────────────────────────────────────────────────────────

function FileTypeIcon({ group, size = 40 }) {
  const p = { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.5, strokeLinecap: 'round', strokeLinejoin: 'round' }
  switch (group) {
    case 'pdf':    return <svg {...p} style={{ color: 'var(--color-danger)' }}><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="9" y1="13" x2="15" y2="13"/><line x1="9" y1="17" x2="15" y2="17"/></svg>
    case 'audio':  return <svg {...p} style={{ color: 'var(--color-info)' }}><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
    case 'video':  return <svg {...p} style={{ color: 'var(--color-warning)' }}><rect x="2" y="5" width="14" height="14" rx="2"/><path d="M16 10l6-3v10l-6-3"/></svg>
    case 'archive':return <svg {...p} style={{ color: 'var(--color-text-secondary)' }}><polyline points="21 8 21 21 3 21 3 8"/><rect x="1" y="3" width="22" height="5"/><line x1="10" y1="12" x2="14" y2="12"/></svg>
    case 'text':   return <svg {...p} style={{ color: 'var(--color-text-muted)' }}><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
    default:       return <svg {...p} style={{ color: 'var(--color-primary)' }}><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
  }
}

// ─── Tag input pill ────────────────────────────────────────────────────────────

function TagInput({ tags, tagInput, onChange, onInputChange, disabled }) {
  return (
    <div style={{
      display: 'flex', flexWrap: 'wrap', gap: 3, alignItems: 'center',
      border: '1px solid var(--color-border)', borderRadius: 'var(--radius-sm)',
      padding: '3px 6px', backgroundColor: 'var(--color-bg-abyss)', minHeight: 32,
    }}>
      {tags.map(t => (
        <span key={t} style={{
          display: 'flex', alignItems: 'center', gap: 3,
          fontSize: 'var(--font-size-xs)', padding: '1px 7px',
          borderRadius: 'var(--radius-full)',
          backgroundColor: 'var(--color-bg-elevated)',
          color: 'var(--color-text-secondary)',
        }}>
          {t}
          {!disabled && (
            <button type="button" onClick={() => onChange(tags.filter(tt => tt !== t))}
              style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 0, lineHeight: 1, color: 'var(--color-text-muted)', fontSize: 11 }}>×</button>
          )}
        </span>
      ))}
      {!disabled && (
        <input
          value={tagInput}
          placeholder={tags.length === 0 ? 'tag, enter…' : ''}
          style={{ border: 'none', outline: 'none', backgroundColor: 'transparent', fontSize: 'var(--font-size-xs)', color: 'var(--color-text-primary)', minWidth: 60, flex: 1, fontFamily: 'var(--font-family-base)' }}
          onChange={e => onInputChange(e.target.value)}
          onKeyDown={e => {
            if (e.key === 'Enter' || e.key === ',') {
              e.preventDefault()
              const val = tagInput.trim().replace(/,$/, '')
              if (val && !tags.includes(val)) onChange([...tags, val])
              onInputChange('')
            }
          }}
        />
      )}
    </div>
  )
}

// ─── Upload Modal ──────────────────────────────────────────────────────────────

function UploadModal({ open, customerId, onClose, onSuccess }) {
  const toast     = useToast()
  const inputRef  = useRef(null)
  const [queue,       setQueue]       = useState([])  // { file, name, category, tags, tagInput, progress }
  const [uploading,   setUploading]   = useState(false)
  const [dragging,    setDragging]    = useState(false)
  const [bulkCat,     setBulkCat]     = useState('media')
  const [bulkTags,    setBulkTags]    = useState([])
  const [bulkTagInput,setBulkTagInput]= useState('')

  // Reset when closed
  useEffect(() => {
    if (!open) { setQueue([]); setBulkCat('media'); setBulkTags([]); setBulkTagInput(''); setUploading(false) }
  }, [open])

  const addFiles = files => {
    const entries = Array.from(files).map(f => ({
      file: f, name: f.name, category: bulkCat,
      tags: [...bulkTags], tagInput: '', progress: null,
    }))
    setQueue(prev => [...prev, ...entries])
  }

  const handleUpload = async () => {
    const pending = queue.filter(e => e.progress === null)
    if (!pending.length || uploading) return
    setUploading(true)
    const tok = localStorage.getItem('access_token') || ''
    for (let i = 0; i < queue.length; i++) {
      const entry = queue[i]
      if (entry.progress !== null) continue
      setQueue(prev => prev.map((e, j) => j === i ? { ...e, progress: 0 } : e))
      try {
        const fd = new FormData()
        const uploadFile = entry.name !== entry.file.name
          ? new File([entry.file], entry.name, { type: entry.file.type })
          : entry.file
        fd.append('file', uploadFile)
        fd.append('customer_id', customerId)
        fd.append('subfolder', entry.category)
        if (entry.tags.length) fd.append('tags', entry.tags.join(','))
        const resp = await fetch('/api/crm/nextcloud/upload', {
          method: 'POST', headers: { Authorization: `Bearer ${tok}` }, body: fd,
        })
        if (!resp.ok) throw new Error()
        setQueue(prev => prev.map((e, j) => j === i ? { ...e, progress: 'done' } : e))
      } catch {
        setQueue(prev => prev.map((e, j) => j === i ? { ...e, progress: 'error' } : e))
      }
    }
    setUploading(false)
    onSuccess?.()
  }

  const pendingCount = queue.filter(e => e.progress === null).length
  const doneCount    = queue.filter(e => e.progress === 'done').length

  if (!open) return null

  return createPortal(
    <div
      onClick={e => { if (e.target === e.currentTarget && !uploading) onClose() }}
      onDragOver={e => { e.preventDefault(); setDragging(true) }}
      onDragLeave={e => { if (!e.currentTarget.contains(e.relatedTarget)) setDragging(false) }}
      onDrop={e => { e.preventDefault(); setDragging(false); addFiles(e.dataTransfer.files) }}
      style={{
        position: 'fixed', inset: 0, zIndex: 1000,
        backgroundColor: dragging ? 'rgba(128,131,255,0.15)' : 'rgba(0,0,0,0.75)',
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        transition: 'background-color 0.15s',
      }}
    >
      <div
        onClick={e => e.stopPropagation()}
        style={{
          backgroundColor: 'var(--color-bg-surface)',
          borderRadius: 'var(--radius-xl)',
          border: '1px solid var(--color-border)',
          boxShadow: 'var(--shadow-lg)',
          width: 'min(900px, 94vw)',
          maxHeight: '88vh',
          display: 'flex', flexDirection: 'column',
          overflow: 'hidden',
        }}
      >
        {/* Header */}
        <div style={{
          display: 'flex', alignItems: 'center', justifyContent: 'space-between',
          padding: 'var(--space-4) var(--space-6)',
          borderBottom: '1px solid var(--color-border)',
          flexShrink: 0,
        }}>
          <span style={{ fontSize: 'var(--font-size-md)', fontWeight: 'var(--font-weight-semibold)', color: 'var(--color-text-primary)', fontFamily: 'var(--font-family-display)' }}>
            Upload Files
          </span>
          {!uploading && (
            <button onClick={onClose}
              style={{ background: 'none', border: 'none', color: 'var(--color-text-muted)', cursor: 'pointer', lineHeight: 1, padding: 4 }}>
              <Icon name="close" size={18} />
            </button>
          )}
        </div>

        {/* Drop zone strip */}
        <div
          onClick={() => inputRef.current?.click()}
          style={{
            padding: 'var(--space-4) var(--space-6)',
            cursor: 'pointer',
            display: 'flex', alignItems: 'center', gap: 'var(--space-4)',
            borderBottom: '1px solid var(--color-border)',
            backgroundColor: dragging ? 'var(--color-primary-subtle)' : 'var(--color-bg-void)',
            flexShrink: 0,
          }}
        >
          <div style={{
            width: 40, height: 40, borderRadius: 'var(--radius-lg)',
            border: '2px dashed var(--color-border-strong)',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            color: 'var(--color-text-muted)', flexShrink: 0,
          }}>
            <Icon name="plus" size={20} />
          </div>
          <div>
            <p style={{ fontSize: 'var(--font-size-sm)', fontWeight: 'var(--font-weight-medium)', color: 'var(--color-text-primary)', margin: 0 }}>
              Click to browse or drop files anywhere in this window
            </p>
            <p style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', margin: '2px 0 0' }}>
              Multiple files supported — all types accepted
            </p>
          </div>
        </div>
        <input ref={inputRef} type="file" multiple style={{ display: 'none' }}
          onChange={e => { addFiles(e.target.files); e.target.value = '' }} />

        {/* Table */}
        <div style={{ flex: 1, overflowY: 'auto' }}>
          {queue.length === 0 ? (
            <div style={{ textAlign: 'center', padding: 'var(--space-12)', color: 'var(--color-text-muted)', fontSize: 'var(--font-size-sm)' }}>
              No files selected yet. Click above or drag &amp; drop.
            </div>
          ) : (
            <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 'var(--font-size-sm)' }}>
              <thead>
                {/* Quick-set row */}
                <tr style={{ backgroundColor: 'var(--color-bg-void)', borderBottom: '2px solid var(--color-border)' }}>
                  <th style={{ padding: 'var(--space-2) var(--space-4)', textAlign: 'left', fontWeight: 'var(--font-weight-semibold)', fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)', whiteSpace: 'nowrap' }}>
                    Quick Set ALL →
                  </th>
                  <th style={{ padding: 'var(--space-2) var(--space-3)', width: 170 }}>
                    <select
                      value={bulkCat}
                      onChange={e => {
                        setBulkCat(e.target.value)
                        setQueue(prev => prev.map(x => x.progress === null ? { ...x, category: e.target.value } : x))
                      }}
                      style={{ width: '100%', fontSize: 'var(--font-size-xs)', padding: '4px 6px', borderRadius: 'var(--radius-sm)', border: '1px solid var(--color-border)', backgroundColor: 'var(--color-bg-abyss)', color: 'var(--color-text-primary)', fontFamily: 'var(--font-family-base)' }}
                    >
                      {CATEGORIES.filter(c => c.value !== 'all').map(c => <option key={c.value} value={c.value}>{c.label}</option>)}
                    </select>
                  </th>
                  <th style={{ padding: 'var(--space-2) var(--space-3)', width: 220 }}>
                    <TagInput
                      tags={bulkTags} tagInput={bulkTagInput}
                      onChange={v => { setBulkTags(v); setQueue(prev => prev.map(x => x.progress === null ? { ...x, tags: v } : x)) }}
                      onInputChange={v => setBulkTagInput(v)}
                    />
                  </th>
                  <th style={{ padding: 'var(--space-2) var(--space-3)', width: 80 }} />
                  <th style={{ padding: 'var(--space-2) var(--space-3)', width: 40 }} />
                </tr>
                {/* Column headers */}
                <tr style={{ backgroundColor: 'var(--color-bg-void)', borderBottom: '1px solid var(--color-border)' }}>
                  {['Filename', 'Type', 'Tags', 'Size', ''].map((h, i) => (
                    <th key={i} style={{ padding: 'var(--space-2) var(--space-4)', textAlign: 'left', fontSize: 'var(--font-size-xs)', fontWeight: 'var(--font-weight-bold)', color: 'var(--color-text-muted)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)' }}>
                      {h}
                    </th>
                  ))}
                </tr>
              </thead>
              <tbody>
                {queue.map((entry, i) => (
                  <tr key={i} style={{ borderBottom: '1px solid var(--color-border)', backgroundColor: i % 2 === 0 ? 'transparent' : 'rgba(192,193,255,0.015)' }}>
                    {/* Filename */}
                    <td style={{ padding: 'var(--space-2) var(--space-4)' }}>
                      <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                        {entry.progress === 'done'  && <Icon name="check"   size={14} color="var(--color-success)" />}
                        {entry.progress === 'error' && <Icon name="warning" size={14} color="var(--color-danger)"  />}
                        {typeof entry.progress === 'number' && (
                          <div style={{ width: 14, height: 14, borderRadius: '50%', border: '2px solid var(--color-primary)', borderTopColor: 'transparent', animation: 'spin 0.7s linear infinite', flexShrink: 0 }} />
                        )}
                        <input
                          value={entry.name}
                          onChange={e => setQueue(prev => prev.map((x, j) => j === i ? { ...x, name: e.target.value } : x))}
                          disabled={entry.progress !== null}
                          style={{ width: '100%', fontSize: 'var(--font-size-sm)', padding: '3px 6px', borderRadius: 'var(--radius-sm)', border: '1px solid var(--color-border)', backgroundColor: 'var(--color-bg-abyss)', color: 'var(--color-text-primary)', fontFamily: 'var(--font-family-base)' }}
                        />
                      </div>
                      {typeof entry.progress === 'number' && (
                        <div style={{ marginTop: 4, height: 2, borderRadius: 1, backgroundColor: 'var(--color-border)', overflow: 'hidden' }}>
                          <div style={{ width: '100%', height: '100%', backgroundColor: 'var(--color-primary)', animation: 'pulse 1s ease-in-out infinite' }} />
                        </div>
                      )}
                    </td>
                    {/* Type */}
                    <td style={{ padding: 'var(--space-2) var(--space-3)' }}>
                      <select
                        value={entry.category}
                        onChange={e => setQueue(prev => prev.map((x, j) => j === i ? { ...x, category: e.target.value } : x))}
                        disabled={entry.progress !== null}
                        style={{ width: '100%', fontSize: 'var(--font-size-xs)', padding: '4px 6px', borderRadius: 'var(--radius-sm)', border: '1px solid var(--color-border)', backgroundColor: 'var(--color-bg-abyss)', color: 'var(--color-text-primary)', fontFamily: 'var(--font-family-base)' }}
                      >
                        {CATEGORIES.filter(c => c.value !== 'all').map(c => <option key={c.value} value={c.value}>{c.label}</option>)}
                      </select>
                    </td>
                    {/* Tags */}
                    <td style={{ padding: 'var(--space-2) var(--space-3)' }}>
                      <TagInput
                        tags={entry.tags} tagInput={entry.tagInput}
                        disabled={entry.progress !== null}
                        onChange={v => setQueue(prev => prev.map((x, j) => j === i ? { ...x, tags: v } : x))}
                        onInputChange={v => setQueue(prev => prev.map((x, j) => j === i ? { ...x, tagInput: v } : x))}
                      />
                    </td>
                    {/* Size */}
                    <td style={{ padding: 'var(--space-2) var(--space-3)', fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', whiteSpace: 'nowrap' }}>
                      {formatFileSize(entry.file.size)}
                    </td>
                    {/* Remove */}
                    <td style={{ padding: 'var(--space-2) var(--space-3)', textAlign: 'center' }}>
                      {entry.progress === null && (
                        <button onClick={() => setQueue(prev => prev.filter((_, j) => j !== i))}
                          style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--color-text-muted)', lineHeight: 1, padding: 4 }}>
                          <Icon name="close" size={14} />
                        </button>
                      )}
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          )}
        </div>

        {/* Footer */}
        <div style={{
          padding: 'var(--space-4) var(--space-6)',
          borderTop: '1px solid var(--color-border)',
          display: 'flex', justifyContent: 'center', alignItems: 'center', gap: 'var(--space-3)',
          flexShrink: 0,
        }}>
          {!uploading && doneCount > 0 && (
            <Button variant="secondary" size="md" onClick={onClose}>Done</Button>
          )}
          <Button
            variant="primary" size="md"
            disabled={pendingCount === 0 || uploading}
            loading={uploading}
            onClick={handleUpload}
          >
            {uploading ? 'Uploading…' : `Upload ${pendingCount} File${pendingCount !== 1 ? 's' : ''}`}
          </Button>
        </div>
      </div>
    </div>,
    document.body
  )
}

// ─── Preview Modal ─────────────────────────────────────────────────────────────

function PreviewModal({ file, previewList, token, canEdit, onClose, onDelete, onNavigate }) {
  const [txtContent, setTxtContent] = useState(null)

  const proxyUrl = file ? `/api/crm/nextcloud/file?path=${encodeURIComponent(file.path)}&token=${encodeURIComponent(token)}` : ''

  useEffect(() => {
    if (!file) return
    const isTxt = file.mime_type === 'text/plain' || file.filename?.endsWith('.txt')
    if (!isTxt) { setTxtContent(null); return }
    setTxtContent(null)
    fetch(proxyUrl).then(r => r.text()).then(t => setTxtContent(t)).catch(() => setTxtContent('(failed to load)'))
  }, [file, proxyUrl])

  // Keyboard nav
  useEffect(() => {
    if (!file) return
    const h = e => {
      if (e.key === 'Escape') onClose()
      if (e.key === 'ArrowLeft')  { const idx = previewList.findIndex(f => f.path === file.path); if (idx > 0) onNavigate(previewList[idx - 1]) }
      if (e.key === 'ArrowRight') { const idx = previewList.findIndex(f => f.path === file.path); if (idx < previewList.length - 1) onNavigate(previewList[idx + 1]) }
    }
    document.addEventListener('keydown', h)
    return () => document.removeEventListener('keydown', h)
  }, [file, previewList, onClose, onNavigate])

  if (!file) return null

  const idx     = previewList.findIndex(f => f.path === file.path)
  const hasPrev = idx > 0
  const hasNext = idx < previewList.length - 1
  const isImg   = file.mime_type?.startsWith('image/')
  const isVideo = file.mime_type?.startsWith('video/')
  const isAudio = file.mime_type?.startsWith('audio/')
  const isPdf   = file.mime_type === 'application/pdf'
  const isTxt   = file.mime_type === 'text/plain' || file.filename?.endsWith('.txt')
  const isWide  = isPdf || isTxt

  const navBtnBase = {
    display: 'flex', alignItems: 'center', gap: 6,
    padding: '6px 14px',
    fontSize: 'var(--font-size-sm)',
    fontFamily: 'var(--font-family-base)',
    borderRadius: 'var(--radius-md)',
    border: '1px solid var(--color-border-strong)',
    backgroundColor: 'var(--color-bg-elevated)',
    cursor: 'pointer',
    transition: 'background-color 0.12s, color 0.12s',
    whiteSpace: 'nowrap',
    textDecoration: 'none',
  }
  const navBtnStyle = active => ({
    ...navBtnBase,
    color: active ? 'var(--color-text-secondary)' : 'var(--color-text-muted)',
    cursor: active ? 'pointer' : 'default',
    opacity: active ? 1 : 0.35,
  })
  const navBtnColored = (colorVar, hoverColorVar) => ({
    ...navBtnBase,
    color: colorVar,
    '--nav-btn-hover-color': hoverColorVar,
  })

  const openNextcloud = async () => {
    try {
      const data = await api.get(`/crm/nextcloud/web-url?path=${encodeURIComponent(file.path)}`)
      window.open(data.url, '_blank', 'noreferrer')
    } catch { /* swallow */ }
  }

  return createPortal(
    <div
      onClick={onClose}
      style={{
        position: 'fixed', inset: 0, zIndex: 1100,
        backgroundColor: 'rgba(0,0,0,0.90)',
        display: 'flex', flexDirection: 'column',
        alignItems: 'center', justifyContent: 'center',
      }}
    >
      <div
        onClick={e => e.stopPropagation()}
        style={{
          backgroundColor: 'var(--color-bg-surface)',
          borderRadius: 'var(--radius-xl)',
          border: '1px solid var(--color-border)',
          boxShadow: '0 24px 64px rgba(0,0,0,0.6)',
          width: isWide ? '82vw' : undefined,
          maxWidth: '92vw',
          maxHeight: '90vh',
          display: 'flex', flexDirection: 'column',
          overflow: 'hidden',
        }}
      >
        {/* Header */}
        <div style={{
          display: 'flex', alignItems: 'center', justifyContent: 'space-between',
          padding: '12px var(--space-5)',
          borderBottom: '1px solid var(--color-border)',
          gap: 'var(--space-3)', flexShrink: 0,
        }}>
          <span style={{
            fontSize: 'var(--font-size-sm)', fontWeight: 'var(--font-weight-medium)',
            color: 'var(--color-text-primary)',
            overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
            maxWidth: '55vw',
          }}>
            {file.filename}
          </span>
          <button onClick={onClose}
            style={{ background: 'none', border: 'none', color: 'var(--color-text-muted)', cursor: 'pointer', lineHeight: 1, padding: 4, flexShrink: 0 }}>
            <Icon name="close" size={20} />
          </button>
        </div>

        {/* Body */}
        <div style={{ flex: 1, overflow: 'auto', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 'var(--space-5)', minHeight: 0 }}>
          {isImg && (
            <img src={proxyUrl} alt={file.filename}
              style={{ maxWidth: '80vw', maxHeight: '72vh', objectFit: 'contain', borderRadius: 'var(--radius-md)' }} />
          )}
          {isVideo && (
            <video controls preload="metadata" style={{ maxWidth: '80vw', maxHeight: '72vh', borderRadius: 'var(--radius-md)' }}>
              <source src={proxyUrl} type={file.mime_type || 'video/mp4'} />
              <source src={proxyUrl} />
            </video>
          )}
          {isAudio && (
            <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--space-4)', padding: 'var(--space-8)' }}>
              <FileTypeIcon group="audio" size={64} />
              <p style={{ fontSize: 'var(--font-size-sm)', fontWeight: 'var(--font-weight-medium)', color: 'var(--color-text-primary)', margin: 0 }}>
                {file.filename}
              </p>
              <audio src={proxyUrl} controls preload="metadata" style={{ width: 'min(400px, 70vw)' }} />
            </div>
          )}
          {isPdf && (
            <iframe src={proxyUrl} title={file.filename}
              style={{ width: '100%', height: '72vh', border: 'none', borderRadius: 'var(--radius-md)' }} />
          )}
          {isTxt && (
            <pre style={{
              width: '100%', minHeight: 200, maxHeight: '72vh', overflow: 'auto',
              margin: 0, padding: 'var(--space-5)',
              fontSize: 'var(--font-size-sm)', lineHeight: 1.7,
              color: 'var(--color-text-primary)',
              backgroundColor: 'var(--color-bg-abyss)',
              borderRadius: 'var(--radius-md)',
              whiteSpace: 'pre-wrap', wordBreak: 'break-word',
              fontFamily: 'var(--font-family-mono)',
            }}>
              {txtContent === null ? 'Loading…' : txtContent}
            </pre>
          )}
          {!isImg && !isVideo && !isAudio && !isPdf && !isTxt && (
            <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--space-4)', padding: 'var(--space-8)' }}>
              <FileTypeIcon group={getMimeGroup(file.mime_type, file.filename)} size={64} />
              <p style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-muted)', margin: 0 }}>No preview available</p>
            </div>
          )}
        </div>

        {/* Footer nav */}
        <div style={{
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          gap: 'var(--space-2)',
          padding: '10px var(--space-5)',
          borderTop: '1px solid var(--color-border)',
          flexShrink: 0, flexWrap: 'wrap',
        }}>
          <button
            onClick={() => hasPrev && onNavigate(previewList[idx - 1])}
            style={navBtnStyle(hasPrev)}
          >
            <Icon name="chevron_left" size={14} /> Prev
          </button>
          <a href={proxyUrl} target="_blank" rel="noreferrer" style={navBtnStyle(true)}>
            <Icon name="expand" size={14} /> Open Full
          </a>
          <button
            onClick={openNextcloud}
            style={{ ...navBtnColored('var(--color-info)', 'var(--color-info-text)') }}
            onMouseEnter={e => { e.currentTarget.style.backgroundColor = 'rgba(56,139,253,0.10)' }}
            onMouseLeave={e => { e.currentTarget.style.backgroundColor = 'var(--color-bg-elevated)' }}
          >
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round">
              <path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/>
            </svg>
            Nextcloud
          </button>
          <a
            href={proxyUrl + '&download=1'} download={file.filename}
            style={{ ...navBtnColored('var(--color-success)', 'var(--color-success)') }}
            onMouseEnter={e => { e.currentTarget.style.backgroundColor = 'rgba(56,189,100,0.10)' }}
            onMouseLeave={e => { e.currentTarget.style.backgroundColor = 'var(--color-bg-elevated)' }}
          >
            <Icon name="download" size={14} /> Download
          </a>
          {canEdit && (
            <button
              onClick={() => { onClose(); onDelete(file) }}
              style={{ ...navBtnColored('var(--color-danger)', 'var(--color-danger)') }}
              onMouseEnter={e => { e.currentTarget.style.backgroundColor = 'var(--color-danger-bg)' }}
              onMouseLeave={e => { e.currentTarget.style.backgroundColor = 'var(--color-bg-elevated)' }}
            >
              <Icon name="delete" size={14} /> Delete
            </button>
          )}
          <button
            onClick={() => hasNext && onNavigate(previewList[idx + 1])}
            style={navBtnStyle(hasNext)}
          >
            Next <Icon name="chevron_right" size={14} />
          </button>
        </div>
      </div>
    </div>,
    document.body
  )
}

// ─── File Grid Card ────────────────────────────────────────────────────────────

function FileCard({ file, token, onDelete, canEdit, onPreview, showStripe, showExtTag }) {
  const [hovered, setHovered] = useState(false)

  const group   = getMimeGroup(file.mime_type, file.filename)
  const isImage = group === 'image'
  const isVideo = group === 'video'
  const isPdf   = group === 'pdf'
  const catMeta = getCatMeta(file.category)
  const dotColor = catMeta?.color?.dot || 'var(--color-text-muted)'

  const thumbUrl = file.thumbnail_path
    ? `/api/crm/nextcloud/file?path=${encodeURIComponent(file.thumbnail_path)}&token=${encodeURIComponent(token)}`
    : null
  const proxyUrl = `/api/crm/nextcloud/file?path=${encodeURIComponent(file.path)}&token=${encodeURIComponent(token)}`

  const showThumb      = (isImage || isVideo || isPdf) && !file.nc_missing
  const useThumbImg    = showThumb && !!thumbUrl
  const usePdfJs       = showThumb && isPdf && !thumbUrl
  const useImgFallback = showThumb && isImage && !thumbUrl
  const ext = getExt(file.filename)

  const handleClick = () => {
    if (isPreviewable(file.mime_type, file.filename)) onPreview(file)
    else window.open(proxyUrl, '_blank', 'noopener,noreferrer')
  }

  return (
    <div
      onMouseEnter={() => setHovered(true)}
      onMouseLeave={() => setHovered(false)}
      onClick={handleClick}
      style={{
        position: 'relative',
        aspectRatio: '1 / 1',
        borderRadius: 'var(--radius-lg)',
        overflow: 'hidden',
        backgroundColor: 'var(--color-bg-surface)',
        border: `1px solid ${hovered ? 'var(--color-border-focus)' : catMeta?.color?.border || 'var(--color-border)'}`,
        boxShadow: hovered ? 'var(--shadow-md)' : 'var(--shadow-card)',
        transition: 'border-color 0.18s, box-shadow 0.18s',
        cursor: 'pointer',
        display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
      }}
    >
      {/* Thumbnail */}
      {useThumbImg && (
        <img src={thumbUrl} alt={file.filename} style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', transition: 'transform 0.4s', transform: hovered ? 'scale(1.06)' : 'scale(1)' }} />
      )}
      {usePdfJs && (
        <div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'hidden', backgroundColor: '#fff' }}>
          <PdfThumbnail url={proxyUrl} />
        </div>
      )}
      {useImgFallback && (
        <img src={proxyUrl} alt={file.filename} style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', transition: 'transform 0.4s', transform: hovered ? 'scale(1.06)' : 'scale(1)' }} />
      )}
      {isVideo && (
        <div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', pointerEvents: 'none', zIndex: 2 }}>
          <div style={{ width: 42, height: 42, borderRadius: '50%', backgroundColor: 'rgba(0,0,0,0.55)', backdropFilter: 'blur(4px)', border: '1.5px solid rgba(255,255,255,0.35)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
            <svg width="16" height="16" viewBox="0 0 24 24" fill="white"><polygon points="5 3 19 12 5 21 5 3"/></svg>
          </div>
        </div>
      )}

      {/* Icon fallback */}
      {!useThumbImg && !usePdfJs && !useImgFallback && (
        <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 'var(--space-3)', padding: 'var(--space-4)', width: '100%', height: '100%' }}>
          <div style={{ width: 60, height: 60, borderRadius: 'var(--radius-lg)', backgroundColor: 'var(--color-bg-elevated)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
            <FileTypeIcon group={group} size={30} />
          </div>
        </div>
      )}

      {/* Category stripe — bottom, padded, hidden on hover */}
      {showStripe && !hovered && (
        <div style={{
          position: 'absolute',
          bottom: 10,
          left: 12,
          right: 12,
          height: 5,
          borderRadius: 2,
          backgroundColor: dotColor,
          opacity: 1,
          zIndex: 4,
          pointerEvents: 'none',

        }} />
      )}

      {/* EXT badge — top-left */}
      {showExtTag && (
        <div style={{
          position: 'absolute', top: 'var(--space-2)', left: 'var(--space-2)', zIndex: 5,
          fontSize: 'var(--font-size-xs)', fontWeight: 'var(--font-weight-bold)', letterSpacing: 'var(--tracking-wide)',
          padding: '2px 6px', borderRadius: 'var(--radius-sm)',
          backgroundColor: 'rgba(10,14,20,0.72)', backdropFilter: 'blur(6px)',
          color: 'var(--color-text-primary)',
        }}>
          {ext}
        </div>
      )}

      {/* Gradient overlay */}
      <div style={{
        position: 'absolute', inset: 0, zIndex: 2, pointerEvents: 'none',
        background: (useThumbImg || usePdfJs || useImgFallback)
          ? 'linear-gradient(to top, rgba(10,14,20,0.92) 0%, rgba(10,14,20,0.1) 55%, transparent 100%)'
          : 'linear-gradient(to top, rgba(10,14,20,0.80) 0%, transparent 60%)',
        opacity: hovered ? 1 : ((useThumbImg || usePdfJs || useImgFallback) ? 0.45 : 0),
        transition: 'opacity 0.2s',
      }} />

      {/* Hover: filename + actions */}
      <div style={{
        position: 'absolute', bottom: 0, left: 0, right: 0, zIndex: 6,
        padding: 'var(--space-3)',
        opacity: hovered ? 1 : 0,
        transform: hovered ? 'translateY(0)' : 'translateY(4px)',
        transition: 'opacity 0.18s, transform 0.18s',
        pointerEvents: hovered ? 'auto' : 'none',
      }}>
        <p style={{ fontSize: 'var(--font-size-xs)', fontWeight: 'var(--font-weight-medium)', color: 'var(--color-text-primary)', margin: '0 0 var(--space-2)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
          {file.filename}
        </p>
        <div style={{ display: 'flex', gap: 'var(--space-1)' }}>
          <button
            onClick={e => { e.stopPropagation(); handleClick() }}
            style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 4, padding: '5px', backgroundColor: 'rgba(28,32,38,0.88)', backdropFilter: 'blur(8px)', border: '1px solid var(--color-border-strong)', borderRadius: 'var(--radius-sm)', color: 'var(--color-text-primary)', fontSize: 'var(--font-size-xs)', fontWeight: 'var(--font-weight-semibold)', cursor: 'pointer' }}
            aria-label="View"
          ><Icon name="expand" size={12} /> View</button>
          <a
            href={proxyUrl + '&download=1'} download={file.filename}
            onClick={e => e.stopPropagation()}
            style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '5px 7px', backgroundColor: 'rgba(28,32,38,0.88)', backdropFilter: 'blur(8px)', border: '1px solid var(--color-border-strong)', borderRadius: 'var(--radius-sm)', color: 'var(--color-text-primary)', lineHeight: 1 }}
            aria-label="Download"
          ><Icon name="download" size={13} /></a>
          {canEdit && (
            <button
              onClick={e => { e.stopPropagation(); onDelete(file) }}
              style={{ padding: '5px 7px', backgroundColor: 'rgba(28,32,38,0.88)', backdropFilter: 'blur(8px)', border: '1px solid var(--color-border-strong)', borderRadius: 'var(--radius-sm)', color: 'var(--color-danger)', cursor: 'pointer', lineHeight: 1 }}
              aria-label="Delete"
            ><Icon name="delete" size={13} /></button>
          )}
        </div>
      </div>
    </div>
  )
}

// ─── File List Row ─────────────────────────────────────────────────────────────

function FileListRow({ file, token, onDelete, canEdit, onPreview }) {
  const group    = getMimeGroup(file.mime_type, file.filename)
  const catMeta  = getCatMeta(file.category)
  const dotColor = catMeta?.color?.dot || 'var(--color-text-muted)'
  const proxyUrl = `/api/crm/nextcloud/file?path=${encodeURIComponent(file.path)}&token=${encodeURIComponent(token)}`

  const handleOpen = () => {
    if (isPreviewable(file.mime_type, file.filename)) onPreview(file)
    else window.open(proxyUrl, '_blank', 'noopener,noreferrer')
  }

  return (
    <div
      style={{ display: 'grid', gridTemplateColumns: '4px 32px 1fr auto auto auto 80px', alignItems: 'center', gap: 'var(--space-3)', padding: 'var(--space-3) var(--space-4)', borderBottom: '1px solid var(--color-border)', transition: 'background-color 0.12s' }}
      onMouseEnter={e => e.currentTarget.style.backgroundColor = 'var(--color-bg-elevated)'}
      onMouseLeave={e => e.currentTarget.style.backgroundColor = 'transparent'}
    >
      <div style={{ width: 3, height: 28, borderRadius: 2, backgroundColor: dotColor }} />
      <FileTypeIcon group={group} size={20} />
      <button onClick={handleOpen} style={{ background: 'none', border: 'none', cursor: 'pointer', textAlign: 'left', padding: 0, fontSize: 'var(--font-size-sm)', fontWeight: 'var(--font-weight-medium)', color: 'var(--color-text-primary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
        {file.filename}
      </button>
      <span style={{ fontSize: 'var(--font-size-xs)', fontWeight: 'var(--font-weight-bold)', padding: '2px 8px', borderRadius: 'var(--radius-sm)', backgroundColor: catMeta?.color?.bg || 'var(--color-bg-elevated)', color: catMeta?.color?.text || 'var(--color-text-muted)', border: `1px solid ${catMeta?.color?.border || 'var(--color-border)'}`, whiteSpace: 'nowrap' }}>
        {catMeta?.label || file.category}
      </span>
      <span style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-muted)', whiteSpace: 'nowrap' }}>{formatFileSize(file.size_bytes)}</span>
      <span style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-muted)', whiteSpace: 'nowrap' }}>{formatDate(file.uploaded_at || file.created_at)}</span>
      <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 'var(--space-1)' }}>
        <Button variant="table-actions" size="sm" onClick={handleOpen} aria-label="Open"><Icon name="expand" size={14} /></Button>
        <Button variant="table-actions" size="sm" onClick={() => { const a = document.createElement('a'); a.href = proxyUrl + '&download=1'; a.download = file.filename; a.click() }} aria-label="Download"><Icon name="download" size={14} /></Button>
        {canEdit && <Button variant="table-actions" size="sm" onClick={() => onDelete(file)} aria-label="Delete"><Icon name="delete" size={14} color="var(--color-danger)" /></Button>}
      </div>
    </div>
  )
}

// ─── Sort dropdown ─────────────────────────────────────────────────────────────

function SortDropdown({ value, onChange }) {
  const [open, setOpen] = useState(false)
  const ref = useRef(null)
  const current = SORT_OPTIONS.find(o => o.value === value) || SORT_OPTIONS[0]
  useEffect(() => {
    if (!open) return
    const h = e => { if (ref.current && !ref.current.contains(e.target)) setOpen(false) }
    document.addEventListener('mousedown', h)
    return () => document.removeEventListener('mousedown', h)
  }, [open])
  return (
    <div ref={ref} style={{ position: 'relative' }}>
      <button style={TB} onClick={() => setOpen(v => !v)}>
        <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="15" y2="12"/><line x1="3" y1="18" x2="9" y2="18"/></svg>
        {current.label}
        <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
      </button>
      {open && (
        <>
          <div onClick={() => setOpen(false)} style={{ position: 'fixed', inset: 0, zIndex: 49 }} />
          <div style={{ position: 'absolute', top: 'calc(100% + 4px)', left: 0, zIndex: 50, backgroundColor: 'var(--color-bg-elevated)', border: '1px solid var(--color-border-strong)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-lg)', overflow: 'hidden', minWidth: 160 }}>
            {SORT_OPTIONS.map(o => (
              <button key={o.value} onClick={() => { onChange(o.value); setOpen(false) }}
                style={{ display: 'block', width: '100%', textAlign: 'left', padding: '8px 14px', fontSize: 'var(--font-size-sm)', fontFamily: 'var(--font-family-base)', border: 'none', cursor: 'pointer', backgroundColor: o.value === value ? 'var(--color-primary-subtle)' : 'transparent', color: o.value === value ? 'var(--color-primary)' : 'var(--color-text-secondary)' }}
                onMouseEnter={e => { if (o.value !== value) e.currentTarget.style.backgroundColor = 'var(--color-bg-island)' }}
                onMouseLeave={e => { e.currentTarget.style.backgroundColor = o.value === value ? 'var(--color-primary-subtle)' : 'transparent' }}
              >{o.label}</button>
            ))}
          </div>
        </>
      )}
    </div>
  )
}

// ─── Gear menu ─────────────────────────────────────────────────────────────────

function GearMenu({ customerId, onRefresh, thumbGenLoading, setThumbGenLoading, setThumbGenResult, showStripe, setShowStripe, showExtTag, setShowExtTag }) {
  const [open, setOpen] = useState(false)
  const ref = useRef(null)
  const { toast } = useToast()

  useEffect(() => {
    if (!open) return
    const h = e => { if (ref.current && !ref.current.contains(e.target)) setOpen(false) }
    document.addEventListener('mousedown', h)
    return () => document.removeEventListener('mousedown', h)
  }, [open])

  const handleGenerateThumbs = async () => {
    setOpen(false); setThumbGenLoading(true); setThumbGenResult(null)
    try {
      const fd = new FormData(); fd.append('customer_id', customerId)
      const tok = localStorage.getItem('access_token') || ''
      const resp = await fetch('/api/crm/nextcloud/generate-thumbs', { method: 'POST', headers: { Authorization: `Bearer ${tok}` }, body: fd })
      if (!resp.ok) throw new Error()
      const result = await resp.json()
      setThumbGenResult(result); onRefresh()
      toast.success('Thumbnails generated', `${result.generated} generated, ${result.skipped} skipped.`)
    } catch { toast.danger('Failed', 'Could not generate thumbnails.') }
    finally { setThumbGenLoading(false) }
  }

  const handleClearThumbs = async () => {
    setOpen(false)
    try {
      const fd = new FormData(); fd.append('customer_id', customerId)
      const tok = localStorage.getItem('access_token') || ''
      const resp = await fetch('/api/crm/nextcloud/clear-thumbs', { method: 'POST', headers: { Authorization: `Bearer ${tok}` }, body: fd })
      if (!resp.ok) throw new Error()
      onRefresh(); toast.success('Thumbnails cleared', 'All thumbnail folders removed.')
    } catch { toast.danger('Failed', 'Could not clear thumbnails.') }
  }

  const rowStyle = (danger) => ({
    display: 'flex', alignItems: 'center', gap: 'var(--space-3)',
    width: '100%', textAlign: 'left', padding: 'var(--space-3) var(--space-4)',
    fontSize: 'var(--font-size-sm)', fontFamily: 'var(--font-family-base)',
    border: 'none', borderBottom: '1px solid var(--color-border)',
    cursor: 'pointer', backgroundColor: 'transparent',
    color: danger ? 'var(--color-danger)' : 'var(--color-text-primary)',
    transition: 'background-color 0.12s',
  })

  const toggleRow = (icon, label, desc, active, onToggle) => (
    <button
      onClick={e => { e.stopPropagation(); onToggle(!active) }}
      style={{ ...rowStyle(false), borderBottom: '1px solid var(--color-border)' }}
      onMouseEnter={e => e.currentTarget.style.backgroundColor = 'var(--color-bg-island)'}
      onMouseLeave={e => e.currentTarget.style.backgroundColor = 'transparent'}
    >
      <div style={{ flexShrink: 0, color: 'var(--color-text-muted)' }}>{icon}</div>
      <div style={{ flex: 1 }}>
        <div style={{ fontWeight: 'var(--font-weight-semibold)' }}>{label}</div>
        <div style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', marginTop: 2, lineHeight: 1.4 }}>{desc}</div>
      </div>
      <div style={{
        width: 28, height: 16, borderRadius: 8, flexShrink: 0,
        backgroundColor: active ? 'var(--color-primary-container)' : 'var(--color-bg-island)',
        border: `1px solid ${active ? 'var(--color-primary-container)' : 'var(--color-border-strong)'}`,
        position: 'relative', transition: 'background-color 0.15s',
      }}>
        <div style={{ position: 'absolute', top: 1, left: active ? 13 : 1, width: 12, height: 12, borderRadius: '50%', backgroundColor: active ? 'var(--color-text-inverse)' : 'var(--color-text-muted)', transition: 'left 0.15s' }} />
      </div>
    </button>
  )

  return (
    <div ref={ref} style={{ position: 'relative' }}>
      <button
        onClick={() => setOpen(v => !v)}
        style={{ ...TB, padding: '0 9px', ...(thumbGenLoading ? { opacity: 0.6, cursor: 'wait' } : {}) }}
        disabled={thumbGenLoading} title="Media settings" aria-label="Settings"
      >
        {thumbGenLoading
          ? <Spinner size="sm" />
          : <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
        }
      </button>
      {open && (
        <>
          <div onClick={() => setOpen(false)} style={{ position: 'fixed', inset: 0, zIndex: 49 }} />
          <div style={{ position: 'absolute', right: 0, top: 'calc(100% + 4px)', zIndex: 50, backgroundColor: 'var(--color-bg-elevated)', border: '1px solid var(--color-border-strong)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-lg)', minWidth: 300, overflow: 'hidden' }}>

            {/* Section label */}
            <div style={{ padding: '8px var(--space-4) 4px', fontSize: 'var(--font-size-xs)', fontWeight: 'var(--font-weight-bold)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)', color: 'var(--color-text-muted)' }}>
              Display
            </div>

            {/* Toggle: category stripe */}
            {toggleRow(
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="9" width="20" height="3" rx="1.5"/></svg>,
              'Category Stripe',
              'Show a colour-coded stripe below the file type tag on each thumbnail.',
              showStripe,
              setShowStripe,
            )}

            {/* Toggle: ext tag */}
            {toggleRow(
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="8" width="10" height="6" rx="1"/><path d="M13 11h5M16 9l2 2-2 2"/></svg>,
              'File Type Tag',
              'Show the file extension badge on the top-left of each thumbnail.',
              showExtTag,
              setShowExtTag,
            )}

            {/* Section label */}
            <div style={{ padding: '8px var(--space-4) 4px', fontSize: 'var(--font-size-xs)', fontWeight: 'var(--font-weight-bold)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)', color: 'var(--color-text-muted)' }}>
              Thumbnails
            </div>

            {/* Generate */}
            <button
              onClick={handleGenerateThumbs}
              style={{ ...rowStyle(false), borderBottom: '1px solid var(--color-border)' }}
              onMouseEnter={e => e.currentTarget.style.backgroundColor = 'var(--color-bg-island)'}
              onMouseLeave={e => e.currentTarget.style.backgroundColor = 'transparent'}
            >
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M8 17l4-8 4 8"/><path d="M10 13h4"/></svg>
              <div style={{ flex: 1 }}>
                <div style={{ fontWeight: 'var(--font-weight-semibold)' }}>Generate Missing Thumbnails</div>
                <div style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', marginTop: 2, lineHeight: 1.4 }}>Creates thumbnails for all files that don't have one yet. Existing thumbnails are skipped.</div>
              </div>
            </button>

            {/* Flush */}
            <button
              onClick={handleClearThumbs}
              style={{ ...rowStyle(true), borderBottom: 'none' }}
              onMouseEnter={e => e.currentTarget.style.backgroundColor = 'var(--color-danger-bg)'}
              onMouseLeave={e => e.currentTarget.style.backgroundColor = 'transparent'}
            >
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4h6v2"/></svg>
              <div style={{ flex: 1 }}>
                <div style={{ fontWeight: 'var(--font-weight-semibold)' }}>Flush All Thumbnails</div>
                <div style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', marginTop: 2, lineHeight: 1.4 }}>Deletes all .thumbs folders for this customer. Thumbnails can be regenerated afterwards.</div>
              </div>
            </button>
          </div>
        </>
      )}
    </div>
  )
}

// ─── Group toggle ──────────────────────────────────────────────────────────────

function GroupToggle({ active, onChange }) {
  return (
    <button onClick={() => onChange(!active)} style={{ ...TB, border: `1px solid ${active ? 'var(--color-primary)' : 'var(--color-border-strong)'}`, color: active ? 'var(--color-primary)' : 'var(--color-text-secondary)' }}>
      <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
        <rect x="3" y="3" width="7" height="4"/><rect x="14" y="3" width="7" height="4"/><rect x="3" y="11" width="7" height="4"/><rect x="14" y="11" width="7" height="4"/>
      </svg>
      Group
    </button>
  )
}

// ─── View toggle ───────────────────────────────────────────────────────────────

function ViewToggle({ view, onChange }) {
  return (
    <div style={{ display: 'flex', border: '1px solid var(--color-border-strong)', borderRadius: 'var(--radius-md)', overflow: 'hidden', height: 34 }}>
      {[
        { key: 'grid', icon: <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg> },
        { key: 'list', icon: <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg> },
      ].map(({ key, icon }) => (
        <button key={key} onClick={() => onChange(key)} aria-label={`${key} view`} style={{ padding: '0 10px', border: 'none', cursor: 'pointer', backgroundColor: view === key ? 'var(--color-primary-container)' : 'var(--color-bg-surface)', color: view === key ? 'var(--color-text-primary)' : 'var(--color-text-muted)', lineHeight: 1, transition: 'background-color 0.12s' }}>
          {icon}
        </button>
      ))}
    </div>
  )
}

// ─── Page size selector ────────────────────────────────────────────────────────

function PageSizeSelector({ value, onChange }) {
  return (
    <div style={{ display: 'flex', border: '1px solid var(--color-border-strong)', borderRadius: 'var(--radius-md)', overflow: 'hidden', height: 34 }}>
      {PAGE_SIZES.map((n, i) => (
        <button key={n} onClick={() => onChange(n)} style={{ padding: '0 9px', border: 'none', borderRight: i < PAGE_SIZES.length - 1 ? '1px solid var(--color-border-strong)' : 'none', cursor: 'pointer', fontSize: 'var(--font-size-xs)', fontWeight: 'var(--font-weight-bold)', fontFamily: 'var(--font-family-base)', backgroundColor: n === value ? 'var(--color-bg-island)' : 'var(--color-bg-surface)', color: n === value ? 'var(--color-primary)' : 'var(--color-text-muted)', transition: 'background-color 0.12s' }}>
          {n}
        </button>
      ))}
    </div>
  )
}

// ─── Pagination ────────────────────────────────────────────────────────────────

function PaginationBar({ total, page, pageSize, onPage }) {
  const totalPages = Math.max(1, Math.ceil(total / pageSize))
  const pages = []
  if (totalPages <= 7) { for (let i = 1; i <= totalPages; i++) pages.push(i) }
  else {
    pages.push(1)
    if (page > 3) pages.push('…')
    for (let i = Math.max(2, page - 1); i <= Math.min(totalPages - 1, page + 1); i++) pages.push(i)
    if (page < totalPages - 2) pages.push('…')
    pages.push(totalPages)
  }
  const nb = (active) => ({ ...TB, padding: '0 8px', width: 34, justifyContent: 'center', ...(active ? { backgroundColor: 'var(--color-primary-container)', borderColor: 'var(--color-primary-container)', color: 'var(--color-text-primary)', fontWeight: 'var(--font-weight-bold)' } : {}) })
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 2 }}>
      <button onClick={() => onPage(page - 1)} disabled={page === 1} style={{ ...nb(false), opacity: page === 1 ? 0.3 : 1, cursor: page === 1 ? 'default' : 'pointer' }} aria-label="Prev"><Icon name="chevron_left" size={14} /></button>
      {pages.map((p, i) => p === '…'
        ? <span key={`e${i}`} style={{ width: 24, textAlign: 'center', fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)' }}>…</span>
        : <button key={p} onClick={() => onPage(p)} style={nb(p === page)}>{p}</button>
      )}
      <button onClick={() => onPage(page + 1)} disabled={page === totalPages} style={{ ...nb(false), opacity: page === totalPages ? 0.3 : 1, cursor: page === totalPages ? 'default' : 'pointer' }} aria-label="Next"><Icon name="chevron_right" size={14} /></button>
    </div>
  )
}

// ─── Empty state ───────────────────────────────────────────────────────────────

function EmptyFiles({ filtered }) {
  return (
    <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: 'var(--space-16)', gap: 'var(--space-3)', textAlign: 'center' }}>
      <div style={{ width: 64, height: 64, borderRadius: 'var(--radius-xl)', backgroundColor: 'var(--color-bg-elevated)', display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 'var(--space-2)' }}>
        <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ color: 'var(--color-text-muted)' }}><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
      </div>
      <p style={{ fontSize: 'var(--font-size-md)', fontWeight: 'var(--font-weight-semibold)', color: 'var(--color-text-secondary)', margin: 0 }}>{filtered ? 'No matching files' : 'No files yet'}</p>
      <p style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-muted)', margin: 0 }}>{filtered ? 'Try adjusting your filters or search.' : 'Upload files to get started.'}</p>
    </div>
  )
}

// ─── Main Component ────────────────────────────────────────────────────────────

export default function FilesTab({ customer, canEdit }) {
  const { id } = customer
  const toast  = useToast()
  const token  = localStorage.getItem('access_token') || ''

  const [media,     setMedia]     = useState([])
  const [ncFiles,   setNcFiles]   = useState([])
  const [thumbMap,  setThumbMap]  = useState({})
  const [loading,   setLoading]   = useState(true)
  const [error,     setError]     = useState('')

  const [category,    setCategory]    = useState('all')
  const [search,      setSearch]      = useState('')
  const [sort,        setSort]        = useState('date_desc')
  const [groupView,   setGroupView]   = useState(false)
  const [view,        setView]        = useState('grid')
  const [page,        setPage]        = useState(1)
  const [pageSize,    setPageSize]    = useState(20)
  const [showUpload,  setShowUpload]  = useState(false)
  const [syncLoading, setSyncLoading] = useState(false)
  const [deleteTarget, setDeleteTarget] = useState(null)
  const [previewFile,  setPreviewFile]  = useState(null)

  // Display settings
  const [showStripe,  setShowStripe]  = useState(true)
  const [showExtTag,  setShowExtTag]  = useState(true)

  // Gear / thumb gen
  const [thumbGenLoading, setThumbGenLoading] = useState(false)
  const [thumbGenResult,  setThumbGenResult]  = useState(null)

  const loadMedia = useCallback(() => {
    setLoading(true); setError('')
    api.get(`/crm/media?customer_id=${id}`)
      .then(data => setMedia(data.items || []))
      .catch(() => setError('Failed to load files.'))
      .finally(() => setLoading(false))
  }, [id])

  const browseNextcloud = useCallback(() => {
    api.get(`/crm/nextcloud/browse-all?customer_id=${id}`)
      .then(data => {
        const items = data.items || []
        setNcFiles(items)
        const thumbsByKey = {}
        for (const f of items) {
          if (!f.path.includes('/.thumbs/')) continue
          const parent = f.path.split('/.thumbs/')[0]
          const stem   = f.path.split('/').pop().replace(/\.[^.]+$/, '')
          thumbsByKey[`${parent}|${stem}`] = f.path
        }
        const map = {}
        for (const f of items) {
          if (f.path.includes('/.thumbs/')) continue
          const parent = f.path.split('/').slice(0, -1).join('/')
          const stem   = f.path.split('/').pop().replace(/\.[^.]+$/, '')
          const thumb  = thumbsByKey[`${parent}|${stem}`]
          if (thumb) map[f.path] = thumb
        }
        setThumbMap(map)
      })
      .catch(() => { setNcFiles([]); setThumbMap({}) })
  }, [id])

  useEffect(() => { loadMedia(); browseNextcloud() }, [loadMedia, browseNextcloud])

  const refresh = useCallback(() => { loadMedia(); browseNextcloud() }, [loadMedia, browseNextcloud])

  const handleSync = async () => {
    setSyncLoading(true)
    try {
      const fd = new FormData(); fd.append('customer_id', id)
      const tok = localStorage.getItem('access_token') || ''
      const resp = await fetch('/api/crm/nextcloud/sync', { method: 'POST', headers: { Authorization: `Bearer ${tok}` }, body: fd })
      if (!resp.ok) throw new Error()
      toast.success('Sync complete', 'File index is up to date.'); refresh()
    } catch { toast.danger('Sync failed', 'Could not sync files from Nextcloud.') }
    finally { setSyncLoading(false) }
  }

  const handleDeleteConfirm = async () => {
    if (!deleteTarget) return
    const path = deleteTarget.path || deleteTarget.nextcloud_path
    try {
      await api.delete(`/crm/nextcloud/file?path=${encodeURIComponent(path)}`)
      toast.success('Deleted', `${deleteTarget.filename} removed.`)
      setDeleteTarget(null); refresh()
    } catch { toast.danger('Delete failed', 'Could not delete this file.') }
  }

  // Merged file list
  const allFiles = (() => {
    const ncFileMap = {}
    for (const f of ncFiles) {
      if (f.path.includes('/.thumbs/')) continue
      const parts = f.path.split('/')
      const rawSub = parts.length >= 3 ? parts[2] : 'other'
      ncFileMap[f.path] = { ...f, category: SUBFOLDER_TO_CATEGORY[rawSub] || rawSub }
    }
    const dbMap = {}
    for (const m of media) dbMap[m.nextcloud_path] = m
    const merged = []
    for (const m of media) {
      merged.push({ id: m.id, filename: m.filename || m.nextcloud_path?.split('/').pop() || '—', path: m.nextcloud_path, mime_type: m.mime_type || '', size_bytes: m.size_bytes || (ncFileMap[m.nextcloud_path]?.size || 0), uploaded_at: m.uploaded_at || m.created_at, category: m.category || detectCategory(m.nextcloud_path || ''), thumbnail_path: thumbMap[m.nextcloud_path] || null, tracked: true })
    }
    for (const [path, f] of Object.entries(ncFileMap)) {
      if (dbMap[path]) continue
      merged.push({ id: `nc-${path}`, filename: f.name || path.split('/').pop(), path, mime_type: f.mime_type || '', size_bytes: f.size || 0, uploaded_at: f.last_modified || null, category: f.category, thumbnail_path: thumbMap[path] || null, tracked: false })
    }
    return merged
  })()

  const filtered = allFiles.filter(f => {
    if (category !== 'all' && f.category !== category) return false
    if (search) { const q = search.toLowerCase(); const n = (f.filename || '').toLowerCase(); const e = n.includes('.') ? n.split('.').pop() : ''; if (!n.includes(q) && e !== q) return false }
    return true
  })

  const sorted = [...filtered].sort((a, b) => {
    switch (sort) {
      case 'date_desc': return new Date(b.uploaded_at || 0) - new Date(a.uploaded_at || 0)
      case 'date_asc':  return new Date(a.uploaded_at || 0) - new Date(b.uploaded_at || 0)
      case 'name_asc':  return a.filename.localeCompare(b.filename)
      case 'name_desc': return b.filename.localeCompare(a.filename)
      case 'size_desc': return (b.size_bytes || 0) - (a.size_bytes || 0)
      case 'size_asc':  return (a.size_bytes || 0) - (b.size_bytes || 0)
      default:          return 0
    }
  })

  const totalCount = sorted.length
  const paged      = sorted.slice((page - 1) * pageSize, page * pageSize)
  const isFiltered = category !== 'all' || !!search

  useEffect(() => { setPage(1) }, [category, search, sort, pageSize])

  const catCounts = {}
  for (const f of allFiles) catCounts[f.category] = (catCounts[f.category] || 0) + 1

  const GROUP_DEFS = [
    { key: 'media',     label: 'Media',              values: ['media', 'sent_media', 'received_media'] },
    { key: 'invoices',  label: 'Invoices / Shipping', values: ['invoices'] },
    { key: 'quotes',    label: 'Quotations',          values: ['quotations'] },
    { key: 'documents', label: 'Documents',            values: ['documents'] },
  ]
  const showGroups = groupView && category === 'all' && !search
  const groupedSections = showGroups
    ? GROUP_DEFS.map(g => ({ ...g, files: sorted.filter(f => g.values.includes(f.category)) })).filter(g => g.files.length > 0)
    : null

  // Previewable list for prev/next nav
  const previewList = sorted.filter(f => isPreviewable(f.mime_type, f.filename))

  const renderGrid = files => (
    <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', gap: 'var(--space-4)' }}>
      {files.map(f => <FileCard key={f.id || f.path} file={f} token={token} canEdit={canEdit} onDelete={setDeleteTarget} onPreview={setPreviewFile} showStripe={showStripe} showExtTag={showExtTag} />)}
    </div>
  )

  const renderList = files => (
    <div style={{ backgroundColor: 'var(--color-bg-surface)', border: '1px solid var(--color-border)', borderRadius: 'var(--radius-lg)', overflow: 'hidden', boxShadow: 'var(--shadow-card)' }}>
      <div style={{ display: 'grid', gridTemplateColumns: '4px 32px 1fr auto auto auto 80px', gap: 'var(--space-3)', padding: 'var(--space-2) var(--space-4)', borderBottom: '1px solid var(--color-border)', backgroundColor: 'var(--color-bg-void)' }}>
        {['', '', 'Name', 'Category', 'Size', 'Uploaded', ''].map((h, i) => (
          <span key={i} style={{ fontSize: 'var(--font-size-xs)', fontWeight: 'var(--font-weight-bold)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)', color: 'var(--color-text-muted)' }}>{h}</span>
        ))}
      </div>
      {files.map(f => <FileListRow key={f.id || f.path} file={f} token={token} canEdit={canEdit} onDelete={setDeleteTarget} onPreview={setPreviewFile} />)}
    </div>
  )

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-5)' }}>

      {/* Scoped overrides */}
      <style>{`.files-toolbar-search .searchbar-input { height: 34px; padding-top: 0; padding-bottom: 0; font-size: var(--font-size-sm); }`}</style>

      {/* Category chips */}
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--space-2)' }}>
        {CATEGORIES.map(cat => {
          const active = category === cat.value
          const count  = cat.value === 'all' ? allFiles.length : (catCounts[cat.value] || 0)
          const col    = cat.color
          return (
            <button key={cat.value} onClick={() => setCategory(cat.value)} style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '5px 12px', borderRadius: 'var(--radius-md)', border: `1px solid ${active ? (col?.border || 'var(--color-primary-container)') : 'var(--color-border-strong)'}`, backgroundColor: active ? (col?.bg || 'var(--color-primary-subtle)') : 'var(--color-bg-surface)', color: active ? (col?.text || 'var(--color-primary)') : 'var(--color-text-muted)', fontSize: 'var(--font-size-xs)', fontWeight: 'var(--font-weight-bold)', fontFamily: 'var(--font-family-base)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)', cursor: 'pointer', transition: 'all 0.15s', whiteSpace: 'nowrap' }}>
              {col && active && <span style={{ width: 6, height: 6, borderRadius: '50%', backgroundColor: col.dot, flexShrink: 0 }} />}
              {cat.label}
              <span style={{ fontSize: 10, fontWeight: 'var(--font-weight-bold)', padding: '1px 5px', borderRadius: 'var(--radius-sm)', backgroundColor: active ? 'rgba(0,0,0,0.18)' : 'var(--color-bg-elevated)', color: active ? (col?.text || 'var(--color-primary)') : 'var(--color-text-muted)' }}>
                {count}
              </span>
            </button>
          )
        })}
      </div>

      {/* Toolbar */}
      <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-2)', flexWrap: 'wrap', padding: 'var(--space-3) var(--space-4)', backgroundColor: 'var(--color-bg-surface)', border: '1px solid var(--color-border)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-card)' }}>
        <SortDropdown value={sort} onChange={setSort} />
        <GroupToggle active={groupView} onChange={setGroupView} />

        <div style={{ width: 1, height: 22, backgroundColor: 'var(--color-border-strong)', flexShrink: 0 }} />

        {/* Search — constrained to 34px toolbar height */}
        <div style={{ flex: 1, minWidth: 140 }} className="files-toolbar-search">
          <SearchBar value={search} onChange={v => { setSearch(v); setPage(1) }} placeholder="Search files…" />
        </div>

        <div style={{ width: 1, height: 22, backgroundColor: 'var(--color-border-strong)', flexShrink: 0 }} />

        <PageSizeSelector value={pageSize} onChange={n => { setPageSize(n); setPage(1) }} />
        {totalCount > 0 && <PaginationBar total={totalCount} page={page} pageSize={pageSize} onPage={setPage} />}

        <div style={{ width: 1, height: 22, backgroundColor: 'var(--color-border-strong)', flexShrink: 0 }} />

        <ViewToggle view={view} onChange={setView} />

        {canEdit && (
          <button onClick={handleSync} style={{ ...TB, ...(syncLoading ? { opacity: 0.6, cursor: 'wait' } : {}) }} disabled={syncLoading} title="Sync from Nextcloud">
            {syncLoading ? <Spinner size="sm" /> : <Icon name="refresh" size={14} />}
            Sync
          </button>
        )}

        {canEdit && (
          <Button variant="primary" size="md" onClick={() => setShowUpload(true)}>
            <span style={{ display: 'flex', alignItems: 'center', gap: 6 }}><Icon name="upload" size={14} /> Upload</span>
          </Button>
        )}

        {canEdit && (
          <GearMenu
            customerId={id} onRefresh={refresh}
            thumbGenLoading={thumbGenLoading} setThumbGenLoading={setThumbGenLoading}
            setThumbGenResult={setThumbGenResult}
            showStripe={showStripe} setShowStripe={setShowStripe}
            showExtTag={showExtTag} setShowExtTag={setShowExtTag}
          />
        )}
      </div>

      {/* Thumb gen result banner */}
      {thumbGenResult && (
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: 'var(--space-3) var(--space-4)', backgroundColor: 'var(--color-primary-subtle)', border: '1px solid var(--color-border-focus)', borderRadius: 'var(--radius-lg)', fontSize: 'var(--font-size-sm)', color: 'var(--color-primary)' }}>
          <span>✓ {thumbGenResult.generated} generated, {thumbGenResult.skipped} skipped{thumbGenResult.failed > 0 ? `, ${thumbGenResult.failed} failed` : ''}</span>
          <button onClick={() => setThumbGenResult(null)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--color-text-muted)', lineHeight: 1 }}><Icon name="close" size={14} /></button>
        </div>
      )}

      {/* File area */}
      {loading ? (
        <div style={{ display: 'flex', justifyContent: 'center', padding: 'var(--space-16)' }}><Spinner size="lg" /></div>
      ) : error ? (
        <div style={{ backgroundColor: 'var(--color-danger-bg)', border: '1px solid var(--color-danger)', borderRadius: 'var(--radius-lg)', padding: 'var(--space-4)', color: 'var(--color-danger)', fontSize: 'var(--font-size-sm)' }}>{error}</div>
      ) : showGroups ? (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-6)' }}>
          {groupedSections.length === 0 ? <EmptyFiles filtered={isFiltered} /> : groupedSections.map(g => (
            <div key={g.key}>
              <p style={{ fontSize: 'var(--font-size-xs)', fontWeight: 'var(--font-weight-bold)', textTransform: 'uppercase', letterSpacing: 'var(--tracking-wide)', color: 'var(--color-text-muted)', margin: '0 0 var(--space-3)' }}>
                {g.label} <span style={{ opacity: 0.6, fontWeight: 'var(--font-weight-normal)' }}>({g.files.length})</span>
              </p>
              {view === 'grid' ? renderGrid(g.files) : renderList(g.files)}
            </div>
          ))}
        </div>
      ) : paged.length === 0 ? (
        <EmptyFiles filtered={isFiltered} />
      ) : view === 'grid' ? renderGrid(paged) : renderList(paged)}

      {/* Modals */}
      <UploadModal open={showUpload} customerId={id} onClose={() => setShowUpload(false)} onSuccess={refresh} />

      <PreviewModal
        file={previewFile}
        previewList={previewList}
        token={token}
        canEdit={canEdit}
        onClose={() => setPreviewFile(null)}
        onDelete={f => { setPreviewFile(null); setDeleteTarget(f) }}
        onNavigate={setPreviewFile}
      />

      <ConfirmDialog
        open={!!deleteTarget}
        variant="danger"
        title="Delete File"
        message={`Permanently delete "${deleteTarget?.filename || 'this file'}" from Nextcloud? This cannot be undone.`}
        confirmLabel="Delete"
        onConfirm={handleDeleteConfirm}
        onCancel={() => setDeleteTarget(null)}
      />
    </div>
  )
}
