/**
 * Canvas V2 Only
 * @TODO Remove this notice when Canvas V2 is the only one in use
 */

import type {
	AddedNodesAndConnections,
	IExecutionResponse,
	INodeUi,
	ITag,
	IUsedCredential,
	IWorkflowData,
	IWorkflowDataUpdate,
	IWorkflowDb,
	XYPosition,
} from '@/Interface';
import { useDataSchema } from '@/composables/useDataSchema';
import { useExternalHooks } from '@/composables/useExternalHooks';
import { useI18n } from '@/composables/useI18n';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { type PinDataSource, usePinnedData } from '@/composables/usePinnedData';
import { useTelemetry } from '@/composables/useTelemetry';
import { useToast } from '@/composables/useToast';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import {
	EnterpriseEditionFeature,
	FORM_TRIGGER_NODE_TYPE,
	STICKY_NODE_TYPE,
	UPDATE_WEBHOOK_ID_NODE_TYPES,
	WEBHOOK_NODE_TYPE,
} from '@/constants';
import {
	AddConnectionCommand,
	AddNodeCommand,
	MoveNodeCommand,
	RemoveConnectionCommand,
	RemoveNodeCommand,
	RenameNodeCommand,
} from '@/models/history';
import { useCanvasStore } from '@/stores/canvas.store';
import { useCredentialsStore } from '@/stores/credentials.store';
import { useExecutionsStore } from '@/stores/executions.store';
import { useHistoryStore } from '@/stores/history.store';
import { useNDVStore } from '@/stores/ndv.store';
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useRootStore } from '@/stores/root.store';
import { useSettingsStore } from '@/stores/settings.store';
import { useTagsStore } from '@/stores/tags.store';
import { useUIStore } from '@/stores/ui.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import type {
	CanvasConnection,
	CanvasConnectionCreateData,
	CanvasConnectionPort,
	CanvasNode,
	CanvasNodeMoveEvent,
} from '@/types';
import { CanvasConnectionMode } from '@/types';
import {
	createCanvasConnectionHandleString,
	mapCanvasConnectionToLegacyConnection,
	mapLegacyConnectionsToCanvasConnections,
	mapLegacyConnectionToCanvasConnection,
	parseCanvasConnectionHandleString,
} from '@/utils/canvasUtilsV2';
import * as NodeViewUtils from '@/utils/nodeViewUtils';
import {
	CONFIGURABLE_NODE_SIZE,
	CONFIGURATION_NODE_SIZE,
	DEFAULT_NODE_SIZE,
	generateOffsets,
	PUSH_NODES_OFFSET,
} from '@/utils/nodeViewUtils';
import type { Connection } from '@vue-flow/core';
import type {
	IConnection,
	IConnections,
	IDataObject,
	INode,
	INodeConnections,
	INodeCredentials,
	INodeInputConfiguration,
	INodeOutputConfiguration,
	INodeTypeDescription,
	INodeTypeNameVersion,
	IPinData,
	IWorkflowBase,
	NodeInputConnections,
	NodeParameterValueType,
	Workflow,
} from 'n8n-workflow';
import { deepCopy, NodeConnectionType, NodeHelpers, TelemetryHelpers } from 'n8n-workflow';
import { computed, nextTick, ref } from 'vue';
import type { useRouter } from 'vue-router';
import { useClipboard } from '@/composables/useClipboard';
import { useUniqueNodeName } from '@/composables/useUniqueNodeName';
import { isPresent } from '../utils/typesUtils';

type AddNodeData = Partial<INodeUi> & {
	type: string;
};

type AddNodeDataWithTypeVersion = AddNodeData & {
	typeVersion: INodeUi['typeVersion'];
};

type AddNodesBaseOptions = {
	dragAndDrop?: boolean;
	trackHistory?: boolean;
	keepPristine?: boolean;
	telemetry?: boolean;
};

type AddNodesOptions = AddNodesBaseOptions & {
	position?: XYPosition;
	trackBulk?: boolean;
};

type AddNodeOptions = AddNodesBaseOptions & {
	openNDV?: boolean;
	isAutoAdd?: boolean;
};

export function useCanvasOperations({ router }: { router: ReturnType<typeof useRouter> }) {
	const rootStore = useRootStore();
	const workflowsStore = useWorkflowsStore();
	const credentialsStore = useCredentialsStore();
	const historyStore = useHistoryStore();
	const uiStore = useUIStore();
	const ndvStore = useNDVStore();
	const nodeTypesStore = useNodeTypesStore();
	const canvasStore = useCanvasStore();
	const settingsStore = useSettingsStore();
	const tagsStore = useTagsStore();
	const nodeCreatorStore = useNodeCreatorStore();
	const executionsStore = useExecutionsStore();

	const i18n = useI18n();
	const toast = useToast();
	const workflowHelpers = useWorkflowHelpers({ router });
	const nodeHelpers = useNodeHelpers();
	const telemetry = useTelemetry();
	const externalHooks = useExternalHooks();
	const clipboard = useClipboard();
	const { uniqueNodeName } = useUniqueNodeName();

	const lastClickPosition = ref<XYPosition>([0, 0]);

	const preventOpeningNDV = !!localStorage.getItem('NodeView.preventOpeningNDV');

	const editableWorkflow = computed(() => workflowsStore.workflow);
	const editableWorkflowObject = computed(() => workflowsStore.getCurrentWorkflow());

	const triggerNodes = computed<INodeUi[]>(() => {
		return workflowsStore.workflowTriggerNodes;
	});

	/**
	 * Node operations
	 */

	function updateNodesPosition(
		events: CanvasNodeMoveEvent[],
		{ trackHistory = false, trackBulk = true } = {},
	) {
		if (trackHistory && trackBulk) {
			historyStore.startRecordingUndo();
		}

		events.forEach(({ id, position }) => {
			updateNodePosition(id, position, { trackHistory });
		});

		if (trackHistory && trackBulk) {
			historyStore.stopRecordingUndo();
		}
	}

	function updateNodePosition(
		id: string,
		position: CanvasNode['position'],
		{ trackHistory = false } = {},
	) {
		const node = workflowsStore.getNodeById(id);
		if (!node) {
			return;
		}

		const oldPosition: XYPosition = [...node.position];
		const newPosition: XYPosition = [position.x, position.y];

		workflowsStore.setNodePositionById(id, newPosition);

		if (trackHistory) {
			historyStore.pushCommandToUndo(new MoveNodeCommand(node.name, oldPosition, newPosition));
		}
	}

	function revertUpdateNodePosition(nodeName: string, position: CanvasNode['position']) {
		const node = workflowsStore.getNodeByName(nodeName);
		if (!node) {
			return;
		}

		updateNodePosition(node.id, position);
	}

	async function renameNode(
		currentName: string,
		newName: string,
		{ trackHistory = false, trackBulk = true } = {},
	) {
		if (currentName === newName) {
			return;
		}

		if (trackHistory && trackBulk) {
			historyStore.startRecordingUndo();
		}

		newName = uniqueNodeName(newName);

		// Rename the node and update the connections
		const workflow = workflowsStore.getCurrentWorkflow(true);
		workflow.renameNode(currentName, newName);

		if (trackHistory) {
			historyStore.pushCommandToUndo(new RenameNodeCommand(currentName, newName));
		}

		// Update also last selected node and execution data
		workflowsStore.renameNodeSelectedAndExecution({ old: currentName, new: newName });

		workflowsStore.setNodes(Object.values(workflow.nodes));
		workflowsStore.setConnections(workflow.connectionsBySourceNode);

		const isRenamingActiveNode = ndvStore.activeNodeName === currentName;
		if (isRenamingActiveNode) {
			ndvStore.activeNodeName = newName;
		}

		if (trackHistory && trackBulk) {
			historyStore.stopRecordingUndo();
		}
	}

	async function revertRenameNode(currentName: string, previousName: string) {
		await renameNode(currentName, previousName);
	}

	function connectAdjacentNodes(id: string, { trackHistory = false } = {}) {
		const node = workflowsStore.getNodeById(id);

		if (!node) {
			return;
		}

		const outputConnectionsByType = workflowsStore.outgoingConnectionsByNodeName(node.name);
		const incomingConnectionsByType = workflowsStore.incomingConnectionsByNodeName(node.name);

		for (const [type, incomingConnectionsByInputIndex] of Object.entries(
			incomingConnectionsByType,
		) as Array<[NodeConnectionType, NodeInputConnections]>) {
			// Only connect nodes connected to the first input of a type
			for (const incomingConnection of incomingConnectionsByInputIndex.at(0) ?? []) {
				const incomingNodeId = workflowsStore.getNodeByName(incomingConnection.node)?.id;

				if (!incomingNodeId) continue;

				// Only connect to nodes connected to the first output of a type
				// For example on an If node, connect to the "true" main output
				for (const outgoingConnection of outputConnectionsByType[type]?.at(0) ?? []) {
					const outgoingNodeId = workflowsStore.getNodeByName(outgoingConnection.node)?.id;

					if (!outgoingNodeId) continue;

					if (trackHistory) {
						historyStore.pushCommandToUndo(
							new AddConnectionCommand([
								{
									node: incomingConnection.node,
									type,
									index: 0,
								},
								{
									node: outgoingConnection.node,
									type,
									index: 0,
								},
							]),
						);
					}

					createConnection({
						source: incomingNodeId,
						sourceHandle: createCanvasConnectionHandleString({
							mode: CanvasConnectionMode.Output,
							type,
						}),
						target: outgoingNodeId,
						targetHandle: createCanvasConnectionHandleString({
							mode: CanvasConnectionMode.Input,
							type,
						}),
					});
				}
			}
		}
	}

	function deleteNode(id: string, { trackHistory = false, trackBulk = true } = {}) {
		const node = workflowsStore.getNodeById(id);
		if (!node) {
			return;
		}

		if (trackHistory && trackBulk) {
			historyStore.startRecordingUndo();
		}

		if (uiStore.lastInteractedWithNodeId === id) {
			uiStore.lastInteractedWithNodeId = undefined;
		}

		connectAdjacentNodes(id, { trackHistory });
		deleteConnectionsByNodeId(id, { trackHistory, trackBulk: false });

		workflowsStore.removeNodeExecutionDataById(id);
		workflowsStore.removeNodeById(id);

		if (trackHistory) {
			historyStore.pushCommandToUndo(new RemoveNodeCommand(node));

			if (trackBulk) {
				historyStore.stopRecordingUndo();
			}
		}

		trackDeleteNode(id);
	}

	function deleteNodes(ids: string[], { trackHistory = true, trackBulk = true } = {}) {
		if (trackHistory && trackBulk) {
			historyStore.startRecordingUndo();
		}

		ids.forEach((id) => deleteNode(id, { trackHistory, trackBulk: false }));

		if (trackHistory && trackBulk) {
			historyStore.stopRecordingUndo();
		}
	}

	function revertDeleteNode(node: INodeUi) {
		workflowsStore.addNode(node);
	}

	function trackDeleteNode(id: string) {
		const node = workflowsStore.getNodeById(id);
		if (!node) {
			return;
		}

		if (node.type === STICKY_NODE_TYPE) {
			telemetry.track('User deleted workflow note', {
				workflow_id: workflowsStore.workflowId,
			});
		} else {
			void externalHooks.run('node.deleteNode', { node });
			telemetry.track('User deleted node', {
				node_type: node.type,
				workflow_id: workflowsStore.workflowId,
			});
		}
	}

	function setNodeActive(id: string) {
		const node = workflowsStore.getNodeById(id);
		if (!node) {
			return;
		}

		setNodeActiveByName(node.name);
	}

	function setNodeActiveByName(name: string) {
		ndvStore.activeNodeName = name;
	}

	function setNodeParameters(id: string, parameters: Record<string, unknown>) {
		const node = workflowsStore.getNodeById(id);
		if (!node) {
			return;
		}

		workflowsStore.setNodeParameters(
			{
				name: node.name,
				value: parameters as NodeParameterValueType,
			},
			true,
		);
	}

	function setNodeSelected(id?: string) {
		if (!id) {
			uiStore.lastInteractedWithNodeId = undefined;
			uiStore.lastSelectedNode = '';
			return;
		}

		const node = workflowsStore.getNodeById(id);
		if (!node) {
			return;
		}

		uiStore.lastInteractedWithNodeId = id;
		uiStore.lastSelectedNode = node.name;
	}

	function toggleNodesDisabled(ids: string[], { trackHistory = true, trackBulk = true } = {}) {
		if (trackHistory && trackBulk) {
			historyStore.startRecordingUndo();
		}

		const nodes = workflowsStore.getNodesByIds(ids);
		nodeHelpers.disableNodes(nodes, { trackHistory, trackBulk: false });

		if (trackHistory && trackBulk) {
			historyStore.stopRecordingUndo();
		}
	}

	function revertToggleNodeDisabled(nodeName: string) {
		const node = workflowsStore.getNodeByName(nodeName);
		if (node) {
			nodeHelpers.disableNodes([node]);
		}
	}

	function toggleNodesPinned(
		ids: string[],
		source: PinDataSource,
		{ trackHistory = true, trackBulk = true } = {},
	) {
		if (trackHistory && trackBulk) {
			historyStore.startRecordingUndo();
		}

		const nodes = workflowsStore.getNodesByIds(ids);
		const nextStatePinned = nodes.some((node) => !workflowsStore.pinDataByNodeName(node.name));

		for (const node of nodes) {
			const pinnedDataForNode = usePinnedData(node);
			if (nextStatePinned) {
				const dataToPin = useDataSchema().getInputDataWithPinned(node);
				if (dataToPin.length !== 0) {
					pinnedDataForNode.setData(dataToPin, source);
				}
			} else {
				pinnedDataForNode.unsetData(source);
			}
		}

		if (trackHistory && trackBulk) {
			historyStore.stopRecordingUndo();
		}
	}

	function requireNodeTypeDescription(
		type: INodeUi['type'],
		version?: INodeUi['typeVersion'],
	): INodeTypeDescription {
		return (
			nodeTypesStore.getNodeType(type, version) ?? {
				properties: [],
				displayName: type,
				name: type,
				group: [],
				description: '',
				version: version ?? 1,
				defaults: {},
				inputs: [],
				outputs: [],
			}
		);
	}

	async function addNodes(nodes: AddedNodesAndConnections['nodes'], options: AddNodesOptions = {}) {
		let insertPosition = options.position;
		let lastAddedNode: INodeUi | undefined;
		const addedNodes: INodeUi[] = [];

		const nodesWithTypeVersion = nodes.map((node) => {
			const typeVersion =
				node.typeVersion ?? resolveNodeVersion(requireNodeTypeDescription(node.type));
			return {
				...node,
				typeVersion,
			};
		});

		await loadNodeTypesProperties(nodesWithTypeVersion);

		if (options.trackHistory && options.trackBulk) {
			historyStore.startRecordingUndo();
		}

		for (const nodeAddData of nodesWithTypeVersion) {
			const { isAutoAdd, openDetail: openNDV, ...node } = nodeAddData;
			const position = node.position ?? insertPosition;
			const nodeTypeDescription = requireNodeTypeDescription(node.type, node.typeVersion);

			try {
				const newNode = addNode(
					{
						...node,
						position,
					},
					nodeTypeDescription,
					{
						...options,
						openNDV,
						isAutoAdd,
					},
				);
				lastAddedNode = newNode;
				addedNodes.push(newNode);
			} catch (error) {
				toast.showError(error, i18n.baseText('error'));
				console.error(error);
				continue;
			}

			// When we're adding multiple nodes, increment the X position for the next one
			insertPosition = [
				lastAddedNode.position[0] + NodeViewUtils.NODE_SIZE * 2 + NodeViewUtils.GRID_SIZE,
				lastAddedNode.position[1],
			];
		}

		if (lastAddedNode) {
			updatePositionForNodeWithMultipleInputs(lastAddedNode);
		}

		if (options.trackHistory && options.trackBulk) {
			historyStore.stopRecordingUndo();
		}

		if (!options.keepPristine) {
			uiStore.stateIsDirty = true;
		}

		return addedNodes;
	}

	function updatePositionForNodeWithMultipleInputs(node: INodeUi) {
		const inputNodes = editableWorkflowObject.value.getParentNodesByDepth(node.name, 1);

		if (inputNodes.length > 1) {
			inputNodes.slice(1).forEach((inputNode, index) => {
				const nodeUi = workflowsStore.getNodeByName(inputNode.name);
				if (!nodeUi) return;

				updateNodePosition(nodeUi.id, {
					x: nodeUi.position[0],
					y: nodeUi.position[1] + 100 * (index + 1),
				});
			});
		}
	}

	/**
	 * Check if maximum allowed number of this type of node has been reached
	 */
	function checkMaxNodesOfTypeReached(nodeTypeDescription: INodeTypeDescription) {
		if (
			nodeTypeDescription.maxNodes !== undefined &&
			workflowHelpers.getNodeTypeCount(nodeTypeDescription.name) >= nodeTypeDescription.maxNodes
		) {
			throw new Error(
				i18n.baseText('nodeView.showMessage.showMaxNodeTypeError.message', {
					adjustToNumber: nodeTypeDescription.maxNodes,
					interpolate: { nodeTypeDataDisplayName: nodeTypeDescription.displayName },
				}),
			);
		}
	}

	function addNode(
		node: AddNodeDataWithTypeVersion,
		nodeTypeDescription: INodeTypeDescription,
		options: AddNodeOptions = {},
	): INodeUi {
		checkMaxNodesOfTypeReached(nodeTypeDescription);

		const nodeData = resolveNodeData(node, nodeTypeDescription);
		if (!nodeData) {
			throw new Error(i18n.baseText('nodeViewV2.showError.failedToCreateNode'));
		}

		workflowsStore.addNode(nodeData);

		if (options.trackHistory) {
			historyStore.pushCommandToUndo(new AddNodeCommand(nodeData));
		}

		if (!options.isAutoAdd) {
			createConnectionToLastInteractedWithNode(nodeData, options);
		}

		void nextTick(() => {
			if (!options.keepPristine) {
				uiStore.stateIsDirty = true;
			}

			workflowsStore.setNodePristine(nodeData.name, true);
			nodeHelpers.matchCredentials(nodeData);
			nodeHelpers.updateNodeParameterIssues(nodeData);
			nodeHelpers.updateNodeCredentialIssues(nodeData);
			nodeHelpers.updateNodeInputIssues(nodeData);

			if (options.telemetry) {
				trackAddNode(nodeData, options);
			}

			if (nodeData.type !== STICKY_NODE_TYPE) {
				void externalHooks.run('nodeView.addNodeButton', { nodeTypeName: nodeData.type });

				if (options.openNDV && !preventOpeningNDV) {
					ndvStore.setActiveNodeName(nodeData.name);
				}
			}
		});

		return nodeData;
	}

	async function revertAddNode(nodeName: string) {
		const node = workflowsStore.getNodeByName(nodeName);
		if (!node) {
			return;
		}

		deleteNode(node.id);
	}

	function createConnectionToLastInteractedWithNode(node: INodeUi, options: AddNodeOptions = {}) {
		const lastInteractedWithNode = uiStore.lastInteractedWithNode;
		if (!lastInteractedWithNode) {
			return;
		}

		const lastInteractedWithNodeId = lastInteractedWithNode.id;
		const lastInteractedWithNodeConnection = uiStore.lastInteractedWithNodeConnection;
		const lastInteractedWithNodeHandle = uiStore.lastInteractedWithNodeHandle;

		// If we have a specific endpoint to connect to
		if (lastInteractedWithNodeHandle) {
			const { type: connectionType, mode } = parseCanvasConnectionHandleString(
				lastInteractedWithNodeHandle,
			);

			const nodeId = node.id;
			const nodeHandle = createCanvasConnectionHandleString({
				mode: CanvasConnectionMode.Input,
				type: connectionType,
				index: 0,
			});

			if (mode === CanvasConnectionMode.Input) {
				createConnection({
					source: nodeId,
					sourceHandle: nodeHandle,
					target: lastInteractedWithNodeId,
					targetHandle: lastInteractedWithNodeHandle,
				});
			} else {
				createConnection({
					source: lastInteractedWithNodeId,
					sourceHandle: lastInteractedWithNodeHandle,
					target: nodeId,
					targetHandle: nodeHandle,
				});
			}
		} else {
			// If a node is last selected then connect between the active and its child ones
			// Connect active node to the newly created one
			createConnection({
				source: lastInteractedWithNodeId,
				sourceHandle: createCanvasConnectionHandleString({
					mode: CanvasConnectionMode.Output,
					type: NodeConnectionType.Main,
					index: 0,
				}),
				target: node.id,
				targetHandle: createCanvasConnectionHandleString({
					mode: CanvasConnectionMode.Input,
					type: NodeConnectionType.Main,
					index: 0,
				}),
			});
		}

		if (lastInteractedWithNodeConnection) {
			deleteConnection(lastInteractedWithNodeConnection, { trackHistory: options.trackHistory });

			const targetNode = workflowsStore.getNodeById(lastInteractedWithNodeConnection.target);
			if (targetNode) {
				createConnection({
					source: node.id,
					sourceHandle: createCanvasConnectionHandleString({
						mode: CanvasConnectionMode.Input,
						type: NodeConnectionType.Main,
						index: 0,
					}),
					target: lastInteractedWithNodeConnection.target,
					targetHandle: lastInteractedWithNodeConnection.targetHandle,
				});
			}
		}
	}

	function trackAddNode(nodeData: INodeUi, options: AddNodeOptions) {
		switch (nodeData.type) {
			case STICKY_NODE_TYPE:
				trackAddStickyNoteNode();
				break;
			default:
				trackAddDefaultNode(nodeData, options);
		}
	}

	function trackAddStickyNoteNode() {
		telemetry.track('User inserted workflow note', {
			workflow_id: workflowsStore.workflowId,
		});
	}

	function trackAddDefaultNode(nodeData: INodeUi, options: AddNodeOptions) {
		nodeCreatorStore.onNodeAddedToCanvas({
			node_type: nodeData.type,
			node_version: nodeData.typeVersion,
			is_auto_add: options.isAutoAdd,
			workflow_id: workflowsStore.workflowId,
			drag_and_drop: options.dragAndDrop,
			input_node_type: uiStore.lastInteractedWithNode
				? uiStore.lastInteractedWithNode.type
				: undefined,
		});
	}

	/**
	 * Resolves the data for a new node
	 */
	function resolveNodeData(
		node: AddNodeDataWithTypeVersion,
		nodeTypeDescription: INodeTypeDescription,
	) {
		const id = node.id ?? nodeHelpers.assignNodeId(node as INodeUi);
		const name = node.name ?? (nodeTypeDescription.defaults.name as string);
		const type = nodeTypeDescription.name;
		const typeVersion = node.typeVersion;
		const position = resolveNodePosition(node as INodeUi, nodeTypeDescription);
		const disabled = node.disabled ?? false;
		const parameters = node.parameters ?? {};

		const nodeData: INodeUi = {
			...node,
			id,
			name,
			type,
			typeVersion,
			position,
			disabled,
			parameters,
		};

		resolveNodeName(nodeData);
		resolveNodeParameters(nodeData, nodeTypeDescription);
		resolveNodeWebhook(nodeData, nodeTypeDescription);

		return nodeData;
	}

	async function loadNodeTypesProperties(
		nodes: Array<Pick<INodeUi, 'type' | 'typeVersion'>>,
	): Promise<void> {
		const allNodeTypeDescriptions: INodeTypeDescription[] = nodeTypesStore.allNodeTypes;

		const nodesToBeFetched: INodeTypeNameVersion[] = [];
		allNodeTypeDescriptions.forEach((nodeTypeDescription) => {
			const nodeVersions = Array.isArray(nodeTypeDescription.version)
				? nodeTypeDescription.version
				: [nodeTypeDescription.version];
			if (
				!!nodes.find(
					(n) => n.type === nodeTypeDescription.name && nodeVersions.includes(n.typeVersion),
				) &&
				!nodeTypeDescription.hasOwnProperty('properties')
			) {
				nodesToBeFetched.push({
					name: nodeTypeDescription.name,
					version: Array.isArray(nodeTypeDescription.version)
						? nodeTypeDescription.version.slice(-1)[0]
						: nodeTypeDescription.version,
				});
			}
		});

		if (nodesToBeFetched.length > 0) {
			// Only call API if node information is actually missing
			await nodeTypesStore.getNodesInformation(nodesToBeFetched);
		}
	}

	function resolveNodeVersion(nodeTypeDescription: INodeTypeDescription) {
		let nodeVersion = nodeTypeDescription.defaultVersion;

		if (typeof nodeVersion === 'undefined') {
			nodeVersion = Array.isArray(nodeTypeDescription.version)
				? nodeTypeDescription.version.slice(-1)[0]
				: nodeTypeDescription.version;
		}

		return nodeVersion;
	}

	function resolveNodeParameters(node: INodeUi, nodeTypeDescription: INodeTypeDescription) {
		const nodeParameters = NodeHelpers.getNodeParameters(
			nodeTypeDescription?.properties ?? [],
			node.parameters,
			true,
			false,
			node,
		);

		node.parameters = nodeParameters ?? {};
	}

	function resolveNodePosition(
		node: Omit<INodeUi, 'position'> & { position?: INodeUi['position'] },
		nodeTypeDescription: INodeTypeDescription,
	) {
		let position: XYPosition | undefined = node.position;
		let pushOffsets: XYPosition = [40, 40];

		if (position) {
			return NodeViewUtils.getNewNodePosition(workflowsStore.allNodes, position, pushOffsets);
		}

		// Available when
		// - clicking the plus button of a node handle
		// - dragging an edge / connection of a node handle
		// - selecting a node, adding a node via the node creator
		const lastInteractedWithNode = uiStore.lastInteractedWithNode;
		// Available when clicking the plus button of a node edge / connection
		const lastInteractedWithNodeConnection = uiStore.lastInteractedWithNodeConnection;
		// Available when dragging an edge / connection from a node
		const lastInteractedWithNodeHandle = uiStore.lastInteractedWithNodeHandle;

		const { type: connectionType, index: connectionIndex } = parseCanvasConnectionHandleString(
			lastInteractedWithNodeHandle ?? lastInteractedWithNodeConnection?.sourceHandle ?? '',
		);

		const nodeSize =
			connectionType === NodeConnectionType.Main ? DEFAULT_NODE_SIZE : CONFIGURATION_NODE_SIZE;

		if (lastInteractedWithNode) {
			const lastInteractedWithNodeTypeDescription = nodeTypesStore.getNodeType(
				lastInteractedWithNode.type,
				lastInteractedWithNode.typeVersion,
			);
			const lastInteractedWithNodeObject = editableWorkflowObject.value.getNode(
				lastInteractedWithNode.name,
			);

			const newNodeInsertPosition = uiStore.lastCancelledConnectionPosition;
			if (newNodeInsertPosition) {
				// When pulling / cancelling a connection.
				// The new node should be placed at the same position as the mouse up event,
				// designated by the `newNodeInsertPosition` value.

				const xOffset = connectionType === NodeConnectionType.Main ? 0 : -nodeSize[0] / 2;
				const yOffset = connectionType === NodeConnectionType.Main ? -nodeSize[1] / 2 : 0;

				position = [newNodeInsertPosition[0] + xOffset, newNodeInsertPosition[1] + yOffset];

				uiStore.lastCancelledConnectionPosition = undefined;
			} else if (lastInteractedWithNodeTypeDescription && lastInteractedWithNodeObject) {
				// When
				// - clicking the plus button of a node handle
				// - clicking the plus button of a node edge / connection
				// - selecting a node, adding a node via the node creator

				const lastInteractedWithNodeInputs = NodeHelpers.getNodeInputs(
					editableWorkflowObject.value,
					lastInteractedWithNodeObject,
					lastInteractedWithNodeTypeDescription,
				);
				const lastInteractedWithNodeInputTypes = NodeHelpers.getConnectionTypes(
					lastInteractedWithNodeInputs,
				);

				const lastInteractedWithNodeScopedInputTypes = (
					lastInteractedWithNodeInputTypes || []
				).filter((input) => input !== NodeConnectionType.Main);

				const lastInteractedWithNodeOutputs = NodeHelpers.getNodeOutputs(
					editableWorkflowObject.value,
					lastInteractedWithNodeObject,
					lastInteractedWithNodeTypeDescription,
				);
				const lastInteractedWithNodeOutputTypes = NodeHelpers.getConnectionTypes(
					lastInteractedWithNodeOutputs,
				);

				const lastInteractedWithNodeMainOutputs = lastInteractedWithNodeOutputTypes.filter(
					(output) => output === NodeConnectionType.Main,
				);

				let yOffset = 0;
				if (lastInteractedWithNodeConnection) {
					// When clicking the plus button of a node edge / connection
					// Compute the y offset for the new node based on the number of main outputs of the source node
					// and shift the downstream nodes accordingly

					shiftDownstreamNodesPosition(lastInteractedWithNode.name, PUSH_NODES_OFFSET, {
						trackHistory: true,
					});
				}

				if (lastInteractedWithNodeMainOutputs.length > 1) {
					const yOffsetValues = generateOffsets(
						lastInteractedWithNodeMainOutputs.length,
						NodeViewUtils.NODE_SIZE,
						NodeViewUtils.GRID_SIZE,
					);

					yOffset = yOffsetValues[connectionIndex];
				}

				let outputs: Array<NodeConnectionType | INodeOutputConfiguration> = [];
				try {
					// It fails when the outputs are an expression. As those nodes have
					// normally no outputs by default and the only reason we need the
					// outputs here is to calculate the position, it is fine to assume
					// that they have no outputs and are so treated as a regular node
					// with only "main" outputs.
					outputs = NodeHelpers.getNodeOutputs(
						editableWorkflowObject.value,
						node as INode,
						nodeTypeDescription,
					);
				} catch (e) {}
				const outputTypes = NodeHelpers.getConnectionTypes(outputs);

				pushOffsets = [100, 0];

				if (
					outputTypes.length > 0 &&
					outputTypes.every((outputName) => outputName !== NodeConnectionType.Main)
				) {
					// When the added node has only non-main outputs (configuration nodes)
					// We want to place the new node directly below the last interacted with node.

					const scopedConnectionIndex = lastInteractedWithNodeScopedInputTypes.findIndex(
						(inputType) => outputs[0] === inputType,
					);

					const lastInteractedWithNodeWidthDivisions = Math.max(
						lastInteractedWithNodeScopedInputTypes.length + 1,
						1,
					);

					position = [
						lastInteractedWithNode.position[0] +
							(CONFIGURABLE_NODE_SIZE[0] / lastInteractedWithNodeWidthDivisions) *
								(scopedConnectionIndex + 1) -
							nodeSize[0] / 2,
						lastInteractedWithNode.position[1] + PUSH_NODES_OFFSET,
					];
				} else {
					// When the node has only main outputs, mixed outputs, or no outputs at all
					// We want to place the new node directly to the right of the last interacted with node.

					let pushOffset = PUSH_NODES_OFFSET;
					if (
						!!lastInteractedWithNodeInputTypes.find((input) => input !== NodeConnectionType.Main)
					) {
						// If the node has scoped inputs, push it down a bit more
						pushOffset += 140;
					}

					// If a node is active then add the new node directly after the current one
					position = [
						lastInteractedWithNode.position[0] + pushOffset,
						lastInteractedWithNode.position[1] + yOffset,
					];
				}
			}
		}

		if (!position) {
			if (nodeTypesStore.isTriggerNode(node.type) && triggerNodes.value.length === 0) {
				// When added node is a trigger, and it's the first one added to the canvas
				// we place it at root to replace the canvas add button

				position = [0, 0];
			} else {
				// When no position is set, we place the node at the last clicked position

				position = lastClickPosition.value;
			}
		}

		return NodeViewUtils.getNewNodePosition(workflowsStore.allNodes, position, pushOffsets);
	}

	function resolveNodeName(node: INodeUi) {
		const localizedName = i18n.localizeNodeName(node.name, node.type);

		node.name = uniqueNodeName(localizedName);
	}

	function resolveNodeWebhook(node: INodeUi, nodeTypeDescription: INodeTypeDescription) {
		if (nodeTypeDescription.webhooks?.length && !node.webhookId) {
			nodeHelpers.assignWebhookId(node);
		}

		// if it's a webhook and the path is empty set the UUID as the default path
		if (
			[WEBHOOK_NODE_TYPE, FORM_TRIGGER_NODE_TYPE].includes(node.type) &&
			node.parameters.path === ''
		) {
			node.parameters.path = node.webhookId as string;
		}
	}

	/**
	 * Moves all downstream nodes of a node
	 */
	function shiftDownstreamNodesPosition(
		sourceNodeName: string,
		margin: number,
		{ trackHistory = false }: { trackHistory?: boolean },
	) {
		const sourceNode = workflowsStore.nodesByName[sourceNodeName];
		const checkNodes = workflowHelpers.getConnectedNodes(
			'downstream',
			editableWorkflowObject.value,
			sourceNodeName,
		);
		for (const nodeName of checkNodes) {
			const node = workflowsStore.nodesByName[nodeName];
			if (node.position[0] < sourceNode.position[0]) {
				continue;
			}

			updateNodePosition(
				node.id,
				{
					x: node.position[0] + margin,
					y: node.position[1],
				},
				{ trackHistory },
			);
		}
	}

	/**
	 * Connection operations
	 */

	function createConnection(
		connection: Connection,
		{ trackHistory = false, keepPristine = false } = {},
	) {
		const sourceNode = workflowsStore.getNodeById(connection.source);
		const targetNode = workflowsStore.getNodeById(connection.target);
		if (!sourceNode || !targetNode) {
			return;
		}

		if (trackHistory) {
			historyStore.pushCommandToUndo(
				new AddConnectionCommand(
					mapCanvasConnectionToLegacyConnection(sourceNode, targetNode, connection),
				),
			);
		}

		const mappedConnection = mapCanvasConnectionToLegacyConnection(
			sourceNode,
			targetNode,
			connection,
		);

		if (!isConnectionAllowed(sourceNode, targetNode, mappedConnection[0], mappedConnection[1])) {
			return;
		}

		workflowsStore.addConnection({
			connection: mappedConnection,
		});

		void nextTick(() => {
			nodeHelpers.updateNodeInputIssues(sourceNode);
			nodeHelpers.updateNodeInputIssues(targetNode);
		});

		if (!keepPristine) {
			uiStore.stateIsDirty = true;
		}
	}

	function revertCreateConnection(connection: [IConnection, IConnection]) {
		const sourceNodeName = connection[0].node;
		const sourceNode = workflowsStore.getNodeByName(sourceNodeName);
		const targetNodeName = connection[1].node;
		const targetNode = workflowsStore.getNodeByName(targetNodeName);

		if (!sourceNode || !targetNode) {
			return;
		}

		deleteConnection(mapLegacyConnectionToCanvasConnection(sourceNode, targetNode, connection));
	}

	function deleteConnectionsByNodeId(
		targetNodeId: string,
		{ trackHistory = false, trackBulk = true } = {},
	) {
		const targetNode = workflowsStore.getNodeById(targetNodeId);
		if (!targetNode) {
			return;
		}

		if (trackHistory && trackBulk) {
			historyStore.startRecordingUndo();
		}

		const connections = workflowsStore.workflow.connections;
		for (const nodeName of Object.keys(connections)) {
			const node = workflowsStore.getNodeByName(nodeName);
			if (!node) {
				continue;
			}

			for (const type of Object.keys(connections[nodeName])) {
				for (const index of Object.keys(connections[nodeName][type])) {
					const connectionsToDelete = connections[nodeName][type][parseInt(index, 10)] ?? [];
					for (const connectionIndex of Object.keys(connectionsToDelete)) {
						const connectionData = connectionsToDelete[parseInt(connectionIndex, 10)];
						if (!connectionData) {
							continue;
						}

						const connectionDataNode = workflowsStore.getNodeByName(connectionData.node);
						if (
							connectionDataNode &&
							(connectionDataNode.id === targetNode.id || node.name === targetNode.name)
						) {
							deleteConnection(
								{
									source: node.id,
									sourceHandle: createCanvasConnectionHandleString({
										mode: CanvasConnectionMode.Output,
										type: type as NodeConnectionType,
										index: parseInt(index, 10),
									}),
									target: connectionDataNode.id,
									targetHandle: createCanvasConnectionHandleString({
										mode: CanvasConnectionMode.Input,
										type: connectionData.type as NodeConnectionType,
										index: connectionData.index,
									}),
								},
								{ trackHistory, trackBulk: false },
							);
						}
					}
				}
			}
		}

		delete workflowsStore.workflow.connections[targetNode.name];

		if (trackHistory && trackBulk) {
			historyStore.stopRecordingUndo();
		}
	}

	function deleteConnection(
		connection: Connection,
		{ trackHistory = false, trackBulk = true } = {},
	) {
		const sourceNode = workflowsStore.getNodeById(connection.source);
		const targetNode = workflowsStore.getNodeById(connection.target);
		if (!sourceNode || !targetNode) {
			return;
		}

		const mappedConnection = mapCanvasConnectionToLegacyConnection(
			sourceNode,
			targetNode,
			connection,
		);

		if (trackHistory && trackBulk) {
			historyStore.startRecordingUndo();
		}

		workflowsStore.removeConnection({
			connection: mappedConnection,
		});

		if (trackHistory) {
			historyStore.pushCommandToUndo(new RemoveConnectionCommand(mappedConnection));

			if (trackBulk) {
				historyStore.stopRecordingUndo();
			}
		}
	}

	function revertDeleteConnection(connection: [IConnection, IConnection]) {
		workflowsStore.addConnection({
			connection,
		});
	}

	function revalidateNodeConnections(id: string, connectionMode: CanvasConnectionMode) {
		const node = workflowsStore.getNodeById(id);
		const isInput = connectionMode === CanvasConnectionMode.Input;
		if (!node) {
			return;
		}

		const nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion);
		if (!nodeType) {
			return;
		}

		const connections = mapLegacyConnectionsToCanvasConnections(
			workflowsStore.workflow.connections,
			workflowsStore.workflow.nodes,
		);

		connections.forEach((connection) => {
			const isRelevantConnection = isInput ? connection.target === id : connection.source === id;

			if (isRelevantConnection) {
				const otherNodeId = isInput ? connection.source : connection.target;

				const otherNode = workflowsStore.getNodeById(otherNodeId);
				if (!otherNode || !connection.data) {
					return;
				}

				const [firstNode, secondNode] = isInput ? [otherNode, node] : [node, otherNode];

				if (
					!isConnectionAllowed(
						firstNode,
						secondNode,
						connection.data.source,
						connection.data.target,
					)
				) {
					void nextTick(() => deleteConnection(connection));
				}
			}
		});
	}

	function revalidateNodeInputConnections(id: string) {
		return revalidateNodeConnections(id, CanvasConnectionMode.Input);
	}

	function revalidateNodeOutputConnections(id: string) {
		return revalidateNodeConnections(id, CanvasConnectionMode.Output);
	}

	function isConnectionAllowed(
		sourceNode: INodeUi,
		targetNode: INodeUi,
		sourceConnection: IConnection | CanvasConnectionPort,
		targetConnection: IConnection | CanvasConnectionPort,
	): boolean {
		const blocklist = [STICKY_NODE_TYPE];

		if (sourceConnection.type !== targetConnection.type) {
			return false;
		}

		if (blocklist.includes(sourceNode.type) || blocklist.includes(targetNode.type)) {
			return false;
		}

		const sourceNodeType = nodeTypesStore.getNodeType(sourceNode.type, sourceNode.typeVersion);
		const sourceWorkflowNode = editableWorkflowObject.value.getNode(sourceNode.name);
		if (!sourceWorkflowNode) {
			return false;
		}

		let sourceNodeOutputs: Array<NodeConnectionType | INodeOutputConfiguration> = [];
		if (sourceNodeType) {
			sourceNodeOutputs =
				NodeHelpers.getNodeOutputs(
					editableWorkflowObject.value,
					sourceWorkflowNode,
					sourceNodeType,
				) || [];
		}

		const sourceNodeHasOutputConnectionOfType = !!sourceNodeOutputs.find((output) => {
			const outputType = typeof output === 'string' ? output : output.type;
			return outputType === sourceConnection.type;
		});

		const sourceNodeHasOutputConnectionPortOfType =
			sourceConnection.index < sourceNodeOutputs.length;

		if (!sourceNodeHasOutputConnectionOfType || !sourceNodeHasOutputConnectionPortOfType) {
			return false;
		}

		const targetNodeType = nodeTypesStore.getNodeType(targetNode.type, targetNode.typeVersion);
		const targetWorkflowNode = editableWorkflowObject.value.getNode(targetNode.name);
		if (!targetWorkflowNode) {
			return false;
		}

		let targetNodeInputs: Array<NodeConnectionType | INodeInputConfiguration> = [];
		if (targetNodeType) {
			targetNodeInputs =
				NodeHelpers.getNodeInputs(
					editableWorkflowObject.value,
					targetWorkflowNode,
					targetNodeType,
				) || [];
		}

		const targetNodeHasInputConnectionOfType = !!targetNodeInputs.find((input) => {
			const inputType = typeof input === 'string' ? input : input.type;
			if (inputType !== targetConnection.type) return false;

			const filter = typeof input === 'object' && 'filter' in input ? input.filter : undefined;
			if (filter?.nodes.length && !filter.nodes.includes(sourceNode.type)) {
				toast.showToast({
					title: i18n.baseText('nodeView.showError.nodeNodeCompatible.title'),
					message: i18n.baseText('nodeView.showError.nodeNodeCompatible.message', {
						interpolate: { sourceNodeName: sourceNode.name, targetNodeName: targetNode.name },
					}),
					type: 'error',
					duration: 5000,
				});

				return false;
			}

			return true;
		});

		const targetNodeHasInputConnectionPortOfType = targetConnection.index < targetNodeInputs.length;

		return targetNodeHasInputConnectionOfType && targetNodeHasInputConnectionPortOfType;
	}

	async function addConnections(
		connections: CanvasConnectionCreateData[] | CanvasConnection[],
		{ trackBulk = true, trackHistory = false, keepPristine = false } = {},
	) {
		await nextTick(); // Connection creation relies on the nodes being already added to the store

		if (trackBulk && trackHistory) {
			historyStore.startRecordingUndo();
		}

		for (const connection of connections) {
			createConnection(connection, { trackHistory, keepPristine });
		}

		if (trackBulk && trackHistory) {
			historyStore.stopRecordingUndo();
		}

		if (!keepPristine) {
			uiStore.stateIsDirty = true;
		}
	}

	/**
	 * Workspace operations
	 */

	function resetWorkspace() {
		// Reset node creator
		nodeCreatorStore.setNodeCreatorState({ createNodeActive: false });
		nodeCreatorStore.setShowScrim(false);

		// Make sure that if there is a waiting test-webhook, it gets removed
		if (workflowsStore.executionWaitingForWebhook) {
			try {
				void workflowsStore.removeTestWebhook(workflowsStore.workflowId);
			} catch (error) {}
		}

		// Reset editable workflow state
		workflowsStore.resetWorkflow();
		workflowsStore.resetState();
		workflowsStore.currentWorkflowExecutions = [];

		// Reset actions
		uiStore.resetLastInteractedWith();
		uiStore.removeActiveAction('workflowRunning');
		uiStore.stateIsDirty = false;

		// Reset executions
		executionsStore.activeExecution = null;

		// Reset credentials updates
		nodeHelpers.credentialsUpdated.value = false;
	}

	function initializeWorkspace(data: IWorkflowDb) {
		workflowHelpers.initState(data);

		data.nodes.forEach((node) => {
			const nodeTypeDescription = requireNodeTypeDescription(node.type, node.typeVersion);
			nodeHelpers.matchCredentials(node);
			resolveNodeParameters(node, nodeTypeDescription);
			resolveNodeWebhook(node, nodeTypeDescription);
		});

		workflowsStore.setNodes(data.nodes);
		workflowsStore.setConnections(data.connections);
	}

	/**
	 * Import operations
	 */

	function removeUnknownCredentials(workflow: IWorkflowDataUpdate) {
		if (!workflow?.nodes) return;

		for (const node of workflow.nodes) {
			if (!node.credentials) continue;

			for (const [name, credential] of Object.entries(node.credentials)) {
				if (typeof credential === 'string' || credential.id === null) continue;

				if (!credentialsStore.getCredentialById(credential.id)) {
					delete node.credentials[name];
				}
			}
		}
	}

	async function addImportedNodesToWorkflow(
		data: IWorkflowDataUpdate,
		{ trackBulk = true, trackHistory = false } = {},
	): Promise<IWorkflowDataUpdate> {
		// Because nodes with the same name maybe already exist, it could
		// be needed that they have to be renamed. Also could it be possible
		// that nodes are not allowed to be created because they have a create
		// limit set. So we would then link the new nodes with the already existing ones.
		// In this object all that nodes get saved in the format:
		//   old-name -> new-name
		const nodeNameTable: {
			[key: string]: string;
		} = {};
		const newNodeNames = new Set<string>();

		if (!data.nodes) {
			// No nodes to add
			throw new Error(i18n.baseText('nodeView.noNodesGivenToAdd'));
		}

		// Get how many of the nodes of the types which have
		// a max limit set already exist
		const nodeTypesCount = workflowHelpers.getNodeTypesMaxCount();

		let oldName: string;
		let newName: string;
		const createNodes: INode[] = [];

		await nodeHelpers.loadNodesProperties(
			data.nodes.map((node) => ({ name: node.type, version: node.typeVersion })),
		);

		data.nodes.forEach((node) => {
			if (nodeTypesCount[node.type] !== undefined) {
				if (nodeTypesCount[node.type].exist >= nodeTypesCount[node.type].max) {
					// Node is not allowed to be created so
					// do not add it to the create list but
					// add the name of the existing node
					// that this one gets linked up instead.
					nodeNameTable[node.name] = nodeTypesCount[node.type].nodeNames[0];
					return;
				} else {
					// Node can be created but increment the
					// counter in case multiple ones are
					// supposed to be created
					nodeTypesCount[node.type].exist += 1;
				}
			}

			oldName = node.name;

			const localized = i18n.localizeNodeName(node.name, node.type);

			newName = uniqueNodeName(localized, Array.from(newNodeNames));

			newNodeNames.add(newName);
			nodeNameTable[oldName] = newName;

			createNodes.push(node);
		});

		// Get only the connections of the nodes that get created
		const newConnections: IConnections = {};
		const currentConnections = data.connections ?? {};
		const createNodeNames = createNodes.map((node) => node.name);
		let sourceNode, type, sourceIndex, connectionIndex, connectionData;
		for (sourceNode of Object.keys(currentConnections)) {
			if (!createNodeNames.includes(sourceNode)) {
				// Node does not get created so skip output connections
				continue;
			}

			const connection: INodeConnections = {};

			for (type of Object.keys(currentConnections[sourceNode])) {
				connection[type] = [];
				for (
					sourceIndex = 0;
					sourceIndex < currentConnections[sourceNode][type].length;
					sourceIndex++
				) {
					const nodeSourceConnections = [];
					const connectionsToCheck = currentConnections[sourceNode][type][sourceIndex];
					if (connectionsToCheck) {
						for (
							connectionIndex = 0;
							connectionIndex < connectionsToCheck.length;
							connectionIndex++
						) {
							connectionData = connectionsToCheck[connectionIndex];
							if (!createNodeNames.includes(connectionData.node)) {
								// Node does not get created so skip input connection
								continue;
							}

							nodeSourceConnections.push(connectionData);
							// Add connection
						}
					}
					connection[type].push(nodeSourceConnections);
				}
			}

			newConnections[sourceNode] = connection;
		}

		// Create a workflow with the new nodes and connections that we can use
		// the rename method
		const tempWorkflow: Workflow = workflowsStore.getWorkflow(createNodes, newConnections);

		// Rename all the nodes of which the name changed
		for (oldName in nodeNameTable) {
			if (oldName === nodeNameTable[oldName]) {
				// Name did not change so skip
				continue;
			}
			tempWorkflow.renameNode(oldName, nodeNameTable[oldName]);
		}

		if (data.pinData) {
			let pinDataSuccess = true;
			for (const nodeName of Object.keys(data.pinData)) {
				// Pin data limit reached
				if (!pinDataSuccess) {
					toast.showError(
						new Error(i18n.baseText('ndv.pinData.error.tooLarge.description')),
						i18n.baseText('ndv.pinData.error.tooLarge.title'),
					);
					continue;
				}

				const node = tempWorkflow.nodes[nodeNameTable[nodeName]];
				try {
					const pinnedDataForNode = usePinnedData(node);
					pinnedDataForNode.setData(data.pinData[nodeName], 'add-nodes');
					pinDataSuccess = true;
				} catch (error) {
					pinDataSuccess = false;
					console.error(error);
				}
			}
		}

		// Add the nodes with the changed node names, expressions and connections
		if (trackBulk && trackHistory) {
			historyStore.startRecordingUndo();
		}

		await addNodes(Object.values(tempWorkflow.nodes), { trackBulk: false, trackHistory });
		await addConnections(
			mapLegacyConnectionsToCanvasConnections(
				tempWorkflow.connectionsBySourceNode,
				Object.values(tempWorkflow.nodes),
			),
			{ trackBulk: false, trackHistory },
		);

		if (trackBulk && trackHistory) {
			historyStore.stopRecordingUndo();
		}

		uiStore.stateIsDirty = true;

		return {
			nodes: Object.values(tempWorkflow.nodes),
			connections: tempWorkflow.connectionsBySourceNode,
		};
	}

	async function importWorkflowData(
		workflowData: IWorkflowDataUpdate,
		source: string,
		importTags = true,
		{ trackBulk = true, trackHistory = true } = {},
	): Promise<IWorkflowDataUpdate> {
		uiStore.resetLastInteractedWith();

		// If it is JSON check if it looks on the first look like data we can use
		if (!workflowData.hasOwnProperty('nodes') || !workflowData.hasOwnProperty('connections')) {
			return {};
		}

		try {
			const nodeIdMap: { [prev: string]: string } = {};
			if (workflowData.nodes) {
				const nodeNames = new Set(workflowData.nodes.map((node) => node.name));
				workflowData.nodes.forEach((node: INode) => {
					// Provide a new name for nodes that don't have one
					if (!node.name) {
						const nodeType = nodeTypesStore.getNodeType(node.type);
						const newName = uniqueNodeName(
							nodeType?.displayName ?? node.type,
							Array.from(nodeNames),
						);
						node.name = newName;
						nodeNames.add(newName);
					}

					// Generate new webhookId if workflow already contains a node with the same webhookId
					if (node.webhookId && UPDATE_WEBHOOK_ID_NODE_TYPES.includes(node.type)) {
						const isDuplicate = Object.values(workflowHelpers.getCurrentWorkflow().nodes).some(
							(n) => n.webhookId === node.webhookId,
						);
						if (isDuplicate) {
							nodeHelpers.assignWebhookId(node);

							if (node.parameters.path) {
								node.parameters.path = node.webhookId as string;
							} else if ((node.parameters.options as IDataObject).path) {
								(node.parameters.options as IDataObject).path = node.webhookId as string;
							}
						}
					}

					// Set all new ids when pasting/importing workflows
					if (node.id) {
						const previousId = node.id;
						const newId = nodeHelpers.assignNodeId(node);
						nodeIdMap[newId] = previousId;
					} else {
						nodeHelpers.assignNodeId(node);
					}
				});
			}

			removeUnknownCredentials(workflowData);

			const nodeGraph = JSON.stringify(
				TelemetryHelpers.generateNodesGraph(
					workflowData as IWorkflowBase,
					workflowHelpers.getNodeTypes(),
					{
						nodeIdMap,
						sourceInstanceId:
							workflowData.meta && workflowData.meta.instanceId !== rootStore.instanceId
								? workflowData.meta.instanceId
								: '',
						isCloudDeployment: settingsStore.isCloudDeployment,
					},
				).nodeGraph,
			);

			if (source === 'paste') {
				telemetry.track('User pasted nodes', {
					workflow_id: workflowsStore.workflowId,
					node_graph_string: nodeGraph,
				});
			} else if (source === 'duplicate') {
				telemetry.track('User duplicated nodes', {
					workflow_id: workflowsStore.workflowId,
					node_graph_string: nodeGraph,
				});
			} else {
				telemetry.track('User imported workflow', {
					source,
					workflow_id: workflowsStore.workflowId,
					node_graph_string: nodeGraph,
				});
			}

			// Fix the node position as it could be totally offscreen
			// and the pasted nodes would so not be directly visible to
			// the user
			workflowHelpers.updateNodePositions(
				workflowData,
				NodeViewUtils.getNewNodePosition(editableWorkflow.value.nodes, lastClickPosition.value),
			);

			await addImportedNodesToWorkflow(workflowData, { trackBulk, trackHistory });

			if (importTags && settingsStore.areTagsEnabled && Array.isArray(workflowData.tags)) {
				await importWorkflowTags(workflowData);
			}

			return workflowData;
		} catch (error) {
			toast.showError(error, i18n.baseText('nodeView.showError.importWorkflowData.title'));
			return {};
		}
	}

	async function importWorkflowTags(workflowData: IWorkflowDataUpdate) {
		const allTags = await tagsStore.fetchAll();
		const tagNames = new Set(allTags.map((tag) => tag.name));

		const workflowTags = workflowData.tags as ITag[];
		const notFound = workflowTags.filter((tag) => !tagNames.has(tag.name));

		const creatingTagPromises: Array<Promise<ITag>> = [];
		for (const tag of notFound) {
			const creationPromise = tagsStore.create(tag.name).then((newTag: ITag) => {
				allTags.push(newTag);
				return newTag;
			});

			creatingTagPromises.push(creationPromise);
		}

		await Promise.all(creatingTagPromises);

		const tagIds = workflowTags.reduce((accu: string[], imported: ITag) => {
			const tag = allTags.find((t) => t.name === imported.name);
			if (tag) {
				accu.push(tag.id);
			}

			return accu;
		}, []);

		workflowsStore.addWorkflowTagIds(tagIds);
	}

	async function fetchWorkflowDataFromUrl(url: string): Promise<IWorkflowDataUpdate | undefined> {
		let workflowData: IWorkflowDataUpdate;

		canvasStore.startLoading();
		try {
			workflowData = await workflowsStore.getWorkflowFromUrl(url);
		} catch (error) {
			toast.showError(error, i18n.baseText('nodeView.showError.getWorkflowDataFromUrl.title'));
			return;
		} finally {
			canvasStore.stopLoading();
		}

		return workflowData;
	}

	function getNodesToSave(nodes: INode[]): IWorkflowData {
		const data = {
			nodes: [] as INodeUi[],
			connections: {} as IConnections,
			pinData: {} as IPinData,
		} satisfies IWorkflowData;

		const exportedNodeNames = new Set<string>();

		for (const node of nodes) {
			const nodeSaveData = workflowHelpers.getNodeDataToSave(node);
			const pinDataForNode = workflowsStore.pinDataByNodeName(node.name);

			if (pinDataForNode) {
				data.pinData[node.name] = pinDataForNode;
			}

			if (
				nodeSaveData.credentials &&
				settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Sharing]
			) {
				nodeSaveData.credentials = filterAllowedCredentials(
					nodeSaveData.credentials,
					workflowsStore.usedCredentials,
				);
			}

			data.nodes.push(nodeSaveData);
			exportedNodeNames.add(node.name);
		}

		data.connections = getConnectionsForNodes(data.nodes, exportedNodeNames);

		workflowHelpers.removeForeignCredentialsFromWorkflow(data, credentialsStore.allCredentials);

		return data;
	}

	function filterAllowedCredentials(
		credentials: INodeCredentials,
		usedCredentials: Record<string, IUsedCredential>,
	): INodeCredentials {
		return Object.fromEntries(
			Object.entries(credentials).filter(([, credential]) => {
				return (
					credential.id &&
					(!usedCredentials[credential.id] || usedCredentials[credential.id]?.currentUserHasAccess)
				);
			}),
		);
	}

	function getConnectionsForNodes(
		nodes: INodeUi[],
		includeNodeNames: Set<string>,
	): Record<string, INodeConnections> {
		const connections: Record<string, INodeConnections> = {};

		for (const node of nodes) {
			const outgoingConnections = workflowsStore.outgoingConnectionsByNodeName(node.name);
			if (!Object.keys(outgoingConnections).length) continue;

			const filteredConnections = filterConnectionsByNodes(outgoingConnections, includeNodeNames);
			if (Object.keys(filteredConnections).length) {
				connections[node.name] = filteredConnections;
			}
		}

		return connections;
	}

	function filterConnectionsByNodes(
		connections: INodeConnections,
		includeNodeNames: Set<string>,
	): INodeConnections {
		const filteredConnections: INodeConnections = {};

		for (const [type, typeConnections] of Object.entries(connections)) {
			const validConnections = typeConnections.map((sourceConnections) =>
				(sourceConnections ?? []).filter((connection) => includeNodeNames.has(connection.node)),
			);

			if (validConnections.length) {
				filteredConnections[type] = validConnections;
			}
		}

		return filteredConnections;
	}

	async function duplicateNodes(ids: string[]) {
		const workflowData = deepCopy(getNodesToSave(workflowsStore.getNodesByIds(ids)));
		const result = await importWorkflowData(workflowData, 'duplicate', false);

		return result.nodes?.map((node) => node.id).filter(isPresent) ?? [];
	}

	async function copyNodes(ids: string[]) {
		const workflowData = deepCopy(getNodesToSave(workflowsStore.getNodesByIds(ids)));

		await clipboard.copy(JSON.stringify(workflowData, null, 2));

		telemetry.track('User copied nodes', {
			node_types: workflowData.nodes.map((node) => node.type),
			workflow_id: workflowsStore.workflowId,
		});
	}

	async function cutNodes(ids: string[]) {
		await copyNodes(ids);
		deleteNodes(ids);
	}

	async function openExecution(executionId: string) {
		let data: IExecutionResponse | undefined;
		try {
			data = await workflowsStore.getExecution(executionId);
		} catch (error) {
			toast.showError(error, i18n.baseText('nodeView.showError.openExecution.title'));
			return;
		}

		if (data === undefined) {
			throw new Error(`Execution with id "${executionId}" could not be found!`);
		}

		initializeWorkspace(data.workflowData);

		workflowsStore.setWorkflowExecutionData(data);

		if (data.mode !== 'manual') {
			workflowsStore.setWorkflowPinData({});
		}

		uiStore.stateIsDirty = false;

		return data;
	}

	return {
		lastClickPosition,
		editableWorkflow,
		editableWorkflowObject,
		triggerNodes,
		requireNodeTypeDescription,
		addNodes,
		addNode,
		resolveNodePosition,
		revertAddNode,
		updateNodesPosition,
		updateNodePosition,
		revertUpdateNodePosition,
		setNodeActive,
		setNodeActiveByName,
		setNodeSelected,
		toggleNodesDisabled,
		revertToggleNodeDisabled,
		toggleNodesPinned,
		setNodeParameters,
		renameNode,
		revertRenameNode,
		deleteNode,
		deleteNodes,
		copyNodes,
		cutNodes,
		duplicateNodes,
		getNodesToSave,
		revertDeleteNode,
		addConnections,
		createConnection,
		revertCreateConnection,
		deleteConnection,
		revertDeleteConnection,
		deleteConnectionsByNodeId,
		revalidateNodeInputConnections,
		revalidateNodeOutputConnections,
		isConnectionAllowed,
		filterConnectionsByNodes,
		importWorkflowData,
		fetchWorkflowDataFromUrl,
		resetWorkspace,
		initializeWorkspace,
		resolveNodeWebhook,
		openExecution,
	};
}
