// AGENTFORGE-IA — Editor visual
const Builder = ({ data, editAgent, setRoute }) => {
  const initialNodes = React.useMemo(() => ([
    { id: 'n1', x: 60, y: 80, kind: 'disparador', icon: 'webhook', name: 'Webhook · ticket.created', color: '#60a5fa', kv: [['canal', 'webhook'], ['filtro', 'prioridad>=3']], tags: ['disparador'] },
    { id: 'n2', x: 360, y: 80, kind: 'herramienta', icon: 'database', name: 'Buscar cliente', color: '#34d399', kv: [['fuente', 'snowflake'], ['tabla', 'clientes']], tags: ['lectura'] },
    { id: 'n3', x: 660, y: 80, kind: 'llm', icon: 'sparkles', name: 'Claude · clasificar + redactar', color: '#ff5b1f', kv: [['modelo', 'claude-3.5-sonnet'], ['temp', '0,2'], ['tokens', '2.400']], tags: ['LLM', 'razonamiento'] },
    { id: 'n4', x: 960, y: 30, kind: 'rama', icon: 'fork', name: '¿gravedad ≥ P2?', color: '#a78bfa', kv: [['ruta_a', '→ escalar'], ['ruta_b', '→ auto-respuesta']], tags: ['rama'] },
    { id: 'n5', x: 1240, y: -10, kind: 'herramienta', icon: 'plug', name: 'Notificar · alerta #cs-leads', color: '#f472b6', kv: [['canal', '#cs-leads'], ['mención', '@guardia']], tags: ['notificar'] },
    { id: 'n6', x: 1240, y: 110, kind: 'herramienta', icon: 'send', name: 'Responder · publicar respuesta', color: '#60a5fa', kv: [['público', 'sí'], ['marcar_resuelto', 'auto']], tags: ['escritura'] },
    { id: 'n7', x: 660, y: 270, kind: 'memoria', icon: 'memory', name: 'Memoria de cliente', color: '#fbbf24', kv: [['ámbito', 'por cliente'], ['ttl', '90 d']], tags: ['memoria'] },
  ]), []);

  const initialEdges = React.useMemo(() => ([
    { from: 'n1', to: 'n2' },
    { from: 'n2', to: 'n3' },
    { from: 'n3', to: 'n4' },
    { from: 'n4', to: 'n5' },
    { from: 'n4', to: 'n6' },
    { from: 'n7', to: 'n3' },
  ]), []);

  const notify = (msg) => window.afNotify && window.afNotify(msg);
  const LEGACY_STORAGE_KEY = 'af_builder_state_v24';
  const SEED_KEY = 'af_builder_seed_v1';
  const BROKEN_PROMPT_PATTERNS = [
    /gimnasio de nivel 1/i,
    /clasifica el ticket en\s*\{facturación,\s*técnico,\s*cuenta,\s*abuso\}/i,
    /nunca prometas un reembolso/i,
  ];

  const sanitizePrompt = React.useCallback((prompt, fallback = '') => {
    const raw = String(prompt || '').trim();
    const isBroken = BROKEN_PROMPT_PATTERNS.some((rx) => rx.test(raw));
    if (isBroken) {
      return (
        String(fallback || '').trim() ||
        'Eres un asistente profesional. Responde de forma clara, precisa y sin inventar datos.'
      );
    }
    return raw;
  }, []);

  const readCreationSeed = () => {
    try {
      const raw = window.localStorage.getItem(SEED_KEY);
      if (!raw) return null;
      const parsed = JSON.parse(raw);
      // Acepta seeds de Onboarding y de Library (Editar agente)
      return parsed && (parsed.fromOnboarding || parsed.fromLibrary) ? parsed : null;
    } catch (_) {
      return null;
    }
  };

  const creationSeed = React.useMemo(() => readCreationSeed(), []);
  const hasExplicitAgentContext = !!(
    editAgent?.id ||
    editAgent?.agent?.id ||
    creationSeed?.fromLibrary ||
    creationSeed?.fromOnboarding
  );
  // Si venimos de "Editar" desde Library, el seed ya fue escrito por handleEditAgent.
  // isEditMode=true en seed indica que debemos mostrar el nombre del agente real, no demo.
  const isEditMode = !!(creationSeed && creationSeed.isEditMode && creationSeed.fromLibrary);
  const startBlank = !hasExplicitAgentContext || !!(creationSeed && creationSeed.blank);
  const seedAgentName = creationSeed?.agentName || null;
  const agentContextKey = React.useMemo(
    () => String(editAgent?.id || creationSeed?.agentId || creationSeed?.agentSlug || 'default'),
    [editAgent?.id, creationSeed?.agentId, creationSeed?.agentSlug]
  );
  const STORAGE_KEY = React.useMemo(
    () => `${LEGACY_STORAGE_KEY}_${agentContextKey}`,
    [agentContextKey]
  );

  /**
   * Carga estado del editor para el agente actual.
   * @returns {null|Record<string, any>}
   */
  const readScopedState = () => {
    try {
      const scoped = window.localStorage.getItem(STORAGE_KEY);
      if (scoped) {
        const parsed = JSON.parse(scoped);
        if (parsed && Array.isArray(parsed.nodes) && Array.isArray(parsed.edges)) return parsed;
      }
      return null;
    } catch (_) {
      return null;
    }
  };

  const savedScoped = React.useMemo(() => readScopedState(), [STORAGE_KEY]);
  const seedFlowNodes = React.useMemo(
    () => (Array.isArray(creationSeed?.flowNodes) ? creationSeed.flowNodes : []),
    [creationSeed?.flowNodes]
  );
  const seedFlowEdges = React.useMemo(
    () => (Array.isArray(creationSeed?.flowEdges) ? creationSeed.flowEdges : []),
    [creationSeed?.flowEdges]
  );
  const usableSavedScoped = React.useMemo(() => {
    if (!savedScoped) return null;
    if (isEditMode && (!Array.isArray(savedScoped.nodes) || savedScoped.nodes.length === 0)) return null;
    return savedScoped;
  }, [isEditMode, savedScoped]);
  const starterNodes = React.useMemo(() => {
    if (startBlank) return [];
    if (seedFlowNodes.length > 0) return seedFlowNodes;
    if (!isEditMode || usableSavedScoped) return initialNodes;
    const displayName = editAgent?.agent?.name || seedAgentName || 'Agente';
    return [
      { id: 'n1', x: 80, y: 90, kind: 'disparador', icon: 'chat', name: `Entrada de ${displayName}`, color: '#60a5fa', kv: [['canal', 'web'], ['modo', 'conversacional']], tags: ['disparador'] },
      { id: 'n2', x: 390, y: 90, kind: 'memoria', icon: 'memory', name: 'Contexto del negocio', color: '#fbbf24', kv: [['origen', 'personalización cliente'], ['prioridad', 'alta']], tags: ['memoria'] },
      { id: 'n3', x: 700, y: 90, kind: 'llm', icon: 'sparkles', name: `${displayName} · razonamiento`, color: '#ff5b1f', kv: [['modelo', 'claude-3.5-sonnet'], ['temp', '0,2'], ['tokens', '2400']], tags: ['LLM'] },
      { id: 'n4', x: 1010, y: 90, kind: 'salida', icon: 'send', name: 'Respuesta al cliente', color: '#34d399', kv: [['formato', 'natural'], ['tono', 'adaptativo']], tags: ['salida'] },
    ];
  }, [isEditMode, usableSavedScoped, startBlank, initialNodes, editAgent?.agent?.name, seedAgentName, seedFlowNodes]);
  const starterEdges = React.useMemo(() => {
    if (startBlank) return [];
    if (seedFlowEdges.length > 0) return seedFlowEdges;
    if (!isEditMode || usableSavedScoped) return initialEdges;
    return [
      { from: 'n1', to: 'n2' },
      { from: 'n2', to: 'n3' },
      { from: 'n3', to: 'n4' },
    ];
  }, [isEditMode, usableSavedScoped, startBlank, initialEdges, seedFlowEdges]);

  const [agentName, setAgentName] = React.useState(
    editAgent?.agent?.name || seedAgentName || usableSavedScoped?.agentName || 'Nuevo agente'
  );
  const [agentTagline, setAgentTagline] = React.useState(
    editAgent?.agent?.tagline || creationSeed?.agentTagline || usableSavedScoped?.agentTagline || ''
  );
  const [agentCategory, setAgentCategory] = React.useState(
    editAgent?.agent?.cat || editAgent?.agent?.category || creationSeed?.category || usableSavedScoped?.agentCategory || 'ops'
  );
  const [nodes, setNodes] = React.useState(usableSavedScoped?.nodes || starterNodes);
  const [edges, setEdges] = React.useState(usableSavedScoped?.edges || starterEdges);
  const [selected, setSelected] = React.useState(usableSavedScoped?.selected || (starterNodes[2]?.id || starterNodes[0]?.id || null));
  const [running, setRunning] = React.useState(false);
  const [zoom, setZoom] = React.useState(usableSavedScoped?.zoom || 100);
  const [tab, setTab] = React.useState(usableSavedScoped?.tab || 'diseño');
  const [mode, setMode] = React.useState(usableSavedScoped?.mode || 'puntero');
  const [showMiniMap, setShowMiniMap] = React.useState(false);
  const [showVars, setShowVars] = React.useState(false);
  const [showNodeMenu, setShowNodeMenu] = React.useState(false);
  const [showToolPicker, setShowToolPicker] = React.useState(false);
  const [showExampleFlow, setShowExampleFlow] = React.useState(false);
  const [showGuide, setShowGuide] = React.useState(true);
  const [publishedAgent, setPublishedAgent] = React.useState(null);
  const [toolConfigIdx, setToolConfigIdx] = React.useState(null);
  const [palQuery, setPalQuery] = React.useState('');
  const [linkFrom, setLinkFrom] = React.useState(null);
  const [selectedEdge, setSelectedEdge] = React.useState(null);
  const [leftWidth, setLeftWidth] = React.useState(savedScoped?.leftWidth || 240);
  const [rightWidth, setRightWidth] = React.useState(savedScoped?.rightWidth || 320);
  const [consoleHeight, setConsoleHeight] = React.useState(savedScoped?.consoleHeight || 170);
  const [versionNotes, setVersionNotes] = React.useState(savedScoped?.versionNotes || 'Ajustes iniciales del flujo');
  const [webhookOutUrl, setWebhookOutUrl] = React.useState(savedScoped?.webhookOutUrl || 'https://webhook.site/your-test-url');
  const [webhookOutMethod, setWebhookOutMethod] = React.useState(savedScoped?.webhookOutMethod || 'POST');
  const [webhookOutBody, setWebhookOutBody] = React.useState(savedScoped?.webhookOutBody || '{"event":"agentforge.flow.updated","source":"editor"}');
  const [webhookOutSecret, setWebhookOutSecret] = React.useState(savedScoped?.webhookOutSecret || '');
  const [knowledgeUrls, setKnowledgeUrls] = React.useState(
    savedScoped?.knowledgeUrls ||
    (Array.isArray(creationSeed?.contextUrls) ? creationSeed.contextUrls : [])
  );
  const [knowledgeDocs, setKnowledgeDocs] = React.useState(
    savedScoped?.knowledgeDocs ||
    (Array.isArray(creationSeed?.contextFiles)
      ? creationSeed.contextFiles.map((f) => ({
          name: f.name || 'documento',
          size: Number(f.size || 0),
          type: f.type || '',
        }))
      : [])
  );
  const [kbUrlInput, setKbUrlInput] = React.useState('');
  const dragRef = React.useRef(null);
  const resizeRef = React.useRef(null);

  const [temperature, setTemperature] = React.useState(savedScoped?.temperature ?? 0.2);
  const [maxTokens, setMaxTokens] = React.useState(savedScoped?.maxTokens ?? 2400);
  const [systemPrompt, setSystemPrompt] = React.useState(
    (creationSeed?.contextNotes
      ? `${creationSeed?.systemPrompt || ''}\n\nContexto adicional aportado por el usuario:\n${creationSeed.contextNotes}`.trim()
      : creationSeed?.systemPrompt) ||
    savedScoped?.systemPrompt ||
    'Eres un asistente profesional. Responde de forma clara, precisa y sin inventar datos.'
  );
  React.useEffect(() => {
    setSystemPrompt((prev) =>
      sanitizePrompt(
        prev,
        creationSeed?.systemPrompt || savedScoped?.systemPrompt || 'Eres un asistente profesional. Responde de forma clara, precisa y sin inventar datos.'
      )
    );
  }, [sanitizePrompt, creationSeed?.systemPrompt, savedScoped?.systemPrompt]);
  const [safeguards, setSafeguards] = React.useState(savedScoped?.safeguards || { pii: true, humanApproval: true, strictJson: false });
  const lastCommittedRef = React.useRef('');

  const snapshot = React.useCallback(
    () =>
      JSON.stringify({
        nodes,
        edges,
        selected,
        zoom,
        tab,
        mode,
        agentName,
        agentTagline,
        agentCategory,
        temperature,
        maxTokens,
        systemPrompt,
        safeguards,
        tools,
        vars,
        leftWidth,
        rightWidth,
        consoleHeight,
        versionNotes,
        webhookOutUrl,
        webhookOutMethod,
        webhookOutBody,
        webhookOutSecret,
        knowledgeUrls,
        knowledgeDocs,
      }),
    [
      nodes, edges, selected, zoom, tab, mode,
      agentName, agentTagline, agentCategory, temperature, maxTokens,
      systemPrompt, safeguards, tools, vars, leftWidth, rightWidth,
      consoleHeight, versionNotes, webhookOutUrl, webhookOutMethod,
      webhookOutBody, webhookOutSecret, knowledgeUrls, knowledgeDocs,
    ]
  );
  const isDirty = lastCommittedRef.current && lastCommittedRef.current !== snapshot();

  const [tools, setTools] = React.useState(savedScoped?.tools || [
    { n: 'snowflake.query', d: 'solo lectura · limitado al esquema clientes', c: '#34d399', enabled: true },
    { n: 'hubspot.contact', d: 'alta/actualización de contacto comercial', c: '#ff7a59', enabled: true },
    { n: 'memory.recall', d: 'ámbito por cliente, TTL 90 d', c: '#fbbf24', enabled: true },
    { n: 'webhook.out', d: 'llamada HTTP saliente con payload JSON', c: '#a78bfa', enabled: true },
  ]);

  const [vars, setVars] = React.useState(savedScoped?.vars || [
    { k: 'workspace', v: 'prod-es' },
    { k: 'owner', v: 'Naomi M.' },
    { k: 'approval_threshold', v: '200' },
  ]);

  const [consoleLines, setConsoleLines] = React.useState([
    '[disparador]   webhook.ticket.created  id=#48207 prioridad=P2',
    '[búsqueda]     snowflake.clientes      fila=842 ms=124',
    '[claude]       clasificar(categoría="facturación.reembolso") confianza=0,94',
    '[claude]       borrador_respuesta(tono="empático", tokens=312)',
    '[rama]         gravedad=P2 → ruta_b (auto-respuesta)',
  ]);

  const [deployMeta, setDeployMeta] = React.useState({
    status: 'borrador',
    version: 'v2.4-beta',
    updatedAgo: 'hace 12 s',
  });

  React.useEffect(() => {
    if (!lastCommittedRef.current) {
      lastCommittedRef.current = snapshot();
    }
  }, [snapshot]);

  React.useEffect(() => {
    // Evita reutilizar un seed antiguo al volver al editor directamente.
    try { window.localStorage.removeItem(SEED_KEY); } catch (_) {}
    // Limpia también estado legado global para evitar contaminación entre agentes.
    try { window.localStorage.removeItem(LEGACY_STORAGE_KEY); } catch (_) {}
  }, []);

  const persistAgentIdentity = async () => {
    const token = sessionStorage.getItem('af_token') || '';
    const targetAgentId =
      editAgent?.agentDbId ||
      editAgent?.agent?.agentDbId ||
      creationSeed?.agentId ||
      publishedAgent?.id ||
      null;
    const targetAgentSlug =
      editAgent?.id ||
      editAgent?.agent?.id ||
      editAgent?.agent?.slug ||
      creationSeed?.agentSlug ||
      publishedAgent?.slug ||
      '';
    const cleanName = agentName.trim();
    if (!cleanName) return false;
    const persistLocalAgentName = (agentId, agentSlug, updatedName, updatedTagline) => {
      const nextName = updatedName || cleanName;
      const nextTagline = updatedTagline || agentTagline || '';
      const matchesAgent = (agent) => (
        String(agent?.id || '') === String(agentSlug || '') ||
        String(agent?.slug || '') === String(agentSlug || '') ||
        String(agent?.agentDbId || '') === String(agentId || '') ||
        String(agent?.dbId || '') === String(agentId || '')
      );
      try {
        const raw = localStorage.getItem('af_custom_agents_v1') || '[]';
        const list = JSON.parse(raw);
        if (Array.isArray(list)) {
          const next = list.map((agent) => matchesAgent(agent)
            ? { ...agent, name: nextName, tagline: nextTagline || agent.tagline || '' }
            : agent);
          localStorage.setItem('af_custom_agents_v1', JSON.stringify(next));
        }
      } catch (_) {}
      try {
        const latest = JSON.parse(localStorage.getItem('af_last_created_agent') || 'null');
        if (latest && matchesAgent(latest)) {
          localStorage.setItem('af_last_created_agent', JSON.stringify({
            ...latest,
            name: nextName,
            tagline: nextTagline || latest.tagline || '',
          }));
        }
      } catch (_) {}
    };
    const dispatchRename = (agentId, agentSlug, updatedName, updatedTagline) => {
      persistLocalAgentName(agentId, agentSlug, updatedName, updatedTagline);
      const detail = {
        agentId: String(agentId || ''),
        agentSlug: String(agentSlug || ''),
        agentName: updatedName || cleanName,
        agentTagline: updatedTagline || agentTagline,
      };
      try { window.dispatchEvent(new CustomEvent('af:agent:renamed', { detail })); } catch (_) {}
      try { window.dispatchEvent(new CustomEvent('af:library:refresh')); } catch (_) {}
      try { window.dispatchEvent(new CustomEvent('af:catalog:refresh')); } catch (_) {}
    };
    if (!token || !targetAgentId) {
      if (!targetAgentSlug) return false;
      dispatchRename('', targetAgentSlug, cleanName, agentTagline);
      pushLog('[metadata]     nombre visible actualizado');
      return true;
    }
    try {
      let res = await fetch('/api/v1/purchases/' + targetAgentId, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
        body: JSON.stringify({
          display_name: cleanName,
          display_tagline: agentTagline.trim() || null,
        }),
      });
      if (!res.ok && (res.status === 403 || res.status === 404)) {
        res = await fetch('/api/v1/agents/' + targetAgentId, {
          method: 'PATCH',
          headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
          body: JSON.stringify({
            name: cleanName,
            tagline: agentTagline.trim() || null,
          }),
        });
      }
      if (!res.ok) {
        let reason = `HTTP ${res.status}`;
        try {
          const payload = await res.json();
          reason = payload?.detail || reason;
        } catch (_) {}
        pushLog(`[metadata]     no se pudo guardar nombre: ${reason}`);
        return false;
      }
      const updated = await res.json().catch(() => null);
      const updatedName = updated?.agent_name || updated?.name;
      const updatedTagline = updated?.agent_tagline || updated?.tagline || agentTagline;
      if (updatedName) {
        try {
          localStorage.setItem(SEED_KEY, JSON.stringify({
            ...(creationSeed || {}),
            agentName: updatedName,
            agentTagline: updatedTagline,
            agentId: targetAgentId,
            agentSlug: targetAgentSlug,
          }));
        } catch (_) {}
      }
      pushLog('[metadata]     nombre visible actualizado');
      dispatchRename(targetAgentId, targetAgentSlug, updatedName, updatedTagline);
      return true;
    } catch (_) {
      return false;
    }
  };

  const sel = nodes.find((n) => n.id === selected) || nodes[0] || null;
  const guideSteps = [
    '1) Izquierda: elige bloques (entrada, herramientas, salida).',
    '2) Centro: conecta bloques para definir el flujo.',
    '3) Derecha: configura modelo, prompt y conocimiento.',
  ];

  React.useEffect(() => {
    if (!running) return;
    const t = setTimeout(() => {
      setRunning(false);
      setConsoleLines((prev) => [...prev.slice(-8), '[ok]           ejecución completada sin errores']);
    }, 2500);
    return () => clearTimeout(t);
  }, [running]);

  const portPos = (n, side) => ({ x: n.x + (side === 'out' ? 220 : 0), y: n.y + 36 });

  const pushLog = (line) => {
    setConsoleLines((prev) => [...prev.slice(-10), line]);
  };

  const runTest = () => {
    setRunning(true);
    pushLog('[test]         suite rápida iniciada en sandbox del flujo');
    notify('Prueba rápida en curso');
  };

  const runWebhookOut = async () => {
    const tool = tools.find((t) => t.n === 'webhook.out');
    if (!tool || !tool.enabled) {
      notify('Habilita webhook.out antes de ejecutar');
      return;
    }

    let parsedBody = {};
    try {
      parsedBody = webhookOutBody.trim() ? JSON.parse(webhookOutBody) : {};
    } catch (_) {
      notify('JSON inválido en body del webhook');
      return;
    }

    try {
      pushLog(`[webhook]      ${webhookOutMethod} ${webhookOutUrl} ...`);
      const res = await fetch('/api/v1/tools/webhook/out', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          url: webhookOutUrl,
          method: webhookOutMethod,
          body: parsedBody,
          secret: webhookOutSecret || null,
        }),
      });
      const data = await res.json();
      if (!res.ok) {
        const detail = data?.detail || `Error ${res.status}`;
        pushLog(`[webhook]      error: ${detail}`);
        notify('Webhook salida falló');
        return;
      }
      pushLog(`[webhook]      OK status=${data.status_code} ${data.latency_ms}ms`);
      notify('Webhook salida ejecutado');
    } catch (err) {
      pushLog(`[webhook]      error de red: ${err?.message || err}`);
      notify('Error de red en webhook salida');
    }
  };

  const publishAgent = async (options = {}) => {
    const goToDeployment = !!options.goToDeployment;
    const token = sessionStorage.getItem('af_token');
    if (!token) { notify('Inicia sesión para publicar'); return; }
    if (isEditMode) {
      const savedIdentity = await persistAgentIdentity();
      const targetAgentId =
        editAgent?.agentDbId ||
        editAgent?.agent?.agentDbId ||
        creationSeed?.agentId ||
        publishedAgent?.id ||
        null;
      if (!goToDeployment || targetAgentId) {
        notify(savedIdentity ? 'Agente actualizado' : 'Cambios locales guardados');
      }
      if (goToDeployment && targetAgentId) {
        try {
          sessionStorage.setItem('af_deploy_agent', JSON.stringify({
            agentId: targetAgentId,
            agentSlug: publishedAgent?.slug || editAgent?.id || editAgent?.agent?.id || creationSeed?.agentSlug || '',
            agentName: publishedAgent?.name || agentName || 'Agente',
          }));
        } catch (_) {}
        pushLog('[deploy]       enviado a Implantación para elegir canal real');
        notify('Elige canal en Implantación: Web, WhatsApp o Telegram');
        if (setRoute) setRoute('deployment');
      }
      if (!goToDeployment || targetAgentId) return;
    }
    const toolNames = tools.filter((t) => t.enabled).map((t) => t.n);
    try {
      pushLog('[publish]      enviando agente al servidor...');
      const res = await fetch('/api/v1/agents/create', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
        body: JSON.stringify({
          name: agentName,
          category: agentCategory || 'ops',
          tagline: agentTagline || agentName,
          system_prompt: systemPrompt,
          flow_nodes: nodes,
          flow_edges: edges,
          tools: toolNames,
          tags: [],
          color: '#ff5b1f',
          knowledge_urls: knowledgeUrls,
          knowledge_docs: knowledgeDocs.map((d) => d.name),
        }),
      });
      const data = await res.json();
      if (!res.ok) { notify(data?.detail || 'Error al publicar'); pushLog(`[publish]      error: ${data?.detail || res.status}`); return; }
      const published = { id: data.id, slug: data.slug, name: data.name || agentName };
      setPublishedAgent(published);
      try {
        window.dispatchEvent(new CustomEvent('af:catalog:refresh'));
      } catch (_) {}
      pushLog(`[publish]      agente publicado: ${data.slug}`);
      pushLog('[publish]      catálogo refrescado (Marketplace/Mis agentes)');
      notify(goToDeployment ? 'Agente guardado. Abriendo Implantación' : 'Agente publicado y catálogo actualizado');
      if (goToDeployment) {
        try {
          sessionStorage.setItem('af_deploy_agent', JSON.stringify({
            agentId: published.id,
            agentSlug: published.slug,
            agentName: published.name || agentName || 'Agente',
          }));
        } catch (_) {}
        if (setRoute) setRoute('deployment');
      }
    } catch (err) {
      pushLog(`[publish]      error de red: ${err?.message || err}`);
      notify('Error de red al publicar');
    }
  };

  const deployFlow = async () => {
    const token = sessionStorage.getItem('af_token');
    if (!token) { notify('Inicia sesión para desplegar'); return; }
    const targetAgentId =
      publishedAgent?.id ||
      editAgent?.agentDbId ||
      editAgent?.agent?.agentDbId ||
      creationSeed?.agentId ||
      null;
    if (!targetAgentId) {
      pushLog('[deploy]       publicando agente antes de implantar');
      await publishAgent({ goToDeployment: true });
      return;
    }
    try {
      sessionStorage.setItem('af_deploy_agent', JSON.stringify({
        agentId: targetAgentId,
        agentSlug: publishedAgent?.slug || editAgent?.id || editAgent?.agent?.id || creationSeed?.agentSlug || '',
        agentName: publishedAgent?.name || agentName || 'Agente',
      }));
    } catch (_) {}
    pushLog('[deploy]       enviado a Implantación para elegir canal real');
    notify('Elige canal en Implantación: Web, WhatsApp o Telegram');
    if (setRoute) {
      setRoute('deployment');
    }
  };

  const duplicateSelectedNode = () => {
    if (!sel) return;
    const next = `n${Date.now().toString().slice(-4)}`;
    const clone = { ...sel, id: next, x: sel.x + 40, y: sel.y + 90, name: `${sel.name} (copia)` };
    setNodes((prev) => [...prev, clone]);
    setSelected(next);
    pushLog(`[edición]      nodo duplicado: ${sel.id} -> ${next}`);
    notify('Nodo duplicado');
    setShowNodeMenu(false);
  };

  const removeSelectedNode = () => {
    if (!sel || nodes.length <= 1) return;
    const idx = nodes.findIndex((n) => n.id === sel.id);
    const fallback = nodes[Math.max(0, idx - 1)]?.id;
    setNodes((prev) => prev.filter((n) => n.id !== sel.id));
    setEdges((prev) => prev.filter((e) => e.from !== sel.id && e.to !== sel.id));
    setSelected(fallback);
    pushLog(`[edición]      nodo eliminado: ${sel.id}`);
    notify('Nodo eliminado');
    setShowNodeMenu(false);
  };

  const saveInspector = () => {
    if (!sel) {
      notify('Añade o selecciona un nodo');
      return;
    }
    setNodes((prev) => prev.map((n) => {
      if (n.id !== sel.id) return n;
      const nextKv = n.kv.map(([k, v]) => {
        if (k === 'temp') return [k, temperature.toFixed(2).replace('.', ',')];
        if (k === 'tokens') return [k, String(maxTokens)];
        if (k === 'modelo') return [k, n.kind === 'llm' ? (document.getElementById('builder-model')?.value || v) : v];
        return [k, v];
      });
      return { ...n, kv: nextKv };
    }));
    pushLog('[config]       configuración del nodo guardada');
    notify('Configuración guardada');
  };

  const addTool = (tool) => {
    setTools((prev) => [...prev, tool]);
    setShowToolPicker(false);
    pushLog(`[tools]        herramienta añadida: ${tool.n}`);
    notify('Herramienta adjuntada');
  };

  const toggleTool = (idx) => {
    setTools((prev) => prev.map((t, i) => (i === idx ? { ...t, enabled: !t.enabled } : t)));
  };

  const selectedTool = toolConfigIdx != null ? tools[toolConfigIdx] : null;

  const beginDrag = (e, nodeId) => {
    if (!(mode === 'mover' || mode == 'puntero')) return;
    if (e.target.classList.contains('flow-port')) return;
    e.preventDefault();
    e.stopPropagation();
    const node = nodes.find((n) => n.id === nodeId);
    if (!node) return;
    dragRef.current = { nodeId, dx: e.clientX - node.x, dy: e.clientY - node.y };
  };

  React.useEffect(() => {
    const onMove = (e) => {
      if (!dragRef.current) return;
      const { nodeId, dx, dy } = dragRef.current;
      setNodes((prev) => prev.map((n) => n.id === nodeId ? { ...n, x: Math.max(20, e.clientX - dx), y: Math.max(20, e.clientY - dy) } : n));
    };
    const onUp = () => {
      if (dragRef.current) {
        const gid = dragRef.current.nodeId;
        setNodes((prev) => prev.map((n) => n.id === gid ? { ...n, x: Math.round(n.x / 10) * 10, y: Math.round(n.y / 10) * 10 } : n));
        pushLog('[edición]      nodo reposicionado');
      }
      dragRef.current = null;
    };
    window.addEventListener('mousemove', onMove);
    window.addEventListener('mouseup', onUp);
    return () => {
      window.removeEventListener('mousemove', onMove);
      window.removeEventListener('mouseup', onUp);
    };
  }, [mode]);

  const startLink = (e, nodeId) => {
    e.stopPropagation();
    setLinkFrom(nodeId);
    notify('Selecciona nodo destino para enlazar');
  };

  const finishLink = (e, toId) => {
    e.stopPropagation();
    if (!linkFrom || linkFrom === toId) return;
    setEdges((prev) => {
      const exists = prev.some((ed) => ed.from === linkFrom && ed.to === toId);
      if (exists) return prev;
      return [...prev, { from: linkFrom, to: toId }];
    });
    pushLog(`[flujo]        enlace creado: ${linkFrom} -> ${toId}`);
    setSelectedEdge(`${linkFrom}->${toId}`);
    setLinkFrom(null);
  };

  
  const removeSelectedEdge = () => {
    if (!selectedEdge) return;
    const [from, to] = selectedEdge.split('->');
    setEdges((prev) => prev.filter((e) => !(e.from === from && e.to === to)));
    pushLog(`[flujo]        enlace eliminado: ${selectedEdge}`);
    setSelectedEdge(null);
  };

  React.useEffect(() => {
    const onKeyDown = (e) => {
      const tag = (e.target && e.target.tagName ? e.target.tagName.toLowerCase() : '');
      const inInput = tag === 'input' || tag === 'textarea' || tag === 'select' || (e.target && e.target.isContentEditable);
      if (inInput) return;

      if (e.key === 'Delete' || e.key === 'Backspace') {
        if (selectedEdge) {
          e.preventDefault();
          removeSelectedEdge();
          return;
        }
        if (selected) {
          e.preventDefault();
          removeSelectedNode();
        }
      }
    };

    window.addEventListener('keydown', onKeyDown);
    return () => window.removeEventListener('keydown', onKeyDown);
  }, [selected, selectedEdge, nodes, edges]);

  const startResize = (type, e) => {
    e.preventDefault();
    resizeRef.current = { type, x: e.clientX, y: e.clientY, leftWidth, rightWidth, consoleHeight };
  };

  React.useEffect(() => {
    const onMove = (e) => {
      if (!resizeRef.current) return;
      const r = resizeRef.current;
      if (r.type == 'left') {
        setLeftWidth(Math.max(200, Math.min(420, r.leftWidth + (e.clientX - r.x))));
      } else if (r.type == 'right') {
        setRightWidth(Math.max(280, Math.min(520, r.rightWidth - (e.clientX - r.x))));
      } else if (r.type == 'bottom') {
        setConsoleHeight(Math.max(120, Math.min(360, r.consoleHeight - (e.clientY - r.y))));
      }
    };
    const onUp = () => { resizeRef.current = null; };
    window.addEventListener('mousemove', onMove);
    window.addEventListener('mouseup', onUp);
    return () => {
      window.removeEventListener('mousemove', onMove);
      window.removeEventListener('mouseup', onUp);
    };
  }, [leftWidth, rightWidth, consoleHeight]);

  const paletteGroups = React.useMemo(() => ([
    { title: 'Disparadores', kind: 'disparador', color: '#60a5fa', items: [['Webhook', 'webhook', 'HTTP'], ['Programado', 'clock', 'cron'], ['Email entrante', 'send', 'SMTP'], ['Evento externo', 'plug', 'evento']] },
    { title: 'Razonamiento', kind: 'llm', color: '#ff5b1f', items: [['Claude · pensar', 'sparkles', 'LLM'], ['Planificar', 'flow', 'agente'], ['Reflexionar', 'eye', 'agente'], ['Decidir', 'fork', 'rama']] },
    { title: 'Herramientas', kind: 'herramienta', color: '#34d399', items: [['Snowflake', 'database', 'SQL'], ['HubSpot', 'users', 'API'], ['Linear', 'grid', 'API'], ['Búsqueda web', 'globe', 'HTTP'], ['Ejecutar código', 'terminal', 'exec']] },
    { title: 'Memoria y estado', kind: 'memoria', color: '#fbbf24', items: [['Vector store', 'memory', 'recuperar'], ['Caché KV', 'database', 'caché'], ['Sesión', 'clock', 'ctx']] },
    { title: 'Salida', kind: 'salida', color: '#a78bfa', items: [['Responder', 'send', 'msg'], ['Webhook salida', 'webhook', 'HTTP'], ['Notificar', 'bell', 'canal']] },
  ]), []);

  const addNodeFromPalette = (name, icon, meta, kind, color) => {
    const id = `n${Date.now().toString().slice(-5)}`;
    const newNode = {
      id,
      x: 180 + (nodes.length % 4) * 280,
      y: 120 + (nodes.length % 3) * 120,
      kind,
      icon,
      name,
      color,
      kv: [['origen', meta], ['estado', 'nuevo']],
      tags: [kind],
    };
    setNodes((prev) => [...prev, newNode]);
    setSelected(id);
    pushLog(`[paleta]       nodo añadido: ${name}`);
    notify(`Bloque añadido: ${name}`);
  };

  React.useEffect(() => {
    pushLog(`[vista]        pestaña activa: ${tab}`);
  }, [tab]);

  React.useEffect(() => {
    const payload = {
      nodes, edges, selected, zoom, tab, mode,
      agentName, agentTagline, agentCategory, temperature, maxTokens, systemPrompt, safeguards, tools, vars, leftWidth, rightWidth, consoleHeight, versionNotes, webhookOutUrl, webhookOutMethod, webhookOutBody, webhookOutSecret,
      knowledgeUrls, knowledgeDocs,
      savedAt: Date.now(),
    };
    const t = setTimeout(() => {
      try {
        window.localStorage.setItem(STORAGE_KEY, JSON.stringify(payload));
      } catch (_) {}
    }, 180);
    return () => clearTimeout(t);
  }, [nodes, edges, selected, zoom, tab, mode, agentName, agentTagline, agentCategory, temperature, maxTokens, systemPrompt, safeguards, tools, vars, leftWidth, rightWidth, consoleHeight, versionNotes, webhookOutUrl, webhookOutMethod, webhookOutBody, webhookOutSecret, knowledgeUrls, knowledgeDocs]);

  const saveNow = async (navigateAfter = false) => {
    try {
      window.localStorage.setItem(STORAGE_KEY, JSON.stringify({
        nodes, edges, selected, zoom, tab, mode,
        agentName, agentTagline, agentCategory, temperature, maxTokens, systemPrompt, safeguards, tools, vars, leftWidth, rightWidth, consoleHeight, versionNotes, webhookOutUrl, webhookOutMethod, webhookOutBody, webhookOutSecret,
        knowledgeUrls, knowledgeDocs,
        savedAt: Date.now(),
      }));
      const backendSaved = await persistAgentIdentity();
      lastCommittedRef.current = snapshot();
      notify(backendSaved ? 'Nombre guardado' : 'Guardado local: no se pudo actualizar backend');
      if (backendSaved && navigateAfter) {
        setRoute('library');
      }
    } catch (_) {
      notify('No se pudo guardar en este navegador');
    }
  };

  const closeEditor = async () => {
    if (!isDirty) {
      setRoute && setRoute('library');
      return;
    }
    const saveBeforeExit = window.confirm('Tienes cambios sin guardar. ¿Quieres guardarlos antes de salir?');
    if (saveBeforeExit) {
      await saveNow(false);
      setRoute && setRoute('library');
      return;
    }
    const discard = window.confirm('¿Salir sin guardar cambios?');
    if (discard) {
      setRoute && setRoute('library');
    }
  };

  const resetFlow = () => {
    const blank = startBlank;
    setNodes(blank ? [] : starterNodes);
    setEdges(blank ? [] : starterEdges);
    setSelected(blank ? null : (starterNodes[2]?.id || starterNodes[0]?.id || null));
    setLinkFrom(null);
    setSelectedEdge(null);
    notify(blank ? 'Lienzo limpio restablecido' : 'Flujo restablecido');
  };

  const loadExampleFlow = () => {
    setNodes(initialNodes);
    setEdges(initialEdges);
    setSelected('n3');
    setTab('diseño');
    setLinkFrom(null);
    setSelectedEdge(null);
    setShowExampleFlow(false);
    notify('Ejemplo cargado en el editor');
    pushLog('[ejemplo]      flujo de referencia cargado');
  };

  return (
    <div className="builder" style={{ gridTemplateColumns: `${leftWidth}px 6px 1fr 6px ${rightWidth}px` }}>
      {isEditMode && (
        <div style={{
          gridColumn: '1 / -1',
          background: 'var(--brand-soft)',
          border: '1px solid var(--brand)',
          borderRadius: 8,
          padding: '8px 14px',
          fontSize: 12.5,
          color: 'var(--ink)',
          display: 'flex',
          alignItems: 'center',
          gap: 8,
          margin: '0 0 12px 0',
        }}>
          <Icon name="edit" size={13} style={{ color: 'var(--brand)' }} />
          <span>Editando agente. Cambia el nombre en el bloque <strong>Nombre visible del agente</strong> y pulsa <strong>Guardar nombre</strong>.</span>
        </div>
      )}
      <div className="bld-pal">
        <div className="pal-search" style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
          <Icon name="search" size={12} />
          <input
            className="input"
            style={{ border: 0, background: 'transparent', height: 22, padding: 0, fontSize: 12 }}
            placeholder="Buscar bloques..."
            value={palQuery}
            onChange={(e) => setPalQuery(e.target.value)}
          />
        </div>
        {paletteGroups.map((g) => {
          const items = g.items.filter(([name, , meta]) => `${name} ${meta}`.toLowerCase().includes(palQuery.toLowerCase()));
          if (!items.length) return null;
          return (
            <React.Fragment key={g.title}>
              <h4>{g.title}</h4>
              {items.map((p, i) => (
                <button
                  key={`${g.title}-${i}`}
                  type="button"
                  className="pal-item"
                  style={{ width: '100%', textAlign: 'left' }}
                  onClick={() => addNodeFromPalette(p[0], p[1], p[2], g.kind, g.color)}
                >
                  <span className="pi-icon" style={{ color: g.color }}><Icon name={p[1]} size={12} /></span>
                  {p[0]}
                  <span className="pi-meta">{p[2]}</span>
                </button>
              ))}
            </React.Fragment>
          );
        })}
      </div>
      <div style={{ cursor: 'col-resize', background: 'linear-gradient(180deg, transparent, var(--brand), transparent)', opacity: 0.95, width: 8 }} onMouseDown={(e) => startResize('left', e)} />

      <div className="bld-canvas" style={{ display: 'flex', flexDirection: 'column', minHeight: 0 }}>
        <div style={{ borderBottom: '1px solid var(--line)', background: 'var(--bg-1)', padding: '10px 14px', display: 'flex', alignItems: 'center', gap: 10 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
            <div className="agent-icon" style={{ width: 26, height: 26, color: '#60a5fa', background: '#60a5fa1a', borderColor: '#60a5fa33' }}><Icon name="headset" size={13} /></div>
            <div>
              <div style={{ fontWeight: 600, fontSize: 13.5, display: 'flex', alignItems: 'center', gap: 8 }}>
                <span>{agentName || 'Nuevo agente'}</span>
                <span className="tag">{deployMeta.version}</span>
              </div>
              <div className="mono" style={{ fontSize: 10.5, color: 'var(--ink-3)' }}>
                atención · {deployMeta.status} · guardado {deployMeta.updatedAgo}
              </div>
            </div>
          </div>
          <div className="canvas-pill" style={{ marginLeft: 12 }}>
            {['diseño', 'prueba', 'versiones', 'desplegar'].map((t) => <button key={t} className={tab === t ? 'active' : ''} onClick={() => { setTab(t); }}>{t}</button>)}
          </div>
          <div style={{ marginLeft: 'auto', display: 'flex', gap: 8 }}>
            <button className="btn" onClick={() => setShowExampleFlow(true)}><Icon name="eye" size={12} /> Ver ejemplo</button>
            <button className={'btn ' + (showGuide ? 'btn-primary' : '')} onClick={() => setShowGuide((v) => !v)}>
              <Icon name="info" size={12} /> {showGuide ? 'Ocultar ayuda' : 'Mostrar ayuda'}
            </button>
            <button className="btn btn-ghost" onClick={duplicateSelectedNode}><Icon name="fork" size={12} /> Bifurcar</button>
            <button className="btn" onClick={removeSelectedNode}><Icon name="x" size={12} /> Eliminar nodo</button>
            <button className="btn" onClick={runTest}><Icon name="play" size={12} /> Ejecutar prueba</button>
            <button className="btn" onClick={runWebhookOut}><Icon name="webhook" size={12} /> Webhook salida</button>
            <button className="btn" onClick={saveNow}><Icon name="check" size={12} /> Guardar</button>
            <button className="btn" onClick={resetFlow}><Icon name="x" size={12} /> Reiniciar</button>
            <button className="btn btn-ghost" onClick={closeEditor}><Icon name="close" size={12} /> Cerrar editor</button>
            <button
              className="btn btn-primary"
              onClick={publishAgent}
              title="Guarda el agente en la plataforma y actualiza Mis agentes / Marketplace, sin iniciar implantación."
            >
              <Icon name="send" size={13} /> Guardar/Publicar
            </button>
            <button
              className="btn btn-primary"
              onClick={deployFlow}
              title="Guarda si hace falta y abre Implantación para elegir Web, WhatsApp o Telegram."
            >
              <Icon name="zap" size={13} /> {running ? 'Ejecutando…' : 'Ir a Implantación'}
            </button>
          </div>
        </div>

        <div style={{
          borderBottom: '1px solid var(--line)',
          background: 'linear-gradient(135deg, rgba(15,23,42,0.92), rgba(255,91,31,0.08))',
          padding: '9px 14px',
          display: 'grid',
          gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
          gap: 10,
        }}>
          <div className="card" style={{ padding: '9px 11px', borderColor: 'rgba(255,91,31,0.28)' }}>
            <div style={{ fontSize: 12, fontWeight: 800 }}><Icon name="send" size={12} /> Guardar/Publicar</div>
            <div style={{ marginTop: 4, color: 'var(--ink-3)', fontSize: 11.5 }}>
              Actualiza el agente y lo deja disponible en Mis agentes y Marketplace, sin desplegarlo aún.
            </div>
          </div>
          <div className="card" style={{ padding: '9px 11px', borderColor: 'rgba(255,91,31,0.42)' }}>
            <div style={{ fontSize: 12, fontWeight: 800 }}><Icon name="zap" size={12} /> Ir a Implantación</div>
            <div style={{ marginTop: 4, color: 'var(--ink-3)', fontSize: 11.5 }}>
              Abre el flujo operativo para activar el agente en Web del cliente, WhatsApp o Telegram.
            </div>
          </div>
        </div>

        {showGuide && (
          <div style={{ borderBottom: '1px solid var(--line)', background: 'linear-gradient(135deg, color-mix(in oklab, var(--brand) 10%, var(--bg-1)), var(--bg-1) 68%)', padding: '10px 14px' }}>
            <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, marginBottom: 6 }}>
              <div style={{ fontSize: 12.5, fontWeight: 700 }}>Guía para usuarios no técnicos</div>
              <button className="btn btn-ghost btn-sm" onClick={() => setShowGuide(false)}><Icon name="x" size={11} /> Cerrar</button>
            </div>
            <div style={{ display: 'grid', gap: 4 }}>
              {guideSteps.map((step, i) => (
                <div key={i} style={{ fontSize: 11.8, color: 'var(--ink-2)' }}>{step}</div>
              ))}
            </div>
            <div style={{ marginTop: 6, fontSize: 11, color: 'var(--ink-3)' }}>
              Recomendado: usa este orden básico: <b>Entrada</b> → <b>Contexto</b> → <b>Razonamiento</b> → <b>Respuesta</b>.
            </div>
          </div>
        )}

        <div style={{
          position: 'relative',
          zIndex: 8,
          borderBottom: '1px solid var(--line)',
          background: 'linear-gradient(135deg, rgba(255,91,31,0.12), var(--bg-2) 44%)',
          padding: '12px 14px',
          display: 'grid',
          gridTemplateColumns: '1fr auto',
          gap: 10,
          alignItems: 'end',
        }}>
          <label style={{ fontSize: 11.5, color: 'var(--ink-2)', fontWeight: 800 }}>
            Nombre del agente
            <input
              className="input"
              value={agentName}
              onChange={(e) => setAgentName(e.target.value)}
              onMouseDown={(e) => e.stopPropagation()}
              onClick={(e) => e.stopPropagation()}
              style={{ marginTop: 6, height: 36, fontWeight: 700, borderColor: 'rgba(255,91,31,0.55)', background: 'rgba(255,255,255,0.055)' }}
              placeholder="Ej: Asistente Cloud Ernesto"
            />
          </label>
          <button className="btn btn-primary" onClick={() => saveNow(true)} style={{ height: 36 }}>
            <Icon name="check" size={12} /> Guardar nombre
          </button>
        </div>

        <div style={{ flex: 1, position: 'relative', overflow: 'auto' }}>
          <div className="bld-canvas-toolbar">
            <div className="canvas-pill">
              <button data-tip="Puntero" data-tip-pos="bottom" className={mode === 'puntero' ? 'active' : ''} onClick={() => setMode('puntero')}><Icon name="arrow-right" size={13} stroke={2.2} /></button>
              <button data-tip="Mover" data-tip-pos="bottom" className={mode === 'mover' ? 'active' : ''} onClick={() => setMode('mover')}><Icon name="grid" size={13} /></button>
              <button data-tip="Comentar" data-tip-pos="bottom" className={mode === 'comentar' ? 'active' : ''} onClick={() => setMode('comentar')}><Icon name="chat" size={13} /></button>
            </div>
            <div className="canvas-pill" style={{ marginLeft: 'auto' }}>
              <button onClick={removeSelectedEdge} disabled={!selectedEdge} title="Eliminar enlace seleccionado">✕ enlace</button>
              <button onClick={() => setZoom(Math.max(50, zoom - 10))}>−</button>
              <button style={{ minWidth: 50 }}>{zoom}%</button>
              <button onClick={() => setZoom(Math.min(200, zoom + 10))}>+</button>
            </div>
            <div className="canvas-pill">
              <button data-tip="Mini-mapa" className={showMiniMap ? 'active' : ''} onClick={() => setShowMiniMap((s) => !s)}><Icon name="grid" size={13} /></button>
              <button data-tip="Variables" className={showVars ? 'active' : ''} onClick={() => setShowVars((s) => !s)}><Icon name="terminal" size={13} /></button>
            </div>
          </div>


          {tab !== 'diseño' && (
            <div style={{ position:'absolute', inset: 12, zIndex: 6, background:'rgba(5,8,18,0.88)', border:'1px solid var(--line)', borderRadius: 10, padding: 16, overflow: 'auto' }}>
              {tab === 'prueba' && (
                <div>
                  <h3 style={{ margin: 0, marginBottom: 10 }}>Prueba del flujo</h3>
                  <p style={{ color:'var(--ink-3)' }}>Ejecuta validaciones con el estado actual del diagrama.</p>
                  <button className="btn btn-primary" onClick={runTest}><Icon name="play" size={12} /> Ejecutar suite de prueba</button>
                </div>
              )}
              {tab === 'versiones' && (
                <div>
                  <h3 style={{ margin: 0, marginBottom: 10 }}>Versiones</h3>
                  <textarea className="textarea" rows={6} value={versionNotes} onChange={(e)=>setVersionNotes(e.target.value)} />
                  <div style={{ marginTop: 10 }}><button className="btn" onClick={saveNow}><Icon name="check" size={12} /> Guardar versión</button></div>
                </div>
              )}
              {tab === 'desplegar' && (
                <div>
                  <h3 style={{ margin: 0, marginBottom: 10 }}>Despliegue</h3>
                  <p style={{ color:'var(--ink-3)' }}>Publica la configuración y registra snapshot del flujo.</p>
                  <button className="btn btn-primary" onClick={deployFlow}><Icon name="zap" size={12} /> Desplegar ahora</button>
                </div>
              )}
            </div>
          )}

          <div style={{ position: 'absolute', inset: 0, transform: `scale(${zoom / 100})`, transformOrigin: '0 0' }}>
            <svg className="flow-svg" style={{ width: 1500, height: 500, pointerEvents: 'auto' }}>
              {edges.map((e, i) => {
                const fromNode = nodes.find((n) => n.id === e.from);
                const toNode = nodes.find((n) => n.id === e.to);
                if (!fromNode || !toNode) return null;
                const a = portPos(fromNode, 'out');
                const b = portPos(toNode, 'in');
                const mx = (a.x + b.x) / 2;
                const d = `M ${a.x} ${a.y} C ${mx} ${a.y}, ${mx} ${b.y}, ${b.x} ${b.y}`;
                const liveSeq = ['n1', 'n2', 'n3', 'n4', 'n6'];
                const isLive = running && liveSeq.includes(e.from) && liveSeq.includes(e.to);
                const edgeId = `${e.from}->${e.to}`; return <path key={i} d={d} className={(isLive ? 'live ' : '') + (selectedEdge === edgeId ? 'selected' : '')} onClick={(ev) => { ev.stopPropagation(); setSelectedEdge(edgeId); }} style={{ cursor: 'pointer', strokeWidth: selectedEdge === edgeId ? 3.2 : undefined, stroke: selectedEdge === edgeId ? 'var(--brand)' : undefined }} />;
              })}
            </svg>

            {nodes.map((n) => (
              <div
                key={n.id}
                className={'flow-node' + (selected === n.id ? ' selected' : '') + (running ? ' active-run' : '')}
                style={{ left: n.x, top: n.y }}
                onClick={(e) => { if (linkFrom && linkFrom !== n.id) { finishLink(e, n.id); } setSelected(n.id); }}
                onMouseDown={(e) => beginDrag(e, n.id)}
              >
                <div className="flow-port in" onClick={(e) => finishLink(e, n.id)} />
                <div className="flow-port out" onClick={(e) => startLink(e, n.id)} />
                <div className="flow-node-head">
                  {linkFrom === n.id && <span className="tag warn" style={{ marginRight: 6 }}>enlazando</span>}
                  <div className="ico" style={{ color: n.color, background: `${n.color}1a` }}><Icon name={n.icon} size={12} /></div>
                  <div className="name">{n.name}</div>
                  <div className="kind">{n.kind}</div>
                </div>
                <div className="flow-node-body">
                  {n.kv.map(([k, v], i) => (
                    <div key={i} className="kv"><span>{k}</span><span>{v}</span></div>
                  ))}
                </div>
                <div className="flow-node-tags">
                  {n.tags.map((t) => <span key={t} className="tag">{t}</span>)}
                </div>
              </div>
            ))}

            {nodes.length === 0 && (
              <div style={{ position: 'absolute', inset: 0, display: 'grid', placeItems: 'center', pointerEvents: 'none' }}>
                <div style={{ pointerEvents: 'auto', textAlign: 'center', background: 'var(--bg-1)', border: '1px solid var(--line)', borderRadius: 12, padding: 18, maxWidth: 420 }}>
                  <div style={{ fontWeight: 600, marginBottom: 6 }}>Lienzo limpio</div>
                  <div style={{ color: 'var(--ink-3)', fontSize: 12.5, marginBottom: 10 }}>Añade bloques desde la paleta izquierda para construir tu flujo desde cero.</div>
                  <button className="btn btn-primary btn-sm" onClick={() => addNodeFromPalette('Webhook', 'webhook', 'HTTP', 'disparador', '#60a5fa')}><Icon name="plus" size={11} /> Añadir primer bloque</button>
                </div>
              </div>
            )}

            {showMiniMap && (
              <div style={{ position: 'absolute', right: 18, bottom: 22, width: 220, background: 'var(--bg-1)', border: '1px solid var(--line)', borderRadius: 10, padding: 8 }}>
                <div className="mono" style={{ fontSize: 10.5, color: 'var(--ink-3)', marginBottom: 6 }}>mini-mapa · nodos</div>
                {nodes.map((n) => (
                  <div key={`mm-${n.id}`} style={{ display: 'flex', justifyContent: 'space-between', fontSize: 11.5, color: n.id === selected ? 'var(--ink)' : 'var(--ink-3)' }}>
                    <span>{n.id}</span><span>{n.name.slice(0, 20)}</span>
                  </div>
                ))}
              </div>
            )}
          </div>
        </div>

        <div style={{ borderTop: '1px solid var(--line)', background: 'var(--bg-1)', height: consoleHeight, overflowY: 'auto', position: 'relative' }}>
          <div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 10, cursor: 'row-resize', zIndex: 4, background: 'linear-gradient(90deg, transparent, var(--brand), transparent)' }} onMouseDown={(e) => startResize('bottom', e)} />
          <div style={{ padding: '8px 14px', display: 'flex', alignItems: 'center', gap: 10, borderBottom: '1px solid var(--line)' }}>
            <span className="mono" style={{ fontSize: 11, color: 'var(--ink-3)', textTransform: 'uppercase', letterSpacing: '0.08em' }}>Consola de ejecución</span>
            <span className={'tag ' + (running ? 'warn' : 'ok')}>{running ? 'streaming' : 'inactiva'}</span>
            <span className="mono" style={{ fontSize: 11, color: 'var(--ink-4)', marginLeft: 'auto' }}>modo {mode} · zoom {zoom}% · {nodes.length} nodos</span>
          </div>
          <pre className="mono" style={{ margin: 0, padding: '10px 14px', fontSize: 11.5, color: 'var(--ink-2)', lineHeight: 1.7 }}>
{`${consoleLines.join('\n')}
${running ? '[runtime]      ejecutando pasos del flujo...' : ''}`}
          </pre>
        </div>
      </div>
      <div style={{ cursor: 'col-resize', background: 'linear-gradient(180deg, transparent, var(--brand), transparent)', opacity: 0.95, width: 8 }} onMouseDown={(e) => startResize('right', e)} />

      <div className="bld-inspector">
        <div className="insp-head">
          <div className="ico" style={{ color: sel ? sel.color : "#7a7f8d", background: sel ? `${sel.color}1a` : "#7a7f8d1a" }}><Icon name={sel ? sel.icon : "grid"} size={14} /></div>
          <div>
            <div style={{ fontWeight: 600, fontSize: 13.5 }}>{sel ? sel.name : "Sin nodo seleccionado"}</div>
            <div className="mono" style={{ fontSize: 10.5, color: 'var(--ink-3)' }}>{sel ? `${sel.id} · ${sel.kind}` : "Añade un bloque o haz clic en uno"}</div>
          </div>
          <button className="btn btn-ghost btn-icon btn-sm" style={{ marginLeft: 'auto' }} onClick={() => setShowNodeMenu((v) => !v)} disabled={!sel}><Icon name="dot-menu" size={14} /></button>
        </div>

        {showNodeMenu && sel && (
          <div className="insp-section" style={{ paddingTop: 10 }}>
            <button className="btn btn-sm" style={{ width: '100%', justifyContent: 'center', marginBottom: 8 }} onClick={duplicateSelectedNode}><Icon name="fork" size={11} /> Duplicar nodo</button>
            <button className="btn btn-sm" style={{ width: '100%', justifyContent: 'center' }} onClick={removeSelectedNode}><Icon name="x" size={11} /> Eliminar nodo</button>
          </div>
        )}

        <div className="insp-section">
          <h5>Configuración</h5>
          {showGuide && <div style={{ fontSize: 11.2, color: 'var(--ink-3)', marginBottom: 8 }}>Aquí ajustas el bloque seleccionado. Si no sabes qué tocar, deja valores por defecto.</div>}
          <div className="insp-row"><label>Modelo</label>
            <select id="builder-model" className="select" defaultValue="claude-3.5-sonnet">
              <option>claude-3.5-sonnet</option><option>claude-3-opus</option><option>claude-haiku-4.5</option>
            </select>
          </div>
          <div className="insp-row"><label>Temperatura <span className="mono" style={{ color: 'var(--ink-3)', float: 'right' }}>{temperature.toFixed(2).replace('.', ',')}</span></label>
            <input type="range" min="0" max="1" step="0.01" value={temperature} onChange={(e) => setTemperature(parseFloat(e.target.value))} style={{ width: '100%' }} />
          </div>
          <div className="insp-row"><label>Máx. tokens</label><input className="input" value={maxTokens} onChange={(e) => setMaxTokens(Number(e.target.value || 0))} /></div>
          <button className="btn btn-sm" style={{ marginTop: 8, width: '100%', justifyContent: 'center' }} onClick={saveInspector}><Icon name="check" size={11} /> Guardar configuración</button>
        </div>

        <div className="insp-section">
          <h5>Prompt de sistema</h5>
          {showGuide && <div style={{ fontSize: 11.2, color: 'var(--ink-3)', marginBottom: 8 }}>Describe con palabras simples qué debe hacer el agente y qué datos no debe inventar.</div>}
          <textarea className="textarea mono" rows={5} style={{ fontSize: 11.5 }} value={systemPrompt} onChange={(e) => setSystemPrompt(e.target.value)} />
        </div>

        <div className="insp-section">
          <h5>Herramientas disponibles <span className="mono" style={{ color: 'var(--ink-4)' }}>{tools.length}</span></h5>
          {showGuide && <div style={{ fontSize: 11.2, color: 'var(--ink-3)', marginBottom: 8 }}>Activa solo las herramientas que de verdad necesita tu agente para evitar errores.</div>}
          {tools.map((t, i) => (
            <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '8px 0', borderBottom: i < tools.length - 1 ? '1px dashed var(--line)' : 'none' }}>
              <span className="status-dot" style={{ background: t.enabled ? t.c : 'var(--ink-4)', boxShadow: `0 0 0 3px ${t.enabled ? t.c : '#000000'}33` }} />
              <div style={{ flex: 1 }}>
                <div className="mono" style={{ fontSize: 11.5 }}>{t.n}</div>
                <div style={{ fontSize: 10.5, color: 'var(--ink-3)' }}>{t.d}</div>
              </div>
              <button className="btn btn-ghost btn-icon btn-sm" onClick={() => setToolConfigIdx(i)}><Icon name="cog" size={12} /></button>
            </div>
          ))}
          <button className="btn btn-sm" style={{ marginTop: 8, width: '100%', justifyContent: 'center' }} onClick={() => setShowToolPicker(true)}><Icon name="plus" size={11} /> Adjuntar herramienta</button>
        </div>


        <div className="insp-section">
          <h5>Base de conocimiento</h5>
          {showGuide && <div style={{ fontSize: 11.2, color: 'var(--ink-3)', marginBottom: 8 }}>Añade URLs y documentos reales (PDF, Word, Excel) para que responda con información de tu negocio.</div>}
          <div style={{ display: 'flex', gap: 6, marginBottom: 8 }}>
            <input
              className="input"
              style={{ flex: 1, fontSize: 11.5 }}
              placeholder="https://..."
              value={kbUrlInput}
              onChange={(e) => setKbUrlInput(e.target.value)}
              onKeyDown={(e) => { if (e.key === 'Enter' && kbUrlInput.trim()) { setKnowledgeUrls((prev) => [...prev, kbUrlInput.trim()]); setKbUrlInput(''); } }}
            />
            <button className="btn btn-sm" onClick={() => { if (!kbUrlInput.trim()) return; setKnowledgeUrls((prev) => [...prev, kbUrlInput.trim()]); setKbUrlInput(''); }}>
              <Icon name="plus" size={11} />
            </button>
          </div>
          {knowledgeUrls.map((url, i) => (
            <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
              <Icon name="globe" size={11} style={{ color: 'var(--ink-3)', flexShrink: 0 }} />
              <span className="mono" style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontSize: 11, color: 'var(--ink-2)' }}>{url}</span>
              <button className="btn btn-ghost btn-icon btn-sm" onClick={() => setKnowledgeUrls((prev) => prev.filter((_, idx) => idx !== i))}><Icon name="x" size={10} /></button>
            </div>
          ))}
          <label className="btn btn-sm" style={{ marginTop: 6, width: '100%', justifyContent: 'center', cursor: 'pointer' }}>
            <Icon name="upload" size={11} /> Adjuntar documento (PDF, Word, Excel)
            <input type="file" style={{ display: 'none' }} accept=".pdf,.doc,.docx,.xls,.xlsx,.txt,.csv" multiple onChange={(e) => { const files = Array.from(e.target.files || []); setKnowledgeDocs((prev) => [...prev, ...files.map((f) => ({ name: f.name, size: f.size }))]); e.target.value = ''; }} />
          </label>
          {knowledgeDocs.map((doc, i) => (
            <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 6, marginTop: 4 }}>
              <Icon name="database" size={11} style={{ color: 'var(--ink-3)', flexShrink: 0 }} />
              <span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontSize: 11, color: 'var(--ink-2)' }}>{doc.name}</span>
              <button className="btn btn-ghost btn-icon btn-sm" onClick={() => setKnowledgeDocs((prev) => prev.filter((_, idx) => idx !== i))}><Icon name="x" size={10} /></button>
            </div>
          ))}
        </div>

        <div className="insp-section">
          <h5>Webhook salida</h5>
          <div className="insp-row"><label>URL</label><input className="input" value={webhookOutUrl} onChange={(e) => setWebhookOutUrl(e.target.value)} /></div>
          <div className="insp-row"><label>Método</label>
            <select className="select" value={webhookOutMethod} onChange={(e) => setWebhookOutMethod(e.target.value)}>
              <option>POST</option><option>GET</option><option>PUT</option><option>PATCH</option><option>DELETE</option>
            </select>
          </div>
          <div className="insp-row"><label>Bearer secret (opcional)</label><input className="input" type="password" value={webhookOutSecret} onChange={(e) => setWebhookOutSecret(e.target.value)} /></div>
          <div className="insp-row"><label>Body JSON</label><textarea className="textarea mono" rows={4} value={webhookOutBody} onChange={(e) => setWebhookOutBody(e.target.value)} /></div>
          <button className="btn btn-sm" style={{ marginTop: 8, width: '100%', justifyContent: 'center' }} onClick={runWebhookOut}><Icon name="webhook" size={11} /> Ejecutar webhook salida</button>
        </div>

        <div className="insp-section">
          <h5>Salvaguardas</h5>
          <div className="settings-row" style={{ borderColor: 'var(--line)' }} onClick={() => { setSafeguards((s) => ({ ...s, pii: !s.pii })); pushLog('[policy]       Anonimizar PII actualizado'); }}>
            <div className="lbl"><b>Anonimizar PII</b><p>elimina emails, nombres, tarjetas</p></div>
            <div className={'toggle' + (safeguards.pii ? ' on' : '')} />
          </div>
          <div className="settings-row" style={{ borderColor: 'var(--line)' }} onClick={() => { setSafeguards((s) => ({ ...s, humanApproval: !s.humanApproval })); pushLog('[policy]       Aprobación humana actualizada'); }}>
            <div className="lbl"><b>Aprobación humana</b><p>obligatoria para reembolsos &gt; 200 €</p></div>
            <div className={'toggle' + (safeguards.humanApproval ? ' on' : '')} />
          </div>
          <div className="settings-row" style={{ border: 0 }} onClick={() => { setSafeguards((s) => ({ ...s, strictJson: !s.strictJson })); pushLog('[policy]       Esquema de salida actualizado'); }}>
            <div className="lbl"><b>Esquema de salida</b><p>validación JSON estricta</p></div>
            <div className={'toggle' + (safeguards.strictJson ? ' on' : '')} />
          </div>
        </div>
      </div>

      {showVars && (
        <div className="crm-detail-back" onClick={(e) => { if (e.target === e.currentTarget) setShowVars(false); }}>
          <div className="crm-detail" style={{ width: 520, maxWidth: '92vw' }}>
            <div className="d-head"><h3>Variables del flujo</h3><button className="btn btn-ghost" onClick={() => setShowVars(false)}><Icon name="x" size={14} /></button></div>
            <div className="d-body" style={{ display: 'grid', gap: 10 }}>
              {vars.map((v, i) => (
                <div key={i} style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
                  <input className="input" value={v.k} onChange={(e) => setVars((prev) => prev.map((it, idx) => idx === i ? { ...it, k: e.target.value } : it))} />
                  <input className="input" value={v.v} onChange={(e) => setVars((prev) => prev.map((it, idx) => idx === i ? { ...it, v: e.target.value } : it))} />
                </div>
              ))}
              <button className="btn btn-sm" onClick={() => setVars((prev) => [...prev, { k: 'new_key', v: 'value' }])}><Icon name="plus" size={11} /> Añadir variable</button>
            </div>
          </div>
        </div>
      )}

      {showToolPicker && (
        <div className="crm-detail-back" onClick={(e) => { if (e.target === e.currentTarget) setShowToolPicker(false); }}>
          <div className="crm-detail" style={{ width: 520, maxWidth: '92vw' }}>
            <div className="d-head"><h3>Adjuntar herramienta</h3><button className="btn btn-ghost" onClick={() => setShowToolPicker(false)}><Icon name="x" size={14} /></button></div>
            <div className="d-body" style={{ display: 'grid', gap: 8 }}>
              {[
                { n: 'hubspot.contact', d: 'alta de contacto y enriquecimiento CRM', c: '#ff7a59' },
                { n: 'linear.create_issue', d: 'crear ticket técnico desde la incidencia', c: '#a78bfa' },
                { n: 'http.web_search', d: 'consulta externa con validación de fuentes', c: '#60a5fa' },
              ].map((t) => (
                <button key={t.n} className="btn" style={{ justifyContent: 'space-between' }} onClick={() => addTool({ ...t, enabled: true })}>
                  <span>{t.n}</span><span className="mono" style={{ color: 'var(--ink-3)', fontSize: 11 }}>{t.d}</span>
                </button>
              ))}
            </div>
          </div>
        </div>
      )}

      {selectedTool && (
        <div className="crm-detail-back" onClick={(e) => { if (e.target === e.currentTarget) setToolConfigIdx(null); }}>
          <div className="crm-detail" style={{ width: 520, maxWidth: '92vw' }}>
            <div className="d-head"><h3>Configurar herramienta</h3><button className="btn btn-ghost" onClick={() => setToolConfigIdx(null)}><Icon name="x" size={14} /></button></div>
            <div className="d-body" style={{ display: 'grid', gap: 10 }}>
              <div className="mono" style={{ fontSize: 12 }}>{selectedTool.n}</div>
              <div style={{ color: 'var(--ink-3)', fontSize: 12 }}>{selectedTool.d}</div>
              <button className="btn" onClick={() => toggleTool(toolConfigIdx)}>{selectedTool.enabled ? 'Desactivar' : 'Activar'} herramienta</button>
              <button className="btn btn-primary" onClick={() => { pushLog(`[tools]        configuración aplicada: ${selectedTool.n}`); notify('Configuración aplicada'); setToolConfigIdx(null); }}>Guardar cambios</button>
            </div>
          </div>
        </div>
      )}

      {showExampleFlow && (
        <div className="crm-detail-back" onClick={(e) => { if (e.target === e.currentTarget) setShowExampleFlow(false); }}>
          <div className="crm-detail" style={{ width: 980, maxWidth: '94vw', maxHeight: '90vh' }}>
            <div className="d-head">
              <h3>Ejemplo por defecto del Editor</h3>
              <button className="btn btn-ghost" onClick={() => setShowExampleFlow(false)}><Icon name="x" size={14} /></button>
            </div>
            <div className="d-body" style={{ display: 'grid', gap: 10 }}>
              <img
                src="/editor-example-default.svg"
                alt="Ejemplo por defecto del flujo en el editor"
                style={{ width: '100%', borderRadius: 10, border: '1px solid var(--line)', background: '#050812' }}
              />
              <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
                <button className="btn" onClick={() => setShowExampleFlow(false)}>Cerrar</button>
                <button className="btn btn-primary" onClick={loadExampleFlow}><Icon name="check" size={12} /> Cargar en el editor</button>
              </div>
            </div>
          </div>
        </div>
      )}
    </div>
  );
};
window.Builder = Builder;
