// Workspace — Vault module: home, editor, folder, history drawer, conflict banner

// ── Vault Home ──────────────────────────────────────────────────────
function VaultHome({ onOpenDoc }) {
  const [q, setQ] = React.useState('');
  const [showNew, setShowNew] = React.useState(false);
  const totalDocs = (window.VAULT_TREE || []).reduce((n, f) => n + (f.count || 0), 0);
  return (
    <div style={{ flex: 1, overflowY: 'auto', padding: '32px 40px 100px', position: 'relative' }}>
      <div style={{ maxWidth: 1280, margin: '0 auto' }}>
        <div style={{ marginBottom: 28 }}>
          <h1 style={{ fontSize: 30, fontWeight: 600, letterSpacing: -0.6, margin: 0, color: 'var(--text-0)' }}>Knowledge Vault</h1>
          <div style={{ marginTop: 6, fontSize: 13, color: 'var(--text-2)' }}>
            <span className="mono" style={{ color: 'var(--text-1)' }}>{totalDocs}</span> {totalDocs === 1 ? 'documento' : 'documentos'}
            {RECENT_DOCS.length > 0 && (<>
              <span style={{ margin: '0 8px', color: 'var(--text-3)' }}>·</span>
              última actividad <span className="mono" style={{ color: 'var(--text-1)' }}>{relTime(RECENT_DOCS[0].mins)}</span>
              {' '}por <span style={{ color: ACTORS[RECENT_DOCS[0].author]?.color || 'var(--text-1)' }}>{ACTORS[RECENT_DOCS[0].author]?.name || RECENT_DOCS[0].author}</span>
            </>)}
          </div>
        </div>

        <div style={{ marginBottom: 36 }}>
          <TextInput value={q} onChange={setQ}
            placeholder="Buscar en todo el Vault…"
            icon="search" kbd="⌘K" size="lg"
            style={{ maxWidth: 720 }} />
        </div>

        {totalDocs === 0 && <EmptyWorkspaceNudge kind="vault" />}

        {RECENT_DOCS.length === 0 ? (
          <div style={{
            padding: '60px 24px', textAlign: 'center',
            border: '1px dashed var(--border)', borderRadius: 'var(--r-lg)',
            background: 'rgba(22,25,34,.4)',
          }}>
            <Icon name="library" size={28} color="var(--text-3)" />
            <div style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-1)', marginTop: 14 }}>El Vault está vacío</div>
            <div style={{ fontSize: 12.5, color: 'var(--text-2)', marginTop: 4, marginBottom: 18 }}>Crea tu primer documento para empezar a llenar el conocimiento del proyecto.</div>
            <Button variant="primary" icon="plus" onClick={() => setShowNew(true)}>Nuevo documento</Button>
          </div>
        ) : (<>
          <SectionHead title="Documentos recientes" sub={`${RECENT_DOCS.length} ${RECENT_DOCS.length === 1 ? 'edición' : 'ediciones'} recientes`} />
          <div style={{
            display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 14,
          }}>
            {RECENT_DOCS.map((d, i) => (
              <DocCard key={i} doc={d} onClick={() => onOpenDoc(d.path)} />
            ))}
          </div>
        </>)}
      </div>

      {/* Floating CTA — hidden when the vault is empty, since the empty-
          state card already shows a prominent "Nuevo documento" button.
          Without this guard both buttons render at once and look duplicated. */}
      {totalDocs > 0 && (
        <button className="btn-reset" onClick={() => setShowNew(true)} style={{
          position: 'absolute', right: 32, bottom: 28,
          height: 44, padding: '0 18px',
          background: 'var(--accent)', color: '#0A0E1A',
          border: 'none', borderRadius: 22,
          fontWeight: 600, fontSize: 13.5,
          display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer',
          boxShadow: '0 10px 32px rgba(79,142,255,.30), 0 0 0 1px rgba(79,142,255,.4)',
        }}>
          <Icon name="plus" size={16} /> Nuevo documento
        </button>
      )}

      {showNew && <NewDocModal onClose={() => setShowNew(false)} onCreated={(path) => { setShowNew(false); onOpenDoc && onOpenDoc(path); }} />}
    </div>
  );
}

function slugify(s) { return s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '').slice(0, 30); }

function DocCard({ doc, onClick }) {
  const [hover, setHover] = React.useState(false);
  return (
    <div onClick={onClick}
      onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
      style={{
        background: 'var(--bg-1)',
        border: '1px solid var(--border)',
        borderRadius: 'var(--r-lg)',
        padding: 16, cursor: 'pointer', position: 'relative',
        transition: 'transform var(--t-mid), border-color var(--t-fast), background var(--t-fast)',
        transform: hover ? 'translateY(-2px)' : 'none',
        borderColor: hover ? 'var(--border-strong)' : 'var(--border)',
      }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 10, fontSize: 11.5, color: 'var(--text-2)' }}>
        <Icon name="folder" size={12} />
        <span className="mono">/{doc.folder}</span>
      </div>
      <div style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-0)', marginBottom: 8, letterSpacing: -0.1, lineHeight: 1.35 }}>{doc.title}</div>
      <div style={{
        fontSize: 12, color: 'var(--text-2)', lineHeight: 1.55, marginBottom: 14,
        display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden',
      }}>{doc.excerpt}</div>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 7, fontSize: 11.5, color: 'var(--text-2)' }}>
          <Avatar actor={doc.author} size={18} />
          {ACTORS[doc.author].name} · {relTime(doc.mins)}
        </div>
        {hover && (
          <div style={{ display: 'flex', gap: 2 }}>
            {['external-link', 'move', 'link'].map(ic => (
              <button key={ic} className="btn-reset" onClick={e => e.stopPropagation()}
                style={{ width: 24, height: 24, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 4, color: 'var(--text-2)' }}
                onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-2)'}
                onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
                <Icon name={ic} size={12} />
              </button>
            ))}
          </div>
        )}
      </div>
    </div>
  );
}

// ── Vault Folder view ───────────────────────────────────────────────
// `folder` is the per-project slug from the URL hash (e.g. "specs"). After
// the per-project folder migration, VAULT_TREE entries are keyed by their
// UUID `id` plus a separate `slug` field; match on slug to keep the URL
// stable across UUIDs.
function VaultFolder({ folder, onOpenDoc, isEmpty }) {
  const meta = VAULT_TREE.find(f => (f.slug || f.id) === folder) || { id: folder, slug: folder, children: [] };
  const allFiles = meta.children || [];
  const [selected, setSelected] = React.useState(new Set());
  const [showNew, setShowNew] = React.useState(false);
  const [uploads, setUploads] = React.useState([]);
  const [uploadingCount, setUploadingCount] = React.useState(0);
  const [uploadError, setUploadError] = React.useState(null);
  const [dragOver, setDragOver] = React.useState(false);
  const fileInputRef = React.useRef(null);

  // Filter / sort / search state. Reset selection whenever any of these
  // changes so we never act on rows that aren't visible.
  const [authorFilter, setAuthorFilter] = React.useState('all'); // 'all' | actorId
  const [sortKey, setSortKey] = React.useState('recent');         // 'recent' | 'title' | 'author' | 'oldest'
  const [search, setSearch] = React.useState('');

  // Authors actually present in this folder — drives the dropdown options.
  const authorIds = React.useMemo(() => {
    const ids = new Set();
    for (const f of allFiles) ids.add(f.author);
    return Array.from(ids);
  }, [allFiles]);

  // Apply filter → search → sort. `mins` is "minutes ago" so smaller = recent.
  const files = React.useMemo(() => {
    const q = search.trim().toLowerCase();
    let out = allFiles;
    if (authorFilter !== 'all') out = out.filter((f) => f.author === authorFilter);
    if (q) out = out.filter((f) => (f.title || '').toLowerCase().includes(q) || (f.id || '').toLowerCase().includes(q));
    out = [...out];
    if (sortKey === 'title') out.sort((a, b) => (a.title || '').localeCompare(b.title || ''));
    else if (sortKey === 'oldest') out.sort((a, b) => (b.mins || 0) - (a.mins || 0));
    else if (sortKey === 'author') out.sort((a, b) => {
      const an = (ACTORS[a.author]?.name || a.author || '').toLowerCase();
      const bn = (ACTORS[b.author]?.name || b.author || '').toLowerCase();
      return an.localeCompare(bn) || (a.mins || 0) - (b.mins || 0);
    });
    else out.sort((a, b) => (a.mins || 0) - (b.mins || 0)); // 'recent' default
    return out;
  }, [allFiles, authorFilter, sortKey, search]);

  // Lazy-load folder children if the sidebar didn't already.
  React.useEffect(() => {
    if (window.ensureFolderChildren) window.ensureFolderChildren(folder).catch(() => {});
  }, [folder]);

  // Load file uploads for this folder.
  const refreshUploads = React.useCallback(async () => {
    try { setUploads(await window.listUploadsInFolder(folder)); }
    catch (e) { console.error(e); }
  }, [folder]);
  React.useEffect(() => { refreshUploads(); }, [refreshUploads]);

  const uploadFiles = async (fileList) => {
    if (!fileList || fileList.length === 0) return;
    setUploadError(null);
    setUploadingCount(fileList.length);
    let ok = 0;
    for (const f of fileList) {
      try { await window.uploadFileToFolder(folder, f); ok++; }
      catch (e) {
        const msg = `${f.name}: ${e.message || 'error'}`;
        setUploadError(msg);
        console.error(e);
      }
    }
    setUploadingCount(0);
    await refreshUploads();
  };

  const onDrop = (e) => {
    e.preventDefault(); setDragOver(false);
    const filesArr = Array.from(e.dataTransfer.files || []);
    if (filesArr.length) uploadFiles(filesArr);
  };

  const onDeleteUpload = async (id) => {
    if (!confirm('¿Borrar este archivo?')) return;
    try { await window.deleteUpload(id); await refreshUploads(); }
    catch (e) { console.error(e); }
  };

  // Soft-delete a single Markdown doc from the row trash icon.
  const onDeleteDoc = async (docId, title) => {
    if (!confirm(`¿Borrar "${title || docId}"?\n\nVa a la papelera (soft-delete).`)) return;
    try { await window.deleteDoc(docId); }
    catch (e) { alert(e?.message || 'No se pudo borrar el documento.'); }
  };

  // Bulk soft-delete every selected doc id. Sequential to avoid hammering
  // the server; fast enough for the few-dozen-row case.
  const onBulkDelete = async () => {
    if (selected.size === 0) return;
    if (!confirm(`¿Borrar ${selected.size} documento${selected.size === 1 ? '' : 's'}?\n\nVa${selected.size === 1 ? '' : 'n'} a la papelera (soft-delete).`)) return;
    const ids = Array.from(selected);
    let failed = 0;
    for (const id of ids) {
      try { await window.deleteDoc(id); }
      catch { failed++; }
    }
    setSelected(new Set());
    if (failed > 0) alert(`${failed} de ${ids.length} no se pudieron borrar.`);
  };

  const isFolderEmpty = isEmpty || (files.length === 0 && uploads.length === 0);

  if (isFolderEmpty) {
    return (
      <div style={{ flex: 1, overflowY: 'auto', padding: '32px 40px', position: 'relative' }}
        onDragOver={(e) => { e.preventDefault(); if (!dragOver) setDragOver(true); }}
        onDragLeave={(e) => { if (e.currentTarget.contains(e.relatedTarget)) return; setDragOver(false); }}
        onDrop={onDrop}>
        {dragOver && <DropOverlay folder={folder} />}
        <div style={{ maxWidth: 1280, margin: '0 auto' }}>
          <div style={{ marginBottom: 24 }}>
            <h1 style={{ fontSize: 24, fontWeight: 600, margin: 0, letterSpacing: -0.4 }} className="mono">/{folder}</h1>
            <div style={{ fontSize: 12.5, color: 'var(--text-2)', marginTop: 4 }}>0 archivos · carpeta vacía</div>
          </div>
          {/* Upload status — shown ABOVE the empty-state CTA so a failed upload doesn't disappear silently while the folder is still rendered as empty. */}
          {uploadingCount > 0 && (
            <div style={{ padding: '8px 12px', background: 'var(--bg-2)', border: '1px solid var(--border)', borderRadius: 6, fontSize: 12.5, color: 'var(--text-1)', marginBottom: 12, display: 'flex', alignItems: 'center', gap: 8 }}>
              <Icon name="loader-2" size={13} /> Subiendo {uploadingCount} archivo{uploadingCount === 1 ? '' : 's'}…
            </div>
          )}
          {uploadError && (
            <div style={{ padding: '8px 12px', background: 'rgba(239,68,68,.08)', border: '1px solid rgba(239,68,68,.25)', borderRadius: 6, fontSize: 12, color: 'var(--danger)', marginBottom: 12, display: 'flex', alignItems: 'center', gap: 8 }}>
              <Icon name="alert-triangle" size={13} />
              <span style={{ flex: 1 }}>{uploadError}</span>
              <button className="btn-reset" onClick={() => setUploadError(null)} style={{ color: 'var(--text-2)' }}>
                <Icon name="x" size={12} />
              </button>
            </div>
          )}
          <EmptyState
            iconName="folder-open"
            title="Esta carpeta está vacía"
            body="Crea un documento Markdown, sube archivos (PDF, JSON, imágenes, lo que sea, hasta 100 MB c/u) o arrástralos a esta zona."
            cta="Subir archivos"
            ctaIcon="upload"
            onCta={() => fileInputRef.current?.click()}
            secondaryCta="Nuevo documento"
            onSecondaryCta={() => setShowNew(true)}
          />
          <input ref={fileInputRef} type="file" multiple style={{ display: 'none' }}
            onChange={(e) => { uploadFiles(Array.from(e.target.files || [])); e.target.value = ''; }} />
        </div>
        {showNew && <NewDocModal presetFolder={folder} onClose={() => setShowNew(false)} onCreated={(p) => { setShowNew(false); onOpenDoc && onOpenDoc(p); }} />}
      </div>
    );
  }

  const toggle = id => setSelected(s => { const n = new Set(s); n.has(id) ? n.delete(id) : n.add(id); return n; });
  const toggleAll = () => setSelected(s => s.size === files.length ? new Set() : new Set(files.map(f => f.id)));

  return (
    <div style={{ flex: 1, overflowY: 'auto', padding: '32px 40px 100px', position: 'relative' }}
      onDragOver={(e) => { e.preventDefault(); if (!dragOver) setDragOver(true); }}
      onDragLeave={(e) => { if (e.currentTarget.contains(e.relatedTarget)) return; setDragOver(false); }}
      onDrop={onDrop}>
      {dragOver && <DropOverlay folder={folder} />}
      <input ref={fileInputRef} type="file" multiple style={{ display: 'none' }}
        onChange={(e) => { uploadFiles(Array.from(e.target.files || [])); e.target.value = ''; }} />
      <div style={{ maxWidth: 1280, margin: '0 auto' }}>
        <SectionHead
          level="h1"
          title={<span className="mono">/{folder}</span>}
          sub={`${files.length} ${files.length === 1 ? 'documento' : 'documentos'} · ${uploads.length} ${uploads.length === 1 ? 'archivo' : 'archivos'}`}
          right={<div style={{ display: 'flex', gap: 8 }}>
            <Button variant="secondary" size="md" icon="upload" onClick={() => fileInputRef.current?.click()}>Subir archivo</Button>
            <Button variant="primary" size="md" icon="plus" onClick={() => setShowNew(true)}>Nuevo doc</Button>
          </div>}
        />
        {uploadError && (
          <div style={{ padding: '8px 12px', background: 'rgba(239,68,68,.08)', border: '1px solid rgba(239,68,68,.25)', borderRadius: 6, fontSize: 12, color: 'var(--danger)', marginBottom: 12 }}>
            {uploadError}
          </div>
        )}
        {uploadingCount > 0 && (
          <div style={{ padding: '8px 12px', background: 'var(--bg-1)', border: '1px solid var(--border)', borderRadius: 6, fontSize: 12, color: 'var(--text-1)', marginBottom: 12, display: 'flex', alignItems: 'center', gap: 8 }}>
            <Icon name="loader-2" size={12} className="spin" /> Subiendo {uploadingCount} {uploadingCount === 1 ? 'archivo' : 'archivos'}…
          </div>
        )}

        <div style={{ display: 'flex', gap: 10, marginBottom: 16, alignItems: 'center' }}>
          <Select
            label="Filtrar por autor"
            value={authorFilter}
            options={[
              { value: 'all', label: 'Todos' },
              ...authorIds.map((id) => ({ value: id, label: ACTORS[id]?.name || id })),
            ]}
            onChange={setAuthorFilter}
          />
          <FolderSearchInput
            value={search}
            onChange={setSearch}
            placeholder={`Buscar en /${folder}…`}
          />
          <div style={{ flex: 1 }} />
          <Select
            label="Ordenar"
            value={sortKey}
            options={[
              { value: 'recent', label: 'Último editado' },
              { value: 'oldest', label: 'Más antiguo' },
              { value: 'title',  label: 'Título (A→Z)' },
              { value: 'author', label: 'Autor' },
            ]}
            onChange={setSortKey}
          />
        </div>

        <div style={{
          background: 'var(--bg-1)', border: '1px solid var(--border)',
          borderRadius: 'var(--r-lg)', overflow: 'hidden',
        }}>
          {/* header */}
          <div style={{
            display: 'grid', gridTemplateColumns: '36px 1fr 100px 220px 80px',
            padding: '10px 14px',
            background: 'var(--bg-2)',
            borderBottom: '1px solid var(--border)',
            fontSize: 11, color: 'var(--text-2)', textTransform: 'uppercase', letterSpacing: 0.6, fontWeight: 600,
          }}>
            <Checkbox checked={selected.size === files.length} mixed={selected.size > 0 && selected.size < files.length} onChange={toggleAll} />
            <span>Título</span>
            <span>Tamaño</span>
            <span>Última edición</span>
            <span></span>
          </div>
          {files.map(f => {
            const isSel = selected.has(f.id);
            return (
              <div key={f.id} onClick={() => onOpenDoc(`${folder}/${f.id}`)}
                style={{
                  display: 'grid', gridTemplateColumns: '36px 1fr 100px 220px 80px',
                  padding: '12px 14px', alignItems: 'center',
                  borderBottom: '1px solid var(--border)',
                  cursor: 'pointer',
                  background: isSel ? 'var(--accent-dim)' : 'transparent',
                  transition: 'background var(--t-fast)',
                }}
                onMouseEnter={e => { if (!isSel) e.currentTarget.style.background = 'var(--bg-2)'; }}
                onMouseLeave={e => { if (!isSel) e.currentTarget.style.background = 'transparent'; }}>
                <span onClick={e => { e.stopPropagation(); toggle(f.id); }}>
                  <Checkbox checked={isSel} onChange={() => toggle(f.id)} />
                </span>
                <div style={{ display: 'flex', alignItems: 'center', gap: 10, minWidth: 0 }}>
                  <Icon name="file-text" size={14} color="var(--text-2)" />
                  <div style={{ minWidth: 0 }}>
                    <div style={{ fontSize: 13, fontWeight: 500, color: 'var(--text-0)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{f.title}</div>
                    <div className="mono" style={{ fontSize: 11, color: 'var(--text-3)', marginTop: 1 }}>{f.id}</div>
                  </div>
                </div>
                <span className="mono" style={{ fontSize: 12, color: 'var(--text-2)' }}>{f.size}</span>
                <div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12, color: 'var(--text-2)' }}>
                  <Avatar actor={f.author} size={18} />
                  {ACTORS[f.author].name} · {relTime(f.mins)}
                </div>
                <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 2 }}>
                  <button className="btn-reset" title="Borrar documento"
                    onClick={(e) => { e.stopPropagation(); onDeleteDoc(f.id, f.title); }}
                    style={{ width: 26, height: 26, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 5, color: 'var(--text-3)' }}
                    onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(239,68,68,.10)'; e.currentTarget.style.color = 'var(--danger)'; }}
                    onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = 'var(--text-3)'; }}>
                    <Icon name="trash-2" size={13} />
                  </button>
                </div>
              </div>
            );
          })}
        </div>
      </div>

      {uploads.length > 0 && (
        <div style={{ marginTop: 24 }}>
          <div style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-2)', textTransform: 'uppercase', letterSpacing: 0.7, marginBottom: 10 }}>
            Archivos subidos · {uploads.length}
          </div>
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: 10 }}>
            {uploads.map(u => <UploadCard key={u.id} upload={u} onDelete={() => onDeleteUpload(u.id)} />)}
          </div>
        </div>
      )}

      {selected.size > 0 && (
        <div style={{
          position: 'absolute', bottom: 24, left: '50%', transform: 'translateX(-50%)',
          background: 'var(--bg-2)', border: '1px solid var(--border-strong)',
          borderRadius: 'var(--r-lg)', padding: '8px 10px',
          display: 'flex', alignItems: 'center', gap: 10,
          boxShadow: '0 12px 36px rgba(0,0,0,.5)',
          animation: 'slide-in-right 200ms',
        }}>
          <span style={{ padding: '0 8px', fontSize: 12.5, color: 'var(--text-1)' }}>
            <span className="mono" style={{ color: 'var(--accent)' }}>{selected.size}</span> seleccionados
          </span>
          <div style={{ width: 1, height: 18, background: 'var(--border)' }} />
          <Button variant="danger" size="sm" icon="trash-2" onClick={onBulkDelete}>Borrar</Button>
          <button className="btn-reset" onClick={() => setSelected(new Set())}
            style={{ width: 26, height: 26, display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--text-2)', borderRadius: 6, marginLeft: 4 }}>
            <Icon name="x" size={14} />
          </button>
        </div>
      )}

      {showNew && <NewDocModal presetFolder={folder} onClose={() => setShowNew(false)} onCreated={(p) => { setShowNew(false); onOpenDoc && onOpenDoc(p); }} />}
    </div>
  );
}

function DropOverlay({ folder }) {
  return (
    <div style={{
      position: 'absolute', inset: 12,
      background: 'color-mix(in srgb, var(--accent) 12%, transparent)',
      border: '2px dashed var(--accent)',
      borderRadius: 'var(--r-lg)',
      zIndex: 50,
      pointerEvents: 'none',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      flexDirection: 'column', gap: 10,
      animation: 'fade-in 100ms',
    }}>
      <Icon name="upload-cloud" size={36} color="var(--accent)" />
      <div style={{ fontSize: 15, fontWeight: 600, color: 'var(--accent)' }}>Soltar archivos en /{folder}</div>
      <div style={{ fontSize: 12, color: 'var(--text-2)' }}>PDF, JSON, imágenes, video, lo que sea — hasta 100 MB por archivo</div>
    </div>
  );
}

// Mime types we can preview inline (read-only) without downloading the file.
// Markdown gets rendered, JSON pretty-printed, everything else shown raw.
function _previewKind(upload) {
  const m = (upload.mimeType || '').toLowerCase();
  const name = (upload.filename || '').toLowerCase();
  if (m === 'text/markdown' || name.endsWith('.md') || name.endsWith('.markdown')) return 'markdown';
  if (m === 'application/json' || name.endsWith('.json')) return 'json';
  if (m === 'application/yaml' || name.endsWith('.yaml') || name.endsWith('.yml')) return 'yaml';
  if (m.startsWith('text/') || /\.(txt|csv|tsv|log|sql|xml|toml|env|ini|sh)$/.test(name)) return 'text';
  return null;
}

function UploadCard({ upload, onDelete }) {
  const [hover, setHover] = React.useState(false);
  const [expanded, setExpanded] = React.useState(false);
  const [showPreview, setShowPreview] = React.useState(false);
  const fmt = formatBytes(upload.sizeBytes);
  const a = ACTORS[upload.uploadedById] || { name: upload.uploadedById, color: 'var(--text-2)' };
  const ago = relTime(Math.max(0, Math.round((Date.now() - new Date(upload.createdAt).getTime()) / 60000)));
  const icon = mimeIcon(upload.mimeType, upload.filename);
  // Image previews: render the actual file inline. The /download endpoint
  // serves images with `Content-Disposition: inline` so the browser embeds
  // them directly. Client-side scaled (T2 decision A); server thumbnails
  // are a follow-up if bandwidth becomes a problem.
  const isImage = upload.mimeType?.startsWith('image/');
  const previewKind = _previewKind(upload);
  const downloadUrl = window.uploadDownloadUrl(upload.id);
  return (
    <div onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
      style={{
        position: 'relative',
        background: 'var(--bg-1)', border: '1px solid var(--border)', borderRadius: 'var(--r-md)',
        padding: '12px 14px',
        display: 'flex', flexDirection: 'column', gap: 10,
        transition: 'border-color var(--t-fast)',
        borderColor: hover ? 'var(--border-strong)' : 'var(--border)',
      }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
        <div style={{ width: 36, height: 36, flexShrink: 0, borderRadius: 6, background: 'var(--bg-2)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--text-1)', overflow: 'hidden' }}>
          {isImage
            ? <img src={downloadUrl} alt={upload.filename}
                style={{ width: '100%', height: '100%', objectFit: 'cover' }}
                onClick={() => setExpanded((v) => !v)} />
            : <Icon name={icon} size={16} />}
        </div>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontSize: 13, fontWeight: 500, color: 'var(--text-0)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{upload.filename}</div>
          <div style={{ fontSize: 11.5, color: 'var(--text-3)', marginTop: 2, display: 'flex', alignItems: 'center', gap: 6 }}>
            <span className="mono">{fmt}</span>
            <span>·</span>
            <Avatar actor={upload.uploadedById} size={14} />
            <span>{a.name}</span>
            <span>·</span>
            <span>{ago}</span>
            {upload.indexed && (
              <>
                <span>·</span>
                <span title={`Texto indexado: ${upload.indexedTextLength} chars`} style={{ display: 'inline-flex', alignItems: 'center', gap: 3, color: 'var(--success)' }}>
                  <Icon name="search" size={10} /> indexado
                </span>
              </>
            )}
            {!upload.indexed && upload.extractionError && (
              <>
                <span>·</span>
                <span title={`Sin indexar: ${upload.extractionError}`} style={{ display: 'inline-flex', alignItems: 'center', gap: 3, color: 'var(--text-3)' }}>
                  <Icon name="alert-circle" size={10} /> sin indexar
                </span>
              </>
            )}
          </div>
        </div>
        {isImage && (
          <button className="btn-reset" title={expanded ? 'Achicar' : 'Ampliar'}
            onClick={() => setExpanded((v) => !v)}
            style={{ width: 26, height: 26, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 5, color: 'var(--text-2)' }}
            onMouseEnter={(e) => { e.currentTarget.style.background = 'var(--bg-2)'; e.currentTarget.style.color = 'var(--text-0)'; }}
            onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = 'var(--text-2)'; }}>
            <Icon name={expanded ? 'minimize-2' : 'maximize-2'} size={13} />
          </button>
        )}
        {previewKind && (
          <button className="btn-reset" title="Ver contenido"
            onClick={() => setShowPreview(true)}
            style={{ width: 26, height: 26, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 5, color: 'var(--text-2)' }}
            onMouseEnter={(e) => { e.currentTarget.style.background = 'var(--bg-2)'; e.currentTarget.style.color = 'var(--text-0)'; }}
            onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = 'var(--text-2)'; }}>
            <Icon name="eye" size={13} />
          </button>
        )}
        <a href={downloadUrl} download={upload.filename} title="Descargar"
          style={{ width: 26, height: 26, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 5, color: 'var(--text-2)', textDecoration: 'none' }}
          onMouseEnter={(e) => { e.currentTarget.style.background = 'var(--bg-2)'; e.currentTarget.style.color = 'var(--text-0)'; }}
          onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = 'var(--text-2)'; }}>
          <Icon name="download" size={13} />
        </a>
        <button className="btn-reset" title="Borrar archivo" onClick={onDelete}
          style={{ width: 26, height: 26, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 5, color: 'var(--text-3)' }}
          onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(239,68,68,.10)'; e.currentTarget.style.color = 'var(--danger)'; }}
          onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = 'var(--text-3)'; }}>
          <Icon name="trash-2" size={13} />
        </button>
      </div>
      {isImage && expanded && (
        <a href={downloadUrl} target="_blank" rel="noreferrer"
          style={{ display: 'block', borderRadius: 6, overflow: 'hidden', background: 'var(--bg-0)' }}>
          <img src={downloadUrl} alt={upload.filename}
            style={{ display: 'block', width: '100%', maxHeight: 480, objectFit: 'contain' }} />
        </a>
      )}
      {showPreview && (
        <UploadPreviewModal upload={upload} kind={previewKind} onClose={() => setShowPreview(false)} />
      )}
    </div>
  );
}

// Read-only modal for textual uploads (.md / .json / .yaml / .txt / .csv / …).
// Fetches the raw file from /api/uploads/:id/download and renders it
// according to `kind`. NOT editable — uploads are static blobs by design;
// for editable docs use "Nuevo doc" which creates a Document instead.
function UploadPreviewModal({ upload, kind, onClose }) {
  const [text, setText] = React.useState(null);
  const [error, setError] = React.useState(null);
  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const res = await fetch(window.uploadDownloadUrl(upload.id), { credentials: 'same-origin' });
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        const t = await res.text();
        if (!cancelled) setText(t);
      } catch (e) {
        if (!cancelled) setError(e.message || 'No se pudo cargar');
      }
    })();
    return () => { cancelled = true; };
  }, [upload.id]);

  // Prettify JSON when possible. Falls back to raw on parse errors.
  const body = React.useMemo(() => {
    if (text == null) return null;
    if (kind === 'json') {
      try { return JSON.stringify(JSON.parse(text), null, 2); }
      catch { return text; }
    }
    return text;
  }, [text, kind]);

  const Md = window.RenderedMd;
  const sizeNote = `${formatBytes(upload.sizeBytes)} · ${upload.mimeType}`;

  return (
    <Modal title={upload.filename} onClose={onClose} width={780}
      footer={<>
        <Button variant="ghost" onClick={onClose}>Cerrar</Button>
        <a href={window.uploadDownloadUrl(upload.id)} download={upload.filename}
          style={{ textDecoration: 'none' }}>
          <Button variant="secondary" icon="download">Descargar</Button>
        </a>
      </>}>
      <div style={{ fontSize: 11, color: 'var(--text-3)', marginBottom: 10, fontFamily: 'var(--font-mono)' }}>
        {sizeNote} · solo lectura ·{' '}
        <span style={{ color: 'var(--text-2)' }}>los uploads son archivos estáticos. Para editar, "Nuevo doc".</span>
      </div>
      {error && (
        <div style={{ padding: '10px 12px', background: 'rgba(239,68,68,.08)', border: '1px solid rgba(239,68,68,.25)', borderRadius: 6, color: 'var(--danger)', fontSize: 12.5 }}>
          {error}
        </div>
      )}
      {!error && body == null && (
        <div style={{ color: 'var(--text-3)', fontSize: 12.5, fontStyle: 'italic' }}>Cargando…</div>
      )}
      {!error && body != null && (
        kind === 'markdown' && Md
          ? <div style={{ fontSize: 13.5, lineHeight: 1.6, color: 'var(--text-1)' }}><Md md={body} /></div>
          : (
            <pre style={{
              margin: 0, padding: '12px 14px',
              background: 'var(--bg-0)', border: '1px solid var(--border)', borderRadius: 6,
              fontSize: 12, fontFamily: 'var(--font-mono)', lineHeight: 1.55,
              color: 'var(--text-0)', whiteSpace: 'pre-wrap', wordBreak: 'break-word',
              maxHeight: '60vh', overflowY: 'auto',
            }}>{body}</pre>
          )
      )}
    </Modal>
  );
}

function formatBytes(b) {
  if (!b) return '0 B';
  if (b < 1024) return `${b} B`;
  if (b < 1024 * 1024) return `${(b / 1024).toFixed(1)} KB`;
  if (b < 1024 * 1024 * 1024) return `${(b / 1024 / 1024).toFixed(1)} MB`;
  return `${(b / 1024 / 1024 / 1024).toFixed(2)} GB`;
}

function mimeIcon(mime, filename) {
  if (mime?.startsWith('image/')) return 'image';
  if (mime === 'application/pdf') return 'file-text';
  if (mime?.startsWith('video/')) return 'video';
  if (mime?.startsWith('audio/')) return 'music';
  if (/zip|tar|gz|7z|rar/i.test(mime || filename || '')) return 'archive';
  if (/json|xml|yaml/i.test(mime || filename || '')) return 'braces';
  if (mime?.startsWith('text/')) return 'file-text';
  return 'file';
}

function Checkbox({ checked, mixed, onChange }) {
  return (
    <button className="btn-reset" onClick={(e) => { e.stopPropagation(); onChange && onChange(!checked); }}
      style={{
        width: 16, height: 16, borderRadius: 3,
        border: `1px solid ${checked || mixed ? 'var(--accent)' : 'var(--border-strong)'}`,
        background: checked || mixed ? 'var(--accent)' : 'transparent',
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        color: '#0A0E1A',
        transition: 'background var(--t-fast), border-color var(--t-fast)',
      }}>
      {checked && <Icon name="check" size={12} strokeWidth={3} />}
      {mixed && <div style={{ width: 8, height: 2, background: '#0A0E1A' }} />}
    </button>
  );
}

// Real select with a dropdown menu. Backwards-compatible: if no `options`
// is passed it renders read-only with the legacy `value` text (for the few
// places that still use it as a label-only chip).
function Select({ label, value, options, onChange }) {
  const [open, setOpen] = React.useState(false);
  const isReal = Array.isArray(options) && typeof onChange === 'function';
  const display = isReal
    ? (options.find((o) => o.value === value)?.label ?? String(value))
    : value;
  const button = (
    <button className="btn-reset"
      onClick={() => isReal && setOpen((o) => !o)}
      style={{
        display: 'flex', alignItems: 'center', gap: 6,
        height: 30, padding: '0 10px',
        background: open ? 'var(--bg-2)' : 'var(--bg-1)',
        border: '1px solid var(--border)',
        borderRadius: 'var(--r-md)', color: 'var(--text-1)', fontSize: 12,
        cursor: isReal ? 'pointer' : 'default',
      }}>
      <span style={{ color: 'var(--text-2)' }}>{label}:</span>
      <span style={{ color: 'var(--text-0)', fontWeight: 500 }}>{display}</span>
      <Icon name="chevron-down" size={12} color="var(--text-2)" />
    </button>
  );
  if (!isReal) return button;
  return (
    <div style={{ position: 'relative' }}>
      {button}
      {open && (
        <>
          <div onClick={() => setOpen(false)} style={{ position: 'fixed', inset: 0, zIndex: 40 }} />
          <div style={{
            position: 'absolute', top: '100%', left: 0, marginTop: 4, minWidth: 200, padding: 4,
            background: 'var(--bg-1)', border: '1px solid var(--border)',
            borderRadius: 'var(--r-md)', boxShadow: '0 12px 32px rgba(0,0,0,.5)', zIndex: 50,
          }}>
            {options.map((o) => {
              const active = o.value === value;
              return (
                <button key={String(o.value)} className="btn-reset"
                  onClick={() => { onChange(o.value); setOpen(false); }}
                  style={{
                    display: 'flex', alignItems: 'center', gap: 8, width: '100%',
                    padding: '7px 10px', borderRadius: 5, textAlign: 'left',
                    background: active ? 'var(--bg-2)' : 'transparent',
                    fontSize: 12.5, color: 'var(--text-0)',
                  }}
                  onMouseEnter={(e) => { if (!active) e.currentTarget.style.background = 'var(--bg-2)'; }}
                  onMouseLeave={(e) => { if (!active) e.currentTarget.style.background = 'transparent'; }}>
                  <span style={{ flex: 1 }}>{o.label}</span>
                  {active && <Icon name="check" size={12} color="var(--accent)" />}
                </button>
              );
            })}
          </div>
        </>
      )}
    </div>
  );
}

// Small controlled search input for the folder view. Local-only — filters
// the already-loaded `files` array by title/slug substring. The global
// full-text search lives in the ⌘K palette.
function FolderSearchInput({ value, onChange, placeholder }) {
  return (
    <div style={{
      display: 'flex', alignItems: 'center', gap: 6,
      height: 30, padding: '0 10px', width: 260,
      background: 'var(--bg-1)', border: '1px solid var(--border)',
      borderRadius: 'var(--r-md)',
    }}>
      <Icon name="search" size={12} color="var(--text-3)" />
      <input
        type="text"
        value={value}
        onChange={(e) => onChange(e.target.value)}
        placeholder={placeholder}
        style={{
          flex: 1, minWidth: 0,
          background: 'transparent', border: 'none', outline: 'none',
          color: 'var(--text-0)', fontSize: 12,
        }}
      />
      {value && (
        <button className="btn-reset" onClick={() => onChange('')}
          style={{ width: 16, height: 16, color: 'var(--text-3)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
          <Icon name="x" size={11} />
        </button>
      )}
    </div>
  );
}

function EmptyState({ iconName, title, body, cta, ctaIcon, onCta, secondaryCta, onSecondaryCta }) {
  return (
    <div style={{
      padding: '80px 24px', textAlign: 'center',
      border: '1px dashed var(--border)', borderRadius: 'var(--r-lg)',
      background: 'rgba(22,25,34,.4)',
    }}>
      <div style={{
        width: 56, height: 56, margin: '0 auto 18px',
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        borderRadius: 14, background: 'var(--bg-1)',
        border: '1px solid var(--border)',
      }}>
        <Icon name={iconName} size={24} color="var(--text-2)" strokeWidth={1.4} />
      </div>
      <div style={{ fontSize: 15, fontWeight: 600, color: 'var(--text-0)', marginBottom: 6 }}>{title}</div>
      <div style={{ fontSize: 13, color: 'var(--text-2)', marginBottom: 22, maxWidth: 440, marginLeft: 'auto', marginRight: 'auto' }}>{body}</div>
      <div style={{ display: 'inline-flex', gap: 8 }}>
        {cta && <Button variant="primary" icon={ctaIcon} onClick={onCta}>{cta}</Button>}
        {secondaryCta && <Button variant="secondary" onClick={onSecondaryCta}>{secondaryCta}</Button>}
      </div>
    </div>
  );
}

Object.assign(window, { VaultHome, VaultFolder, DocCard, Checkbox, Select, EmptyState, UploadCard, UploadPreviewModal, DropOverlay });
