import { useState, useEffect, useCallback, useRef, useMemo, lazy, Suspense } from 'react';
import {
  ReactFlow,
  MiniMap,
  applyNodeChanges,
  applyEdgeChanges,
  addEdge,
  Background,
  Controls,
  PanOnScrollMode,
  type Node,
  type Edge,
  type ReactFlowInstance,
} from 'reactflow';
import 'reactflow/dist/style.css';
import { useRouter } from 'next/router';
import {
  Alert,
  Box,
  Typography,
  CircularProgress,
  Drawer,
  Tooltip,
  Divider,
  FormControlLabel,
  Checkbox,
  Menu,
  MenuItem,
  ListItemIcon,
  ListItemText as MuiListItemText,
} from '@mui/material';
import { Input } from '@components1/ds/Input';
import { Select } from '@components1/ds/Select';
import { Chip as DsChip } from '@components1/ds/Chip';
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import AddIcon from '@mui/icons-material/Add';
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
import StopCircleOutlinedIcon from '@mui/icons-material/StopCircleOutlined';
import CloseIcon from '@mui/icons-material/Close';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import PublishedWithChangesIcon from '@mui/icons-material/PublishedWithChanges';

type WorkflowVersionEntry = {
  id: string;
  workflow_id: string;
  version_number: number;
  source: 'create' | 'publish' | 'restore';
  restored_from_version?: number | null;
  name?: string | null;
  description?: string | null;
  is_live?: boolean;
  // Per-version gate (V746). When this row is live, workflows.status mirrors
  // this value. Surfaced in the version-history drawer so users can toggle
  // any version's status from a single place (one dropdown per row).
  status?: 'ACTIVE' | 'PAUSED' | 'INACTIVE';
  created_by?: string;
  created_by_user?: { id: string; display_name: string } | null;
  created_at: string;
};
import WorkflowHeader from './WorkflowHeader';
import NodeCategoriesSidebar from './modals/NodeCategoriesSidebar';
import TriggerSelectorPopup from './modals/TriggerSelectorPopup';
import { ds } from 'src/utils/colors';

// Extracted components (lightweight, always visible)
import { TriggerWarningMessage, ExecutionStatusBar } from './components';
import Loader from '@components1/common/Loader';
import { createWorkflowCreateRequest, createWorkflowUpdateRequest, prepareWorkflowForSave } from './utils/workflowApiUtils';
import { convertWorkflowToReactFlow } from './utils/workflowLayoutEngine';
import { validateWorkflowForSave, wouldCreateCycle } from './utils/workflowValidation';
import { parseDurationToSeconds } from './utils/taskUtils';
import { extractTriggersFromNodes, extractTasksFromWorkflowNodes, getPreviousNodesOutputSchemas } from './utils/workflowTaskExtraction';
import { disableTask, enableTask } from './utils/toggleTaskDisable';
import {
  cleanupSwitchReferencesAfterDelete,
  findExecutionTaskForNode,
  getPreviousTasksForNode,
  getSwitchDryRunEligibility,
} from './utils/templateUtils';
import { buildWorkflowFromAIResponse, type AIGenerateWorkflowResponse, sanitizeTaskId, buildFilterExpression } from './utils';
import { parseConditionLabel, hasConditionalStyling } from './utils/conditionParser';

// Custom imports
import { nodeCategories, generateNodeCategories } from './constants/nodeCategories';
import type { TaskDefinitionAPIResponse, NodeCategories, WorkflowStatus, WorkflowSettings } from './types';
import { ActionNode, TriggerNode, SwitchNode } from './nodes';
import DeletableEdge from './components/DeletableEdge';
import ConditionalEdge from './components/ConditionalEdge';
import { useExecutionData } from './hooks/useExecutionData';
import { useWorkflowInteractions } from './hooks/useWorkflowInteractions';
import { validateTaskData } from './hooks/useTaskValidation';
import apiWorkflow from '@api1/workflow';
import { toast as snackbar } from '@components1/ds/Toast';
import { parseHttpResponseBodyMessage } from 'src/utils/common';
import { Button } from '@components1/ds/Button';
import {
  manualTriggerIcon,
  SettingOutlineIconGrey,
  PlayIconBlue,
  workflowUserIcon,
  workflowWebhookIcon,
  workflowCalendarIcon,
  SaveIconOutline,
} from '@assets';
import { getNubiIconUrl, useTenantBranding } from '@hooks/useTenantBranding';
import { useUnsavedChangesTracking } from './hooks/useUnsavedChangesTracking';
import { useJsonEditorSync } from './hooks/useJsonEditorSync';
import { useWorkflowHistory } from './hooks/useWorkflowHistory';
import { useWorkflowClipboard } from './hooks/useWorkflowClipboard';
import { useWorkflowShortcuts } from './hooks/useWorkflowShortcuts';
import SafeIcon from '@components1/common/SafeIcon';
import { hasFeatureAccess, hasWriteAccess } from '@lib/auth';
import { Modal } from '@components1/ds/Modal';
import { Text } from '@components1/common';

// Lazy-loaded heavy components — only loaded when actually needed
const ExecutionsView = lazy(() => import('./ExecutionsView'));
const ActionDetailsSidebar = lazy(() => import('./ActionDetailsSidebar'));
const JsonEditorTab = lazy(() => import('./components/JsonEditorTab'));
const WorkflowSettingsModal = lazy(() => import('./components/WorkflowSettingsModal'));
const TriggerConfigSidebar = lazy(() => import('./components/TriggerConfigSidebar'));
const NubiChatSidebar = lazy(() => import('@components1/common/NubiChatSidebar'));
const TestResponseModal = lazy(() => import('./components/TestResponseModal'));
const DryRunResultModal = lazy(() => import('./components/DryRunResultModal'));
const TriggerWorkflowModal = lazy(() => import('./components/TriggerWorkflowModal'));

interface WorkflowData {
  id: string | null;
  name: string;
  status?: WorkflowStatus;
  definition: {
    version: string;
    timeout: string;
    inputs: any[];
    tasks: any[];
    triggers: Array<{ type: string; params: any; layout?: { x: number; y: number } }>;
    output: any;
    // set_execution_tags?: string[];
    retry_policy: {
      maximum_attempts: number;
      initial_interval: string;
      maximum_interval: string;
      backoff_coefficient: number;
    };
    layout?: { viewport?: { x: number; y: number; zoom: number } };
  };
  tags: Record<string, any>;
  created_from_session_id?: string | null;
  live_version_id?: string | null;
  live_version_number?: number | null;
  live_version_name?: string | null;
  live_version_status?: WorkflowStatus | null;
  // V747: workflow_versions row the current draft was branched off. Drives the
  // editor's lineage chip ("Draft based on vN") and the publish dialog hint.
  draft_version_id?: string | null;
  draft_version_number?: number | null;
  draft_version_name?: string | null;
}

/** Filter tasks to only those in the included set and clean up dangling depends_on references. */
const filterTasksForPartialRun = (tasks: any[], includedIds: Set<string>): any[] =>
  tasks
    .filter((t: any) => includedIds.has(t.id))
    .map((t: any) => {
      if (!t.depends_on || t.depends_on.length === 0) return t;
      const cleanedDeps = t.depends_on.filter((dep: string) => includedIds.has(dep));
      return { ...t, depends_on: cleanedDeps.length > 0 ? cleanedDeps : undefined };
    });

function getEdgeStrokeColor(isSwitchEdge: boolean, isSwitchDefault: boolean, hasCondition: boolean, parsedCondition: any): string {
  if (isSwitchEdge) {
    return isSwitchDefault ? '#9CA3AF' : '#8B5CF6';
  }
  if (hasCondition && parsedCondition) {
    return parsedCondition.color;
  }
  return 'rgb(192, 192, 192)';
}

function rejectConnection(sourceNodeId: string, targetNodeId: string, setNodes: any) {
  const markRejection = (rejected: boolean) => (nodesSnapshot: any[]) =>
    nodesSnapshot.map((node) =>
      node.id === targetNodeId || node.id === sourceNodeId ? { ...node, data: { ...node.data, connectionRejected: rejected } } : node
    );

  setNodes(markRejection(true));
  setTimeout(() => setNodes(markRejection(false)), 800);
}

/** Calculate horizontal offset for nodes created from switch handles so they spread out. */
function getSwitchHandleXOffset(sourceNode: any, sourceHandleId: string | undefined): number {
  if (sourceNode.type !== 'switch' || !sourceHandleId?.startsWith('switch-')) return 0;

  const cases: Array<{ value: string }> = sourceNode.data?.taskConfig?.config?.cases || [];
  const validCases = cases.filter((c: { value: string }) => c.value);
  const totalHandles = validCases.length + 1; // +1 for default
  let handleIndex = totalHandles - 1; // default is last

  if (sourceHandleId !== 'switch-default') {
    const caseValue = sourceHandleId.replace('switch-case-', '');
    const found = validCases.findIndex((c: { value: string }) => c.value === caseValue);
    handleIndex = found === -1 ? 0 : found;
  }

  const centerIndex = (totalHandles - 1) / 2;
  return (handleIndex - centerIndex) * 350;
}

function buildNewEdge(connection: any, nodes: any[]) {
  const isSwitchEdge = connection.sourceHandle?.startsWith('switch-');
  const isSwitchDefault = connection.sourceHandle === 'switch-default';

  const targetNode = nodes.find((n: any) => n.id === connection.target);
  const targetCondition = targetNode?.data?.taskConfig?.if;
  const hasCondition = !isSwitchEdge && hasConditionalStyling(targetCondition);
  const parsedCondition = hasCondition ? parseConditionLabel(targetCondition) : null;

  return {
    id: `edge_${connection.source}_${connection.target}_${Date.now()}`,
    source: connection.source,
    target: connection.target,
    sourceHandle: connection.sourceHandle,
    targetHandle: connection.targetHandle,
    type: hasCondition ? 'conditional' : 'smoothstep',
    ...(isSwitchEdge ? { label: isSwitchDefault ? 'default' : connection.sourceHandle?.replace('switch-case-', '') } : {}),
    ...(hasCondition && parsedCondition
      ? {
          data: {
            ...parsedCondition,
            condition: targetCondition,
          },
        }
      : {}),
    style: {
      strokeWidth: isSwitchEdge || hasCondition ? 2 : 1,
      stroke: getEdgeStrokeColor(isSwitchEdge, isSwitchDefault, hasCondition, parsedCondition),
      ...(isSwitchDefault ? { strokeDasharray: '5 5' } : {}),
    },
  };
}

type WorkflowBuilderMode = 'editor' | 'json' | 'executions';

interface WorkflowBuilderNotebookProps {
  mode?: 'create' | 'edit';
}

const WorkflowBuilderNoteBook: React.FC<WorkflowBuilderNotebookProps> = ({ mode }) => {
  const router = useRouter();
  const { assistantName } = useTenantBranding();
  const [currentMode, setCurrentMode] = useState<WorkflowBuilderMode>('editor');
  const [jsonPanelVisible, setJsonPanelVisible] = useState<boolean>(false); // Control JSON panel visibility

  const [jsonWindowWidth, setJsonWindowWidth] = useState(400);
  const [nubiChatWindowWidth, setNubiChatWindowWidth] = useState(500);

  // Refs for drag resize
  const isResizingRef = useRef<'nubi' | 'json' | null>(null);
  const startXRef = useRef(0);
  const startWidthRef = useRef(0);

  const handleResizeMouseDown = useCallback(
    (panel: 'nubi' | 'json') => (e: React.MouseEvent) => {
      e.preventDefault();
      e.stopPropagation();
      isResizingRef.current = panel;
      startXRef.current = e.clientX;
      startWidthRef.current = panel === 'nubi' ? nubiChatWindowWidth : jsonWindowWidth;

      const handleMouseMove = (moveEvent: MouseEvent) => {
        if (!isResizingRef.current) return;
        const delta = moveEvent.clientX - startXRef.current;
        const newWidth = isResizingRef.current === 'nubi' ? startWidthRef.current + delta : startWidthRef.current - delta;
        const clampedWidth = Math.max(300, Math.min(800, newWidth));
        if (isResizingRef.current === 'nubi') {
          setNubiChatWindowWidth(clampedWidth);
        } else {
          setJsonWindowWidth(clampedWidth);
        }
      };

      const handleMouseUp = () => {
        isResizingRef.current = null;
        document.removeEventListener('mousemove', handleMouseMove);
        document.removeEventListener('mouseup', handleMouseUp);
        document.body.style.cursor = '';
        document.body.style.userSelect = '';
        // Recenter workflow after drag resize
        reactFlowInstanceRef.current?.fitView({
          padding: 0.15,
          maxZoom: 0.9,
          minZoom: 0.75,
          duration: 300,
        });
      };

      document.body.style.cursor = 'col-resize';
      document.body.style.userSelect = 'none';
      document.addEventListener('mousemove', handleMouseMove);
      document.addEventListener('mouseup', handleMouseUp);
    },
    [nubiChatWindowWidth, jsonWindowWidth]
  );

  // Get workflow ID from URL parameters
  const workflowId = router.query.workflowId as string;
  const accountId = router.query.accountId as string;

  // State flag to track workflow creation transition
  const [isTransitioningFromCreateToEdit, setIsTransitioningFromCreateToEdit] = useState(false);

  // Determine if this is a new workflow, accounting for transition state
  const isNewWorkflow = (workflowId === 'new' || mode === 'create') && !isTransitioningFromCreateToEdit;

  // Read-only users cannot mutate workflows — gate all mutation surfaces below.
  const canEdit = hasWriteAccess(accountId);

  // Workflow-specific state
  const [workflowData, setWorkflowData] = useState<WorkflowData | null>(null);
  const workflowDataRef = useRef(workflowData);
  useEffect(() => {
    workflowDataRef.current = workflowData;
  }, [workflowData]);
  const [loading, setLoading] = useState<boolean>(true);
  // Tracks whether the workflow has been fully loaded and state is ready for unsaved changes tracking
  const [isInitialized, setIsInitialized] = useState<boolean>(false);

  const [workflowSettings, setWorkflowSettings] = useState<WorkflowSettings>({
    timeout: '5m',
    maxInterval: '1m',
    retries: 3,
    inputs: [],
    outputs: {},
    tags: [],
    status: 'ACTIVE',
  });
  const workflowSettingsRef = useRef(workflowSettings);
  useEffect(() => {
    workflowSettingsRef.current = workflowSettings;
  }, [workflowSettings]);

  // Trigger modal state for Run/DryRun input collection
  const [triggerModalOpen, setTriggerModalOpen] = useState(false);
  const [triggerModalMode, setTriggerModalMode] = useState<'run' | 'dryrun'>('run');
  const [triggerModalLoading, setTriggerModalLoading] = useState(false);
  // Which kind of run was requested:
  //   'current' — snapshot the on-screen draft (use_draft_definition=true).
  //              User stays on the editor.
  //   'live'    — trigger the published live version. After success, navigate
  //              to the Executions tab and auto-select the new execution so
  //              the user sees what's actually running.
  const [runVariant, setRunVariant] = useState<'current' | 'live'>('current');
  // Anchor for the Run-button split menu (chevron next to the primary button).
  const [runMenuAnchor, setRunMenuAnchor] = useState<HTMLElement | null>(null);
  // Execution ID we want ExecutionsView to auto-select on its next refresh.
  // Pushed by `executeRun` after a `live` trigger; consumed by ExecutionsView's
  // `pendingSelectionRef` path (also used by the Retry flow).
  const [pendingExecutionId, setPendingExecutionId] = useState<string | undefined>(undefined);

  // DEPRECATED: Replaced with aiWorkflowInitializedRef (see line 162)
  // const [aiWorkflowInitialized, setAiWorkflowInitialized] = useState<boolean>(false);

  // Update workflow settings from loaded workflow data
  useEffect(() => {
    if (workflowData?.definition) {
      setWorkflowSettings((prev) => {
        return {
          timeout: workflowData.definition.timeout ?? prev.timeout,
          maxInterval: workflowData.definition.retry_policy?.maximum_interval ?? prev.maxInterval,
          retries: workflowData.definition.retry_policy?.maximum_attempts ?? prev.retries,
          inputs: workflowData.definition.inputs ?? prev.inputs,
          outputs: workflowData.definition.output ?? prev.outputs,
          tags: Object.entries(workflowData.tags || {}).map(([key, value]) => (value ? `${key}:${value}` : key)),
          status: workflowData.status ?? prev.status,
        };
      });

      // Extract ai_session_id from tags for existing AI-generated workflows
      const aiTag = workflowData.tags?.ai_session_id;
      if (aiTag && !aiSessionId) {
        setAiSessionId(aiTag);
      }
    }
  }, [workflowData]);

  // Initialize currentMode from URL hash fragment (e.g. #editor, #executions)
  useEffect(() => {
    const hash = router.asPath.split('#')[1];
    if (hash && (hash === 'editor' || hash === 'json' || hash === 'executions')) {
      // Read-only users have no editor access — coerce any editor/json hash to executions.
      if (!canEdit && hash !== 'executions') {
        setCurrentMode('executions');
        return;
      }
      setCurrentMode(hash);
    } else if (!canEdit) {
      setCurrentMode('executions');
    }
  }, [router.asPath, canEdit]);

  // Removed actionsList state as it's no longer needed for workflow tasks
  const [nodes, setNodes] = useState<Node[]>([]);
  const nodesRef = useRef(nodes);
  useEffect(() => {
    nodesRef.current = nodes;
  }, [nodes]);
  const [edges, setEdges] = useState<Edge[]>([]);
  const [autoFitViewport, setAutoFitViewport] = useState<{ x: number; y: number; zoom: number } | null>(null);
  const [persistedViewport, setPersistedViewport] = useState<{ x: number; y: number; zoom: number } | null>(null);

  // ReactFlow instance ref for manual fitView control
  const reactFlowInstanceRef = useRef<ReactFlowInstance | null>(null);
  const hasInitiallyFit = useRef(false);

  // Dynamic categories state
  const [dynamicCategories, setDynamicCategories] = useState<NodeCategories>(nodeCategories);
  const [taskDefinitions, setTaskDefinitions] = useState<any[]>([]);

  // Template state
  const templateInitializedRef = useRef(false);

  // Event-context prefill state (from Events page → Create Automation)
  const eventTriggerInitializedRef = useRef(false);

  // AI Conversation stateƒ
  const [aiSessionId, setAiSessionId] = useState<string>('');
  // URL-driven conversation loading: when the route includes
  // `?conversation_id=<uuid>` (or `?session_id=<uuid>`), surface that
  // conversation in the NuBi sidebar so users can revisit the prompts that
  // produced this automation (#29511). The query is normalised to strings
  // here so downstream effects don't have to keep handling string|string[].
  const urlConversationId = useMemo(() => {
    const v = router.query.conversation_id;
    return Array.isArray(v) ? v[0] : v || '';
  }, [router.query.conversation_id]);
  const urlSessionId = useMemo(() => {
    const v = router.query.session_id;
    return Array.isArray(v) ? v[0] : v || '';
  }, [router.query.session_id]);
  // URL ids win; fall back to the workflow's originating chat session
  // (workflows.created_from_session_id) so opening an AI-built workflow
  // reloads the conversation that produced it.
  const workflowSessionId: string = workflowData?.created_from_session_id || '';
  const effectiveSessionId = urlSessionId || (urlConversationId ? '' : workflowSessionId);
  // Tracks the URL ids we last auto-opened the NuBi sidebar for, so manual
  // closes are respected on subsequent re-renders within the same route.
  const autoOpenedNubiUrlRef = useRef<string>('');

  // NuBi Chat Sidebar state
  const [showNubiChat, setShowNubiChat] = useState(false); // Overlay mode - hidden by default
  const [nubiChatFeatureEnabled, setNubiChatFeatureEnabled] = useState(false); // Feature flag for NubiChat
  const [nubiChatContext, setNubiChatContext] = useState<{
    type: 'workflow' | 'workflowbuilder';
    data?: any;
  }>({
    type: 'workflow',
    data: {},
  });
  const [nubiChatSuffix] = useState<string>('');
  const [configWarningDismissed, setConfigWarningDismissed] = useState(false);

  // Version history (revert) state. Lives in this component so the drawer and
  // confirm dialog can share data without prop drilling.
  const [historyOpen, setHistoryOpen] = useState(false);
  const [versions, setVersions] = useState<WorkflowVersionEntry[]>([]);
  const [loadingVersions, setLoadingVersions] = useState(false);
  const [confirmRestoreVersion, setConfirmRestoreVersion] = useState<WorkflowVersionEntry | null>(null);
  const [restoring, setRestoring] = useState(false);
  // Publish + live-pointer state. Save = draft only; Publish = snapshot draft
  // into a new immutable version row. Live = the version Execute runs.
  const [publishDialogOpen, setPublishDialogOpen] = useState(false);
  const [publishName, setPublishName] = useState('');
  const [publishDescription, setPublishDescription] = useState('');
  const [publishSetLive, setPublishSetLive] = useState(true);
  // Initial status the published version (and the workflow row, when
  // setLive=true) lands in. Default PAUSED matches the backend default so the
  // user must opt in to ACTIVE — V746 contract. The version-history drawer's
  // per-row dropdown is the long-term knob, but offering the initial value
  // here saves a round-trip when the user already knows what they want.
  const [publishStatus, setPublishStatus] = useState<'ACTIVE' | 'PAUSED' | 'INACTIVE'>('PAUSED');
  const [publishing, setPublishing] = useState(false);
  const [confirmLiveVersion, setConfirmLiveVersion] = useState<WorkflowVersionEntry | null>(null);
  const [settingLive, setSettingLive] = useState(false);

  // Keep nubiChatContext in sync with workflowData (id + definition)
  // so that NuBi chat always sends the current workflow context to the backend
  useEffect(() => {
    if (!workflowData) {
      return;
    }
    setNubiChatContext((prev) => {
      const newId = workflowData.id || undefined;
      const newDef = workflowData.definition || undefined;
      if (prev.data?.id === newId && prev.data?.definition === newDef) {
        return prev;
      }
      return {
        ...prev,
        data: { ...prev.data, id: newId, definition: newDef },
      };
    });
  }, [workflowData?.id, workflowData?.definition]);

  // Detect config placeholders in AI-generated workflows
  const configPlaceholders = useMemo(() => {
    if (!workflowData?.definition?.tasks || !aiSessionId) {
      return [];
    }
    const placeholders = new Set<string>();
    const configRegex = /\{\{\s*Configs[.[']+(\w+)/g;
    const scanValue = (val: unknown) => {
      if (typeof val === 'string') {
        let match;
        while ((match = configRegex.exec(val)) !== null) {
          placeholders.add(match[1]);
        }
      } else if (typeof val === 'object' && val !== null) {
        Object.values(val).forEach(scanValue);
      }
    };
    workflowData.definition.tasks.forEach((task: any) => {
      if (task.params) {
        scanValue(task.params);
      }
    });
    return Array.from(placeholders);
  }, [workflowData?.definition?.tasks, aiSessionId]);

  // Check NubiChat feature flag on mount
  useEffect(() => {
    const checkNubiChatFeatureAccess = async () => {
      try {
        const hasAccess = await hasFeatureAccess('WORKFLOWS');
        setNubiChatFeatureEnabled(hasAccess);
      } catch (error) {
        console.error('Error checking NubiChat feature access:', error);
        setNubiChatFeatureEnabled(false);
      }
    };

    checkNubiChatFeatureAccess();
  }, []);

  // Auto-open the NuBi sidebar when the route carries a conversation_id (or
  // session_id) query param so the linked conversation is rendered without a
  // manual toggle. The actual fetch is owned by KubernetesLLMResponseGenerator
  // — we only flip the visibility flag and forward the id through context.
  // Keyed on the URL id so closing the sidebar does not get fought by every
  // re-render; navigating to a different conversation re-triggers the open.
  useEffect(() => {
    if (!nubiChatFeatureEnabled) return;
    const urlId = urlConversationId || urlSessionId || workflowSessionId;
    if (!urlId) return;
    if (autoOpenedNubiUrlRef.current === urlId) return;
    autoOpenedNubiUrlRef.current = urlId;
    setShowNubiChat(true);
  }, [urlConversationId, urlSessionId, workflowSessionId, nubiChatFeatureEnabled]);

  // Adjust viewport when JSON panel or NubiChat visibility changes
  useEffect(() => {
    if (reactFlowInstanceRef.current && nodes.length > 0 && hasInitiallyFit.current) {
      const timer = setTimeout(() => {
        // Always recenter workflow when sidebars change to keep it centered
        const rfInstance = reactFlowInstanceRef.current;
        if (rfInstance) {
          // Always call fitView to recenter the workflow when sidebars open/close
          // This ensures the workflow stays centered regardless of which sidebars are visible
          rfInstance.fitView({
            padding: 0.15,
            maxZoom: 0.9,
            minZoom: 0.75,
            duration: 300,
          });
        }
      }, 350); // Wait for CSS transitions to complete (300ms + buffer)

      return () => clearTimeout(timer);
    }
  }, [jsonPanelVisible, showNubiChat]); // Only react to sidebar changes, not node changes

  // Apply autoFitViewport when it changes (from JSON apply, workflow load, or AI generation)
  useEffect(() => {
    if (autoFitViewport && reactFlowInstanceRef.current && nodes.length > 0) {
      const timer = setTimeout(() => {
        // Use fitView to center all nodes properly
        // Must wait for all sidebar transitions and canvas resize to complete
        reactFlowInstanceRef.current?.fitView({
          padding: 0.4,
          maxZoom: 0.9,
          minZoom: 0.75,
          duration: 400,
        });
        // Clear the viewport after applying to prevent re-application
        setAutoFitViewport(null);
        hasInitiallyFit.current = true;
      }, 500); // Wait longer for all CSS transitions (300ms) + rendering + sidebar states

      return () => clearTimeout(timer);
    }
  }, [autoFitViewport, nodes.length]);

  // Restore persisted viewport (from workflow definition.layout.viewport) without re-fitting.
  useEffect(() => {
    if (persistedViewport && reactFlowInstanceRef.current && nodes.length > 0) {
      const timer = setTimeout(() => {
        reactFlowInstanceRef.current?.setViewport(persistedViewport, { duration: 0 });
        setPersistedViewport(null);
        hasInitiallyFit.current = true;
      }, 500);

      return () => clearTimeout(timer);
    }
  }, [persistedViewport, nodes.length]);

  // Use ref instead of state to track AI workflow initialization (synchronous, no re-renders)
  const aiWorkflowInitializedRef = useRef(false);
  // Guards the plain empty-workflow init from re-firing when `taskDefinitions` resolves
  // and the init effect re-runs — without it, setNodes([]) wipes a trigger the user already
  // placed via the inline empty-state card. See #30510.
  const newEmptyWorkflowInitializedRef = useRef(false);

  // JSON Editor sync hook - handles all JSON editor state and synchronization
  const {
    jsonEditorText,
    jsonValid,
    jsonParseError,
    jsonHasUnsavedChanges,
    isApplyingJson,
    lastAppliedSource,
    jsonBeforeLlmApply,
    handleJsonChangeWithRevertClear,
    applyJsonToWorkflow,
    revertLastLlmApply,
  } = useJsonEditorSync({
    nodes,
    edges,
    workflowSettings,
    workflowData,
    taskDefinitions,
    currentMode,
    loading,
    setNodes,
    setEdges,
    setWorkflowData,
    setWorkflowSettings,
    setAutoFitViewport,
    setCurrentMode,
    setJsonPanelVisible,
  });

  // Load task definitions on mount
  useEffect(() => {
    const loadTaskDefinitions = async () => {
      try {
        const response = (await apiWorkflow.listTaskDefinitions()) as TaskDefinitionAPIResponse;

        if (response?.data?.workflow_list_taskdefinitions?.tasks) {
          const tasks = response.data.workflow_list_taskdefinitions.tasks;

          // Patch task definitions to ensure Python support in scripting tasks
          tasks.forEach((task) => {
            if (task.name === 'scripting.run_script' && task.input_schema?.language?.enum) {
              const languages = task.input_schema.language.enum;
              if (!languages.includes('python')) {
                languages.push('python');
              }
            }
          });

          // Store task definitions for schema viewer
          setTaskDefinitions(tasks);

          // Generate dynamic categories from task definitions
          const categories = generateNodeCategories(tasks);
          setDynamicCategories(categories);
        }
      } catch (error) {
        console.error('Failed to load task definitions:', error);
        // Keep default static categories on error
      }
    };

    loadTaskDefinitions();
  }, []);
  // Load workflow on mount or when IDs change
  useEffect(() => {
    // Skip loading if we're in transition state to prevent conflicts
    if (isTransitioningFromCreateToEdit) {
      return;
    }

    if (isNewWorkflow) {
      // Skip if AI workflow already initialized (prevent re-initialization when taskDefinitions loads)
      if (aiWorkflowInitializedRef.current) {
        setLoading(false);
        return;
      }

      setLoading(true);

      // Check if this is an AI-generated workflow
      const aiGeneratedParam = router.query.aiGenerated as string;
      const loadFromAI = router.query.loadFromAI === 'true';

      // Handle AI-generated workflows (both URL param and sessionStorage methods)
      if ((aiGeneratedParam || loadFromAI) && taskDefinitions.length > 0) {
        try {
          let workflowJson: string | null = null;

          // Method 1: sessionStorage (preferred - no URL length limit)
          if (loadFromAI) {
            workflowJson = sessionStorage.getItem('aiGeneratedWorkflow');

            if (!workflowJson) {
              console.warn('No AI workflow data found in sessionStorage - it may have been already loaded. Creating empty workflow.');
              // Don't throw error - just fall through to create empty workflow
              // This happens when user refreshes the page after loading AI workflow
              throw new Error('ALREADY_LOADED');
            }
          }
          // Method 2: URL parameter (backward compatibility)
          else if (aiGeneratedParam) {
            workflowJson = decodeURIComponent(aiGeneratedParam);
          }

          if (!workflowJson) {
            console.warn('No workflow data available');
            throw new Error('NO_DATA');
          }

          // Build the response structure expected by loadWorkflowFromAI
          // Use session_id for NuBi chat — NubiChatSidebar passes this as sessionId to
          // KubernetesLLMResponseGeneratorV2, which queries the DB via session_id field.
          // Using conversation_id (DB id column) here would cause the lookup to fail.
          const sessionId = sessionStorage.getItem('aiSessionId') || '';
          const initialQuery = sessionStorage.getItem('aiInitialQuery') || 'Generate workflow';

          const aiResponse: AIGenerateWorkflowResponse = {
            data: {
              ai_generate_workflow: {
                data: {
                  response: [workflowJson],
                  query: initialQuery,
                  chain_name: 'workflow_builder',
                  conversation_id: sessionId,
                  session_id: sessionId,
                  message_id: '',
                  agent_id: '',
                  status: 'COMPLETED',
                },
              },
            },
          };

          // Store session_id for chat continuation
          setAiSessionId(sessionId);

          // Mark AI workflow as initialized BEFORE loading to prevent re-initialization (using ref = synchronous)
          aiWorkflowInitializedRef.current = true;

          // Use the loadWorkflowFromAI function to process and load the workflow
          // Pass sessionId directly to avoid stale closure (setAiSessionId is async)
          loadWorkflowFromAI(aiResponse, sessionId);

          // Clean up sessionStorage after successful load
          if (loadFromAI) {
            sessionStorage.removeItem('aiGeneratedWorkflow');
            sessionStorage.removeItem('aiInitialQuery');
            // Keep conversation context for chat continuation
            // Will be cleared when component unmounts or user starts new workflow

            // Remove loadFromAI from URL to prevent re-load on refresh
            const newQuery = { ...router.query };
            delete newQuery.loadFromAI;
            router.replace(
              {
                pathname: router.pathname,
                query: newQuery,
              },
              undefined,
              { shallow: true }
            );
          }

          setLoading(false);
          setIsInitialized(true);
          return;
        } catch (error) {
          const errorMessage = error instanceof Error ? error.message : 'Unknown error';

          // Handle expected errors gracefully
          if (errorMessage === 'ALREADY_LOADED' || errorMessage === 'NO_DATA') {
            console.warn('AI workflow data not available, falling through to create empty workflow');
            // Don't show error to user - this is expected when refreshing page
          } else {
            console.error('Failed to load AI-generated workflow:', error);
            snackbar.error('Failed to load AI-generated automation. Creating empty automation instead.');
          }

          // Clean up failed sessionStorage data
          sessionStorage.removeItem('aiGeneratedWorkflow');

          // Remove loadFromAI from URL
          if (loadFromAI) {
            const newQuery = { ...router.query };
            delete newQuery.loadFromAI;
            router.replace(
              {
                pathname: router.pathname,
                query: newQuery,
              },
              undefined,
              { shallow: true }
            );
          }

          // Fall through to create empty workflow
        }
      }

      // Handle template-loaded workflows (sessionStorage method, similar to AI pattern)
      const loadFromTemplate = router.query.loadFromTemplate === 'true';
      if (loadFromTemplate && taskDefinitions.length > 0 && !templateInitializedRef.current) {
        try {
          const templateJson = sessionStorage.getItem('templateWorkflow');
          if (templateJson) {
            const templateData = JSON.parse(templateJson);
            const definition = templateData.definition || {};

            // Build workflow data with null id (new workflow)
            const templateWorkflowData: WorkflowData = {
              id: null,
              name: templateData.name || 'New Automation',
              definition: {
                version: definition.version || 'v1',
                timeout: definition.timeout || '',
                inputs: definition.inputs || [],
                tasks: definition.tasks || [],
                triggers: definition.triggers || [{ type: 'manual', params: {} }],
                output: definition.output || {},
                retry_policy: definition.retry_policy || {
                  maximum_attempts: 3,
                  initial_interval: '1s',
                  maximum_interval: '',
                  backoff_coefficient: 2.0,
                },
              },
              tags: templateData.tags || {},
            };

            templateInitializedRef.current = true;
            setWorkflowData(templateWorkflowData);

            // Convert to ReactFlow nodes/edges
            const {
              nodes: templateNodes,
              edges: templateEdges,
              viewport,
            } = convertWorkflowToReactFlow(
              templateWorkflowData.definition,
              {
                minHorizontalSpacing: 250,
                minVerticalSpacing: 180,
                minTriggerSpacing: 250,
                minConditionalSpacing: 120,
              },
              taskDefinitions
            );
            setNodes(templateNodes);
            setEdges(templateEdges);
            const templatePersistedViewport = templateWorkflowData.definition.layout?.viewport;
            if (templatePersistedViewport) {
              setPersistedViewport(templatePersistedViewport);
            } else if (viewport) {
              setAutoFitViewport(viewport);
            }

            // Run per-task validation to flag nodes with missing required fields
            const invalidTaskNames: string[] = [];
            templateNodes.forEach((node) => {
              if (node.type === 'action' && node.data?.taskConfig && !node.data.taskConfig.valid) {
                invalidTaskNames.push(node.data.taskConfig.id || node.data.label || node.data.taskConfig.type);
              }
            });
            if (invalidTaskNames.length > 0) {
              snackbar.error(`${invalidTaskNames.length} task(s) have missing required fields: ${invalidTaskNames.join(', ')}`);
            }

            // Clean up sessionStorage
            sessionStorage.removeItem('templateWorkflow');

            // Remove loadFromTemplate from URL
            const newQuery = { ...router.query };
            delete newQuery.loadFromTemplate;
            router.replace({ pathname: router.pathname, query: newQuery }, undefined, { shallow: true });

            snackbar.success(`Template "${templateData.name}" loaded successfully`);
            setLoading(false);
            setIsInitialized(true);
            return;
          }
        } catch (error) {
          console.error('Failed to load template workflow:', error);
          snackbar.error('Failed to load template. Creating empty automation instead.');
          sessionStorage.removeItem('templateWorkflow');
        }
      }

      // Event-context prefill: when navigating from an event row's "Create Automation",
      // pre-populate an Event Trigger with the row's filters so the user doesn't re-type them.
      const eventTypeQ = typeof router.query.eventType === 'string' ? router.query.eventType : '';
      const eventPriorityQ = typeof router.query.eventPriority === 'string' ? router.query.eventPriority : '';
      const eventSourceQ = typeof router.query.eventSource === 'string' ? router.query.eventSource : '';
      const eventClusterQ = typeof router.query.eventCluster === 'string' ? router.query.eventCluster : '';
      const eventNamespaceQ = typeof router.query.eventNamespace === 'string' ? router.query.eventNamespace : '';
      const hasEventContext = !!(eventTypeQ || eventPriorityQ || eventSourceQ || eventClusterQ || eventNamespaceQ);

      if (hasEventContext && taskDefinitions.length > 0 && !eventTriggerInitializedRef.current) {
        try {
          const filter = buildFilterExpression({
            event_type: eventTypeQ,
            priority: eventPriorityQ,
            source: eventSourceQ,
            cluster: eventClusterQ,
            namespace: eventNamespaceQ,
          });

          const eventWorkflowData: WorkflowData = {
            id: null,
            name: 'New Automation',
            definition: {
              version: 'v1',
              timeout: '',
              inputs: [],
              tasks: [],
              triggers: [{ type: 'event', params: { filter } }],
              output: {},
              retry_policy: {
                maximum_attempts: 3,
                initial_interval: '1s',
                maximum_interval: '',
                backoff_coefficient: 2.0,
              },
            },
            tags: {},
          };

          eventTriggerInitializedRef.current = true;
          setWorkflowData(eventWorkflowData);

          const {
            nodes: eventNodes,
            edges: eventEdges,
            viewport,
          } = convertWorkflowToReactFlow(
            eventWorkflowData.definition,
            {
              minHorizontalSpacing: 250,
              minVerticalSpacing: 180,
              minTriggerSpacing: 250,
              minConditionalSpacing: 120,
            },
            taskDefinitions
          );
          setNodes(eventNodes);
          setEdges(eventEdges);
          if (viewport) setAutoFitViewport(viewport);

          // Auto-open the trigger config sidebar on the new event-trigger node so
          // the user immediately sees the prefilled filters.
          const triggerNode = eventNodes.find((n: any) => n.type === 'trigger');
          if (triggerNode) {
            setSelectedNode(triggerNode);
            setTriggerConfigSidebarOpen(true);
          }

          // Strip event-context params from URL so refresh after edit doesn't re-init.
          const newQuery = { ...router.query };
          delete newQuery.eventType;
          delete newQuery.eventPriority;
          delete newQuery.eventSource;
          delete newQuery.eventCluster;
          delete newQuery.eventNamespace;
          router.replace({ pathname: router.pathname, query: newQuery }, undefined, { shallow: true });

          setLoading(false);
          setIsInitialized(true);
          return;
        } catch (error) {
          console.error('Failed to prefill event trigger:', error);
          // Fall through to empty workflow on failure
        }
      }

      // If taskDefinitions not loaded yet and we have an event-context prefill, wait
      if (hasEventContext && taskDefinitions.length === 0) {
        setLoading(false);
        return;
      }

      // If taskDefinitions not loaded yet and we have template parameter, wait
      if (loadFromTemplate && taskDefinitions.length === 0) {
        setLoading(false);
        return;
      }

      // If taskDefinitions not loaded yet and we have AI parameter, wait
      if ((aiGeneratedParam || loadFromAI) && taskDefinitions.length === 0) {
        setLoading(false);
        return;
      }

      // Don't create empty workflow if AI workflow, template, or event-trigger prefill was initialized,
      // or if the plain empty workflow was already initialized this mount (prevents re-firing after
      // taskDefinitions loads and wiping a trigger node the user already placed — #30510).
      if (
        aiWorkflowInitializedRef.current ||
        templateInitializedRef.current ||
        eventTriggerInitializedRef.current ||
        newEmptyWorkflowInitializedRef.current
      ) {
        setLoading(false);
        return;
      }

      // Create empty workflow (default behavior)
      const newWorkflowData: WorkflowData = {
        id: null,
        name: 'New Automation',
        definition: {
          version: 'v1',
          timeout: '',
          inputs: [],
          tasks: [],
          triggers: [{ type: 'manual', params: {} }],
          output: {},
          retry_policy: {
            maximum_attempts: 3,
            initial_interval: '1s',
            maximum_interval: '',
            backoff_coefficient: 2.0,
          },
        },
        tags: {},
      };

      setWorkflowData(newWorkflowData);
      setNodes([]);
      setEdges([]);
      setLoading(false);
      setIsInitialized(true);
      newEmptyWorkflowInitializedRef.current = true;
    } else if (workflowId && accountId) {
      // Skip redundant reload for existing workflows that are already initialized
      // (taskDefinitions.length change triggers this effect but existing workflows don't need reloading)
      if (isInitialized && workflowData?.id) {
        return;
      }

      const loadData = async () => {
        setLoading(true);
        try {
          const response: any = await apiWorkflow.getWorkflowById(accountId, workflowId);

          const errorMessage = parseHttpResponseBodyMessage(response);
          if (errorMessage) {
            snackbar.error(errorMessage);
            console.error('Workflow load error:', response.errors);
            return;
          }

          const workflow = response.data?.workflow_get;
          if (workflow) {
            setWorkflowData(workflow);
            // Fresh load — assume the saved draft is in sync with live until the
            // user edits. (Cross-session stale drafts aren't detectable here.)
            setDraftAheadOfLive(false);

            let taskCount = 0;
            let triggerCount = 0;

            // Convert workflow definition to nodes/edges format
            if (workflow.definition) {
              const {
                nodes,
                edges: convertedEdges,
                viewport,
              } = convertWorkflowToReactFlow(
                workflow.definition,
                {
                  minHorizontalSpacing: 250,
                  minVerticalSpacing: 180,
                  minTriggerSpacing: 250,
                  minConditionalSpacing: 120,
                },
                taskDefinitions
              );
              setNodes(nodes);
              setEdges(convertedEdges);
              const loadedPersistedViewport = workflow.definition?.layout?.viewport;
              if (loadedPersistedViewport) {
                setPersistedViewport(loadedPersistedViewport);
                setAutoFitViewport(null);
              } else {
                setAutoFitViewport(viewport || null);
              }

              // Calculate task and trigger counts
              taskCount = nodes.filter((n: any) => n.type === 'action').length;
              triggerCount = nodes.filter((n: any) => n.type === 'trigger').length;
            }

            // Open NubiChat with full workflow context for existing workflows

            // Generate initial message with workflow details
            let workflowJson = '';
            try {
              const workflowObject: any = {
                id: workflow.id,
                name: workflow.name,
                definition: workflow.definition,
                tags: workflow.tags,
              };
              if (workflow.status) {
                workflowObject.status = workflow.status;
              }
              workflowJson = JSON.stringify(workflowObject, null, 2);
            } catch (jsonError) {
              console.error('Failed to create workflow JSON:', jsonError);
            }

            setNubiChatContext({
              type: 'workflow',
              data: {
                name: workflow.name,
                id: workflow.id,
                status: workflow.status,
                taskCount,
                triggerCount,
                definition: workflow.definition,
                workflowJson,
                conversationId: workflow.tags?.ai_session_id || aiSessionId,
              },
            });
            setIsInitialized(true);
          } else {
            snackbar.error('Failed to load automation');
            router.push('/auto-pilot?accountId=' + accountId + '#workflow');

            setLoading(false);
          }
        } catch (error) {
          console.error('Error loading workflow:', error);
          snackbar.error('Failed to load automation');
          router.push('/auto-pilot?accountId=' + accountId + '#workflow');
        } finally {
          setLoading(false);
        }
      };
      loadData();
    }
  }, [
    isNewWorkflow,
    workflowId,
    accountId,
    isTransitioningFromCreateToEdit,
    router.query.aiGenerated,
    router.query.loadFromAI,
    router.query.loadFromTemplate,
    router.query.eventType,
    router.query.eventPriority,
    router.query.eventSource,
    router.query.eventCluster,
    router.query.eventNamespace,
    taskDefinitions.length,
    // Note: aiWorkflowInitializedRef, templateInitializedRef, eventTriggerInitializedRef are refs, not in dependencies
  ]);

  // Reset AI/template/event-trigger workflow flags when navigating to a new bare workflow
  useEffect(() => {
    if (
      isNewWorkflow &&
      !router.query.loadFromAI &&
      !router.query.aiGenerated &&
      !router.query.loadFromTemplate &&
      !router.query.eventType &&
      !router.query.eventPriority &&
      !router.query.eventSource &&
      !router.query.eventCluster &&
      !router.query.eventNamespace
    ) {
      aiWorkflowInitializedRef.current = false;
      templateInitializedRef.current = false;
      eventTriggerInitializedRef.current = false;
      newEmptyWorkflowInitializedRef.current = false;
    }
  }, [
    isNewWorkflow,
    router.query.loadFromAI,
    router.query.aiGenerated,
    router.query.loadFromTemplate,
    router.query.eventType,
    router.query.eventPriority,
    router.query.eventSource,
    router.query.eventCluster,
    router.query.eventNamespace,
  ]);

  // Standard React Flow event handlers using built-in utilities.
  // Remove changes (keyboard delete, selection delete) must also scrub any switch-case
  // `next` / `default_next` refs to the removed task ids — the custom per-node delete buttons
  // (ActionNode/SwitchNode) do the same cleanup via their own handlers.
  const onNodesChange = useCallback(
    (changes: any) => {
      const removeChanges = Array.isArray(changes) ? changes.filter((c: any) => c?.type === 'remove' && c?.id) : [];
      setNodes((nds: Node[]) => {
        const applied = applyNodeChanges(changes, nds);
        if (removeChanges.length === 0) return applied;
        const deletedTaskIds = new Set<string>();
        removeChanges.forEach((c: any) => {
          const removed = nds.find((n) => n.id === c.id);
          if (!removed || (removed.type !== 'action' && removed.type !== 'switch')) return;
          const configId = removed.data?.taskConfig?.id;
          if (configId) deletedTaskIds.add(configId);
          deletedTaskIds.add(sanitizeTaskId(removed.id));
        });
        return cleanupSwitchReferencesAfterDelete(applied, deletedTaskIds);
      });
    },
    [setNodes]
  );

  const onEdgesChange = useCallback(
    (changes: any) => {
      setEdges((eds: Edge[]) => applyEdgeChanges(changes, eds));
    },
    [setEdges]
  );

  const onConnect = useCallback(
    (connection: any) => {
      const sourceNodeId = connection.source;
      const targetNodeId = connection.target;

      // Check if this connection would create a cycle
      if (wouldCreateCycle(sourceNodeId, targetNodeId, edges)) {
        rejectConnection(sourceNodeId, targetNodeId, setNodes);
        return;
      }

      const newEdge = buildNewEdge(connection, nodes);
      setEdges((eds: Edge[]) => addEdge(newEdge, eds));
    },
    [edges, nodes, setEdges, setNodes]
  );

  // Filter state for executions
  const [executionStatusFilter, setExecutionStatusFilter] = useState('All');
  const [executionTriggerTypeFilter, _setExecutionTriggerTypeFilter] = useState('All');

  // Helper functions to convert UI values to API values
  const getExecutionStatusFilter = (status: string) => {
    if (status === 'All') {
      return undefined;
    }
    // Map user-friendly names to backend values
    const statusMap: { [key: string]: string } = {
      Running: 'RUNNING',
      Completed: 'COMPLETED',
      Failed: 'FAILED',
      Canceled: 'CANCELED',
      Terminated: 'TERMINATED',
      'Timed Out': 'TIMED_OUT',
      'Continued As New': 'CONTINUED_AS_NEW',
      Unspecified: 'UNSPECIFIED',
    };
    return statusMap[status] || status.toUpperCase();
  };

  const getExecutionTriggerTypeFilter = (type: string) => {
    if (type === 'All') {
      return undefined;
    }
    return type.toLowerCase();
  };

  // Workflow execution data
  const { executionData, executionLoading, fetchExecutionData, hasMore, hasPrevious, loadingMore, goToNextPage, goToPreviousPage } = useExecutionData(
    {
      workflowId,
      accountId,
      currentMode,
      workflowDataId: workflowData?.id || undefined,
      status: getExecutionStatusFilter(executionStatusFilter),
      triggerType: getExecutionTriggerTypeFilter(executionTriggerTypeFilter),
    }
  );

  // Workflow execution state
  const runbookId = workflowId;
  const [isTestRunning, setIsTestRunning] = useState(false);
  const isTestRunningRef = useRef(false);
  const currentExecutionIdRef = useRef<string | null>(null);
  const [pollingInterval, setPollingInterval] = useState<any>(null);
  const [approvalLoading, setApprovalLoading] = useState<string | null>(null);

  // Dry run state
  const [isDryRunning, setIsDryRunning] = useState(false);
  const dryRunPollingRef = useRef<ReturnType<typeof setInterval> | null>(null);
  const dryRunIdRef = useRef<string | null>(null);
  const dryRunExecutionIdRef = useRef<string | null>(null);
  const [dryRunResult, setDryRunResult] = useState<any>(null);
  const [showDryRunModal, setShowDryRunModal] = useState(false);

  // Test task response dialog state
  const [testResponseDialog, setTestResponseDialog] = useState<{
    open: boolean;
    taskType: string;
    responseData: any;
  }>({
    open: false,
    taskType: '',
    responseData: null,
  });
  // Removed actionDataMap as it's no longer needed with WorkflowActionRenderer

  // Trigger selector popup state
  const [triggerPopupOpen, setTriggerPopupOpen] = useState(false);

  // Settings modal state (moved from WorkflowHeader to support bottom toolbar)
  const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);

  // Track context when opening sidebar from half-edge or edge add button
  const [addNodeIntent, setAddNodeIntent] = useState<{
    type: 'half-edge' | 'edge-insert';
    sourceNodeId?: string;
    sourceHandleId?: string;
    edgeId?: string;
    edgeSource?: string;
    edgeTarget?: string;
    edgeSourceHandle?: string;
    edgeData?: any;
    edgeStyle?: any;
    edgeType?: string;
    edgeLabel?: string;
  } | null>(null);

  const {
    selectedNode,
    setSelectedNode,
    sidebarOpen,
    setSidebarOpen,
    expandedCategory,
    actionDetailsSidebarOpen,
    setActionDetailsSidebarOpen,
    triggerConfigSidebarOpen,
    setTriggerConfigSidebarOpen,
    selectedActionType,
    setSelectedActionType,
    nodeTypesConfig,
    onNodeClick,
    addNode,
    toggleCategory,
    deselectAllNodes,
    closeTriggerConfigSidebar,
    updateTriggerConfig,
  } = useWorkflowInteractions(dynamicCategories, taskDefinitions);

  // Saved draft is ahead of the published live version (saved but not published).
  // Best-effort, session-scoped: flips true once the user makes any change, and
  // resets on fresh load and on a successful Publish.
  const [draftAheadOfLive, setDraftAheadOfLive] = useState(false);

  // Unsaved changes tracking hook
  const {
    hasUnsavedChanges, // Surfaced in the top-right state strip (canvas vs saved draft)
    showUnsavedChangesDialog,
    setShowUnsavedChangesDialog: _setShowUnsavedChangesDialog, // Exposed for manual control if needed
    exitVariant,
    handleConfirmNavigation,
    handleCancelNavigation,
    updateSavedSnapshot: _updateSavedSnapshot,
    pauseChangeDetection,
    resumeChangeDetection,
  } = useUnsavedChangesTracking({
    nodes,
    edges,
    workflowSettings,
    workflowData,
    loading,
    isInitialized,
    enabled: canEdit,
    hasUnpublishedChanges: draftAheadOfLive,
  });

  // Once the user diverges from the saved draft, the saved draft will be ahead
  // of the live version until they Publish.
  useEffect(() => {
    if (hasUnsavedChanges) setDraftAheadOfLive(true);
  }, [hasUnsavedChanges]);

  const { undo, redo } = useWorkflowHistory({
    nodes,
    edges,
    workflowSettings,
    setNodes,
    setEdges,
    setWorkflowSettings,
    enabled: isInitialized && !loading,
    workflowId,
  });

  const { copySelection, cutSelection, paste, duplicateSelection } = useWorkflowClipboard({
    setNodes,
    setEdges,
  });

  useWorkflowShortcuts({
    enabled: currentMode === 'editor',
    nodes,
    edges,
    setNodes,
    copySelection,
    cutSelection,
    paste,
    duplicateSelection,
    undo,
    redo,
    onEscape: () => {
      if (actionDetailsSidebarOpen) {
        setActionDetailsSidebarOpen(false);
        deselectAllNodes(setNodes);
      } else if (triggerConfigSidebarOpen) {
        closeTriggerConfigSidebar(setNodes);
      } else {
        deselectAllNodes(setNodes);
      }
    },
  });

  // Function to open dedicated trigger selector popup
  const openSidebarWithTriggers = () => {
    setTriggerPopupOpen(true);
  };

  // Handle tab switching
  const handleTabChange = useCallback(
    (tab: string) => {
      const returningToEditor = tab === 'editor' && currentMode === 'executions';
      setCurrentMode(tab as 'editor' | 'json' | 'executions');
      // When the user comes back from the Executions tab, wipe the execution badges/colors that
      // were painted on the canvas by updateNodeStatusesFromTasks — the editor should present a
      // clean canvas, not a frozen view of the last run.
      if (returningToEditor) {
        setNodes((prev) =>
          prev.map((node) => {
            if (node.type !== 'action' && node.type !== 'switch') return node;
            const { executionStatus, executionDuration, lastExecutionTime, executionOutput, executionError, ...rest } = node.data || {};
            const hadAny =
              executionStatus !== undefined ||
              executionDuration !== undefined ||
              lastExecutionTime !== undefined ||
              executionOutput !== undefined ||
              executionError !== undefined;
            return hadAny ? { ...node, data: rest } : node;
          })
        );
      }
      // Update URL hash to reflect the current tab
      const basePath = router.asPath.split('#')[0];
      router.replace(`${basePath}#${tab}`, undefined, { shallow: true });
    },
    [router, currentMode, setNodes]
  );

  // Save workflow function
  const handlePrettifyLayout = useCallback(() => {
    if (nodes.length === 0) return;

    const strippedTasks = extractTasksFromWorkflowNodes(nodes, edges).map(({ layout: _layout, ...t }) => t);
    const strippedTriggers = extractTriggersFromNodes(nodes).map(({ layout: _layout, ...t }) => t);

    const definition = {
      ...(workflowData?.definition || {}),
      tasks: strippedTasks,
      triggers: strippedTriggers,
    };

    const {
      nodes: newNodes,
      edges: newEdges,
      viewport,
    } = convertWorkflowToReactFlow(
      definition,
      {
        minHorizontalSpacing: 250,
        minVerticalSpacing: 180,
        minTriggerSpacing: 250,
        minConditionalSpacing: 120,
      },
      taskDefinitions
    );

    setNodes(newNodes);
    setEdges(newEdges);
    setPersistedViewport(null);
    setAutoFitViewport(viewport || null);
  }, [nodes, edges, workflowData, taskDefinitions]);

  // Returns true when the save (create or update) was actually persisted.
  // false signals an early bail or backend failure so callers like the
  // Publish-confirm path can abort instead of snapshotting a stale draft.
  const handleSaveWorkflow = async (): Promise<boolean> => {
    try {
      // Block if on JSON tab with invalid JSON
      if (currentMode === 'json' && !jsonValid) {
        snackbar.error('Cannot save: JSON is invalid. Please fix errors or switch to Editor tab.');
        return false;
      }

      // Block if on JSON tab with unsaved changes
      if (currentMode === 'json' && jsonHasUnsavedChanges) {
        snackbar.error('Cannot save: You have unapplied JSON changes. Click "Apply" first.');
        return false;
      }

      // Validate workflow
      const validationErrors = validateWorkflowForSave(workflowData, nodes, (nodes: Node[]) => extractTasksFromWorkflowNodes(nodes, edges));
      if (validationErrors.length > 0) {
        snackbar.error(`Cannot save automation: ${validationErrors.join(', ')}`);
        return false;
      }

      // Validate that sum of task timeouts does not exceed workflow timeout
      const taskTimeoutsForValidation = nodes.filter((n) => n.data?.taskConfig?.timeout).map((n) => n.data.taskConfig.timeout as string);
      if (workflowSettings.timeout && taskTimeoutsForValidation.length > 0) {
        const workflowSec = parseDurationToSeconds(workflowSettings.timeout);
        if (!isNaN(workflowSec) && workflowSec > 0) {
          const totalTaskSec = taskTimeoutsForValidation.reduce((sum, t) => {
            const sec = parseDurationToSeconds(t);
            return sum + (isNaN(sec) ? 0 : sec);
          }, 0);
          if (totalTaskSec > 0 && totalTaskSec > workflowSec) {
            snackbar.error(
              'Cannot save: Sum of all task timeouts exceeds the automation timeout. Reduce task timeouts or increase the automation timeout.'
            );
            return false;
          }
        }
      }

      // Prepare workflow data using utility function
      const { definition: workflowDefinition } = prepareWorkflowForSave(
        nodes,
        edges,
        (nodes: Node[]) => extractTasksFromWorkflowNodes(nodes, edges),
        extractTriggersFromNodes,
        workflowSettings,
        workflowData?.definition,
        reactFlowInstanceRef.current?.getViewport()
      );

      // Inject ai_session_id tag if this is an AI-generated workflow
      const settingsForSave = { ...workflowSettings };
      if (aiSessionId && !settingsForSave.tags.some((t) => t.startsWith('ai_session_id:'))) {
        settingsForSave.tags = [...settingsForSave.tags, `ai_session_id:${aiSessionId}`];
      }

      if (isNewWorkflow) {
        // Create new workflow using utility function
        const createRequest = createWorkflowCreateRequest(
          accountId,
          workflowData?.name || 'New Automation',
          workflowDefinition,
          settingsForSave,
          aiSessionId || undefined
        );

        const response: any = await apiWorkflow.createWorkflow(createRequest);

        const errorMessage = parseHttpResponseBodyMessage(response);
        if (errorMessage) {
          snackbar.error(errorMessage);
          console.error('Workflow creation error:', response.errors);
          return false;
        }

        const newWorkflowId = response.data?.workflow_create?.id;
        if (newWorkflowId) {
          snackbar.success(`Automation "${workflowData?.name}" created successfully`);

          // Pause change detection during save-and-redirect flow to prevent false positive unsaved changes
          pauseChangeDetection();

          // Set transition flag to prevent state conflicts
          setIsTransitioningFromCreateToEdit(true);

          // Update workflowData with the new ID immediately
          setWorkflowData((prev) => (prev ? { ...prev, id: newWorkflowId } : null));

          // Redirect to edit mode after creation
          router.replace(`/workflow/${newWorkflowId}?accountId=${accountId}`);

          // After URL change, reload the workflow data to ensure complete sync
          setTimeout(async () => {
            try {
              const reloadResponse: any = await apiWorkflow.getWorkflowById(accountId, newWorkflowId);
              const reloadedWorkflow = reloadResponse.data?.workflow_get;

              if (reloadedWorkflow) {
                setWorkflowData(reloadedWorkflow);

                // Update nodes and edges with fresh data
                if (reloadedWorkflow.definition) {
                  const { nodes: reloadedNodes, edges: reloadedEdges } = convertWorkflowToReactFlow(
                    reloadedWorkflow.definition,
                    {
                      minHorizontalSpacing: 250,
                      minVerticalSpacing: 180,
                      minTriggerSpacing: 250,
                      minConditionalSpacing: 120,
                    },
                    taskDefinitions
                  );
                  setNodes(reloadedNodes);
                  setEdges(reloadedEdges);
                }

                // Open NubiChat with workflow context after successful creation
                setNubiChatContext({
                  type: 'workflow',
                  data: {
                    name: reloadedWorkflow.name,
                    id: reloadedWorkflow.id,
                    status: reloadedWorkflow.status,
                    definition: reloadedWorkflow.definition,
                    conversationId: aiSessionId,
                  },
                });
              }

              // Resume change detection after state has stabilized
              resumeChangeDetection();

              // Clear transition flag after successful sync
              setIsTransitioningFromCreateToEdit(false);
            } catch (error) {
              console.error('Error reloading workflow after creation:', error);
              // Resume change detection even if reload fails
              resumeChangeDetection();
              // Clear transition flag even if reload fails
              setIsTransitioningFromCreateToEdit(false);
            }
          }, 500); // Small delay to ensure router update completes
        } else {
          snackbar.error('Failed to get new automation ID');
          return false;
        }
      } else {
        // Update existing workflow using utility function
        const updateRequest = createWorkflowUpdateRequest(
          accountId,
          workflowData?.id || workflowId,
          workflowData?.name || 'Automation',
          workflowDefinition,
          settingsForSave
        );

        const response: any = await apiWorkflow.updateWorkflow(updateRequest);

        const errorMessage = parseHttpResponseBodyMessage(response);
        if (errorMessage) {
          snackbar.error(errorMessage);
          console.error('Workflow update error:', response.errors);
          console.error('Full error details:', JSON.stringify(response.errors, null, 2));
          console.error('Workflow definition being sent:', workflowDefinition);
          return false;
        }

        const updateStatus = response.data?.workflow_update?.definition;
        if (updateStatus === 'success' || updateStatus) {
          snackbar.success(`Automation "${workflowData?.name}" updated successfully`);

          // Pause change detection during reload to prevent false positive unsaved changes
          // (updateSavedSnapshot would capture stale closure values before React re-renders)
          pauseChangeDetection();

          // Reload the workflow data after successful update
          try {
            const reloadResponse: any = await apiWorkflow.getWorkflowById(accountId, workflowId);

            const reloadErrorMessage = parseHttpResponseBodyMessage(reloadResponse);
            if (reloadErrorMessage) {
              console.error('Failed to reload workflow after save:', reloadErrorMessage);
              resumeChangeDetection();
            } else {
              const reloadedWorkflow = reloadResponse.data?.workflow_get;
              if (reloadedWorkflow) {
                setWorkflowData(reloadedWorkflow);

                // Update nodes and edges with reloaded data
                if (reloadedWorkflow.definition) {
                  const { nodes: reloadedNodes, edges: reloadedEdges } = convertWorkflowToReactFlow(
                    reloadedWorkflow.definition,
                    {
                      minHorizontalSpacing: 250,
                      minVerticalSpacing: 180,
                      minTriggerSpacing: 250,
                      minConditionalSpacing: 120,
                    },
                    taskDefinitions
                  );
                  setNodes(reloadedNodes);
                  setEdges(reloadedEdges);
                }

                // Resume change detection - snapshot will be taken on next render with updated state
                resumeChangeDetection();
              } else {
                resumeChangeDetection();
              }
            }
          } catch (reloadError) {
            console.error('Error reloading workflow after save:', reloadError);
            resumeChangeDetection();
          }
        } else {
          snackbar.error('Failed to update automation - no success status returned');
          return false;
        }
      }
      return true;
    } catch (error) {
      console.error('Error saving workflow:', error);
      snackbar.error('Error saving automation. Check console for details.');
      return false;
    }
  };

  // handleSaveWorkflow is a plain function reborn every render, closing over the
  // latest nodes/edges/workflowData. Memoized callers (e.g. handleConfirmPublish)
  // would otherwise capture the instance from the render where their deps last
  // changed — once the canvas is dirty, hasUnsavedChanges stops toggling, so the
  // captured closure freezes on stale canvas state and publishes the wrong draft.
  // Mirror the freshest function into a ref so memoized callers always invoke the
  // current closure without having to list handleSaveWorkflow's many deps.
  const handleSaveWorkflowRef = useRef(handleSaveWorkflow);
  handleSaveWorkflowRef.current = handleSaveWorkflow;

  const openHistoryDrawer = useCallback(async () => {
    if (!workflowId || isNewWorkflow || !accountId) {
      return;
    }
    setHistoryOpen(true);
    setLoadingVersions(true);
    try {
      const response: any = await apiWorkflow.listWorkflowVersions(accountId, workflowId);
      const errMsg = parseHttpResponseBodyMessage(response);
      if (errMsg) {
        snackbar.error(errMsg);
        setVersions([]);
      } else {
        const list = (response?.data?.workflow_list_versions?.versions ?? []) as WorkflowVersionEntry[];
        setVersions(list);
      }
    } catch (err) {
      console.error('Failed to load workflow versions:', err);
      snackbar.error('Failed to load version history.');
      setVersions([]);
    } finally {
      setLoadingVersions(false);
    }
  }, [accountId, isNewWorkflow, workflowId]);

  const handleConfirmRestore = useCallback(async () => {
    if (!confirmRestoreVersion || !accountId || !workflowId) return;
    const targetVersion = confirmRestoreVersion.version_number;
    setRestoring(true);
    try {
      const response: any = await apiWorkflow.restoreWorkflowVersion(accountId, workflowId, targetVersion);
      const errMsg = parseHttpResponseBodyMessage(response);
      if (errMsg) {
        snackbar.error(errMsg);
        return;
      }

      // Reload the workflow so editor state reflects the restored definition.
      // Reuse the same reload-after-save path used by handleSaveWorkflow to keep
      // unsaved-change tracking consistent.
      pauseChangeDetection();
      try {
        const reloadResponse: any = await apiWorkflow.getWorkflowById(accountId, workflowId);
        const reloadedWorkflow = reloadResponse?.data?.workflow_get;
        if (reloadedWorkflow) {
          setWorkflowData(reloadedWorkflow);
          if (reloadedWorkflow.definition) {
            const { nodes: reloadedNodes, edges: reloadedEdges } = convertWorkflowToReactFlow(
              reloadedWorkflow.definition,
              {
                minHorizontalSpacing: 250,
                minVerticalSpacing: 180,
                minTriggerSpacing: 250,
                minConditionalSpacing: 120,
              },
              taskDefinitions
            );
            setNodes(reloadedNodes);
            setEdges(reloadedEdges);
          }
        }
      } finally {
        resumeChangeDetection();
      }

      snackbar.success(`Checked out version ${targetVersion}.`);
      setConfirmRestoreVersion(null);
      setHistoryOpen(false);
      // Refresh the version list in the background so the new restore row
      // shows up next time the drawer opens.
      try {
        const refreshed: any = await apiWorkflow.listWorkflowVersions(accountId, workflowId);
        setVersions((refreshed?.data?.workflow_list_versions?.versions ?? []) as WorkflowVersionEntry[]);
      } catch {
        // best-effort; drawer is closed, ignore
      }
    } catch (err) {
      console.error('Failed to restore workflow version:', err);
      snackbar.error('Failed to restore version.');
    } finally {
      setRestoring(false);
    }
  }, [
    accountId,
    confirmRestoreVersion,
    pauseChangeDetection,
    resumeChangeDetection,
    setEdges,
    setNodes,
    setWorkflowData,
    taskDefinitions,
    workflowId,
  ]);

  // refreshVersions reloads the version list. Used after publish / make-live
  // so the panel reflects the new state. Best-effort: errors logged + dropped.
  const refreshVersions = useCallback(async () => {
    if (!accountId || !workflowId) return;
    try {
      const response: any = await apiWorkflow.listWorkflowVersions(accountId, workflowId);
      const list = (response?.data?.workflow_list_versions?.versions ?? []) as WorkflowVersionEntry[];
      setVersions(list);
    } catch (err) {
      console.error('Failed to refresh workflow versions:', err);
    }
  }, [accountId, workflowId]);

  const openPublishDialog = useCallback(() => {
    setPublishName('');
    setPublishDescription('');
    setPublishSetLive(true);
    // Reset to PAUSED each open so the safer default always wins.
    setPublishStatus('PAUSED');
    setPublishDialogOpen(true);
  }, []);

  const handleConfirmPublish = useCallback(async () => {
    if (!accountId || !workflowId) return;
    setPublishing(true);
    try {
      // Publish always implies "save + publish". The backend's PublishVersion
      // snapshots workflows.definition (the saved draft) into a new
      // workflow_versions row — if the canvas has unsaved edits and we skip
      // the save here, the published snapshot would be the previous saved
      // draft, NOT what the user is staring at. Run handleSaveWorkflow first
      // and abort the publish if it failed so the version never lies.
      if (hasUnsavedChanges) {
        // Call through the ref so we always run the latest handleSaveWorkflow
        // closure (current nodes/edges), not the one frozen when this memoized
        // callback was last recreated.
        const saved = await handleSaveWorkflowRef.current();
        if (!saved) {
          return;
        }
      }
      const response: any = await apiWorkflow.publishWorkflowVersion(accountId, workflowId, {
        name: publishName.trim() || null,
        description: publishDescription.trim() || null,
        setLive: publishSetLive,
        status: publishStatus,
      });
      const errMsg = parseHttpResponseBodyMessage(response);
      if (errMsg) {
        snackbar.error(errMsg);
        return;
      }
      const v = response?.data?.workflows_create_version as WorkflowVersionEntry | undefined;
      snackbar.success(v ? `Published v${v.version_number}${publishSetLive ? ' (live)' : ''}.` : 'Published.');
      setPublishDialogOpen(false);
      await refreshVersions();
      if (publishSetLive) {
        // Draft is now the live version. Clear the ahead-of-live flag and refresh
        // workflowData so the state strip reflects the new live version number.
        setDraftAheadOfLive(false);
        try {
          const reload: any = await apiWorkflow.getWorkflowById(accountId, workflowId);
          const reloaded = reload?.data?.workflow_get;
          if (reloaded) setWorkflowData(reloaded);
        } catch {
          /* non-fatal: strip will refresh on next load */
        }
      }
    } catch (err) {
      console.error('Failed to publish workflow version:', err);
      snackbar.error('Failed to publish workflow.');
    } finally {
      setPublishing(false);
    }
    // handleSaveWorkflow is reached via handleSaveWorkflowRef (a stable ref kept
    // in sync with the latest closure every render), so it is intentionally NOT
    // a dependency here — the ref guarantees we never publish stale canvas state
    // without re-creating this callback on every edit.
  }, [accountId, hasUnsavedChanges, publishDescription, publishName, publishSetLive, publishStatus, refreshVersions, workflowId]);

  // Per-version status mutation, fired from the dropdown next to each row in
  // the version-history drawer. Tracks which version is currently being
  // mutated so we can disable the matching control while the request is in
  // flight; this keeps the UI responsive for non-target rows.
  const [statusUpdatingVersionNumber, setStatusUpdatingVersionNumber] = useState<number | null>(null);
  const handleChangeVersionStatus = useCallback(
    async (versionNumber: number, status: 'ACTIVE' | 'PAUSED' | 'INACTIVE') => {
      if (!accountId || !workflowId) return;
      setStatusUpdatingVersionNumber(versionNumber);
      try {
        const response: any = await apiWorkflow.updateWorkflowVersionStatus(accountId, workflowId, versionNumber, status);
        const errMsg = parseHttpResponseBodyMessage(response);
        if (errMsg) {
          snackbar.error(errMsg);
          return;
        }
        snackbar.success(`v${versionNumber} → ${status.toLowerCase()}.`);
        await refreshVersions();
        // If the mutated version was live, workflows.status mirrored — reload
        // workflowData so the strip reflects the new state without a manual
        // refresh.
        try {
          const reload: any = await apiWorkflow.getWorkflowById(accountId, workflowId);
          const reloaded = reload?.data?.workflow_get;
          if (reloaded) setWorkflowData(reloaded);
        } catch {
          /* non-fatal: workflow data refresh failure */
        }
      } catch (err) {
        console.error('Failed to update workflow version status:', err);
        snackbar.error('Failed to update version status.');
      } finally {
        setStatusUpdatingVersionNumber(null);
      }
    },
    [accountId, refreshVersions, workflowId]
  );

  const handleConfirmMakeLive = useCallback(async () => {
    if (!confirmLiveVersion || !accountId || !workflowId) return;
    const targetVersion = confirmLiveVersion.version_number;
    setSettingLive(true);
    try {
      const response: any = await apiWorkflow.makeWorkflowVersionLive(accountId, workflowId, targetVersion);
      const errMsg = parseHttpResponseBodyMessage(response);
      if (errMsg) {
        snackbar.error(errMsg);
        return;
      }
      snackbar.success(`v${targetVersion} is now live.`);
      setConfirmLiveVersion(null);
      await refreshVersions();
    } catch (err) {
      console.error('Failed to make version live:', err);
      snackbar.error('Failed to set live version.');
    } finally {
      setSettingLive(false);
    }
  }, [accountId, confirmLiveVersion, refreshVersions, workflowId]);

  // Function to load workflow from AI-generated response
  // This function can be called to populate the workflow builder with an AI-generated workflow
  const loadWorkflowFromAI = useCallback(
    (aiResponse: AIGenerateWorkflowResponse, conversationSessionId?: string) => {
      try {
        // Build workflow from AI response
        const result = buildWorkflowFromAIResponse(aiResponse, taskDefinitions);

        if (!result.success) {
          snackbar.error(result.error || 'Failed to build automation from AI response');
          return;
        }

        // Update workflow data
        if (result.workflowData) {
          setWorkflowData(result.workflowData);
        }

        // Update nodes and edges
        if (result.nodes && result.edges) {
          setNodes(result.nodes);
          setEdges(result.edges);

          // Update viewport if provided
          if (result.viewport) {
            setAutoFitViewport(result.viewport);
          }
        }

        // Extract workflow JSON for NubiChat context
        let workflowJson = '';
        try {
          const workflowObject: any = {
            name: result.workflowData?.name,
            definition: result.workflowData?.definition,
            tags: result.workflowData?.tags,
          };
          if ((result.workflowData as any)?.status) {
            workflowObject.status = (result.workflowData as any).status;
          }
          workflowJson = JSON.stringify(workflowObject, null, 2);
        } catch (jsonError) {
          console.error('Failed to parse workflow JSON for NubiChat:', jsonError);
          // Skip JSON if parsing fails
        }

        // Open NubiChat with workflowbuilder context
        setNubiChatContext({
          type: 'workflowbuilder',
          data: {
            workflowJson,
            conversationId: conversationSessionId || aiSessionId,
          },
        });
        setShowNubiChat(true);

        snackbar.success(`Automation "${result.workflowData?.name}" loaded successfully from AI response`);
      } catch (error) {
        console.error('Error loading workflow from AI response:', error);
        snackbar.error('Failed to load automation from AI response');
      }
    },
    [accountId, taskDefinitions]
  );

  // Helper: get default trigger inputs from manual trigger nodes
  const getDefaultTriggerInputs = useCallback(() => {
    const currentNodes = nodesRef.current;
    const triggerNodes = currentNodes.filter((node) => node.type === 'trigger' && node.data.trigger?.type === 'manual');
    return triggerNodes.length > 0 ? triggerNodes[0].data.trigger?.params?.inputs || {} : {};
  }, []);

  // Helper: get input schema from workflow settings
  const getInputSchema = useCallback(() => {
    const settings = workflowSettingsRef.current;
    if (!settings?.inputs || settings.inputs.length === 0) return [];
    return settings.inputs.map((input) => ({
      id: input.id,
      type: input.type,
      description: input.description || `Input parameter: ${input.id}`,
      default: input.default,
    }));
  }, []);

  // Helper: get primary trigger type
  const getTriggerType = useCallback(() => {
    const currentNodes = nodesRef.current;
    const triggerNodes = currentNodes.filter((node) => node.type === 'trigger');
    if (triggerNodes.length === 0) return 'manual';
    const manualTrigger = triggerNodes.find((n) => n.data.trigger?.type === 'manual');
    return manualTrigger ? 'manual' : triggerNodes[0].data.trigger?.type || 'manual';
  }, []);

  // Validate workflow state before run/dry-run, returns error message or null if valid.
  // `variant` is retained for the call signature; validation no longer differs
  // by variant now that the DRAFT status (and its run gate) has been removed.
  const validateBeforeExecution = useCallback(
    (mode: 'run' | 'dryrun', _variant: 'current' | 'live' = 'current'): string | null => {
      if (mode === 'run' && isTestRunning) return 'Manual run already in progress';
      if (mode === 'dryrun' && (isDryRunning || isTestRunning)) return 'A run is already in progress';
      if (currentMode === 'json' && !jsonValid) return 'JSON is invalid. Please fix errors or switch to Editor tab.';
      if (currentMode === 'json' && jsonHasUnsavedChanges) return 'You have unapplied JSON changes. Click "Apply" first.';
      const validationErrors = validateWorkflowForSave(workflowDataRef.current, nodesRef.current, (nodes: Node[]) =>
        extractTasksFromWorkflowNodes(nodes, edges)
      );
      if (validationErrors.length > 0) return `Cannot execute: ${validationErrors.join(', ')}`;
      return null;
    },
    [isTestRunning, isDryRunning, edges]
  );

  // Run button click: validate, save, then open trigger input modal.
  // `variant` is 'current' for the canvas-default "Run current" path (draft
  // snapshot, user stays on the editor) or 'live' for the secondary "Run
  // live version" menu item (published live version, navigates to the
  // executions tab on success).
  const handleRunButtonClick = useCallback(
    async (variant: 'current' | 'live' = 'current') => {
      setRunVariant(variant);
      try {
        const validationError = validateBeforeExecution('run', variant);
        if (validationError) {
          snackbar.error(validationError);
          return;
        }

        // Get the latest workflow data from ref
        const currentWorkflowData = workflowDataRef.current;
        const currentNodes = nodesRef.current;

        // Get current workflow settings from ref to avoid stale closure
        const currentWorkflowSettings = workflowSettingsRef.current;

        // Prepare workflow data using utility function with latest nodes from ref
        const { definition: workflowDefinition } = prepareWorkflowForSave(
          currentNodes,
          edges,
          (nodes: Node[]) => extractTasksFromWorkflowNodes(nodes, edges),
          extractTriggersFromNodes,
          currentWorkflowSettings,
          currentWorkflowData?.definition,
          reactFlowInstanceRef.current?.getViewport()
        );

        // Use workflow ID from workflowData if it exists
        let currentWorkflowId = currentWorkflowData?.id || workflowId;

        if (!currentWorkflowData?.id) {
          // Create new workflow first
          const createRequest = createWorkflowCreateRequest(
            accountId,
            currentWorkflowData?.name || 'New Automation',
            workflowDefinition,
            currentWorkflowSettings,
            aiSessionId || undefined
          );

          const createResponse: any = await apiWorkflow.createWorkflow(createRequest);

          if (createResponse.errors) {
            snackbar.error(`Failed to create automation: ${createResponse.errors.join(', ')}`);
            console.error('Workflow creation error:', createResponse.errors);
            return;
          }

          currentWorkflowId = createResponse.data?.workflow_create?.id;
          if (!currentWorkflowId) {
            snackbar.error('Failed to get new automation ID');
            return;
          }

          snackbar.success('Automation created and saved successfully');

          // Set transition flag and update workflowData with new ID
          setIsTransitioningFromCreateToEdit(true);
          setWorkflowData((prev) => (prev ? { ...prev, id: currentWorkflowId } : null));

          // Update URL to reflect the new workflow ID
          router.replace(`/workflow/${currentWorkflowId}?accountId=${accountId}`);
        } else if (variant === 'current' && hasUnsavedChanges) {
          // "Run current" runs the on-screen draft, so we must persist the
          // draft first or the backend would execute a stale snapshot.
          // Critically, we ONLY save when hasUnsavedChanges is true — otherwise
          // every click of Run was silently writing the draft back and bumping
          // updated_by on the workflow row, corrupting the "last edited by"
          // audit trail for a user who never actually edited anything.
          //
          // "Run live" never needs this branch: it executes the published live
          // version (workflow_versions row), so workflows.definition is
          // irrelevant to the run.
          const updateRequest = createWorkflowUpdateRequest(
            accountId,
            currentWorkflowId,
            currentWorkflowData?.name || 'Automation',
            workflowDefinition,
            currentWorkflowSettings
          );

          const updateResponse: any = await apiWorkflow.updateWorkflow(updateRequest);

          if (updateResponse.errors) {
            snackbar.error('Failed to update automation before manual run');
            console.error('Workflow update error:', updateResponse.errors);
            return;
          }

          snackbar.success('Automation saved successfully');
        }

        // Open the trigger input modal instead of immediately executing
        setTriggerModalMode('run');
        setTriggerModalOpen(true);
      } catch (error) {
        console.error('Error during save before run:', error);
        snackbar.error('Failed to save automation');
      }
    },
    [validateBeforeExecution, edges, workflowId, accountId, isNewWorkflow, router, workflowSettings, hasUnsavedChanges]
  );

  // Dry Run button click: validate, then open trigger input modal
  const handleDryRunButtonClick = useCallback(async () => {
    const validationError = validateBeforeExecution('dryrun');
    if (validationError) {
      snackbar.error(validationError);
      return;
    }

    // Open the trigger input modal for dry run
    setTriggerModalMode('dryrun');
    setTriggerModalOpen(true);
  }, [validateBeforeExecution]);

  // Helper function to prepare dry-run request with optional task filtering
  const prepareDryRunRequest = useCallback(
    (taskIdsToInclude?: string[], inputsOverride?: any) => {
      const currentNodes = nodesRef.current;
      const currentWorkflowData = workflowDataRef.current;

      // Prepare workflow definition, optionally filtering tasks
      // When filtering, also clean up depends_on references to excluded tasks
      // to avoid sending a broken dependency graph to the server
      const taskExtractor = taskIdsToInclude
        ? (nodes: Node[]) => filterTasksForPartialRun(extractTasksFromWorkflowNodes(nodes, edges), new Set(taskIdsToInclude))
        : (nodes: Node[]) => extractTasksFromWorkflowNodes(nodes, edges);

      const { definition } = prepareWorkflowForSave(
        currentNodes,
        edges,
        taskExtractor,
        extractTriggersFromNodes,
        workflowSettings,
        currentWorkflowData?.definition
      );

      // Use provided inputs override, or extract from manual trigger nodes
      const manualTriggerInputs =
        inputsOverride !== undefined
          ? inputsOverride
          : (() => {
              const triggerNodes = currentNodes.filter((node) => node.type === 'trigger' && node.data.trigger?.type === 'manual');
              return triggerNodes.length > 0 ? triggerNodes[0].data.trigger?.params?.inputs || {} : {};
            })();

      return {
        account_id: accountId,
        definition,
        inputs: manualTriggerInputs,
        // so dry-run notifications show the automation name, not a placeholder
        name: currentWorkflowData?.name,
      };
    },
    [edges, workflowSettings, accountId]
  );

  // Execute a workflow run with user-provided inputs
  const executeRun = useCallback(
    async (inputs: any) => {
      const currentWorkflowId = workflowDataRef.current?.id || workflowId;
      if (!currentWorkflowId) {
        snackbar.error('Cannot trigger automation: No automation ID');
        return;
      }

      // Clear execution states from previous run
      setNodes((prevNodes) =>
        prevNodes.map((node) => {
          if (!(node.type === 'action' || node.type === 'switch') || !node.data.taskConfig) return node;
          const needsIdSync = !node.data.taskConfig.id;
          const needsStatusClear = !!node.data.executionStatus;
          if (!needsIdSync && !needsStatusClear) return node;
          return {
            ...node,
            data: {
              ...node.data,
              taskConfig: needsIdSync ? { ...node.data.taskConfig, id: sanitizeTaskId(node.id) } : node.data.taskConfig,
              ...(needsStatusClear
                ? { executionStatus: undefined, lastExecutionTime: undefined, executionOutput: undefined, executionError: undefined }
                : {}),
            },
          };
        })
      );

      setIsTestRunning(true);
      isTestRunningRef.current = true;

      const variant = runVariant;
      const response: any = await apiWorkflow.triggerWorkflow({
        account_id: accountId,
        id: currentWorkflowId,
        inputs,
        use_draft_definition: variant === 'current',
      });

      const errorMessage = parseHttpResponseBodyMessage(response);
      if (errorMessage) {
        console.error('Workflow trigger failed with errors:', response.errors);
        snackbar.error(errorMessage);
        setIsTestRunning(false);
        isTestRunningRef.current = false;
        return;
      }

      const triggerData = response?.data?.workflow_execute;
      if (triggerData?.execution_id) {
        snackbar.success('Automation execution started successfully!');
        fetchExecutionData();
        // On "Run live", jump to the Executions tab and auto-select the
        // new run so the user can immediately see the live-version canvas
        // and task progress. "Run current" keeps the user on the editor
        // so they can keep iterating.
        if (variant === 'live') {
          setPendingExecutionId(triggerData.execution_id);
          setCurrentMode('executions');
        }
        setTimeout(() => startPollingWorkflowExecution(triggerData.execution_id), 1000);
      } else {
        console.error('No execution ID returned from trigger:', response);
        snackbar.error('Failed to get execution ID from automation trigger');
        setIsTestRunning(false);
        isTestRunningRef.current = false;
      }
    },
    [workflowId, accountId, fetchExecutionData, runVariant]
  );

  // Poll for dry run completion
  const startPollingDryRun = useCallback(
    (dryrunId: string, executionId: string) => {
      // Clear any existing dry run polling
      if (dryRunPollingRef.current !== null) {
        clearInterval(dryRunPollingRef.current);
        dryRunPollingRef.current = null;
      }
      const startTime = Date.now();
      const terminalStatuses = ['COMPLETED', 'COMPLETE', 'FAILED', 'COMPLETE_WITH_ERROR', 'CANCELED', 'TERMINATED', 'TIMED_OUT'];
      const poll = async () => {
        if (Date.now() - startTime > 600000) {
          if (dryRunPollingRef.current !== null) {
            clearInterval(dryRunPollingRef.current);
            dryRunPollingRef.current = null;
          }
          dryRunIdRef.current = null;
          dryRunExecutionIdRef.current = null;
          setIsDryRunning(false);
          snackbar.error('Dry run timed out');
          return;
        }
        try {
          const response: any = await apiWorkflow.getWorkflowExecution(accountId, dryrunId, executionId);
          const execution = response?.data?.workflow_get_execution;
          if (execution === null || execution === undefined) {
            return;
          }
          if (terminalStatuses.includes(execution.status)) {
            if (dryRunPollingRef.current !== null) {
              clearInterval(dryRunPollingRef.current);
              dryRunPollingRef.current = null;
            }
            const dryRunResponse = {
              status: execution.status,
              output: execution.workflow_result,
              error: execution.error,
              tasks: execution.tasks,
            };
            setDryRunResult(dryRunResponse);
            setShowDryRunModal(true);
            setIsDryRunning(false);
            dryRunIdRef.current = null;
            dryRunExecutionIdRef.current = null;
            if (execution.status === 'COMPLETED' || execution.status === 'COMPLETE') {
              snackbar.success('Dry run completed successfully');
            } else if (execution.status === 'FAILED') {
              snackbar.error('Dry run failed: ' + (execution.error || 'Unknown error'));
            } else if (execution.status === 'CANCELED' || execution.status === 'TERMINATED') {
              snackbar.info('Dry run canceled');
            }
          }
        } catch (error) {
          console.error('Dry run polling error:', error);
        }
      };
      poll();
      dryRunPollingRef.current = setInterval(poll, 3000);
    },
    [accountId]
  );

  // Execute a dry run with user-provided inputs
  const executeDryRun = useCallback(
    async (inputs: any) => {
      setIsDryRunning(true);

      const request = prepareDryRunRequest(undefined, inputs);
      const response: any = await apiWorkflow.triggerWorkflowDryRun(request);

      const errorMessage = parseHttpResponseBodyMessage(response);
      if (errorMessage) {
        console.error('Dry-run failed with errors:', response.errors);
        snackbar.error(errorMessage);
        setIsDryRunning(false);
        return;
      }

      const result = response?.data?.workflow_dryrun_execute;

      // If already terminal (fast failure during validation), show immediately
      if (result?.status === 'COMPLETED' || result?.status === 'FAILED') {
        setDryRunResult(result);
        setShowDryRunModal(true);
        if (result?.status === 'COMPLETED') {
          snackbar.success('Dry run completed successfully');
        } else {
          snackbar.error('Dry run failed: ' + (result?.error || 'Unknown error'));
        }
        setIsDryRunning(false);
        return;
      }

      // Async: start polling
      if (result?.dryrun_id && result?.execution_id) {
        dryRunIdRef.current = result.dryrun_id;
        dryRunExecutionIdRef.current = result.execution_id;
        startPollingDryRun(result.dryrun_id, result.execution_id);
      } else {
        snackbar.error('Unexpected dry run response');
        setIsDryRunning(false);
      }
    },
    [prepareDryRunRequest, startPollingDryRun]
  );

  // Handle trigger from modal: execute with user-provided inputs
  const handleTriggerFromModal = useCallback(
    async (inputs: any) => {
      setTriggerModalLoading(true);
      try {
        if (triggerModalMode === 'run') {
          await executeRun(inputs);
        } else {
          await executeDryRun(inputs);
        }
      } catch (error) {
        console.error('Error during trigger from modal:', error);
        snackbar.error(triggerModalMode === 'run' ? 'Failed to run automation' : 'Failed to execute dry run');
        if (triggerModalMode === 'run') {
          setIsTestRunning(false);
          isTestRunningRef.current = false;
        } else {
          setIsDryRunning(false);
        }
      } finally {
        setTriggerModalLoading(false);
        setTriggerModalOpen(false);
      }
    },
    [triggerModalMode, executeRun, executeDryRun]
  );

  // Legacy handleTestRun kept for backward compatibility (trigger node play button).
  // Defaults to "Run current" so the inline play button matches the canvas
  // primary action.
  const handleTestRun = useCallback(async () => {
    await handleRunButtonClick('current');
  }, [handleRunButtonClick]);

  // Poll for dry run and resolve promise when complete
  const pollDryRunUntilDone = useCallback(
    (dryrunId: string, executionId: string): Promise<any> => {
      return new Promise((resolve) => {
        const startTime = Date.now();
        const terminalStatuses = ['COMPLETED', 'COMPLETE', 'FAILED', 'COMPLETE_WITH_ERROR', 'CANCELED', 'TERMINATED', 'TIMED_OUT'];
        let interval: ReturnType<typeof setInterval> | null = null;
        const poll = async () => {
          if (Date.now() - startTime > 600000) {
            if (interval !== null) {
              clearInterval(interval);
            }
            snackbar.error('Dry run timed out');
            resolve(null);
            return;
          }
          try {
            const response: any = await apiWorkflow.getWorkflowExecution(accountId, dryrunId, executionId);
            const execution = response?.data?.workflow_get_execution;
            if (execution === null || execution === undefined) {
              return;
            }
            if (terminalStatuses.includes(execution.status)) {
              if (interval !== null) {
                clearInterval(interval);
              }
              resolve({
                status: execution.status,
                output: execution.workflow_result,
                error: execution.error,
                tasks: execution.tasks,
              });
            }
          } catch (error) {
            console.error('Dry run polling error:', error);
          }
        };
        poll();
        interval = setInterval(poll, 3000);
      });
    },
    [accountId]
  );

  // Trigger dry run and wait for result (handles both sync failure and async polling)
  const triggerAndAwaitDryRun = useCallback(
    async (request: any): Promise<any> => {
      const response: any = await apiWorkflow.triggerWorkflowDryRun(request);

      const errorMessage = parseHttpResponseBodyMessage(response);
      if (errorMessage) {
        snackbar.error(errorMessage);
        return null;
      }

      const result = response?.data?.workflow_dryrun_execute;

      // If already terminal (fast failure during validation), return immediately
      if (result?.status === 'COMPLETED' || result?.status === 'FAILED') {
        return result;
      }

      // Async: poll until complete
      if (result?.dryrun_id && result?.execution_id) {
        return await pollDryRunUntilDone(result.dryrun_id, result.execution_id);
      }

      snackbar.error('Unexpected dry run response');
      return null;
    },
    [pollDryRunUntilDone]
  );

  // Run all previous steps for a given task (dry-run up to but not including the task)
  const handleRunPreviousSteps = useCallback(
    async (currentTaskId: string): Promise<Record<string, any>> => {
      const currentNodes = nodesRef.current;

      // Get all tasks that are predecessors of the current task
      // Sanitize task IDs to match the format used in extractTasksFromWorkflowNodes
      const previousTasks = getPreviousTasksForNode(currentTaskId, currentNodes, edges, taskDefinitions);
      const previousTaskIds = previousTasks.map((t: any) => sanitizeTaskId(t.id));

      if (previousTaskIds.length === 0) {
        snackbar.info('No previous tasks to run');
        return {};
      }

      const request = prepareDryRunRequest(previousTaskIds);
      const result = await triggerAndAwaitDryRun(request);
      if (!result) {
        return {};
      }

      // Extract per-task outputs from response
      const taskOutputs: Record<string, any> = {};
      if (result?.tasks && Array.isArray(result.tasks)) {
        result.tasks.forEach((task: any) => {
          taskOutputs[task.id] = task.output;
        });
      }

      if (result?.status === 'COMPLETED') {
        snackbar.success('Previous steps completed successfully');
      } else if (result?.status === 'FAILED') {
        snackbar.error('Previous steps failed: ' + (result?.error || 'Unknown error'));
      }

      return taskOutputs;
    },
    [edges, taskDefinitions, prepareDryRunRequest, triggerAndAwaitDryRun]
  );

  // Dry-run from start up to and including a specific task. For switch targets, also include
  // direct children so the chosen branch actually runs (the switch alone produces no useful trace).
  const handleDryRunToTask = useCallback(
    async (targetTaskId: string): Promise<any> => {
      const currentNodes = nodesRef.current;

      const previousTasks = getPreviousTasksForNode(targetTaskId, currentNodes, edges, taskDefinitions);
      const targetNode = currentNodes.find((n) => n.id === targetTaskId);
      const switchChildIds =
        targetNode?.type === 'switch' ? getSwitchDryRunEligibility(targetTaskId, currentNodes, edges).childNodeIds.map(sanitizeTaskId) : [];

      // Sanitize task IDs to match the format used in extractTasksFromWorkflowNodes
      const taskIds = [...previousTasks.map((t: any) => sanitizeTaskId(t.id)), sanitizeTaskId(targetTaskId), ...switchChildIds];

      const request = prepareDryRunRequest(taskIds);
      return await triggerAndAwaitDryRun(request);
    },
    [edges, taskDefinitions, prepareDryRunRequest, triggerAndAwaitDryRun]
  );

  // Run a single task in isolation (real execution)
  const handleRunTask = useCallback(
    async (taskType: string, params: any): Promise<any> => {
      try {
        const response: any = await apiWorkflow.triggerTask(accountId, taskType, params);

        // Check for GraphQL-level errors (network, auth, etc)
        const errorMessage = parseHttpResponseBodyMessage(response);
        if (errorMessage) {
          return { error: errorMessage };
        }

        const taskResult = response?.data?.workflow_execute_task;
        if (taskResult?.status === 'FAILED') {
          return { error: taskResult.error || 'Task execution failed' };
        }
        return taskResult?.result ?? taskResult;
      } catch (error: any) {
        console.error('Failed to run task:', error);
        return { error: error?.message || 'Failed to run task' };
      }
    },
    [accountId]
  );

  // Memoize sidebar derived values keyed off nodes/edges so a `{}` literal
  // isn't returned with a fresh reference every render (which previously
  // re-fired the sidebar's taskData sync effect and the cascading API fetches).
  const sidebarSelectedNode = useMemo(() => nodes.find((node) => node.selected && (node.type === 'action' || node.type === 'switch')), [nodes]);
  const sidebarTaskData = useMemo(() => sidebarSelectedNode?.data?.taskConfig?.config ?? {}, [sidebarSelectedNode]);
  const sidebarValidationErrors = useMemo(() => sidebarSelectedNode?.data?.taskConfig?.errors ?? {}, [sidebarSelectedNode]);
  const sidebarPreviousNodeOutputSchema = useMemo(
    () => (sidebarSelectedNode ? getPreviousNodesOutputSchemas(sidebarSelectedNode.id, edges, nodes, taskDefinitions) : {}),
    [sidebarSelectedNode, edges, nodes, taskDefinitions]
  );

  // Track the sidebar's target node id in a ref so the change handlers stay
  // stable across renders. Without this, including sidebarSelectedNode in
  // the useCallback deps would re-create the handler every time nodes
  // changes, defeating the render-loop fix below.
  const sidebarSelectedNodeIdRef = useRef<string | undefined>(undefined);
  sidebarSelectedNodeIdRef.current = sidebarSelectedNode?.id;

  // Pending unsaved-edits state for the action sidebar close-confirmation dialog.
  // When the user closes the sidebar with uncommitted changes, the sidebar passes
  // pendingData here; the confirmation dialog renders as a sibling (not nested).
  const [pendingDiscard, setPendingDiscard] = useState<{ nodeId: string; pendingData: any } | null>(null);

  // Same pattern for the trigger configuration sidebar — buffered pending config
  // surfaced when the user tries to close the trigger sidebar with unsaved edits.
  const [pendingTriggerDiscard, setPendingTriggerDiscard] = useState<{ nodeId: string; pendingConfig: { type: string; params: any } } | null>(null);

  // Close ActionDetailsSidebar when its selected node disappears (e.g. node
  // deleted from the canvas). The sidebar buffers localData internally, so
  // without this effect it would keep rendering the deleted node's stale
  // config — and the dirty-comparison could trip the unsaved-changes modal.
  useEffect(() => {
    if (actionDetailsSidebarOpen && !sidebarSelectedNode) {
      setActionDetailsSidebarOpen(false);
      setPendingDiscard(null);
    }
  }, [actionDetailsSidebarOpen, sidebarSelectedNode, setActionDetailsSidebarOpen]);

  // Stable handlers for ActionDetailsSidebar so its child effects (which list
  // these in deps) don't re-fire on every parent render. Without useCallback,
  // every WorkflowBuilderNotebook render created new arrow refs, cascading
  // into infinite-render loops in the sidebar's data fetchers. Updates are
  // scoped to the sidebar's specific node id so multi-select doesn't
  // accidentally overwrite siblings' configs.
  const handleSidebarTaskDataChange = useCallback(
    (taskData: any) => {
      const targetId = sidebarSelectedNodeIdRef.current;
      if (!targetId) return;
      setNodes((prevNodes) =>
        prevNodes.map((node) => {
          if (node.id === targetId && (node.type === 'action' || node.type === 'switch') && node.data.taskConfig) {
            const validation = validateTaskData(node.data.taskConfig.type, taskData, taskDefinitions);
            return {
              ...node,
              data: {
                ...node.data,
                taskConfig: {
                  ...node.data.taskConfig,
                  config: taskData,
                  valid: validation.isValid,
                  errors: validation.errors,
                },
              },
            };
          }
          return node;
        })
      );
    },
    [setNodes, taskDefinitions]
  );

  const handleSidebarTaskConfigChange = useCallback(
    (field: string, value: any) => {
      const targetId = sidebarSelectedNodeIdRef.current;
      if (!targetId) return;
      setNodes((prevNodes) =>
        prevNodes.map((node) => {
          if (node.id === targetId && (node.type === 'action' || node.type === 'switch') && node.data.taskConfig) {
            return {
              ...node,
              data: {
                ...node.data,
                taskConfig: {
                  ...node.data.taskConfig,
                  [field]: value ?? undefined,
                },
              },
            };
          }
          return node;
        })
      );
    },
    [setNodes]
  );

  // Start polling for workflow execution status
  const startPollingWorkflowExecution = (executionId: string) => {
    // Store the current execution ID so the cancel handler can access it
    currentExecutionIdRef.current = executionId;

    // Clear any existing polling
    if (pollingInterval) {
      clearInterval(pollingInterval);
    }

    // Refresh executions listing before starting polling to ensure the new execution is visible
    fetchExecutionData();

    // Set poll start time for timeout safety
    const startTime = Date.now();

    const poll = async () => {
      try {
        // Safety timeout - stop polling after 10 minutes
        if (Date.now() - startTime > 600000) {
          stopPolling();
          setIsTestRunning(false);
          isTestRunningRef.current = false;
          return;
        }

        // Check if component is still running test
        if (!isTestRunningRef.current) {
          stopPolling();
          return;
        }

        const response: any = await apiWorkflow.getWorkflowExecution(accountId, workflowId!, executionId);

        if (response?.errors) {
          console.error('Error fetching workflow execution:', response.errors);
          return;
        }

        const executionData = response?.data?.workflow_get_execution;
        if (executionData) {
          // Update node statuses based on task execution data
          if (executionData.tasks && Array.isArray(executionData.tasks)) {
            updateNodeStatusesFromTasks(executionData.tasks);
          }

          // Check if execution is complete (handle all terminal statuses)
          const terminalStatuses = [
            'COMPLETED',
            'COMPLETE',
            'FAILED',
            'COMPLETE_WITH_ERROR',
            'CANCELED',
            'TERMINATED',
            'TIMED_OUT',
            'CONTINUED_AS_NEW',
          ];

          if (terminalStatuses.includes(executionData.status)) {
            stopPolling();
            setIsTestRunning(false);
            isTestRunningRef.current = false;
            currentExecutionIdRef.current = null;
            updateAllNodeStatesToFinalState(executionData.status);

            // Clear transition flag if it was set during test run
            setIsTransitioningFromCreateToEdit(false);

            switch (executionData.status) {
              case 'COMPLETED':
              case 'COMPLETE':
                snackbar.success('Automation execution completed successfully!');
                break;
              case 'COMPLETE_WITH_ERROR':
                snackbar.warning('Automation execution completed with errors');
                break;
              case 'FAILED':
                snackbar.error('Automation execution failed');
                break;
              case 'CANCELED':
                snackbar.info('Automation execution was canceled');
                break;
              case 'TERMINATED':
                snackbar.error('Automation execution was terminated');
                break;
              case 'TIMED_OUT':
                snackbar.error('Automation execution timed out');
                break;
              case 'CONTINUED_AS_NEW':
                snackbar.info('Automation execution continued as new instance');
                break;
              default:
                snackbar.info(`Automation execution finished with status: ${executionData.status}`);
                break;
            }
          }
        }
      } catch (error) {
        console.error('Polling error:', error);
        // Continue polling even on error
      }
    };

    // Poll immediately and then every 4 seconds
    poll();
    const interval = setInterval(poll, 4000);
    setPollingInterval(interval);
  };

  // Cancel the currently running execution
  const handleCancelExecution = useCallback(async () => {
    const execId = currentExecutionIdRef.current;
    if (!execId || !workflowId) return;

    try {
      const response: any = await apiWorkflow.cancelExecution({
        account_id: accountId,
        id: workflowId,
        execution_id: execId,
      });
      const cancelMsg = response?.data?.workflow_cancel_execution?.message;
      if (cancelMsg?.toLowerCase().includes('workflow execution canceled successfully')) {
        snackbar.success(cancelMsg);
      } else {
        snackbar.error(cancelMsg || 'Failed to cancel execution');
      }
      // Keep polling; it will detect the CANCELED terminal status and clean up
    } catch {
      snackbar.error('Failed to cancel execution');
    }
  }, [accountId, workflowId]);

  // Submit an approval decision for a pending core.approval task in the active run
  const handleCompleteApproval = useCallback(
    async (taskId: string, status: string, comments?: string) => {
      const execId = currentExecutionIdRef.current;
      if (!execId || !workflowId || !accountId || !taskId) return;
      const loadingKey = `${taskId}:${status}`;
      try {
        setApprovalLoading(loadingKey);
        const response: any = await apiWorkflow.completeApproval({
          account_id: accountId,
          workflow_id: workflowId,
          execution_id: execId,
          task_id: taskId,
          status,
          comments,
        });
        const errorMessage = parseHttpResponseBodyMessage(response);
        if (errorMessage) {
          snackbar.error(`Failed to record approval: ${errorMessage}`);
          return;
        }
        snackbar.success(`Approval recorded as "${status}"`);
      } catch (error) {
        console.error('Error completing approval:', error);
        snackbar.error('Failed to record approval');
      } finally {
        setApprovalLoading(null);
      }
    },
    [accountId, workflowId]
  );

  // Cancel the currently running dry run
  const handleCancelDryRun = useCallback(async () => {
    const dryrunId = dryRunIdRef.current;
    const execId = dryRunExecutionIdRef.current;
    if (accountId == null || dryrunId == null || execId == null) return;

    try {
      const response: any = await apiWorkflow.cancelExecution({
        account_id: accountId,
        id: dryrunId,
        execution_id: execId,
      });
      const cancelMsg = response?.data?.workflow_cancel_execution?.message;
      if (cancelMsg?.toLowerCase()?.includes('workflow execution canceled successfully')) {
        snackbar.success(cancelMsg);
      } else {
        snackbar.error(cancelMsg || 'Failed to cancel dry run');
      }
      // Keep polling; it will detect the CANCELED terminal status and clean up
    } catch {
      snackbar.error('Failed to cancel dry run');
    }
  }, [accountId]);

  // Handle individual task testing
  const handleTestTask = useCallback(
    async (taskType: string, params: any) => {
      try {
        snackbar.info(`Starting manual run for task: ${taskType}`);

        const response: any = await apiWorkflow.triggerTask(accountId, taskType, params);

        const errorMessage = parseHttpResponseBodyMessage(response);
        if (errorMessage) {
          console.error('Task trigger failed with errors:', response.errors);
          snackbar.error(errorMessage);
          return;
        }

        const taskResult = response?.data?.workflow_execute_task;

        // Refresh executions list to show the new task execution
        fetchExecutionData();

        if (taskResult?.status === 'FAILED') {
          snackbar.error(taskResult.error || 'Task execution failed');
          setTestResponseDialog({ open: true, taskType, responseData: taskResult });
          return;
        }

        snackbar.success('Task test completed successfully!');

        if (taskResult) {
          setTestResponseDialog({
            open: true,
            taskType,
            responseData: taskResult,
          });
        }
      } catch (error) {
        console.error('Error testing task:', error);
        snackbar.error('Failed to test task');
      }
    },
    [accountId]
  );

  // Handle trigger run from trigger node
  const handleTriggerRun = useCallback(async () => {
    // Use the same logic as handleTestRun but specifically for trigger nodes
    await handleTestRun();
  }, [handleTestRun]);

  // Handle add node from a half-edge (unconnected source handle)
  const handleAddFromHandle = useCallback(
    (nodeId: string, handleId: string) => {
      setAddNodeIntent({ type: 'half-edge', sourceNodeId: nodeId, sourceHandleId: handleId });
      setSidebarOpen(true);
    },
    [setSidebarOpen]
  );

  // Handle add node on an existing edge (insert between two nodes)
  const handleAddOnEdgeRef = useRef<(edgeId: string) => void>(() => {});
  handleAddOnEdgeRef.current = useCallback(
    (edgeId: string) => {
      const edge = edges.find((e) => e.id === edgeId);
      if (edge) {
        setAddNodeIntent({
          type: 'edge-insert',
          edgeId: edge.id,
          edgeSource: edge.source,
          edgeTarget: edge.target,
          edgeSourceHandle: edge.sourceHandle || undefined,
          edgeData: edge.data,
          edgeStyle: edge.style,
          edgeType: edge.type,
          edgeLabel: typeof edge.label === 'string' ? edge.label : undefined,
        });
        setSidebarOpen(true);
      }
    },
    [edges, setSidebarOpen]
  );
  const stableHandleAddOnEdge = useCallback((edgeId: string) => {
    handleAddOnEdgeRef.current(edgeId);
  }, []);

  // Stable refs for node type callbacks — prevents customNodeTypes from invalidating (React Flow error #002)
  const handleTestTaskRef = useRef(handleTestTask);
  handleTestTaskRef.current = handleTestTask;
  const handleTriggerRunRef = useRef(handleTriggerRun);
  handleTriggerRunRef.current = handleTriggerRun;
  const handleAddFromHandleRef = useRef(handleAddFromHandle);
  handleAddFromHandleRef.current = handleAddFromHandle;
  const accountIdRef = useRef(accountId);
  accountIdRef.current = accountId;

  // Create custom node types — MUST be stable to prevent React Flow error #002 (unmount/remount all nodes)
  const customNodeTypes = useMemo(
    () => ({
      ...nodeTypesConfig,
      action: (props: any) => (
        <ActionNode
          {...props}
          onTestTask={handleTestTaskRef.current}
          accountId={accountIdRef.current}
          onAddFromHandle={handleAddFromHandleRef.current}
        />
      ),
      trigger: (props: any) => <TriggerNode {...props} onTriggerRun={handleTriggerRunRef.current} onAddFromHandle={handleAddFromHandleRef.current} />,
      switch: (props: any) => <SwitchNode {...props} onAddFromHandle={handleAddFromHandleRef.current} />,
    }),
    [nodeTypesConfig]
  );

  // Custom edge types — MUST be stable to prevent React Flow error #002
  const customEdgeTypes = useMemo(
    () => ({
      smoothstep: (props: any) => <DeletableEdge {...props} onAddOnEdge={stableHandleAddOnEdge} />,
      conditional: (props: any) => <ConditionalEdge {...props} onAddOnEdge={stableHandleAddOnEdge} />,
    }),
    // eslint-disable-next-line react-hooks/exhaustive-deps -- stableHandleAddOnEdge is stable (empty-deps useCallback)
    []
  );

  // Update node statuses from workflow task execution data
  const updateNodeStatusesFromTasks = (tasks: any[]) => {
    const taskStatusMap = new Map<string, any>();

    // Create a map of task ID to status (match by ID only, not type)
    // Type-based matching is unreliable when multiple tasks share the same type
    tasks.forEach((task) => {
      taskStatusMap.set(task.id, {
        status: task.status,
        startTime: task.start_time,
        endTime: task.end_time,
        output: task.output,
        error: task.error,
      });
    });

    // Update nodes with execution status. findExecutionTaskForNode handles the switch-renaming
    // quirk (see executor.go:1142) and infers switch-node status from the child that actually ran.
    setNodes((prevNodes) =>
      prevNodes.map((node) => {
        if ((node.type === 'action' || node.type === 'switch') && node.data.taskConfig) {
          const match = findExecutionTaskForNode(node, prevNodes, edges, taskStatusMap);
          if (match) {
            const { task: taskStatus } = match;
            return {
              ...node,
              data: {
                ...node.data,
                executionStatus: taskStatus.status,
                lastExecutionTime: taskStatus.startTime || new Date().toISOString(),
                executionOutput: taskStatus.output,
                executionError: taskStatus.error,
              },
            };
          }
        }
        return node;
      })
    );
  };

  // Stop polling
  const stopPolling = () => {
    if (pollingInterval) {
      clearInterval(pollingInterval);
      setPollingInterval(null);
    }

    fetchExecutionData();
  };

  // Update all nodes to final execution state
  const updateAllNodeStatesToFinalState = (_executionStatus: string) => {
    setNodes((prevNodes) =>
      prevNodes.map((node) => {
        if ((node.type === 'action' || node.type === 'switch') && node.data.taskConfig) {
          // Preserve task-specific status if already set by polling (e.g. COMPLETED, FAILED)
          if (node.data.executionStatus) {
            return node;
          }
          // Tasks that were never reached get SKIPPED status
          return {
            ...node,
            data: {
              ...node.data,
              executionStatus: 'SKIPPED',
              lastExecutionTime: new Date().toISOString(),
            },
          };
        }
        return node;
      })
    );

    // Also update edges to final state
    setEdges((prevEdges) =>
      prevEdges.map((edge) => ({
        ...edge,
        animated: false, // Stop all animations
        style: {
          stroke: 'rgb(175, 175, 175)',
          strokeWidth: 1,
        },
      }))
    );
  };

  // Cleanup polling on unmount or when isTestRunning changes
  useEffect(() => {
    return () => {
      if (pollingInterval) {
        clearInterval(pollingInterval);
      }
    };
  }, [pollingInterval]);

  // Cleanup dry run polling on unmount
  useEffect(() => {
    return () => {
      if (dryRunPollingRef.current) {
        clearInterval(dryRunPollingRef.current);
      }
    };
  }, []);

  // Sync ref with state changes
  useEffect(() => {
    isTestRunningRef.current = isTestRunning;
  }, [isTestRunning]);

  // Safety mechanism: Clear transition flag after a reasonable timeout
  useEffect(() => {
    if (isTransitioningFromCreateToEdit) {
      const timeoutId = setTimeout(() => {
        console.warn('Clearing transition flag due to timeout - this should not normally happen');
        setIsTransitioningFromCreateToEdit(false);
      }, 5000); // 5 second timeout

      return () => clearTimeout(timeoutId);
    }
  }, [isTransitioningFromCreateToEdit]);

  // Stop polling when isTestRunning becomes false (but not on initial render)
  useEffect(() => {
    // Only stop polling if test was running and is now stopped
    // Don't stop on initial mount when isTestRunning is false by default
    if (!isTestRunning && pollingInterval) {
      stopPolling();
    }
  }, [isTestRunning, pollingInterval]);

  // Show loading state while router is not ready or runbookId is not available
  if (!router.isReady || !runbookId) {
    return (
      <Box
        sx={{
          width: '100%',
          height: '100vh',
          display: 'flex',
          justifyContent: 'center',
          alignItems: 'center',
          backgroundColor: 'rgb(243, 243, 243)',
        }}
      >
        <Typography sx={{ color: 'var(--ds-gray-600)' }}>Unable to get workflowId from URL params</Typography>
      </Box>
    );
  }

  return (
    <Box
      sx={{
        display: 'flex',
        flexDirection: 'column',
        width: '100%',
        height: '100vh',
        overflow: 'hidden',
      }}
    >
      {/* Header Component - Spans entire width */}
      <WorkflowHeader
        workflowTitle={workflowData?.name || (isNewWorkflow ? 'New Automation' : 'Loading Automation...')}
        onTabChange={isNewWorkflow ? undefined : handleTabChange}
        activeTab={canEdit ? (currentMode === 'json' ? 'editor' : currentMode) : 'executions'}
        allowTitleEdit={canEdit}
        canEdit={canEdit}
        onTitleChange={(newTitle) => {
          setWorkflowData((prev) =>
            prev
              ? {
                  ...prev,
                  name: newTitle,
                }
              : null
          );
        }}
        accountId={accountId}
        showStateStrip={canEdit && currentMode !== 'executions'}
        hasUnsavedChanges={hasUnsavedChanges}
        draftAheadOfLive={draftAheadOfLive}
        liveVersionNumber={workflowData?.live_version_number}
        liveVersionName={workflowData?.live_version_name}
        liveVersionId={workflowData?.live_version_id}
        draftVersionNumber={workflowData?.draft_version_number}
        draftVersionName={workflowData?.draft_version_name}
        draftVersionId={workflowData?.draft_version_id}
        isNewWorkflow={isNewWorkflow}
        onPublish={openPublishDialog}
        onHistory={openHistoryDrawer}
      />

      {/* Config placeholder warning for AI-generated workflows */}
      {configPlaceholders.length > 0 && !configWarningDismissed && (
        <Alert severity='info' onClose={() => setConfigWarningDismissed(true)} sx={{ borderRadius: 0 }}>
          This workflow uses configuration placeholders: <strong>{configPlaceholders.join(', ')}</strong>. Please ensure these are set in the Config
          Manager before running.
        </Alert>
      )}

      {/* Content Area - Sidebar + Main Content */}
      <Box
        sx={{
          display: 'flex',
          flex: 1,
          overflow: 'hidden',
          height: 'calc(100vh - 60px)', // Subtract header height
          position: 'relative',
        }}
      >
        {/* Main Workflow Content */}
        <Box
          sx={{
            flex: 1,
            height: '100%',
            position: 'relative',
            backgroundColor: 'rgb(246, 246, 246)',
            overflow: 'auto',
          }}
        >
          {/* Render different views based on current mode */}
          {currentMode === 'executions' ? (
            /* Executions View - Full screen dedicated view */
            <Box sx={{ height: '100%', width: '100%' }}>
              <Suspense fallback={<Loader style={{ height: '100%', width: '100%' }} />}>
                <ExecutionsView
                  workflowId={workflowId}
                  accountId={accountId}
                  executions={executionData}
                  loading={executionLoading}
                  onRefresh={fetchExecutionData}
                  taskDefinitions={taskDefinitions}
                  editorNodes={nodes}
                  editorEdges={edges}
                  hasMore={hasMore}
                  hasPrevious={hasPrevious}
                  loadingMore={loadingMore}
                  onNext={goToNextPage}
                  onPrevious={goToPreviousPage}
                  selectedStatus={executionStatusFilter}
                  // selectedTriggerType={executionTriggerTypeFilter}
                  onStatusChange={setExecutionStatusFilter}
                  // onTriggerTypeChange={setExecutionTriggerTypeFilter}
                  pendingExecutionId={pendingExecutionId}
                  onPendingExecutionConsumed={() => setPendingExecutionId(undefined)}
                />
              </Suspense>
            </Box>
          ) : (
            /* Editor View - Can be split view (editor + JSON) or just editor */
            <>
              {/* Sidebar Component - Only in Editor mode */}
              <NodeCategoriesSidebar
                open={sidebarOpen}
                onClose={() => {
                  setSidebarOpen(false);
                  setAddNodeIntent(null);
                }}
                categories={dynamicCategories}
                expandedCategory={expandedCategory}
                onToggleCategory={toggleCategory}
                onAddNode={(categoryKey, subcategoryKey) => {
                  if (addNodeIntent?.type === 'half-edge' && addNodeIntent.sourceNodeId) {
                    // Auto-place below source node and auto-connect
                    const sourceNode = nodes.find((n) => n.id === addNodeIntent.sourceNodeId);
                    if (sourceNode) {
                      const xOffset = getSwitchHandleXOffset(sourceNode, addNodeIntent.sourceHandleId);
                      const newPosition = { x: sourceNode.position.x + xOffset, y: sourceNode.position.y + 180 };
                      setNodes((nds) => {
                        const newNode = addNode(categoryKey, subcategoryKey, nds, newPosition);
                        const connection = {
                          source: addNodeIntent.sourceNodeId,
                          target: newNode.id,
                          sourceHandle: addNodeIntent.sourceHandleId,
                          targetHandle: 'action-input',
                        };
                        const newEdge = buildNewEdge(connection, [...nds, newNode]);
                        setEdges((eds) => [...eds, newEdge]);
                        return [...nds, newNode];
                      });
                    }
                  } else if (addNodeIntent?.type === 'edge-insert' && addNodeIntent.edgeSource && addNodeIntent.edgeTarget) {
                    // Insert node between two connected nodes
                    const sourceNode = nodes.find((n) => n.id === addNodeIntent.edgeSource);
                    const targetNode = nodes.find((n) => n.id === addNodeIntent.edgeTarget);
                    if (sourceNode && targetNode) {
                      const LAYER_HEIGHT = 180;
                      const newPosition = {
                        x: sourceNode.position.x,
                        y: sourceNode.position.y + LAYER_HEIGHT,
                      };
                      setNodes((nds) => {
                        // Push all nodes at or below the target's Y down to make room for the inserted node
                        const targetY = targetNode.position.y;
                        const shiftedNodes = nds.map((n) => {
                          if (n.position.y >= targetY) {
                            return { ...n, position: { ...n.position, y: n.position.y + LAYER_HEIGHT } };
                          }
                          return n;
                        });
                        const newNode = addNode(categoryKey, subcategoryKey, shiftedNodes, newPosition);
                        setEdges((eds) => {
                          // Remove original edge
                          const filteredEdges = eds.filter((e) => e.id !== addNodeIntent.edgeId);
                          // Edge: source -> newNode (preserve original edge's style/data)
                          const connectionAtoNew = {
                            source: addNodeIntent.edgeSource,
                            target: newNode.id,
                            sourceHandle: addNodeIntent.edgeSourceHandle,
                            targetHandle: 'action-input',
                          };
                          const edgeAtoNew = buildNewEdge(connectionAtoNew, [...shiftedNodes, newNode]);
                          // Preserve original edge styling for switch/conditional edges
                          if (addNodeIntent.edgeType === 'conditional' && addNodeIntent.edgeData) {
                            edgeAtoNew.type = 'conditional';
                            edgeAtoNew.data = addNodeIntent.edgeData;
                            if (addNodeIntent.edgeStyle) edgeAtoNew.style = addNodeIntent.edgeStyle;
                          }
                          if (addNodeIntent.edgeLabel) {
                            (edgeAtoNew as any).label = addNodeIntent.edgeLabel;
                          }
                          // Edge: newNode -> target (plain edge)
                          const sourceHandle = newNode.type === 'switch' ? 'switch-default' : 'action-output';
                          const connectionNewToB = {
                            source: newNode.id,
                            target: addNodeIntent.edgeTarget,
                            sourceHandle: newNode.type === 'trigger' ? 'trigger-output' : sourceHandle,
                            targetHandle: 'action-input',
                          };
                          const edgeNewToB = buildNewEdge(connectionNewToB, [...shiftedNodes, newNode]);
                          return [...filteredEdges, edgeAtoNew, edgeNewToB];
                        });
                        return [...shiftedNodes, newNode];
                      });
                    }
                  } else {
                    // Default: random placement (toolbar "Add Action" button)
                    setNodes((nds) => {
                      const newNode = addNode(categoryKey, subcategoryKey, nds);
                      return [...nds, newNode];
                    });
                  }
                  setSidebarOpen(false);
                  setAddNodeIntent(null);
                }}
              />
              {/* Trigger Selector Popup - Dedicated popup for trigger nodes */}
              <TriggerSelectorPopup
                open={triggerPopupOpen}
                onClose={() => setTriggerPopupOpen(false)}
                onSelectTrigger={(triggerKey) => {
                  setNodes((nds) => {
                    const newNode = addNode('triggers', triggerKey, nds);
                    return [...nds, newNode];
                  });
                  setTriggerPopupOpen(false);
                }}
              />
              {/* Split View Container - NubiChat + Editor + JSON */}
              <Box sx={{ height: '100%', width: '100%', display: 'flex', flexDirection: 'row', position: 'relative', overflow: 'hidden' }}>
                {/* NubiChat Sidebar - Sliding Panel on Left (Feature Flag Controlled) */}
                {nubiChatFeatureEnabled && (
                  <Box
                    sx={{
                      position: 'absolute',
                      left: showNubiChat ? '0' : `-${nubiChatWindowWidth}px`,
                      top: 0,
                      height: '100%',
                      width: `${nubiChatWindowWidth}px`,
                      borderRight: '1px solid rgb(226, 226, 227)',
                      backgroundColor: ds.background[100],
                      overflow: 'hidden',
                      transition: isResizingRef.current ? 'none' : 'left 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
                      zIndex: 20,
                    }}
                  >
                    <Suspense
                      fallback={
                        <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
                          <CircularProgress size={24} />
                        </Box>
                      }
                    >
                      {showNubiChat && (
                        <NubiChatSidebar
                          isVisible={true}
                          onClose={() => setShowNubiChat(false)}
                          accountId={accountId}
                          position='left'
                          mode='fixed'
                          width={`${nubiChatWindowWidth}px`}
                          showHeader={true}
                          context={nubiChatContext}
                          querySuffix={nubiChatSuffix}
                          urlConversationId={urlConversationId}
                          urlSessionId={effectiveSessionId}
                        />
                      )}
                      {/* Drag resize handle */}
                      <Box
                        onMouseDown={handleResizeMouseDown('nubi')}
                        sx={{
                          position: 'absolute',
                          right: -3,
                          top: 0,
                          width: '8px',
                          height: '100%',
                          cursor: 'col-resize',
                          zIndex: 25,
                          display: 'flex',
                          alignItems: 'center',
                          justifyContent: 'center',
                          '&::after': {
                            content: '""',
                            width: '2px',
                            height: '32px',
                            borderRadius: 'var(--ds-radius-sm)',
                            backgroundColor: 'rgba(0, 0, 0, 0.15)',
                            transition: 'background-color 0.15s, height 0.15s',
                          },
                          '&:hover::after': {
                            backgroundColor: 'rgba(59, 130, 246, 0.7)',
                            height: '48px',
                          },
                          '&:hover': {
                            backgroundColor: 'rgba(59, 130, 246, 0.08)',
                          },
                        }}
                      />
                    </Suspense>
                  </Box>
                )}
                {/* NubiChat Toggle Button (Feature Flag Controlled) */}
                {nubiChatFeatureEnabled && (
                  <Box
                    id='workflow-nubi-chat-toggle'
                    onClick={() => setShowNubiChat(!showNubiChat)}
                    sx={{
                      position: 'absolute',
                      left: showNubiChat ? `${nubiChatWindowWidth}px` : '0',
                      top: '50%',
                      ml: '-25px',
                      transform: 'translateY(-50%) rotate(-90deg)',
                      transformOrigin: 'center',
                      zIndex: 30,
                      backgroundColor: ds.background[100],
                      border: `1px solid ${ds.brand[200]}`,
                      borderLeft: showNubiChat ? `1px solid ${ds.brand[200]}` : 'none',
                      borderTopLeftRadius: 0,
                      borderBottomLeftRadius: '8px',
                      borderTopRightRadius: 0,
                      borderBottomRightRadius: '8px',
                      padding: 'var(--ds-space-2) var(--ds-space-3)',
                      transition: isResizingRef.current ? 'none' : 'left 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
                      cursor: 'pointer',
                      display: 'flex',
                      alignItems: 'center',
                      gap: 'var(--ds-space-1)',
                      '&:hover': {
                        backgroundColor: 'rgb(249, 249, 249)',
                      },
                      boxShadow: '2px 0 8px rgba(0, 0, 0, 0.1)',
                    }}
                  >
                    {showNubiChat ? (
                      <ChevronLeftIcon fontSize='small' sx={{ color: ds.gray[700], transform: 'rotate(90deg)' }} />
                    ) : (
                      <ChevronRightIcon fontSize='small' sx={{ color: ds.gray[700], transform: 'rotate(90deg)' }} />
                    )}
                    <Typography
                      sx={{
                        fontSize: 'var(--ds-text-small)',
                        fontWeight: 'var(--ds-font-weight-medium)',
                        color: ds.gray[700],
                        whiteSpace: 'nowrap',
                      }}
                    >
                      AI Chat
                    </Typography>
                  </Box>
                )}
                {/* Middle: Workflow Editor */}
                <Box
                  sx={{
                    height: '100%',
                    flex: 1,
                    marginLeft: nubiChatFeatureEnabled && showNubiChat ? `${nubiChatWindowWidth}px` : '0',
                    marginRight: jsonPanelVisible ? `${jsonWindowWidth}px` : '0',
                    position: 'relative',
                    transition: isResizingRef.current
                      ? 'none'
                      : 'margin-left 0.3s cubic-bezier(0.4, 0, 0.2, 1), margin-right 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
                  }}
                >
                  {loading ? (
                    <Loader style={{ height: '100%', width: '100%' }} />
                  ) : (
                    <>
                      {/* Workflow Canvas - Editor Mode Only */}
                      <Box
                        sx={{
                          width: '100%',
                          height: '100%',
                        }}
                      >
                        <ReactFlow
                          nodes={nodes}
                          edges={edges}
                          onNodesChange={onNodesChange}
                          onEdgesChange={onEdgesChange}
                          onConnect={onConnect}
                          onInit={(instance) => {
                            reactFlowInstanceRef.current = instance;
                            // Perform initial fitView after instance is ready
                            if (nodes.length > 0) {
                              setTimeout(() => {
                                instance.fitView({
                                  padding: 0.15,
                                  maxZoom: 0.9,
                                  minZoom: 0.75,
                                  duration: 0,
                                });
                                hasInitiallyFit.current = true;
                              }, 50);
                            }
                          }}
                          defaultEdgeOptions={{
                            type: 'smoothstep',
                            style: {
                              strokeWidth: 1,
                              stroke: 'rgb(175, 175, 175)',
                            },
                          }}
                          connectionLineType={'smoothstep' as any}
                          className='editor-mode'
                          style={{ width: '100%', height: '100%' }}
                          onNodeClick={(event: any, node: any) =>
                            onNodeClick(event, node, currentMode, setNodes, setSelectedActionType, setActionDetailsSidebarOpen, setSidebarOpen)
                          }
                          onPaneClick={() => deselectAllNodes(setNodes)}
                          nodeTypes={customNodeTypes}
                          edgeTypes={customEdgeTypes}
                          fitView={false}
                          fitViewOptions={{
                            padding: 0.15,
                            maxZoom: 0.9,
                            minZoom: 0.75,
                          }}
                          snapToGrid={true}
                          snapGrid={[19, 15]}
                          nodesDraggable={true}
                          nodesConnectable={true}
                          elementsSelectable={true}
                          panOnScroll
                          panOnScrollMode={PanOnScrollMode.Vertical}
                          panOnScrollSpeed={0.8}
                          zoomOnScroll={false}
                          zoomOnPinch
                          proOptions={{
                            hideAttribution: true,
                          }}
                        >
                          <MiniMap
                            nodeStrokeWidth={2}
                            position='bottom-right'
                            style={{
                              width: 50,
                              height: 100,
                              backgroundColor: 'white',
                              border: '1px solid rgb(187, 187, 187)',
                              borderRadius: 'var(--ds-radius-sm)',
                              margin: '0px var(--ds-space-3) var(--ds-space-7) 0px',
                            }}
                          />
                          {/* Soften the canvas dots to be less distracting */}
                          <Background color='rgba(0, 0, 0, 0.42)' />
                          <Controls style={{ marginBottom: 'var(--ds-space-7)' }} />
                        </ReactFlow>
                      </Box>

                      {/* Loading message for AI workflows */}
                      {isNewWorkflow && (router.query.loadFromAI || router.query.aiGenerated) && nodes.length === 0 && (
                        <Box
                          sx={{
                            position: 'absolute',
                            top: '50%',
                            left: '50%',
                            transform: 'translate(-50%, -50%)',
                            textAlign: 'center',
                            zIndex: 15,
                          }}
                        >
                          <Typography variant='h6' sx={{ color: ds.gray[700], mb: 2 }}>
                            Loading AI-generated workflow...
                          </Typography>
                          <Box sx={{ display: 'flex', justifyContent: 'center' }}>
                            <CircularProgress />
                          </Box>
                        </Box>
                      )}

                      {/* Empty State - Consolidated trigger selection for new workflows */}
                      {isNewWorkflow &&
                        nodes.length === 0 &&
                        !router.query.loadFromAI &&
                        !router.query.aiGenerated &&
                        !router.query.loadFromTemplate &&
                        !router.query.eventType &&
                        !router.query.eventPriority &&
                        !router.query.eventSource &&
                        !router.query.eventCluster &&
                        !router.query.eventNamespace &&
                        !aiWorkflowInitializedRef.current &&
                        !templateInitializedRef.current &&
                        !eventTriggerInitializedRef.current && (
                          <Box
                            sx={{
                              position: 'absolute',
                              top: '50%',
                              left: '50%',
                              transform: 'translate(-50%, -50%)',
                              border: '1px solid rgb(226, 226, 227)',
                              backgroundColor: 'white',
                              borderRadius: 'var(--ds-radius-xl)',
                              padding: 'var(--ds-space-6) var(--ds-space-5)',
                              textAlign: 'center',
                              zIndex: 15,
                            }}
                          >
                            <Box
                              sx={{
                                display: 'flex',
                                justifyContent: 'center',
                                mb: 2,
                              }}
                            >
                              <Box
                                sx={{
                                  width: '36px',
                                  height: '36px',
                                  borderRadius: 'var(--ds-radius-xl)',
                                  background: 'linear-gradient(135deg, var(--ds-green-500) 0%, var(--ds-green-500) 100%)',
                                  display: 'flex',
                                  alignItems: 'center',
                                  justifyContent: 'center',
                                  padding: 'var(--ds-space-4)',
                                  boxShadow: '0 4px 12px rgba(34, 197, 94, 0.3)',
                                  border: '2px solid rgba(255, 255, 255, 0.2)',
                                }}
                              >
                                <SafeIcon
                                  src={manualTriggerIcon?.default || manualTriggerIcon}
                                  alt='trigger-icon'
                                  width={36}
                                  height={36}
                                  style={{ filter: 'brightness(0) invert(1)' }}
                                />
                              </Box>
                            </Box>
                            <Typography
                              variant='h5'
                              sx={{
                                color: ds.gray[700],
                                fontWeight: 'var(--ds-font-weight-semibold)',
                                fontSize: 'var(--ds-text-heading)',
                                fontFamily: 'poppins',
                                letterSpacing: '-0.010em',
                              }}
                            >
                              How should your Automation begin?
                            </Typography>
                            <Typography
                              variant='body1'
                              sx={{ color: ds.gray[400], mb: 3, fontSize: 'var(--ds-text-body-lg)', mt: 'var(--ds-space-1)' }}
                            >
                              Pick a trigger to kick things off. You can always change it later
                            </Typography>
                            <Box sx={{ display: 'flex', gap: 'var(--ds-space-3)', justifyContent: 'center' }}>
                              {(
                                [
                                  {
                                    key: 'manual',
                                    label: 'Manual Trigger',
                                    description: 'Start automation manually',
                                    icon: workflowUserIcon?.default || workflowUserIcon,
                                    color: 'var(--ds-teal-500)',
                                  },
                                  {
                                    key: 'webhook',
                                    label: 'Webhook',
                                    description: 'HTTP endpoint trigger',
                                    icon: workflowWebhookIcon?.default || workflowWebhookIcon,
                                    color: 'var(--ds-amber-500)',
                                  },
                                  {
                                    key: 'schedule',
                                    label: 'Schedule',
                                    description: 'Time-based trigger',
                                    icon: workflowCalendarIcon?.default || workflowCalendarIcon,
                                    color: 'var(--ds-blue-500)',
                                  },
                                  {
                                    key: 'event',
                                    label: 'Event Trigger',
                                    description: 'Event-based trigger',
                                    icon: null,
                                    emoji: '\u26A1',
                                    color: 'var(--ds-amber-400)',
                                  },
                                  {
                                    key: 'optimization',
                                    label: 'Optimization',
                                    description: 'Triggered by new recommendations',
                                    icon: null,
                                    emoji: '\uD83D\uDCA1',
                                    color: 'var(--ds-purple-400)',
                                  },
                                ] as { key: string; label: string; description: string; icon: any; emoji?: string; color: string }[]
                              ).map((trigger) => (
                                <Box
                                  key={trigger.key}
                                  id={`wf-builder-trigger-option-${trigger.key}-card`}
                                  data-testid={`trigger-option-${trigger.key}`}
                                  onClick={() => {
                                    setNodes((nds) => {
                                      const newNode = addNode('triggers', trigger.key, nds);
                                      return [...nds, newNode];
                                    });
                                  }}
                                  sx={{
                                    display: 'flex',
                                    flexDirection: 'column',
                                    alignItems: 'center',
                                    padding: 'var(--ds-space-4) var(--ds-space-3)',
                                    borderRadius: 'var(--ds-radius-xl)',
                                    border: `1px solid color-mix(in srgb, ${trigger.color} 30%, transparent)`,
                                    backgroundColor: `color-mix(in srgb, ${trigger.color} 8%, transparent)`,
                                    cursor: 'pointer',
                                    transition: 'all 0.2s',
                                    width: '120px',
                                    '&:hover': {
                                      backgroundColor: `color-mix(in srgb, ${trigger.color} 15%, transparent)`,
                                      borderColor: `color-mix(in srgb, ${trigger.color} 50%, transparent)`,
                                      transform: 'translateY(-2px)',
                                      boxShadow: `0 4px 12px color-mix(in srgb, ${trigger.color} 20%, transparent)`,
                                    },
                                  }}
                                >
                                  <Box
                                    sx={{
                                      display: 'flex',
                                      alignItems: 'center',
                                      justifyContent: 'center',
                                      width: '44px',
                                      height: '44px',
                                      borderRadius: 'var(--ds-radius-lg)',
                                      backgroundColor: `color-mix(in srgb, ${trigger.color} 15%, transparent)`,
                                      border: `1px solid color-mix(in srgb, ${trigger.color} 30%, transparent)`,
                                      mb: 1,
                                    }}
                                  >
                                    {trigger.icon ? (
                                      <SafeIcon src={trigger.icon} alt={trigger.label} width={24} height={24} />
                                    ) : (
                                      <span style={{ fontSize: 'var(--ds-text-heading)' }}>{trigger.emoji}</span>
                                    )}
                                  </Box>
                                  <Typography
                                    sx={{
                                      fontSize: 'var(--ds-text-body)',
                                      fontWeight: 'var(--ds-font-weight-semibold)',
                                      color: ds.gray[700],
                                      fontFamily: 'poppins',
                                      lineHeight: 1.3,
                                    }}
                                  >
                                    {trigger.label}
                                  </Typography>
                                  <Typography
                                    sx={{
                                      fontSize: 'var(--ds-text-caption)',
                                      color: 'var(--ds-gray-600)',
                                      mt: 0.5,
                                      lineHeight: 1.3,
                                    }}
                                  >
                                    {trigger.description}
                                  </Typography>
                                </Box>
                              ))}
                            </Box>
                          </Box>
                        )}

                      {/* Guidance Message for workflows without triggers */}
                      {!isNewWorkflow && nodes.length > 0 && !nodes.some((node) => node.type === 'trigger') && (
                        <TriggerWarningMessage onAddTrigger={openSidebarWithTriggers} />
                      )}

                      {/* Execution Status Bar - Show during manual run; surfaces approval prompt + buttons */}
                      <ExecutionStatusBar
                        visible={isTestRunning}
                        completedTasks={
                          nodes.filter((n) => n.type === 'action' && (n.data.executionStatus === 'COMPLETED' || n.data.executionStatus === 'FAILED'))
                            .length
                        }
                        totalTasks={nodes.filter((n) => n.type === 'action').length}
                        pendingApprovals={nodes
                          .filter(
                            (n) =>
                              n.type === 'action' &&
                              n.data.taskConfig?.type === 'core.approval' &&
                              String(n.data.executionStatus ?? '').toUpperCase() === 'SCHEDULED'
                          )
                          .map((n) => {
                            const opts = Array.isArray(n.data.taskConfig?.config?.approval_options)
                              ? (n.data.taskConfig.config.approval_options as any[]).filter((o: any) => typeof o === 'string' && o.length > 0)
                              : [];
                            return { taskId: n.data.taskConfig.id || n.id, options: opts };
                          })}
                        onApprove={handleCompleteApproval}
                        approvalLoading={approvalLoading}
                      />

                      {/* Bottom Action Toolbar - Positioned within canvas - Only show when nodes exist */}
                      {nodes.length > 0 && (
                        <Box
                          sx={{
                            position: 'absolute',
                            bottom: '24px',
                            left: '50%',
                            transform: 'translateX(-50%)',
                            zIndex: 100,
                            backgroundColor: ds.background[100],
                            border: `1px solid ${ds.gray[200]}`,
                            borderRadius: 'var(--ds-radius-xl)',
                            padding: 'var(--ds-space-2) var(--ds-space-2)',
                            boxShadow: '0 -2px 16px rgba(0, 0, 0, 0.08)',
                            display: 'flex',
                            alignItems: 'center',
                            gap: 'var(--ds-space-2)',
                            whiteSpace: 'nowrap',
                            width: 'max-content',
                          }}
                        >
                          {/* Add Action Button */}
                          {canEdit && (
                            <Button
                              id='workflow-add-action-btn'
                              tone='secondary'
                              size='sm'
                              icon={<AddIcon sx={{ fontSize: 'var(--ds-text-title)' }} />}
                              onClick={() => setSidebarOpen(true)}
                            >
                              Add Action
                            </Button>
                          )}

                          {/* Divider */}
                          {canEdit && <Box sx={{ width: '1px', height: '28px', backgroundColor: ds.brand[200] }} />}

                          {/* Save draft / Publish Button.
                              - Create mode (isNewWorkflow=true): the first "save"
                                actually creates the workflow row AND snapshots v1,
                                so calling it "Save draft" misled users into
                                thinking nothing was being published. Show "Publish".
                              - Edit mode: keeps "Save draft" — the action only
                                persists the on-screen draft and does NOT snapshot
                                a new version; the explicit Publish button in the
                                top-right state strip is what produces a version. */}
                          {canEdit && (
                            <Tooltip
                              title={
                                isNewWorkflow
                                  ? 'Creates the workflow and saves v1 as a Paused live version. Activate it from the version status control once you are ready.'
                                  : 'Saves your edits as a draft. Publish to make them the live version that all triggers run.'
                              }
                            >
                              <span>
                                <Button
                                  id='workflow-save-btn'
                                  tone='secondary'
                                  size='sm'
                                  icon={<SafeIcon src={SaveIconOutline} alt={isNewWorkflow ? 'Publish' : 'Save draft'} width={16} height={16} />}
                                  onClick={handleSaveWorkflow}
                                >
                                  {isNewWorkflow ? 'Publish' : 'Save draft'}
                                </Button>
                              </span>
                            </Tooltip>
                          )}

                          {/* Publish + History moved to the top-right state strip (WorkflowStateStrip). */}

                          {/* Divider */}
                          <Box sx={{ width: '1px', height: '28px', backgroundColor: ds.brand[200] }} />

                          {/* Run split button: primary = "Run current" (draft snapshot, stays on editor).
                              Chevron menu = "Run live version" (jumps to executions tab on success). */}
                          {canEdit && !isNewWorkflow && (
                            <Box sx={{ display: 'flex', alignItems: 'center' }} data-testid='run-split-button'>
                              <Button
                                id='run-current-btn'
                                tone='ghost'
                                size='sm'
                                icon={<SafeIcon src={PlayIconBlue} alt='Run' width={16} height={16} />}
                                disabled={isTestRunning || isDryRunning}
                                loading={isTestRunning}
                                onClick={() => handleRunButtonClick('current')}
                              >
                                {isTestRunning ? 'Running...' : 'Run current'}
                              </Button>
                              <Button
                                id='run-variant-menu-btn'
                                composition='icon-only'
                                tone='ghost'
                                size='sm'
                                aria-label='More run options'
                                icon={<ArrowDropDownIcon sx={{ fontSize: 20 }} />}
                                disabled={isTestRunning || isDryRunning}
                                onClick={(e: React.MouseEvent<HTMLElement>) => setRunMenuAnchor(e.currentTarget)}
                              />
                              <Menu
                                anchorEl={runMenuAnchor}
                                open={Boolean(runMenuAnchor)}
                                onClose={() => setRunMenuAnchor(null)}
                                anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
                                transformOrigin={{ vertical: 'top', horizontal: 'right' }}
                              >
                                <MenuItem
                                  id='run-current-menu-item'
                                  data-testid='run-current-menu-item'
                                  onClick={() => {
                                    setRunMenuAnchor(null);
                                    handleRunButtonClick('current');
                                  }}
                                >
                                  <ListItemIcon>
                                    <PlayArrowIcon fontSize='small' />
                                  </ListItemIcon>
                                  <MuiListItemText primary='Run current' secondary='Run the unsaved/draft definition on screen' />
                                </MenuItem>
                                <MenuItem
                                  id='run-live-menu-item'
                                  data-testid='run-live-btn'
                                  disabled={!workflowDataRef.current?.live_version_id}
                                  onClick={() => {
                                    setRunMenuAnchor(null);
                                    handleRunButtonClick('live');
                                  }}
                                >
                                  <ListItemIcon>
                                    <PublishedWithChangesIcon fontSize='small' />
                                  </ListItemIcon>
                                  <MuiListItemText
                                    primary='Run live version'
                                    secondary={
                                      workflowDataRef.current?.live_version_id
                                        ? `Trigger the published live version${
                                            workflowDataRef.current?.live_version_number ? ` (v${workflowDataRef.current.live_version_number})` : ''
                                          }`
                                        : 'No live version published yet'
                                    }
                                  />
                                </MenuItem>
                              </Menu>
                            </Box>
                          )}

                          {/* Stop Button — visible only during an active run */}
                          {canEdit && isTestRunning && !isNewWorkflow && (
                            <Button
                              id='workflow-stop-btn'
                              tone='danger'
                              size='sm'
                              icon={<StopCircleOutlinedIcon sx={{ fontSize: 16 }} />}
                              onClick={handleCancelExecution}
                            >
                              Cancel
                            </Button>
                          )}

                          {/* Dry Run Button */}
                          {canEdit && (
                            <Button
                              id='workflow-dry-run-btn'
                              tone='secondary'
                              size='sm'
                              icon={<SafeIcon src={PlayIconBlue} alt='Dry Run' width={16} height={16} />}
                              disabled={isDryRunning || isTestRunning}
                              loading={isDryRunning}
                              onClick={handleDryRunButtonClick}
                            >
                              {isDryRunning ? 'Running...' : 'Dry Run'}
                            </Button>
                          )}

                          {/* Dry Run Stop Button — visible only during an active dry run */}
                          {canEdit && isDryRunning && (
                            <Button
                              id='workflow-dry-run-stop-btn'
                              tone='danger'
                              size='sm'
                              icon={<StopCircleOutlinedIcon sx={{ fontSize: 16 }} />}
                              onClick={handleCancelDryRun}
                            >
                              Cancel
                            </Button>
                          )}

                          {/* Divider */}
                          <Box sx={{ width: '1px', height: '28px', backgroundColor: ds.brand[200] }} />

                          {/* NuBi AI Chat Button */}
                          {nubiChatFeatureEnabled && (
                            <Button
                              id='workflow-nubi-chat-btn'
                              composition='icon-only'
                              tone='ghost'
                              size='sm'
                              tooltip={`Ask ${assistantName} AI`}
                              aria-label={`Ask ${assistantName} AI`}
                              icon={<SafeIcon src={getNubiIconUrl()} alt='AI Chat' width={22} height={22} />}
                              onClick={() => setShowNubiChat(!showNubiChat)}
                            />
                          )}

                          {/* Settings Button */}
                          <Button
                            id='workflow-settings-btn'
                            composition='icon-only'
                            tone='ghost'
                            size='sm'
                            tooltip='Automation Settings'
                            aria-label='Automation Settings'
                            icon={<SafeIcon src={SettingOutlineIconGrey} alt='Settings' width={20} height={20} />}
                            onClick={() => setIsSettingsModalOpen(true)}
                          />

                          {/* Prettify Button - re-runs auto layout */}
                          <Button
                            id='workflow-prettify-btn'
                            composition='icon-only'
                            tone='ghost'
                            size='sm'
                            tooltip='Prettify Layout'
                            tooltipPlacement='top'
                            aria-label='Prettify Layout'
                            icon={<AutoFixHighIcon sx={{ fontSize: 'var(--ds-text-heading)' }} />}
                            disabled={nodes.length === 0}
                            onClick={handlePrettifyLayout}
                          />

                          {/* The workflow-row status dropdown that used to live
                              here was removed in the V746 rollout. Status now
                              lives on workflow_versions.status (per-row toggle
                              in the version-history drawer + initial value in
                              the Publish dialog). workflows.status is a derived
                              mirror of the live version's status, so editing it
                              from the toolbar would either drift from the live
                              version or fight the same field two ways. */}
                        </Box>
                      )}
                    </>
                  )}
                </Box>
                {/* Toggle Button for JSON Panel */}
                <Box
                  id='workflow-json-panel-toggle'
                  onClick={() => setJsonPanelVisible(!jsonPanelVisible)}
                  sx={{
                    position: 'absolute',
                    right: jsonPanelVisible ? `${jsonWindowWidth}px` : '0',
                    top: '50%',
                    mr: '-22px',
                    transform: 'translateY(-50%) rotate(90deg)',
                    transformOrigin: 'center',
                    zIndex: 30,
                    backgroundColor: ds.background[100],
                    border: `1px solid ${ds.brand[200]}`,
                    borderRight: jsonPanelVisible ? `1px solid ${ds.brand[200]}` : 'none',
                    borderBottomLeftRadius: '8px',
                    borderTopRightRadius: 0,
                    borderBottomRightRadius: '8px',
                    padding: 'var(--ds-space-2) var(--ds-space-3)',
                    transition: isResizingRef.current ? 'none' : 'right 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
                    cursor: 'pointer',
                    display: 'flex',
                    alignItems: 'center',
                    gap: 'var(--ds-space-1)',
                    '&:hover': {
                      backgroundColor: 'rgb(249, 249, 249)',
                    },
                    boxShadow: '-2px 0 8px rgba(0, 0, 0, 0.1)',
                  }}
                >
                  <Typography
                    sx={{
                      fontSize: 'var(--ds-text-small)',
                      fontWeight: 'var(--ds-font-weight-medium)',
                      color: ds.gray[700],
                      whiteSpace: 'nowrap',
                    }}
                  >
                    JSON
                  </Typography>
                  {jsonPanelVisible ? (
                    <ChevronRightIcon fontSize='small' sx={{ color: ds.gray[700], transform: 'rotate(-90deg)' }} />
                  ) : (
                    <ChevronLeftIcon fontSize='small' sx={{ color: ds.gray[700], transform: 'rotate(-90deg)' }} />
                  )}
                </Box>
                {/* Right Side: JSON Editor (sliding panel) */}
                <Box
                  sx={{
                    position: 'absolute',
                    right: jsonPanelVisible ? '0' : `-${jsonWindowWidth}px`,
                    top: 0,
                    height: 'calc(100% - 8px)',
                    width: `${jsonWindowWidth}px`,
                    borderLeft: '1px solid rgb(226, 226, 227)',
                    backgroundColor: 'rgb(30, 30, 30)',
                    overflow: 'hidden',
                    transition: isResizingRef.current ? 'none' : 'right 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
                    zIndex: 20,
                  }}
                >
                  {/* Drag resize handle */}
                  <Suspense
                    fallback={
                      <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
                        <CircularProgress size={24} />
                      </Box>
                    }
                  >
                    <Box
                      onMouseDown={handleResizeMouseDown('json')}
                      sx={{
                        position: 'absolute',
                        left: -3,
                        top: 0,
                        width: '8px',
                        height: '100%',
                        cursor: 'col-resize',
                        zIndex: 25,
                        display: 'flex',
                        alignItems: 'center',
                        justifyContent: 'center',
                        '&::after': {
                          content: '""',
                          width: '2px',
                          height: '32px',
                          borderRadius: 'var(--ds-radius-sm)',
                          backgroundColor: 'rgba(0, 0, 0, 0.15)',
                          transition: 'background-color 0.15s, height 0.15s',
                        },
                        '&:hover::after': {
                          backgroundColor: 'rgba(59, 130, 246, 0.7)',
                          height: '48px',
                        },
                        '&:hover': {
                          backgroundColor: 'rgba(59, 130, 246, 0.08)',
                        },
                      }}
                    />
                    <JsonEditorTab
                      jsonText={jsonEditorText}
                      onChange={handleJsonChangeWithRevertClear}
                      onApply={applyJsonToWorkflow}
                      isValid={jsonValid}
                      parseError={jsonParseError}
                      hasUnsavedChanges={jsonHasUnsavedChanges}
                      disabled={loading || isApplyingJson}
                      canRevert={lastAppliedSource === 'llm' && jsonBeforeLlmApply !== ''}
                      onRevert={revertLastLlmApply}
                      isLoading={isApplyingJson}
                    />
                  </Suspense>
                </Box>
              </Box>
              {/* Action Details Sidebar - Only in Editor mode (lazy-loaded) */}
              {actionDetailsSidebarOpen && (
                <Suspense fallback={null}>
                  <ActionDetailsSidebar
                    open={actionDetailsSidebarOpen}
                    onClose={() => {
                      setActionDetailsSidebarOpen(false);
                      deselectAllNodes(setNodes);
                    }}
                    selectedActionType={selectedActionType}
                    nodes={nodes}
                    edges={edges}
                    onTaskDataChange={handleSidebarTaskDataChange}
                    onTaskConfigChange={handleSidebarTaskConfigChange}
                    taskDefinitions={taskDefinitions}
                    taskData={sidebarTaskData}
                    validationErrors={sidebarValidationErrors}
                    viewOnlyMode={false}
                    previousNodeOutputSchema={sidebarPreviousNodeOutputSchema}
                    accountId={accountId}
                    onRunPreviousSteps={handleRunPreviousSteps}
                    onDryRunToTask={handleDryRunToTask}
                    onRunTask={handleRunTask}
                    workflowInputs={workflowSettings.inputs}
                    workflowTimeout={workflowSettings.timeout}
                    currentWorkflowId={workflowData?.id || (workflowId !== 'new' ? workflowId : undefined)}
                    onRequestCloseWithUnsaved={(data) => {
                      const nodeId = sidebarSelectedNodeIdRef.current;
                      if (nodeId) {
                        setPendingDiscard({ nodeId, pendingData: data });
                        setActionDetailsSidebarOpen(false);
                      }
                    }}
                    pendingData={pendingDiscard?.pendingData ?? undefined}
                    onPendingDataConsumed={() => setPendingDiscard(null)}
                    onToggleDisable={(disable: boolean) => {
                      const selectedNode = nodes.find((node) => node.selected && (node.type === 'action' || node.type === 'switch'));
                      if (!selectedNode) return;
                      const result = disable ? disableTask(selectedNode.id, nodes, edges) : enableTask(selectedNode.id, nodes, edges);
                      setNodes(result.nodes);
                      setEdges(result.edges);
                    }}
                  />
                </Suspense>
              )}
              {/* Action sidebar close-confirmation (sibling, not nested) */}
              <Modal
                open={!!pendingDiscard && !actionDetailsSidebarOpen}
                handleClose={() => {
                  setActionDetailsSidebarOpen(true);
                }}
                title='You have unsaved changes'
                width='sm'
                contentStyles={{ padding: '0px' }}
                actionButtons={
                  <Box sx={{ display: 'flex', gap: 2, justifyContent: 'flex-end', p: 2 }}>
                    <Button id='action-close-confirm-cancel-btn' tone='secondary' size='md' onClick={() => setActionDetailsSidebarOpen(true)}>
                      Cancel
                    </Button>
                    <Button
                      id='action-close-confirm-discard-btn'
                      tone='danger'
                      size='md'
                      onClick={() => {
                        setPendingDiscard(null);
                        deselectAllNodes(setNodes);
                      }}
                    >
                      Discard changes
                    </Button>
                    <Button
                      id='action-close-confirm-save-btn'
                      tone='primary'
                      size='md'
                      onClick={() => {
                        if (pendingDiscard) {
                          handleSidebarTaskDataChange(pendingDiscard.pendingData);
                        }
                        setPendingDiscard(null);
                        deselectAllNodes(setNodes);
                      }}
                    >
                      Save changes
                    </Button>
                  </Box>
                }
              >
                <Box padding='24px'>
                  <Typography sx={{ fontSize: 'var(--ds-text-body-lg)', color: ds.gray[700] }}>
                    Save your edits to this action, or discard them and keep the previous configuration?
                  </Typography>
                </Box>
              </Modal>
              {/* Trigger Configuration Sidebar (lazy-loaded) */}
              {triggerConfigSidebarOpen && (
                <Suspense fallback={null}>
                  <TriggerConfigSidebar
                    open={triggerConfigSidebarOpen}
                    selectedNode={selectedNode}
                    onClose={() => closeTriggerConfigSidebar(setNodes)}
                    onSave={(nodeId, triggerConfig) => {
                      updateTriggerConfig(nodeId, triggerConfig, setNodes);
                    }}
                    accountId={accountId}
                    workflowData={workflowDataRef.current}
                    onRequestCloseWithUnsaved={(config) => {
                      const nodeId = selectedNode?.id;
                      if (nodeId) {
                        setPendingTriggerDiscard({ nodeId, pendingConfig: config });
                        setTriggerConfigSidebarOpen(false);
                      }
                    }}
                    pendingConfig={pendingTriggerDiscard?.pendingConfig ?? null}
                    onPendingConfigConsumed={() => setPendingTriggerDiscard(null)}
                  />
                </Suspense>
              )}
              {/* Trigger sidebar close-confirmation (sibling, not nested) */}
              <Modal
                open={!!pendingTriggerDiscard && !triggerConfigSidebarOpen}
                handleClose={() => {
                  setTriggerConfigSidebarOpen(true);
                }}
                title='You have unsaved changes'
                width='sm'
                contentStyles={{ padding: '0px' }}
                actionButtons={
                  <Box sx={{ display: 'flex', gap: 2, justifyContent: 'flex-end', p: 2 }}>
                    <Button id='trigger-close-confirm-cancel-btn' tone='secondary' size='md' onClick={() => setTriggerConfigSidebarOpen(true)}>
                      Cancel
                    </Button>
                    <Button
                      id='trigger-close-confirm-discard-btn'
                      tone='danger'
                      size='md'
                      onClick={() => {
                        setPendingTriggerDiscard(null);
                        closeTriggerConfigSidebar(setNodes);
                      }}
                    >
                      Discard changes
                    </Button>
                    <Button
                      id='trigger-close-confirm-save-btn'
                      tone='primary'
                      size='md'
                      onClick={() => {
                        if (pendingTriggerDiscard) {
                          updateTriggerConfig(pendingTriggerDiscard.nodeId, pendingTriggerDiscard.pendingConfig, setNodes);
                        }
                        setPendingTriggerDiscard(null);
                        closeTriggerConfigSidebar(setNodes);
                      }}
                    >
                      Save changes
                    </Button>
                  </Box>
                }
              >
                <Box padding='24px'>
                  <Typography sx={{ fontSize: 'var(--ds-text-body-lg)', color: ds.gray[700] }}>
                    Save your edits to this trigger, or discard them and keep the previous configuration?
                  </Typography>
                </Box>
              </Modal>
            </>
          )}

          {/* Test Task Response Modal (lazy-loaded) */}
          {testResponseDialog.open && (
            <Suspense fallback={null}>
              <TestResponseModal
                open={testResponseDialog.open}
                onClose={() => setTestResponseDialog({ open: false, taskType: '', responseData: null })}
                taskType={testResponseDialog.taskType}
                responseData={testResponseDialog.responseData}
              />
            </Suspense>
          )}

          {/* Dry Run Result Modal (lazy-loaded) */}
          {showDryRunModal && (
            <Suspense fallback={null}>
              <DryRunResultModal
                open={showDryRunModal}
                onClose={() => setShowDryRunModal(false)}
                result={dryRunResult}
                draftVersionNumber={workflowData?.draft_version_number ?? undefined}
              />
            </Suspense>
          )}

          {/* Trigger Input Modal for Run/Dry Run (lazy-loaded) */}
          {triggerModalOpen && (
            <Suspense fallback={null}>
              <TriggerWorkflowModal
                open={triggerModalOpen}
                onClose={() => {
                  setTriggerModalOpen(false);
                  setTriggerModalLoading(false);
                }}
                workflowName={workflowData?.name || 'Automation'}
                triggerType={getTriggerType()}
                defaultInputs={getDefaultTriggerInputs()}
                inputSchema={getInputSchema()}
                onTrigger={handleTriggerFromModal}
                loading={triggerModalLoading}
                liveVersionNumber={workflowData?.live_version_number ?? undefined}
                liveVersionName={workflowData?.live_version_name ?? undefined}
                draftVersionNumber={workflowData?.draft_version_number ?? undefined}
                runVariant={runVariant}
              />
            </Suspense>
          )}

          {/* Workflow Settings Modal (lazy-loaded) */}
          {isSettingsModalOpen && (
            <Suspense fallback={null}>
              <WorkflowSettingsModal
                open={isSettingsModalOpen}
                onClose={() => setIsSettingsModalOpen(false)}
                onSave={setWorkflowSettings}
                initialSettings={workflowSettings}
                taskTimeouts={nodes.filter((n) => n.data?.taskConfig?.timeout).map((n) => n.data.taskConfig.timeout as string)}
              />
            </Suspense>
          )}

          {/* Version History Drawer */}
          <Drawer anchor='right' open={historyOpen} onClose={() => setHistoryOpen(false)}>
            <Box sx={{ width: 420, display: 'flex', flexDirection: 'column', height: '100%' }}>
              <Box sx={{ p: 2, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
                <Typography variant='h6' sx={{ fontWeight: 'var(--ds-font-weight-semibold)' }}>
                  Version History
                </Typography>
                <Button
                  composition='icon-only'
                  tone='ghost'
                  size='sm'
                  aria-label='Close history'
                  icon={<CloseIcon sx={{ fontSize: 18 }} />}
                  onClick={() => setHistoryOpen(false)}
                />
              </Box>
              <Divider />
              <Box sx={{ flex: 1, overflowY: 'auto' }}>
                {loadingVersions ? (
                  <Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}>
                    <CircularProgress size={24} />
                  </Box>
                ) : versions.length === 0 ? (
                  <Box sx={{ p: 3 }}>
                    <Typography variant='body2' color='text.secondary'>
                      No published versions yet. Click Publish to snapshot the current draft.
                    </Typography>
                  </Box>
                ) : (
                  // Row layout replaced MUI ListItem + secondaryAction because
                  // secondaryAction is positioned absolutely on the right and
                  // its vertical extent does not contribute to the parent row
                  // height. With the Select + Make Live + Checkout stack, the
                  // actions overflowed downward into the next row visually
                  // (see V8RiiM/iNeTAh screenshots). Switching to an explicit
                  // two-column Box flex layout makes the actions column size
                  // the row, so each row's height = max(text, actions) and
                  // adjacent rows can't collide.
                  <Box>
                    {versions.map((v, idx) => {
                      const author = v.created_by_user?.display_name || v.created_by || 'unknown';
                      const when = v.created_at ? new Date(v.created_at).toLocaleString() : '';
                      const sourceLabel = v.source === 'restore' && v.restored_from_version ? `restored from v${v.restored_from_version}` : v.source;
                      return (
                        <Box
                          key={v.id}
                          sx={{
                            display: 'flex',
                            alignItems: 'flex-start',
                            justifyContent: 'space-between',
                            gap: 2,
                            px: 2,
                            py: 2,
                            borderBottom: idx === versions.length - 1 ? 'none' : '1px solid var(--ds-brand-150)',
                          }}
                        >
                          <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, minWidth: 0, flex: 1 }}>
                            <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
                              <DsChip size='xs' variant='tag' tone='neutral'>{`v${v.version_number}`}</DsChip>
                              {v.is_live && (
                                <DsChip size='xs' variant='status' tone='success'>
                                  Live
                                </DsChip>
                              )}
                              {v.name && (
                                <DsChip size='xs' variant='tag' tone='neutral'>
                                  {v.name}
                                </DsChip>
                              )}
                              <DsChip size='xs' variant='tag' tone='info'>
                                {sourceLabel}
                              </DsChip>
                            </Box>
                            {v.description && (
                              <Typography variant='caption' sx={{ fontStyle: 'italic', color: 'var(--ds-gray-700)' }}>
                                {v.description}
                              </Typography>
                            )}
                            <Typography variant='caption' sx={{ color: 'var(--ds-gray-700)' }}>
                              {when}
                            </Typography>
                            <Typography variant='caption' sx={{ color: 'var(--ds-gray-600)' }}>
                              by {author}
                            </Typography>
                          </Box>
                          <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, alignItems: 'stretch', flexShrink: 0, minWidth: 130 }}>
                            {/* Per-version status dropdown (DS Select). Mutating
                                any version updates workflow_versions.status; if
                                the row is live the DAO also mirrors it onto
                                workflows.status in the same tx. clearable=false
                                because status must always have a value — DS
                                Select otherwise renders an inline clear (✕). */}
                            {/* All three row controls (Select, Make Live, Checkout)
                                cross-disable on any of the three in-flight markers.
                                Each mutation reloads workflow data or live pointer,
                                so concurrent clicks would race on stale UI state. */}
                            <Select
                              size='sm'
                              value={v.status ?? 'PAUSED'}
                              onChange={(next) => handleChangeVersionStatus(v.version_number, next as 'ACTIVE' | 'PAUSED' | 'INACTIVE')}
                              disabled={statusUpdatingVersionNumber !== null || settingLive || restoring}
                              clearable={false}
                              options={[
                                { value: 'ACTIVE', label: 'Active' },
                                { value: 'PAUSED', label: 'Paused' },
                                { value: 'INACTIVE', label: 'Inactive' },
                              ]}
                              data-testid={`workflow-version-status-select-v${v.version_number}`}
                            />
                            {!v.is_live && (
                              <Button
                                id={`workflow-make-live-v${v.version_number}-btn`}
                                tone='primary'
                                size='sm'
                                onClick={() => setConfirmLiveVersion(v)}
                                disabled={statusUpdatingVersionNumber !== null || settingLive || restoring}
                              >
                                Make Live
                              </Button>
                            )}
                            <Button
                              id={`workflow-restore-v${v.version_number}-btn`}
                              tone='secondary'
                              size='sm'
                              onClick={() => setConfirmRestoreVersion(v)}
                              disabled={statusUpdatingVersionNumber !== null || settingLive || restoring}
                            >
                              Checkout
                            </Button>
                          </Box>
                        </Box>
                      );
                    })}
                  </Box>
                )}
              </Box>
            </Box>
          </Drawer>

          {/* Restore Confirmation Dialog */}
          <Modal
            open={!!confirmRestoreVersion}
            handleClose={() => (restoring ? undefined : setConfirmRestoreVersion(null))}
            width='sm'
            title={`Checkout version ${confirmRestoreVersion?.version_number ?? ''}?`}
            actionButtons={
              <Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1, p: 2 }}>
                <Button tone='secondary' size='md' onClick={() => setConfirmRestoreVersion(null)} disabled={restoring}>
                  Cancel
                </Button>
                <Button
                  id='workflow-restore-confirm-btn'
                  tone='primary'
                  size='md'
                  onClick={handleConfirmRestore}
                  disabled={restoring}
                  loading={restoring}
                >
                  {restoring ? 'Checking out…' : 'Checkout'}
                </Button>
              </Box>
            }
          >
            <Box sx={{ p: 'var(--ds-space-4) 0' }}>
              <Typography variant='body2' sx={{ color: ds.gray[700] }}>
                Your current draft will be replaced with the contents of v{confirmRestoreVersion?.version_number}. The live version that runs
                executions is NOT changed. Publish again to snapshot the restored draft as a new version.
              </Typography>
            </Box>
          </Modal>

          {/* Publish Dialog */}
          <Modal
            open={publishDialogOpen}
            handleClose={() => (publishing ? undefined : setPublishDialogOpen(false))}
            width='sm'
            title='Publish workflow version'
            actionButtons={
              <Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1, p: 2 }}>
                <Button tone='secondary' size='md' onClick={() => setPublishDialogOpen(false)} disabled={publishing}>
                  Cancel
                </Button>
                <Button
                  id='workflow-publish-confirm-btn'
                  tone='primary'
                  size='md'
                  onClick={handleConfirmPublish}
                  disabled={publishing}
                  loading={publishing}
                >
                  {publishing ? 'Publishing…' : 'Publish'}
                </Button>
              </Box>
            }
          >
            <Box sx={{ p: 'var(--ds-space-4) 0' }}>
              <Typography variant='body2' sx={{ mb: 2, color: ds.gray[700] }}>
                Snapshot the current draft as a new immutable version. Optionally give it a label and a short changelog. New executions will run this
                version if you mark it live.
              </Typography>
              <Box data-testid='workflow-publish-name-input' sx={{ mb: 1.5 }}>
                <Input
                  size='sm'
                  label='Name (optional)'
                  placeholder='e.g. release-2026-05-18'
                  value={publishName}
                  onChange={setPublishName}
                  disabled={publishing}
                />
              </Box>
              <Box data-testid='workflow-publish-description-input'>
                <Input
                  size='sm'
                  label='Description (optional)'
                  placeholder='What changed in this version?'
                  value={publishDescription}
                  onChange={setPublishDescription}
                  disabled={publishing}
                  type='textarea'
                  minRows={2}
                />
              </Box>
              {/* Per-version status (V746). Default PAUSED so the user has to
                  actively choose ACTIVE — matches the backend default and
                  prevents surprise auto-fires on publish. When setLive is on,
                  the chosen status also mirrors onto workflows.status via
                  DAO.SetLiveVersion in a single tx. The version-history drawer
                  exposes the same Select per row for after-the-fact changes. */}
              <Box data-testid='workflow-publish-status-select' sx={{ mt: 1.5 }}>
                <Select
                  size='sm'
                  label='Version status'
                  help='Active = all triggers fire. Paused = manual only. Inactive = blocked. Change later from the version history drawer.'
                  value={publishStatus}
                  onChange={(next) => setPublishStatus(next as 'ACTIVE' | 'PAUSED' | 'INACTIVE')}
                  disabled={publishing}
                  clearable={false}
                  options={[
                    { value: 'ACTIVE', label: 'Active' },
                    { value: 'PAUSED', label: 'Paused' },
                    { value: 'INACTIVE', label: 'Inactive' },
                  ]}
                />
              </Box>
              <FormControlLabel
                sx={{ mt: 1 }}
                control={
                  <Checkbox
                    checked={publishSetLive}
                    onChange={(e) => setPublishSetLive(e.target.checked)}
                    disabled={publishing}
                    data-testid='workflow-publish-setlive-checkbox'
                  />
                }
                label='Make this version live (new executions will run it)'
              />
            </Box>
          </Modal>

          {/* Make-Live Confirmation Dialog */}
          <Modal
            open={!!confirmLiveVersion}
            handleClose={() => (settingLive ? undefined : setConfirmLiveVersion(null))}
            width='sm'
            title={`Make v${confirmLiveVersion?.version_number ?? ''} the live version?`}
            actionButtons={
              <Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1, p: 2 }}>
                <Button tone='secondary' size='md' onClick={() => setConfirmLiveVersion(null)} disabled={settingLive}>
                  Cancel
                </Button>
                <Button
                  id='workflow-make-live-confirm-btn'
                  tone='primary'
                  size='md'
                  onClick={handleConfirmMakeLive}
                  disabled={settingLive}
                  loading={settingLive}
                >
                  {settingLive ? 'Switching…' : 'Make Live'}
                </Button>
              </Box>
            }
          >
            <Box sx={{ p: 'var(--ds-space-4) 0' }}>
              <Typography variant='body2' sx={{ color: ds.gray[700] }}>
                New executions will run v{confirmLiveVersion?.version_number}. Your current draft is preserved and remains separate — switching the
                live pointer does not modify what you&apos;re editing.
              </Typography>
            </Box>
          </Modal>

          {/* Exit-confirmation modal. Two variants share the same component so
              the user reads copy that matches what they're actually about to
              lose. The unpublished variant replaces the previous opt-in
              snackbar that users often missed; the unsaved variant is the
              legacy canvas-edits warning. */}
          <Modal
            open={showUnsavedChangesDialog}
            title={exitVariant === 'unpublished' ? 'Unpublished changes' : 'Unsaved changes'}
            actionButtons={
              <Box sx={{ display: 'flex', gap: 2, justifyContent: 'flex-end', p: 2 }}>
                <Button id='workflow-unsaved-cancel-btn' tone='secondary' size='md' onClick={handleCancelNavigation} disabled={loading}>
                  Stay
                </Button>
                <Button id='workflow-unsaved-leave-btn' tone='primary' size='md' onClick={handleConfirmNavigation} disabled={loading}>
                  Leave page
                </Button>
              </Box>
            }
            handleClose={handleCancelNavigation}
          >
            <Box padding={'24px'}>
              {exitVariant === 'unpublished' ? (
                <Text
                  value={
                    workflowDataRef.current?.live_version_number
                      ? `Your draft is saved but not published. All triggers will keep running Live version: v${workflowDataRef.current.live_version_number}, not your draft. Leave anyway?`
                      : 'Your draft is saved but not published. All triggers will keep running the current live version, not your draft. Leave anyway?'
                  }
                />
              ) : (
                <Text value={'You have unsaved canvas edits. Leaving will lose them. Are you sure you want to leave?'} />
              )}
            </Box>
          </Modal>
        </Box>
      </Box>
    </Box>
  );
};

export default WorkflowBuilderNoteBook;
