/* global React */
const { useState, useMemo, useEffect, useRef } = React;

// ─── Helpers ─────────────────────────────────────────────────────────────────
function hashId(id) {
  let h = 0;
  for (let i = 0; i < id.length; i++) h = (Math.imul(h, 31) + id.charCodeAt(i)) | 0;
  return Math.abs(h);
}

const KIND_COLOR = {
  core:     'var(--c-core)',
  agent:    'var(--c-agent)',
  skill:    'var(--c-skill)',
  workflow: 'var(--c-workflow)',
};
const STATUS_COLOR = { ok:'var(--c-ok)', warn:'var(--c-warn)', err:'var(--c-error)', off:'var(--c-off)' };

// ─── Layout ───────────────────────────────────────────────────────────────────
function buildNeighbors(edges) {
  const m = new Map();
  edges.forEach(e => {
    if (!m.has(e.source)) m.set(e.source, new Set());
    if (!m.has(e.target)) m.set(e.target, new Set());
    m.get(e.source).add(e.target);
    m.get(e.target).add(e.source);
  });
  return m;
}

function useLayout(nodes, edges, kind, size, seed) {
  return useMemo(() => {
    const opts = { width: size.w, height: size.h, seed };
    if (kind === 'radial') return window.NeuraLayout.radialLayout(nodes, edges, opts);
    if (kind === 'hier')   return window.NeuraLayout.hierarchicalLayout(nodes, edges, opts);
    return window.NeuraLayout.forceLayout(nodes, edges, opts);
  }, [nodes, edges, kind, size.w, size.h, seed]);
}

// ─── Login Screen ─────────────────────────────────────────────────────────────
function LoginScreen({ onLogin }) {
  const [password, setPassword] = useState('');
  const [err,      setErr]      = useState('');
  const [busy,     setBusy]     = useState(false);

  const login = async () => {
    if (!password.trim()) { setErr('Digite a senha de acesso.'); return; }
    setBusy(true); setErr('');
    try {
      const r = await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ password }),
      });
      const data = await r.json().catch(() => ({}));
      if (r.ok && data.token) {
        localStorage.setItem('neura-token', data.token);
        // Buscar dados do usuário
        const me = await fetch('/api/auth/me', {
          headers: { Authorization: `Bearer ${data.token}` },
        }).then(r2 => r2.json()).catch(() => ({ name: 'admin', agents: '*' }));
        onLogin(me);
      } else {
        setErr(data.error || 'Senha incorreta.');
        setPassword('');
      }
    } catch { setErr('Não foi possível conectar ao servidor.'); }
    finally   { setBusy(false); }
  };

  return (
    <div className="nl-overlay">
      <div className="nl-box">
        <div className="nl-logo">NEURA</div>
        <div className="nl-sub">editor de skills · agentes · n8n</div>
        <div className="nl-field">
          <input
            autoFocus
            className="nl-input"
            type="password"
            placeholder="Senha de acesso…"
            value={password}
            onChange={e => setPassword(e.target.value)}
            onKeyDown={e => e.key === 'Enter' && login()}
          />
        </div>
        {err && <div className="nl-err">{err}</div>}
        <button className="na-btn primary nl-btn" onClick={login} disabled={busy}>
          {busy ? 'Verificando…' : 'Entrar'}
        </button>
      </div>
    </div>
  );
}

// ─── History Panel (KAN-509) ──────────────────────────────────────────────────
function HistoryPanel({ onClose, authToken, agents }) {
  const [entries,     setEntries]     = useState([]);
  const [loading,     setLoading]     = useState(true);
  const [agentFilter, setAgentFilter] = useState('');
  const [err,         setErr]         = useState('');

  const load = (agent) => {
    setLoading(true); setErr('');
    const url = `/api/history?limit=60${agent ? `&agent=${encodeURIComponent(agent)}` : ''}`;
    fetch(url, { headers: authToken ? { Authorization: `Bearer ${authToken}` } : {} })
      .then(r => r.json())
      .then(d => { if (d.entries) setEntries(d.entries); else setErr(d.error || 'Erro'); })
      .catch(e => setErr(e.message))
      .finally(() => setLoading(false));
  };

  useEffect(() => { load(''); }, []);

  const onFilterChange = (agent) => {
    setAgentFilter(agent);
    load(agent);
  };

  const shortMsg = msg => {
    const m = msg.replace(/^neura:\s*/i, '');
    return m.length > 60 ? m.slice(0, 60) + '…' : m;
  };

  return (
    <div className="nh-overlay" onClick={e => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="nh-panel">
        <div className="nh-head">
          <span className="nh-title">Histórico de Edições</span>
          <div style={{ display:'flex', gap:8, alignItems:'center' }}>
            <select className="ns-select" style={{ padding:'4px 8px', fontSize:11 }}
              value={agentFilter} onChange={e => onFilterChange(e.target.value)}>
              <option value="">Todos os agentes</option>
              {agents.map(a => <option key={a.name} value={a.name}>{a.name}</option>)}
            </select>
            <button className="nc-icon-btn" onClick={onClose}>
              <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M18 6 6 18M6 6l12 12"/></svg>
            </button>
          </div>
        </div>

        <div className="nh-body">
          {loading && <div className="insp-loading" style={{ padding:20 }}>carregando histórico…</div>}
          {err     && <div className="nl-err" style={{ padding:16 }}>{err}</div>}
          {!loading && !err && entries.length === 0 && (
            <div style={{ padding:20, color:'var(--ink-mute)', fontFamily:'var(--font-mono)', fontSize:12, textAlign:'center' }}>
              Nenhuma edição encontrada.
            </div>
          )}
          {!loading && entries.map((e, i) => (
            <div key={i} className="nh-entry">
              <div className="nh-entry-date">{e.date}</div>
              <div className="nh-entry-msg">{shortMsg(e.message)}</div>
              <div className="nh-entry-meta">
                <span className="nh-author">{e.author}</span>
                <span className="nh-hash" title={e.hash}>{e.hash?.slice(0,7)}</span>
              </div>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

// ─── Header ───────────────────────────────────────────────────────────────────
function Header({ query, setQuery, onReorganize, onOpenChat, onOpenHistory, onOpenMonitor, currentUser }) {
  const logout = () => {
    localStorage.removeItem('neura-token');
    window.location.reload();
  };

  return (
    <header className="na-header">
      <div className="na-brand">
        <div className="na-logo">NEURA</div>
        <span className="na-sub">editor de skills · agentes · n8n</span>
      </div>
      <div className="na-search">
        <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
          <circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/>
        </svg>
        <input placeholder="busca — nome, skill, agente…" value={query} onChange={e => setQuery(e.target.value)} />
        <span className="na-kbd">⌘K</span>
      </div>
      <div className="na-actions">
        <button className="na-btn" onClick={onOpenHistory} title="Ver histórico de edições">Histórico</button>
        <button className="na-btn" onClick={onReorganize}>Reorganizar</button>
        <button className="na-btn" onClick={onOpenMonitor}>monitor</button>
        <button className="na-btn primary" onClick={onOpenChat}>chat n8n</button>
        {currentUser && !currentUser.dev && (
          <button className="nc-icon-btn" onClick={logout} title="Sair">
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
              <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
              <polyline points="16 17 21 12 16 7"/>
              <line x1="21" y1="12" x2="9" y2="12"/>
            </svg>
          </button>
        )}
      </div>
    </header>
  );
}

// ─── Chat N8n ─────────────────────────────────────────────────────────────────
function genUserId() {
  try {
    const arr = new Uint8Array(8);
    crypto.getRandomValues(arr);
    return 'u-' + Array.from(arr).map(b => b.toString(16).padStart(2,'0')).join('');
  } catch {
    return 'u-' + Math.random().toString(36).slice(2,10) + Math.random().toString(36).slice(2,10);
  }
}

function renderMsgText(text) {
  const parts = text.split(/(```[\s\S]*?```)/g);
  return parts.map((part, i) => {
    if (part.startsWith('```')) {
      const code = part.replace(/^```\w*\n?/, '').replace(/\n?```$/, '');
      return <pre key={i} className="nc-code-block">{code}</pre>;
    }
    return <span key={i} style={{ whiteSpace: 'pre-wrap' }}>{part}</span>;
  });
}

function ChatN8n({ onClose }) {
  const [cfg, setCfg] = useState(() => {
    try { return JSON.parse(localStorage.getItem('neura-n8n-cfg') || '{}'); } catch { return {}; }
  });
  const [webhook, setWebhook] = useState(cfg.webhook || '');
  const [apiKey,  setApiKey]  = useState(cfg.apiKey  || '');
  const [showCfg, setShowCfg] = useState(!cfg.webhook);

  const [userId] = useState(() => {
    let id = localStorage.getItem('neura-n8n-uid');
    if (!id) { id = genUserId(); localStorage.setItem('neura-n8n-uid', id); }
    return id;
  });

  const [msgs,        setMsgs]        = useState(() => {
    try { return JSON.parse(localStorage.getItem('neura-n8n-hist') || '[]'); } catch { return []; }
  });
  const [input,       setInput]       = useState('');
  const [attachments, setAttachments] = useState([]);
  const [sending,     setSending]     = useState(false);
  const [fileKey,     setFileKey]     = useState(0); // força remount do input file após cada envio

  const bottomRef  = useRef(null);
  const fileRef    = useRef(null);
  const textaRef   = useRef(null);

  useEffect(() => {
    try { localStorage.setItem('neura-n8n-hist', JSON.stringify(msgs.slice(-200))); } catch {}
  }, [msgs]);

  useEffect(() => {
    bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [msgs, sending]);

  const autoResize = () => {
    const el = textaRef.current;
    if (!el) return;
    el.style.height = 'auto';
    el.style.height = Math.min(140, el.scrollHeight) + 'px';
  };

  const saveCfg = () => {
    const c = { webhook: webhook.trim(), apiKey: apiKey.trim() };
    setCfg(c);
    localStorage.setItem('neura-n8n-cfg', JSON.stringify(c));
    setShowCfg(false);
  };

  const newSession = () => {
    if (!window.confirm('Iniciar nova sessão? O histórico local será apagado.')) return;
    localStorage.removeItem('neura-n8n-uid');
    localStorage.removeItem('neura-n8n-hist');
    window.location.reload();
  };

  const send = async () => {
    const text = input.trim();
    // Captura snapshot dos anexos ANTES de qualquer setState (evita closure stale)
    const currentFiles = attachments.slice();
    if (!text && currentFiles.length === 0) return;
    if (!cfg.webhook) { setShowCfg(true); return; }

    // Garante mensagem não-vazia (workflow n8n exige 'message')
    const messageText = text || (currentFiles.length > 0 ? `Arquivo em anexo: ${currentFiles[0].name}` : '');

    // Ler arquivo como base64 ANTES de limpar estado (evita bug no segundo envio)
    let attachment = null;
    if (currentFiles.length > 0) {
      const file = currentFiles[0];
      try {
        const b64 = await new Promise((resolve, reject) => {
          const r = new FileReader();
          r.onload  = () => resolve(r.result.split(',')[1]);
          r.onerror = () => reject(new Error('Erro ao ler o arquivo'));
          r.readAsDataURL(file);
        });
        attachment = { name: file.name, type: file.type, size: file.size, data: b64 };
      } catch (readErr) {
        // leitura falhou — envia mensagem sem o arquivo
        console.warn('[neura] FileReader error:', readErr.message);
      }
    }

    // Só agora atualiza a UI (limpa input, anexos, ativa spinner)
    const msgId = Date.now().toString(36) + Math.random().toString(36).slice(2,5);
    setMsgs(m => [...m, {
      id: msgId,
      role: 'user',
      text,
      files: currentFiles.map(f => ({ name: f.name, size: f.size, type: f.type })),
      ts: Date.now(),
    }]);
    setInput('');
    setAttachments([]);
    if (fileRef.current) fileRef.current.value = ''; // garante reset do input file
    setSending(true);
    if (textaRef.current) { textaRef.current.style.height = 'auto'; }

    try {
      // Envia via server.js (/api/agent/send): ele assina o HMAC com o
      // WEBHOOK_HMAC_SECRET do .env e encaminha pro webhook do n8n.
      const t0 = performance.now();
      const res = await fetch('/api/agent/send', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          webhook: cfg.webhook,
          apiKey: cfg.apiKey || '',
          message: messageText,
          session_id: userId,
          ...(attachment ? { attachment } : {}),
        }),
      });
      // server.js envolve a resposta: { ok, status, took_ms, data: <n8n response> }
      const env = await res.json().catch(() => ({}));
      const took = env.took_ms != null ? env.took_ms : Math.round(performance.now() - t0);
      // desembrulha: data é o JSON que o n8n respondeu
      const j = env.data;
      const obj = Array.isArray(j) ? j[0] : (j || {});

      let reply = '';
      let meta = null;

      const found = obj?.response
                 ?? obj?.output
                 ?? obj?.message
                 ?? obj?.text
                 ?? obj?.answer
                 ?? obj?.content
                 ?? obj?.reply
                 ?? obj?.result
                 ?? obj?.raw;

      reply = (found !== undefined && found !== null)
        ? String(found)
        : JSON.stringify(j ?? env, null, 2);

      // Remove scratchpad do LangChain que vaza no output
      reply = reply.replace(/<tool_call>[\s\S]*?<\/tool_call>/gi, '').trim();
      reply = reply.replace(/<tool_response>[\s\S]*?<\/tool_response>/gi, '').trim();
      reply = reply.replace(/\n{3,}/g, '\n\n').trim();

      // Metadados do workflow Neura
      const ms = obj?.response_time_ms ?? took;
      if (obj?.active_skill || obj?.history_count !== undefined || obj?.agent_id) {
        meta = {
          ms,
          skill:   obj.active_skill          || null,
          agente:  obj.agent_id              || null,
          hist:    obj.history_count         ?? null,
          loaded:  obj.neura_skills_loaded   ?? null,
        };
      }

      if (env.ok === false) {
        reply = '⚠ HTTP ' + (env.status || '?') + (obj?.error ? ' · ' + obj.error : '') + '\n\n' + reply;
      }
      if (!reply || reply.trim() === '') {
        reply = '⚠ O webhook respondeu sem conteúdo.';
      }

      const doc = typeof extractDoc === 'function' ? extractDoc(reply) : null;
      setMsgs(m => [...m, { id: msgId + 'r', role: 'assistant', text: reply, meta, doc, ts: Date.now() }]);
    } catch (e) {
      setMsgs(m => [...m, { id: msgId + 'e', role: 'assistant', text: '⚠ ' + e.message, ts: Date.now(), err: true }]);
    } finally {
      setSending(false);
      setFileKey(k => k + 1); // remonta o input file — garante que aceita novo arquivo
    }
  };

  const onKeyDown = e => {
    if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); }
  };

  const onFileChange = e => {
    setAttachments(prev => [...prev, ...Array.from(e.target.files)]);
    e.target.value = '';
  };

  const removeFile = i => setAttachments(prev => prev.filter((_, idx) => idx !== i));
  const fmtTs = ts => new Date(ts).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });
  const fmtSize = s => s < 1024 ? s + 'B' : s < 1048576 ? (s/1024).toFixed(0) + 'KB' : (s/1048576).toFixed(1) + 'MB';

  return (
    <div className="nc-overlay" onClick={e => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="nc-panel">

        {/* ── Header ── */}
        <div className="nc-header">
          <div className="nc-header-left">
            <span className="nc-logo">CHAT · N8N</span>
            <span className="nc-session" title={userId}>
              <span className="nc-dot" style={{ background: cfg.webhook ? 'var(--c-ok)' : 'var(--c-off)' }}/>
              {userId.slice(0, 12)}…
            </span>
          </div>
          <div className="nc-header-actions">
            <button className="nc-icon-btn" title="Configurações" onClick={() => setShowCfg(v => !v)}>
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
                <circle cx="12" cy="12" r="3"/>
                <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
              </svg>
            </button>
            <button className="nc-icon-btn" title="Nova sessão" onClick={newSession}>
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
                <polyline points="1 4 1 10 7 10"/>
                <path d="M3.51 15a9 9 0 1 0 .49-4.95"/>
              </svg>
            </button>
            <button className="nc-icon-btn nc-close-btn" title="Fechar" onClick={onClose}>
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
                <path d="M18 6 6 18M6 6l12 12"/>
              </svg>
            </button>
          </div>
        </div>

        {/* ── Config panel ── */}
        {showCfg && (
          <div className="nc-cfg">
            <div className="nc-cfg-grid">
              <div className="nc-cfg-row">
                <label className="nc-label">Webhook URL</label>
                <input
                  className="nc-input"
                  placeholder="https://seu-n8n.app/webhook/…"
                  value={webhook}
                  onChange={e => setWebhook(e.target.value)}
                  onKeyDown={e => e.key === 'Enter' && saveCfg()}
                />
              </div>
              <div className="nc-cfg-row">
                <label className="nc-label">API Key</label>
                <input
                  className="nc-input"
                  type="password"
                  placeholder="Bearer token (opcional)"
                  value={apiKey}
                  onChange={e => setApiKey(e.target.value)}
                  onKeyDown={e => e.key === 'Enter' && saveCfg()}
                />
              </div>
            </div>
            <div className="nc-cfg-footer">
              <code className="nc-session-code">session: {userId}</code>
              <button className="na-btn primary" onClick={saveCfg} disabled={!webhook.trim()}>
                Salvar
              </button>
            </div>
          </div>
        )}

        {/* ── Messages ── */}
        <div className="nc-messages">
          {msgs.length === 0 && !sending && (
            <div className="nc-empty">
              <div className="nc-empty-glyph">◎</div>
              <div>
                {cfg.webhook
                  ? 'Nenhuma mensagem ainda.\nDigite algo para começar.\n\n📎 Use o clipe para enviar PDFs, documentos ou imagens.\nO n8n extrai o texto e injeta no contexto da IA.'
                  : 'Configure o webhook para começar.\nClique no ícone ⚙ no canto superior direito.'}
              </div>
            </div>
          )}

          {msgs.map(m => (
            <div key={m.id} className={`nc-msg nc-msg--${m.role}${m.err ? ' nc-msg--err' : ''}`}>
              <div className="nc-msg-bubble">
                {m.text && <div className="nc-msg-text">{renderMsgText(m.text)}</div>}
                {m.meta && (
                  <div className="nc-msg-meta">
                    {m.meta.skill  && <span className="nc-meta-chip nc-meta-skill">⬡ {m.meta.skill}</span>}
                    {m.meta.agente && <span className="nc-meta-chip">agent: {m.meta.agente}</span>}
                    {m.meta.hist   != null && <span className="nc-meta-chip">hist: {m.meta.hist}</span>}
                    {m.meta.ms     != null && <span className="nc-meta-chip">{m.meta.ms}ms</span>}
                  </div>
                )}
                {m.files && m.files.length > 0 && (
                  <div className="nc-msg-files">
                    {m.files.map((f, i) => (
                      <span key={i} className="nc-file-chip">
                        <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
                          <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
                          <polyline points="14 2 14 8 20 8"/>
                        </svg>
                        {f.name}
                        {f.size && <span className="nc-file-size">{fmtSize(f.size)}</span>}
                      </span>
                    ))}
                  </div>
                )}
              </div>
              <div className="nc-msg-ts">{fmtTs(m.ts)}</div>
            </div>
          ))}

          {sending && (
            <div className="nc-msg nc-msg--assistant">
              <div className="nc-msg-bubble nc-typing">
                <span/><span/><span/>
              </div>
            </div>
          )}
          <div ref={bottomRef}/>
        </div>

        {/* ── Attachments preview ── */}
        {attachments.length > 0 && (
          <div className="nc-attachments">
            {attachments.map((f, i) => (
              <div key={i} className="nc-att-item">
                <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
                  <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
                  <polyline points="14 2 14 8 20 8"/>
                </svg>
                <span>{f.name}</span>
                <span className="nc-att-size">{fmtSize(f.size)}</span>
                <button onClick={() => removeFile(i)}>×</button>
              </div>
            ))}
          </div>
        )}

        {/* ── Input bar ── */}
        <div className="nc-input-bar">
          <input type="file" key={fileKey} ref={fileRef} style={{ display: 'none' }} multiple
            accept=".pdf,.txt,.docx,.md,.jpg,.jpeg,.png"
            onChange={onFileChange}/>
          <button className="nc-icon-btn" title="Anexar PDF, imagem ou documento — o n8n extrai o texto e injeta no contexto da IA" onClick={() => fileRef.current?.click()}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
              <path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48"/>
            </svg>
          </button>
          <textarea
            ref={textaRef}
            className="nc-textarea"
            placeholder={cfg.webhook ? 'Mensagem… (Enter envia · Shift+Enter nova linha)' : 'Configure o webhook primeiro…'}
            value={input}
            onChange={e => { setInput(e.target.value); autoResize(); }}
            onKeyDown={onKeyDown}
            disabled={sending}
            rows={1}
          />
          <button
            className="nc-send"
            onClick={send}
            disabled={sending || (!input.trim() && attachments.length === 0)}
            title="Enviar"
          >
            {sending
              ? <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" style={{ animation: 'spin 1s linear infinite' }}><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
              : <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
            }
          </button>
        </div>

      </div>
    </div>
  );
}

// ─── Monitor (saúde · métricas · logs) ───────────────────────────────────────
function MetricCard({ k, v }) {
  return (
    <div className="nm-card">
      <div className="nm-v">{v == null ? '-' : v}</div>
      <div className="nm-k">{k}</div>
    </div>
  );
}

function MonitorPanel({ onClose }) {
  const [agent, setAgent]     = useState(() => localStorage.getItem('neura-mon-agent') || 'agente');
  const [health, setHealth]   = useState(null);
  const [metrics, setMetrics] = useState(null);
  const [logs, setLogs]       = useState([]);
  const [err, setErr]         = useState(null);
  const [loading, setLoading] = useState(false);

  const refresh = async () => {
    setLoading(true); setErr(null);
    localStorage.setItem('neura-mon-agent', agent);
    const q = '?agent=' + encodeURIComponent(agent || 'agente');
    const token = localStorage.getItem('neura-token') || '';
    const authHeaders = token ? { Authorization: `Bearer ${token}` } : {};
    try {
      const [h, m, l] = await Promise.all([
        fetch('/api/agent/health' + q, { headers: authHeaders }).then(r => r.json()),
        fetch('/api/agent/metrics' + q, { headers: authHeaders }).then(r => r.json()),
        fetch('/api/agent/logs' + q + '&limit=50', { headers: authHeaders }).then(r => r.json()),
      ]);
      if (h && h.error) throw new Error(h.error);
      setHealth(h); setMetrics(m); setLogs(Array.isArray(l) ? l : []);
    } catch (e) { setErr(e.message); }
    setLoading(false);
  };

  useEffect(() => { refresh(); }, []);

  const fmt = d => { try { return new Date(d).toLocaleString('pt-BR'); } catch { return d || '-'; } };
  const st = (health && health.status) || 'idle';

  return (
    <div className="nc-overlay" onClick={e => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="nc-panel nm-panel">
        <div className="nc-header">
          <div className="nc-header-left">
            <span className="nc-logo">MONITOR · AGENTE</span>
            <span className={'nm-status nm-' + st}><span className="nc-dot"/>{st.toUpperCase()}</span>
          </div>
          <div className="nc-header-actions">
            <input className="nc-input nm-agent" value={agent} onChange={e => setAgent(e.target.value)} title="agent_id"/>
            <button className="na-btn" onClick={refresh} disabled={loading}>{loading ? '...' : 'atualizar'}</button>
            <button className="nc-icon-btn nc-close-btn" title="Fechar" onClick={onClose}>
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M18 6 6 18M6 6l12 12"/></svg>
            </button>
          </div>
        </div>
        <div className="nm-body">
          {err && <div className="nm-err">⚠ {err}</div>}
          <div className="nm-grid">
            <MetricCard k="execuções"    v={metrics && metrics.total_executions}/>
            <MetricCard k="tempo médio"  v={metrics ? (metrics.avg_latency_ms ?? '-') + ' ms' : null}/>
            <MetricCard k="p95"          v={metrics ? (metrics.p95_latency_ms ?? '-') + ' ms' : null}/>
            <MetricCard k="taxa de erro" v={metrics ? ((metrics.error_rate || 0) * 100).toFixed(1) + '%' : null}/>
            <MetricCard k="tokens"       v={metrics && metrics.total_tokens}/>
            <MetricCard k="tokens/exec"  v={metrics && metrics.avg_tokens_per_exec}/>
            <MetricCard k="custo"        v={metrics ? '$' + (metrics.total_cost_usd ?? 0) : null}/>
            <MetricCard k="compressões"  v={metrics && metrics.compression_runs}/>
          </div>
          <div className="nm-health">
            saúde ({health?.window_minutes ?? 15}min): {health?.executions ?? 0} exec · {health?.errors ?? 0} erros · último ok {fmt(health?.last_success_at)}
          </div>
          <div className="nm-logs">
            <table className="nm-table">
              <thead><tr><th>quando</th><th>status</th><th>modelo</th><th>tokens</th><th>$</th><th>ms</th><th>compr</th><th>sessão</th></tr></thead>
              <tbody>
                {logs.length === 0 && <tr><td colSpan="8" className="nm-empty">sem registros</td></tr>}
                {logs.map((r, i) => (
                  <tr key={i}>
                    <td>{fmt(r.created_at)}</td>
                    <td className={r.status === 'success' ? 'nm-ok' : 'nm-bad'}>{r.status}</td>
                    <td>{r.model || '-'}</td>
                    <td>{r.total_tokens == null ? '-' : r.total_tokens}</td>
                    <td>{r.cost_usd == null ? '-' : r.cost_usd}</td>
                    <td>{r.latency_ms == null ? '-' : r.latency_ms}</td>
                    <td>{r.compressed ? 'sim' : '-'}</td>
                    <td title={r.session_id}>{(r.session_id || '-').slice(0, 14)}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>
      </div>
    </div>
  );
}

// ─── Left Nav ─────────────────────────────────────────────────────────────────
function LeftNav({ kindFilters, toggleKind, activeList, selected, setSelected, counts }) {
  const kinds = [
    { k: 'core', label: 'registry' }, { k: 'agent', label: 'agentes' },
    { k: 'skill', label: 'skills' },  { k: 'workflow', label: 'workflows' },
  ];
  return (
    <aside className="na-left">
      <div className="na-section">
        <div className="na-section-title">Tipos</div>
        <div className="na-filters">
          {kinds.map(({ k, label }) => (
            <div key={k} className={'na-filter' + (kindFilters[k] ? ' active' : '')} onClick={() => toggleKind(k)}>
              <span className="sw" style={{ background: KIND_COLOR[k] }} />
              <span>{label}</span>
              <span className="count">{counts[k] ?? 0}</span>
            </div>
          ))}
        </div>
      </div>
      <div className="na-section">
        <div className="na-section-title">Agentes ({activeList.length})</div>
      </div>
      <div className="na-list">
        {activeList.map(n => (
          <div key={n.id} className={'na-list-item' + (selected === n.id ? ' active' : '')} onClick={() => setSelected(n.id)}>
            <span className="dot" style={{ background: STATUS_COLOR[n.status] || KIND_COLOR[n.kind] }} />
            <span>{n.name}</span>
            <span className="meta">{n.skills ? n.skills + ' sk' : ''}</span>
          </div>
        ))}
      </div>
    </aside>
  );
}

// ─── Skill Templates (KAN-501) ────────────────────────────────────────────────
const SKILL_TEMPLATES = [
  {
    id: 'contestacao', label: 'Contestação Trabalhista', icon: '⚖',
    fields: {
      nome: 'contestacao-trabalhista',
      quando: 'Use quando houver reclamatória trabalhista para defender. Acionar quando alguém disser: "chegou contestação", "preciso defender", "reclamante entrou com ação", "protocolar defesa", "prazo de contestação".',
      procedimento: '1. Identificar todos os pedidos do reclamante\n2. Verificar documentos disponíveis (TRCT, ponto, LTCAT, ASO, CCT)\n3. Montar defesa por tópico: jornada, verbas rescisórias, insalubridade, dano moral\n4. Incluir tópicos fixos do escritório (Reforma Trabalhista, limitação Rcl 77.179 STF)\n5. Incluir preliminares se cabíveis\n6. Acionar revisor-senior antes do protocolo',
      retorno: 'Contestação completa com tópicos numerados, fundamentos jurídicos e pedidos de improcedência discriminados.',
      grupo: 'redação',
    },
  },
  {
    id: 'recurso', label: 'Recurso Ordinário (TRT)', icon: '📋',
    fields: {
      nome: 'recurso-ordinario',
      quando: 'Use quando houver sentença condenatória para recorrer ao TRT. Acionar quando alguém disser: "saiu a sentença", "preciso recorrer", "prazo de recurso", "depositar preparo", "TRT".',
      procedimento: '1. Calcular tempestividade (8 dias úteis da publicação)\n2. Verificar valor da condenação e calcular depósito recursal\n3. Identificar cada condenação a impugnar\n4. Por condenação: erro de fato ou de direito? Documento que contradiz?\n5. Montar razões por condenação separadamente\n6. Acionar revisor-senior antes do protocolo',
      retorno: 'Recurso Ordinário com demonstração de preparo, tempestividade e razões de reforma por condenação.',
      grupo: 'redação',
    },
  },
  {
    id: 'intake', label: 'Intake de Cliente', icon: '👤',
    fields: {
      nome: 'intake-cliente',
      quando: 'Use no primeiro atendimento de um novo cliente. Acionar quando alguém disser: "chegou cliente novo", "fazer intake", "abrir ficha", "novo caso", "consulta inicial", "primeiro atendimento".',
      procedimento: '1. Identificar área do caso (trabalhista, cível, previdenciário)\n2. Coletar dados do cliente (nome, CPF, contato)\n3. Coletar dados da contraparte\n4. Resumo dos fatos e pretensão do cliente\n5. Verificar prazo prescricional\n6. Levantar documentos necessários\n7. Indicar próxima skill',
      retorno: 'Ficha de cliente completa com dados, resumo do caso, documentos a providenciar e próxima etapa indicada.',
      grupo: 'atendimento',
    },
  },
  {
    id: 'analise', label: 'Análise Documental', icon: '🔍',
    fields: {
      nome: 'analise-documental',
      quando: 'Use quando houver documentos de processo para analisar. Acionar quando alguém disser: "analisar documentos", "o que esse TRCT prova", "verificar as provas", "análise probatória", "mapear documentos".',
      procedimento: '1. Catalogar documentos recebidos\n2. Para cada documento: o que prova a favor? O que fragiliza?\n3. Mapear tese de defesa → documento que sustenta\n4. Identificar lacunas (documentos que faltam)\n5. Emitir relatório de análise com próximos passos',
      retorno: 'Relatório com lista de documentos, pontos fortes/vulneráveis da defesa, lacunas críticas e mapa tese-documento.',
      grupo: 'análise',
    },
  },
  {
    id: 'revisao', label: 'Revisão Sênior', icon: '✅',
    fields: {
      nome: 'revisao-senior',
      quando: 'Use antes de protocolar qualquer peça. Acionar quando alguém disser: "revisa essa peça", "pode protocolar?", "dupla garantia", "checar antes de mandar", "passar pelo revisor".',
      procedimento: '1. Identificar o tipo de peça\n2. Aplicar checklist: identificação, formatação, estrutura, substantivo\n3. Verificar jurisprudência (tribunal + número + data + relator)\n4. Verificar pedidos vs. fundamentos\n5. Emitir relatório: APROVADA / APROVADA COM RESSALVAS / REPROVADA',
      retorno: 'Relatório de revisão com itens críticos 🔴, importantes 🟠, atenção 🟡 e itens aprovados ✅.',
      grupo: 'qualidade',
    },
  },
  {
    id: 'peticao', label: 'Petição Inicial', icon: '📄',
    fields: {
      nome: 'peticao-inicial',
      quando: 'Use para ajuizar ação em nome do cliente. Acionar quando alguém disser: "entrar com ação", "ajuizar", "petição inicial", "abrir processo", "protocolar inicial".',
      procedimento: '1. Qualificação das partes e endereço\n2. Dos fatos (narrativa objetiva)\n3. Do direito (fundamentos legais + doutrina)\n4. Da jurisprudência aplicável\n5. Dos pedidos (discriminados com valor estimado)\n6. Requerimentos processuais (gratuidade, provas, etc.)\n7. Revisar antes do protocolo',
      retorno: 'Petição inicial completa com todos os tópicos, pedidos discriminados e requerimentos processuais.',
      grupo: 'redação',
    },
  },
  {
    id: 'resposta', label: 'Skill Genérica', icon: '💬',
    fields: {
      nome: 'nova-skill',
      quando: 'Descreva quando esta skill deve ser acionada — palavras-chave e situações.',
      procedimento: '1. Primeiro passo\n2. Segundo passo\n3. Terceiro passo',
      retorno: 'Descreva o que esta skill retorna — formato e conteúdo esperado.',
      grupo: 'atendimento',
    },
  },
];

// ─── Skill validation (KAN-502) ───────────────────────────────────────────────
function validateSkillContent(text) {
  const erros = [];
  if (!text || text.trim().length === 0)
    erros.push({ nivel: 'critico', msg: 'Conteúdo vazio — a skill não pode ser salva em branco.' });
  else if (text.trim().length < 80)
    erros.push({ nivel: 'aviso', msg: `Conteúdo muito curto (${text.trim().length} chars). Uma skill útil tem pelo menos 80 caracteres.` });

  const hasFrontmatter = /^---\s*\n/.test(text.trim());
  if (hasFrontmatter) {
    if (!/\bname\s*:/.test(text))
      erros.push({ nivel: 'aviso', msg: 'Frontmatter sem campo "name:" — o n8n pode não identificar esta skill.' });
    if (!/\bdescription\s*:/.test(text))
      erros.push({ nivel: 'aviso', msg: 'Frontmatter sem campo "description:" — o n8n pode não carregar corretamente.' });
  }
  return erros;
}

// ─── Inspector ────────────────────────────────────────────────────────────────
function Inspector({ node, graph, aFetch: _aFetch, canEdit: _canEdit }) {
  // Usa aFetch se disponível, senão fetch normal
  const aFetch = _aFetch || fetch;
  // canEdit: verifica se o usuário pode editar o agente deste nó
  const getAgentId = () => {
    if (!node) return '';
    if (node.kind === 'agent') return node.name;
    if (node.kind === 'skill') return node.agent?.replace('agent:', '') || node.id.split(':')[0];
    return '';
  };
  const editable = !_canEdit || _canEdit(getAgentId());
  // ── Skill editor state ──────────────────────────────────────────────────
  const [content, setContent]   = useState('');
  const [orig,    setOrig]      = useState('');
  const [loading, setLoading]   = useState(false);
  const [saving,  setSaving]    = useState(false);
  const [saved,   setSaved]     = useState(false); // 'pushed' | 'warn' | false
  const [err,     setErr]       = useState('');

  // ── Agent tabs state ────────────────────────────────────────────────────
  const [activeTab,           setActiveTab]           = useState('info'); // 'info' | 'personality'
  const [personality,         setPersonality]         = useState('');
  const [personalityOrig,     setPersonalityOrig]     = useState('');
  const [personalityLoading,  setPersonalityLoading]  = useState(false);
  const [personalitySaving,   setPersonalitySaving]   = useState(false);
  const [personalitySaved,    setPersonalitySaved]    = useState(false);
  const [personalityErr,      setPersonalityErr]      = useState('');

  // Nova skill modal state (KAN-500/501)
  const [newSkillModal, setNewSkillModal] = useState(false);
  const [newSkillStep,  setNewSkillStep]  = useState('template'); // 'template' | 'form'
  const [newSkillFields, setNewSkillFields] = useState({
    nome: '', quando: '', procedimento: '', retorno: '', grupo: 'atendimento',
  });
  const [creating,      setCreating]      = useState(false);

  // Validation state (KAN-502)
  const [validErros, setValidErros] = useState([]);
  const [validForce, setValidForce] = useState(false);

  // Delete skill state
  const [deleting, setDeleting] = useState(false);

  // ── Reset ao trocar de nó ───────────────────────────────────────────────
  useEffect(() => {
    setActiveTab('info');
    setPersonality(''); setPersonalityOrig('');
    setPersonalityErr(''); setPersonalitySaved(false);
    if (node?.kind === 'skill' && node.file) {
      setLoading(true); setSaved(false); setErr('');
      aFetch(`/api/skill?file=${encodeURIComponent(node.file)}`)
        .then(r => r.json())
        .then(d => { setContent(d.content || ''); setOrig(d.content || ''); })
        .catch(() => setErr('Falha ao carregar arquivo.'))
        .finally(() => setLoading(false));
    } else { setContent(''); setOrig(''); }
  }, [node?.id]);

  // ── Carrega PERSONALITY.md quando a aba é aberta ────────────────────────
  useEffect(() => {
    if (activeTab !== 'personality' || node?.kind !== 'agent') return;
    if (personality !== '') return; // já carregado
    setPersonalityLoading(true); setPersonalityErr('');
    aFetch(`/api/personality?agent=${encodeURIComponent(node.name)}`)
      .then(r => r.json())
      .then(d => { setPersonality(d.content || ''); setPersonalityOrig(d.content || ''); })
      .catch(() => setPersonalityErr('Falha ao carregar personalidade.'))
      .finally(() => setPersonalityLoading(false));
  }, [activeTab, node?.id]);

  const savePersonality = () => {
    if (!node) return;
    setPersonalitySaving(true); setPersonalityErr('');
    aFetch(`/api/personality?agent=${encodeURIComponent(node.name)}`, {
      method: 'POST', headers: { 'Content-Type': 'text/plain; charset=utf-8' }, body: personality,
    })
      .then(async r => {
        const d = await r.json().catch(() => ({}));
        if (!r.ok) setPersonalityErr(d.error || `HTTP ${r.status}`);
        else if (d.ok) {
          setPersonalityOrig(personality);
          setPersonalitySaved(d.git || 'pushed');
          setTimeout(() => setPersonalitySaved(false), 4000);
        } else setPersonalityErr(d.error || 'Falha ao salvar.');
      })
      .catch(e => setPersonalityErr(e.message))
      .finally(() => setPersonalitySaving(false));
  };

  // KAN-502 — validação antes de salvar
  const save = () => {
    if (!node?.file) return;
    const erros = validateSkillContent(content);
    const criticos = erros.filter(e => e.nivel === 'critico');
    if (criticos.length > 0 && !validForce) {
      setValidErros(erros); return;
    }
    setValidErros([]); setValidForce(false);
    setSaving(true); setErr('');
    aFetch(`/api/skill?file=${encodeURIComponent(node.file)}`, {
      method: 'POST', headers: { 'Content-Type': 'text/plain; charset=utf-8' }, body: content,
    })
      .then(async r => {
        const d = await r.json().catch(() => ({}));
        if (!r.ok) setErr(d.error || `HTTP ${r.status}`);
        else if (d.ok) { setOrig(content); setSaved(d.git || 'pushed'); setTimeout(() => setSaved(false), 4000); }
        else setErr(d.error || 'Falha ao salvar no Git.');
      })
      .catch(e => setErr(e.message))
      .finally(() => setSaving(false));
  };

  const download = () => {
    if (!content && !node?.desc) return;
    const text = content || node.desc;
    const blob = new Blob([text], { type: 'text/plain; charset=utf-8' });
    const url  = URL.createObjectURL(blob);
    const a    = Object.assign(document.createElement('a'), {
      href: url,
      download: node.file?.split('/').pop() || `${node.name}.md`,
    });
    document.body.appendChild(a); a.click();
    document.body.removeChild(a); URL.revokeObjectURL(url);
  };

  const copy = () => navigator.clipboard?.writeText(content || node?.desc || '').catch(() => {});

  // KAN-500 — gera SKILL.md a partir dos campos do formulário guiado
  const buildSkillFromForm = (fields) => {
    const id = fields.nome.trim().toLowerCase().replace(/\s+/g,'-').replace(/[^a-z0-9-]/g,'');
    return [
      `---`,
      `name: ${id}`,
      `description: >`,
      `  ${fields.quando.replace(/\n/g,' ').slice(0,200)}`,
      `---`,
      ``,
      `# ${fields.nome}`,
      ``,
      `## Quando acionar`,
      ``,
      fields.quando,
      ``,
      `## Procedimento`,
      ``,
      fields.procedimento,
      ``,
      `## O que retornar`,
      ``,
      fields.retorno,
      ``,
    ].join('\n');
  };

  const openNewSkillModal = () => {
    setNewSkillStep('template');
    setNewSkillFields({ nome:'', quando:'', procedimento:'', retorno:'', grupo:'atendimento' });
    setNewSkillModal(true);
  };

  const selectTemplate = (tpl) => {
    setNewSkillFields({ ...tpl.fields });
    setNewSkillStep('form');
  };

  const createSkill = () => {
    const id = newSkillFields.nome.trim().toLowerCase().replace(/\s+/g,'-').replace(/[^a-z0-9-]/g,'');
    if (!id) return;
    const agentId = node.name;
    const initialContent = buildSkillFromForm(newSkillFields);
    setCreating(true);
    aFetch(`/api/skill/new?agent=${encodeURIComponent(agentId)}&id=${encodeURIComponent(id)}`, {
      method: 'POST', headers: { 'Content-Type': 'text/plain; charset=utf-8' }, body: initialContent,
    })
      .then(r => r.json())
      .then(d => {
        if (d.ok) { setNewSkillModal(false); window.location.reload(); }
        else alert('Erro: ' + (d.error || 'falha ao criar skill'));
      })
      .catch(e => alert('Erro: ' + e.message))
      .finally(() => setCreating(false));
  };

  const deleteSkill = () => {
    if (!node?.file) return;
    if (!window.confirm(`Apagar a skill "${node.name}"? Esta ação não pode ser desfeita.`)) return;
    setDeleting(true);
    aFetch(`/api/skill?file=${encodeURIComponent(node.file)}`, { method: 'DELETE' })
      .then(r => r.json())
      .then(d => {
        if (d.ok) window.location.reload();
        else alert('Erro: ' + (d.error || 'falha ao apagar skill'));
      })
      .catch(e => alert('Erro: ' + e.message))
      .finally(() => setDeleting(false));
  };

  if (!node) return (
    <aside className="na-right">
      <div className="na-empty"><div className="glyph">·</div><div>clique em um nó para<br/>inspecionar e editar</div></div>
    </aside>
  );

  const neighbors = graph.edges
    .filter(e => e.source === node.id || e.target === node.id)
    .map(e => e.source === node.id ? e.target : e.source);
  const connected = [...new Set(neighbors)].map(id => graph.nodes.find(n => n.id === id)).filter(Boolean);
  const isDirty      = node.kind === 'skill' && content !== orig;
  const isPersoDirty = node.kind === 'agent' && personality !== personalityOrig;

  const downloadPersonality = () => {
    const blob = new Blob([personality], { type: 'text/plain; charset=utf-8' });
    const url  = URL.createObjectURL(blob);
    const a    = Object.assign(document.createElement('a'), {
      href: url, download: `${node.name}-PERSONALITY.md`,
    });
    document.body.appendChild(a); a.click();
    document.body.removeChild(a); URL.revokeObjectURL(url);
  };

  return (
    <aside className="na-right">
      <div className="na-insp-head">
        <div className="na-insp-kind">
          <span className="dot" style={{ background: KIND_COLOR[node.kind] }} />
          <span>{node.kind}</span>
          {node.status && <span style={{ marginLeft:8, color: STATUS_COLOR[node.status] }}>● {node.status}</span>}
        </div>
        <div className="na-insp-name">{node.name}</div>
        <div className="na-insp-path">{node.file || (node.kind==='agent' ? `${node.name}/agent.manifest.json` : 'neura-registry.json')}</div>
        {!editable && <div className="na-readonly-badge">🔒 somente leitura</div>}
      </div>

      {/* ── Abas para agente ─────────────────────────────────────── */}
      {node.kind === 'agent' && (
        <div className="na-tabs">
          <button className={`na-tab${activeTab==='info' ? ' active' : ''}`} onClick={() => setActiveTab('info')}>
            Info
          </button>
          <button className={`na-tab${activeTab==='personality' ? ' active' : ''}`} onClick={() => setActiveTab('personality')}>
            Personalidade
            {node.hasPersonality && <span className="na-tab-dot" style={{ background: 'var(--c-ok)' }}/>}
          </button>
        </div>
      )}

      {/* ── Skill editor ─────────────────────────────────────────── */}
      {node.kind === 'skill' && (
        <div className="na-skill-body">
          <div className="na-skill-meta-bar">
            <dl className="na-kv">
              <dt>agente</dt><dd>{node.agent?.replace('agent:','') || node.id.split(':')[0]}</dd>
              <dt>conexões</dt><dd>{connected.length}</dd>
              {node.tokens > 0 && <><dt>tokens base</dt><dd>{node.tokens}</dd></>}
            </dl>
            <div style={{ marginTop:10 }}>
              <div className="na-insp-label" style={{ marginBottom:6 }}>Conectado a</div>
              <div className="na-chips">
                {connected.slice(0,8).map(c => (
                  <span key={c.id} className="na-chip-mini" style={{ color: KIND_COLOR[c.kind] }}>{c.name}</span>
                ))}
              </div>
            </div>
          </div>
          <div className="na-skill-editor-section">
            <div className="na-insp-label skill-label-row">
              <span>SKILL.md</span>
              <span className="skill-badges">
                {isDirty && !saved && <span className="badge warn">● editado</span>}
                {saved === 'pushed' && <span className="badge ok">✓ salvo · pushed</span>}
                {saved === 'warn'   && <span className="badge warn">✓ salvo · git warn</span>}
                {err     && <span className="badge err" title={err}>⚠ erro</span>}
              </span>
            </div>
            {loading
              ? <div className="insp-loading">carregando…</div>
              : <textarea
                  className={`na-skill-editor${validErros.some(e=>e.nivel==='critico')?' na-editor-err':''}`}
                  value={content}
                  onChange={e => { setContent(e.target.value); setSaved(false); setValidErros([]); setValidForce(false); }}
                  spellCheck={false}
                  placeholder="# Nome da Skill&#10;&#10;## Contexto&#10;…"
                />
            }
            {validErros.length > 0 && (
              <div className="na-valid-erros">
                {validErros.map((e,i) => (
                  <div key={i} className={`na-valid-row na-valid-${e.nivel}`}>
                    {e.nivel==='critico' ? '🔴' : '🟡'} {e.msg}
                  </div>
                ))}
                {validErros.some(e=>e.nivel==='critico') && (
                  <button className="na-btn" style={{marginTop:6,fontSize:11}} onClick={() => { setValidForce(true); save(); }}>
                    Salvar mesmo assim
                  </button>
                )}
              </div>
            )}
            <div className="skill-meta">
              {content.length} chars · ~{Math.ceil(content.split(/\s+/).filter(Boolean).length / 0.75)} tokens
            </div>
          </div>
        </div>
      )}

      {/* ── Agente: aba Info ─────────────────────────────────────── */}
      {node.kind === 'agent' && activeTab === 'info' && (
        <div className="na-insp-body">
          <div>
            <div className="na-insp-label">Descrição</div>
            <div className="na-insp-desc">{node.desc}</div>
          </div>
          <div>
            <div className="na-insp-label">Metadados</div>
            <dl className="na-kv">
              <dt>id</dt><dd>{node.id}</dd>
              {node.skills != null && <><dt>skills</dt><dd>{node.skills}</dd></>}
              <dt>personalidade</dt>
              <dd style={{ color: node.hasPersonality ? 'var(--c-ok)' : 'var(--ink-mute)' }}>
                {node.hasPersonality ? '✓ configurada' : '— não definida'}
              </dd>
              <dt>conexões</dt><dd>{connected.length}</dd>
            </dl>
          </div>
          <div>
            <div className="na-insp-label">Conectado a</div>
            <div className="na-chips">
              {connected.length === 0
                ? <span style={{ color:'var(--ink-mute)', fontSize:12 }}>nenhuma</span>
                : connected.slice(0,14).map(c => (
                    <span key={c.id} className="na-chip-mini" style={{ color: KIND_COLOR[c.kind] }}>{c.name}</span>
                  ))
              }
            </div>
          </div>
        </div>
      )}

      {/* ── Agente: aba Personalidade ────────────────────────────── */}
      {node.kind === 'agent' && activeTab === 'personality' && (
        <div className="na-skill-body">
          <div className="na-personality-hint">
            <span className="na-personality-hint-icon">◈</span>
            <span>Define tom, estilo de escrita e formato das mensagens do agente no n8n.</span>
          </div>
          <div className="na-skill-editor-section">
            <div className="na-insp-label skill-label-row">
              <span>PERSONALITY.md</span>
              <span className="skill-badges">
                {isPersoDirty && !personalitySaved && <span className="badge warn">● editado</span>}
                {personalitySaved === 'pushed' && <span className="badge ok">✓ salvo · pushed</span>}
                {personalitySaved === 'warn'   && <span className="badge warn">✓ salvo · git warn</span>}
                {personalityErr   && <span className="badge err" title={personalityErr}>⚠ erro</span>}
              </span>
            </div>
            {personalityLoading
              ? <div className="insp-loading">carregando…</div>
              : <textarea
                  className="na-skill-editor na-personality-editor"
                  value={personality}
                  onChange={e => { setPersonality(e.target.value); setPersonalitySaved(false); }}
                  spellCheck={false}
                  placeholder="# Personalidade do agente…"
                />
            }
            <div className="skill-meta">
              {personality.length} chars · ~{Math.ceil(personality.split(/\s+/).filter(Boolean).length / 0.75)} tokens
            </div>
          </div>
        </div>
      )}

      {/* ── Outros nós (core, workflow) ──────────────────────────── */}
      {node.kind !== 'skill' && node.kind !== 'agent' && (
        <div className="na-insp-body">
          <div>
            <div className="na-insp-label">Descrição</div>
            <div className="na-insp-desc">{node.desc}</div>
          </div>
          <div>
            <div className="na-insp-label">Metadados</div>
            <dl className="na-kv">
              <dt>id</dt><dd>{node.id}</dd>
              {node.skills != null && <><dt>skills</dt><dd>{node.skills}</dd></>}
              <dt>conexões</dt><dd>{connected.length}</dd>
            </dl>
          </div>
          <div>
            <div className="na-insp-label">Conectado a</div>
            <div className="na-chips">
              {connected.length === 0
                ? <span style={{ color:'var(--ink-mute)', fontSize:12 }}>nenhuma</span>
                : connected.slice(0,14).map(c => (
                    <span key={c.id} className="na-chip-mini" style={{ color: KIND_COLOR[c.kind] }}>{c.name}</span>
                  ))
              }
            </div>
          </div>
        </div>
      )}

      {/* ── Footer ───────────────────────────────────────────────── */}
      <div className="na-insp-foot">
        {node.kind === 'skill' && editable && (
          <button
            className="na-btn"
            style={{ color: 'var(--c-error,#e55)', borderColor: 'var(--c-error,#e55)' }}
            onClick={deleteSkill}
            disabled={deleting}
          >
            {deleting ? 'Apagando…' : 'Apagar'}
          </button>
        )}

        {/* Skill footer */}
        {node.kind === 'skill' && <>
          <button className="na-btn" onClick={download}>Baixar .md</button>
          {isDirty
            ? <button className="na-btn primary" onClick={save} disabled={saving || !editable}>{saving ? 'Salvando…' : 'Salvar'}</button>
            : <button className="na-btn primary" onClick={copy}>Copiar</button>
          }
        </>}

        {/* Agent footer — aba Info */}
        {node.kind === 'agent' && activeTab === 'info' && <>
          <button className="na-btn primary" onClick={() => setActiveTab('personality')} style={{ flex:1, justifyContent:'center' }}>
            Editar Personalidade
          </button>
          {editable && <button className="na-btn primary" onClick={openNewSkillModal}>Nova Skill</button>}
        </>}

        {/* Agent footer — aba Personalidade */}
        {node.kind === 'agent' && activeTab === 'personality' && <>
          <button className="na-btn" onClick={downloadPersonality}>Baixar .md</button>
          {isPersoDirty
            ? <button className="na-btn primary" onClick={savePersonality} disabled={personalitySaving}>
                {personalitySaving ? 'Salvando…' : 'Salvar'}
              </button>
            : <button className="na-btn primary" onClick={() => navigator.clipboard?.writeText(personality).catch(()=>{})}>Copiar</button>
          }
        </>}

        {/* Non-agent, non-skill */}
        {node.kind !== 'skill' && node.kind !== 'agent' && <>
          <button className="na-btn" onClick={download}>Baixar .md</button>
          <button className="na-btn primary" onClick={copy}>Copiar</button>
        </>}
      </div>

      {/* ── Modal Nova Skill — 2 etapas (KAN-500/501) ── */}
      {newSkillModal && (
        <div className="ns-overlay" onClick={e => { if (e.target === e.currentTarget) setNewSkillModal(false); }}>
          <div className="ns-panel">

            {/* Header do modal */}
            <div className="ns-head">
              <span className="ns-title">
                {newSkillStep === 'template' ? 'Nova Skill em ' : '← '}
                <strong>{node.name}</strong>
              </span>
              <button className="nc-icon-btn" onClick={() => setNewSkillModal(false)}>
                <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M18 6 6 18M6 6l12 12"/></svg>
              </button>
            </div>

            {/* ETAPA 1 — Galeria de templates */}
            {newSkillStep === 'template' && (
              <>
                <div className="ns-subtitle">Escolha um template ou comece do zero</div>
                <div className="ns-grid">
                  {SKILL_TEMPLATES.map(tpl => (
                    <button key={tpl.id} className="ns-tpl-card" onClick={() => selectTemplate(tpl)}>
                      <span className="ns-tpl-icon">{tpl.icon}</span>
                      <span className="ns-tpl-label">{tpl.label}</span>
                    </button>
                  ))}
                </div>
              </>
            )}

            {/* ETAPA 2 — Formulário guiado */}
            {newSkillStep === 'form' && (
              <>
                <div className="ns-subtitle">Preencha os campos — o SKILL.md será gerado automaticamente</div>
                <div className="ns-form">

                  <div className="ns-field">
                    <label className="ns-label">Nome da skill <span className="ns-req">*</span></label>
                    <input
                      autoFocus
                      className="ns-input"
                      placeholder="ex: contestacao-trabalhista"
                      value={newSkillFields.nome}
                      onChange={e => setNewSkillFields(f => ({ ...f, nome: e.target.value }))}
                    />
                    <div className="ns-hint">Slug único, sem espaços (gerado automaticamente se usar espaços)</div>
                  </div>

                  <div className="ns-field">
                    <label className="ns-label">Quando acionar <span className="ns-req">*</span></label>
                    <textarea
                      className="ns-textarea"
                      rows={3}
                      placeholder="Descreva as situações e palavras-chave que acionam esta skill…"
                      value={newSkillFields.quando}
                      onChange={e => setNewSkillFields(f => ({ ...f, quando: e.target.value }))}
                    />
                  </div>

                  <div className="ns-field">
                    <label className="ns-label">Procedimento <span className="ns-req">*</span></label>
                    <textarea
                      className="ns-textarea"
                      rows={4}
                      placeholder="1. Primeiro passo&#10;2. Segundo passo&#10;3. Terceiro passo"
                      value={newSkillFields.procedimento}
                      onChange={e => setNewSkillFields(f => ({ ...f, procedimento: e.target.value }))}
                    />
                  </div>

                  <div className="ns-field">
                    <label className="ns-label">O que retornar</label>
                    <textarea
                      className="ns-textarea"
                      rows={2}
                      placeholder="Descreva o formato e conteúdo da resposta esperada…"
                      value={newSkillFields.retorno}
                      onChange={e => setNewSkillFields(f => ({ ...f, retorno: e.target.value }))}
                    />
                  </div>

                  <div className="ns-field">
                    <label className="ns-label">Grupo</label>
                    <select
                      className="ns-select"
                      value={newSkillFields.grupo}
                      onChange={e => setNewSkillFields(f => ({ ...f, grupo: e.target.value }))}
                    >
                      {['atendimento','análise','redação','qualidade','identidade','configuração'].map(g => (
                        <option key={g} value={g}>{g}</option>
                      ))}
                    </select>
                  </div>

                </div>

                <div className="ns-foot">
                  <button className="na-btn" onClick={() => setNewSkillStep('template')}>← Templates</button>
                  <button
                    className="na-btn primary"
                    onClick={createSkill}
                    disabled={creating || !newSkillFields.nome.trim() || !newSkillFields.quando.trim() || !newSkillFields.procedimento.trim()}
                  >
                    {creating ? 'Criando…' : 'Criar Skill'}
                  </button>
                </div>
              </>
            )}

          </div>
        </div>
      )}
    </aside>
  );
}

// ─── Canvas Overlay ───────────────────────────────────────────────────────────
function CanvasOverlay({ layoutKind, setLayoutKind }) {
  return (
    <>
      <div className="na-overlay">
        <button className={layoutKind==='force'  ? 'active':''} onClick={() => setLayoutKind('force')}>força</button>
        <button className={layoutKind==='radial' ? 'active':''} onClick={() => setLayoutKind('radial')}>radial</button>
        <button className={layoutKind==='hier'   ? 'active':''} onClick={() => setLayoutKind('hier')}>hierárquico</button>
      </div>
      <div className="na-legend">
        {[['core','registry'],['agent','agente'],['skill','skill'],['workflow','workflow']].map(([k,l]) => (
          <span key={k} className="it"><span className="sw" style={{ background:`var(--c-${k})` }}/> {l}</span>
        ))}
      </div>
    </>
  );
}

// ─── Node Renderers ───────────────────────────────────────────────────────────
function SolidNode({ n, state }) {
  const r = n.kind==='core' ? 16 : n.kind==='agent' ? 10 : n.kind==='workflow' ? 7 : 4;
  const color = KIND_COLOR[n.kind];
  const showLabel = n.kind !== 'skill' || state !== 'idle';
  return (
    <>
      <circle r={Math.max(r + 8, 14)} fill="transparent"/>
      {state==='sel' && <circle r={r+14} fill="none" stroke={color} strokeWidth="0.5" opacity="0.22"/>}
      {state==='sel' && <circle r={r+7}  fill="none" stroke={color} strokeWidth="0.8" opacity="0.15"/>}
      <circle r={r} fill={color}/>
      {n.status && n.kind==='agent' && (
        <circle cx={r-2} cy={-r+2} r="3" fill={STATUS_COLOR[n.status]} stroke="var(--bg)" strokeWidth="1.2"/>
      )}
      {showLabel && (
        <text className={'n-label '+(state!=='idle'?'strong':n.kind==='core'||n.kind==='agent'?'':'dim')} y={r+14}>
          {n.name}
        </text>
      )}
    </>
  );
}

function OutlineNode({ n, state }) {
  const r = n.kind==='core'?18:n.kind==='agent'?12:n.kind==='workflow'?9:5;
  const color = KIND_COLOR[n.kind];
  const filled = n.kind==='skill'||n.kind==='core';
  const showLabel = n.kind!=='skill'||state!=='idle';
  return (
    <>
      <circle r={Math.max(r + 8, 14)} fill="transparent"/>
      {state==='sel' && <circle r={r+6} fill="none" stroke={color} strokeWidth="1" opacity="0.45"/>}
      <circle r={r} fill={filled?color:'var(--bg)'} stroke={color} strokeWidth={filled?0:1.5}/>
      {n.kind==='agent' && (
        <text y="3" textAnchor="middle" style={{fontFamily:'var(--font-mono)',fontSize:9,fill:color,letterSpacing:'0.08em'}}>
          {n.name.slice(0,2).toUpperCase()}
        </text>
      )}
      {n.status&&n.kind==='agent' && <circle cx={r-1} cy={-r+1} r="3" fill={STATUS_COLOR[n.status]} stroke="var(--bg)" strokeWidth="1.2"/>}
      {showLabel && <text className={'n-label '+(state!=='idle'?'strong':'')} y={r+14}>{n.name}</text>}
    </>
  );
}

function CardNode({ n, state, agentCount }) {
  const color = KIND_COLOR[n.kind];
  const agentsTotal = agentCount ?? window.NEURA_GRAPH?.agents?.length ?? 0;
  if (n.kind==='skill') {
    const w=Math.max(64,n.name.length*7.2),h=18;
    return (<>
      <rect x={-w/2} y={-h/2} width={w} height={h} rx="3" className={'n-card-body'+(state!=='idle'?' hi':'')}/>
      <rect x={-w/2} y={-h/2} width="2" height={h} fill={color}/>
      <text y="3" textAnchor="middle" style={{fontFamily:'var(--font-mono)',fontSize:10,fill:state!=='idle'?'var(--ink)':'var(--ink-dim)'}}>{n.name}</text>
    </>);
  }
  if (n.kind==='core') return (<>
    <rect x={-70} y={-20} width={140} height={40} rx="3" className={'n-card-body'+(state!=='idle'?' hi':'')}/>
    <circle cx={-56} r="4" fill={color}/>
    <text x={-46} y={-4} style={{fontFamily:'var(--font-mono)',fontSize:11,fill:'var(--ink)'}}>neura-registry</text>
    <text x={-46} y={10}  style={{fontFamily:'var(--font-mono)',fontSize:9, fill:'var(--ink-mute)'}}>index · {agentsTotal} agentes</text>
  </>);
  if (n.kind==='agent') { const w=128,h=48; return (<>
    <rect x={-w/2} y={-h/2} width={w} height={h} rx="4" className={'n-card-body'+(state!=='idle'?' hi':'')}/>
    <rect x={-w/2} y={-h/2} width="3" height={h} fill={color}/>
    <text x={-w/2+10} y={-h/2+18} style={{fontFamily:'var(--font-mono)',fontSize:12,fill:'var(--ink)'}}>{n.name}</text>
    <text x={-w/2+10} y={-h/2+34} style={{fontFamily:'var(--font-mono)',fontSize:9, fill:'var(--ink-mute)'}}>agent · {n.skills} skills</text>
    {n.status && <circle cx={w/2-10} cy={-h/2+10} r="3.5" fill={STATUS_COLOR[n.status]}/>}
  </>); }
  if (n.kind==='workflow') { const w=110,h=28; return (<>
    <rect x={-w/2} y={-h/2} width={w} height={h} rx="14" fill="var(--bg-raised)" stroke={color} strokeWidth="1"/>
    <circle cx={-w/2+12} r="3" fill={color}/>
    <text x={-w/2+22} y="3" style={{fontFamily:'var(--font-mono)',fontSize:10.5,fill:'var(--ink)'}}>{n.name}</text>
  </>); }
  return null;
}

// ─── Graph SVG ────────────────────────────────────────────────────────────────
function GraphSVG({ nodes, edges, layout, size, zoom, pan, selected, setSelected, hover, setHover, focusId, focusNeighbors, nodeStyle, nodeOverrides, onNodeDragStart, draggingIds, agentCount }) {
  const hasFocus  = !!focusId;
  const isInFocus = id => !hasFocus || id === focusId || focusNeighbors.has(id);

  // Resolved position (override > layout)
  const pos = id => nodeOverrides[id] || layout[id];

  return (
    <svg viewBox={`0 0 ${size.w} ${size.h}`} preserveAspectRatio="xMidYMid meet">
      <g transform={`translate(${pan.x} ${pan.y}) scale(${zoom}) translate(${size.w*(1-zoom)/(2*zoom)} ${size.h*(1-zoom)/(2*zoom)})`}>
        {/* Edges */}
        <g>
          {edges.map((e, i) => {
            const a = pos(e.source), b = pos(e.target);
            if (!a || !b) return null;
            const active = hasFocus && (e.source===focusId || e.target===focusId);
            const dim    = hasFocus && !active;
            const mx=(a.x+b.x)/2, my=(a.y+b.y)/2-12;
            return <path key={i} d={`M ${a.x} ${a.y} Q ${mx} ${my} ${b.x} ${b.y}`} className={'e'+(dim?' dim':active?' hi':'')}/>;
          })}
        </g>
        {/* Nodes */}
        <g>
          {nodes.map(n => {
            const p = pos(n.id); if (!p) return null;
            const state   = n.id===selected?'sel':n.id===hover?'hover':'idle';
            const dim     = !isInFocus(n.id);
            const isDrag  = draggingIds.has(n.id);
            const delay   = (hashId(n.id) % 420) / 100;
            const floatCl = isDrag ? '' : `float-${hashId(n.id) % 6}`;
            return (
              <g key={n.id} transform={`translate(${p.x} ${p.y})`}
                 style={{ cursor: isDrag ? 'grabbing' : 'grab' }}
                 onMouseDown={e => { e.stopPropagation(); onNodeDragStart(e, n.id, p); }}>
                <g className={`node np-${n.kind} ${floatCl}`+(dim?' dim':'')+(state!=='idle'?' hi':'')}
                   style={{ animationDelay:`${delay}s` }}
                   onMouseEnter={() => setHover(n.id)}
                   onMouseLeave={() => setHover(null)}
                   onClick={e => { e.stopPropagation(); setSelected(n.id); }}>
                  {nodeStyle==='solid'   && <SolidNode   n={n} state={state}/>}
                  {nodeStyle==='outline' && <OutlineNode n={n} state={state}/>}
                  {nodeStyle==='card'    && <CardNode    n={n} state={state} agentCount={agentCount}/>}
                </g>
              </g>
            );
          })}
        </g>
      </g>
    </svg>
  );
}

// ─── Main App ─────────────────────────────────────────────────────────────────
function NeuraApp({ nodeStyle = 'solid' }) {
  const [graph,   setGraph]   = useState(null);
  const [user,    setUser]    = useState(null);   // null = verificando, false = não auth
  const [authDone, setAuthDone] = useState(false);

  // Verifica auth ao carregar
  useEffect(() => {
    const token = localStorage.getItem('neura-token') || '';
    fetch('/api/auth/me', { headers: token ? { Authorization: `Bearer ${token}` } : {} })
      .then(r => r.ok ? r.json() : null)
      .then(u => { setUser(u || false); setAuthDone(true); })
      .catch(() => { setUser(false); setAuthDone(true); });
  }, []);

  // Carrega grafo após auth confirmada
  useEffect(() => {
    if (!authDone || !user) return;
    const token = localStorage.getItem('neura-token') || '';
    fetch('/api/graph', { headers: token ? { Authorization: `Bearer ${token}` } : {} })
      .then(r => r.json())
      .then(data => setGraph(data))
      .catch(() => setGraph(window.NEURA_GRAPH || null));
  }, [authDone, user]);

  if (!authDone) return (
    <div className="neura-app" style={{ display:'flex', alignItems:'center', justifyContent:'center', color:'var(--ink-mute)', fontFamily:'var(--font-mono)', fontSize:13 }}>
      carregando…
    </div>
  );

  // Mostrar login apenas se auth está habilitada (server retornou 401)
  if (user === false) return <LoginScreen onLogin={u => { setUser(u); }} />;

  if (!graph) return (
    <div className="neura-app" style={{ display:'flex', alignItems:'center', justifyContent:'center', color:'var(--ink-mute)', fontFamily:'var(--font-mono)', fontSize:13 }}>
      carregando grafo…
    </div>
  );
  return <NeuraAppInner graph={graph} nodeStyle={nodeStyle} currentUser={user} />;
}

function NeuraAppInner({ graph, nodeStyle, currentUser }) {
  const [selected,     setSelected]     = useState(() => graph.agents[0]?.id || 'core');
  const [hover,        setHover]        = useState(null);
  const [query,        setQuery]        = useState('');
  const [layoutKind,   setLayoutKind]   = useState('force');
  const [layoutSeed,   setLayoutSeed]   = useState(7);
  const [zoom,         setZoom]         = useState(1);
  const [pan,          setPan]          = useState({ x:0, y:0 });
  const [kindFilters,  setKindFilters]  = useState({ core:true, agent:true, skill:true, workflow:true });
  const [size,         setSize]         = useState({ w:900, h:600 });
  const [nodeOverrides,setNodeOverrides]= useState({});
  const [draggingIds,  setDraggingIds]  = useState(new Set());
  const [showChat,    setShowChat]    = useState(false);
  const [showHistory, setShowHistory] = useState(false);
  const [showMonitor, setShowMonitor] = useState(false);

  const authToken = localStorage.getItem('neura-token') || '';
  // Helper para fetch autenticado
  const aFetch = (url, opts = {}) => {
    const headers = { ...(opts.headers || {}), ...(authToken ? { Authorization: `Bearer ${authToken}` } : {}) };
    return fetch(url, { ...opts, headers });
  };

  const canvasRef   = useRef(null);
  const panDragRef  = useRef(null);
  const nodeDragRef = useRef(null);

  useEffect(() => {
    setSelected(sel => (graph.nodes.some(n => n.id === sel) ? sel : (graph.agents[0]?.id || 'core')));
  }, [graph]);

  useEffect(() => {
    if (!canvasRef.current) return;
    const ro = new ResizeObserver(() => {
      const r = canvasRef.current.getBoundingClientRect();
      setSize({ w: Math.max(400, r.width), h: Math.max(300, r.height) });
    });
    ro.observe(canvasRef.current);
    return () => ro.disconnect();
  }, []);

  const visibleNodes = useMemo(() =>
    graph.nodes.filter(n => {
      if (!kindFilters[n.kind]) return false;
      if (query && !n.name.toLowerCase().includes(query.toLowerCase()) && !n.id.includes(query.toLowerCase())) return false;
      return true;
    }),
  [graph.nodes, kindFilters, query]);

  const visibleIds   = useMemo(() => new Set(visibleNodes.map(n => n.id)), [visibleNodes]);
  const visibleEdges = useMemo(() => graph.edges.filter(e => visibleIds.has(e.source) && visibleIds.has(e.target)), [graph.edges, visibleIds]);

  const layout    = useLayout(visibleNodes, visibleEdges, layoutKind, size, layoutSeed);
  const neighbors = useMemo(() => buildNeighbors(visibleEdges), [visibleEdges]);

  const counts = useMemo(() => {
    const c = { core:0, agent:0, skill:0, workflow:0 };
    graph.nodes.forEach(n => { c[n.kind] = (c[n.kind]||0)+1; });
    return c;
  }, [graph.nodes]);

  const focusId        = hover || selected;
  const focusNeighbors = focusId ? (neighbors.get(focusId)||new Set()) : new Set();
  const selectedNode   = graph.nodes.find(n => n.id===selected);

  const toggleKind = k => setKindFilters(f => ({ ...f, [k]: !f[k] }));

  // ── Node drag (puxa vizinhos junto) ──
  const onNodeDragStart = (e, nodeId, currentPos) => {
    const neighborIds = graph.edges
      .filter(ed => ed.source === nodeId || ed.target === nodeId)
      .map(ed => ed.source === nodeId ? ed.target : ed.source);
    const dragNodes = [{ id: nodeId, px: currentPos.x, py: currentPos.y }];
    neighborIds.forEach(nid => {
      const p = nodeOverrides[nid] || layout?.[nid];
      if (p) dragNodes.push({ id: nid, px: p.x, py: p.y });
    });
    nodeDragRef.current = { sx: e.clientX, sy: e.clientY, nodes: dragNodes };
    setDraggingIds(new Set(dragNodes.map(d => d.id)));
  };

  // ── Canvas pan / wheel ──
  const onCanvasMouseDown = e => {
    if (e.target.closest('.node')) return;
    panDragRef.current = { sx: e.clientX, sy: e.clientY, px: pan.x, py: pan.y };
  };

  const onMouseMove = e => {
    if (nodeDragRef.current) {
      const { sx, sy, nodes } = nodeDragRef.current;
      const dx = (e.clientX - sx) / zoom;
      const dy = (e.clientY - sy) / zoom;
      const updates = {};
      nodes.forEach(({ id, px, py }) => { updates[id] = { x: px + dx, y: py + dy }; });
      setNodeOverrides(prev => ({ ...prev, ...updates }));
      return;
    }
    if (panDragRef.current) {
      const { sx, sy, px, py } = panDragRef.current;
      setPan({ x: px + (e.clientX - sx), y: py + (e.clientY - sy) });
    }
  };

  const onMouseUp = () => {
    nodeDragRef.current = null;
    panDragRef.current  = null;
    setDraggingIds(new Set());
  };

  const onWheel = e => {
    e.preventDefault();
    setZoom(z => Math.max(0.2, Math.min(3, z - e.deltaY * 0.001)));
  };

  return (
    <div className="neura-app">
      <Header
        query={query} setQuery={setQuery}
        onReorganize={() => { setNodeOverrides({}); setLayoutSeed(s => s + Math.ceil(Math.random()*97)+3); }}
        onOpenChat={() => setShowChat(true)}
        onOpenHistory={() => setShowHistory(true)}
        onOpenMonitor={() => setShowMonitor(true)}
        currentUser={currentUser}
      />
      {showMonitor && <MonitorPanel onClose={() => setShowMonitor(false)} />}
      {showChat    && <ChatN8n onClose={() => setShowChat(false)} />}
      {showHistory && (
        <HistoryPanel
          onClose={() => setShowHistory(false)}
          authToken={authToken}
          agents={graph.agents}
        />
      )}
      <div className="na-body">
        <LeftNav kindFilters={kindFilters} toggleKind={toggleKind}
          activeList={graph.agents.map(a => ({ ...a, kind:'agent' }))}
          selected={selected} setSelected={setSelected} counts={counts}
        />
        <div className="na-canvas" ref={canvasRef}
          onMouseDown={onCanvasMouseDown} onMouseMove={onMouseMove}
          onMouseUp={onMouseUp} onMouseLeave={onMouseUp} onWheel={onWheel}>
          <GraphSVG
            nodes={visibleNodes} edges={visibleEdges}
            layout={layout} size={size} zoom={zoom} pan={pan}
            selected={selected} setSelected={setSelected}
            hover={hover} setHover={setHover}
            focusId={focusId} focusNeighbors={focusNeighbors}
            nodeStyle={nodeStyle}
            nodeOverrides={nodeOverrides}
            onNodeDragStart={onNodeDragStart}
            draggingIds={draggingIds}
            agentCount={graph.agents.length}
          />
          <CanvasOverlay layoutKind={layoutKind} setLayoutKind={setLayoutKind}/>
        </div>
        <Inspector node={selectedNode} graph={graph} aFetch={aFetch} canEdit={agentId => {
          if (!currentUser) return false;
          if (currentUser.agents === '*') return true;
          if (Array.isArray(currentUser.agents)) return currentUser.agents.includes(agentId);
          return false;
        }} />
      </div>
    </div>
  );
}

window.NeuraApp = NeuraApp;
