import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
  applyEdgeChanges,
  applyNodeChanges,
  Background,
  Controls,
  MiniMap,
  ReactFlow,
  ReactFlowProvider,
  useReactFlow,
} from '@xyflow/react';
import { WorkflowProvider } from './context/WorkflowContext.jsx';
import Toolbar from './components/Toolbar.jsx';
import NodePalette from './components/NodePalette.jsx';
import SettingsDialog from './components/SettingsDialog.jsx';
import InteractivePanel from './components/InteractivePanel.jsx';
import {
  appendTranscriptLog,
  getLlmSessionLog,
  getDeepinfraKeyStatus,
  loadConfig,
  loadDefaultWorkflow,
  loadPresets,
  evaluateAssistantAnswer,
  saveCustomPreset,
  saveDefaultWorkflow,
  saveDeepinfraKey,
  simulateUserAnswer,
  startTestRun,
  startTranscriptLog,
  DEFAULT_BACKEND_URL,
  DEFAULT_LIVE_DEBUG_URL,
} from './lib/api.js';
import {
  buildComponentNodeDataFromTemplate,
  loadComponentLibrary,
  mergeComponentTemplates,
  persistComponentLibrary,
  upsertComponentTemplateFromNode,
} from './lib/componentLibrary.js';
import {
  loadAppSettings,
  normalizeBackendUrl,
  persistAppSettings,
  resolveBackendUrl,
  resolveLiveDebugUrl,
} from './lib/settings.js';
import {
  COMPONENT_NODE_TYPE,
  createComponentSubgraph,
  createNodeInstance,
  getNodeHandles,
  hydrateNode,
  isComponentNodeType,
  listNodeDefinitions,
  nodeTypes,
} from './lib/nodeRegistry.js';
import {
  AUTOSAVE_KEY,
  LEGACY_AUTOSAVE_KEY,
  connectEdge,
  createComponentFromSelection,
  createBlankWorkflow,
  createDefaultWorkflow,
  continueInteractiveWorkflowRun,
  createInteractiveWorkflowRun,
  DEFAULT_VIEWPORT,
  getGraphAtPath,
  hydrateGraph,
  isValidConnection,
  parseWorkflowData,
  serializeWorkflow,
  updateGraphAtPath,
} from './lib/workflow.js';

const THEME_KEY = 'nodes_ui_flow_theme';
const LLM_PROVIDERS = new Set(['ollama', 'deepinfra']);
const FALLBACK_LLM_PROVIDER = 'ollama';
const DEFAULT_LLM_ROLE_PROMPT =
  'Ты посетитель выставки RosUpack. Отвечай коротко, естественно и по делу. Ты представляешь средний или крупный бизнес.';

function detectInitialTheme() {
  const savedTheme = window.localStorage.getItem(THEME_KEY);
  if (savedTheme === 'dark' || savedTheme === 'light') {
    return savedTheme;
  }

  return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
}

function isEditableTarget(target) {
  if (!(target instanceof HTMLElement)) {
    return false;
  }

  return Boolean(target.closest('input, textarea, select, [contenteditable="true"], [contenteditable=""]'));
}

function mergeRuntimeIntoEditorGraph(editorGraph, runtimeGraph) {
  const runtimeNodesById = new Map((runtimeGraph?.nodes || []).map((node) => [node.id, node]));

  return hydrateGraph({
    ...editorGraph,
    nodes: (editorGraph?.nodes || []).map((node) => {
      const runtimeNode = runtimeNodesById.get(node.id);
      if (!runtimeNode) {
        return node;
      }

      const nextData = {
        ...node.data,
        runtime: runtimeNode.data?.runtime || node.data?.runtime,
      };

      if (isComponentNodeType(node.type) && node.data?.subgraph && runtimeNode.data?.subgraph) {
        nextData.subgraph = mergeRuntimeIntoEditorGraph(node.data.subgraph, runtimeNode.data.subgraph);
      }

      return hydrateNode({
        ...node,
        data: nextData,
      });
    }),
    edges: editorGraph?.edges || [],
    viewport: editorGraph?.viewport || null,
  });
}

function createLlmSessionId() {
  return `llm_session_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
}

function safeDownloadName(value) {
  return String(value || 'workflow')
    .replace(/\.json$/i, '')
    .replace(/[^a-zA-Z0-9а-яА-ЯёЁ._-]+/g, '_')
    .replace(/^_+|_+$/g, '') || 'workflow';
}

function normalizeLlmProvider(value) {
  return LLM_PROVIDERS.has(value) ? value : FALLBACK_LLM_PROVIDER;
}

function formatMessageRole(role) {
  return role === 'assistant' ? 'АССИСТЕНТ' : 'ПОЛЬЗОВАТЕЛЬ';
}

function formatSessionLog({ workflowName, sessionId, provider, transcript, runtimeLogs, llmCalls }) {
  const lines = [
    `Workflow: ${workflowName}`,
    `Сессия: ${sessionId || 'не запущена'}`,
    `Provider: ${provider}`,
    `Сохранено: ${new Date().toLocaleString()}`,
    '',
    '=== Диалог ===',
    '',
  ];

  if (transcript.length === 0) {
    lines.push('В текущей сессии нет сообщений диалога.', '');
  } else {
    transcript.forEach((message) => {
      lines.push(`${formatMessageRole(message.role)}: ${message.text}`, '');
    });
  }

  lines.push('=== Runtime события ===', '');

  if (!runtimeLogs?.length) {
    lines.push('В текущей сессии нет runtime-событий.', '');
  } else {
    runtimeLogs.forEach((entry) => {
      lines.push(`[${entry.level || 'info'}] ${entry.message || ''}`);
    });
    lines.push('');
  }

  lines.push('=== LLM-вызовы ===', '');

  if (llmCalls.length === 0) {
    lines.push('В текущей сессии не было LLM-вызовов.', '');
  } else {
    llmCalls.forEach((call, index) => {
      lines.push(
        `#${index + 1} ${call.timestamp || ''}`,
        `Endpoint: ${call.endpoint || ''}`,
        `Node: ${call.node_id || ''}`,
        `Provider: ${call.provider || ''}`,
        `Model: ${call.model || ''}`,
        '',
        'Сообщения:',
      );

      (call.messages || []).forEach((message) => {
        lines.push(`${String(message.role || 'unknown').toUpperCase()}:`);
        lines.push(message.content || '');
        lines.push('');
      });

      lines.push('Сырой ответ:');
      lines.push(JSON.stringify(call.response || {}, null, 2));
      lines.push('');
    });
  }

  return lines.join('\n');
}

function resolveLiveDebugWebSocketUrl(liveDebugUrl) {
  const normalized = normalizeBackendUrl(liveDebugUrl) || DEFAULT_LIVE_DEBUG_URL;
  return `${normalized.replace(/^http:/i, 'ws:').replace(/^https:/i, 'wss:')}/workflow-runtime/active/live`;
}

function getLiveDebugEventKey(event) {
  return [
    event.timestamp || '',
    event.type || '',
    event.nodeId || event.node_id || '',
    event.title || '',
    event.question || event.answer || event.message || event.error || JSON.stringify(event.memoryPatch || {}),
  ].join('|');
}

function WorkflowEditor() {
  const reactFlow = useReactFlow();
  const fileInputRef = useRef(null);
  const fileHandleRef = useRef(null);
  const spawnCounterRef = useRef(0);
  const interactiveRunRef = useRef(null);
  const interactiveRunPathRef = useRef([]);
  const logCounterRef = useRef(0);
  const transcriptCounterRef = useRef(0);
  const autoAnsweredQuestionRef = useRef(null);
  const interactiveTranscriptRef = useRef([]);
  const lastUserAnswerRef = useRef('');
  const llmTestModeRef = useRef(false);
  const llmTestStopRequestedRef = useRef(false);
  const testRunRef = useRef(null);
  const testAutoAnswerCountRef = useRef(0);
  const transcriptLogRef = useRef(null);
  const writeTranscriptLogsRef = useRef(false);
  const llmSessionIdRef = useRef('');
  const suppressNextUserAnswerLogRef = useRef(false);
  const liveDebugSocketRef = useRef(null);
  const liveDebugEventKeysRef = useRef(new Set());

  const [workflowDocument, setWorkflowDocument] = useState(null);
  const [graphPath, setGraphPath] = useState([]);
  const [backendUrl, setBackendUrl] = useState(DEFAULT_BACKEND_URL);
  const [liveDebugUrl, setLiveDebugUrl] = useState(DEFAULT_LIVE_DEBUG_URL);
  const [defaultBackendUrl, setDefaultBackendUrl] = useState(DEFAULT_BACKEND_URL);
  const [defaultLlmProvider, setDefaultLlmProvider] = useState(FALLBACK_LLM_PROVIDER);
  const [defaultDeepinfraApiKey, setDefaultDeepinfraApiKey] = useState('');
  const [presets, setPresets] = useState({ default: [], custom: [] });
  const [componentTemplates, setComponentTemplates] = useState(() => loadComponentLibrary());
  const [isRunning, setIsRunning] = useState(false);
  const [notice, setNotice] = useState('Редактор workflow готов');
  const [isReady, setIsReady] = useState(false);
  const [isSettingsOpen, setIsSettingsOpen] = useState(false);
  const [settingsBackendUrl, setSettingsBackendUrl] = useState(DEFAULT_BACKEND_URL);
  const [settingsLiveDebugUrl, setSettingsLiveDebugUrl] = useState('');
  const [settingsLlmProvider, setSettingsLlmProvider] = useState(FALLBACK_LLM_PROVIDER);
  const [settingsDeepinfraApiKey, setSettingsDeepinfraApiKey] = useState('');
  const [settingsDeepinfraHasKey, setSettingsDeepinfraHasKey] = useState(false);
  const [settingsDeepinfraKeyStatus, setSettingsDeepinfraKeyStatus] = useState('');
  const [isDemoMode, setIsDemoMode] = useState(false);
  const [theme, setTheme] = useState(detectInitialTheme);
  const [isPaletteVisible, setIsPaletteVisible] = useState(true);
  const [pendingQuestion, setPendingQuestion] = useState(null);
  const [interactiveTranscript, setInteractiveTranscript] = useState([]);
  const [interactiveLogs, setInteractiveLogs] = useState([]);
  const [answerDraft, setAnswerDraft] = useState('');
  const [llmProvider, setLlmProvider] = useState(FALLBACK_LLM_PROVIDER);
  const [llmAutoAnswer, setLlmAutoAnswer] = useState(false);
  const [llmTestMode, setLlmTestMode] = useState(false);
  const [writeTranscriptLogs, setWriteTranscriptLogs] = useState(false);
  const [llmRolePrompt, setLlmRolePrompt] = useState(DEFAULT_LLM_ROLE_PROMPT);
  const [liveDebugStatus, setLiveDebugStatus] = useState('off');
  const currentGraph = workflowDocument ? getGraphAtPath(workflowDocument, graphPath) : { nodes: [], edges: [], viewport: null };
  const nodes = currentGraph.nodes || [];
  const edges = currentGraph.edges || [];
  const selectedNodeCount = nodes.filter((node) => node.selected).length;

  useEffect(() => {
    document.documentElement.dataset.theme = theme;
    document.documentElement.style.colorScheme = theme;
    window.localStorage.setItem(THEME_KEY, theme);
  }, [theme]);

  const applyComponentTemplates = useCallback((templates, options = {}) => {
    const { persist = true } = options;

    setComponentTemplates(templates);
    setWorkflowDocument((previousDocument) =>
      previousDocument
        ? {
            ...previousDocument,
            componentTemplates: templates,
          }
        : previousDocument,
    );

    if (persist) {
      persistComponentLibrary(templates);
    }
  }, []);

  const syncViewportForGraph = useCallback(
    (graph, options = {}) => {
      requestAnimationFrame(() => {
        if (graph?.viewport && !options.fitView) {
          reactFlow.setViewport(graph.viewport, { duration: 0 });
        } else {
          reactFlow.fitView({ padding: 0.2, duration: 240 });
        }
      });
    },
    [reactFlow],
  );

  useEffect(() => {
    let isActive = true;

    const initialize = async () => {
      const storedComponentTemplates = loadComponentLibrary();
      const storedSettings = loadAppSettings();
      const [config, loadedPresets, serverDefaultWorkflow] = await Promise.all([
        loadConfig(),
        loadPresets(),
        loadDefaultWorkflow().catch((error) => {
          console.error('Failed to load server default workflow:', error);
          return null;
        }),
      ]);
      if (!isActive) {
        return;
      }

      const resolvedDefaultBackendUrl = normalizeBackendUrl(config.backendUrl) || DEFAULT_BACKEND_URL;
      const resolvedBackendUrl = resolveBackendUrl(resolvedDefaultBackendUrl, storedSettings);
      const resolvedDefaultLiveDebugUrl = normalizeBackendUrl(config.liveDebugUrl) || DEFAULT_LIVE_DEBUG_URL;
      const resolvedLiveDebugUrl = resolveLiveDebugUrl(resolvedDefaultLiveDebugUrl, storedSettings);
      const resolvedDefaultLlmProvider = normalizeLlmProvider(config.llmProvider);
      const resolvedDefaultDeepinfraApiKey = String(config.deepinfraApiKey || '').trim();

      setDefaultBackendUrl(resolvedDefaultBackendUrl);
      setDefaultLlmProvider(resolvedDefaultLlmProvider);
      setDefaultDeepinfraApiKey(resolvedDefaultDeepinfraApiKey);
      setIsDemoMode(Boolean(config.demo));
      setBackendUrl(resolvedBackendUrl);
      setLiveDebugUrl(resolvedLiveDebugUrl);
      setSettingsBackendUrl(resolvedBackendUrl);
      setSettingsLiveDebugUrl(storedSettings.liveDebugUrlOverride || '');
      setPresets(loadedPresets);

      let documentToLoad = null;

      const autosave = localStorage.getItem(AUTOSAVE_KEY);
      if (autosave) {
        try {
          documentToLoad = parseWorkflowData(JSON.parse(autosave));
          setNotice('Восстановлен autosave');
        } catch (error) {
          console.error('Failed to restore React Flow autosave:', error);
        }
      }

      if (!documentToLoad) {
        const legacyAutosave = localStorage.getItem(LEGACY_AUTOSAVE_KEY);
        if (legacyAutosave) {
          try {
            documentToLoad = parseWorkflowData(JSON.parse(legacyAutosave));
            setNotice('Импортирован старый autosave LiteGraph');
          } catch (error) {
            console.error('Failed to import LiteGraph autosave:', error);
          }
        }
      }

      if (!documentToLoad && serverDefaultWorkflow) {
        try {
          documentToLoad = parseWorkflowData(serverDefaultWorkflow);
          setNotice('Загружен workflow с сервера');
        } catch (error) {
          console.error('Failed to parse server default workflow:', error);
        }
      }

      if (!documentToLoad) {
        documentToLoad = createDefaultWorkflow();
      }

      const mergedTemplates = mergeComponentTemplates(documentToLoad.componentTemplates || [], storedComponentTemplates);
      const initialLlmProvider = normalizeLlmProvider(config.llmProvider || documentToLoad.settings?.llmProvider);
      setLlmRolePrompt(documentToLoad.settings?.llmRolePrompt || DEFAULT_LLM_ROLE_PROMPT);
      setLlmProvider(initialLlmProvider);
      setWorkflowDocument({
        ...documentToLoad,
        componentTemplates: mergedTemplates,
        settings: {
          ...(documentToLoad.settings || {}),
          llmRolePrompt: documentToLoad.settings?.llmRolePrompt || DEFAULT_LLM_ROLE_PROMPT,
          llmProvider: initialLlmProvider,
        },
      });
      setComponentTemplates(mergedTemplates);
      persistComponentLibrary(mergedTemplates);
      if (initialLlmProvider === 'deepinfra' && resolvedDefaultDeepinfraApiKey) {
        saveDeepinfraKey(resolvedBackendUrl, resolvedDefaultDeepinfraApiKey).catch((error) => {
          console.error('Failed to apply DeepInfra key from config:', error);
        });
      }
      setGraphPath([]);
      syncViewportForGraph(documentToLoad.rootGraph, {
        fitView: !documentToLoad.rootGraph.viewport,
      });
      setIsReady(true);
    };

    initialize();

    return () => {
      isActive = false;
    };
  }, [syncViewportForGraph]);

  useEffect(() => {
    if (!isReady || !workflowDocument) {
      return;
    }

    const timeoutId = window.setTimeout(() => {
      localStorage.setItem(AUTOSAVE_KEY, JSON.stringify(serializeWorkflow(workflowDocument)));
    }, 180);

    return () => {
      window.clearTimeout(timeoutId);
    };
  }, [isReady, workflowDocument]);

  const updateCurrentGraph = useCallback(
    (updater) => {
      setWorkflowDocument((previousDocument) => updateGraphAtPath(previousDocument, graphPath, updater));
    },
    [graphPath],
  );

  const updateWorkflowSettings = useCallback((patch) => {
    setWorkflowDocument((previousDocument) =>
      previousDocument
        ? {
            ...previousDocument,
            settings: {
              ...(previousDocument.settings || {}),
              ...patch,
            },
          }
        : previousDocument,
    );
  }, []);

  const handleLlmRolePromptChange = useCallback(
    (value) => {
      setLlmRolePrompt(value);
      updateWorkflowSettings({ llmRolePrompt: value });
    },
    [updateWorkflowSettings],
  );

  const refreshPresets = useCallback(async () => {
    const loadedPresets = await loadPresets();
    setPresets(loadedPresets);
    return loadedPresets;
  }, []);

  const patchNodeData = useCallback(
    (nodeId, patch) => {
      updateCurrentGraph((graph) => ({
        ...graph,
        nodes: graph.nodes.map((node) => {
          if (node.id !== nodeId) {
            return node;
          }

          const nextPatch = typeof patch === 'function' ? patch(node.data, node) : patch;

          return hydrateNode({
            ...node,
            data: {
              ...node.data,
              ...nextPatch,
            },
          });
        }),
      }));
    },
    [updateCurrentGraph],
  );

  const replaceNodeData = useCallback(
    (nodeId, updater) => {
      updateCurrentGraph((graph) => ({
        ...graph,
        nodes: graph.nodes.map((node) => {
          if (node.id !== nodeId) {
            return node;
          }

          const nextData = typeof updater === 'function' ? updater(node.data, node) : updater;

          return hydrateNode({
            ...node,
            data: nextData,
          });
        }),
      }));
    },
    [updateCurrentGraph],
  );

  const removeHandleConnections = useCallback(
    (nodeId, handleId, direction) => {
      updateCurrentGraph((graph) => ({
        ...graph,
        edges: graph.edges.filter((edge) => {
          if (direction === 'source') {
            return !(edge.source === nodeId && edge.sourceHandle === handleId);
          }

          return !(edge.target === nodeId && edge.targetHandle === handleId);
        }),
      }));
    },
    [updateCurrentGraph],
  );

  const savePreset = useCallback(
    async (name, text) => {
      try {
        const success = await saveCustomPreset(backendUrl, name, text);
        if (success) {
          await refreshPresets();
          setNotice(`Preset saved: ${name}`);
        }
        return success;
      } catch (error) {
        console.error('Failed to save preset:', error);
        setNotice('Не удалось сохранить пресет');
        return false;
      }
    },
    [backendUrl, refreshPresets],
  );

  const saveComponentNode = useCallback(
    (nodeId) => {
      const componentNode = nodes.find((node) => node.id === nodeId && isComponentNodeType(node.type));
      if (!componentNode) {
        setNotice('Компонентная нода не найдена');
        return false;
      }

      const { template, templates } = upsertComponentTemplateFromNode(componentTemplates, componentNode);
      applyComponentTemplates(templates);
      patchNodeData(nodeId, { libraryId: template.id });
      setNotice(`Saved component node: ${template.title}`);
      return true;
    },
    [applyComponentTemplates, componentTemplates, nodes, patchNodeData],
  );

  const addNode = useCallback(
    (type, overrides = {}) => {
      const offset = spawnCounterRef.current % 5;
      spawnCounterRef.current += 1;

      const position = reactFlow.screenToFlowPosition({
        x: Math.round(window.innerWidth * 0.62) + offset * 26,
        y: Math.round(window.innerHeight * 0.38) + offset * 26,
      });

      updateCurrentGraph((graph) => ({
        ...graph,
        nodes: [...graph.nodes, createNodeInstance(type, position, overrides)],
      }));
      setNotice(`Added ${type}`);
    },
    [reactFlow, updateCurrentGraph],
  );

  const addComponentNodeFromLibrary = useCallback(
    (templateId) => {
      const template = componentTemplates.find((candidate) => candidate.id === templateId);
      if (!template) {
        setNotice('Сохраненная компонентная нода не найдена');
        return;
      }

      addNode(COMPONENT_NODE_TYPE, {
        data: buildComponentNodeDataFromTemplate(template),
      });
      setNotice(`Added component node: ${template.title}`);
    },
    [addNode, componentTemplates],
  );

  const navigateToPath = useCallback(
    (nextPath, options = {}) => {
      if (!workflowDocument) {
        return;
      }

      const nextGraph = getGraphAtPath(workflowDocument, nextPath);
      setGraphPath(nextPath);
      syncViewportForGraph(nextGraph, options);
    },
    [syncViewportForGraph, workflowDocument],
  );

  const openComponent = useCallback(
    (nodeId) => {
      const componentNode = nodes.find((node) => node.id === nodeId && isComponentNodeType(node.type));
      if (!componentNode) {
        return;
      }

      const nextPath = [...graphPath, nodeId];
      navigateToPath(nextPath, {
        fitView: !componentNode.data.subgraph?.viewport,
      });
      setNotice(`Opened ${componentNode.data.title}`);
    },
    [graphPath, navigateToPath, nodes],
  );

  const goBackOneLevel = useCallback(() => {
    if (graphPath.length === 0) {
      return;
    }

    navigateToPath(graphPath.slice(0, -1), { fitView: false });
    setNotice('Вернулись в родительский граф');
  }, [graphPath, navigateToPath]);

  useEffect(() => {
    if (!isSettingsOpen) {
      return undefined;
    }

    const handleKeyDown = (event) => {
      if (event.key !== 'Escape' || event.defaultPrevented || event.repeat) {
        return;
      }

      event.preventDefault();
      setIsSettingsOpen(false);
    };

    window.addEventListener('keydown', handleKeyDown);

    return () => {
      window.removeEventListener('keydown', handleKeyDown);
    };
  }, [isSettingsOpen]);

  useEffect(() => {
    if (isSettingsOpen || graphPath.length === 0) {
      return undefined;
    }

    if (graphPath.length === 0) {
      return undefined;
    }

    const handleKeyDown = (event) => {
      if (event.key !== 'Escape' || event.defaultPrevented || event.repeat) {
        return;
      }

      if (isEditableTarget(event.target)) {
        return;
      }

      event.preventDefault();
      goBackOneLevel();
    };

    window.addEventListener('keydown', handleKeyDown);

    return () => {
      window.removeEventListener('keydown', handleKeyDown);
    };
  }, [goBackOneLevel, graphPath.length, isSettingsOpen]);

  const breadcrumbs = useMemo(() => {
    if (!workflowDocument) {
      return [];
    }

    const items = [
      {
        id: 'root',
        label: 'Корень',
      },
    ];

    let graph = workflowDocument.rootGraph;

    graphPath.forEach((nodeId, index) => {
      const node = graph.nodes.find((candidate) => candidate.id === nodeId && isComponentNodeType(candidate.type));
      if (!node) {
        return;
      }

      const pathSlice = graphPath.slice(0, index + 1);
      items.push({
        id: nodeId,
        label: node.data.title || 'Компонент',
        onClick: () => navigateToPath(pathSlice, { fitView: !node.data.subgraph?.viewport }),
      });

      graph = node.data.subgraph || createComponentSubgraph();
    });

    return items;
  }, [graphPath, navigateToPath, workflowDocument]);

  const onNodesChange = useCallback(
    (changes) => {
      updateCurrentGraph((graph) => ({
        ...graph,
        nodes: applyNodeChanges(changes, graph.nodes).map((node) => hydrateNode(node)),
      }));
    },
    [updateCurrentGraph],
  );

  const onEdgesChange = useCallback(
    (changes) => {
      updateCurrentGraph((graph) => ({
        ...graph,
        edges: applyEdgeChanges(changes, graph.edges),
      }));
    },
    [updateCurrentGraph],
  );

  const onConnect = useCallback(
    (connection) => {
      updateCurrentGraph((graph) => ({
        ...graph,
        edges: connectEdge(connection, graph.edges),
      }));
    },
    [updateCurrentGraph],
  );

  const handleCreateComponent = useCallback(() => {
    const result = createComponentFromSelection(currentGraph);

    if (result.error) {
      setNotice(result.error);
      return;
    }

    setWorkflowDocument((previousDocument) => updateGraphAtPath(previousDocument, graphPath, () => result.graph));
    setNotice('Компонент создан. Дважды нажмите на него или кнопку Открыть, чтобы войти.');
  }, [currentGraph, graphPath]);

  const handleToggleTheme = useCallback(() => {
    setTheme((currentTheme) => (currentTheme === 'dark' ? 'light' : 'dark'));
  }, []);

  const handleOpenSettings = useCallback(() => {
    setSettingsBackendUrl(backendUrl);
    setSettingsLiveDebugUrl(liveDebugUrl === DEFAULT_LIVE_DEBUG_URL ? '' : liveDebugUrl);
    setSettingsLlmProvider(normalizeLlmProvider(llmProvider));
    setSettingsDeepinfraApiKey(defaultDeepinfraApiKey);
    setSettingsDeepinfraKeyStatus('Проверяю DeepInfra key...');
    setIsSettingsOpen(true);

    getDeepinfraKeyStatus(backendUrl)
      .then((status) => {
        setSettingsDeepinfraHasKey(Boolean(status.has_key));
        setSettingsDeepinfraKeyStatus(
          status.has_key ? 'DeepInfra key уже сохранен локально на backend.' : 'DeepInfra key еще не сохранен.',
        );
      })
      .catch((error) => {
        console.error('Failed to check DeepInfra key:', error);
        setSettingsDeepinfraHasKey(false);
        setSettingsDeepinfraKeyStatus('Не удалось проверить DeepInfra key.');
      });
  }, [backendUrl, defaultDeepinfraApiKey, liveDebugUrl, llmProvider]);

  const handleSettingsResetToDefault = useCallback(() => {
    setSettingsBackendUrl(defaultBackendUrl);
    setSettingsLiveDebugUrl('');
    setSettingsLlmProvider(defaultLlmProvider);
    setSettingsDeepinfraApiKey(defaultDeepinfraApiKey);
  }, [defaultBackendUrl, defaultDeepinfraApiKey, defaultLlmProvider]);

  const handleSettingsSave = useCallback(async () => {
    const normalizedDraft = normalizeBackendUrl(settingsBackendUrl);
    const normalizedLiveDebugDraft = normalizeBackendUrl(settingsLiveDebugUrl);
    const nextLlmProvider = normalizeLlmProvider(settingsLlmProvider);

    if (!normalizedDraft) {
      setNotice('Введите корректный Backend URL');
      return;
    }

    const normalizedDefault = normalizeBackendUrl(defaultBackendUrl) || DEFAULT_BACKEND_URL;
    const backendUrlOverride = normalizedDraft === normalizedDefault ? '' : normalizedDraft;
    const nextBackendUrl = backendUrlOverride || normalizedDefault;
    const liveDebugUrlOverride = normalizedLiveDebugDraft && normalizedLiveDebugDraft !== DEFAULT_LIVE_DEBUG_URL
      ? normalizedLiveDebugDraft
      : '';
    const nextLiveDebugUrl = liveDebugUrlOverride || DEFAULT_LIVE_DEBUG_URL;

    if (nextLlmProvider === 'deepinfra') {
      const keyDraft = settingsDeepinfraApiKey.trim();
      let hasDeepinfraKey = settingsDeepinfraHasKey;

      try {
        if (keyDraft) {
          const saveResult = await saveDeepinfraKey(nextBackendUrl, keyDraft);
          if (!saveResult.success) {
            setSettingsDeepinfraKeyStatus(saveResult.message || 'Не удалось сохранить DeepInfra key.');
            setNotice(saveResult.message || 'Не удалось сохранить DeepInfra key');
            return;
          }

          hasDeepinfraKey = true;
          setSettingsDeepinfraApiKey('');
        } else {
          const status = await getDeepinfraKeyStatus(nextBackendUrl);
          hasDeepinfraKey = Boolean(status.has_key);
        }
      } catch (error) {
        console.error('Failed to save/check DeepInfra key:', error);
        setSettingsDeepinfraKeyStatus(error instanceof Error ? error.message : 'Не удалось сохранить или проверить DeepInfra key.');
        setNotice(error instanceof Error ? error.message : 'Не удалось сохранить или проверить DeepInfra key');
        return;
      }

      setSettingsDeepinfraHasKey(hasDeepinfraKey);
      if (!hasDeepinfraKey) {
        setSettingsDeepinfraKeyStatus('Введите DeepInfra API key перед сохранением provider DeepInfra.');
        setNotice('Введите DeepInfra API key');
        return;
      }
    }

    persistAppSettings({ backendUrlOverride, liveDebugUrlOverride });
    setBackendUrl(nextBackendUrl);
    setLiveDebugUrl(nextLiveDebugUrl);
    setSettingsBackendUrl(nextBackendUrl);
    setSettingsLiveDebugUrl(liveDebugUrlOverride);
    setLlmProvider(nextLlmProvider);
    updateWorkflowSettings({ llmProvider: nextLlmProvider });
    setIsSettingsOpen(false);
    setNotice(`Настройки сохранены: ${nextLlmProvider}, backend ${nextBackendUrl}, live ${nextLiveDebugUrl}`);
  }, [
    defaultBackendUrl,
    settingsBackendUrl,
    settingsDeepinfraApiKey,
    settingsDeepinfraHasKey,
    settingsLiveDebugUrl,
    settingsLlmProvider,
    updateWorkflowSettings,
  ]);

  const appendInteractiveLog = useCallback((level, message) => {
    const id = `log-${logCounterRef.current}`;
    logCounterRef.current += 1;
    setInteractiveLogs((currentLogs) => [
      ...currentLogs,
      {
        id,
        level,
        message,
        time: new Date().toLocaleTimeString(),
      },
    ]);
  }, []);

  const getWorkflowLogName = useCallback(() => fileHandleRef.current?.name || 'workflow', []);

  const ensureTranscriptLog = useCallback(async () => {
    if (!writeTranscriptLogsRef.current) {
      return null;
    }

    if (transcriptLogRef.current) {
      return transcriptLogRef.current;
    }

    const log = await startTranscriptLog(backendUrl, getWorkflowLogName());
    transcriptLogRef.current = {
      logId: log.log_id,
      logPath: log.log_path,
    };
    appendInteractiveLog('info', `Txt-лог: ${log.log_path}`);
    return transcriptLogRef.current;
  }, [appendInteractiveLog, backendUrl, getWorkflowLogName]);

  const writeTranscriptMessage = useCallback(
    (role, text) => {
      if (!writeTranscriptLogsRef.current) {
        return;
      }

      ensureTranscriptLog()
        .then((log) => {
          if (!log?.logPath) {
            return;
          }
          return appendTranscriptLog(backendUrl, log.logPath, role, text);
        })
        .catch((error) => {
          appendInteractiveLog(
            'error',
            error instanceof Error ? error.message : 'Не удалось записать txt-лог',
          );
        });
    },
    [appendInteractiveLog, backendUrl, ensureTranscriptLog],
  );

  const appendTranscriptMessage = useCallback((role, text) => {
    const id = `message-${transcriptCounterRef.current}`;
    transcriptCounterRef.current += 1;
    const message = {
      id,
      role,
      text,
    };
    interactiveTranscriptRef.current = [...interactiveTranscriptRef.current, message];
    setInteractiveTranscript(interactiveTranscriptRef.current);
    writeTranscriptMessage(role, text);
  }, [writeTranscriptMessage]);

  const handleInteractiveEvent = useCallback(
    (event) => {
      if (event.type === 'assistant-question') {
        appendTranscriptMessage('assistant', event.question);
        if (!llmAutoAnswer && !llmTestModeRef.current) {
          appendInteractiveLog('info', `Вопрос: ${event.question}`);
        }
        if (llmTestModeRef.current && testRunRef.current) {
          evaluateAssistantAnswer(
            backendUrl,
            '',
            event.question,
            interactiveTranscriptRef.current,
            testRunRef.current,
            'question',
            llmProvider,
            llmSessionIdRef.current,
          )
            .then((evaluation) => {
              if (evaluation.ok) {
                appendInteractiveLog('debug', `LLM-тест вопроса OK: ${evaluation.reason || 'вопрос принят'}`);
              } else {
                appendInteractiveLog(
                  'error',
                  `LLM-тест вопроса провален: ${evaluation.reason || 'плохой вопрос ассистента'}. Лог: ${evaluation.log_path || 'test log'}`,
                );
              }
            })
            .catch((error) => {
              appendInteractiveLog(
                'error',
                error instanceof Error ? error.message : 'Не удалось оценить вопрос ассистента',
              );
            });
        }
        return;
      }

      if (event.type === 'user-answer') {
        lastUserAnswerRef.current = event.answer;
        appendTranscriptMessage('user', event.answer);
        if (suppressNextUserAnswerLogRef.current) {
          suppressNextUserAnswerLogRef.current = false;
        } else if (!llmAutoAnswer && !llmTestModeRef.current) {
          appendInteractiveLog('info', `Ответ: ${event.answer}`);
        }
        return;
      }

      if (event.type === 'assistant-message') {
        appendTranscriptMessage('assistant', event.message);
        if (!llmAutoAnswer && !llmTestModeRef.current) {
          appendInteractiveLog('info', `Ассистент: ${event.message}`);
        }
        if (llmTestModeRef.current && testRunRef.current) {
          evaluateAssistantAnswer(
            backendUrl,
            lastUserAnswerRef.current,
            event.message,
            interactiveTranscriptRef.current,
            testRunRef.current,
            'answer',
            llmProvider,
            llmSessionIdRef.current,
          )
            .then((evaluation) => {
              if (evaluation.ok) {
                appendInteractiveLog('debug', `LLM-тест OK: ${evaluation.reason || 'ответ принят'}`);
              } else {
                appendInteractiveLog(
                  'error',
                  `LLM-тест провален: ${evaluation.reason || 'плохой ответ ассистента'}. Лог: ${evaluation.log_path || 'test log'}`,
                );
              }
            })
            .catch((error) => {
              appendInteractiveLog(
                'error',
                error instanceof Error ? error.message : 'Не удалось оценить ответ ассистента',
              );
            });
        }
        return;
      }

      if (event.type === 'memory-update') {
        const summary = Object.entries(event.memoryPatch || {})
          .map(([key, value]) => `{${key}}=${value}`)
          .join(', ');
        appendInteractiveLog('info', `Память сохранена: ${summary}`);
        return;
      }

      if (event.type === 'node-start') {
        appendInteractiveLog('debug', `Старт ноды: ${event.title}`);
        return;
      }

      if (event.type === 'node-success') {
        appendInteractiveLog('debug', `Нода завершена: ${event.title}`);
        return;
      }

      if (event.type === 'skip') {
        appendInteractiveLog('debug', `Нода пропущена: ${event.title}`);
        return;
      }

      if (event.type === 'node-error') {
        appendInteractiveLog('error', `Ошибка в ${event.title}: ${event.error}`);
        return;
      }

      if (event.type === 'restart') {
        appendInteractiveLog('info', 'Сценарий перезапускается с начала');
      }
    },
    [appendInteractiveLog, appendTranscriptMessage, backendUrl, llmAutoAnswer, llmProvider],
  );

  const applyLiveDebugPayload = useCallback(
    (payload) => {
      if (!payload || typeof payload !== 'object') {
        return;
      }

      if (payload.type === 'live-disabled') {
        setLiveDebugStatus('disabled');
        setNotice('Live debug выключен в runtime. Включите галочку в окне main.py.');
        appendInteractiveLog('error', 'Runtime не отдает live debug: галочка в main.py выключена.');
        return;
      }

      const state = payload.state || {};
      const latestResponse = state.last_response || state.lastResponse || null;
      const runtimeResponse = state.runtime || latestResponse || payload.runtime || null;
      const nextGraph = runtimeResponse?.graph || state.graph || null;

      if (nextGraph) {
        const hydratedGraph = hydrateGraph(nextGraph);
        setWorkflowDocument((previousDocument) =>
          previousDocument
            ? updateGraphAtPath(previousDocument, [], (editorGraph) =>
                mergeRuntimeIntoEditorGraph(editorGraph, hydratedGraph),
              )
            : previousDocument,
        );

        if (graphPath.length > 0) {
          setGraphPath([]);
        }
      }

      const nextPendingQuestion = state.pending_question || runtimeResponse?.pending_question || null;
      setPendingQuestion(nextPendingQuestion);

      const events = Array.isArray(payload.events) && payload.events.length > 0
        ? payload.events
        : Array.isArray(runtimeResponse?.events) && runtimeResponse.events.length > 0
          ? runtimeResponse.events
          : Array.isArray(latestResponse?.events)
            ? latestResponse.events
          : [];

      events.forEach((event) => {
        const eventKey = getLiveDebugEventKey(event);
        if (liveDebugEventKeysRef.current.has(eventKey)) {
          return;
        }

        liveDebugEventKeysRef.current.add(eventKey);
        handleInteractiveEvent(event);
      });

      const status = state.status || runtimeResponse?.status || 'running';
      if (status === 'paused') {
        setNotice('Live debug: workflow ждет ответ пользователя');
      } else if (status === 'complete') {
        setNotice('Live debug: workflow завершен');
      } else if (status === 'idle') {
        setNotice('Live debug: runtime подключен, активного workflow пока нет');
      } else {
        setNotice('Live debug: workflow выполняется');
      }
    },
    [appendInteractiveLog, graphPath.length, handleInteractiveEvent],
  );

  const closeLiveDebugConnection = useCallback((status = 'off') => {
    const socket = liveDebugSocketRef.current;
    liveDebugSocketRef.current = null;

    if (socket && socket.readyState !== WebSocket.CLOSED) {
      socket.close();
    }

    setLiveDebugStatus(status);
  }, []);

  const handleToggleLiveDebug = useCallback(() => {
    if (liveDebugSocketRef.current) {
      closeLiveDebugConnection('off');
      setNotice('Live debug отключен');
      appendInteractiveLog('info', 'Live debug отключен');
      return;
    }

    if (!workflowDocument) {
      setNotice('Откройте workflow JSON перед live debug');
      return;
    }

    const wsUrl = resolveLiveDebugWebSocketUrl(liveDebugUrl);
    let socket;

    try {
      socket = new WebSocket(wsUrl);
    } catch (error) {
      console.error('Failed to open live debug socket:', error);
      setLiveDebugStatus('error');
      setNotice('Не удалось открыть WebSocket live debug');
      return;
    }

    liveDebugSocketRef.current = socket;
    liveDebugEventKeysRef.current = new Set();
    transcriptCounterRef.current = 0;
    interactiveTranscriptRef.current = [];
    transcriptLogRef.current = null;
    lastUserAnswerRef.current = '';
    autoAnsweredQuestionRef.current = null;
    llmTestModeRef.current = false;
    llmTestStopRequestedRef.current = false;
    setLlmTestMode(false);
    setPendingQuestion(null);
    setInteractiveTranscript([]);
    setInteractiveLogs([]);
    setAnswerDraft('');
    setLiveDebugStatus('connecting');
    setNotice('Подключаю live debug...');

    socket.addEventListener('open', () => {
      setLiveDebugStatus('connected');
      setNotice('Live debug подключен');
      appendInteractiveLog('info', `Live debug подключен: ${wsUrl}`);
    });

    socket.addEventListener('message', (event) => {
      try {
        applyLiveDebugPayload(JSON.parse(event.data));
      } catch (error) {
        console.error('Failed to parse live debug message:', error);
        appendInteractiveLog('error', 'Live debug прислал невалидный JSON');
      }
    });

    socket.addEventListener('error', () => {
      setLiveDebugStatus('error');
      setNotice('Ошибка WebSocket live debug');
    });

    socket.addEventListener('close', (event) => {
      liveDebugSocketRef.current = null;
      if (event.code === 1008) {
        setLiveDebugStatus('disabled');
        return;
      }

      setLiveDebugStatus((currentStatus) => (currentStatus === 'disabled' ? currentStatus : 'off'));
    });
  }, [
    appendInteractiveLog,
    applyLiveDebugPayload,
    backendUrl,
    closeLiveDebugConnection,
    liveDebugUrl,
    workflowDocument,
  ]);

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

  const continueInteractiveRun = useCallback(
    async (userAnswer = null) => {
      if (!interactiveRunRef.current) {
        return;
      }

      if (llmTestStopRequestedRef.current) {
        setIsRunning(false);
        return;
      }

      setIsRunning(true);
      setNotice(userAnswer ? 'Продолжаю интерактивный прогон...' : 'Запускаю интерактивный прогон...');

      try {
        const executionPath = [...interactiveRunPathRef.current];
        const result = await continueInteractiveWorkflowRun(interactiveRunRef.current, {
          backendUrl,
          llmProvider,
          llmSessionId: llmSessionIdRef.current,
          userAnswer,
          knowledgeContextId: '',
          testRun: testRunRef.current,
          onGraphChange: (nextGraph) => {
            const hydratedRuntimeGraph = hydrateGraph(nextGraph);
            setWorkflowDocument((previousDocument) =>
              updateGraphAtPath(previousDocument, executionPath, (editorGraph) =>
                mergeRuntimeIntoEditorGraph(editorGraph, hydratedRuntimeGraph),
              ),
            );
          },
          onEvent: handleInteractiveEvent,
        });

        interactiveRunRef.current = result.state;
        setPendingQuestion(result.pendingQuestion);

        if (llmTestStopRequestedRef.current) {
          appendInteractiveLog('info', `LLM-тест остановлен. Лог: ${testRunRef.current?.logPath || 'test log'}`);
          llmTestModeRef.current = false;
          llmTestStopRequestedRef.current = false;
          setLlmTestMode(false);
          setNotice('LLM-тест остановлен');
          return;
        }

        if (result.status === 'restart') {
          const restartState = createInteractiveWorkflowRun(result.state.graph);
          interactiveRunRef.current = restartState;
          llmSessionIdRef.current = createLlmSessionId();
          transcriptCounterRef.current = 0;
          interactiveTranscriptRef.current = [];
          transcriptLogRef.current = null;
          lastUserAnswerRef.current = '';
          autoAnsweredQuestionRef.current = null;
          setInteractiveTranscript([]);
          appendInteractiveLog('info', 'Интерактивный прогон перезапущен');
          const restartResult = await continueInteractiveWorkflowRun(restartState, {
            backendUrl,
            llmProvider,
            llmSessionId: llmSessionIdRef.current,
            knowledgeContextId: '',
            testRun: testRunRef.current,
            onGraphChange: (nextGraph) => {
              const hydratedRuntimeGraph = hydrateGraph(nextGraph);
              setWorkflowDocument((previousDocument) =>
                updateGraphAtPath(previousDocument, executionPath, (editorGraph) =>
                  mergeRuntimeIntoEditorGraph(editorGraph, hydratedRuntimeGraph),
                ),
              );
            },
            onEvent: handleInteractiveEvent,
          });
          interactiveRunRef.current = restartResult.state;
          setPendingQuestion(restartResult.pendingQuestion);
          setNotice(restartResult.status === 'complete' ? 'Интерактивный прогон завершен' : 'Ждем ответ пользователя');
        } else if (result.status === 'complete') {
          appendInteractiveLog('info', 'Интерактивный прогон завершен');
          if (llmTestModeRef.current) {
            appendInteractiveLog('info', `LLM-тест завершен. Лог: ${testRunRef.current?.logPath || 'test log'}`);
            llmTestModeRef.current = false;
            llmTestStopRequestedRef.current = false;
            setLlmTestMode(false);
          }
          setNotice(`Интерактивный прогон завершен: ${result.state.order.length} нод`);
        } else {
          setNotice('Ждем ответ пользователя');
        }
      } catch (error) {
        console.error('Interactive workflow execution failed:', error);
        appendInteractiveLog('error', error instanceof Error ? error.message : 'Интерактивный прогон упал');
        if (llmTestModeRef.current) {
          llmTestModeRef.current = false;
          llmTestStopRequestedRef.current = false;
          setLlmTestMode(false);
        }
        setNotice(error instanceof Error ? error.message : 'Интерактивный прогон упал');
      } finally {
        setIsRunning(false);
      }
    },
    [appendInteractiveLog, backendUrl, handleInteractiveEvent, llmProvider],
  );

  const handleRun = useCallback(async () => {
    if (isRunning || !workflowDocument) {
      return;
    }

    const executionPath = [...graphPath];
    const graphForRun = getGraphAtPath(workflowDocument, executionPath);

    logCounterRef.current = 0;
    transcriptCounterRef.current = 0;
    interactiveTranscriptRef.current = [];
    transcriptLogRef.current = null;
    llmSessionIdRef.current = createLlmSessionId();
    lastUserAnswerRef.current = '';
    llmTestModeRef.current = false;
    llmTestStopRequestedRef.current = false;
    setLlmTestMode(false);
    testRunRef.current = null;
    testAutoAnswerCountRef.current = 0;
    interactiveRunPathRef.current = executionPath;
    interactiveRunRef.current = createInteractiveWorkflowRun(graphForRun);
    autoAnsweredQuestionRef.current = null;
    setPendingQuestion(null);
    setInteractiveTranscript([]);
    setInteractiveLogs([]);
    setAnswerDraft('');
    appendInteractiveLog('info', 'Интерактивный прогон запущен');

    await continueInteractiveRun();
  }, [appendInteractiveLog, continueInteractiveRun, graphPath, isRunning, workflowDocument]);

  const handleLlmTestRun = useCallback(async () => {
    if (isRunning || llmTestMode || !workflowDocument) {
      return;
    }

    const workflowName = fileHandleRef.current?.name || 'workflow';
    const testRun = await startTestRun(backendUrl, workflowName);
    testRunRef.current = {
      testRunId: testRun.test_run_id,
      workflowName,
      logPath: testRun.log_path,
    };
    llmTestModeRef.current = true;
    llmTestStopRequestedRef.current = false;
    setLlmTestMode(true);
    testAutoAnswerCountRef.current = 0;

    const executionPath = [...graphPath];
    const graphForRun = getGraphAtPath(workflowDocument, executionPath);

    logCounterRef.current = 0;
    transcriptCounterRef.current = 0;
    interactiveTranscriptRef.current = [];
    transcriptLogRef.current = null;
    llmSessionIdRef.current = createLlmSessionId();
    lastUserAnswerRef.current = '';
    autoAnsweredQuestionRef.current = null;
    interactiveRunPathRef.current = executionPath;
    interactiveRunRef.current = createInteractiveWorkflowRun(graphForRun);
    setPendingQuestion(null);
    setInteractiveTranscript([]);
    setInteractiveLogs([]);
    setAnswerDraft('');
    appendInteractiveLog('info', `LLM-тест запущен. Лог: ${testRun.log_path}`);

    await continueInteractiveRun();
  }, [appendInteractiveLog, backendUrl, continueInteractiveRun, graphPath, isRunning, llmTestMode, workflowDocument]);

  const handleStopLlmTest = useCallback(() => {
    if (!llmTestModeRef.current) {
      return;
    }

    llmTestStopRequestedRef.current = true;
    llmTestModeRef.current = false;
    setLlmTestMode(false);
    autoAnsweredQuestionRef.current = null;
    appendInteractiveLog('info', `Останавливаю LLM-тест... Лог: ${testRunRef.current?.logPath || 'test log'}`);
    setNotice('Останавливаю LLM-тест...');
  }, [appendInteractiveLog]);

  const handleNew = useCallback(() => {
    const confirmed = window.confirm(
      'Не забудьте сохранить текущий workflow если нужно.\n\nСоздать новый workflow?',
    );

    if (!confirmed) {
      return;
    }

    const nextDocument = createBlankWorkflow();
    const nextDocumentWithSettings = {
      ...nextDocument,
      settings: {
        ...(nextDocument.settings || {}),
        llmRolePrompt: DEFAULT_LLM_ROLE_PROMPT,
        llmProvider: defaultLlmProvider,
      },
    };
    setLlmRolePrompt(DEFAULT_LLM_ROLE_PROMPT);
    setLlmProvider(defaultLlmProvider);
    setWorkflowDocument(nextDocumentWithSettings);
    applyComponentTemplates(componentTemplates, { persist: false });
    setGraphPath([]);
    syncViewportForGraph(nextDocumentWithSettings.rootGraph, { fitView: true });
    setNotice('Создан новый workflow');
  }, [applyComponentTemplates, componentTemplates, defaultLlmProvider, syncViewportForGraph]);

  const writeWorkflowToHandle = useCallback(async (fileHandle, data) => {
    const writable = await fileHandle.createWritable();
    await writable.write(data);
    await writable.close();
  }, []);

  const downloadWorkflowFallback = useCallback((data) => {
    const blob = new Blob([data], { type: 'application/json' });
    const url = URL.createObjectURL(blob);
    const link = document.createElement('a');
    link.href = url;
    link.download = `workflow_${Date.now()}.json`;
    link.click();
    URL.revokeObjectURL(url);
  }, []);

  const handleSaveAs = useCallback(async () => {
    if (!workflowDocument) {
      return;
    }

    const data = JSON.stringify(
      serializeWorkflow({
        ...workflowDocument,
        settings: {
          ...(workflowDocument.settings || {}),
          llmRolePrompt,
          llmProvider,
        },
      }),
      null,
      2,
    );

    try {
      if ('showSaveFilePicker' in window) {
        const fileHandle = await window.showSaveFilePicker({
          suggestedName: `workflow_${Date.now()}.json`,
          types: [
            {
              description: 'JSON-файлы',
              accept: { 'application/json': ['.json'] },
            },
          ],
          ...(fileHandleRef.current ? { startIn: fileHandleRef.current } : {}),
        });

        fileHandleRef.current = fileHandle;
        await writeWorkflowToHandle(fileHandle, data);
      } else {
        downloadWorkflowFallback(data);
      }

      setNotice('Workflow сохранен');
    } catch (error) {
      if (error?.name !== 'AbortError') {
        console.error('Failed to save workflow:', error);
        setNotice('Не удалось сохранить workflow');
      }
    }
  }, [downloadWorkflowFallback, llmProvider, llmRolePrompt, workflowDocument, writeWorkflowToHandle]);

  const handleSave = useCallback(async () => {
    if (!workflowDocument) {
      return;
    }

    const document = serializeWorkflow({
        ...workflowDocument,
        settings: {
          ...(workflowDocument.settings || {}),
          llmRolePrompt,
          llmProvider,
        },
    });

    try {
      const response = await saveDefaultWorkflow(backendUrl, document);
      setNotice(response?.path ? `Workflow сохранен на сервере: ${response.path}` : 'Workflow сохранен на сервере');
    } catch (error) {
      if (error?.name !== 'AbortError') {
        console.error('Failed to save workflow:', error);
        setNotice('Не удалось сохранить workflow на сервере');
      }
    }
  }, [backendUrl, llmProvider, llmRolePrompt, workflowDocument]);

  const loadFromText = useCallback(
    (contents) => {
      const parsedDocument = parseWorkflowData(JSON.parse(contents));
      const mergedTemplates = mergeComponentTemplates(parsedDocument.componentTemplates || [], loadComponentLibrary());
      const parsedLlmProvider = normalizeLlmProvider(parsedDocument.settings?.llmProvider);
      setWorkflowDocument({
        ...parsedDocument,
        componentTemplates: mergedTemplates,
        settings: {
          ...(parsedDocument.settings || {}),
          llmRolePrompt: parsedDocument.settings?.llmRolePrompt || DEFAULT_LLM_ROLE_PROMPT,
          llmProvider: parsedLlmProvider,
        },
      });
      setLlmRolePrompt(parsedDocument.settings?.llmRolePrompt || DEFAULT_LLM_ROLE_PROMPT);
      setLlmProvider(parsedLlmProvider);
      setComponentTemplates(mergedTemplates);
      persistComponentLibrary(mergedTemplates);
      setGraphPath([]);
      syncViewportForGraph(parsedDocument.rootGraph, { fitView: true });
      setNotice('Workflow открыт');
    },
    [syncViewportForGraph],
  );

  const handleLoad = useCallback(async () => {
    try {
      if ('showOpenFilePicker' in window) {
        const [fileHandle] = await window.showOpenFilePicker({
          types: [
            {
              description: 'JSON-файлы',
              accept: { 'application/json': ['.json'] },
            },
          ],
          multiple: false,
          ...(fileHandleRef.current ? { startIn: fileHandleRef.current } : {}),
        });

        fileHandleRef.current = fileHandle;
        const file = await fileHandle.getFile();
        loadFromText(await file.text());
      } else {
        fileInputRef.current?.click();
      }
    } catch (error) {
      if (error?.name !== 'AbortError') {
        console.error('Failed to load workflow:', error);
        setNotice('Не удалось открыть workflow');
      }
    }
  }, [loadFromText]);

  const handleSubmitAnswer = useCallback(
    async (event) => {
      event.preventDefault();
      const answer = answerDraft.trim();

      if (!answer || !pendingQuestion) {
        return;
      }

      setAnswerDraft('');
      suppressNextUserAnswerLogRef.current = true;
      await continueInteractiveRun(answer);
    },
    [answerDraft, continueInteractiveRun, pendingQuestion],
  );

  const saveTextFile = useCallback(async (suggestedName, data) => {
    if ('showSaveFilePicker' in window) {
      const fileHandle = await window.showSaveFilePicker({
        suggestedName,
        types: [
          {
            description: 'Текстовые файлы',
            accept: { 'text/plain': ['.txt'] },
          },
        ],
      });
      const writable = await fileHandle.createWritable();
      await writable.write(data);
      await writable.close();
      return;
    }

    const blob = new Blob([data], { type: 'text/plain;charset=utf-8' });
    const url = URL.createObjectURL(blob);
    const link = document.createElement('a');
    link.href = url;
    link.download = suggestedName;
    link.click();
    URL.revokeObjectURL(url);
  }, []);

  const handleSaveSessionLog = useCallback(async () => {
    const sessionId = llmSessionIdRef.current;

    try {
      const llmLog = sessionId ? await getLlmSessionLog(backendUrl, sessionId) : { calls: [] };
      const workflowName = getWorkflowLogName();
      const contents = formatSessionLog({
        workflowName,
        sessionId,
        provider: llmProvider,
        transcript: interactiveTranscriptRef.current,
        runtimeLogs: interactiveLogs,
        llmCalls: Array.isArray(llmLog.calls) ? llmLog.calls : [],
      });
      const filename = `${safeDownloadName(workflowName)}_${sessionId || Date.now()}_session_log.txt`;

      await saveTextFile(filename, contents);
      setNotice('Лог сессии сохранен');
    } catch (error) {
      if (error?.name !== 'AbortError') {
        console.error('Failed to save session log:', error);
        setNotice(error instanceof Error ? error.message : 'Не удалось сохранить лог сессии');
      }
    }
  }, [backendUrl, getWorkflowLogName, interactiveLogs, llmProvider, saveTextFile]);

  const handleSimulateAnswer = useCallback(async () => {
    if (!pendingQuestion || isRunning) {
      return;
    }

    if (llmTestStopRequestedRef.current) {
      return;
    }

    setIsRunning(true);

    try {
      const answer = await simulateUserAnswer(
        backendUrl,
        llmProvider,
        llmRolePrompt,
        pendingQuestion.question,
        interactiveTranscript,
        llmTestModeRef.current ? testRunRef.current : null,
        llmSessionIdRef.current,
      );

      if (llmTestStopRequestedRef.current) {
        appendInteractiveLog('info', `LLM-тест остановлен. Лог: ${testRunRef.current?.logPath || 'test log'}`);
        llmTestStopRequestedRef.current = false;
        setNotice('LLM-тест остановлен');
        return;
      }

      if (!answer) {
        appendInteractiveLog('error', 'LLM вернула пустой ответ');
        setNotice('LLM вернула пустой ответ');
        return;
      }

      setAnswerDraft('');
      await continueInteractiveRun(answer);
    } catch (error) {
      suppressNextUserAnswerLogRef.current = false;
      console.error('Failed to simulate user answer:', error);
      appendInteractiveLog('error', error instanceof Error ? error.message : 'Не удалось сгенерировать ответ пользователя');
      setNotice(error instanceof Error ? error.message : 'Не удалось сгенерировать ответ пользователя');
    } finally {
      setIsRunning(false);
    }
  }, [
    appendInteractiveLog,
    backendUrl,
    continueInteractiveRun,
    interactiveTranscript,
    isRunning,
    llmAutoAnswer,
    llmProvider,
    llmRolePrompt,
    pendingQuestion,
  ]);

  const pendingQuestionKey = pendingQuestion
    ? `${pendingQuestion.nodeId}:${pendingQuestion.retryForNodeId || ''}:${pendingQuestion.question}`
    : '';

  useEffect(() => {
    const shouldAutoAnswer = llmAutoAnswer || llmTestMode;

    if (!shouldAutoAnswer) {
      autoAnsweredQuestionRef.current = null;
      return;
    }

    if (llmTestStopRequestedRef.current) {
      return;
    }

    if (!pendingQuestion || !pendingQuestionKey || isRunning || !llmRolePrompt.trim()) {
      return;
    }

    if (autoAnsweredQuestionRef.current === pendingQuestionKey) {
      return;
    }

    if (llmTestMode && testAutoAnswerCountRef.current >= 40) {
      appendInteractiveLog('error', 'LLM-тест остановлен: достигнут лимит 40 автоответов. Проверьте цикл сценария или ответы.');
      llmTestModeRef.current = false;
      llmTestStopRequestedRef.current = false;
      setLlmTestMode(false);
      return;
    }

    testAutoAnswerCountRef.current += 1;
    autoAnsweredQuestionRef.current = pendingQuestionKey;
    handleSimulateAnswer();
  }, [
    appendInteractiveLog,
    handleSimulateAnswer,
    isRunning,
    llmAutoAnswer,
    llmRolePrompt,
    llmTestMode,
    pendingQuestion,
    pendingQuestionKey,
  ]);

  const contextValue = useMemo(
    () => ({
      addNode,
      backendUrl,
      getNodeHandles,
      graphPath,
      isRunning,
      openComponent,
      patchNodeData,
      saveComponentNode,
      presets,
      refreshPresets,
      removeHandleConnections,
      replaceNodeData,
      savePreset,
    }),
    [
      addNode,
      backendUrl,
      getNodeHandles,
      graphPath,
      isRunning,
      openComponent,
      patchNodeData,
      saveComponentNode,
      presets,
      refreshPresets,
      removeHandleConnections,
      replaceNodeData,
      savePreset,
    ],
  );

  return (
    <WorkflowProvider value={contextValue}>
      <div className={`app-shell ${isDemoMode ? 'app-shell--demo' : ''}`}>
        <Toolbar
          backendUrl={backendUrl}
          liveDebugUrl={liveDebugUrl}
          breadcrumbs={breadcrumbs}
          canCreateComponent={selectedNodeCount > 0}
          isDemo={isDemoMode}
          isInsideComponent={graphPath.length > 0}
          isRunning={isRunning}
          notice={notice}
          onCreateComponent={handleCreateComponent}
          onGoBack={goBackOneLevel}
          onGoRoot={() => navigateToPath([], { fitView: false })}
          onOpenSettings={handleOpenSettings}
          theme={theme}
          onToggleTheme={handleToggleTheme}
          onRun={handleRun}
          liveDebugStatus={liveDebugStatus}
          onToggleLiveDebug={handleToggleLiveDebug}
          llmTestMode={llmTestMode}
          onLlmTestRun={handleLlmTestRun}
          onStopLlmTest={handleStopLlmTest}
          onNew={handleNew}
          onSave={handleSave}
          onSaveAs={handleSaveAs}
          onLoad={handleLoad}
        />

        {!isDemoMode ? (
          <div className={`app-shell__body ${isPaletteVisible ? '' : 'app-shell__body--palette-hidden'}`}>
            {isPaletteVisible ? (
              <NodePalette
                definitions={listNodeDefinitions()}
                componentTemplates={componentTemplates}
                onAddNode={addNode}
                onAddComponentNode={addComponentNodeFromLibrary}
                onHide={() => setIsPaletteVisible(false)}
              />
            ) : null}

            <div className="app-shell__canvas">
              <button
                type="button"
                className="palette-toggle"
                onClick={() => setIsPaletteVisible((current) => !current)}
              >
                {isPaletteVisible ? 'Скрыть библиотеку' : 'Показать библиотеку'}
              </button>
              <div className="flow-surface">
                <ReactFlow
                  nodes={nodes}
                  edges={edges}
                  nodeTypes={nodeTypes}
                  onNodesChange={onNodesChange}
                  onEdgesChange={onEdgesChange}
                  onConnect={onConnect}
                  onNodeDoubleClick={(_, node) => {
                    if (isComponentNodeType(node.type)) {
                      openComponent(node.id);
                    }
                  }}
                  onMoveEnd={() => {
                    updateCurrentGraph((graph) => ({
                      ...graph,
                      viewport: reactFlow.getViewport(),
                    }));
                  }}
                  isValidConnection={(connection) => isValidConnection(connection, nodes, edges)}
                  defaultViewport={DEFAULT_VIEWPORT}
                  fitView
                  minZoom={0.25}
                  maxZoom={1.8}
                  colorMode={theme}
                  proOptions={{ hideAttribution: true }}
                >
                  <Background
                    color={theme === 'dark' ? 'rgba(158, 180, 199, 0.16)' : 'rgba(99, 115, 129, 0.18)'}
                    gap={18}
                    size={1}
                  />
                  <MiniMap
                    pannable
                    zoomable
                    maskColor={theme === 'dark' ? 'rgba(12, 16, 22, 0.7)' : 'rgba(244, 248, 251, 0.75)'}
                    nodeColor={theme === 'dark' ? '#7fe4be' : '#3f7f6c'}
                    nodeStrokeColor={theme === 'dark' ? '#ebf2f8' : '#20303b'}
                  />
                  <Controls />
                </ReactFlow>
              </div>
            </div>
          </div>
        ) : null}

        <InteractivePanel
          isDemo={isDemoMode}
          pendingQuestion={pendingQuestion}
          transcript={interactiveTranscript}
          inputValue={answerDraft}
          isBusy={isRunning}
          llmProvider={llmProvider}
          llmAutoAnswer={llmAutoAnswer}
          writeTranscriptLogs={writeTranscriptLogs}
          llmRolePrompt={llmRolePrompt}
          onInputChange={setAnswerDraft}
          onLlmAutoAnswerChange={setLlmAutoAnswer}
          onWriteTranscriptLogsChange={(checked) => {
            writeTranscriptLogsRef.current = checked;
            setWriteTranscriptLogs(checked);
            if (!checked) {
              transcriptLogRef.current = null;
            }
          }}
          onLlmRolePromptChange={handleLlmRolePromptChange}
          onSubmitAnswer={handleSubmitAnswer}
          onSimulateAnswer={handleSimulateAnswer}
          onSaveSessionLog={handleSaveSessionLog}
        />

        <input
          ref={fileInputRef}
          type="file"
          accept=".json"
          style={{ display: 'none' }}
          onChange={async (event) => {
            const file = event.target.files?.[0];
            if (!file) {
              return;
            }

            try {
              loadFromText(await file.text());
            } catch (error) {
              console.error('Failed to parse workflow file:', error);
              setNotice('Не удалось прочитать workflow');
            } finally {
              event.target.value = '';
            }
          }}
        />

        <SettingsDialog
          isOpen={isSettingsOpen}
          backendUrl={settingsBackendUrl}
          defaultBackendUrl={defaultBackendUrl}
          liveDebugUrl={settingsLiveDebugUrl}
          llmProvider={settingsLlmProvider}
          deepinfraApiKey={settingsDeepinfraApiKey}
          deepinfraHasKey={settingsDeepinfraHasKey}
          deepinfraKeyStatus={settingsDeepinfraKeyStatus}
          onBackendUrlChange={setSettingsBackendUrl}
          onLiveDebugUrlChange={setSettingsLiveDebugUrl}
          onLlmProviderChange={setSettingsLlmProvider}
          onDeepinfraApiKeyChange={setSettingsDeepinfraApiKey}
          onClose={() => setIsSettingsOpen(false)}
          onResetToDefault={handleSettingsResetToDefault}
          onSave={handleSettingsSave}
        />
      </div>
    </WorkflowProvider>
  );
}

export default function App() {
  return (
    <ReactFlowProvider>
      <WorkflowEditor />
    </ReactFlowProvider>
  );
}
