import type { MouseEvent } from 'react'
import type {
  NodeDragHandler,
  NodeMouseHandler,
  OnConnect,
  OnConnectEnd,
  OnConnectStart,
  ResizeParamsWithDirection,
} from 'reactflow'
import type { PluginDefaultValue } from '../block-selector/types'
import type { IterationNodeType } from '../nodes/iteration/types'
import type { LoopNodeType } from '../nodes/loop/types'
import type { VariableAssignerNodeType } from '../nodes/variable-assigner/types'
import type { Edge, Node, OnNodeAdd } from '../types'
import type { RAGPipelineVariables } from '@/models/pipeline'
import { toast } from '@langgenius/dify-ui/toast'
import { useSuspenseQuery } from '@tanstack/react-query'
import { produce } from 'immer'
import { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
  getConnectedEdges,
  getOutgoers,
  useReactFlow,
} from 'reactflow'
import { systemFeaturesQueryOptions } from '@/service/system-features'
import { collaborationManager } from '../collaboration/core/collaboration-manager'
import {
  CUSTOM_EDGE,
  ITERATION_CHILDREN_Z_INDEX,
  ITERATION_PADDING,
  LOOP_CHILDREN_Z_INDEX,
  LOOP_PADDING,
  NODE_WIDTH_X_OFFSET,
  X_OFFSET,
  Y_OFFSET,
} from '../constants'
import { getNodeUsedVars } from '../nodes/_base/components/variable/utils'
import { CUSTOM_ITERATION_START_NODE } from '../nodes/iteration-start/constants'
import { useNodeIterationInteractions } from '../nodes/iteration/use-interactions'
import { CUSTOM_LOOP_START_NODE } from '../nodes/loop-start/constants'
import { useNodeLoopInteractions } from '../nodes/loop/use-interactions'
import { CUSTOM_NOTE_NODE } from '../note-node/constants'
import { useWorkflowStore } from '../store'
import { BlockEnum, ControlMode, isTriggerNode } from '../types'

import {
  generateNewNode,
  genNewNodeTitleFromOld,
  getNestedNodePosition,
  getNodeCustomTypeByNodeDataType,
  getNodesConnectedSourceOrTargetHandleIdsMap,
  getTopLeftNodePosition,
  isClipboardEdgeStructurallyValid,
  isClipboardNodeStructurallyValid,
  isClipboardValueCompatibleWithDefault,
  readWorkflowClipboard,
  sanitizeClipboardValueByDefault,
  writeWorkflowClipboard,
} from '../utils'
import { useWorkflowHistoryStore } from '../workflow-history-store'
import { useAutoGenerateWebhookUrl } from './use-auto-generate-webhook-url'
import { useCollaborativeWorkflow } from './use-collaborative-workflow'
import { useHelpline } from './use-helpline'
import useInspectVarsCrud from './use-inspect-vars-crud'
import { useNodesMetaData } from './use-nodes-meta-data'
import { useNodesSyncDraft } from './use-nodes-sync-draft'
import {
  useNodesReadOnly,
  useWorkflow,
  useWorkflowReadOnly,
} from './use-workflow'
import {
  useWorkflowHistory,
  WorkflowHistoryEvent,
} from './use-workflow-history'

// Entry node deletion restriction has been removed to allow empty workflows

// Entry node (Start/Trigger) wrapper offsets for alignment
// Must match the values in use-helpline.ts
const ENTRY_NODE_WRAPPER_OFFSET = {
  x: 0,
  y: 21, // Adjusted based on visual testing feedback
} as const

const pruneClipboardNodesWithFilteredAncestors = (
  sourceNodes: Node[],
  candidateNodes: Node[],
): Node[] => {
  const candidateNodeIds = new Set(candidateNodes.map(node => node.id))
  const filteredRootIds = sourceNodes
    .filter(node => !candidateNodeIds.has(node.id))
    .map(node => node.id)

  if (!filteredRootIds.length)
    return candidateNodes

  const childrenByParent = new Map<string, string[]>()
  sourceNodes.forEach((node) => {
    if (!node.parentId)
      return

    const children = childrenByParent.get(node.parentId) ?? []
    children.push(node.id)
    childrenByParent.set(node.parentId, children)
  })

  const filteredNodeIds = new Set(filteredRootIds)
  const queue = [...filteredRootIds]

  while (queue.length) {
    const currentNodeId = queue.shift()!
    const children = childrenByParent.get(currentNodeId) ?? []
    children.forEach((childId) => {
      if (filteredNodeIds.has(childId))
        return

      filteredNodeIds.add(childId)
      queue.push(childId)
    })
  }

  return candidateNodes.filter(node => !filteredNodeIds.has(node.id))
}

const getUniquePastedNodeTitle = (
  sourceTitle: string,
  reservedTitles: Set<string>,
) => {
  let titleCandidate = sourceTitle

  while (reservedTitles.has(titleCandidate))
    titleCandidate = genNewNodeTitleFromOld(titleCandidate)

  reservedTitles.add(titleCandidate)
  return titleCandidate
}

const isNoteLinkClickTarget = (target: EventTarget | null, node: Node) => {
  return node.type === CUSTOM_NOTE_NODE
    && target instanceof HTMLElement
    && !!target.closest('.note-editor-theme_link')
}

export const useNodesInteractions = () => {
  const { t } = useTranslation()
  const { data: appDslVersion } = useSuspenseQuery({
    ...systemFeaturesQueryOptions(),
    select: s => s.app_dsl_version,
  })
  const collaborativeWorkflow = useCollaborativeWorkflow()
  const workflowStore = useWorkflowStore()
  const reactflow = useReactFlow()
  const { store: workflowHistoryStore } = useWorkflowHistoryStore()
  const { handleSyncWorkflowDraft } = useNodesSyncDraft()
  const { getAfterNodesInSameBranch } = useWorkflow()
  const { getNodesReadOnly } = useNodesReadOnly()
  const { getWorkflowReadOnly } = useWorkflowReadOnly()
  const { handleSetHelpline } = useHelpline()
  const { handleNodeIterationChildDrag, handleNodeIterationChildrenCopy }
    = useNodeIterationInteractions()
  const { handleNodeLoopChildDrag, handleNodeLoopChildrenCopy }
    = useNodeLoopInteractions()
  const dragNodeStartPosition = useRef({ x: 0, y: 0 } as {
    x: number
    y: number
  })
  const { nodesMap: nodesMetaDataMap } = useNodesMetaData()

  const {
    saveStateToHistory,
    undo,
    redo,
  } = useWorkflowHistory()
  const autoGenerateWebhookUrl = useAutoGenerateWebhookUrl()

  const handleNodeDragStart = useCallback<NodeDragHandler>(
    (_, node) => {
      workflowStore.setState({ nodeAnimation: false })

      if (getNodesReadOnly())
        return

      if (
        node.type === CUSTOM_ITERATION_START_NODE
        || node.type === CUSTOM_NOTE_NODE
      ) {
        return
      }

      if (
        node.type === CUSTOM_LOOP_START_NODE
        || node.type === CUSTOM_NOTE_NODE
      ) {
        return
      }

      dragNodeStartPosition.current = {
        x: node.position.x,
        y: node.position.y,
      }
    },
    [workflowStore, getNodesReadOnly],
  )

  const handleNodeDrag = useCallback<NodeDragHandler>(
    (e, node: Node) => {
      if (getNodesReadOnly())
        return

      if (node.type === CUSTOM_ITERATION_START_NODE)
        return

      if (node.type === CUSTOM_LOOP_START_NODE)
        return

      e.stopPropagation()

      const { nodes, setNodes } = collaborativeWorkflow.getState()

      const { restrictPosition } = handleNodeIterationChildDrag(node)
      const { restrictPosition: restrictLoopPosition }
        = handleNodeLoopChildDrag(node)

      const { showHorizontalHelpLineNodes, showVerticalHelpLineNodes }
        = handleSetHelpline(node)
      const showHorizontalHelpLineNodesLength
        = showHorizontalHelpLineNodes.length
      const showVerticalHelpLineNodesLength = showVerticalHelpLineNodes.length

      const newNodes = produce(nodes, (draft) => {
        const currentNode = draft.find(n => n.id === node.id)!

        // Check if current dragging node is an entry node
        const isCurrentEntryNode = isTriggerNode(node.data.type as any) || node.data.type === BlockEnum.Start

        // X-axis alignment with offset consideration
        if (showVerticalHelpLineNodesLength > 0) {
          const targetNode = showVerticalHelpLineNodes[0]
          const isTargetEntryNode = isTriggerNode(targetNode!.data.type as any) || targetNode!.data.type === BlockEnum.Start

          // Calculate the wrapper position needed to align the inner nodes
          // Target inner position = target.position + target.offset
          // Current inner position should equal target inner position
          // So: current.position + current.offset = target.position + target.offset
          // Therefore: current.position = target.position + target.offset - current.offset
          const targetOffset = isTargetEntryNode ? ENTRY_NODE_WRAPPER_OFFSET.x : 0
          const currentOffset = isCurrentEntryNode ? ENTRY_NODE_WRAPPER_OFFSET.x : 0
          currentNode.position.x = targetNode!.position.x + targetOffset - currentOffset
        }
        else if (restrictPosition.x !== undefined) {
          currentNode.position.x = restrictPosition.x
        }
        else if (restrictLoopPosition.x !== undefined) {
          currentNode.position.x = restrictLoopPosition.x
        }
        else {
          currentNode.position.x = node.position.x
        }

        // Y-axis alignment with offset consideration
        if (showHorizontalHelpLineNodesLength > 0) {
          const targetNode = showHorizontalHelpLineNodes[0]
          const isTargetEntryNode = isTriggerNode(targetNode!.data.type as any) || targetNode!.data.type === BlockEnum.Start

          const targetOffset = isTargetEntryNode ? ENTRY_NODE_WRAPPER_OFFSET.y : 0
          const currentOffset = isCurrentEntryNode ? ENTRY_NODE_WRAPPER_OFFSET.y : 0
          currentNode.position.y = targetNode!.position.y + targetOffset - currentOffset
        }
        else if (restrictPosition.y !== undefined) {
          currentNode.position.y = restrictPosition.y
        }
        else if (restrictLoopPosition.y !== undefined) {
          currentNode.position.y = restrictLoopPosition.y
        }
        else {
          currentNode.position.y = node.position.y
        }
      })
      setNodes(newNodes)
    },
    [getNodesReadOnly, collaborativeWorkflow, handleNodeIterationChildDrag, handleNodeLoopChildDrag, handleSetHelpline],
  )

  const handleNodeDragStop = useCallback<NodeDragHandler>(
    (_, node) => {
      const { setHelpLineHorizontal, setHelpLineVertical }
        = workflowStore.getState()

      if (getNodesReadOnly())
        return

      const { x, y } = dragNodeStartPosition.current
      if (!(x === node.position.x && y === node.position.y)) {
        setHelpLineHorizontal()
        setHelpLineVertical()
        handleSyncWorkflowDraft()

        if (x !== 0 && y !== 0) {
          // selecting a note will trigger a drag stop event with x and y as 0
          saveStateToHistory(WorkflowHistoryEvent.NodeDragStop, {
            nodeId: node.id,
          })
        }
      }
    },
    [
      workflowStore,
      getNodesReadOnly,
      saveStateToHistory,
      handleSyncWorkflowDraft,
    ],
  )

  const handleNodeEnter = useCallback<NodeMouseHandler>(
    (_, node) => {
      if (getNodesReadOnly())
        return

      if (
        node.type === CUSTOM_NOTE_NODE
        || node.type === CUSTOM_ITERATION_START_NODE
      ) {
        return
      }

      if (
        node.type === CUSTOM_LOOP_START_NODE
        || node.type === CUSTOM_NOTE_NODE
      ) {
        return
      }

      const { nodes, edges, setNodes, setEdges } = collaborativeWorkflow.getState()
      const {
        connectingNodePayload,
        setEnteringNodePayload,
      } = workflowStore.getState()
      if (connectingNodePayload) {
        if (connectingNodePayload.nodeId === node.id)
          return
        const connectingNode: Node = nodes.find(
          n => n.id === connectingNodePayload.nodeId,
        )!
        const sameLevel = connectingNode.parentId === node.parentId

        if (sameLevel) {
          setEnteringNodePayload({
            nodeId: node.id,
            nodeData: node.data as VariableAssignerNodeType,
          })
          const fromType = connectingNodePayload.handleType

          const newNodes = produce(nodes, (draft) => {
            draft.forEach((n) => {
              if (
                n.id === node.id
                && fromType === 'source'
                && (node.data.type === BlockEnum.VariableAssigner
                  || node.data.type === BlockEnum.VariableAggregator)
              ) {
                if (!node.data.advanced_settings?.group_enabled)
                  n.data._isEntering = true
              }
              if (
                n.id === node.id
                && fromType === 'target'
                && (connectingNode.data.type === BlockEnum.VariableAssigner
                  || connectingNode.data.type === BlockEnum.VariableAggregator)
                && node.data.type !== BlockEnum.IfElse
                && node.data.type !== BlockEnum.QuestionClassifier
                && node.data.type !== BlockEnum.HumanInput
              ) {
                n.data._isEntering = true
              }
            })
          })
          setNodes(newNodes, false)
        }
      }
      const newEdges = produce(edges, (draft) => {
        const connectedEdges = getConnectedEdges([node], edges)

        connectedEdges.forEach((edge) => {
          const currentEdge = draft.find(e => e.id === edge.id)
          if (currentEdge)
            currentEdge.data._connectedNodeIsHovering = true
        })
      })
      setEdges(newEdges, false)
    },
    [collaborativeWorkflow, workflowStore, getNodesReadOnly],
  )

  const handleNodeLeave = useCallback<NodeMouseHandler>(
    (_, node) => {
      if (getNodesReadOnly())
        return

      if (
        node.type === CUSTOM_NOTE_NODE
        || node.type === CUSTOM_ITERATION_START_NODE
      ) {
        return
      }

      if (
        node.type === CUSTOM_NOTE_NODE
        || node.type === CUSTOM_LOOP_START_NODE
      ) {
        return
      }

      const { setEnteringNodePayload } = workflowStore.getState()
      setEnteringNodePayload(undefined)
      const { nodes, setNodes, edges, setEdges } = collaborativeWorkflow.getState()
      const newNodes = produce(nodes, (draft) => {
        draft.forEach((node) => {
          node.data._isEntering = false
        })
      })
      setNodes(newNodes, false)
      const newEdges = produce(edges, (draft) => {
        draft.forEach((edge) => {
          edge.data._connectedNodeIsHovering = false
        })
      })
      setEdges(newEdges, false)
    },
    [collaborativeWorkflow, workflowStore, getNodesReadOnly],
  )

  const handleNodeSelect = useCallback(
    (
      nodeId: string,
      cancelSelection?: boolean,
      initShowLastRunTab?: boolean,
    ) => {
      if (initShowLastRunTab)
        workflowStore.setState({ initShowLastRunTab: true })
      const { nodes, setNodes, edges, setEdges } = collaborativeWorkflow.getState()

      const newNodes = produce(nodes, (draft) => {
        draft.forEach((node) => {
          const selected = node.id === nodeId && !cancelSelection
          node.selected = selected
          node.data.selected = selected
        })
      })
      setNodes(newNodes, false)

      const connectedEdges = getConnectedEdges(
        [{ id: nodeId } as Node],
        edges,
      ).map(edge => edge.id)
      const newEdges = produce(edges, (draft) => {
        draft.forEach((edge) => {
          if (connectedEdges.includes(edge.id)) {
            edge.data = {
              ...edge.data,
              _connectedNodeIsSelected: !cancelSelection,
            }
          }
          else {
            edge.data = {
              ...edge.data,
              _connectedNodeIsSelected: false,
            }
          }
        })
      })
      setEdges(newEdges, false)
    },
    [collaborativeWorkflow],
  )

  const handleNodeClick = useCallback<NodeMouseHandler>(
    (event, node) => {
      const { controlMode } = workflowStore.getState()
      if (controlMode === ControlMode.Comment)
        return
      if (isNoteLinkClickTarget(event.target, node))
        return
      if (node.type === CUSTOM_ITERATION_START_NODE)
        return
      if (node.type === CUSTOM_LOOP_START_NODE)
        return
      if (node.data.type === BlockEnum.DataSourceEmpty)
        return
      handleNodeSelect(node.id)
    },
    [handleNodeSelect, workflowStore],
  )

  const handleNodeConnect = useCallback<OnConnect>(
    ({ source, sourceHandle, target, targetHandle }) => {
      if (source === target)
        return
      if (getNodesReadOnly())
        return

      const { nodes, edges, setNodes, setEdges } = collaborativeWorkflow.getState()
      const targetNode = nodes.find(node => node.id === target!)
      const sourceNode = nodes.find(node => node.id === source!)

      if (targetNode?.parentId !== sourceNode?.parentId)
        return

      if (
        sourceNode?.type === CUSTOM_NOTE_NODE
        || targetNode?.type === CUSTOM_NOTE_NODE
      ) {
        return
      }

      if (
        edges.some(edge =>
          edge.source === source
          && edge.sourceHandle === sourceHandle
          && edge.target === target
          && edge.targetHandle === targetHandle)
      ) {
        return
      }

      const parendNode = nodes.find(node => node.id === targetNode?.parentId)
      const isInIteration
        = parendNode && parendNode.data.type === BlockEnum.Iteration
      const isInLoop = !!parendNode && parendNode.data.type === BlockEnum.Loop

      const newEdge = {
        id: `${source}-${sourceHandle}-${target}-${targetHandle}`,
        type: CUSTOM_EDGE,
        source: source!,
        target: target!,
        sourceHandle,
        targetHandle,
        data: {
          sourceType: nodes.find(node => node.id === source)!.data.type,
          targetType: nodes.find(node => node.id === target)!.data.type,
          isInIteration,
          iteration_id: isInIteration ? targetNode?.parentId : undefined,
          isInLoop,
          loop_id: isInLoop ? targetNode?.parentId : undefined,
        },
        zIndex: targetNode?.parentId
          ? isInIteration
            ? ITERATION_CHILDREN_Z_INDEX
            : LOOP_CHILDREN_Z_INDEX
          : 0,
      }
      const nodesConnectedSourceOrTargetHandleIdsMap
        = getNodesConnectedSourceOrTargetHandleIdsMap(
          [{ type: 'add', edge: newEdge }],
          nodes,
        )
      const newNodes = produce(nodes, (draft: Node[]) => {
        draft.forEach((node) => {
          if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
            node.data = {
              ...node.data,
              ...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
            }
          }
        })
      })
      const newEdges = produce(edges, (draft) => {
        draft.push(newEdge)
      })

      setNodes(newNodes)
      setEdges(newEdges)

      handleSyncWorkflowDraft()
      saveStateToHistory(WorkflowHistoryEvent.NodeConnect, {
        nodeId: targetNode?.id,
      })
    },
    [
      getNodesReadOnly,
      collaborativeWorkflow,
      workflowStore,
      handleSyncWorkflowDraft,
      saveStateToHistory,
    ],
  )

  const handleNodeConnectStart = useCallback<OnConnectStart>(
    (_, { nodeId, handleType, handleId }) => {
      if (getNodesReadOnly())
        return

      if (nodeId && handleType) {
        const { setConnectingNodePayload } = workflowStore.getState()
        const { nodes } = collaborativeWorkflow.getState()
        const node = nodes.find(n => n.id === nodeId)!

        if (node.type === CUSTOM_NOTE_NODE)
          return

        if (
          node.data.type === BlockEnum.VariableAggregator
          || node.data.type === BlockEnum.VariableAssigner
        ) {
          if (handleType === 'target')
            return
        }

        setConnectingNodePayload({
          nodeId,
          nodeType: node.data.type,
          handleType,
          handleId,
        })
      }
    },
    [collaborativeWorkflow, workflowStore, getNodesReadOnly],
  )

  const handleNodeConnectEnd = useCallback<OnConnectEnd>(
    (e: any) => {
      if (getNodesReadOnly())
        return

      const {
        connectingNodePayload,
        setConnectingNodePayload,
        enteringNodePayload,
        setEnteringNodePayload,
      } = workflowStore.getState()
      if (connectingNodePayload && enteringNodePayload) {
        const { setShowAssignVariablePopup, hoveringAssignVariableGroupId }
          = workflowStore.getState()
        const { screenToFlowPosition } = reactflow
        const { nodes, setNodes } = collaborativeWorkflow.getState()
        const fromHandleType = connectingNodePayload.handleType
        const fromHandleId = connectingNodePayload.handleId
        const fromNode = nodes.find(
          n => n.id === connectingNodePayload.nodeId,
        )!
        const toNode = nodes.find(n => n.id === enteringNodePayload.nodeId)!
        const toParentNode = nodes.find(n => n.id === toNode.parentId)

        if (fromNode.parentId !== toNode.parentId)
          return

        const { x, y } = screenToFlowPosition({ x: e.x, y: e.y })

        if (
          fromHandleType === 'source'
          && (toNode.data.type === BlockEnum.VariableAssigner
            || toNode.data.type === BlockEnum.VariableAggregator)
        ) {
          const groupEnabled = toNode.data.advanced_settings?.group_enabled
          const firstGroupId = toNode.data.advanced_settings?.groups[0].groupId
          let handleId = 'target'

          if (groupEnabled) {
            if (hoveringAssignVariableGroupId)
              handleId = hoveringAssignVariableGroupId
            else handleId = firstGroupId
          }
          const newNodes = produce(nodes, (draft) => {
            draft.forEach((node) => {
              if (node.id === toNode.id) {
                node.data._showAddVariablePopup = true
                node.data._holdAddVariablePopup = true
              }
            })
          })
          setNodes(newNodes)
          setShowAssignVariablePopup({
            nodeId: fromNode.id,
            nodeData: fromNode.data,
            variableAssignerNodeId: toNode.id,
            variableAssignerNodeData: toNode.data,
            variableAssignerNodeHandleId: handleId,
            parentNode: toParentNode,
            x: x - toNode.positionAbsolute!.x,
            y: y - toNode.positionAbsolute!.y,
          })
          handleNodeConnect({
            source: fromNode.id,
            sourceHandle: fromHandleId,
            target: toNode.id,
            targetHandle: 'target',
          })
        }
      }
      setConnectingNodePayload(undefined)
      setEnteringNodePayload(undefined)
    },
    [collaborativeWorkflow, handleNodeConnect, getNodesReadOnly, workflowStore, reactflow],
  )

  const { deleteNodeInspectorVars } = useInspectVarsCrud()
  const handleNodeDelete = useCallback(
    (nodeId: string) => {
      if (getNodesReadOnly())
        return

      const { nodes, setNodes, edges, setEdges } = collaborativeWorkflow.getState()
      const currentNodeIndex = nodes.findIndex(node => node.id === nodeId)
      const currentNode = nodes[currentNodeIndex]

      if (!currentNode)
        return

      if (
        nodesMetaDataMap?.[currentNode.data.type as BlockEnum]?.metaData
          .isUndeletable
      ) {
        return
      }

      deleteNodeInspectorVars(nodeId)
      if (currentNode.data.type === BlockEnum.Iteration) {
        const iterationChildren = nodes.filter(
          node => node.parentId === currentNode.id,
        )

        if (iterationChildren.length) {
          if (currentNode.data._isBundled) {
            iterationChildren.forEach((child) => {
              handleNodeDelete(child.id)
            })
            return handleNodeDelete(nodeId)
          }
          else {
            if (iterationChildren.length === 1) {
              handleNodeDelete(iterationChildren[0]!.id)
              handleNodeDelete(nodeId)

              return
            }
            const { setShowConfirm, showConfirm } = workflowStore.getState()

            if (!showConfirm) {
              setShowConfirm({
                title: t('nodes.iteration.deleteTitle', { ns: 'workflow' }),
                desc: t('nodes.iteration.deleteDesc', { ns: 'workflow' }) || '',
                onConfirm: () => {
                  iterationChildren.forEach((child) => {
                    handleNodeDelete(child.id)
                  })
                  handleNodeDelete(nodeId)
                  handleSyncWorkflowDraft()
                  setShowConfirm(undefined)
                },
              })
              return
            }
          }
        }
      }

      if (currentNode.data.type === BlockEnum.Loop) {
        const loopChildren = nodes.filter(
          node => node.parentId === currentNode.id,
        )

        if (loopChildren.length) {
          if (currentNode.data._isBundled) {
            loopChildren.forEach((child) => {
              handleNodeDelete(child.id)
            })
            return handleNodeDelete(nodeId)
          }
          else {
            if (loopChildren.length === 1) {
              handleNodeDelete(loopChildren[0]!.id)
              handleNodeDelete(nodeId)

              return
            }
            const { setShowConfirm, showConfirm } = workflowStore.getState()

            if (!showConfirm) {
              setShowConfirm({
                title: t('nodes.loop.deleteTitle', { ns: 'workflow' }),
                desc: t('nodes.loop.deleteDesc', { ns: 'workflow' }) || '',
                onConfirm: () => {
                  loopChildren.forEach((child) => {
                    handleNodeDelete(child.id)
                  })
                  handleNodeDelete(nodeId)
                  handleSyncWorkflowDraft()
                  setShowConfirm(undefined)
                },
              })
              return
            }
          }
        }
      }

      if (currentNode.data.type === BlockEnum.DataSource) {
        const { id } = currentNode
        const { ragPipelineVariables, setRagPipelineVariables }
          = workflowStore.getState()
        if (ragPipelineVariables && setRagPipelineVariables) {
          const newRagPipelineVariables: RAGPipelineVariables = []
          ragPipelineVariables.forEach((variable) => {
            if (variable.belong_to_node_id === id)
              return
            newRagPipelineVariables.push(variable)
          })
          setRagPipelineVariables(newRagPipelineVariables)
        }
      }

      const connectedEdges = getConnectedEdges([{ id: nodeId } as Node], edges)
      const nodesConnectedSourceOrTargetHandleIdsMap
        = getNodesConnectedSourceOrTargetHandleIdsMap(
          connectedEdges.map(edge => ({ type: 'remove', edge })),
          nodes,
        )
      const newNodes = produce(nodes, (draft: Node[]) => {
        draft.forEach((node) => {
          if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
            node.data = {
              ...node.data,
              ...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
            }
          }

          if (node.id === currentNode.parentId) {
            node.data._children = node.data._children?.filter(
              child => child.nodeId !== nodeId,
            )
          }
        })
        draft.splice(currentNodeIndex, 1)
      })
      setNodes(newNodes, true, 'nodes:perform-batch-cascade-delete')
      const newEdges = produce(edges, (draft) => {
        return draft.filter(
          edge =>
            !connectedEdges.some(connectedEdge => connectedEdge.id === edge.id),
        )
      })
      setEdges(newEdges)
      handleSyncWorkflowDraft()

      if (currentNode.type === CUSTOM_NOTE_NODE) {
        saveStateToHistory(WorkflowHistoryEvent.NoteDelete, {
          nodeId: currentNode.id,
        })
      }
      else {
        saveStateToHistory(WorkflowHistoryEvent.NodeDelete, {
          nodeId: currentNode.id,
        })
      }
    },
    [
      getNodesReadOnly,
      collaborativeWorkflow,
      handleSyncWorkflowDraft,
      saveStateToHistory,
      workflowStore,
      t,
      nodesMetaDataMap,
      deleteNodeInspectorVars,
    ],
  )

  const handleNodeAdd = useCallback<OnNodeAdd>(
    (
      {
        nodeType,
        sourceHandle = 'source',
        targetHandle = 'target',
        pluginDefaultValue,
      },
      { prevNodeId, prevNodeSourceHandle, nextNodeId, nextNodeTargetHandle },
    ) => {
      if (getNodesReadOnly())
        return

      const { nodes, setNodes, edges, setEdges } = collaborativeWorkflow.getState()
      const nodesWithSameType = nodes.filter(
        node => node.data.type === nodeType,
      )
      const nodeMetaData = nodesMetaDataMap?.[nodeType]
      if (!nodeMetaData)
        return
      const { defaultValue } = nodeMetaData
      const { newNode, newIterationStartNode, newLoopStartNode }
        = generateNewNode({
          type: getNodeCustomTypeByNodeDataType(nodeType),
          data: {
            ...(defaultValue as any),
            title:
              nodesWithSameType.length > 0
                ? `${defaultValue.title} ${nodesWithSameType.length + 1}`
                : defaultValue.title,
            ...pluginDefaultValue,
            selected: true,
            _showAddVariablePopup:
              (nodeType === BlockEnum.VariableAssigner
                || nodeType === BlockEnum.VariableAggregator)
              && !!prevNodeId,
            _holdAddVariablePopup: false,
          },
          position: {
            x: 0,
            y: 0,
          },
        })
      if (prevNodeId && !nextNodeId) {
        const prevNodeIndex = nodes.findIndex(node => node.id === prevNodeId)
        const prevNode = nodes[prevNodeIndex]
        const outgoers = getOutgoers(prevNode!, nodes, edges).sort(
          (a, b) => a.position.y - b.position.y,
        )
        const lastOutgoer = outgoers.at(-1)

        newNode.data._connectedTargetHandleIds
          = nodeType === BlockEnum.DataSource ? [] : [targetHandle]
        newNode.data._connectedSourceHandleIds = []
        newNode.position = {
          x: lastOutgoer
            ? lastOutgoer.position.x
            : prevNode!.position.x + prevNode!.width! + X_OFFSET,
          y: lastOutgoer
            ? lastOutgoer.position.y + lastOutgoer.height! + Y_OFFSET
            : prevNode!.position.y,
        }
        newNode.parentId = prevNode!.parentId
        newNode.extent = prevNode!.extent

        const parentNode
          = nodes.find(node => node.id === prevNode!.parentId) || null
        const isInIteration
          = !!parentNode && parentNode.data.type === BlockEnum.Iteration
        const isInLoop
          = !!parentNode && parentNode.data.type === BlockEnum.Loop

        if (prevNode!.parentId) {
          newNode.data.isInIteration = isInIteration
          newNode.data.isInLoop = isInLoop
          if (isInIteration) {
            newNode.data.iteration_id = parentNode.id
            newNode.zIndex = ITERATION_CHILDREN_Z_INDEX
          }
          if (isInLoop) {
            newNode.data.loop_id = parentNode.id
            newNode.zIndex = LOOP_CHILDREN_Z_INDEX
          }
          if (
            isInIteration
            && (newNode.data.type === BlockEnum.Answer
              || newNode.data.type === BlockEnum.Tool
              || newNode.data.type === BlockEnum.Assigner)
          ) {
            const iterNodeData: IterationNodeType = parentNode.data
            iterNodeData._isShowTips = true
          }
          if (
            isInLoop
            && (newNode.data.type === BlockEnum.Answer
              || newNode.data.type === BlockEnum.Tool
              || newNode.data.type === BlockEnum.Assigner)
          ) {
            const iterNodeData: IterationNodeType = parentNode.data
            iterNodeData._isShowTips = true
          }
        }

        let newEdge = null
        if (nodeType !== BlockEnum.DataSource) {
          newEdge = {
            id: `${prevNodeId}-${prevNodeSourceHandle}-${newNode.id}-${targetHandle}`,
            type: CUSTOM_EDGE,
            source: prevNodeId,
            sourceHandle: prevNodeSourceHandle,
            target: newNode.id,
            targetHandle,
            data: {
              sourceType: prevNode!.data.type,
              targetType: newNode.data.type,
              isInIteration,
              isInLoop,
              iteration_id: isInIteration ? prevNode!.parentId : undefined,
              loop_id: isInLoop ? prevNode!.parentId : undefined,
              _connectedNodeIsSelected: true,
            },
            zIndex: prevNode!.parentId
              ? isInIteration
                ? ITERATION_CHILDREN_Z_INDEX
                : LOOP_CHILDREN_Z_INDEX
              : 0,
          }
        }

        const nodesConnectedSourceOrTargetHandleIdsMap
          = getNodesConnectedSourceOrTargetHandleIdsMap(
            (newEdge ? [{ type: 'add', edge: newEdge }] : []),
            nodes,
          )
        const newNodes = produce(nodes, (draft: Node[]) => {
          draft.forEach((node) => {
            node.data.selected = false

            if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
              node.data = {
                ...node.data,
                ...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
              }
            }

            if (
              node.data.type === BlockEnum.Iteration
              && prevNode!.parentId === node.id
            ) {
              node.data._children?.push({
                nodeId: newNode.id,
                nodeType: newNode.data.type,
              })
            }

            if (
              node.data.type === BlockEnum.Loop
              && prevNode!.parentId === node.id
            ) {
              node.data._children?.push({
                nodeId: newNode.id,
                nodeType: newNode.data.type,
              })
            }
          })
          draft.push(newNode)

          if (newIterationStartNode)
            draft.push(newIterationStartNode)

          if (newLoopStartNode)
            draft.push(newLoopStartNode)
        })

        if (
          newNode.data.type === BlockEnum.VariableAssigner
          || newNode.data.type === BlockEnum.VariableAggregator
        ) {
          const { setShowAssignVariablePopup } = workflowStore.getState()

          setShowAssignVariablePopup({
            nodeId: prevNode!.id,
            nodeData: prevNode!.data,
            variableAssignerNodeId: newNode.id,
            variableAssignerNodeData: newNode.data as VariableAssignerNodeType,
            variableAssignerNodeHandleId: targetHandle,
            parentNode: nodes.find(node => node.id === newNode.parentId),
            x: -25,
            y: 44,
          })
        }
        const newEdges = produce(edges, (draft) => {
          draft.forEach((item) => {
            item.data = {
              ...item.data,
              _connectedNodeIsSelected: false,
            }
          })
          if (newEdge)
            draft.push(newEdge)
        })

        setNodes(newNodes)
        setEdges(newEdges)
      }
      if (!prevNodeId && nextNodeId) {
        const nextNodeIndex = nodes.findIndex(node => node.id === nextNodeId)
        const nextNode = nodes[nextNodeIndex]!
        if (
          nodeType !== BlockEnum.IfElse
          && nodeType !== BlockEnum.QuestionClassifier
          && nodeType !== BlockEnum.HumanInput
        ) {
          newNode.data._connectedSourceHandleIds = [sourceHandle]
        }
        newNode.data._connectedTargetHandleIds = []
        newNode.position = {
          x: nextNode.position.x,
          y: nextNode.position.y,
        }
        newNode.parentId = nextNode.parentId
        newNode.extent = nextNode.extent

        const parentNode
          = nodes.find(node => node.id === nextNode.parentId) || null
        const isInIteration
          = !!parentNode && parentNode.data.type === BlockEnum.Iteration
        const isInLoop
          = !!parentNode && parentNode.data.type === BlockEnum.Loop

        if (parentNode && nextNode.parentId) {
          newNode.data.isInIteration = isInIteration
          newNode.data.isInLoop = isInLoop
          if (isInIteration) {
            newNode.data.iteration_id = parentNode.id
            newNode.zIndex = ITERATION_CHILDREN_Z_INDEX
          }
          if (isInLoop) {
            newNode.data.loop_id = parentNode.id
            newNode.zIndex = LOOP_CHILDREN_Z_INDEX
          }
        }

        let newEdge

        if (
          nodeType !== BlockEnum.IfElse
          && nodeType !== BlockEnum.QuestionClassifier
          && nodeType !== BlockEnum.HumanInput
          && nodeType !== BlockEnum.LoopEnd
        ) {
          newEdge = {
            id: `${newNode.id}-${sourceHandle}-${nextNodeId}-${nextNodeTargetHandle}`,
            type: CUSTOM_EDGE,
            source: newNode.id,
            sourceHandle,
            target: nextNodeId,
            targetHandle: nextNodeTargetHandle,
            data: {
              sourceType: newNode.data.type,
              targetType: nextNode.data.type,
              isInIteration,
              isInLoop,
              iteration_id: isInIteration ? nextNode.parentId : undefined,
              loop_id: isInLoop ? nextNode.parentId : undefined,
              _connectedNodeIsSelected: true,
            },
            zIndex: nextNode.parentId
              ? isInIteration
                ? ITERATION_CHILDREN_Z_INDEX
                : LOOP_CHILDREN_Z_INDEX
              : 0,
          }
        }

        let nodesConnectedSourceOrTargetHandleIdsMap: Record<string, any>
        if (newEdge) {
          nodesConnectedSourceOrTargetHandleIdsMap
            = getNodesConnectedSourceOrTargetHandleIdsMap(
              [{ type: 'add', edge: newEdge }],
              nodes,
            )
        }

        const afterNodesInSameBranch = getAfterNodesInSameBranch(nextNodeId!)
        const afterNodesInSameBranchIds = afterNodesInSameBranch.map(
          node => node.id,
        )
        const newNodes = produce(nodes, (draft) => {
          draft.forEach((node) => {
            node.data.selected = false

            if (afterNodesInSameBranchIds.includes(node.id))
              node.position.x += NODE_WIDTH_X_OFFSET

            if (nodesConnectedSourceOrTargetHandleIdsMap?.[node.id]) {
              node.data = {
                ...node.data,
                ...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
              }
            }

            if (
              node.data.type === BlockEnum.Iteration
              && nextNode.parentId === node.id
            ) {
              node.data._children?.push({
                nodeId: newNode.id,
                nodeType: newNode.data.type,
              })
            }

            if (
              node.data.type === BlockEnum.Iteration
              && node.data.start_node_id === nextNodeId
            ) {
              node.data.start_node_id = newNode.id
              node.data.startNodeType = newNode.data.type
            }

            if (
              node.data.type === BlockEnum.Loop
              && nextNode.parentId === node.id
            ) {
              node.data._children?.push({
                nodeId: newNode.id,
                nodeType: newNode.data.type,
              })
            }

            if (
              node.data.type === BlockEnum.Loop
              && node.data.start_node_id === nextNodeId
            ) {
              node.data.start_node_id = newNode.id
              node.data.startNodeType = newNode.data.type
            }
          })
          draft.push(newNode)
          if (newIterationStartNode)
            draft.push(newIterationStartNode)
          if (newLoopStartNode)
            draft.push(newLoopStartNode)
        })
        if (newEdge) {
          const newEdges = produce(edges, (draft) => {
            draft.forEach((item) => {
              item.data = {
                ...item.data,
                _connectedNodeIsSelected: false,
              }
            })
            draft.push(newEdge)
          })

          setNodes(newNodes)
          setEdges(newEdges)
        }
        else {
          setNodes(newNodes)
        }
      }
      if (prevNodeId && nextNodeId) {
        const prevNode = nodes.find(node => node.id === prevNodeId)!
        const nextNode = nodes.find(node => node.id === nextNodeId)!

        newNode.data._connectedTargetHandleIds
          = nodeType === BlockEnum.DataSource ? [] : [targetHandle]
        newNode.data._connectedSourceHandleIds = [sourceHandle]
        newNode.position = {
          x: nextNode.position.x,
          y: nextNode.position.y,
        }
        newNode.parentId = prevNode.parentId
        newNode.extent = prevNode.extent

        const parentNode
          = nodes.find(node => node.id === prevNode.parentId) || null
        const isInIteration
          = !!parentNode && parentNode.data.type === BlockEnum.Iteration
        const isInLoop
          = !!parentNode && parentNode.data.type === BlockEnum.Loop

        if (parentNode && prevNode.parentId) {
          newNode.data.isInIteration = isInIteration
          newNode.data.isInLoop = isInLoop
          if (isInIteration) {
            newNode.data.iteration_id = parentNode.id
            newNode.zIndex = ITERATION_CHILDREN_Z_INDEX
          }
          if (isInLoop) {
            newNode.data.loop_id = parentNode.id
            newNode.zIndex = LOOP_CHILDREN_Z_INDEX
          }
        }

        const currentEdgeIndex = edges.findIndex(
          edge => edge.source === prevNodeId && edge.target === nextNodeId,
        )
        let newPrevEdge = null

        if (nodeType !== BlockEnum.DataSource) {
          newPrevEdge = {
            id: `${prevNodeId}-${prevNodeSourceHandle}-${newNode.id}-${targetHandle}`,
            type: CUSTOM_EDGE,
            source: prevNodeId,
            sourceHandle: prevNodeSourceHandle,
            target: newNode.id,
            targetHandle,
            data: {
              sourceType: prevNode.data.type,
              targetType: newNode.data.type,
              isInIteration,
              isInLoop,
              iteration_id: isInIteration ? prevNode.parentId : undefined,
              loop_id: isInLoop ? prevNode.parentId : undefined,
              _connectedNodeIsSelected: true,
            },
            zIndex: prevNode.parentId
              ? isInIteration
                ? ITERATION_CHILDREN_Z_INDEX
                : LOOP_CHILDREN_Z_INDEX
              : 0,
          }
        }

        let newNextEdge: Edge | null = null

        const nextNodeParentNode
          = nodes.find(node => node.id === nextNode.parentId) || null
        const isNextNodeInIteration
          = !!nextNodeParentNode
            && nextNodeParentNode.data.type === BlockEnum.Iteration
        const isNextNodeInLoop
          = !!nextNodeParentNode
            && nextNodeParentNode.data.type === BlockEnum.Loop

        if (
          nodeType !== BlockEnum.IfElse
          && nodeType !== BlockEnum.QuestionClassifier
          && nodeType !== BlockEnum.HumanInput
          && nodeType !== BlockEnum.LoopEnd
        ) {
          newNextEdge = {
            id: `${newNode.id}-${sourceHandle}-${nextNodeId}-${nextNodeTargetHandle}`,
            type: CUSTOM_EDGE,
            source: newNode.id,
            sourceHandle,
            target: nextNodeId,
            targetHandle: nextNodeTargetHandle,
            data: {
              sourceType: newNode.data.type,
              targetType: nextNode.data.type,
              isInIteration: isNextNodeInIteration,
              isInLoop: isNextNodeInLoop,
              iteration_id: isNextNodeInIteration
                ? nextNode.parentId
                : undefined,
              loop_id: isNextNodeInLoop ? nextNode.parentId : undefined,
              _connectedNodeIsSelected: true,
            },
            zIndex: nextNode.parentId
              ? isNextNodeInIteration
                ? ITERATION_CHILDREN_Z_INDEX
                : LOOP_CHILDREN_Z_INDEX
              : 0,
          }
        }
        const nodesConnectedSourceOrTargetHandleIdsMap
          = getNodesConnectedSourceOrTargetHandleIdsMap(
            [
              { type: 'remove', edge: edges[currentEdgeIndex]! },
              ...(newPrevEdge ? [{ type: 'add', edge: newPrevEdge }] : []),
              ...(newNextEdge ? [{ type: 'add', edge: newNextEdge }] : []),
            ],
            [...nodes, newNode],
          )

        const afterNodesInSameBranch = getAfterNodesInSameBranch(nextNodeId!)
        const afterNodesInSameBranchIds = afterNodesInSameBranch.map(
          node => node.id,
        )
        const newNodes = produce(nodes, (draft) => {
          draft.forEach((node) => {
            node.data.selected = false

            if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
              node.data = {
                ...node.data,
                ...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
              }
            }
            if (afterNodesInSameBranchIds.includes(node.id))
              node.position.x += NODE_WIDTH_X_OFFSET

            if (
              node.data.type === BlockEnum.Iteration
              && prevNode.parentId === node.id
            ) {
              node.data._children?.push({
                nodeId: newNode.id,
                nodeType: newNode.data.type,
              })
            }
            if (
              node.data.type === BlockEnum.Loop
              && prevNode.parentId === node.id
            ) {
              node.data._children?.push({
                nodeId: newNode.id,
                nodeType: newNode.data.type,
              })
            }
          })
          draft.push(newNode)
          if (newIterationStartNode)
            draft.push(newIterationStartNode)
          if (newLoopStartNode)
            draft.push(newLoopStartNode)
        })
        setNodes(newNodes)
        if (
          newNode.data.type === BlockEnum.VariableAssigner
          || newNode.data.type === BlockEnum.VariableAggregator
        ) {
          const { setShowAssignVariablePopup } = workflowStore.getState()

          setShowAssignVariablePopup({
            nodeId: prevNode.id,
            nodeData: prevNode.data,
            variableAssignerNodeId: newNode.id,
            variableAssignerNodeData: newNode.data as VariableAssignerNodeType,
            variableAssignerNodeHandleId: targetHandle,
            parentNode: nodes.find(node => node.id === newNode.parentId),
            x: -25,
            y: 44,
          })
        }
        const newEdges = produce(edges, (draft) => {
          draft.splice(currentEdgeIndex, 1)
          draft.forEach((item) => {
            item.data = {
              ...item.data,
              _connectedNodeIsSelected: false,
            }
          })
          if (newPrevEdge)
            draft.push(newPrevEdge)

          if (newNextEdge)
            draft.push(newNextEdge)
        })
        setEdges(newEdges)
      }
      handleSyncWorkflowDraft()
      saveStateToHistory(WorkflowHistoryEvent.NodeAdd, { nodeId: newNode.id })
    },
    [
      getNodesReadOnly,
      collaborativeWorkflow,
      handleSyncWorkflowDraft,
      saveStateToHistory,
      workflowStore,
      getAfterNodesInSameBranch,
      nodesMetaDataMap,
    ],
  )

  const handleNodeChange = useCallback(
    (
      currentNodeId: string,
      nodeType: BlockEnum,
      sourceHandle: string,
      pluginDefaultValue?: PluginDefaultValue,
    ) => {
      if (getNodesReadOnly())
        return

      const { nodes, setNodes, edges, setEdges } = collaborativeWorkflow.getState()
      const currentNode = nodes.find(node => node.id === currentNodeId)!
      const connectedEdges = getConnectedEdges([currentNode], edges)
      const nodesWithSameType = nodes.filter(
        node => node.data.type === nodeType,
      )
      const nodeMetaData = nodesMetaDataMap?.[nodeType]
      if (!nodeMetaData)
        return
      const { defaultValue } = nodeMetaData
      const {
        newNode: newCurrentNode,
        newIterationStartNode,
        newLoopStartNode,
      } = generateNewNode({
        type: getNodeCustomTypeByNodeDataType(nodeType),
        data: {
          ...(defaultValue as any),
          title:
            nodesWithSameType.length > 0
              ? `${defaultValue.title} ${nodesWithSameType.length + 1}`
              : defaultValue.title,
          ...pluginDefaultValue,
          _connectedSourceHandleIds: [],
          _connectedTargetHandleIds: [],
          selected: currentNode.data.selected,
          isInIteration: currentNode.data.isInIteration,
          isInLoop: currentNode.data.isInLoop,
          iteration_id: currentNode.data.iteration_id,
          loop_id: currentNode.data.loop_id,
        },
        position: {
          x: currentNode.position.x,
          y: currentNode.position.y,
        },
        parentId: currentNode.parentId,
        extent: currentNode.extent,
        zIndex: currentNode.zIndex,
      })
      const parentNode = nodes.find(node => node.id === currentNode.parentId)
      const newNodeIsInIteration
        = !!parentNode && parentNode.data.type === BlockEnum.Iteration
      const newNodeIsInLoop
        = !!parentNode && parentNode.data.type === BlockEnum.Loop
      const outgoingEdges = connectedEdges.filter(
        edge => edge.source === currentNodeId,
      )
      const normalizedSourceHandle = sourceHandle || 'source'
      const outgoingHandles = new Set(
        outgoingEdges.map(edge => edge.sourceHandle || 'source'),
      )
      const branchSourceHandle = currentNode.data._targetBranches?.[0]?.id
      let outgoingHandleToPreserve = normalizedSourceHandle
      if (!outgoingHandles.has(outgoingHandleToPreserve)) {
        if (branchSourceHandle && outgoingHandles.has(branchSourceHandle))
          outgoingHandleToPreserve = branchSourceHandle
        else if (outgoingHandles.has('source'))
          outgoingHandleToPreserve = 'source'
        else
          outgoingHandleToPreserve = outgoingEdges[0]?.sourceHandle || 'source'
      }
      const outgoingEdgesToPreserve = outgoingEdges.filter(
        edge => (edge.sourceHandle || 'source') === outgoingHandleToPreserve,
      )
      const outgoingEdgeIds = new Set(
        outgoingEdgesToPreserve.map(edge => edge.id),
      )
      const newNodeSourceHandle = newCurrentNode.data._targetBranches?.[0]?.id || 'source'
      const reconnectedEdges = connectedEdges.reduce<Edge[]>(
        (acc, edge) => {
          if (outgoingEdgeIds.has(edge.id)) {
            const originalTargetNode = nodes.find(
              node => node.id === edge.target,
            )
            const targetNodeForEdge
              = originalTargetNode && originalTargetNode.id !== currentNodeId
                ? originalTargetNode
                : newCurrentNode
            if (!targetNodeForEdge)
              return acc

            const targetHandle = edge.targetHandle || 'target'
            const targetParentNode
              = targetNodeForEdge.id === newCurrentNode.id
                ? parentNode || null
                : nodes.find(node => node.id === targetNodeForEdge.parentId)
                  || null
            const isInIteration
              = !!targetParentNode
                && targetParentNode.data.type === BlockEnum.Iteration
            const isInLoop
              = !!targetParentNode
                && targetParentNode.data.type === BlockEnum.Loop

            acc.push({
              ...edge,
              id: `${newCurrentNode.id}-${newNodeSourceHandle}-${targetNodeForEdge.id}-${targetHandle}`,
              source: newCurrentNode.id,
              sourceHandle: newNodeSourceHandle,
              target: targetNodeForEdge.id,
              targetHandle,
              type: CUSTOM_EDGE,
              data: {
                ...edge.data,
                sourceType: newCurrentNode.data.type,
                targetType: targetNodeForEdge.data.type,
                isInIteration,
                iteration_id: isInIteration
                  ? targetNodeForEdge.parentId
                  : undefined,
                isInLoop,
                loop_id: isInLoop ? targetNodeForEdge.parentId : undefined,
                _connectedNodeIsSelected: false,
              },
              zIndex: targetNodeForEdge.parentId
                ? isInIteration
                  ? ITERATION_CHILDREN_Z_INDEX
                  : LOOP_CHILDREN_Z_INDEX
                : 0,
            })
          }

          if (
            edge.target === currentNodeId
            && edge.source !== currentNodeId
            && !outgoingEdgeIds.has(edge.id)
          ) {
            const sourceNode = nodes.find(node => node.id === edge.source)
            if (!sourceNode)
              return acc

            const targetHandle = edge.targetHandle || 'target'
            const sourceHandle = edge.sourceHandle || 'source'

            acc.push({
              ...edge,
              id: `${sourceNode.id}-${sourceHandle}-${newCurrentNode.id}-${targetHandle}`,
              source: sourceNode.id,
              sourceHandle,
              target: newCurrentNode.id,
              targetHandle,
              type: CUSTOM_EDGE,
              data: {
                ...edge.data,
                sourceType: sourceNode.data.type,
                targetType: newCurrentNode.data.type,
                isInIteration: newNodeIsInIteration,
                iteration_id: newNodeIsInIteration
                  ? newCurrentNode.parentId
                  : undefined,
                isInLoop: newNodeIsInLoop,
                loop_id: newNodeIsInLoop ? newCurrentNode.parentId : undefined,
                _connectedNodeIsSelected: false,
              },
              zIndex: newCurrentNode.parentId
                ? newNodeIsInIteration
                  ? ITERATION_CHILDREN_Z_INDEX
                  : LOOP_CHILDREN_Z_INDEX
                : 0,
            })
          }

          return acc
        },
        [],
      )
      const nodesWithNewNode = produce(nodes, (draft) => {
        draft.forEach((node) => {
          node.data.selected = false
        })
        const index = draft.findIndex(node => node.id === currentNodeId)

        draft.splice(index, 1, newCurrentNode)
        if (newIterationStartNode)
          draft.push(newIterationStartNode)
        if (newLoopStartNode)
          draft.push(newLoopStartNode)
      })
      const nodesConnectedSourceOrTargetHandleIdsMap
        = getNodesConnectedSourceOrTargetHandleIdsMap(
          [
            ...connectedEdges.map(edge => ({ type: 'remove', edge })),
            ...reconnectedEdges.map(edge => ({ type: 'add', edge })),
          ],
          nodesWithNewNode,
        )
      const newNodes = produce(nodesWithNewNode, (draft) => {
        draft.forEach((node) => {
          if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
            node.data = {
              ...node.data,
              ...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
            }
          }
        })
      })
      setNodes(newNodes)
      const remainingEdges = edges.filter(
        edge =>
          !connectedEdges.some(connectedEdge => connectedEdge.id === edge.id),
      )
      setEdges([...remainingEdges, ...reconnectedEdges])
      if (nodeType === BlockEnum.TriggerWebhook) {
        handleSyncWorkflowDraft(true, true, {
          onSuccess: () => autoGenerateWebhookUrl(newCurrentNode.id),
        })
      }
      else {
        handleSyncWorkflowDraft()
      }

      saveStateToHistory(WorkflowHistoryEvent.NodeChange, {
        nodeId: currentNodeId,
      })
    },
    [
      getNodesReadOnly,
      collaborativeWorkflow,
      handleSyncWorkflowDraft,
      saveStateToHistory,
      nodesMetaDataMap,
      autoGenerateWebhookUrl,
    ],
  )

  const handleNodesCancelSelected = useCallback(() => {
    const { nodes, setNodes } = collaborativeWorkflow.getState()
    const newNodes = produce(nodes, (draft) => {
      draft.forEach((node) => {
        node.data.selected = false
      })
    })
    setNodes(newNodes)
  }, [collaborativeWorkflow])

  const handleNodeContextMenu = useCallback(
    (e: MouseEvent, node: Node) => {
      if (
        node.type === CUSTOM_NOTE_NODE
        || node.type === CUSTOM_ITERATION_START_NODE
      ) {
        e.stopPropagation()
        return
      }

      if (
        node.type === CUSTOM_NOTE_NODE
        || node.type === CUSTOM_LOOP_START_NODE
      ) {
        e.stopPropagation()
        return
      }

      e.preventDefault()
      workflowStore.setState({
        contextMenuTarget: {
          type: 'node',
          nodeId: node.id,
        },
      })
      handleNodeSelect(node.id, true)
    },
    [workflowStore, handleNodeSelect],
  )

  const isNodeCopyable = useCallback((node: Node) => {
    if (
      node.type === CUSTOM_ITERATION_START_NODE
      || node.type === CUSTOM_LOOP_START_NODE
    ) {
      return false
    }

    if (
      node.data.type === BlockEnum.Start
      || node.data.type === BlockEnum.LoopEnd
      || node.data.type === BlockEnum.KnowledgeBase
      || node.data.type === BlockEnum.DataSourceEmpty
    ) {
      return false
    }

    if (node.type === CUSTOM_NOTE_NODE)
      return true

    const nodeMeta = nodesMetaDataMap?.[node.data.type as BlockEnum]
    if (!nodeMeta)
      return false

    const { metaData } = nodeMeta
    return !metaData.isSingleton
  }, [nodesMetaDataMap])

  const getNodeDefaultValueForPaste = useCallback((node: Node) => {
    if (node.type === CUSTOM_NOTE_NODE)
      return {}

    const nodeMeta = nodesMetaDataMap?.[node.data.type as BlockEnum]
    return nodeMeta?.defaultValue
  }, [nodesMetaDataMap])

  const handleNodesCopy = useCallback(
    (nodeId?: string) => {
      if (getNodesReadOnly())
        return

      const { setClipboardData } = workflowStore.getState()
      const { nodes, edges } = collaborativeWorkflow.getState()
      let nodesToCopy: Node[] = []

      if (nodeId) {
        const nodeToCopy = nodes.find(node => node.id === nodeId && isNodeCopyable(node))
        if (nodeToCopy)
          nodesToCopy = [nodeToCopy]
      }
      else {
        const bundledNodes = nodes.filter((node) => {
          if (!node.data._isBundled)
            return false

          if (!isNodeCopyable(node))
            return false

          if (node.type === CUSTOM_NOTE_NODE)
            return true
          return !node.data.isInIteration && !node.data.isInLoop
        })

        if (bundledNodes.length) {
          nodesToCopy = bundledNodes
        }
        else {
          const selectedNodes = nodes.filter(
            node => node.data.selected && isNodeCopyable(node),
          )

          if (selectedNodes.length)
            nodesToCopy = selectedNodes
        }
      }

      if (!nodesToCopy.length)
        return

      const copiedNodesMap = new Map(nodesToCopy.map(node => [node.id, node]))
      const queue = nodesToCopy
        .filter(node => node.data.type === BlockEnum.Iteration || node.data.type === BlockEnum.Loop)
        .map(node => node.id)

      while (queue.length) {
        const parentId = queue.shift()!
        nodes.forEach((node) => {
          if (node.parentId !== parentId || copiedNodesMap.has(node.id))
            return

          copiedNodesMap.set(node.id, node)
          if (node.data.type === BlockEnum.Iteration || node.data.type === BlockEnum.Loop)
            queue.push(node.id)
        })
      }

      const copiedNodes = [...copiedNodesMap.values()]
      const copiedNodeIds = new Set(copiedNodes.map(node => node.id))
      const copiedEdges = edges.filter(
        edge => copiedNodeIds.has(edge.source) && copiedNodeIds.has(edge.target),
      )

      const clipboardData = {
        nodes: copiedNodes,
        edges: copiedEdges,
      }

      setClipboardData(clipboardData)
      void writeWorkflowClipboard(clipboardData, appDslVersion).catch(() => {})
    },
    [getNodesReadOnly, workflowStore, collaborativeWorkflow, isNodeCopyable, appDslVersion],
  )

  const handleNodesPaste = useCallback(async () => {
    if (getNodesReadOnly())
      return

    const {
      clipboardElements: storeClipboardElements,
      clipboardEdges: storeClipboardEdges,
      mousePosition,
      setClipboardData,
    } = workflowStore.getState()
    const clipboardData = await readWorkflowClipboard(appDslVersion)
    const hasSystemClipboard = clipboardData.nodes.length > 0
    const shouldRunCompatibilityCheck = hasSystemClipboard && clipboardData.isVersionMismatch

    const clipboardElements = hasSystemClipboard
      ? clipboardData.nodes
      : storeClipboardElements
    const clipboardEdges = hasSystemClipboard
      ? clipboardData.edges
      : storeClipboardEdges

    if (hasSystemClipboard)
      setClipboardData(clipboardData)

    const validatedClipboardElements = clipboardElements.filter(isClipboardNodeStructurallyValid)
    const validatedClipboardEdges = clipboardEdges.filter(isClipboardEdgeStructurallyValid)

    if (!validatedClipboardElements.length)
      return

    const { nodes, setNodes, edges, setEdges } = collaborativeWorkflow.getState()
    const reservedNodeTitles = new Set(
      nodes
        .map(node => node.data.title)
        .filter((title): title is string => typeof title === 'string'),
    )

    const nodesToPaste: Node[] = []
    const edgesToPaste: Edge[] = []

    let compatibleClipboardElements = validatedClipboardElements.filter((node) => {
      if (node.type === CUSTOM_NOTE_NODE)
        return true

      const nodeDefaultValue = getNodeDefaultValueForPaste(node)
      if (!nodeDefaultValue)
        return false

      if (
        shouldRunCompatibilityCheck
        && !isClipboardValueCompatibleWithDefault(nodeDefaultValue, node.data)
      ) {
        return false
      }

      return true
    })

    if (shouldRunCompatibilityCheck) {
      compatibleClipboardElements = pruneClipboardNodesWithFilteredAncestors(
        validatedClipboardElements,
        compatibleClipboardElements,
      )
    }

    const compatibleClipboardNodeIds = new Set(
      compatibleClipboardElements.map(node => node.id),
    )
    const filteredNodeCount = shouldRunCompatibilityCheck
      ? validatedClipboardElements.length - compatibleClipboardElements.length
      : 0
    const filteredEdgeCount = shouldRunCompatibilityCheck
      ? validatedClipboardEdges.filter(edge =>
        !compatibleClipboardNodeIds.has(edge.source)
        || !compatibleClipboardNodeIds.has(edge.target),
      ).length
      : 0

    if (
      shouldRunCompatibilityCheck
      && (filteredNodeCount > 0 || filteredEdgeCount > 0)
    ) {
      toast.warning(t('common.clipboardVersionCompatibilityWarning', {
        ns: 'workflow',
      }))
    }

    if (!compatibleClipboardElements.length)
      return

    const rootClipboardNodes = compatibleClipboardElements.filter(
      node => !node.parentId || !compatibleClipboardNodeIds.has(node.parentId),
    )
    const positionReferenceNodes = rootClipboardNodes.length
      ? rootClipboardNodes
      : compatibleClipboardElements
    const { x, y } = getTopLeftNodePosition(positionReferenceNodes)
    const { screenToFlowPosition } = reactflow
    const currentPosition = screenToFlowPosition({
      x: mousePosition.pageX,
      y: mousePosition.pageY,
    })
    const offsetX = currentPosition.x - x
    const offsetY = currentPosition.y - y
    let idMapping: Record<string, string> = {}
    const pastedNodesMap: Record<string, Node> = {}
    const parentChildrenToAppend: { parentId: string, childId: string, childType: BlockEnum }[] = []
    const selectedNodes = nodes.filter(node => node.selected)
    // Keep this list aligned with availableBlocksFilter(inContainer)
    // in use-available-blocks.ts.
    const commonNestedDisallowPasteNodes = [
      BlockEnum.End,
      BlockEnum.Iteration,
      BlockEnum.Loop,
      BlockEnum.DataSource,
      BlockEnum.KnowledgeBase,
      BlockEnum.HumanInput,
    ]
    // Same-canvas copy keeps the source container selected, so only treat a
    // selected container as the paste target when it is not part of the clipboard.
    const selectedContainerNode = selectedNodes.length === 1
      && (selectedNodes[0]?.data.type === BlockEnum.Iteration || selectedNodes[0]?.data.type === BlockEnum.Loop)
      && !compatibleClipboardNodeIds.has(selectedNodes[0].id)
      ? selectedNodes[0]
      : undefined

    rootClipboardNodes.forEach((nodeToPaste, index) => {
      const nodeDefaultValue = getNodeDefaultValueForPaste(nodeToPaste)
      if (nodeToPaste.type !== CUSTOM_NOTE_NODE && !nodeDefaultValue)
        return

      if (selectedContainerNode && commonNestedDisallowPasteNodes.includes(nodeToPaste.data.type))
        return

      const mergedData = shouldRunCompatibilityCheck
        ? sanitizeClipboardValueByDefault(nodeDefaultValue ?? {}, nodeToPaste.data) as Record<string, unknown>
        : {
            ...(nodeToPaste.type !== CUSTOM_NOTE_NODE ? nodeDefaultValue : {}),
            ...nodeToPaste.data,
          }
      const sourceTitle = typeof mergedData.title === 'string'
        ? mergedData.title
        : typeof nodeToPaste.data.title === 'string'
          ? nodeToPaste.data.title
          : 'Node'
      const sourceDesc = typeof mergedData.desc === 'string'
        ? mergedData.desc
        : typeof nodeToPaste.data.desc === 'string'
          ? nodeToPaste.data.desc
          : ''

      const { newNode, newIterationStartNode, newLoopStartNode }
        = generateNewNode({
          type: nodeToPaste.type,
          data: {
            ...mergedData,
            type: nodeToPaste.data.type,
            desc: sourceDesc,
            selected: false,
            _isBundled: false,
            _connectedSourceHandleIds: [],
            _connectedTargetHandleIds: [],
            _dimmed: false,
            isInIteration: false,
            iteration_id: undefined,
            isInLoop: false,
            loop_id: undefined,
            title: getUniquePastedNodeTitle(sourceTitle, reservedNodeTitles),
          },
          position: {
            x: nodeToPaste.position.x + offsetX,
            y: nodeToPaste.position.y + offsetY,
          },
          extent: nodeToPaste.extent,
          zIndex: nodeToPaste.zIndex,
        })
      newNode.id = newNode.id + index

      let newChildren: Node[] = []
      if (nodeToPaste.data.type === BlockEnum.Iteration) {
        if (newIterationStartNode) {
          newIterationStartNode.parentId = newNode.id
          const iterationNodeData = newNode.data as IterationNodeType
          iterationNodeData.start_node_id = newIterationStartNode.id
        }

        const oldIterationStartNodeInClipboard = compatibleClipboardElements.find(
          n =>
            n.parentId === nodeToPaste.id
            && n.type === CUSTOM_ITERATION_START_NODE,
        )
        if (oldIterationStartNodeInClipboard && newIterationStartNode)
          idMapping[oldIterationStartNodeInClipboard.id] = newIterationStartNode.id

        const copiedIterationChildren = compatibleClipboardElements.filter(
          n =>
            n.parentId === nodeToPaste.id
            && n.type !== CUSTOM_ITERATION_START_NODE,
        )
        if (copiedIterationChildren.length) {
          copiedIterationChildren.forEach((child, childIndex) => {
            const childType = child.data.type
            const childDefaultValue = getNodeDefaultValueForPaste(child)
            if (child.type !== CUSTOM_NOTE_NODE && !childDefaultValue)
              return

            const mergedChildData = shouldRunCompatibilityCheck
              ? sanitizeClipboardValueByDefault(childDefaultValue ?? {}, child.data) as Record<string, unknown>
              : {
                  ...(child.type !== CUSTOM_NOTE_NODE ? childDefaultValue : {}),
                  ...child.data,
                }
            const childSourceTitle = typeof mergedChildData.title === 'string'
              ? mergedChildData.title
              : typeof child.data.title === 'string'
                ? child.data.title
                : 'Node'
            const childSourceDesc = typeof mergedChildData.desc === 'string'
              ? mergedChildData.desc
              : typeof child.data.desc === 'string'
                ? child.data.desc
                : ''

            const { newNode: newChild } = generateNewNode({
              type: child.type,
              data: {
                ...mergedChildData,
                desc: childSourceDesc,
                selected: false,
                _isBundled: false,
                _connectedSourceHandleIds: [],
                _connectedTargetHandleIds: [],
                _dimmed: false,
                title: getUniquePastedNodeTitle(childSourceTitle, reservedNodeTitles),
                isInIteration: true,
                iteration_id: newNode.id,
                isInLoop: false,
                loop_id: undefined,
                type: childType,
              },
              position: child.position,
              positionAbsolute: child.positionAbsolute,
              parentId: newNode.id,
              extent: child.extent,
              zIndex: ITERATION_CHILDREN_Z_INDEX,
            })
            newChild.id = `${newNode.id}${newChild.id + childIndex}`
            idMapping[child.id] = newChild.id
            newChildren.push(newChild)
          })
        }
        else {
          const oldIterationStartNode = nodes.find(
            n =>
              n.parentId === nodeToPaste.id
              && n.type === CUSTOM_ITERATION_START_NODE,
          )
          if (oldIterationStartNode && newIterationStartNode)
            idMapping[oldIterationStartNode.id] = newIterationStartNode.id

          const { copyChildren, newIdMapping }
            = handleNodeIterationChildrenCopy(
              nodeToPaste.id,
              newNode.id,
              idMapping,
            )
          newChildren = copyChildren
          idMapping = newIdMapping
        }

        newChildren.forEach((child) => {
          newNode.data._children?.push({
            nodeId: child.id,
            nodeType: child.data.type,
          })
        })
        if (newIterationStartNode)
          newChildren.push(newIterationStartNode)
      }
      else if (nodeToPaste.data.type === BlockEnum.Loop) {
        if (newLoopStartNode) {
          newLoopStartNode.parentId = newNode.id
          const loopNodeData = newNode.data as LoopNodeType
          loopNodeData.start_node_id = newLoopStartNode.id
        }

        const oldLoopStartNodeInClipboard = compatibleClipboardElements.find(
          n =>
            n.parentId === nodeToPaste.id
            && n.type === CUSTOM_LOOP_START_NODE,
        )
        if (oldLoopStartNodeInClipboard && newLoopStartNode)
          idMapping[oldLoopStartNodeInClipboard.id] = newLoopStartNode.id

        const copiedLoopChildren = compatibleClipboardElements.filter(
          n =>
            n.parentId === nodeToPaste.id
            && n.type !== CUSTOM_LOOP_START_NODE,
        )
        if (copiedLoopChildren.length) {
          copiedLoopChildren.forEach((child, childIndex) => {
            const childType = child.data.type
            const childDefaultValue = getNodeDefaultValueForPaste(child)
            if (child.type !== CUSTOM_NOTE_NODE && !childDefaultValue)
              return

            const mergedChildData = shouldRunCompatibilityCheck
              ? sanitizeClipboardValueByDefault(childDefaultValue ?? {}, child.data) as Record<string, unknown>
              : {
                  ...(child.type !== CUSTOM_NOTE_NODE ? childDefaultValue : {}),
                  ...child.data,
                }
            const childSourceTitle = typeof mergedChildData.title === 'string'
              ? mergedChildData.title
              : typeof child.data.title === 'string'
                ? child.data.title
                : 'Node'
            const childSourceDesc = typeof mergedChildData.desc === 'string'
              ? mergedChildData.desc
              : typeof child.data.desc === 'string'
                ? child.data.desc
                : ''

            const { newNode: newChild } = generateNewNode({
              type: child.type,
              data: {
                ...mergedChildData,
                desc: childSourceDesc,
                selected: false,
                _isBundled: false,
                _connectedSourceHandleIds: [],
                _connectedTargetHandleIds: [],
                _dimmed: false,
                title: getUniquePastedNodeTitle(childSourceTitle, reservedNodeTitles),
                isInIteration: false,
                iteration_id: undefined,
                isInLoop: true,
                loop_id: newNode.id,
                type: childType,
              },
              position: child.position,
              positionAbsolute: child.positionAbsolute,
              parentId: newNode.id,
              extent: child.extent,
              zIndex: LOOP_CHILDREN_Z_INDEX,
            })
            newChild.id = `${newNode.id}${newChild.id + childIndex}`
            idMapping[child.id] = newChild.id
            newChildren.push(newChild)
          })
        }
        else {
          const oldLoopStartNode = nodes.find(
            n =>
              n.parentId === nodeToPaste.id
              && n.type === CUSTOM_LOOP_START_NODE,
          )
          if (oldLoopStartNode && newLoopStartNode)
            idMapping[oldLoopStartNode.id] = newLoopStartNode.id

          const { copyChildren, newIdMapping }
            = handleNodeLoopChildrenCopy(
              nodeToPaste.id,
              newNode.id,
              idMapping,
            )
          newChildren = copyChildren
          idMapping = newIdMapping
        }

        newChildren.forEach((child) => {
          newNode.data._children?.push({
            nodeId: child.id,
            nodeType: child.data.type,
          })
        })
        if (newLoopStartNode)
          newChildren.push(newLoopStartNode)
      }
      else if (selectedContainerNode) {
        if (selectedContainerNode.data.type === BlockEnum.Iteration || selectedContainerNode.data.type === BlockEnum.Loop) {
          const isIteration = selectedContainerNode.data.type === BlockEnum.Iteration

          newNode.data.isInIteration = isIteration
          newNode.data.iteration_id = isIteration ? selectedContainerNode.id : undefined
          newNode.data.isInLoop = !isIteration
          newNode.data.loop_id = !isIteration ? selectedContainerNode.id : undefined

          newNode.parentId = selectedContainerNode.id
          newNode.zIndex = isIteration ? ITERATION_CHILDREN_Z_INDEX : LOOP_CHILDREN_Z_INDEX
          newNode.positionAbsolute = {
            x: newNode.position.x,
            y: newNode.position.y,
          }
          newNode.position = getNestedNodePosition(newNode, selectedContainerNode)
          parentChildrenToAppend.push({
            parentId: selectedContainerNode.id,
            childId: newNode.id,
            childType: newNode.data.type,
          })
        }
      }

      idMapping[nodeToPaste.id] = newNode.id
      nodesToPaste.push(newNode)
      pastedNodesMap[newNode.id] = newNode

      if (newChildren.length) {
        newChildren.forEach((child) => {
          pastedNodesMap[child.id] = child
        })
        nodesToPaste.push(...newChildren)
      }
    })

    const sourceEdges = validatedClipboardEdges

    sourceEdges.forEach((edge) => {
      const sourceId = idMapping[edge.source]
      const targetId = idMapping[edge.target]

      if (sourceId && targetId) {
        const sourceNode = pastedNodesMap[sourceId]
        const targetNode = pastedNodesMap[targetId]
        if (!sourceNode || !targetNode)
          return

        const parentNode = sourceNode.parentId && sourceNode.parentId === targetNode.parentId
          ? pastedNodesMap[sourceNode.parentId] ?? nodes.find(n => n.id === sourceNode.parentId)
          : null
        const isInIteration = parentNode?.data.type === BlockEnum.Iteration
        const isInLoop = parentNode?.data.type === BlockEnum.Loop
        const newEdge: Edge = {
          ...edge,
          id: `${sourceId}-${edge.sourceHandle}-${targetId}-${edge.targetHandle}`,
          source: sourceId,
          target: targetId,
          data: {
            ...(edge.data || {}),
            sourceType: sourceNode.data.type,
            targetType: targetNode.data.type,
            isInIteration,
            iteration_id: isInIteration ? parentNode?.id : undefined,
            isInLoop,
            loop_id: isInLoop ? parentNode?.id : undefined,
            _connectedNodeIsSelected: false,
          },
          zIndex: parentNode
            ? isInIteration
              ? ITERATION_CHILDREN_Z_INDEX
              : isInLoop
                ? LOOP_CHILDREN_Z_INDEX
                : 0
            : 0,
        }
        edgesToPaste.push(newEdge)
      }
    })

    const newNodes = produce(nodes, (draft: Node[]) => {
      parentChildrenToAppend.forEach(({ parentId, childId, childType }) => {
        const p = draft.find(n => n.id === parentId)
        if (p)
          p.data._children?.push({ nodeId: childId, nodeType: childType })
      })
      draft.push(...nodesToPaste)
    })

    setNodes(newNodes)
    setEdges([...edges, ...edgesToPaste])
    saveStateToHistory(WorkflowHistoryEvent.NodePaste, {
      nodeId: nodesToPaste?.[0]?.id,
    })
    handleSyncWorkflowDraft()
  }, [
    getNodesReadOnly,
    workflowStore,
    collaborativeWorkflow,
    reactflow,
    t,
    saveStateToHistory,
    handleSyncWorkflowDraft,
    handleNodeIterationChildrenCopy,
    handleNodeLoopChildrenCopy,
    getNodeDefaultValueForPaste,
    appDslVersion,
  ])

  const handleNodesDuplicate = useCallback(
    (nodeId?: string) => {
      if (getNodesReadOnly())
        return

      handleNodesCopy(nodeId)
      handleNodesPaste()
    },
    [getNodesReadOnly, handleNodesCopy, handleNodesPaste],
  )

  const handleNodesDelete = useCallback(() => {
    if (getNodesReadOnly())
      return

    const { nodes, edges } = collaborativeWorkflow.getState()

    const bundledNodes = nodes.filter(
      node => node.data._isBundled,
    )

    if (bundledNodes.length) {
      bundledNodes.forEach(node => handleNodeDelete(node.id))

      return
    }

    const edgeSelected = edges.some(edge => edge.selected)
    if (edgeSelected)
      return

    const selectedNode = nodes.find(
      node => node.data.selected,
    )

    if (selectedNode)
      handleNodeDelete(selectedNode.id)
  }, [collaborativeWorkflow, getNodesReadOnly, handleNodeDelete])

  const handleNodeResize = useCallback(
    (nodeId: string, params: ResizeParamsWithDirection) => {
      if (getNodesReadOnly())
        return

      const { nodes, setNodes } = collaborativeWorkflow.getState()
      const { x, y, width, height } = params

      const currentNode = nodes.find(n => n.id === nodeId)!
      const childrenNodes = nodes.filter(n =>
        currentNode.data._children?.find((c: any) => c.nodeId === n.id),
      )
      let rightNode: Node
      let bottomNode: Node

      childrenNodes.forEach((n) => {
        if (rightNode) {
          if (n.position.x + n.width! > rightNode.position.x + rightNode.width!)
            rightNode = n
        }
        else {
          rightNode = n
        }
        if (bottomNode) {
          if (
            n.position.y + n.height!
            > bottomNode.position.y + bottomNode.height!
          ) {
            bottomNode = n
          }
        }
        else {
          bottomNode = n
        }
      })

      if (rightNode! && bottomNode!) {
        const parentNode = nodes.find(n => n.id === rightNode.parentId)
        const paddingMap
          = parentNode?.data.type === BlockEnum.Iteration
            ? ITERATION_PADDING
            : LOOP_PADDING

        if (width < rightNode!.position.x + rightNode.width! + paddingMap.right)
          return
        if (
          height
          < bottomNode.position.y + bottomNode.height! + paddingMap.bottom
        ) {
          return
        }
      }
      const newNodes = produce(nodes, (draft) => {
        draft.forEach((n) => {
          if (n.id === nodeId) {
            n.data.width = width
            n.data.height = height
            n.width = width
            n.height = height
            n.position.x = x
            n.position.y = y
          }
        })
      })
      setNodes(newNodes)
      handleSyncWorkflowDraft()
      saveStateToHistory(WorkflowHistoryEvent.NodeResize, { nodeId })
    },
    [getNodesReadOnly, collaborativeWorkflow, handleSyncWorkflowDraft, saveStateToHistory],
  )

  const handleNodeDisconnect = useCallback(
    (nodeId: string) => {
      if (getNodesReadOnly())
        return

      const { nodes, setNodes, edges, setEdges } = collaborativeWorkflow.getState()
      const currentNode = nodes.find(node => node.id === nodeId)!
      const connectedEdges = getConnectedEdges([currentNode], edges)
      const nodesConnectedSourceOrTargetHandleIdsMap
        = getNodesConnectedSourceOrTargetHandleIdsMap(
          connectedEdges.map(edge => ({ type: 'remove', edge })),
          nodes,
        )
      const newNodes = produce(nodes, (draft: Node[]) => {
        draft.forEach((node) => {
          if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
            node.data = {
              ...node.data,
              ...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
            }
          }
        })
      })
      setNodes(newNodes)
      const newEdges = produce(edges, (draft) => {
        return draft.filter(
          edge =>
            !connectedEdges.some(connectedEdge => connectedEdge.id === edge.id),
        )
      })
      setEdges(newEdges)
      handleSyncWorkflowDraft()
      saveStateToHistory(WorkflowHistoryEvent.EdgeDelete)
    },
    [collaborativeWorkflow, getNodesReadOnly, handleSyncWorkflowDraft, saveStateToHistory],
  )

  const handleHistoryBack = useCallback(() => {
    if (getNodesReadOnly() || getWorkflowReadOnly())
      return

    undo()
    const { edges, nodes } = workflowHistoryStore.getState()
    if (edges.length === 0 && nodes.length === 0)
      return
    const { setNodes, setEdges } = collaborativeWorkflow.getState()

    const shouldBroadcast = collaborationManager.isConnected()
    setEdges(edges, shouldBroadcast)
    setNodes(nodes, shouldBroadcast, 'nodes:history-back')
    if (shouldBroadcast)
      collaborationManager.emitHistoryAction('undo')
    workflowStore.setState({ contextMenuTarget: undefined })
  }, [
    collaborativeWorkflow,
    workflowStore,
    undo,
    workflowHistoryStore,
    getNodesReadOnly,
    getWorkflowReadOnly,
  ])

  const handleHistoryForward = useCallback(() => {
    if (getNodesReadOnly() || getWorkflowReadOnly())
      return

    redo()
    const { edges, nodes } = workflowHistoryStore.getState()
    if (edges.length === 0 && nodes.length === 0)
      return
    const { setNodes, setEdges } = collaborativeWorkflow.getState()

    const shouldBroadcast = collaborationManager.isConnected()
    setEdges(edges, shouldBroadcast)
    setNodes(nodes, shouldBroadcast, 'nodes:history-forward')
    if (shouldBroadcast)
      collaborationManager.emitHistoryAction('redo')
    workflowStore.setState({ contextMenuTarget: undefined })
  }, [
    collaborativeWorkflow,
    redo,
    workflowStore,
    workflowHistoryStore,
    getNodesReadOnly,
    getWorkflowReadOnly,
  ])

  const [isDimming, setIsDimming] = useState(false)
  /** Add opacity-30 to all nodes except the nodeId */
  const dimOtherNodes = useCallback(() => {
    if (isDimming)
      return
    const { nodes, setNodes, edges, setEdges } = collaborativeWorkflow.getState()

    const selectedNode = nodes.find(n => n.data.selected)
    if (!selectedNode)
      return

    setIsDimming(true)

    // const workflowNodes = useStore(s => s.getNodes())
    const workflowNodes = nodes

    const usedVars = getNodeUsedVars(selectedNode)
    const dependencyNodes: Node[] = []
    usedVars.forEach((valueSelector) => {
      const node = workflowNodes.find(node => node.id === valueSelector?.[0])
      if (node) {
        if (!dependencyNodes.includes(node))
          dependencyNodes.push(node)
      }
    })

    const outgoers = getOutgoers(selectedNode as Node, nodes as Node[], edges)
    for (let currIdx = 0; currIdx < outgoers.length; currIdx++) {
      const node = outgoers[currIdx]
      const outgoersForNode = getOutgoers(node!, nodes as Node[], edges)
      outgoersForNode.forEach((item) => {
        const existed = outgoers.some(v => v.id === item.id)
        if (!existed)
          outgoers.push(item)
      })
    }

    const dependentNodes: Node[] = []
    outgoers.forEach((node) => {
      const usedVars = getNodeUsedVars(node)
      const used = usedVars.some(v => v?.[0] === selectedNode.id)
      if (used) {
        const existed = dependentNodes.some(v => v.id === node.id)
        if (!existed)
          dependentNodes.push(node)
      }
    })

    const dimNodes = [...dependencyNodes, ...dependentNodes, selectedNode]

    const newNodes = produce(nodes, (draft) => {
      draft.forEach((n) => {
        const dimNode = dimNodes.find(v => v.id === n.id)
        if (!dimNode)
          n.data._dimmed = true
      })
    })

    setNodes(newNodes)

    const tempEdges: Edge[] = []

    dependencyNodes.forEach((n) => {
      tempEdges.push({
        id: `tmp_${n.id}-source-${selectedNode.id}-target`,
        type: CUSTOM_EDGE,
        source: n.id,
        sourceHandle: 'source_tmp',
        target: selectedNode.id,
        targetHandle: 'target_tmp',
        animated: true,
        data: {
          sourceType: n.data.type,
          targetType: selectedNode.data.type,
          _isTemp: true,
          _connectedNodeIsHovering: true,
        },
      })
    })
    dependentNodes.forEach((n) => {
      tempEdges.push({
        id: `tmp_${selectedNode.id}-source-${n.id}-target`,
        type: CUSTOM_EDGE,
        source: selectedNode.id,
        sourceHandle: 'source_tmp',
        target: n.id,
        targetHandle: 'target_tmp',
        animated: true,
        data: {
          sourceType: selectedNode.data.type,
          targetType: n.data.type,
          _isTemp: true,
          _connectedNodeIsHovering: true,
        },
      })
    })

    const newEdges = produce(edges, (draft) => {
      draft.forEach((e) => {
        e.data._dimmed = true
      })
      draft.push(...tempEdges)
    })
    setEdges(newEdges)
  }, [isDimming, collaborativeWorkflow])

  /** Restore all nodes to full opacity */
  const undimAllNodes = useCallback(() => {
    const { nodes, setNodes, edges, setEdges } = collaborativeWorkflow.getState()
    setIsDimming(false)

    const newNodes = produce(nodes, (draft) => {
      draft.forEach((n) => {
        n.data._dimmed = false
      })
    })

    setNodes(newNodes)

    const newEdges = produce(
      edges.filter(e => !e.data._isTemp),
      (draft) => {
        draft.forEach((e) => {
          e.data._dimmed = false
        })
      },
    )
    setEdges(newEdges)
  }, [collaborativeWorkflow])

  return {
    handleNodeDragStart,
    handleNodeDrag,
    handleNodeDragStop,
    handleNodeEnter,
    handleNodeLeave,
    handleNodeSelect,
    handleNodeClick,
    handleNodeConnect,
    handleNodeConnectStart,
    handleNodeConnectEnd,
    handleNodeDelete,
    handleNodeChange,
    handleNodeAdd,
    handleNodesCancelSelected,
    handleNodeContextMenu,
    handleNodesCopy,
    handleNodesPaste,
    handleNodesDuplicate,
    handleNodesDelete,
    handleNodeResize,
    handleNodeDisconnect,
    handleHistoryBack,
    handleHistoryForward,
    dimOtherNodes,
    undimAllNodes,
  }
}
