// Coachinside AI Hub — Claude Desktop-like SPA.
const { useState, useEffect, useRef, useCallback, useMemo } = React;

// ─── API helper ───
async function api(url, opts = {}) {
  const r = await fetch(url, {
    ...opts,
    credentials: 'same-origin',
    headers: opts.body instanceof FormData
      ? (opts.headers || {})
      : { 'Content-Type': 'application/json', ...(opts.headers || {}) }
  });
  const j = await r.json().catch(() => ({}));
  if (!r.ok) throw Object.assign(new Error(j.error || 'request_failed'), { status: r.status, code: j.error, meta: j.meta });
  return j;
}

function useMe() {
  const [me, setMe] = useState({ loading: true, user: null });
  const reload = useCallback(async () => {
    try { const j = await api('/api/me'); setMe({ loading: false, user: j.user }); }
    catch { setMe({ loading: false, user: null }); }
  }, []);
  useEffect(() => { reload(); }, [reload]);
  return { me, reload };
}

function useConfig() {
  const [cfg, setCfg] = useState({ providers: [], allowed_email_domains: [] });
  useEffect(() => { api('/api/config').then(setCfg).catch(() => {}); }, []);
  return cfg;
}

function extractVars(t) { const m = String(t || '').match(/\{\{\s*([a-zA-Z0-9_ .-]+)\s*\}\}/g) || []; return Array.from(new Set(m.map(x => x.replace(/[{}]/g, '').trim()))); }
function fillVars(t, values) { return String(t || '').replace(/\{\{\s*([a-zA-Z0-9_ .-]+)\s*\}\}/g, (_, k) => values[k.trim()] ?? ''); }

// ─── Auth screen ───
function AuthScreen({ config, onAuth }) {
  const [mode, setMode] = useState('register');
  const [form, setForm] = useState({ email: '', password: '', display_name: '' });
  const [err, setErr] = useState(null);
  const [busy, setBusy] = useState(false);
  const domainHint = config.allowed_email_domains?.length ? `@${config.allowed_email_domains.join(', @')}` : '';

  async function submit(e) {
    e.preventDefault(); setErr(null); setBusy(true);
    try {
      const path = mode === 'register' ? '/api/auth/register' : '/api/auth/login';
      const body = mode === 'register' ? form : { email: form.email, password: form.password };
      await api(path, { method: 'POST', body: JSON.stringify(body) });
      onAuth();
    } catch (e) {
      setErr(errorLabel(e.code, e.meta));
    } finally { setBusy(false); }
  }

  return (
    <div className="auth-screen">
      <div className="auth-card">
        <div className="auth-logo">C</div>
        <h1>Coachinside AI Hub</h1>
        <p className="auth-sub">{mode === 'register' ? 'Create your account' : 'Welcome back'}</p>
        <form className="auth-form" onSubmit={submit}>
          <label>
            <span>Email</span>
            <input type="email" required autoComplete="email" value={form.email} onChange={e => setForm(f => ({ ...f, email: e.target.value }))} />
            {mode === 'register' && domainHint && <div className="auth-hint">Only {domainHint} emails accepted.</div>}
          </label>
          {mode === 'register' && (
            <label>
              <span>Name</span>
              <input type="text" value={form.display_name} onChange={e => setForm(f => ({ ...f, display_name: e.target.value }))} />
            </label>
          )}
          <label>
            <span>Password</span>
            <input type="password" required minLength="8"
              autoComplete={mode === 'register' ? 'new-password' : 'current-password'}
              value={form.password} onChange={e => setForm(f => ({ ...f, password: e.target.value }))} />
          </label>
          {err && <div className="auth-error">{err}</div>}
          <div className="auth-actions">
            <button type="button" className="auth-toggle" onClick={() => setMode(mode === 'register' ? 'login' : 'register')}>
              {mode === 'register' ? 'Have an account? Sign in' : 'New here? Create account'}
            </button>
            <button type="submit" className="mb-btn primary" disabled={busy}>
              {busy ? '…' : (mode === 'register' ? 'Create account' : 'Sign in')}
            </button>
          </div>
        </form>
      </div>
    </div>
  );
}

// ─── Sidebar ───
function ThreadRow({ thread: t, active, projects, onSelect, onRename, onDelete, onMove }) {
  const [menuOpen, setMenuOpen] = useState(false);
  const [editing, setEditing] = useState(false);
  const [draft, setDraft] = useState(t.title);
  const menuRef = useRef(null);
  const inputRef = useRef(null);

  useEffect(() => {
    if (!menuOpen) return;
    const off = (e) => { if (menuRef.current && !menuRef.current.contains(e.target)) setMenuOpen(false); };
    window.addEventListener('mousedown', off);
    return () => window.removeEventListener('mousedown', off);
  }, [menuOpen]);

  useEffect(() => {
    if (editing && inputRef.current) { inputRef.current.focus(); inputRef.current.select(); }
  }, [editing]);

  useEffect(() => { setDraft(t.title); }, [t.title]);

  function startEdit() { setDraft(t.title); setEditing(true); }
  function saveEdit() {
    const v = draft.trim().slice(0, 120);
    setEditing(false);
    if (v && v !== t.title) onRename(t, v);
  }
  function cancelEdit() { setEditing(false); setDraft(t.title); }

  const inProjects = projects.filter(p => p.id !== t.project_id);

  return (
    <div className={'sb-item' + (active ? ' active' : '')}
         onClick={() => !editing && onSelect(t)}
         onDoubleClick={(e) => { e.stopPropagation(); startEdit(); }}
         title="Double-click to rename · ⋯ for more">
      <div className="sb-item-row">
        {editing ? (
          <input
            ref={inputRef}
            className="sb-item-name-input"
            value={draft}
            onClick={(e) => e.stopPropagation()}
            onChange={(e) => setDraft(e.target.value)}
            onBlur={saveEdit}
            onKeyDown={(e) => {
              e.stopPropagation();
              if (e.key === 'Enter') { e.preventDefault(); saveEdit(); }
              else if (e.key === 'Escape') { e.preventDefault(); cancelEdit(); }
            }}
          />
        ) : (
          <div className="sb-item-name">{t.title}</div>
        )}
        <div className="sb-row-actions" ref={menuRef}>
          <button className="sb-row-kebab" onClick={(e) => { e.stopPropagation(); setMenuOpen(o => !o); }}>⋯</button>
          {menuOpen && (
            <div className="sb-menu" onClick={(e) => e.stopPropagation()}>
              <button onClick={() => { setMenuOpen(false); startEdit(); }}>✎ Rename</button>
              {t.project_id && <button onClick={() => { setMenuOpen(false); onMove(t, null); }}>↩ Remove from project</button>}
              {inProjects.length > 0 && (
                <div className="sb-menu-sub">
                  <div className="sb-menu-sub-label">Move to project</div>
                  {inProjects.map(p => (
                    <button key={p.id} onClick={() => { setMenuOpen(false); onMove(t, p.id); }}>
                      {p.icon || (p.name || '?')[0]?.toUpperCase()} {p.name}
                    </button>
                  ))}
                </div>
              )}
              <button className="danger" onClick={() => { setMenuOpen(false); onDelete(t); }}>🗑 Delete</button>
            </div>
          )}
        </div>
      </div>
      <div className="sb-item-meta">{t.model} · {t.message_count || 0} msgs</div>
    </div>
  );
}

function Sidebar({ user, projects, threads, skills, activeThreadSlug, activeProjectSlug, onSelectThread, onSelectProject, onRenameThread, onDeleteThread, onMoveThread, onOpenGlobal, onNewChat, onNewProject, onSkills, onUseSkill, onSettings, onLogout, onReload }) {
  const [q, setQ] = useState('');
  const [searchResults, setSearchResults] = useState(null);
  useEffect(() => {
    if (!q || q.length < 2) { setSearchResults(null); return; }
    const t = setTimeout(async () => {
      try { const j = await api('/api/search?q=' + encodeURIComponent(q)); setSearchResults(j); } catch {}
    }, 250);
    return () => clearTimeout(t);
  }, [q]);
  const filteredThreads = threads.filter(t => !q || t.title.toLowerCase().includes(q.toLowerCase()));
  const filteredProjects = projects.filter(p => !q || p.name.toLowerCase().includes(q.toLowerCase()));
  const filteredSkills = (skills || []).filter(s => !q || s.name.toLowerCase().includes(q.toLowerCase()));
  const inProjectContext = Boolean(activeProjectSlug);
  const contextLabel = inProjectContext
    ? `Chats in ${projects.find(p => p.slug === activeProjectSlug)?.name || 'project'}`
    : 'Recents';

  return (
    <aside className="sb">
      <div className="sb-head" onClick={onOpenGlobal} style={{ cursor: 'pointer' }}>
        <div className="sb-logo">C</div>
        <div className="sb-brand">
          <div className="sb-title">Coachinside AI Hub</div>
          <div className="sb-sub">Team workspace</div>
        </div>
      </div>
      <div className="sb-toolbar">
        <button className="new-chat-btn" onClick={onNewChat}>＋ New chat</button>
        <button className="icon-btn" onClick={onReload} title="Refresh">↻</button>
      </div>
      <div className="sb-search-wrap">
        <input className="sb-search" placeholder="Search…" value={q} onChange={e => setQ(e.target.value)} />
      </div>

      <div className="sb-list">
        {/* SEARCH RESULTS across messages */}
        {searchResults && (searchResults.messages?.length || 0) > 0 && (
          <>
            <div className="sb-section">Message hits · {searchResults.messages.length}</div>
            {searchResults.messages.slice(0, 8).map(m => (
              <div className="sb-item" key={'sr-' + m.id}
                   onClick={() => { const t = threads.find(tt => tt.slug === m.thread_slug); if (t) onSelectThread(t); }}>
                <div className="sb-item-name">{m.thread_title}</div>
                <div className="sb-item-meta" style={{ whiteSpace: 'normal', color: 'var(--ink-mute)', fontFamily: 'inherit', fontSize: 11 }}>{m.snippet.slice(0, 140)}…</div>
              </div>
            ))}
          </>
        )}

        {/* SKILLS */}
        <div className="sb-section sb-section-row">
          <span>Skills · {filteredSkills.length}</span>
          <button className="sb-mini-btn" onClick={onSkills} title="Manage skills">＋</button>
        </div>
        {filteredSkills.length === 0 ? (
          <div className="sb-empty">No skills yet. Save reusable prompts (cold email, meeting brief, SEO brief…) once, click to run.</div>
        ) : filteredSkills.slice(0, 6).map(s => (
          <div key={s.id} className="sb-item sb-item-skill" onClick={() => onUseSkill(s)}>
            <div className="sb-item-row">
              <span className="sb-item-icon" style={s.color ? { background: s.color, color: '#fff' } : null}>
                {s.icon || (s.name || '?')[0]?.toUpperCase()}
              </span>
              <div className="sb-item-name">{s.name}</div>
              {s.scope === 'team' && <span className="sb-badge">team</span>}
            </div>
          </div>
        ))}
        {filteredSkills.length > 6 && (
          <div className="sb-empty" style={{ padding: '4px 12px' }}>
            <button className="sb-mini-btn" style={{ width: 'auto', padding: '2px 8px' }} onClick={onSkills}>View all {filteredSkills.length}</button>
          </div>
        )}

        {/* PROJECTS */}
        <div className="sb-section sb-section-row" style={{ marginTop: 14 }}>
          <span>Projects · {filteredProjects.length}</span>
          <button className="sb-mini-btn" onClick={onNewProject} title="New project">＋</button>
        </div>
        {filteredProjects.length === 0 ? (
          <div className="sb-empty">No projects yet. Create one to bundle chats with shared instructions + files.</div>
        ) : filteredProjects.map(p => (
          <div key={p.slug}
               className={'sb-item sb-item-project' + (activeProjectSlug === p.slug ? ' active' : '')}
               onClick={() => onSelectProject(p)}>
            <div className="sb-item-row">
              <span className="sb-item-icon" style={p.color ? { background: p.color, color: '#fff' } : null}>
                {p.icon || (p.name || '?')[0]?.toUpperCase()}
              </span>
              <div className="sb-item-name">{p.name}</div>
              {p.is_shared && <span className="sb-badge">team</span>}
            </div>
            <div className="sb-item-meta">
              {p.threads_count || 0} chats · {p.files_count || 0} files · {p.my_role}
            </div>
          </div>
        ))}

        {/* CHATS */}
        <div className="sb-section" style={{ marginTop: 14 }}>
          {contextLabel} · {filteredThreads.length}
          {inProjectContext && (
            <button className="sb-mini-btn" onClick={onOpenGlobal} title="Back to all recents" style={{ marginLeft: 6 }}>×</button>
          )}
        </div>
        {filteredThreads.length === 0 ? (
          <div className="sb-empty">No chats {inProjectContext ? 'in this project' : 'yet'}.</div>
        ) : filteredThreads.map(t => (
          <ThreadRow key={t.slug} thread={t} active={activeThreadSlug === t.slug}
            projects={projects} onSelect={onSelectThread} onRename={onRenameThread}
            onDelete={onDeleteThread} onMove={onMoveThread} />
        ))}
      </div>

      <div className="sb-user" onClick={onSettings}>
        <div className="sb-user-avatar">{initials(user)}</div>
        <div className="sb-user-info">
          <div className="sb-user-email">{user.display_name || user.email}</div>
          <div className="sb-user-role">{user.role} · settings</div>
        </div>
        <button className="icon-btn" title="Sign out" onClick={(e) => { e.stopPropagation(); onLogout(); }}>⏻</button>
      </div>
    </aside>
  );
}

// ─── Message rendering ───
function renderMarkdown(md) {
  const html = window.marked ? window.marked.parse(md || '') : escapeHtml(md || '');
  return window.DOMPurify ? window.DOMPurify.sanitize(html, { ADD_ATTR: ['target'] }) : html;
}

// Detect renderable code blocks (html, svg, mermaid) in a message.
function extractArtifacts(text) {
  const out = [];
  const rx = /```(html|svg|mermaid)\s*\n([\s\S]*?)```/g;
  let m;
  while ((m = rx.exec(text || ''))) out.push({ lang: m[1], code: m[2] });
  return out;
}
function escapeHtml(s) { return String(s).replace(/[&<>"']/g, c => ({ '&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;', "'":'&#39;' }[c])); }

function ArtifactPreview({ artifact }) {
  const iframeRef = useRef(null);
  useEffect(() => {
    if (!iframeRef.current) return;
    let doc;
    if (artifact.lang === 'html') doc = artifact.code;
    else if (artifact.lang === 'svg') doc = `<!doctype html><meta charset="utf-8"><style>body{margin:0;background:transparent}svg{max-width:100%;height:auto}</style>${artifact.code}`;
    else if (artifact.lang === 'mermaid') doc = `<!doctype html><meta charset="utf-8"><style>body{margin:0;padding:12px;background:transparent;font-family:system-ui}</style><pre class="mermaid">${escapeHtml(artifact.code)}</pre><script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"><\/script><script>mermaid.initialize({startOnLoad:true,theme:'dark'})<\/script>`;
    iframeRef.current.srcdoc = doc || '';
  }, [artifact]);
  return (
    <div className="artifact-preview">
      <div className="artifact-head">
        <span className="artifact-lang mono">{artifact.lang}</span>
        <span style={{ flex: 1 }} />
        <button className="mb-btn" onClick={() => { navigator.clipboard?.writeText(artifact.code); }}>Copy</button>
      </div>
      {/* allow-scripts WITHOUT allow-same-origin: combining the two lets srcdoc
          script reach window.parent and defeat the sandbox entirely. Artifacts
          render fine without same-origin. */}
      <iframe ref={iframeRef} className="artifact-iframe" sandbox="allow-scripts" title="artifact" />
    </div>
  );
}

function MessageAttachments({ attachments }) {
  if (!attachments?.length) return null;
  return (
    <div className="msg-attachments">
      {attachments.map(a => a.kind === 'image'
        ? <img key={a.id} className="msg-image" src={`/api/attachments/${a.id}`} alt={a.filename} />
        : <span key={a.id} className="attach-chip">📎 {a.filename}</span>
      )}
    </div>
  );
}

function ChatMessages({ messages, streaming }) {
  const scrollRef = useRef(null);
  useEffect(() => {
    if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
  }, [messages, streaming]);

  return (
    <div className="msg-list" ref={scrollRef}>
      {messages.map((m, i) => {
        const arts = m.role === 'assistant' ? extractArtifacts(m.content) : [];
        return (
          <div key={m.id || 'live-' + i} className={'msg-row ' + m.role}>
            <div className="msg-inner">
              {m.role === 'assistant' && <div className="msg-role">Assistant · {m.model || ''}</div>}
              <MessageAttachments attachments={m.attachments} />
              {m.role === 'assistant' ? (
                <div className="msg-bubble">
                  <div className="msg-md" dangerouslySetInnerHTML={{ __html: renderMarkdown(m.content) + (m.streaming ? '<span class="msg-caret"></span>' : '') }} />
                </div>
              ) : (
                <div className="msg-bubble">{m.content}</div>
              )}
              {arts.map((a, j) => <ArtifactPreview key={i + '-a-' + j} artifact={a} />)}
            </div>
          </div>
        );
      })}
    </div>
  );
}

// ─── Full search results view (main pane) ───
function SearchResults({ term, results, onOpenThread, onClear }) {
  const msgs = results?.messages || [];
  const threads = results?.threads || [];
  return (
    <div className="search-view">
      <div className="search-head">
        <div>
          <h2 className="search-title">Search</h2>
          <div className="search-sub">
            {threads.length + msgs.length} result{threads.length + msgs.length === 1 ? '' : 's'} for “{term}”
          </div>
        </div>
        <button className="mb-btn" onClick={onClear}>Clear</button>
      </div>

      {threads.length > 0 && (
        <div className="search-section">
          <div className="search-section-h">Chats ({threads.length})</div>
          {threads.map(t => (
            <div className="search-hit" key={'t' + t.id} onClick={() => onOpenThread(t.slug)}>
              <div className="search-hit-title">{t.title}</div>
              <div className="search-hit-meta">updated {new Date(t.updated_at).toLocaleString()}</div>
            </div>
          ))}
        </div>
      )}

      {msgs.length > 0 && (
        <div className="search-section">
          <div className="search-section-h">Messages ({msgs.length})</div>
          {msgs.map(m => (
            <div className="search-hit" key={'m' + m.id} onClick={() => onOpenThread(m.thread_slug)}>
              <div className="search-hit-title">{m.thread_title}</div>
              <div className="search-hit-snippet">
                <Highlight text={m.snippet} term={term} />
              </div>
              <div className="search-hit-meta">{m.role} · {new Date(m.created_at).toLocaleString()}</div>
            </div>
          ))}
        </div>
      )}

      {threads.length === 0 && msgs.length === 0 && (
        <div className="field-hint" style={{ padding: 20 }}>Nothing found. Try a different term.</div>
      )}
    </div>
  );
}

function Highlight({ text, term }) {
  if (!term) return <>{text}</>;
  const i = String(text).toLowerCase().indexOf(String(term).toLowerCase());
  if (i < 0) return <>{text}</>;
  return (
    <>
      {text.slice(0, i)}
      <mark className="search-mark">{text.slice(i, i + term.length)}</mark>
      {text.slice(i + term.length)}
    </>
  );
}

// ─── Tool activity strip (what the assistant is actually doing) ───
function ToolActivity({ items }) {
  if (!items?.length) return null;
  return (
    <div className="tool-activity">
      {items.map((it, i) => (
        <div key={i} className={'tool-chip' + (it.running ? ' running' : it.ok === false ? ' failed' : ' done')}>
          <span className="tool-dot" />
          <span className="tool-name">{it.kind === 'rag' ? '📚 library' : '🔧 ' + it.name}</span>
          <span className="tool-label">{it.running ? 'running…' : (it.label || 'done')}</span>
          {it.ms != null && !it.running && <span className="tool-ms">{it.ms}ms</span>}
        </div>
      ))}
    </div>
  );
}

// ─── Proposed write actions awaiting human approval ───
function ProposalCards({ proposals, onApprove, onDismiss, busy }) {
  if (!proposals?.length) return null;
  return (
    <div className="proposals">
      {proposals.map((p, i) => (
        <div className="proposal" key={i}>
          <div className="proposal-head">
            <span className="proposal-badge">needs your approval</span>
            <span className="proposal-action">{p.action === 'send_email' ? '✉️ Send email' : p.action === 'post_slack' ? '💬 Post to Slack' : p.action}</span>
          </div>
          {p.action === 'send_email' && (
            <div className="proposal-body">
              <div><span className="proposal-k">To</span> {p.preview.to}</div>
              <div><span className="proposal-k">Subject</span> {p.preview.subject}</div>
              <pre className="proposal-text">{p.preview.body}</pre>
            </div>
          )}
          {p.action === 'post_slack' && (
            <div className="proposal-body">
              <div><span className="proposal-k">Channel</span> {p.preview.channel_id}</div>
              <pre className="proposal-text">{p.preview.text}</pre>
            </div>
          )}
          <div className="proposal-actions">
            <button className="mb-btn" onClick={() => onDismiss(i)}>Discard</button>
            <button className="mb-btn primary" disabled={busy} onClick={() => onApprove(p, i)}>
              {busy ? 'Sending…' : 'Approve & send'}
            </button>
          </div>
        </div>
      ))}
    </div>
  );
}

// ─── Composer ───
function Composer({ onSend, onCancel, onAttach, streaming, disabled, prefill, pendingAttachments, onRemoveAttachment }) {
  const [text, setText] = useState('');
  const ref = useRef(null);
  useEffect(() => { if (ref.current) autoSize(ref.current); }, [text]);
  useEffect(() => {
    if (prefill?.text != null) {
      setText(prefill.text);
      setTimeout(() => { if (ref.current) { ref.current.focus(); autoSize(ref.current); } }, 0);
    }
  }, [prefill?.nonce]);

  function keyDown(e) {
    if (e.key === 'Enter' && !e.shiftKey && !streaming) {
      e.preventDefault();
      const v = text.trim();
      if (!v) return;
      onSend(v); setText('');
    }
  }
  function send() { if (!text.trim() || streaming) return; onSend(text.trim()); setText(''); }

  return (
    <div className="composer-wrap">
      {pendingAttachments?.length > 0 && (
        <div className="composer-attachments">
          {pendingAttachments.map(a => (
            <span className="attach-chip" key={a.id}>
              {a.kind === 'image' ? '🖼' : '📎'} {a.filename}
              {a.has_text && <span style={{ color: 'var(--ok)' }}> · indexed</span>}
              {onRemoveAttachment && <button className="rm" onClick={() => onRemoveAttachment(a)}>×</button>}
            </span>
          ))}
        </div>
      )}
      <div className="composer">
        <textarea ref={ref}
          placeholder={streaming ? 'Generating…' : 'Message the assistant. 📎 for files & images, ⚡ for skills.'}
          value={text}
          disabled={disabled}
          onKeyDown={keyDown}
          onChange={e => setText(e.target.value)} />
        <div className="composer-bar">
          <button className="composer-attach" onClick={onAttach} title="Attach file or image" disabled={streaming || disabled}>📎</button>
          <div className="spacer" />
          {streaming
            ? <button className="composer-send stop" onClick={onCancel}>Stop</button>
            : <button className="composer-send" onClick={send} disabled={!text.trim() || disabled}>Send ↵</button>}
        </div>
      </div>
    </div>
  );
}

function autoSize(el) {
  el.style.height = 'auto';
  el.style.height = Math.min(260, el.scrollHeight) + 'px';
}

// ─── ConfirmDialog: reusable confirm or prompt (avoids blocked window.confirm/prompt) ───
function ConfirmDialog({ dialog, onClose }) {
  const [value, setValue] = useState(dialog.prompt ?? '');
  const inputRef = useRef(null);
  useEffect(() => { if (dialog.prompt !== undefined && inputRef.current) { inputRef.current.focus(); inputRef.current.select(); } }, []);
  return (
    <Modal title={dialog.title} onClose={onClose} width={460}
      actions={<>
        <button className="mb-btn" onClick={onClose}>Cancel</button>
        <button className={'mb-btn ' + (dialog.danger ? 'danger' : 'primary')}
          onClick={() => dialog.onConfirm(dialog.prompt !== undefined ? value : true)}>
          {dialog.confirmLabel || 'OK'}
        </button>
      </>}>
      {dialog.body && <div className="field-hint">{dialog.body}</div>}
      {dialog.prompt !== undefined && (
        <input ref={inputRef} className="field-input" value={value}
          onChange={(e) => setValue(e.target.value)}
          onKeyDown={(e) => {
            if (e.key === 'Enter') { e.preventDefault(); dialog.onConfirm(value); }
            else if (e.key === 'Escape') { e.preventDefault(); onClose(); }
          }} />
      )}
    </Modal>
  );
}

function Toast({ toast, onClose }) {
  return (
    <div className={'toast ' + (toast.kind || 'err')} onClick={onClose}>
      {toast.text}
    </div>
  );
}

// ─── Modal shell ───
function Modal({ title, onClose, children, actions, width }) {
  useEffect(() => {
    const key = (e) => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', key);
    return () => window.removeEventListener('keydown', key);
  }, [onClose]);
  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" style={width ? { width } : null} onClick={e => e.stopPropagation()}>
        <div className="modal-head">
          <h3>{title}</h3>
          <button className="icon-btn" onClick={onClose}>×</button>
        </div>
        <div className="modal-body">{children}</div>
        {actions && <div className="modal-actions">{actions}</div>}
      </div>
    </div>
  );
}

// ─── New project modal ───
function NewProjectModal({ providers, onClose, onCreated }) {
  const [form, setForm] = useState({
    name: '', description: '', system_prompt: '',
    default_provider: providers[0]?.id || 'anthropic',
    default_model: providers[0]?.models?.[0]?.id || 'claude-sonnet-5',
    is_shared: true, color: '#d97757', icon: ''
  });
  const [err, setErr] = useState(null);
  const [busy, setBusy] = useState(false);
  const prov = providers.find(p => p.id === form.default_provider);

  async function submit() {
    if (!form.name.trim()) { setErr('Name required'); return; }
    setBusy(true); setErr(null);
    try {
      const j = await api('/api/projects', { method: 'POST', body: JSON.stringify(form) });
      onCreated(j.project);
    } catch (e) { setErr(errorLabel(e.code)); } finally { setBusy(false); }
  }

  return (
    <Modal title="New project" onClose={onClose} width={620}
      actions={<>
        <button className="mb-btn" onClick={onClose}>Cancel</button>
        <button className="mb-btn primary" onClick={submit} disabled={busy}>{busy ? '…' : 'Create'}</button>
      </>}>
      <div className="field">
        <div className="field-label">Name</div>
        <input className="field-input" autoFocus value={form.name} onChange={e => setForm(f => ({ ...f, name: e.target.value }))} placeholder="Q4 Marketing campaigns" />
      </div>
      <div className="field">
        <div className="field-label">Description (optional)</div>
        <input className="field-input" value={form.description} onChange={e => setForm(f => ({ ...f, description: e.target.value }))} placeholder="What this project is about" />
      </div>
      <div className="field">
        <div className="field-label">Custom instructions (system prompt)</div>
        <textarea className="field-textarea" value={form.system_prompt} onChange={e => setForm(f => ({ ...f, system_prompt: e.target.value }))} placeholder="Written every chat in this project. E.g.: 'You are a marketing copywriter for Coachinside. Tone: direct, friendly, no jargon.'" />
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
        <div className="field">
          <div className="field-label">Default provider</div>
          <select className="field-input" value={form.default_provider} onChange={e => {
            const p = providers.find(pp => pp.id === e.target.value);
            setForm(f => ({ ...f, default_provider: e.target.value, default_model: p?.models?.[0]?.id || 'default' }));
          }}>
            {providers.map(p => <option key={p.id} value={p.id}>{p.label}</option>)}
          </select>
        </div>
        <div className="field">
          <div className="field-label">Default model</div>
          <select className="field-input" value={form.default_model} onChange={e => setForm(f => ({ ...f, default_model: e.target.value }))}>
            {(prov?.models || []).map(m => <option key={m.id} value={m.id}>{m.label}</option>)}
          </select>
        </div>
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: '80px 80px 1fr', gap: 12, alignItems: 'end' }}>
        <div className="field">
          <div className="field-label">Icon</div>
          <input className="field-input" value={form.icon} maxLength={2} onChange={e => setForm(f => ({ ...f, icon: e.target.value }))} placeholder="🚀" />
        </div>
        <div className="field">
          <div className="field-label">Color</div>
          <input className="field-input" type="color" value={form.color} onChange={e => setForm(f => ({ ...f, color: e.target.value }))} />
        </div>
        <div className="field">
          <label style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer' }}>
            <input type="checkbox" checked={form.is_shared} onChange={e => setForm(f => ({ ...f, is_shared: e.target.checked }))} />
            <span style={{ fontSize: 12 }}>Share with entire team (all members can edit)</span>
          </label>
        </div>
      </div>
      {err && <div className="field-error">{err}</div>}
    </Modal>
  );
}

// ─── Project settings modal (edit + files + members + delete) ───
function ProjectSettingsModal({ project, providers, currentUser, onClose, onChange, onDeleted, confirm }) {
  const [tab, setTab] = useState('general');
  const [form, setForm] = useState({
    name: project.name, description: project.description || '',
    system_prompt: project.system_prompt || '',
    default_provider: project.default_provider, default_model: project.default_model,
    is_shared: !!project.is_shared, color: project.color || '#d97757', icon: project.icon || ''
  });
  const [members, setMembers] = useState(project.members || []);
  const [files, setFiles] = useState(project.files || []);
  const [inviteEmail, setInviteEmail] = useState('');
  const [inviteRole, setInviteRole] = useState('editor');
  const [msg, setMsg] = useState(null);
  const fileInputRef = useRef(null);
  const prov = providers.find(p => p.id === form.default_provider);
  const canManage = project.my_role === 'owner' || project.my_role === 'admin';

  async function saveGeneral() {
    setMsg(null);
    try {
      const j = await api('/api/projects/' + project.id, { method: 'PATCH', body: JSON.stringify(form) });
      onChange(j.project);
      setMsg({ ok: 'Saved.' });
    } catch (e) { setMsg({ err: errorLabel(e.code) }); }
  }
  async function inviteMember() {
    setMsg(null);
    try {
      await api(`/api/projects/${project.id}/members`, { method: 'POST', body: JSON.stringify({ email: inviteEmail, role: inviteRole }) });
      const j = await api('/api/projects/' + project.slug);
      setMembers(j.project.members || []); onChange(j.project);
      setInviteEmail(''); setMsg({ ok: 'Member added.' });
    } catch (e) { setMsg({ err: errorLabel(e.code) }); }
  }
  function removeMember(uid, label) {
    confirm({
      title: 'Remove member',
      body: `Remove ${label} from "${project.name}"? They lose access to its chats and files.`,
      confirmLabel: 'Remove', danger: true,
      onConfirm: async () => {
        confirm(null);
        await api(`/api/projects/${project.id}/members/${uid}`, { method: 'DELETE' });
        const j = await api('/api/projects/' + project.slug);
        setMembers(j.project.members || []); onChange(j.project);
      }
    });
  }
  async function pickFile() { fileInputRef.current?.click(); }
  async function onFile(e) {
    const file = e.target.files?.[0]; e.target.value = '';
    if (!file) return;
    const fd = new FormData(); fd.append('file', file);
    try {
      await api('/api/projects/' + project.slug + '/files', { method: 'POST', body: fd });
      const j = await api('/api/projects/' + project.slug);
      setFiles(j.project.files || []); onChange(j.project);
    } catch (e) { alert('Upload failed: ' + (e.code || e.message)); }
  }
  function deleteFile(id, filename) {
    confirm({
      title: 'Delete file',
      body: `Delete "${filename}" from this project? It is also removed from the knowledge base.`,
      confirmLabel: 'Delete', danger: true,
      onConfirm: async () => {
        confirm(null);
        await api(`/api/projects/${project.slug}/files/${id}`, { method: 'DELETE' });
        const j = await api('/api/projects/' + project.slug);
        setFiles(j.project.files || []); onChange(j.project);
      }
    });
  }
  function deleteProject() {
    confirm({
      title: 'Delete project',
      body: `Delete "${project.name}"? Its files and membership are removed. Chats are kept and become un-filed. This cannot be undone.`,
      confirmLabel: 'Delete project', danger: true,
      onConfirm: async () => {
        confirm(null);
        await api('/api/projects/' + project.id, { method: 'DELETE' });
        onDeleted();
      }
    });
  }

  return (
    <Modal title={`Project — ${project.name}`} onClose={onClose} width={680}
      actions={<button className="mb-btn primary" onClick={onClose}>Done</button>}>
      <div style={{ display: 'flex', gap: 8, borderBottom: '1px solid var(--line)', paddingBottom: 8, marginBottom: 4 }}>
        {['general', 'files', 'members', 'danger'].map(t => (
          <button key={t} className={'mb-btn' + (tab === t ? ' primary' : '')} onClick={() => setTab(t)}>
            {t === 'general' ? 'General' : t === 'files' ? `Files (${files.length})` : t === 'members' ? `Members (${members.length})` : 'Danger'}
          </button>
        ))}
      </div>

      {tab === 'general' && (
        <>
          <div className="field"><div className="field-label">Name</div>
            <input className="field-input" disabled={!canManage} value={form.name} onChange={e => setForm(f => ({ ...f, name: e.target.value }))} /></div>
          <div className="field"><div className="field-label">Description</div>
            <input className="field-input" disabled={!canManage} value={form.description} onChange={e => setForm(f => ({ ...f, description: e.target.value }))} /></div>
          <div className="field"><div className="field-label">Custom instructions</div>
            <textarea className="field-textarea" disabled={!canManage} value={form.system_prompt} onChange={e => setForm(f => ({ ...f, system_prompt: e.target.value }))} /></div>
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
            <div className="field"><div className="field-label">Default provider</div>
              <select className="field-input" disabled={!canManage} value={form.default_provider} onChange={e => {
                const pp = providers.find(pp => pp.id === e.target.value);
                setForm(f => ({ ...f, default_provider: e.target.value, default_model: pp?.models?.[0]?.id || 'default' }));
              }}>
                {providers.map(p => <option key={p.id} value={p.id}>{p.label}</option>)}
              </select>
            </div>
            <div className="field"><div className="field-label">Default model</div>
              <select className="field-input" disabled={!canManage} value={form.default_model} onChange={e => setForm(f => ({ ...f, default_model: e.target.value }))}>
                {(prov?.models || []).map(m => <option key={m.id} value={m.id}>{m.label}</option>)}
              </select>
            </div>
          </div>
          <div style={{ display: 'grid', gridTemplateColumns: '80px 80px 1fr', gap: 12, alignItems: 'end' }}>
            <div className="field"><div className="field-label">Icon</div>
              <input className="field-input" disabled={!canManage} value={form.icon} maxLength={2} onChange={e => setForm(f => ({ ...f, icon: e.target.value }))} /></div>
            <div className="field"><div className="field-label">Color</div>
              <input className="field-input" disabled={!canManage} type="color" value={form.color} onChange={e => setForm(f => ({ ...f, color: e.target.value }))} /></div>
            <div className="field"><label style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: canManage ? 'pointer' : 'not-allowed' }}>
              <input type="checkbox" disabled={!canManage} checked={form.is_shared} onChange={e => setForm(f => ({ ...f, is_shared: e.target.checked }))} />
              <span style={{ fontSize: 12 }}>Shared with entire team</span>
            </label></div>
          </div>
          {canManage && <button className="mb-btn primary" onClick={saveGeneral} style={{ alignSelf: 'flex-start' }}>Save</button>}
        </>
      )}

      {tab === 'files' && (
        <>
          <div className="field-hint">Files uploaded here are indexed once and injected into every chat in this project.</div>
          <div className="keys-list">
            {files.length === 0 ? <div className="field-hint">No files yet.</div> :
              files.map(f => (
                <div className="key-row" key={f.id}>
                  <div className="key-row-info">
                    <div className="key-row-title">📄 {f.filename} {f.has_text ? <span style={{ color: 'var(--ok)', fontSize: 10 }}>indexed</span> : <span style={{ color: 'var(--warn)', fontSize: 10 }}>not indexed</span>}</div>
                    <div className="key-row-meta">{prettyBytes(f.bytes)} · {f.mime || '—'}</div>
                  </div>
                  {canManage && <button className="mb-btn danger" onClick={() => deleteFile(f.id, f.filename)}>Delete</button>}
                </div>
              ))}
          </div>
          {canManage && <>
            <button className="mb-btn" onClick={pickFile}>+ Upload file (PDF / DOCX / TXT / MD / CSV)</button>
            <input ref={fileInputRef} type="file" hidden accept=".pdf,.docx,.txt,.md,.csv,.json" onChange={onFile} />
          </>}
        </>
      )}

      {tab === 'members' && (
        <>
          {canManage && (
            <div className="field">
              <div className="field-label">Invite by email</div>
              <div style={{ display: 'flex', gap: 8 }}>
                <input className="field-input" style={{ flex: 1 }} type="email" placeholder="teammate@coachinside.com"
                  value={inviteEmail} onChange={e => setInviteEmail(e.target.value)} />
                <select className="mb-select" value={inviteRole} onChange={e => setInviteRole(e.target.value)}>
                  <option value="viewer">Viewer</option>
                  <option value="editor">Editor</option>
                  <option value="admin">Admin</option>
                </select>
                <button className="mb-btn primary" onClick={inviteMember} disabled={!inviteEmail}>Add</button>
              </div>
            </div>
          )}
          <div className="keys-list">
            {members.length === 0 ? <div className="field-hint">Only you have explicit access.{project.is_shared ? ' Team-shared: all users can edit.' : ''}</div> :
              members.map(m => (
                <div className="key-row" key={m.id}>
                  <div className="key-row-info">
                    <div className="key-row-title">{m.display_name || m.email}</div>
                    <div className="key-row-meta">{m.email} · {m.role}</div>
                  </div>
                  {canManage && m.id !== currentUser.id && <button className="mb-btn danger" onClick={() => removeMember(m.id, m.display_name || m.email)}>Remove</button>}
                </div>
              ))}
          </div>
        </>
      )}

      {tab === 'danger' && (
        <>
          <div className="field-hint">Deleting a project removes its files + membership but leaves the chats intact (they become un-filed).</div>
          {project.my_role === 'owner'
            ? <button className="mb-btn danger" onClick={deleteProject}>Delete this project</button>
            : <div className="field-hint">Only the project owner can delete.</div>}
        </>
      )}

      {msg?.ok && <div className="field-hint" style={{ color: 'var(--ok)' }}>{msg.ok}</div>}
      {msg?.err && <div className="field-error">{msg.err}</div>}
    </Modal>
  );
}

function prettyBytes(n) {
  if (!n) return '0 B';
  const u = ['B', 'KB', 'MB', 'GB']; let i = 0;
  while (n >= 1024 && i < u.length - 1) { n /= 1024; i++; }
  return `${n.toFixed(n < 10 ? 1 : 0)} ${u[i]}`;
}

// ─── Project detail (main area when project active, no thread active) ───
function ProjectDetail({ project, onOpenChat, onNewChatInProject, onSettings }) {
  return (
    <div className="project-view">
      <div className="project-hero">
        <div className="project-hero-icon" style={project.color ? { background: project.color, color: '#fff' } : null}>
          {project.icon || (project.name || '?')[0]?.toUpperCase()}
        </div>
        <div style={{ flex: 1, minWidth: 0 }}>
          <h1 className="project-name">{project.name}</h1>
          {project.description && <div className="project-desc">{project.description}</div>}
          <div className="project-meta mono">
            {project.threads_count} chats · {project.files_count} files · {project.is_shared ? 'team-shared' : 'private'} · your role: {project.my_role}
          </div>
        </div>
        <div style={{ display: 'flex', gap: 8, flexShrink: 0 }}>
          <button className="mb-btn" onClick={onSettings}>⚙︎ Settings</button>
          <button className="mb-btn primary" onClick={onNewChatInProject}>＋ New chat in project</button>
        </div>
      </div>

      {project.system_prompt && (
        <div className="project-section">
          <div className="project-section-h">Custom instructions</div>
          <div className="project-sysprompt">{project.system_prompt}</div>
        </div>
      )}

      <div className="project-section">
        <div className="project-section-h">Files ({(project.files || []).length}) — auto-injected into every chat</div>
        {(project.files || []).length === 0
          ? <div className="field-hint">No files. Open Settings → Files to upload PDFs, DOCX, MD, TXT.</div>
          : <div className="project-file-grid">
              {(project.files || []).map(f => (
                <div className="project-file-chip" key={f.id}>📄 {f.filename}</div>
              ))}
            </div>}
      </div>

      <div className="project-section">
        <div className="project-section-h">Chats in this project ({(project.threads || []).length})</div>
        {(project.threads || []).length === 0
          ? <div className="field-hint">No chats yet. Click <b>New chat in project</b>.</div>
          : <div className="project-thread-list">
              {(project.threads || []).map(t => (
                <div className="project-thread-item" key={t.slug} onClick={() => onOpenChat(t)}>
                  <div className="project-thread-title">{t.title}</div>
                  <div className="project-thread-meta">{t.model} · updated {new Date(t.updated_at).toLocaleString()}</div>
                </div>
              ))}
            </div>}
      </div>
    </div>
  );
}

// ─── Settings modal (API keys — user + team) ───
function SettingsModal({ user, providers, onClose }) {
  const [keys, setKeys] = useState([]);
  const [teamKeys, setTeamKeys] = useState([]);
  const [add, setAdd] = useState(null); // null | { scope, form }
  const [msg, setMsg] = useState(null);

  const reload = useCallback(async () => {
    const k = await api('/api/keys').catch(() => ({ keys: [] }));
    setKeys(k.keys || []);
    if (user.role === 'admin') {
      const t = await api('/api/admin/team-keys').catch(() => ({ keys: [] }));
      setTeamKeys(t.keys || []);
    }
  }, [user.role]);
  useEffect(() => { reload(); }, [reload]);

  function startAdd(scope) {
    setMsg(null);
    setAdd({ scope, form: { provider: providers[0]?.id || 'anthropic', label: 'default', api_key: '', endpoint: '' } });
  }

  async function save() {
    const url = add.scope === 'team' ? '/api/admin/team-keys' : '/api/keys';
    try {
      await api(url, { method: 'POST', body: JSON.stringify(add.form) });
      setAdd(null); reload();
    } catch (e) { setMsg({ err: e.code || 'save_failed' }); }
  }
  async function remove(scope, id) {
    const url = scope === 'team' ? '/api/admin/team-keys/' + id : '/api/keys/' + id;
    await api(url, { method: 'DELETE' });
    reload();
  }

  const selectedProv = add ? providers.find(p => p.id === add.form.provider) : null;

  return (
    <Modal title="Settings" onClose={onClose}
      actions={<button className="mb-btn primary" onClick={onClose}>Done</button>}>
      <div className="field-hint">Signed in as <b>{user.email}</b> ({user.role}). API keys encrypted with AES-256-GCM. Personal keys override team-wide.</div>

      {user.role === 'admin' && (
        <div>
          <div className="field-label" style={{ marginBottom: 8 }}>Team-wide keys (shared with all users)</div>
          <div className="keys-list">
            {teamKeys.length === 0 ? <div className="field-hint">No team keys yet.</div> :
              teamKeys.map(k => (
                <div className="key-row" key={'t' + k.id}>
                  <div className="key-row-info">
                    <div className="key-row-title">{k.provider} · <span style={{ color: 'var(--ink-mute)' }}>{k.label}</span></div>
                    <div className="key-row-meta">{k.endpoint || '—'}</div>
                  </div>
                  <button className="mb-btn danger" onClick={() => remove('team', k.id)}>Delete</button>
                </div>
              ))}
          </div>
          <button className="mb-btn" style={{ marginTop: 8 }} onClick={() => startAdd('team')}>+ Add team key</button>
        </div>
      )}

      <div>
        <div className="field-label" style={{ marginBottom: 8 }}>Your personal keys</div>
        <div className="keys-list">
          {keys.length === 0 ? <div className="field-hint">None. Falls back to team keys.</div> :
            keys.map(k => (
              <div className="key-row" key={'u' + k.id}>
                <div className="key-row-info">
                  <div className="key-row-title">{k.provider} · <span style={{ color: 'var(--ink-mute)' }}>{k.label}</span></div>
                  <div className="key-row-meta">{k.endpoint || '—'}</div>
                </div>
                <button className="mb-btn danger" onClick={() => remove('user', k.id)}>Delete</button>
              </div>
            ))}
        </div>
        <button className="mb-btn" style={{ marginTop: 8 }} onClick={() => startAdd('user')}>+ Add personal key</button>
      </div>

      {add && (
        <div style={{ padding: 14, border: '1px dashed var(--line)', borderRadius: 8, display: 'flex', flexDirection: 'column', gap: 10 }}>
          <div className="field-label">{add.scope === 'team' ? 'New team key' : 'New personal key'}</div>
          <div className="field">
            <div className="field-label">Provider</div>
            <select className="field-input" value={add.form.provider}
              onChange={e => {
                const provId = e.target.value;
                const p = providers.find(pp => pp.id === provId);
                setAdd(a => ({ ...a, form: { ...a.form, provider: provId, endpoint: p?.defaultEndpoint || '' } }));
              }}>
              {providers.map(p => <option key={p.id} value={p.id}>{p.label}</option>)}
            </select>
          </div>
          <div className="field">
            <div className="field-label">Label</div>
            <input className="field-input" value={add.form.label}
              onChange={e => setAdd(a => ({ ...a, form: { ...a.form, label: e.target.value } }))} />
          </div>
          {selectedProv?.needsKey && (
            <div className="field">
              <div className="field-label">API key</div>
              <input className="field-input" type="password" value={add.form.api_key} placeholder="sk-..."
                onChange={e => setAdd(a => ({ ...a, form: { ...a.form, api_key: e.target.value } }))} />
            </div>
          )}
          {selectedProv?.defaultEndpoint !== undefined && (
            <div className="field">
              <div className="field-label">Endpoint (optional)</div>
              <input className="field-input" value={add.form.endpoint} placeholder={selectedProv.defaultEndpoint || ''}
                onChange={e => setAdd(a => ({ ...a, form: { ...a.form, endpoint: e.target.value } }))} />
            </div>
          )}
          {msg?.err && <div className="field-error">{errorLabel(msg.err)}</div>}
          <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
            <button className="mb-btn" onClick={() => setAdd(null)}>Cancel</button>
            <button className="mb-btn primary" onClick={save}>Save</button>
          </div>
        </div>
      )}
    </Modal>
  );
}

// ─── Skills modal (list, CRUD, use) ───
function SkillsModal({ user, providers, onClose, onUse }) {
  const [skills, setSkills] = useState([]);
  const [editing, setEditing] = useState(null); // skill object or 'new'
  const [msg, setMsg] = useState(null);
  const reload = useCallback(async () => {
    try { const j = await api('/api/skills'); setSkills(j.skills || []); } catch {}
  }, []);
  useEffect(() => { reload(); }, [reload]);

  function startNew() { setEditing({ name: '', description: '', icon: '', color: '#d97757', prompt: '', system_prompt: '', scope: 'personal', default_provider: '', default_model: '', tags: '' }); }
  async function save() {
    try {
      if (editing.id) await api('/api/skills/' + editing.id, { method: 'PATCH', body: JSON.stringify(editing) });
      else await api('/api/skills', { method: 'POST', body: JSON.stringify(editing) });
      setEditing(null); reload();
    } catch (e) { setMsg({ err: errorLabel(e.code) }); }
  }
  async function remove(id) {
    await api('/api/skills/' + id, { method: 'DELETE' }); reload();
  }

  if (editing) {
    return (
      <Modal title={editing.id ? `Edit skill · ${editing.name}` : 'New skill'} onClose={() => setEditing(null)} width={640}
        actions={<><button className="mb-btn" onClick={() => setEditing(null)}>Cancel</button><button className="mb-btn primary" onClick={save}>Save</button></>}>
        <div className="field"><div className="field-label">Name</div><input className="field-input" value={editing.name} onChange={e => setEditing({ ...editing, name: e.target.value })} /></div>
        <div className="field"><div className="field-label">Description</div><input className="field-input" value={editing.description || ''} onChange={e => setEditing({ ...editing, description: e.target.value })} /></div>
        <div style={{ display: 'grid', gridTemplateColumns: '80px 80px 1fr 140px', gap: 12 }}>
          <div className="field"><div className="field-label">Icon</div><input className="field-input" maxLength={2} value={editing.icon || ''} onChange={e => setEditing({ ...editing, icon: e.target.value })} /></div>
          <div className="field"><div className="field-label">Color</div><input className="field-input" type="color" value={editing.color || '#d97757'} onChange={e => setEditing({ ...editing, color: e.target.value })} /></div>
          <div className="field"><div className="field-label">Tags (comma)</div><input className="field-input" value={editing.tags || ''} onChange={e => setEditing({ ...editing, tags: e.target.value })} placeholder="cold-email, seo, ..." /></div>
          <div className="field"><div className="field-label">Scope</div>
            <select className="field-input" value={editing.scope || 'personal'} onChange={e => setEditing({ ...editing, scope: e.target.value })}>
              <option value="personal">Personal</option>
              {user.role === 'admin' && <option value="team">Team-wide</option>}
            </select>
          </div>
        </div>
        <div className="field"><div className="field-label">Prompt (use {'{{variable}}'} for placeholders)</div>
          <textarea className="field-textarea" style={{ minHeight: 160 }} value={editing.prompt || ''} onChange={e => setEditing({ ...editing, prompt: e.target.value })}
            placeholder="Draft a cold outreach email to {{prospect_name}} at {{company}} about {{topic}}." />
          <div className="field-hint">Detected variables: {extractVars(editing.prompt).join(', ') || '—'}</div>
        </div>
        <div className="field"><div className="field-label">System prompt override (optional)</div>
          <textarea className="field-textarea" value={editing.system_prompt || ''} onChange={e => setEditing({ ...editing, system_prompt: e.target.value })} /></div>
        {msg?.err && <div className="field-error">{msg.err}</div>}
      </Modal>
    );
  }

  return (
    <Modal title="Skills — Prompt library" onClose={onClose} width={640}
      actions={<><button className="mb-btn" onClick={startNew}>+ New skill</button><button className="mb-btn primary" onClick={onClose}>Done</button></>}>
      <div className="field-hint">Reusable prompt templates. Click a skill to prefill the composer.</div>
      <div className="keys-list">
        {skills.length === 0 ? <div className="field-hint">No skills yet.</div> :
          skills.map(s => (
            <div className="key-row" key={s.id}>
              <div style={{ width: 28, height: 28, borderRadius: 7, background: s.color || 'var(--accent)', color: '#fff', display: 'grid', placeItems: 'center', fontWeight: 600, fontSize: 13, flexShrink: 0 }}>
                {s.icon || (s.name || '?')[0]?.toUpperCase()}
              </div>
              <div className="key-row-info">
                <div className="key-row-title">{s.name} {s.scope === 'team' && <span className="sb-badge" style={{ marginLeft: 6 }}>team</span>}</div>
                <div className="key-row-meta">{s.description || '—'} {(s.variables || []).length > 0 && `· vars: ${s.variables.join(', ')}`} · used {s.use_count || 0}×</div>
              </div>
              <button className="mb-btn primary" onClick={() => { onUse(s); onClose(); }}>Use</button>
              <button className="mb-btn" onClick={() => setEditing(s)}>Edit</button>
              <button className="mb-btn danger" onClick={() => remove(s.id)}>Delete</button>
            </div>
          ))}
      </div>
    </Modal>
  );
}

// ─── Plugins modal — give the assistant new abilities at runtime ───
const PLUGIN_PRESETS = [
  {
    label: '🖼  Image generation — free, no key needed',
    ready: true,
    plugin: {
      name: 'generate_image', method: 'GET',
      description: 'Generate an image from a text prompt. Returns an image URL you can embed or share. Use whenever the user asks for a picture, illustration, mockup, thumbnail or any visual.',
      url: 'https://image.pollinations.ai/prompt/{{input.prompt}}',
      input_schema: { type: 'object', properties: { prompt: { type: 'string', description: 'Description of the image to generate.' } }, required: ['prompt'] }
    }
  },
  {
    label: '🖼  Image — fal.ai FLUX schnell (fal key · ~$0.003 per image)',
    plugin: {
      name: 'fal_image', method: 'POST',
      description: 'Generate a high-quality image from a text prompt using FLUX. Returns an image URL. Use for marketing visuals, social posts, mockups and illustrations when quality matters.',
      url: 'https://fal.run/fal-ai/flux/schnell',
      headers: { 'content-type': 'application/json', authorization: 'Key {{SECRET}}' },
      body_template: { prompt: '{{input.prompt}}', image_size: 'landscape_16_9', num_images: 1 },
      input_schema: { type: 'object', properties: { prompt: { type: 'string', description: 'Description of the image to generate.' } }, required: ['prompt'] },
      result_path: 'images.0.url'
    }
  },
  {
    label: '🎬  Video — fal.ai LTX (fal key · ~$0.04 per 5s clip)',
    plugin: {
      name: 'generate_video', method: 'POST',
      description: 'Generate a short video clip from a text prompt. Returns a playable video URL. Use when the user asks for a video, clip, animation, ad or motion graphic.',
      url: 'https://fal.run/fal-ai/ltx-video',
      headers: { 'content-type': 'application/json', authorization: 'Key {{SECRET}}' },
      body_template: { prompt: '{{input.prompt}}' },
      input_schema: { type: 'object', properties: { prompt: { type: 'string', description: 'Description of the video to generate.' } }, required: ['prompt'] },
      result_path: 'video.url'
    }
  },
  {
    label: '🔎  Live web search — Tavily (needs API key, free tier)',
    plugin: {
      name: 'web_search', method: 'POST',
      description: 'Search the live web and return ranked results with snippets. Use for anything current: news, competitors, prices, people, market data.',
      url: 'https://api.tavily.com/search',
      headers: { 'content-type': 'application/json' },
      body_template: { api_key: '{{SECRET}}', query: '{{input.query}}', max_results: 5 },
      input_schema: { type: 'object', properties: { query: { type: 'string', description: 'What to search for.' } }, required: ['query'] },
      result_path: 'results'
    }
  },
  {
    label: '🗣  Text to speech — OpenAI (needs API key)',
    plugin: {
      name: 'text_to_speech', method: 'POST',
      description: 'Convert text into spoken audio. Returns an audio URL. Use for voiceovers, podcast drafts or accessibility.',
      url: 'https://api.openai.com/v1/audio/speech',
      headers: { 'content-type': 'application/json', authorization: 'Bearer {{SECRET}}' },
      body_template: { model: 'tts-1', voice: 'alloy', input: '{{input.text}}' },
      input_schema: { type: 'object', properties: { text: { type: 'string', description: 'Text to speak.' } }, required: ['text'] }
    }
  }
];

function PluginsModal({ user, onClose }) {
  const [list, setList] = useState([]);
  const [mode, setMode] = useState(null);
  const [form, setForm] = useState(null);
  const [mcp, setMcp] = useState({ name: '', server_url: '', secret: '', discovered: null });
  const [msg, setMsg] = useState(null);
  const [busy, setBusy] = useState(false);

  const reload = useCallback(async () => {
    try { const j = await api('/api/plugins'); setList(j.plugins || []); } catch {}
  }, []);
  useEffect(() => { reload(); }, [reload]);

  function startPreset(preset) {
    setMode('http');
    setForm({
      scope: 'team', secret: '', result_path: '', ...preset.plugin,
      input_schema_text: JSON.stringify(preset.plugin.input_schema, null, 2),
      headers_text: preset.plugin.headers ? JSON.stringify(preset.plugin.headers, null, 2) : '',
      body_text: preset.plugin.body_template ? JSON.stringify(preset.plugin.body_template, null, 2) : ''
    });
  }
  function startBlank() {
    setMode('http');
    setForm({
      name: '', description: '', url: '', method: 'POST', scope: 'team', secret: '', result_path: '',
      input_schema_text: JSON.stringify({ type: 'object', properties: { query: { type: 'string', description: 'What to do' } }, required: ['query'] }, null, 2),
      headers_text: '{\n  "content-type": "application/json"\n}', body_text: ''
    });
  }

  async function saveHttp() {
    setBusy(true); setMsg(null);
    try {
      await api('/api/plugins/http', { method: 'POST', body: JSON.stringify({
        name: form.name, description: form.description, scope: form.scope,
        method: form.method, url: form.url, secret: form.secret || undefined,
        input_schema: JSON.parse(form.input_schema_text || '{}'),
        headers: form.headers_text ? JSON.parse(form.headers_text) : undefined,
        body_template: form.body_text ? JSON.parse(form.body_text) : undefined,
        result_path: form.result_path || undefined
      })});
      setMode(null); setForm(null); reload();
    } catch (e) { setMsg({ err: errorLabel(e.code) }); } finally { setBusy(false); }
  }

  async function discover() {
    setBusy(true); setMsg(null);
    try {
      const j = await api('/api/plugins/mcp/discover', { method: 'POST', body: JSON.stringify({ server_url: mcp.server_url, secret: mcp.secret || undefined }) });
      setMcp(m => ({ ...m, discovered: j }));
    } catch (e) { setMsg({ err: e.code || e.message }); } finally { setBusy(false); }
  }
  async function installMcp() {
    setBusy(true); setMsg(null);
    try {
      await api('/api/plugins/mcp', { method: 'POST', body: JSON.stringify({ name: mcp.name, server_url: mcp.server_url, secret: mcp.secret || undefined, scope: 'team' }) });
      setMode(null); setMcp({ name: '', server_url: '', secret: '', discovered: null }); reload();
    } catch (e) { setMsg({ err: e.code || e.message }); } finally { setBusy(false); }
  }

  async function toggle(p) { await api('/api/plugins/' + p.id, { method: 'PATCH', body: JSON.stringify({ enabled: !p.enabled }) }); reload(); }
  async function remove(p) { await api('/api/plugins/' + p.id, { method: 'DELETE' }); reload(); }
  async function testPlugin(p) {
    setMsg({ ok: `Testing ${p.name}…` });
    try {
      const props = p.config?.input_schema?.properties || {};
      const key = Object.keys(props)[0];
      const j = await api('/api/plugins/' + p.id + '/test', { method: 'POST', body: JSON.stringify({ input: key ? { [key]: 'test' } : {} }) });
      const r = j.result || {};
      setMsg(r.error ? { err: `${p.name}: ${r.error}` } : { ok: `${p.name} ✓ ${JSON.stringify(r).slice(0, 180)}` });
      reload();
    } catch (e) { setMsg({ err: e.code || e.message }); }
  }

  if (mode === 'http' && form) {
    return (
      <Modal title="Add capability — HTTP API" onClose={() => { setMode(null); setForm(null); }} width={680}
        actions={<><button className="mb-btn" onClick={() => { setMode(null); setForm(null); }}>Cancel</button>
                   <button className="mb-btn primary" disabled={busy} onClick={saveHttp}>{busy ? '…' : 'Add capability'}</button></>}>
        <div className="field-hint">Describe any HTTPS API once and it becomes a tool the assistant can call by itself. Use <code>{'{{input.field}}'}</code> for arguments and <code>{'{{SECRET}}'}</code> wherever the API key belongs.</div>
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 100px 130px', gap: 12 }}>
          <div className="field"><div className="field-label">Tool name (a-z, _)</div>
            <input className="field-input" value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} placeholder="generate_image" /></div>
          <div className="field"><div className="field-label">Method</div>
            <select className="field-input" value={form.method} onChange={e => setForm({ ...form, method: e.target.value })}>
              <option>POST</option><option>GET</option><option>PUT</option></select></div>
          <div className="field"><div className="field-label">Scope</div>
            <select className="field-input" value={form.scope} onChange={e => setForm({ ...form, scope: e.target.value })}>
              {user.role === 'admin' && <option value="team">Whole team</option>}
              <option value="personal">Just me</option></select></div>
        </div>
        <div className="field"><div className="field-label">When should the assistant use this?</div>
          <textarea className="field-textarea" style={{ minHeight: 70 }} value={form.description}
            onChange={e => setForm({ ...form, description: e.target.value })}
            placeholder="Generate an image from a text prompt. Use whenever the user asks for a picture…" /></div>
        <div className="field"><div className="field-label">URL</div>
          <input className="field-input" value={form.url} onChange={e => setForm({ ...form, url: e.target.value })} placeholder="https://api.example.com/v1/generate" /></div>
        <div className="field"><div className="field-label">API key / token (optional — encrypted at rest)</div>
          <input className="field-input" type="password" value={form.secret} onChange={e => setForm({ ...form, secret: e.target.value })} placeholder="sk-…" /></div>
        <div className="field"><div className="field-label">Inputs (JSON schema)</div>
          <textarea className="field-textarea" value={form.input_schema_text} onChange={e => setForm({ ...form, input_schema_text: e.target.value })} /></div>
        <div className="field"><div className="field-label">Headers (JSON, optional)</div>
          <textarea className="field-textarea" style={{ minHeight: 66 }} value={form.headers_text} onChange={e => setForm({ ...form, headers_text: e.target.value })} /></div>
        <div className="field"><div className="field-label">Body template (JSON, optional)</div>
          <textarea className="field-textarea" style={{ minHeight: 66 }} value={form.body_text} onChange={e => setForm({ ...form, body_text: e.target.value })} /></div>
        <div className="field"><div className="field-label">Result path (optional, e.g. <code>output.0</code>)</div>
          <input className="field-input" value={form.result_path} onChange={e => setForm({ ...form, result_path: e.target.value })} /></div>
        {msg?.err && <div className="field-error">{msg.err}</div>}
      </Modal>
    );
  }

  if (mode === 'mcp') {
    return (
      <Modal title="Add capability — MCP server" onClose={() => setMode(null)} width={640}
        actions={<><button className="mb-btn" onClick={() => setMode(null)}>Cancel</button>
          {mcp.discovered
            ? <button className="mb-btn primary" disabled={busy || !mcp.name} onClick={installMcp}>{busy ? '…' : `Install ${mcp.discovered.tools.length} tool(s)`}</button>
            : <button className="mb-btn primary" disabled={busy || !mcp.server_url} onClick={discover}>{busy ? '…' : 'Discover tools'}</button>}</>}>
        <div className="field-hint">Connect a Model Context Protocol server and every tool it exposes becomes available to the assistant automatically — video, scraping, databases, whatever the server provides.</div>
        <div className="field"><div className="field-label">Server URL (streamable HTTP)</div>
          <input className="field-input" value={mcp.server_url} onChange={e => setMcp({ ...mcp, server_url: e.target.value, discovered: null })} placeholder="https://mcp.example.com/mcp" /></div>
        <div className="field"><div className="field-label">Auth token (optional — encrypted at rest)</div>
          <input className="field-input" type="password" value={mcp.secret} onChange={e => setMcp({ ...mcp, secret: e.target.value })} /></div>
        {mcp.discovered && (<>
          <div className="field"><div className="field-label">Name this plugin (a-z, _)</div>
            <input className="field-input" value={mcp.name} onChange={e => setMcp({ ...mcp, name: e.target.value })} placeholder="video_tools" /></div>
          <div className="field-label">Found {mcp.discovered.tools.length} tool(s) on {mcp.discovered.server?.name || 'server'}</div>
          <div className="keys-list">
            {mcp.discovered.tools.map(t => (
              <div className="key-row" key={t.name}>
                <div className="key-row-info">
                  <div className="key-row-title">{t.name}</div>
                  <div className="key-row-meta">{(t.description || '').slice(0, 110)}</div>
                </div></div>))}
          </div></>)}
        {msg?.err && <div className="field-error">{msg.err}</div>}
      </Modal>
    );
  }

  return (
    <Modal title="Plugins — give the assistant new abilities" onClose={onClose} width={700}
      actions={<button className="mb-btn primary" onClick={onClose}>Done</button>}>
      <div className="field-hint">The assistant can only do what it has tools for. Add an API or an MCP server here and it gains that ability immediately — no redeploy, no code.</div>

      <div>
        <div className="field-label" style={{ marginBottom: 8 }}>Installed ({list.length})</div>
        <div className="keys-list">
          {list.length === 0 ? <div className="field-hint">Nothing installed yet — try a quick-add below.</div> :
            list.map(p => (
              <div className="key-row" key={p.id}>
                <div className="key-row-info">
                  <div className="key-row-title">
                    {p.kind === 'mcp' ? '🔌' : '⚡'} {p.name}
                    {!p.enabled && <span className="sb-badge" style={{ marginLeft: 6, color: 'var(--ink-dim)' }}>off</span>}
                    {p.scope === 'team' && <span className="sb-badge" style={{ marginLeft: 6 }}>team</span>}
                    {p.has_secret && <span className="sb-badge" style={{ marginLeft: 6, color: 'var(--ok)', borderColor: 'var(--ok)' }}>key</span>}
                  </div>
                  <div className="key-row-meta">
                    {p.kind === 'mcp' ? `${p.config.tool_count} tool(s) · ${p.config.server_url}` : `${p.config.method} ${p.config.url}`}
                    {p.call_count ? ` · used ${p.call_count}×` : ''}
                    {p.last_error ? ` · ⚠ ${String(p.last_error).slice(0, 50)}` : ''}
                  </div>
                </div>
                <button className="mb-btn" onClick={() => testPlugin(p)}>Test</button>
                <button className="mb-btn" onClick={() => toggle(p)}>{p.enabled ? 'Disable' : 'Enable'}</button>
                <button className="mb-btn danger" onClick={() => remove(p)}>Delete</button>
              </div>))}
        </div>
      </div>

      <div>
        <div className="field-label" style={{ marginBottom: 8 }}>Quick add</div>
        <div className="keys-list">
          {PLUGIN_PRESETS.map(p => (
            <div className="key-row" key={p.label}>
              <div className="key-row-info"><div className="key-row-title">{p.label}</div></div>
              <button className={'mb-btn' + (p.ready ? ' primary' : '')} onClick={() => startPreset(p)}>Add</button>
            </div>))}
        </div>
      </div>

      <div style={{ display: 'flex', gap: 8 }}>
        <button className="mb-btn" onClick={startBlank}>+ Custom HTTP API</button>
        <button className="mb-btn" onClick={() => setMode('mcp')}>+ MCP server</button>
      </div>

      {msg?.ok && <div className="field-hint" style={{ color: 'var(--ok)', fontFamily: 'var(--f-mono)', fontSize: 11, wordBreak: 'break-all' }}>{msg.ok}</div>}
      {msg?.err && <div className="field-error" style={{ wordBreak: 'break-all' }}>{msg.err}</div>}
    </Modal>
  );
}

// ─── Connectors modal ───
function ConnectorsModal({ onClose }) {
  const [data, setData] = useState({ connectors: [], integrations: [] });
  const [manualFor, setManualFor] = useState(null); // provider id
  const [manualToken, setManualToken] = useState('');
  const [msg, setMsg] = useState(null);
  const reload = useCallback(async () => {
    try { const j = await api('/api/connectors'); setData(j); } catch {}
  }, []);
  useEffect(() => { reload(); }, [reload]);

  async function connect(id) {
    setMsg(null);
    try {
      const j = await api(`/api/connectors/${id}/authorize`);
      const win = window.open(j.url, 'oauth', 'width=600,height=800');
      const iv = setInterval(async () => {
        if (win?.closed) { clearInterval(iv); reload(); }
      }, 1000);
    } catch (e) { setMsg({ err: `${id}: ${e.code || e.message}` }); }
  }
  async function saveManual() {
    try {
      await api(`/api/connectors/${manualFor}/manual`, { method: 'POST', body: JSON.stringify({ token: manualToken, label: 'default' }) });
      setManualFor(null); setManualToken(''); reload();
    } catch (e) { setMsg({ err: e.code || e.message }); }
  }
  async function disconnect(id) {
    await api('/api/integrations/' + id, { method: 'DELETE' }); reload();
  }

  return (
    <Modal title="Connectors" onClose={onClose} width={640}
      actions={<button className="mb-btn primary" onClick={onClose}>Done</button>}>
      <div className="field-hint">Connect Google Workspace, Slack, GitHub, Notion, WhatsApp. Requires the admin to set OAuth app credentials in server env; then any team member can connect their own account.</div>
      <div className="keys-list">
        {data.connectors.map(c => {
          const active = data.integrations.filter(i => i.provider === c.id);
          return (
            <div className="key-row" key={c.id}>
              <div className="key-row-info">
                <div className="key-row-title">{c.label} {!c.available && <span className="sb-badge" style={{ background: 'transparent', color: 'var(--warn)', borderColor: 'var(--warn)' }}>needs env {c.env_hint.join(' + ')}</span>}</div>
                <div className="key-row-meta">{c.description} · {active.length ? `${active.length} connected` : 'not connected'}</div>
              </div>
              {c.manual_token
                ? <button className="mb-btn primary" onClick={() => setManualFor(c.id)} disabled={!c.available}>Paste token</button>
                : <button className="mb-btn primary" onClick={() => connect(c.id)} disabled={!c.available}>Connect</button>}
            </div>
          );
        })}
      </div>
      {data.integrations.length > 0 && (
        <>
          <div className="field-label" style={{ marginTop: 8 }}>Your connected accounts</div>
          <div className="keys-list">
            {data.integrations.map(i => (
              <div className="key-row" key={i.id}>
                <div className="key-row-info">
                  <div className="key-row-title">{i.provider} · <span style={{ color: 'var(--ink-mute)' }}>{i.account_label}</span></div>
                  <div className="key-row-meta">{i.scopes || '—'}</div>
                </div>
                <button className="mb-btn danger" onClick={() => disconnect(i.id)}>Disconnect</button>
              </div>
            ))}
          </div>
        </>
      )}
      {manualFor && (
        <div style={{ padding: 14, border: '1px dashed var(--line)', borderRadius: 8, display: 'flex', flexDirection: 'column', gap: 10 }}>
          <div className="field-label">Paste access token for {manualFor}</div>
          <input className="field-input" type="password" value={manualToken} onChange={e => setManualToken(e.target.value)} />
          <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
            <button className="mb-btn" onClick={() => { setManualFor(null); setManualToken(''); }}>Cancel</button>
            <button className="mb-btn primary" onClick={saveManual} disabled={!manualToken}>Save</button>
          </div>
        </div>
      )}
      {msg?.err && <div className="field-error">{msg.err}</div>}
    </Modal>
  );
}

// ─── Company Library (admin: manage org files that RAG uses) ───
function CompanyLibraryModal({ onClose }) {
  const [files, setFiles] = useState([]);
  const fileInputRef = useRef(null);
  const reload = useCallback(async () => {
    try { const j = await api('/api/org-files'); setFiles(j.files || []); } catch {}
  }, []);
  useEffect(() => { reload(); }, [reload]);

  async function pick() { fileInputRef.current?.click(); }
  async function onFile(e) {
    const file = e.target.files?.[0]; e.target.value = '';
    if (!file) return;
    const fd = new FormData(); fd.append('file', file);
    try { await api('/api/org-files', { method: 'POST', body: fd }); reload(); }
    catch (err) { alert('Upload failed: ' + (err.code || err.message)); }
  }
  async function remove(id) {
    await api('/api/org-files/' + id, { method: 'DELETE' });
    reload();
  }

  return (
    <Modal title="Company library — RAG on all chats" onClose={onClose} width={620}
      actions={<><button className="mb-btn" onClick={pick}>+ Upload file</button><button className="mb-btn primary" onClick={onClose}>Done</button></>}>
      <input ref={fileInputRef} type="file" hidden accept=".pdf,.docx,.txt,.md,.csv,.json,.html" onChange={onFile} />
      <div className="field-hint">Files here are always-on context for every chat by every user. Use for team playbooks, product docs, brand guidelines, competitor intel, price sheets.</div>
      <div className="keys-list">
        {files.length === 0 ? <div className="field-hint">No files yet.</div> :
          files.map(f => (
            <div className="key-row" key={f.id}>
              <div className="key-row-info">
                <div className="key-row-title">📚 {f.filename} {f.has_text ? <span style={{ color: 'var(--ok)', fontSize: 10 }}>indexed</span> : <span style={{ color: 'var(--warn)', fontSize: 10 }}>not indexed</span>}</div>
                <div className="key-row-meta">{prettyBytes(f.bytes)} · {f.mime || '—'} · {f.tags || ''}</div>
              </div>
              <button className="mb-btn danger" onClick={() => remove(f.id)}>Delete</button>
            </div>
          ))}
      </div>
    </Modal>
  );
}

// ─── Analytics modal (admin) ───
function AnalyticsModal({ onClose }) {
  const [data, setData] = useState(null);
  const [days, setDays] = useState(30);
  useEffect(() => {
    api('/api/analytics?days=' + days).then(setData).catch(() => {});
  }, [days]);
  if (!data) return <Modal title="Analytics" onClose={onClose}><div className="field-hint">Loading…</div></Modal>;
  const maxDay = Math.max(1, ...data.days_series.map(d => d.tokens_out));
  const totalCost = data.models.reduce((a, m) => a + (m.cost_usd || 0), 0);
  return (
    <Modal title="Analytics — token usage" onClose={onClose} width={700}
      actions={<button className="mb-btn primary" onClick={onClose}>Done</button>}>
      <div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
        <div className="field-label">Range</div>
        <select className="mb-select" value={days} onChange={e => setDays(parseInt(e.target.value, 10))}>
          <option value="7">Last 7 days</option>
          <option value="30">Last 30 days</option>
          <option value="90">Last 90 days</option>
          <option value="365">Last year</option>
        </select>
        <span style={{ marginLeft: 'auto', fontFamily: 'var(--f-mono)', fontSize: 12, color: 'var(--ink-mute)' }}>
          {data.counts.users} users · {data.counts.threads} chats · {data.counts.messages} msgs · ${totalCost.toFixed(4)} est.
        </span>
      </div>
      <div className="field-label">Per day (output tokens)</div>
      <div className="chart-bars">
        {data.days_series.length === 0 ? <div className="field-hint">No data.</div> :
          data.days_series.map(d => (
            <div className="chart-bar" key={d.day} title={`${d.day}: ${d.tokens_out} out / ${d.tokens_in} in`}>
              <div className="chart-bar-fill" style={{ height: (d.tokens_out / maxDay * 100) + '%' }}></div>
              <div className="chart-bar-label mono">{d.day.slice(5)}</div>
            </div>
          ))}
      </div>
      <div className="field-label" style={{ marginTop: 8 }}>Per model</div>
      <div className="keys-list">
        {data.models.map(m => (
          <div className="key-row" key={m.model}>
            <div className="key-row-info">
              <div className="key-row-title">{m.model}</div>
              <div className="key-row-meta">{m.msg_count} msgs · in {formatNum(m.tokens_in)} / out {formatNum(m.tokens_out)}</div>
            </div>
            <div style={{ color: 'var(--ink-mute)', fontFamily: 'var(--f-mono)' }}>${m.cost_usd.toFixed(4)}</div>
          </div>
        ))}
      </div>
      <div className="field-label" style={{ marginTop: 8 }}>Per user</div>
      <div className="keys-list">
        {data.users.map(u => (
          <div className="key-row" key={u.email}>
            <div className="key-row-info">
              <div className="key-row-title">{u.display_name || u.email}</div>
              <div className="key-row-meta">{u.msg_count} msgs · in {formatNum(u.tokens_in)} / out {formatNum(u.tokens_out)}</div>
            </div>
          </div>
        ))}
      </div>
    </Modal>
  );
}

function formatNum(n) { if (n == null) return '0'; if (n < 1000) return String(n); if (n < 1e6) return (n / 1000).toFixed(1) + 'k'; return (n / 1e6).toFixed(2) + 'M'; }

// ─── Main App ───
function App() {
  const { me, reload: reloadMe } = useMe();
  const config = useConfig();
  const [projects, setProjects] = useState([]);
  const [threads, setThreads] = useState([]);
  const [activeProject, setActiveProject] = useState(null);    // project detail view
  const [active, setActive] = useState(null);                  // active thread
  const [messages, setMessages] = useState([]);
  const [streaming, setStreaming] = useState(false);
  const [status, setStatus] = useState('idle');
  const [modal, setModal] = useState(null);
  const wsRef = useRef(null);
  const fileInputRef = useRef(null);
  const [pendingAttachments, setPendingAttachments] = useState([]);

  const [skills, setSkills] = useState([]);
  const [composerPrefill, setComposerPrefill] = useState(null);
  const [skillVarDialog, setSkillVarDialog] = useState(null);
  const [toolActivity, setToolActivity] = useState([]);
  const [pendingProposals, setPendingProposals] = useState([]);

  const loadThreads = useCallback(async () => {
    try { const j = await api('/api/threads'); setThreads(j.threads || []); } catch {}
  }, []);
  const loadProjects = useCallback(async () => {
    try { const j = await api('/api/projects'); setProjects(j.projects || []); } catch {}
  }, []);
  const loadSkills = useCallback(async () => {
    try { const j = await api('/api/skills'); setSkills(j.skills || []); } catch {}
  }, []);
  const reloadActiveProject = useCallback(async (slug) => {
    if (!slug) return;
    try {
      const j = await api('/api/projects/' + slug);
      setActiveProject(j.project);
    } catch {}
  }, []);

  useEffect(() => { if (me.user) { loadThreads(); loadProjects(); loadSkills(); } }, [me.user, loadThreads, loadProjects, loadSkills]);

  // Use a skill: if it has {{vars}}, ask for them, then prefill composer.
  async function useSkill(s) {
    const vars = s.variables || extractVars(s.prompt);
    async function run(values) {
      const filled = fillVars(s.prompt, values || {});
      // Ensure a chat exists to receive the prompt.
      let target = active;
      if (!target) {
        const provider = s.default_provider || activeProject?.default_provider || config.providers?.[0]?.id || 'anthropic';
        const model = s.default_model || activeProject?.default_model || config.providers?.[0]?.models?.[0]?.id || 'claude-sonnet-5';
        const j = await api('/api/threads', { method: 'POST', body: JSON.stringify({ title: s.name, provider, model, system_prompt: s.system_prompt || undefined, project_id: activeProject?.id || null }) });
        await loadThreads();
        target = j.thread; setActive(j.thread); setMessages([]);
      }
      setComposerPrefill({ text: filled, nonce: Date.now() });
      api('/api/skills/' + s.id + '/use', { method: 'POST' }).catch(() => {});
      loadSkills();
    }
    if (vars.length === 0) return run({});
    setSkillVarDialog({ skill: s, vars, values: Object.fromEntries(vars.map(v => [v, ''])), run });
  }

  // Filter sidebar chats to project context or unfiled.
  const visibleThreads = useMemo(() => {
    if (activeProject) return threads.filter(t => t.project_id === activeProject.id);
    return threads.filter(t => !t.project_id);
  }, [threads, activeProject]);

  async function selectThread(t) {
    const j = await api('/api/threads/' + t.slug);
    setActive(j.thread);
    setMessages(j.messages || []);
    setPendingAttachments([]);
    // If thread belongs to a project and no project active yet, load it too.
    if (j.thread.project_id && (!activeProject || activeProject.id !== j.thread.project_id)) {
      const p = projects.find(pp => pp.id === j.thread.project_id);
      if (p) reloadActiveProject(p.slug);
    }
  }

  async function selectProject(p) {
    setActive(null); setMessages([]);
    await reloadActiveProject(p.slug);
  }

  function openGlobal() {
    setActiveProject(null); setActive(null); setMessages([]);
  }

  async function newChat() {
    const projectId = activeProject?.id || null;
    const provider = activeProject?.default_provider || config.providers?.[0]?.id || 'anthropic';
    const model = activeProject?.default_model || config.providers?.[0]?.models?.[0]?.id || 'claude-sonnet-5';
    const j = await api('/api/threads', {
      method: 'POST',
      body: JSON.stringify({ title: 'New chat', provider, model, project_id: projectId })
    });
    await loadThreads();
    if (projectId) await reloadActiveProject(activeProject.slug);
    setActive(j.thread);
    setMessages([]);
    setPendingAttachments([]);
  }

  async function changeProvider(providerId) {
    if (!active) return;
    const p = config.providers.find(pp => pp.id === providerId);
    const newModel = p?.models?.[0]?.id || 'default';
    const j = await api('/api/threads/' + active.id, { method: 'PATCH', body: JSON.stringify({ provider: providerId, model: newModel }) });
    setActive(j.thread);
  }
  async function changeModel(modelId) {
    if (!active) return;
    const j = await api('/api/threads/' + active.id, { method: 'PATCH', body: JSON.stringify({ model: modelId }) });
    setActive(j.thread);
  }

  function ensureWS() {
    if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) return wsRef.current;
    const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
    const ws = new WebSocket(`${proto}//${location.host}/ws/chat`);
    wsRef.current = ws;
    ws.onopen = () => setStatus('ready');
    ws.onclose = () => setStatus('idle');
    ws.onerror = () => setStatus('error');
    return ws;
  }

  async function sendMessage(content) {
    if (!active) return;
    // Optimistic: push user + placeholder assistant to messages.
    setMessages(m => [...m, { id: 'u-' + Date.now(), role: 'user', content }]);
    setStreaming(true);
    const ws = ensureWS();
    const openAndSend = () => {
      const asstIdx = messages.length + 1;
      setMessages(m => [...m, { id: 'a-live', role: 'assistant', content: '', model: active.model, streaming: true }]);
      ws.send(JSON.stringify({ type: 'send', thread_slug: active.slug, content, provider: active.provider, model: active.model }));
    };
    if (ws.readyState === WebSocket.OPEN) openAndSend(); else ws.addEventListener('open', openAndSend, { once: true });

    ws.onmessage = (ev) => {
      let msg; try { msg = JSON.parse(ev.data); } catch { return; }
      if (msg.type === 'delta') {
        setMessages(mm => mm.map((m, i) => i === mm.length - 1 && m.streaming ? { ...m, content: m.content + msg.text } : m));
      } else if (msg.type === 'assistant_start') {
        setToolActivity([]);
      } else if (msg.type === 'context') {
        setToolActivity(a => [...a, { kind: 'rag', label: `retrieved ${msg.hits.length} passage(s)`, detail: msg.hits.map(h => h.filename).join(', ') }]);
      } else if (msg.type === 'tool_start') {
        setToolActivity(a => [...a, { kind: 'tool', name: msg.name, running: true, detail: JSON.stringify(msg.input).slice(0, 120) }]);
      } else if (msg.type === 'tool_result') {
        setToolActivity(a => a.map(x => x.name === msg.name && x.running ? { ...x, running: false, ok: msg.ok, label: msg.summary, ms: msg.ms } : x));
      } else if (msg.type === 'proposals') {
        setPendingProposals(msg.proposals || []);
      } else if (msg.type === 'done') {
        setMessages(mm => mm.map((m, i) => i === mm.length - 1 && m.streaming ? { ...m, streaming: false, id: msg.message_id } : m));
        setStreaming(false);
        setPendingAttachments([]);
        loadThreads();
      } else if (msg.type === 'error') {
        setMessages(mm => {
          const last = mm[mm.length - 1];
          const err = `\n\n> **Error:** ${msg.reason || msg.code}${msg.detail ? ` — ${msg.detail}` : ''}`;
          if (last && last.streaming) {
            return mm.map((m, i) => i === mm.length - 1 ? { ...m, content: m.content + err, streaming: false } : m);
          }
          return [...mm, { id: 'e-' + Date.now(), role: 'assistant', content: err, streaming: false }];
        });
        setStreaming(false);
      } else if (msg.type === 'cancelled') {
        setMessages(mm => mm.map((m, i) => i === mm.length - 1 && m.streaming ? { ...m, streaming: false } : m));
        setStreaming(false);
      }
    };
  }

  function cancelStream() {
    if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
      wsRef.current.send(JSON.stringify({ type: 'cancel' }));
    }
  }

  async function pickFile() { fileInputRef.current?.click(); }
  async function onFile(e) {
    const file = e.target.files?.[0];
    e.target.value = '';
    if (!file || !active) return;
    const fd = new FormData();
    fd.append('file', file);
    try {
      const j = await api('/api/threads/' + active.slug + '/attachments', { method: 'POST', body: fd });
      setPendingAttachments(a => [...a, j.attachment]);
      // reload thread so extracted-text status is picked up next message
      const t = await api('/api/threads/' + active.slug);
      setActive(t.thread);
    } catch (err) {
      alert('Upload failed: ' + (err.code || err.message));
    }
  }

  async function logout() {
    await api('/api/auth/logout', { method: 'POST' });
    setActive(null); setMessages([]); reloadMe();
  }

  const [confirmDialog, setConfirmDialog] = useState(null); // { title, body, confirmLabel, danger, onConfirm }
  const [toast, setToast] = useState(null);
  function flashToast(text, kind = 'err') {
    setToast({ text, kind });
    setTimeout(() => setToast(null), 4000);
  }

  const [approving, setApproving] = useState(false);
  async function approveProposal(p, idx) {
    setApproving(true);
    try {
      const r = await api('/api/tools/approve', {
        method: 'POST',
        body: JSON.stringify({ action: p.action, payload: p.preview })
      });
      if (r.ok) {
        flashToast(p.action === 'send_email' ? 'Email sent.' : 'Posted to Slack.', 'ok');
        setPendingProposals(list => list.filter((_, j) => j !== idx));
      } else {
        flashToast('Failed: ' + (r.error || 'unknown'));
      }
    } catch (e) {
      flashToast('Failed: ' + (e.code || e.message));
    } finally { setApproving(false); }
  }

  async function renameThread(t, newTitle) {
    let name = newTitle;
    if (typeof name !== 'string') return; // triggered from inline edit only
    name = name.trim().slice(0, 120);
    if (!name || name === t.title) return;
    try {
      const j = await api('/api/threads/' + t.id, { method: 'PATCH', body: JSON.stringify({ title: name }) });
      await loadThreads();
      if (active?.id === t.id) setActive(j.thread);
      if (activeProject) await reloadActiveProject(activeProject.slug);
    } catch (e) { flashToast('Rename failed: ' + (e.code || e.message)); }
  }
  function renameActive() {
    if (!active) return;
    // Kick sidebar row into edit mode by opening a small inline prompt via ConfirmDialog
    setConfirmDialog({
      title: 'Rename chat',
      body: 'Enter a new name for this chat.',
      prompt: active.title,
      confirmLabel: 'Rename',
      onConfirm: async (value) => {
        const name = String(value || '').trim().slice(0, 120);
        setConfirmDialog(null);
        if (!name || name === active.title) return;
        try {
          const j = await api('/api/threads/' + active.id, { method: 'PATCH', body: JSON.stringify({ title: name }) });
          await loadThreads();
          setActive(j.thread);
          if (activeProject) await reloadActiveProject(activeProject.slug);
        } catch (e) { flashToast('Rename failed: ' + (e.code || e.message)); }
      }
    });
  }
  function deleteThread(t) {
    setConfirmDialog({
      title: 'Delete chat',
      body: `Delete chat "${t.title}"? This cannot be undone.`,
      confirmLabel: 'Delete',
      danger: true,
      onConfirm: async () => {
        setConfirmDialog(null);
        try {
          await api('/api/threads/' + t.id, { method: 'DELETE' });
          if (active?.id === t.id) { setActive(null); setMessages([]); }
          await loadThreads();
          if (activeProject) await reloadActiveProject(activeProject.slug);
        } catch (e) { flashToast('Delete failed: ' + (e.code || e.message)); }
      }
    });
  }
  async function moveThreadToProject(t, projectId) {
    try {
      const j = await api('/api/threads/' + t.id + '/project', { method: 'PATCH', body: JSON.stringify({ project_id: projectId }) });
      await loadThreads();
      if (active?.id === t.id) setActive(j.thread);
      if (activeProject) await reloadActiveProject(activeProject.slug);
      if (projectId) {
        const p = projects.find(pp => pp.id === projectId);
        if (p) await reloadActiveProject(p.slug);
      }
    } catch (e) { flashToast('Move failed: ' + (e.code || e.message)); }
  }

  if (me.loading) return <div className="auth-screen"><div className="auth-card"><h1>Loading…</h1></div></div>;
  if (!me.user) return <AuthScreen config={config} onAuth={reloadMe} />;

  const activeProviderMeta = config.providers.find(p => p.id === active?.provider);
  const availableModels = activeProviderMeta?.models || [];

  const mainTitle = active ? active.title : (activeProject ? activeProject.name : 'Coachinside AI Hub');

  return (
    <div className="app">
      <Sidebar user={me.user} projects={projects} threads={visibleThreads} skills={skills}
        activeThreadSlug={active?.slug} activeProjectSlug={activeProject?.slug}
        onSelectThread={selectThread} onSelectProject={selectProject}
        onRenameThread={renameThread} onDeleteThread={deleteThread} onMoveThread={moveThreadToProject}
        onOpenGlobal={openGlobal} onNewChat={newChat} onNewProject={() => setModal('newProject')}
        onSkills={() => setModal('skills')} onUseSkill={useSkill}
        onSettings={() => setModal('settings')} onLogout={logout}
        onReload={() => { loadThreads(); loadProjects(); loadSkills(); if (activeProject) reloadActiveProject(activeProject.slug); }} />
      <main className="main">
        <div className="mainbar">
          <div className="mb-title" onDoubleClick={active ? renameActive : undefined} title={active ? 'Double-click to rename' : ''} style={active ? { cursor: 'text' } : null}>
            {activeProject && !active && <span style={{ color: 'var(--ink-mute)', marginRight: 6 }}>{activeProject.icon || '📁'}</span>}
            {activeProject && active && <span style={{ color: 'var(--ink-dim)', marginRight: 6, fontSize: 12 }}>{activeProject.name} /</span>}
            {mainTitle}
            {active && <button className="rename-btn" onClick={renameActive} title="Rename chat">✎</button>}
          </div>
          <div className="mb-controls">
            {active && <>
              <select className="mb-select" value={active.provider} onChange={e => changeProvider(e.target.value)}>
                {config.providers.map(p => <option key={p.id} value={p.id}>{p.label}</option>)}
              </select>
              <select className="mb-select" value={active.model} onChange={e => changeModel(e.target.value)}>
                {availableModels.map(m => <option key={m.id} value={m.id}>{m.label}</option>)}
              </select>
            </>}
            {active && (
              <a className="mb-btn" href={`/api/threads/${active.id}/export.md`} download title="Export chat as Markdown">⤓ Export</a>
            )}
            {activeProject && !active && (
              <button className="mb-btn" onClick={() => setModal('projectSettings')}>⚙︎ Project settings</button>
            )}
            <button className="mb-btn" onClick={() => setModal('plugins')} title="Plugins — add abilities">🧩</button>
            <button className="mb-btn" onClick={() => setModal('connectors')} title="Connectors">🔌</button>
            {me.user.role === 'admin' && <button className="mb-btn" onClick={() => setModal('library')} title="Company library">📚</button>}
            {me.user.role === 'admin' && <button className="mb-btn" onClick={() => setModal('analytics')} title="Analytics">📊</button>}
            <button className="mb-btn" onClick={() => setModal('settings')}>Settings</button>
          </div>
        </div>
        <div className="chat-pane">
          {active ? (
            <>
              <ChatMessages messages={messages} streaming={streaming} />
              <ToolActivity items={toolActivity} />
              <ProposalCards proposals={pendingProposals} busy={approving}
                onApprove={approveProposal}
                onDismiss={(i) => setPendingProposals(list => list.filter((_, j) => j !== i))} />
              <Composer onSend={sendMessage} onCancel={cancelStream} onAttach={pickFile} streaming={streaming}
                prefill={composerPrefill} pendingAttachments={pendingAttachments}
                onRemoveAttachment={(a) => setPendingAttachments(list => list.filter(x => x.id !== a.id))} />
              <input ref={fileInputRef} type="file" hidden accept=".pdf,.docx,.txt,.md,.csv,.json,image/*" onChange={onFile} />
            </>
          ) : activeProject ? (
            <ProjectDetail
              project={activeProject}
              onOpenChat={selectThread}
              onNewChatInProject={newChat}
              onSettings={() => setModal('projectSettings')} />
          ) : (
            <div className="chat-empty">
              <div className="chat-empty-logo">C</div>
              <h2>Welcome, {me.user.display_name || me.user.email.split('@')[0]}</h2>
              <p>Start a <b>+ New chat</b> for something one-off. Or create a <b>Project</b> to bundle related chats with shared instructions and files.</p>
            </div>
          )}
        </div>
      </main>

      {modal === 'settings' && <SettingsModal user={me.user} providers={config.providers} onClose={() => setModal(null)} />}
      {modal === 'newProject' && <NewProjectModal providers={config.providers} onClose={() => setModal(null)}
        onCreated={async (p) => { setModal(null); await loadProjects(); await reloadActiveProject(p.slug); }} />}
      {modal === 'projectSettings' && activeProject && <ProjectSettingsModal
        project={activeProject} providers={config.providers} currentUser={me.user}
        onClose={() => setModal(null)}
        onChange={(p) => { setActiveProject({ ...activeProject, ...p }); loadProjects(); }}
        confirm={setConfirmDialog}
        onDeleted={async () => { setModal(null); setActiveProject(null); await loadProjects(); await loadThreads(); }} />}
      {modal === 'skills' && <SkillsModal user={me.user} providers={config.providers}
        onClose={() => { setModal(null); loadSkills(); }} onUse={useSkill} />}
      {modal === 'plugins' && <PluginsModal user={me.user} onClose={() => setModal(null)} />}
      {modal === 'connectors' && <ConnectorsModal onClose={() => setModal(null)} />}
      {modal === 'library' && <CompanyLibraryModal onClose={() => setModal(null)} />}
      {modal === 'analytics' && <AnalyticsModal onClose={() => setModal(null)} />}
      {skillVarDialog && (
        <Modal title={`Run skill — ${skillVarDialog.skill.name}`} onClose={() => setSkillVarDialog(null)} width={520}
          actions={<>
            <button className="mb-btn" onClick={() => setSkillVarDialog(null)}>Cancel</button>
            <button className="mb-btn primary" onClick={() => { const d = skillVarDialog; setSkillVarDialog(null); d.run(d.values); }}>Insert prompt</button>
          </>}>
          <div className="field-hint">Fill the placeholders. The finished prompt lands in the composer — review, then hit Send.</div>
          {skillVarDialog.vars.map(v => (
            <div className="field" key={v}>
              <div className="field-label">{v}</div>
              <input className="field-input" value={skillVarDialog.values[v] || ''}
                onChange={e => setSkillVarDialog(d => ({ ...d, values: { ...d.values, [v]: e.target.value } }))} />
            </div>
          ))}
        </Modal>
      )}
      {confirmDialog && <ConfirmDialog dialog={confirmDialog} onClose={() => setConfirmDialog(null)} />}
      {toast && <Toast toast={toast} onClose={() => setToast(null)} />}
    </div>
  );
}

function initials(user) {
  const src = user.display_name || user.email || '?';
  return src.split(/[\s@.]/).filter(Boolean).slice(0, 2).map(s => s[0]?.toUpperCase()).join('') || '?';
}

function errorLabel(code, meta) {
  const map = {
    invalid_credentials: 'Invalid email or password.',
    email_taken: 'Email already registered.',
    bad_email: 'Email format invalid.',
    password_too_short: 'Password must be at least 8 characters.',
    email_domain_not_allowed: `Only these email domains can register: ${(meta?.allowed || []).join(', ')}`,
    auth_required: 'Sign in required.',
    thread_not_found: 'Chat not found.',
    forbidden: 'You do not have permission for that.',
    no_key_configured: 'No API key configured for that provider. Open Settings to add one.',
    unknown_provider: 'Unknown provider.',
    key_required: 'API key required for this provider.',
    stream_failed: 'Streaming failed. Check API key and endpoint.',
    empty_message: 'Empty message.',
    name_required: 'Name required.',
    user_not_found: 'That user does not exist in the system.',
    project_forbidden: "You don't have access to that project.",
    owner_only: 'Only the owner can do that.',
    bad_role: 'Invalid role.'
  };
  return map[code] || `Something went wrong (${code}).`;
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
