<script setup lang="ts">
import {
	computed,
	defineAsyncComponent,
	nextTick,
	onMounted,
	ref,
	useCssModule,
	watch,
	h,
	onBeforeUnmount,
	useTemplateRef,
} from 'vue';
import { useEditorContext } from '@/app/composables/useEditorContext';
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router';
import WorkflowCanvas from '@/features/workflows/canvas/components/WorkflowCanvas.vue';
import FocusSidebar from '@/app/components/FocusSidebar.vue';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useUIStore } from '@/app/stores/ui.store';
import CanvasRunWorkflowButton from '@/features/workflows/canvas/components/elements/buttons/CanvasRunWorkflowButton.vue';
import { useI18n } from '@n8n/i18n';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
import { useWorkflowsListStore } from '@/app/stores/workflowsList.store';
import { useRunWorkflow } from '@/app/composables/useRunWorkflow';
import { useGlobalLinkActions } from '@/app/composables/useGlobalLinkActions';
import type {
	AddedNodesAndConnections,
	INodeUi,
	IWorkflowDb,
	NodeCreatorOpenSource,
	NodeFilterType,
	ToggleNodeCreatorOptions,
	XYPosition,
} from '@/Interface';
import type { IExecutionResponse } from '@/features/execution/executions/executions.types';
import type { WorkflowDataUpdate } from '@n8n/rest-api-client/api/workflows';
import type {
	Connection,
	Dimensions,
	ViewportTransform,
	XYPosition as VueFlowXYPosition,
} from '@vue-flow/core';
import type {
	CanvasNode,
	CanvasNodeMoveEvent,
	ConnectStartEvent,
	GroupExpansionMode,
	ViewportBoundaries,
} from '@/features/workflows/canvas/canvas.types';
import {
	CanvasConnectionMode,
	CanvasNodeRenderType,
} from '@/features/workflows/canvas/canvas.types';
import {
	CHAT_TRIGGER_NODE_TYPE,
	DRAG_EVENT_DATA_KEY,
	FROM_AI_PARAMETERS_MODAL_KEY,
	MAIN_HEADER_TABS,
	MANUAL_CHAT_TRIGGER_NODE_TYPE,
	MODAL_CONFIRM,
	NODE_CREATOR_OPEN_SOURCES,
	STICKY_NODE_TYPE,
	VALID_WORKFLOW_IMPORT_URL_REGEX,
	VIEWS,
	WORKFLOW_SETTINGS_MODAL_KEY,
	ABOUT_MODAL_KEY,
	PRODUCTION_ONLY_TRIGGER_NODE_TYPES,
	HUMAN_IN_THE_LOOP_CATEGORY,
} from '@/app/constants';
import { useSourceControlStore } from '@/features/integrations/sourceControl.ee/sourceControl.store';
import { useNodeCreatorStore } from '@/features/shared/nodeCreator/nodeCreator.store';
import { useExternalHooks } from '@/app/composables/useExternalHooks';
import {
	jsonParse,
	EVALUATION_TRIGGER_NODE_TYPE,
	EVALUATION_NODE_TYPE,
	isTriggerNode,
	NodeHelpers,
	NodeConnectionTypes,
} from 'n8n-workflow';
import type {
	NodeConnectionType,
	IDataObject,
	ExecutionSummary,
	IConnection,
	INodeParameters,
	IWorkflowGroup,
} from 'n8n-workflow';
import { useToast } from '@/app/composables/useToast';
import { useCredentialsStore } from '@/features/credentials/credentials.store';
import { useEnvironmentsStore } from '@/features/settings/environments.ee/environments.store';
import { historyBus } from '@/app/models/history';
import { useCanvasOperations } from '@/app/composables/useCanvasOperations';
import { useCanvasStore } from '@/app/stores/canvas.store';
import { useMessage } from '@/app/composables/useMessage';
import { useDocumentTitle } from '@/app/composables/useDocumentTitle';
import { useNpsSurveyStore } from '@/app/stores/npsSurvey.store';
import { useTelemetry } from '@/app/composables/useTelemetry';
import { useProjectsStore } from '@/features/collaboration/projects/projects.store';
import { useNodeHelpers } from '@/app/composables/useNodeHelpers';
import { useUsersStore } from '@/features/settings/users/users.store';
import { sourceControlEventBus } from '@/features/integrations/sourceControl.ee/sourceControl.eventBus';
import { useTagsStore } from '@/features/shared/tags/tags.store';

import { injectNDVStore } from '@/features/ndv/shared/ndv.store';
import { getBounds, getNodeViewTab } from '@/app/utils/nodeViewUtils';
import { isChatNode } from '@/app/utils/aiUtils';
import CanvasStopCurrentExecutionButton from '@/features/workflows/canvas/components/elements/buttons/CanvasStopCurrentExecutionButton.vue';
import CanvasStopWaitingForWebhookButton from '@/features/workflows/canvas/components/elements/buttons/CanvasStopWaitingForWebhookButton.vue';
import { nodeViewEventBus } from '@/app/event-bus';
import type { PinDataSource } from '@/app/composables/usePinnedData';
import { useClipboard } from '@/app/composables/useClipboard';
import { useBeforeUnload } from '@/app/composables/useBeforeUnload';
import { getResourcePermissions } from '@n8n/permissions';
import NodeViewUnfinishedWorkflowMessage from '@/app/components/NodeViewUnfinishedWorkflowMessage.vue';
import {
	parseCanvasConnectionHandleString,
	shouldIgnoreCanvasShortcut,
} from '@/features/workflows/canvas/canvas.utils';
import type { CanvasLayoutEvent } from '@/features/workflows/canvas/composables/useCanvasLayout';
import { useWorkflowSaving } from '@/app/composables/useWorkflowSaving';
import { usePostMessageControls } from '@/app/composables/usePostMessageHandler';
import { useBuilderStore } from '@/features/ai/assistant/builder.store';
import KeyboardShortcutTooltip from '@/app/components/KeyboardShortcutTooltip.vue';
import { useWorkflowExtraction } from '@/app/composables/useWorkflowExtraction';
import { useAgentRequestStore } from '@n8n/stores/useAgentRequestStore';
import { needsAgentInput } from '@/app/utils/nodes/nodeTransforms';
import { useLogsStore } from '@/app/stores/logs.store';
import { canvasEventBus } from '@/features/workflows/canvas/canvas.eventBus';
import CanvasChatButton from '@/features/workflows/canvas/components/elements/buttons/CanvasChatButton.vue';
import { useFocusPanelStore } from '@/app/stores/focusPanel.store';
import { useEmptyStateBuilderPromptStore } from '@/experiments/emptyStateBuilderPrompt/stores/emptyStateBuilderPrompt.store';
import { useEvaluationsWizardSidepanelStore } from '@/features/ai/evaluation.ee/wizardSidepanel.store';
import { useEvaluationsWizardSidepanelExperiment } from '@/experiments/evaluationsWizardSidepanel/useEvaluationsWizardSidepanelExperiment';
import EvaluationsCanvasInfoCard from '@/features/ai/evaluation.ee/components/EvaluationsCanvasInfoCard/EvaluationsCanvasInfoCard.vue';
import { useChatPanelStore } from '@/features/ai/assistant/chatPanel.store';
import { useChatHubPanelStore } from '@/features/ai/chatHub/chatHubPanel.store';
import { useKeybindings } from '@/app/composables/useKeybindings';
import { type ContextMenuAction } from '@/features/shared/contextMenu/composables/useContextMenuItems';
import { useExperimentalNdvStore } from '@/features/workflows/canvas/experimental/experimentalNdv.store';
import { useActivityDetection } from '@/app/composables/useActivityDetection';
import { useCollaborationStore } from '@/features/collaboration/collaboration/collaboration.store';
import { useInjectWorkflowId } from '@/app/composables/useInjectWorkflowId';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';

import { N8nCallout, N8nCanvasThinkingPill, N8nCanvasCollaborationPill } from '@n8n/design-system';
import { useWorkflowHelpers } from '../composables/useWorkflowHelpers';
import { findTriggerNodeToAutoSelect } from '@/features/execution/executions/executions.utils';

defineOptions({
	name: 'NodeView',
});

const LazyNodeCreation = defineAsyncComponent(
	async () => await import('@/features/shared/nodeCreator/views/NodeCreation.vue'),
);

const LazyNodeDetailsView = defineAsyncComponent(
	async () => await import('@/features/ndv/shared/views/NodeDetailsView.vue'),
);
const LazyNodeDetailsViewV2 = defineAsyncComponent(
	async () => await import('@/features/ndv/shared/views/NodeDetailsViewV2.vue'),
);

const LazySetupWorkflowCredentialsButton = defineAsyncComponent(
	async () =>
		await import('@/features/workflows/templates/components/SetupWorkflowCredentialsButton.vue'),
);

const $style = useCssModule();
const router = useRouter();
const route = useRoute();
const { canOpenNDV } = usePostMessageControls();
const i18n = useI18n();
const telemetry = useTelemetry();
const externalHooks = useExternalHooks();
const toast = useToast();
const message = useMessage();
const documentTitle = useDocumentTitle();
const workflowSaving = useWorkflowSaving({
	router,
	onSaved: (isFirstSave) => {
		canvasEventBus.emit('saved:workflow', { isFirstSave });
	},
});
const nodeHelpers = useNodeHelpers();
const clipboard = useClipboard({ onPaste: onClipboardPaste });

const nodeTypesStore = useNodeTypesStore();
const uiStore = useUIStore();
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = injectWorkflowDocumentStore();
const workflowExecutionState = computed(() =>
	useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId),
);
const workflowsListStore = useWorkflowsListStore();
const sourceControlStore = useSourceControlStore();
const nodeCreatorStore = useNodeCreatorStore();
const credentialsStore = useCredentialsStore();
const environmentsStore = useEnvironmentsStore();
const canvasStore = useCanvasStore();
const npsSurveyStore = useNpsSurveyStore();
const projectsStore = useProjectsStore();
const usersStore = useUsersStore();
const tagsStore = useTagsStore();

const ndvStore = injectNDVStore();
const focusPanelStore = useFocusPanelStore();
const evaluationsWizardSidepanelStore = useEvaluationsWizardSidepanelStore();
const { isFeatureEnabled: isEvaluationsWizardSidepanelEnabled } =
	useEvaluationsWizardSidepanelExperiment();
const builderStore = useBuilderStore();
const agentRequestStore = useAgentRequestStore();
const logsStore = useLogsStore();
const experimentalNdvStore = useExperimentalNdvStore();
const collaborationStore = useCollaborationStore();
const emptyStateBuilderPromptStore = useEmptyStateBuilderPromptStore();
const chatPanelStore = useChatPanelStore();
const chatHubPanelStore = useChatHubPanelStore();
const workflowHelpers = useWorkflowHelpers();

// Initialize activity detection for collaboration
useActivityDetection();

const { addBeforeUnloadEventBindings, removeBeforeUnloadEventBindings } = useBeforeUnload({
	route,
});
const { registerCustomAction, unregisterCustomAction } = useGlobalLinkActions();
const { runWorkflow, runEntireWorkflow, stopCurrentExecution, stopWaitingForWebhook } =
	useRunWorkflow({ router });
const {
	updateNodePosition,
	updateNodesPosition,
	tidyUp,
	revertUpdateNodePosition,
	renameNode,
	revertRenameNode,
	revertReplaceNodeParameters,
	setNodeActive,
	setNodeSelected,
	toggleNodesDisabled,
	revertToggleNodeDisabled,
	toggleNodesPinned,
	setNodeParameters,
	deleteNode,
	deleteNodes,
	copyNodes,
	cutNodes,
	duplicateNodes,
	revertDeleteNode,
	revertAddNode,
	createConnection,
	revertCreateConnection,
	deleteConnection,
	revertDeleteConnection,
	revalidateNodeInputConnections,
	revalidateNodeOutputConnections,
	setNodeActiveByName,
	clearNodeActive,
	tryToOpenSubworkflowInNewTab,
	importWorkflowData,
	fetchWorkflowDataFromUrl,
	resetWorkspace,
	initializeWorkspace,
	lastClickPosition,
	startChat,
	addNodesAndConnections,
	fitView,
} = useCanvasOperations();
const { extractWorkflow } = useWorkflowExtraction();

useKeybindings({
	ctrl_alt_o: () => uiStore.openModal(ABOUT_MODAL_KEY),
});

const canvasRef = useTemplateRef('canvas');
const isLoading = ref(true);
const readOnlyNotification = ref<null | { visible: boolean }>(null);
const fallbackNodes = ref<INodeUi[]>([]);

const workflowId = useInjectWorkflowId();
const routeNodeId = computed(() => {
	const nodeId = route.params.nodeId;
	return Array.isArray(nodeId) ? nodeId[0] : nodeId;
});

// Check if this is a new workflow by looking for the ?new query param
const isNewWorkflowRoute = computed(() => {
	return route.query.new === 'true';
});

// Check if canvas controls should be hidden (e.g., for workflow preview thumbnails)
const hideCanvasControls = computed(() => {
	return route.query.hideControls === 'true';
});

const stripedCanvasBackground = computed(() => route.query.canvasBackground !== 'dots');

const isDemoRoute = computed(() => route.name === VIEWS.DEMO);
const isReadOnlyRoute = computed(() => !!route?.meta?.readOnlyCanvas);
const isReadOnlyEnvironment = computed(() => {
	return sourceControlStore.preferences.branchReadOnly;
});
const isNDVV2 = computed(() => true);

// Per-editor host overrides (AI features + read-only). The artifact host marks
// the canvas read-only while a workflow-builder agent mutates the workflow.
const { readOnly: externalReadOnly, expandGroups: externalExpandGroups } = useEditorContext();

const isCanvasReadOnly = computed(() => {
	return (
		isDemoRoute.value ||
		isReadOnlyEnvironment.value ||
		collaborationStore.shouldBeReadOnly ||
		!(workflowPermissions.value.update ?? projectPermissions.value.workflow.update) ||
		(workflowDocumentStore?.value?.isArchived ?? false) ||
		(builderStore.streaming && !builderStore.isHelpStreaming) ||
		externalReadOnly.value
	);
});

const groupExpansionMode = computed<GroupExpansionMode | undefined>(() => {
	return isDemoRoute.value ? 'all' : externalExpandGroups.value;
});

const canExecuteOnCanvas = computed(() => {
	if (isDemoRoute.value) {
		return route.query.canExecute === 'true';
	}
	if (workflowDocumentStore?.value?.isArchived) return false;
	if (builderStore.streaming) return false;
	if (externalReadOnly?.value) return false;
	return !!(workflowPermissions.value.execute ?? projectPermissions.value.workflow.execute);
});

const isWriterAnotherTab = computed(() => {
	return collaborationStore.isCurrentUserWriter && !collaborationStore.isCurrentTabWriter;
});

const showFallbackNodes = computed(() => triggerNodes.value.length === 0);

const keyBindingsEnabled = computed(() => {
	return !ndvStore.value.activeNode && uiStore.activeModals.length === 0;
});

const isLogsPanelOpen = computed(() => logsStore.isOpen);

/**
 * Initialization
 */

function initializeRoute() {
	// Open node panel if the route has a corresponding action
	if (route.query.action === 'addEvaluationTrigger') {
		void addEvaluationTriggerNodeFromRoute();
	} else if (route.query.action === 'addEvaluationNode') {
		nodeCreatorStore.openNodeCreatorForActions(
			workflowId.value,
			EVALUATION_NODE_TYPE,
			NODE_CREATOR_OPEN_SOURCES.ADD_EVALUATION_NODE_BUTTON,
		);
	} else if (route.query.action === 'executeEvaluation') {
		if (evaluationTriggerNode.value) {
			void runEntireWorkflow('node', evaluationTriggerNode.value.name);
		}
	} else if (
		route.query.action === 'openEvaluationsWizard' &&
		isEvaluationsWizardSidepanelEnabled.value
	) {
		telemetry.track('User opened evaluations wizard', {
			workflow_id: workflowId.value,
			source: 'empty_state',
		});
		evaluationsWizardSidepanelStore.open(0);
	}

	// Handle debug mode event binding (data loading is handled by WorkflowLayout)
	// Always remove first to prevent duplicate listeners, then add only if on debug route
	canvasEventBus.off('saved:workflow', onSaveFromWithinExecutionDebug);
	if (route.name === VIEWS.EXECUTION_DEBUG) {
		canvasEventBus.on('saved:workflow', onSaveFromWithinExecutionDebug);
	}

	// Update node issues after workflow is loaded
	void nextTick(() => {
		updateNodesIssues();
	});
}

function updateNodesIssues() {
	nodeHelpers.updateNodesInputIssues();
	nodeHelpers.updateNodesCredentialsIssues();
	nodeHelpers.updateNodesParameterIssues();
}

/**
 * Workflow
 */

async function openWorkflow(data: IWorkflowDb) {
	resetWorkspace();
	// Show AI_BUILDING status if builder is actively streaming, otherwise IDLE
	if (builderStore.streaming) {
		documentTitle.setDocumentTitle(data.name, 'AI_BUILDING');
	} else {
		documentTitle.setDocumentTitle(data.name, 'IDLE');
	}

	await initializeWorkspace(data);

	void externalHooks.run('workflow.open', {
		workflowId: data.id,
		workflowName: data.name,
	});

	fitView();
}

/**
 * Nodes
 */

const triggerNodes = computed(() => {
	return (
		workflowDocumentStore?.value?.allNodes.filter((node) =>
			nodeTypesStore.isTriggerNode(node.type),
		) ?? []
	);
});

const containsTriggerNodes = computed(() => triggerNodes.value.length > 0);

const allTriggerNodesDisabled = computed(() => {
	const disabledTriggerNodes = triggerNodes.value.filter((node) => node.disabled);
	return disabledTriggerNodes.length === triggerNodes.value.length;
});

const selectableTriggerNodes = computed(() =>
	triggerNodes.value.filter((node) => !node.disabled && !isChatNode(node)),
);
const isRunButtonSplit = computed(() => {
	return (
		selectableTriggerNodes.value.length > 1 &&
		workflowExecutionState.value.selectedTriggerNodeName !== undefined
	);
});

function onTidyUp(
	event: CanvasLayoutEvent,
	options?: {
		trackEvents?: boolean;
		trackHistory?: boolean;
		trackBulk?: boolean;
	},
) {
	tidyUp(event, options);
}

function onExtractWorkflow(nodeIds: string[]) {
	extractWorkflow(nodeIds);
}

function onUpdateNodesPosition(events: CanvasNodeMoveEvent[]) {
	updateNodesPosition(events, { trackHistory: true });
}

function onUpdateNodePosition(id: string, position: CanvasNode['position']) {
	updateNodePosition(id, position, { trackHistory: true });
}

function onRevertNodePosition({ nodeName, position }: { nodeName: string; position: XYPosition }) {
	revertUpdateNodePosition(nodeName, { x: position[0], y: position[1] });
}

function onDeleteNode(id: string) {
	const matchedFallbackNode = fallbackNodes.value.findIndex((node) => node.id === id);
	if (matchedFallbackNode >= 0) {
		fallbackNodes.value.splice(matchedFallbackNode, 1);
	} else {
		deleteNode(id, { trackHistory: true });
	}
}

function onDeleteNodes(ids: string[]) {
	deleteNodes(ids);
}

function onRevertDeleteNode({ node }: { node: INodeUi }) {
	revertDeleteNode(node);
}

function onRevertAddNodeGroup({ group }: { group: IWorkflowGroup }) {
	workflowDocumentStore.value.deleteGroup(group.id);
}

function onRevertRemoveNodeGroup({ group }: { group: IWorkflowGroup }) {
	workflowDocumentStore.value.restoreGroup(group);
}

function onRevertUpdateNodeGroup({ group }: { group: IWorkflowGroup }) {
	workflowDocumentStore.value.restoreGroup(group);
}

function onToggleNodeDisabled(id: string) {
	if (!checkIfEditingIsAllowed()) {
		return;
	}

	toggleNodesDisabled([id]);
}

function onRevertToggleNodeDisabled({ nodeName }: { nodeName: string }) {
	revertToggleNodeDisabled(nodeName);
}

function onToggleNodesDisabled(ids: string[]) {
	if (!checkIfEditingIsAllowed()) {
		return;
	}

	toggleNodesDisabled(ids);
}

function onClickNode(_id: string, event: VueFlowXYPosition) {
	lastClickPosition.value = [event.x, event.y];
	closeNodeCreator();
}

async function onSetNodeActivated(id: string, event?: MouseEvent) {
	if (isDemoRoute.value && !canOpenNDV.value) return;

	// Handle Ctrl/Cmd + Double Click case
	if (event?.metaKey || event?.ctrlKey) {
		const didOpen = await tryToOpenSubworkflowInNewTab(id);
		if (didOpen) {
			return;
		}
	}

	setNodeActive(id, 'canvas_default_view');
}

function onOpenSubWorkflow(id: string) {
	tryToOpenSubworkflowInNewTab(id);
}

function onSetNodeDeactivated() {
	clearNodeActive();
}

function onSetNodeSelected(id?: string) {
	closeNodeCreator();
	setNodeSelected(id);
}

async function onCopyNodes(ids: string[]) {
	await copyNodes(ids);

	toast.showMessage({ title: i18n.baseText('generic.copiedToClipboard'), type: 'success' });
}

async function onClipboardPaste(plainTextData: string): Promise<void> {
	if (
		isCanvasReadOnly.value ||
		getNodeViewTab(route) !== MAIN_HEADER_TABS.WORKFLOW ||
		!keyBindingsEnabled.value ||
		!checkIfEditingIsAllowed()
	) {
		return;
	}

	let workflowData: WorkflowDataUpdate | null | undefined = null;

	// Check if it is an URL which could contain workflow data
	if (plainTextData.match(VALID_WORKFLOW_IMPORT_URL_REGEX)) {
		const importConfirm = await message.confirm(
			i18n.baseText('nodeView.confirmMessage.onClipboardPasteEvent.message', {
				interpolate: { plainTextData },
			}),
			i18n.baseText('nodeView.confirmMessage.onClipboardPasteEvent.headline'),
			{
				type: 'warning',
				confirmButtonText: i18n.baseText(
					'nodeView.confirmMessage.onClipboardPasteEvent.confirmButtonText',
				),
				cancelButtonText: i18n.baseText(
					'nodeView.confirmMessage.onClipboardPasteEvent.cancelButtonText',
				),
			},
		);

		if (importConfirm !== MODAL_CONFIRM) {
			return;
		}

		workflowData = await fetchWorkflowDataFromUrl(plainTextData);
	} else {
		// Pasted data is possible workflow data
		workflowData = jsonParse<WorkflowDataUpdate | null>(plainTextData, { fallbackValue: null });
	}

	if (!workflowData) {
		return;
	}

	const result = await importWorkflowData(workflowData, 'paste', {
		importTags: false,
		viewport: viewportBoundaries.value,
	});
	const ids = result.nodes?.map((node) => node.id) ?? [];

	canvasRef.value?.ensureNodesAreVisible(ids);
}

async function onCutNodes(ids: string[]) {
	if (isCanvasReadOnly.value) {
		await copyNodes(ids);
	} else {
		await cutNodes(ids);
	}
}

async function onDuplicateNodes(ids: string[]) {
	if (!checkIfEditingIsAllowed()) {
		return;
	}

	const newIds = await duplicateNodes(ids, {
		viewport: viewportBoundaries.value,
	});

	canvasRef.value?.ensureNodesAreVisible(newIds);
}

function onPinNodes(ids: string[], source: PinDataSource) {
	if (!checkIfEditingIsAllowed()) {
		return;
	}

	toggleNodesPinned(ids, source);
}

function onContextMenuAction(action: ContextMenuAction, nodeIds: string[]) {
	canvasRef.value?.executeContextMenuAction(action, nodeIds);
}

function addWorkflowSavedEventBindings() {
	canvasEventBus.on('saved:workflow', npsSurveyStore.showNpsSurveyIfPossible);
}

function removeWorkflowSavedEventBindings() {
	canvasEventBus.off('saved:workflow', npsSurveyStore.showNpsSurveyIfPossible);
	canvasEventBus.off('saved:workflow', onSaveFromWithinExecutionDebug);
}

async function onCreateWorkflow() {
	await router.push({ name: VIEWS.NEW_WORKFLOW });
}

async function onSaveWorkflow() {
	await workflowSaving.saveCurrentWorkflow({});
}

function onRenameNode(name: string) {
	if (ndvStore.value.activeNode?.name) {
		void renameNode(ndvStore.value.activeNode.name, name);
	}
}

async function onOpenRenameNodeModal(id: string) {
	const currentName = workflowDocumentStore?.value?.getNodeById(id)?.name ?? '';

	const activeElement = document.activeElement;

	if (activeElement && shouldIgnoreCanvasShortcut(activeElement)) {
		// If an input is focused, do not open the rename modal
		return;
	}

	if (!keyBindingsEnabled.value || document.querySelector('.rename-prompt')) return;

	try {
		const promptResponsePromise = message.prompt(
			i18n.baseText('nodeView.prompt.newName') + ':',
			i18n.baseText('nodeView.prompt.renameNode') + `: ${currentName}`,
			{
				customClass: 'rename-prompt',
				confirmButtonText: i18n.baseText('nodeView.prompt.rename'),
				cancelButtonText: i18n.baseText('nodeView.prompt.cancel'),
				inputErrorMessage: i18n.baseText('nodeView.prompt.invalidName'),
				inputValue: currentName,
				inputValidator: (value: string) => {
					if (!value.trim()) {
						return i18n.baseText('nodeView.prompt.invalidName');
					}
					return true;
				},
			},
		);

		// Wait till input is displayed
		await nextTick();

		// Focus and select input content
		const nameInput = document.querySelector<HTMLInputElement>('.rename-prompt .el-input__inner');
		nameInput?.focus();
		nameInput?.select();

		// Stop propagation for space key to prevent VueFlow from intercepting it
		// when modifier keys (like Shift) are pressed. See: https://github.com/bcakmakoglu/vue-flow/issues/1999
		const handleKeyDown = (e: KeyboardEvent) => {
			if (e.key === ' ') {
				e.stopPropagation();
			}
		};
		nameInput?.addEventListener('keydown', handleKeyDown);

		const promptResponse = await promptResponsePromise;

		nameInput?.removeEventListener('keydown', handleKeyDown);

		if (promptResponse.action === MODAL_CONFIRM) {
			await renameNode(currentName, promptResponse.value, { trackHistory: true });
		}
	} catch (e) {}
}

async function onRevertRenameNode({
	currentName,
	newName,
}: {
	currentName: string;
	newName: string;
}) {
	await revertRenameNode(currentName, newName);
}

async function onRevertReplaceNodeParameters({
	nodeId,
	currentProperties,
	newProperties,
}: {
	nodeId: string;
	currentProperties: INodeParameters;
	newProperties: INodeParameters;
}) {
	await revertReplaceNodeParameters(nodeId, currentProperties, newProperties);
}

function onUpdateNodeParameters(id: string, parameters: Record<string, unknown>) {
	setNodeParameters(id, parameters);
}

function onUpdateNodeInputs(id: string) {
	revalidateNodeInputConnections(id);
}

function onUpdateNodeOutputs(id: string) {
	revalidateNodeOutputConnections(id);
}

function onClickNodeAdd(source: string, sourceHandle: string) {
	nodeCreatorStore.openNodeCreatorForConnectingNode({
		workflowId: workflowId.value,
		connection: {
			source,
			sourceHandle,
		},
		eventSource: NODE_CREATOR_OPEN_SOURCES.PLUS_ENDPOINT,
	});
}

/**
 * Credentials
 */

async function loadCredentials() {
	let options: { workflowId: string } | { projectId: string };

	if (workflowId.value && !isNewWorkflowRoute.value) {
		options = { workflowId: workflowId.value };
	} else {
		const queryParam =
			typeof route.query?.projectId === 'string' ? route.query?.projectId : undefined;
		const projectId = queryParam ?? projectsStore.personalProject?.id;
		if (projectId === undefined) {
			throw new Error(
				'Could not find projectId in the query nor could I find the personal project in the project store',
			);
		}

		options = { projectId };
	}

	await credentialsStore.fetchAllCredentialsForWorkflow(options);
}

/**
 * Connections
 */

function onCreateConnection(connection: Connection) {
	createConnection(connection, { trackHistory: true });
}

function onRevertCreateConnection({ connection }: { connection: [IConnection, IConnection] }) {
	revertCreateConnection(connection);
}

function onCreateConnectionCancelled(
	event: ConnectStartEvent,
	position: VueFlowXYPosition,
	mouseEvent?: MouseEvent,
) {
	const preventDefault = (mouseEvent?.target as HTMLElement).classList?.contains('clickable');
	if (preventDefault) {
		return;
	}

	uiStore.lastInteractedWithNodeId = event.nodeId;
	uiStore.lastInteractedWithNodeHandle = event.handleId;
	uiStore.lastCancelledConnectionPosition = [position.x, position.y];

	setTimeout(() => {
		if (!event.nodeId) return;

		nodeCreatorStore.openNodeCreatorForConnectingNode({
			workflowId: workflowId.value,
			connection: {
				source: event.nodeId,
				sourceHandle: event.handleId,
			},
			eventSource: NODE_CREATOR_OPEN_SOURCES.NODE_CONNECTION_DROP,
		});
	});
}

function onDeleteConnection(connection: Connection) {
	deleteConnection(connection, { trackHistory: true });
}

function onRevertDeleteConnection({ connection }: { connection: [IConnection, IConnection] }) {
	revertDeleteConnection(connection);
}

/**
 * Import / Export
 */

async function onImportWorkflowDataEvent(data: IDataObject) {
	const workflowData = data.data as WorkflowDataUpdate;
	const trackEvents = typeof data.trackEvents === 'boolean' ? data.trackEvents : undefined;
	const setStateDirty = typeof data.setStateDirty === 'boolean' ? data.setStateDirty : undefined;

	await importWorkflowData(workflowData, 'file', {
		viewport: viewportBoundaries.value,
		regenerateIds: data.regenerateIds === true || data.regenerateIds === undefined,
		trackEvents,
		setStateDirty,
	});

	await nextTick();
	fitView();

	selectNodes(workflowData.nodes?.map((node) => node.id) ?? []);
	if (data.tidyUp) {
		const nodesIdsToTidyUp = data.nodesIdsToTidyUp as string[];
		setTimeout(async () => {
			canvasEventBus.emit('tidyUp', {
				source: 'import-workflow-data',
				nodeIdsFilter: nodesIdsToTidyUp,
				trackEvents,
			});

			await nextTick();
			fitView();
		}, 0);
	}
}

async function onImportWorkflowUrlEvent(data: IDataObject) {
	const workflowData = await fetchWorkflowDataFromUrl(data.url as string);
	if (!workflowData) {
		return;
	}

	await importWorkflowData(workflowData, 'url', {
		viewport: viewportBoundaries.value,
	});

	canvasRef.value?.ensureNodesAreVisible(workflowData.nodes?.map((node) => node.id) ?? []);
}

function addImportEventBindings() {
	nodeViewEventBus.on('importWorkflowData', onImportWorkflowDataEvent);
	nodeViewEventBus.on('importWorkflowUrl', onImportWorkflowUrlEvent);
	nodeViewEventBus.on('openChat', onOpenChat);
}

function removeImportEventBindings() {
	nodeViewEventBus.off('importWorkflowData', onImportWorkflowDataEvent);
	nodeViewEventBus.off('importWorkflowUrl', onImportWorkflowUrlEvent);
	nodeViewEventBus.off('openChat', onOpenChat);
}

/**
 * Node creator
 */
const nodeCreatorReplaceTargetId = ref<string | undefined>(undefined);

function onNodeCreatorClose() {
	nodeCreatorReplaceTargetId.value = undefined;
}

async function onAddNodesAndConnections(
	{ nodes, connections }: AddedNodesAndConnections,
	dragAndDrop = false,
	position?: XYPosition,
) {
	if (!checkIfEditingIsAllowed()) {
		return;
	}

	if (nodeCreatorReplaceTargetId.value !== undefined) {
		uiStore.resetLastInteractedWith();

		nodes = nodes.map((x) => ({
			...x,
			openDetail: false,
		}));
	}

	const { addedNodes } = await addNodesAndConnections(nodes, connections, {
		dragAndDrop,
		position,
		viewport: viewportBoundaries.value,
		telemetry: true,
		replaceNodeId: nodeCreatorReplaceTargetId.value,
	});

	if (addedNodes.length > 0) {
		const lastAddedNodeId = addedNodes[addedNodes.length - 1].id;
		selectNodes([lastAddedNodeId]);
	}
}

async function onRevertAddNode({ node }: { node: INodeUi }) {
	await revertAddNode(node.name);
}

function onSwitchActiveNode(nodeName: string) {
	const node = workflowDocumentStore?.value?.getNodeByName(nodeName);
	if (!node) return;

	setNodeActiveByName(nodeName, 'other');
	selectNodes([node.id]);
}

function onOpenSelectiveNodeCreator(
	node: string,
	connectionType: NodeConnectionType,
	connectionIndex: number = 0,
) {
	nodeCreatorStore.openSelectiveNodeCreator({
		workflowId: workflowId.value,
		node,
		connectionType,
		connectionIndex,
	});
}

function onToggleNodeCreator(options: ToggleNodeCreatorOptions) {
	nodeCreatorStore.setNodeCreatorState({ ...options, workflowId: workflowId.value });

	if (!options.createNodeActive) {
		nodeCreatorReplaceTargetId.value = undefined;
		if (!options.hasAddedNodes) uiStore.resetLastInteractedWith();
	}
}

function onOpenNodeCreatorFromCanvas(source: NodeCreatorOpenSource) {
	onToggleNodeCreator({ createNodeActive: true, source });
}

function onOpenNodeCreatorForTriggerNodes(source: NodeCreatorOpenSource) {
	nodeCreatorStore.openNodeCreatorForTriggerNodes(workflowId.value, source);
}

function onToggleFocusPanel() {
	focusPanelStore.toggleFocusPanel();
	telemetry.track(`User ${focusPanelStore.focusPanelActive ? 'opened' : 'closed'} focus panel`, {
		source: 'canvasKeyboardShortcut',
		parameters: focusPanelStore.focusedNodeParametersInTelemetryFormat,
		parameterCount: focusPanelStore.focusedNodeParametersInTelemetryFormat.length,
	});
}

function closeNodeCreator() {
	if (nodeCreatorStore.isCreateNodeActive) {
		nodeCreatorStore.isCreateNodeActive = false;
	}
}

function onCreateSticky() {
	void onAddNodesAndConnections({ nodes: [{ type: STICKY_NODE_TYPE }], connections: [] });
}

function onClickConnectionAdd(connection: Connection) {
	const { type, mode } = parseCanvasConnectionHandleString(connection.sourceHandle);
	const isAddBetwenTool =
		type === NodeConnectionTypes.AiTool && mode === CanvasConnectionMode.Output;
	if (isAddBetwenTool) {
		nodeCreatorStore.openNodeCreatorForConnectingNode({
			workflowId: workflowId.value,
			connection,
			eventSource: NODE_CREATOR_OPEN_SOURCES.NODE_CONNECTION_ACTION,
			nodeCreatorView: HUMAN_IN_THE_LOOP_CATEGORY,
		});
	} else {
		nodeCreatorStore.openNodeCreatorForConnectingNode({
			workflowId: workflowId.value,
			connection,
			eventSource: NODE_CREATOR_OPEN_SOURCES.NODE_CONNECTION_ACTION,
		});
	}
}

function onClickReplaceNode(nodeId: string) {
	const node = workflowDocumentStore?.value?.getNodeById(nodeId);
	const expression = workflowDocumentStore?.value?.getExpressionHandler();
	if (!node || !expression) return;
	const nodeType = nodeTypesStore.getNodeType(node.type);
	if (!nodeType) return;

	nodeCreatorReplaceTargetId.value = nodeId;
	nodeCreatorStore.openingContext = 'replacement';
	if (isTriggerNode(nodeType)) {
		nodeCreatorStore.openNodeCreatorForTriggerNodes(
			workflowId.value,
			NODE_CREATOR_OPEN_SOURCES.REPLACE_NODE_ACTION,
		);
	} else {
		const inputs = NodeHelpers.getNodeInputs({ expression }, node, nodeType).map((output) =>
			typeof output === 'string' ? output : output.type,
		);
		const outputs = NodeHelpers.getNodeOutputs({ expression }, node, nodeType).map((output) =>
			typeof output === 'string' ? output : output.type,
		);

		// We want to infer a matching filter to show, e.g. when swapping out tools
		// But without direct identification on various node types
		// Our best bet is to rely in input and/or output types, and defaulting
		// back to showing all nodes in edge cases
		if (inputs[0] && outputs[0] && inputs[0] !== outputs[0]) {
			nodeCreatorStore.openNodeCreatorForRegularNodes(
				workflowId.value,
				NODE_CREATOR_OPEN_SOURCES.REPLACE_NODE_ACTION,
			);
		} else {
			nodeCreatorStore.openSelectiveNodeCreator({
				workflowId: workflowId.value,
				connectionType: inputs[0] ?? outputs[0],
				node: node.name,
			});
		}
	}
}

/**
 * Permissions
 */

const workflowPermissions = computed(() => {
	return workflowId.value
		? getResourcePermissions(workflowsListStore.getWorkflowById(workflowId.value)?.scopes).workflow
		: {};
});

const projectPermissions = computed(() => {
	const project = route.query?.projectId
		? projectsStore.myProjects.find((p) => p.id === route.query.projectId)
		: (projectsStore.currentProject ?? projectsStore.personalProject);
	return getResourcePermissions(project?.scopes);
});

/**
 * Executions
 */

const isStoppingExecution = ref(false);

const isWorkflowRunning = computed(() => workflowExecutionState.value.isWorkflowRunning);
const isExecutionWaitingForWebhook = computed(
	() => workflowExecutionState.value.executionWaitingForWebhook,
);

const isExecutionDisabled = computed(() => {
	if (
		containsChatTriggerNodes.value &&
		isOnlyChatTriggerNodeActive.value &&
		!chatTriggerNodePinnedData.value
	) {
		return true;
	}

	return !containsTriggerNodes.value || allTriggerNodesDisabled.value;
});

const isRunWorkflowButtonVisible = computed(
	() => !isOnlyChatTriggerNodeActive.value || chatTriggerNodePinnedData.value,
);
const isStopExecutionButtonVisible = computed(
	() => isWorkflowRunning.value && !isExecutionWaitingForWebhook.value,
);
const isStopWaitingForWebhookButtonVisible = computed(
	() => isWorkflowRunning.value && isExecutionWaitingForWebhook.value,
);

async function onRunWorkflowToNode(id: string) {
	const node = workflowDocumentStore?.value?.getNodeById(id);
	if (!node) return;

	if (needsAgentInput(node) && nodeTypesStore.isToolNode(node.type)) {
		uiStore.openModalWithData({
			name: FROM_AI_PARAMETERS_MODAL_KEY,
			data: {
				nodeName: node.name,
			},
		});
	} else {
		trackRunWorkflowToNode(node);
		agentRequestStore.clearAgentRequests(workflowId.value, node.id);

		void runWorkflow({
			destinationNode: { nodeName: node.name, mode: 'inclusive' },
			source: 'Node.executeNode',
		});
	}
}

async function copyWebhookUrl(id: string, webhookType: 'test' | 'production') {
	const node = workflowDocumentStore?.value?.getNodeById(id);

	if (!node) return;

	const nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion);
	if (!nodeType?.webhooks?.length) return;

	const webhook = nodeType.webhooks[0];
	const webhookUrl = await workflowHelpers.getWebhookUrl(webhook, node, webhookType);

	if (!webhookUrl) return;

	void clipboard.copy(webhookUrl);

	toast.showMessage({
		title: i18n.baseText('nodeWebhooks.showMessage.title'),
		type: 'success',
	});
}

async function onCopyTestUrl(id: string) {
	const node = workflowDocumentStore?.value?.getNodeById(id);
	const isProductionOnly = PRODUCTION_ONLY_TRIGGER_NODE_TYPES.includes(node?.type ?? '');

	if (isProductionOnly) {
		toast.showMessage({
			title: i18n.baseText('nodeWebhooks.showMessage.testWebhookUrl'),
			type: 'warning',
		});
		return;
	}

	void copyWebhookUrl(id, 'test');
}

async function onCopyProductionUrl(id: string) {
	const isWorkflowActive = workflowDocumentStore?.value?.active ?? false;
	if (!isWorkflowActive) {
		toast.showMessage({
			title: i18n.baseText('nodeWebhooks.showMessage.not.active'),
			type: 'warning',
		});
		return;
	}
	void copyWebhookUrl(id, 'production');
}

function trackRunWorkflowToNode(node: INodeUi) {
	const telemetryPayload = {
		node_type: node.type,
		workflow_id: workflowId.value,
		source: 'canvas',
		push_ref: ndvStore.value.pushRef,
	};

	telemetry.track('User clicked execute node button', telemetryPayload);
	void externalHooks.run('nodeView.onRunNode', telemetryPayload);
}

function onExecutionOpenedWithError(data: IExecutionResponse) {
	if (!data.finished && data.data?.resultData?.error) {
		// Check if any node contains an error
		let nodeErrorFound = false;
		if (data.data.resultData.runData) {
			const runData = data.data.resultData.runData;
			errorCheck: for (const nodeName of Object.keys(runData)) {
				for (const taskData of runData[nodeName]) {
					if (taskData.error) {
						nodeErrorFound = true;
						break errorCheck;
					}
				}
			}
		}

		if (
			!nodeErrorFound &&
			(data.data.resultData.error.stack ?? data.data.resultData.error.message)
		) {
			// Display some more information for now in console to make debugging easier
			console.error(`Execution ${data.id} error:`);
			console.error(data.data.resultData.error.stack);
			toast.showMessage({
				title: i18n.baseText('nodeView.showError.workflowError'),
				message: data.data.resultData.error.message,
				type: 'error',
				duration: 0,
			});
		}
	}
}

function onExecutionOpenedWithWaitTill(data: IExecutionResponse) {
	if ((data as ExecutionSummary).waitTill) {
		toast.showMessage({
			title: i18n.baseText('nodeView.thisExecutionHasntFinishedYet'),
			message: h(NodeViewUnfinishedWorkflowMessage),
			type: 'warning',
			duration: 0,
		});
	}
}

function addExecutionOpenedEventBindings() {
	canvasEventBus.on('open:execution', onExecutionOpenedWithError);
	canvasEventBus.on('open:execution', onExecutionOpenedWithWaitTill);
}

function removeExecutionOpenedEventBindings() {
	canvasEventBus.off('open:execution', onExecutionOpenedWithError);
	canvasEventBus.off('open:execution', onExecutionOpenedWithWaitTill);
}

async function onStopExecution() {
	isStoppingExecution.value = true;
	await stopCurrentExecution();
	isStoppingExecution.value = false;
}

async function onStopWaitingForWebhook() {
	await stopWaitingForWebhook();
}

function onRunWorkflowButtonMouseEnter() {
	nodeViewEventBus.emit('runWorkflowButton:mouseenter');
}

function onRunWorkflowButtonMouseLeave() {
	nodeViewEventBus.emit('runWorkflowButton:mouseleave');
}

/**
 * Chat
 */

const chatTriggerNode = computed(() => {
	return workflowDocumentStore?.value?.allNodes.find(
		(node) => node.type === CHAT_TRIGGER_NODE_TYPE,
	);
});

const containsChatTriggerNodes = computed(() => {
	return (
		!isExecutionWaitingForWebhook.value &&
		!!workflowDocumentStore?.value?.allNodes.find(
			(node) =>
				[MANUAL_CHAT_TRIGGER_NODE_TYPE, CHAT_TRIGGER_NODE_TYPE].includes(node.type) &&
				node.disabled !== true,
		)
	);
});

const isOnlyChatTriggerNodeActive = computed(() => {
	return triggerNodes.value.every((node) => node.disabled || node.type === CHAT_TRIGGER_NODE_TYPE);
});

const chatTriggerNodePinnedData = computed(() => {
	if (!chatTriggerNode.value) return null;

	return workflowDocumentStore?.value?.pinnedDataByNodeName?.[chatTriggerNode.value.name];
});

const isChatHubAvailable = computed(() => {
	return (
		chatHubPanelStore.isFloatingChatEnabled &&
		chatTriggerNode.value?.parameters?.availableInChat === true
	);
});

const isChatHubPanelOpen = computed(() => chatHubPanelStore.isOpen);

// Close chat hub panel when availableInChat is toggled off
watch(isChatHubAvailable, (available) => {
	if (!available && isChatHubPanelOpen.value) {
		chatHubPanelStore.close();
	}
});

function onOpenChat() {
	if (isChatHubAvailable.value) {
		chatHubPanelStore.open();
	} else {
		startChat('main');
	}
}

function onToggleChat() {
	if (isChatHubAvailable.value) {
		if (isChatHubPanelOpen.value) {
			chatHubPanelStore.close();
		} else {
			chatHubPanelStore.open();
		}
	} else {
		if (isLogsPanelOpen.value) {
			logsStore.toggleOpen(false);
		} else {
			startChat('main');
		}
	}
}

/**
 * Evaluation
 */
const evaluationTriggerNode = computed(() => {
	return workflowDocumentStore?.value?.allNodes.find(
		(node) => node.type === EVALUATION_TRIGGER_NODE_TYPE,
	);
});

const isHandlingEvaluationTriggerRouteAction = ref(false);

async function addEvaluationTriggerNodeFromRoute() {
	if (isHandlingEvaluationTriggerRouteAction.value) return;

	isHandlingEvaluationTriggerRouteAction.value = true;

	try {
		if (!canvasRef.value) {
			await new Promise<void>((resolve) => {
				const stop = watch(canvasRef, (val) => {
					if (val) {
						stop();
						resolve();
					}
				});
			});
		}

		if (!checkIfEditingIsAllowed()) return;

		if (evaluationTriggerNode.value) {
			setNodeActiveByName(evaluationTriggerNode.value.name, 'other');
			canvasRef.value?.ensureNodesAreVisible([evaluationTriggerNode.value.id]);
			return;
		}

		const { addedNodes } = await addNodesAndConnections(
			[{ type: EVALUATION_TRIGGER_NODE_TYPE, openDetail: true }],
			[],
			{
				viewport: viewportBoundaries.value,
				telemetry: true,
			},
		);

		if (addedNodes.length > 0) {
			const addedNode = addedNodes[addedNodes.length - 1];
			await nextTick();
			canvasRef.value?.ensureNodesAreVisible([addedNode.id]);
		}
	} finally {
		try {
			await router.replace({ query: { ...route.query, action: undefined } });
		} finally {
			isHandlingEvaluationTriggerRouteAction.value = false;
		}
	}
}

/**
 * History events
 */

function addUndoRedoEventBindings() {
	historyBus.on('nodeMove', onRevertNodePosition);
	historyBus.on('revertAddNode', onRevertAddNode);
	historyBus.on('revertRemoveNode', onRevertDeleteNode);
	historyBus.on('revertAddConnection', onRevertCreateConnection);
	historyBus.on('revertRemoveConnection', onRevertDeleteConnection);
	historyBus.on('revertRenameNode', onRevertRenameNode);
	historyBus.on('revertReplaceNodeParameters', onRevertReplaceNodeParameters);
	historyBus.on('enableNodeToggle', onRevertToggleNodeDisabled);
	historyBus.on('revertAddNodeGroup', onRevertAddNodeGroup);
	historyBus.on('revertRemoveNodeGroup', onRevertRemoveNodeGroup);
	historyBus.on('revertUpdateNodeGroup', onRevertUpdateNodeGroup);
}

function removeUndoRedoEventBindings() {
	historyBus.off('nodeMove', onRevertNodePosition);
	historyBus.off('revertAddNode', onRevertAddNode);
	historyBus.off('revertRemoveNode', onRevertDeleteNode);
	historyBus.off('revertAddConnection', onRevertCreateConnection);
	historyBus.off('revertRemoveConnection', onRevertDeleteConnection);
	historyBus.off('revertRenameNode', onRevertRenameNode);
	historyBus.off('revertReplaceNodeParameters', onRevertReplaceNodeParameters);
	historyBus.off('enableNodeToggle', onRevertToggleNodeDisabled);
	historyBus.off('revertAddNodeGroup', onRevertAddNodeGroup);
	historyBus.off('revertRemoveNodeGroup', onRevertRemoveNodeGroup);
	historyBus.off('revertUpdateNodeGroup', onRevertUpdateNodeGroup);
}

/**
 * Source control
 */

async function onSourceControlPull() {
	try {
		await Promise.all([
			environmentsStore.fetchAllVariables(),
			tagsStore.fetchAll(),
			loadCredentials(),
			projectsStore.getAvailableProjects(),
		]);

		if (workflowId.value && !uiStore.stateIsDirty) {
			const workflowData = await workflowsListStore.fetchWorkflow(workflowId.value);
			if (workflowData) {
				await openWorkflow(workflowData);
			}
		}
	} catch (error) {
		console.error(error);
	}
}

function addSourceControlEventBindings() {
	sourceControlEventBus.on('pull', onSourceControlPull);
}

function removeSourceControlEventBindings() {
	sourceControlEventBus.off('pull', onSourceControlPull);
}

/**
 * Command bar
 * */
function addCommandBarEventBindings() {
	canvasEventBus.on('create:sticky', onCreateSticky);
}

function removeCommandBarEventBindings() {
	canvasEventBus.off('create:sticky', onCreateSticky);
}

/**
 * Permission checks
 */

function checkIfEditingIsAllowed(): boolean {
	if (!uiStore.nodeViewInitialized) {
		return true;
	}

	if (readOnlyNotification.value?.visible) {
		return false;
	}

	if (!(workflowPermissions.value.update ?? projectPermissions.value.workflow.update)) {
		return false;
	}

	if (collaborationStore.shouldBeReadOnly) {
		return false;
	}

	if (isReadOnlyRoute.value || isReadOnlyEnvironment.value) {
		const messageContext = isReadOnlyRoute.value ? 'executions' : 'workflows';
		readOnlyNotification.value = toast.showMessage({
			title: i18n.baseText(
				isReadOnlyEnvironment.value
					? `readOnlyEnv.showMessage.${messageContext}.title`
					: 'readOnly.showMessage.executions.title',
			),
			message: i18n.baseText(
				isReadOnlyEnvironment.value
					? `readOnlyEnv.showMessage.${messageContext}.message`
					: 'readOnly.showMessage.executions.message',
			),
			type: 'info',
		}) as unknown as { visible: boolean };

		return false;
	}

	return true;
}

function onAcquireEditingClick() {
	collaborationStore.requestWriteAccessForce();
}

function checkIfRouteIsAllowed() {
	if (
		isReadOnlyEnvironment.value &&
		[VIEWS.NEW_WORKFLOW, VIEWS.TEMPLATE_IMPORT].find((view) => view === route.name)
	) {
		void nextTick(async () => {
			resetWorkspace();
			uiStore.markStateClean();

			await router.replace({ name: VIEWS.HOMEPAGE });
		});
	}
}

/**
 * Debug mode
 */

async function onSaveFromWithinExecutionDebug() {
	if (route.name !== VIEWS.EXECUTION_DEBUG) return;

	await router.replace({
		name: VIEWS.WORKFLOW,
		params: { workflowId: workflowId.value },
	});
}

/**
 * Canvas
 */

const viewportTransform = ref<ViewportTransform>({ x: 0, y: 0, zoom: 1 });
const viewportDimensions = ref<Dimensions>({ width: 0, height: 0 });

const viewportBoundaries = computed<ViewportBoundaries>(() =>
	getBounds(viewportTransform.value, viewportDimensions.value),
);

function onViewportChange(viewport: ViewportTransform, dimensions: Dimensions) {
	viewportTransform.value = viewport;
	viewportDimensions.value = dimensions;
	uiStore.nodeViewOffsetPosition = [viewport.x, viewport.y];
}

function selectNodes(ids: string[]) {
	setTimeout(() => canvasEventBus.emit('nodes:select', { ids }));
}

/**
 * Mouse events
 */

function onClickPane(position: VueFlowXYPosition) {
	lastClickPosition.value = [position.x, position.y];
	onSetNodeSelected();
}

function onSelectionEnd(position: VueFlowXYPosition) {
	lastClickPosition.value = [position.x, position.y];
}

/**
 * Drag and Drop events
 */

async function onDragAndDrop(position: VueFlowXYPosition, event: DragEvent) {
	if (!event.dataTransfer) {
		return;
	}

	const dropData = jsonParse<AddedNodesAndConnections>(
		event.dataTransfer.getData(DRAG_EVENT_DATA_KEY),
	);

	if (dropData) {
		const insertNodePosition: XYPosition = [position.x, position.y];

		await onAddNodesAndConnections(dropData, true, insertNodePosition);

		onToggleNodeCreator({ createNodeActive: false, hasAddedNodes: true });
	}
}

/**
 * Custom Actions
 */

function registerCustomActions() {
	registerCustomAction({
		key: 'openNodeDetail',
		action: ({ node }: { node: string }) => {
			setNodeActiveByName(node, 'other');
		},
	});

	registerCustomAction({
		key: 'openSelectiveNodeCreator',
		action: ({
			creatorview: creatorView,
			connectiontype: connectionType,
			node,
		}: {
			creatorview: NodeFilterType;
			connectiontype: NodeConnectionType;
			node: string;
		}) => {
			nodeCreatorStore.openSelectiveNodeCreator({
				workflowId: workflowId.value,
				node,
				connectionType,
				creatorView,
			});
		},
	});

	registerCustomAction({
		key: 'showNodeCreator',
		action: () => {
			ndvStore.value.unsetActiveNodeName();

			void nextTick(() => {
				void onOpenNodeCreatorForTriggerNodes(NODE_CREATOR_OPEN_SOURCES.NODE_SHORTCUT);
			});
		},
	});
}

function unregisterCustomActions() {
	unregisterCustomAction('openNodeDetail');
	unregisterCustomAction('openSelectiveNodeCreator');
	unregisterCustomAction('showNodeCreator');
}

function showAddFirstStepIfEnabled() {
	if (uiStore.addFirstStepOnLoad) {
		void onOpenNodeCreatorForTriggerNodes(
			uiStore.addFirstStepOnLoadSource ?? NODE_CREATOR_OPEN_SOURCES.TRIGGER_PLACEHOLDER_BUTTON,
		);
		uiStore.addFirstStepOnLoad = false;
		uiStore.addFirstStepOnLoadSource = undefined;
	}
}

async function handlePendingBuilderPrompt() {
	const pendingPrompt = emptyStateBuilderPromptStore.consumePendingPrompt();
	if (pendingPrompt) {
		await chatPanelStore.open({ mode: 'builder', showCoachmark: false });
		await builderStore.sendChatMessage({
			text: pendingPrompt,
			initialGeneration: true,
			source: 'empty-state',
		});
	}
}

/**
 * Routing
 */

function updateNodeRoute(nodeId: string) {
	const nodeUi = workflowDocumentStore?.value?.findNodeByPartialId(nodeId);
	if (nodeUi) {
		setNodeActive(nodeUi.id, 'other');
	} else {
		toast.showToast({
			title: i18n.baseText('nodeView.showMessage.ndvUrl.missingNodes.title'),
			message: i18n.baseText('nodeView.showMessage.ndvUrl.missingNodes.content'),
			type: 'warning',
		});
		void router.replace({
			name: route.name,
			params: { workflowId: workflowId.value },
			query: route.query,
		});
	}
}

watch([() => route.name, () => route.params.workflowId], () => {
	// Handle route-specific actions (query actions, debug mode event binding, node issues)
	initializeRoute();
});

watch(
	() => {
		return (
			isLoading.value ||
			isCanvasReadOnly.value ||
			(workflowDocumentStore?.value?.allNodes ?? []).length !== 0
		);
	},
	(isReadOnlyOrLoading) => {
		if (isReadOnlyOrLoading) {
			fallbackNodes.value = [];
			return;
		}

		const addNodesItem: INodeUi = {
			id: CanvasNodeRenderType.AddNodes,
			name: CanvasNodeRenderType.AddNodes,
			type: CanvasNodeRenderType.AddNodes,
			typeVersion: 1,
			position: [0, 0],
			parameters: {},
		};

		const choicePromptItem: INodeUi = {
			id: CanvasNodeRenderType.ChoicePrompt,
			name: CanvasNodeRenderType.ChoicePrompt,
			type: CanvasNodeRenderType.ChoicePrompt,
			typeVersion: 1,
			position: [0, 0],
			parameters: {},
			draggable: false,
		};

		fallbackNodes.value = builderStore.isAIBuilderEnabled ? [choicePromptItem] : [addNodesItem];
	},
);

// This keeps the selected node in sync if the URL is updated
watch(
	() => route.params.nodeId,
	async (newId) => {
		if (typeof newId !== 'string' || newId === '') ndvStore.value.unsetActiveNodeName();
		else {
			updateNodeRoute(newId);
		}
	},
);

// This keeps URL in sync if the activeNode is changed
watch(
	() => ndvStore.value.activeNode,
	async (val) => {
		// This is just out of caution
		if (!([VIEWS.WORKFLOW] as string[]).includes(String(route.name))) return;

		// Route params default to '' instead of undefined if not present
		const nodeId = val?.id ? workflowsStore.getPartialIdForNode(val?.id) : '';

		if (nodeId !== route.params.nodeId) {
			await router.replace({
				name: route.name,
				params: { workflowId: workflowId.value, nodeId },
				query: route.query,
			});
		}
	},
);

// Acquire write access when user makes first edit, and trigger auto-save
watch(
	() => uiStore.dirtyStateSetCount,
	(dirtyStateSetCount) => {
		if (dirtyStateSetCount > 0) {
			// Skip write access and auto-save in demo mode
			if (isDemoRoute.value) return;

			collaborationStore.requestWriteAccess();

			// Trigger auto-save (debounced) for writers only
			// Skip auto-save when AI Builder is streaming to keep version history clean
			if (!builderStore.streaming) {
				void workflowSaving.autoSaveWorkflow();
			}
		}
	},
);

// Trigger auto-save when AI Builder streaming ends with unsaved changes
watch(
	() => builderStore.streaming,
	(isStreaming, wasStreaming) => {
		// Only trigger when streaming just ended (was streaming, now not)
		if (wasStreaming && !isStreaming) {
			// Trigger auto-save if there are unsaved changes
			if (uiStore.stateIsDirty && !collaborationStore.shouldBeReadOnly) {
				void workflowSaving.autoSaveWorkflow();
			}
		}
	},
);

const workflowExecutionTriggerNodeName = computed(() => {
	if (!isWorkflowRunning.value) {
		return undefined;
	}

	if (workflowExecutionState.value.activeExecution?.triggerNode) {
		return workflowExecutionState.value.activeExecution.triggerNode;
	}

	// In case of partial execution, triggerNode is not set, so I'm trying to find from runData
	return Object.keys(
		workflowExecutionState.value.activeExecution?.data?.resultData.runData ?? {},
	).find((name) =>
		workflowDocumentStore?.value?.workflowTriggerNodes.some((node) => node.name === name),
	);
});

watch(
	[selectableTriggerNodes, workflowExecutionTriggerNodeName],
	([newSelectable, currentTrigger], [oldSelectable]) => {
		if (currentTrigger !== undefined) {
			workflowExecutionState.value.setSelectedTriggerNodeName(currentTrigger);
			return;
		}

		if (
			workflowExecutionState.value.selectedTriggerNodeName === undefined ||
			newSelectable.every(
				(node) => node.name !== workflowExecutionState.value.selectedTriggerNodeName,
			)
		) {
			workflowExecutionState.value.setSelectedTriggerNodeName(
				findTriggerNodeToAutoSelect(selectableTriggerNodes.value, nodeTypesStore.getNodeType)?.name,
			);
			return;
		}

		const newTrigger = newSelectable?.find((node) =>
			oldSelectable?.every((old) => old.name !== node.name),
		);

		if (newTrigger !== undefined) {
			// Select newly added node
			workflowExecutionState.value.setSelectedTriggerNodeName(newTrigger.name);
		}
	},
	{ immediate: true },
);

onBeforeRouteLeave(async (to, from, next) => {
	// Close the focus panel when leaving the workflow view
	if (focusPanelStore.focusPanelActive) {
		focusPanelStore.closeFocusPanel();
	}

	if (isReadOnlyEnvironment.value) {
		next();
		return;
	}

	const toNodeViewTab = getNodeViewTab(to);
	const isNavigatingBetweenWorkflows =
		[VIEWS.WORKFLOW, VIEWS.NEW_WORKFLOW].includes(from.name as VIEWS) &&
		[VIEWS.WORKFLOW, VIEWS.NEW_WORKFLOW].includes(to.name as VIEWS) &&
		from.params.workflowId !== to.params.workflowId;

	const shouldSkipPrompt =
		toNodeViewTab === MAIN_HEADER_TABS.EXECUTIONS ||
		toNodeViewTab === MAIN_HEADER_TABS.EVALUATION ||
		from.name === VIEWS.TEMPLATE_IMPORT ||
		(toNodeViewTab === MAIN_HEADER_TABS.WORKFLOW && from.name === VIEWS.EXECUTION_DEBUG);

	if (shouldSkipPrompt && !isNavigatingBetweenWorkflows) {
		next();
		return;
	}

	await workflowSaving.promptSaveUnsavedWorkflowChanges(next, {
		async confirm() {
			if (from.name === VIEWS.NEW_WORKFLOW) {
				const savedWorkflowId = workflowId.value;
				await router.replace({
					name: VIEWS.WORKFLOW,
					params: { workflowId: savedWorkflowId },
				});
				await router.push(to);
				return false;
			}
			workflowsStore.setWorkflowId('');
			return true;
		},
	});
});

/**
 * Lifecycle
 */

onMounted(async () => {
	// Register callback for collaboration store to refresh canvas when workflow updates arrive
	collaborationStore.setRefreshCanvasCallback(async (workflow) => {
		await initializeWorkspace(workflow);
	});

	try {
		initializeRoute();

		// Once view is initialized, pick up all toast notifications
		// waiting in the store and display them
		toast.showNotificationForViews([VIEWS.WORKFLOW, VIEWS.NEW_WORKFLOW]);

		if (route.query.settings) {
			uiStore.openModal(WORKFLOW_SETTINGS_MODAL_KEY);
			void router.replace({ query: { settings: undefined } });
		}
	} finally {
		isLoading.value = false;

		void externalHooks.run('nodeView.mount').catch(() => {});

		// A delay here makes opening the NDV a bit less jarring
		setTimeout(() => {
			if (routeNodeId.value) {
				updateNodeRoute(routeNodeId.value);
			}
		}, 500);

		// Check for pending builder prompt from empty state experiment
		void handlePendingBuilderPrompt();
	}

	void usersStore.showPersonalizationSurvey();
	checkIfRouteIsAllowed();

	addSourceControlEventBindings();
	addWorkflowSavedEventBindings();
	addBeforeUnloadEventBindings();
	addImportEventBindings();
	addExecutionOpenedEventBindings();
	addCommandBarEventBindings();
	registerCustomActions();
	addUndoRedoEventBindings();
	showAddFirstStepIfEnabled();
});

onBeforeUnmount(() => {
	uiStore.closeModal(WORKFLOW_SETTINGS_MODAL_KEY);
	toast.clearAllStickyNotifications();
	workflowDocumentStore?.value?.setViewport(viewportTransform.value);

	removeSourceControlEventBindings();
	removeWorkflowSavedEventBindings();
	removeBeforeUnloadEventBindings();
	removeImportEventBindings();
	removeExecutionOpenedEventBindings();
	removeCommandBarEventBindings();
	unregisterCustomActions();
	removeUndoRedoEventBindings();
	canvasEventBus.off('saved:workflow', onSaveFromWithinExecutionDebug);
});
</script>

<template>
	<div :class="$style.wrapper">
		<WorkflowCanvas
			v-if="!isLoading"
			:id="workflowDocumentStore?.workflowId"
			ref="canvas"
			:fallback-nodes="fallbackNodes"
			:show-fallback-nodes="showFallbackNodes"
			:event-bus="canvasEventBus"
			:read-only="isCanvasReadOnly"
			:group-expansion-mode="groupExpansionMode"
			:can-execute="canExecuteOnCanvas"
			:executing="isWorkflowRunning"
			:key-bindings="keyBindingsEnabled"
			:suppress-interaction="experimentalNdvStore.isMapperOpen"
			:striped-background="stripedCanvasBackground"
			:hide-controls="hideCanvasControls"
			:initial-viewport="workflowDocumentStore?.viewport"
			@update:nodes:position="onUpdateNodesPosition"
			@update:node:position="onUpdateNodePosition"
			@update:node:activated="onSetNodeActivated"
			@update:node:deactivated="onSetNodeDeactivated"
			@update:node:selected="onSetNodeSelected"
			@update:node:enabled="onToggleNodeDisabled"
			@update:node:name="onOpenRenameNodeModal"
			@update:node:parameters="onUpdateNodeParameters"
			@update:node:inputs="onUpdateNodeInputs"
			@update:node:outputs="onUpdateNodeOutputs"
			@update:logs-open="logsStore.toggleOpen($event)"
			@update:logs:input-open="logsStore.toggleInputOpen"
			@update:logs:output-open="logsStore.toggleOutputOpen"
			@update:has-range-selection="canvasStore.setHasRangeSelection"
			@open:sub-workflow="onOpenSubWorkflow"
			@click:node="onClickNode"
			@click:node:add="onClickNodeAdd"
			@run:node="onRunWorkflowToNode"
			@copy:production:url="onCopyProductionUrl"
			@copy:test:url="onCopyTestUrl"
			@delete:node="onDeleteNode"
			@create:connection="onCreateConnection"
			@create:connection:cancelled="onCreateConnectionCancelled"
			@delete:connection="onDeleteConnection"
			@click:connection:add="onClickConnectionAdd"
			@click:pane="onClickPane"
			@create:node="onOpenNodeCreatorFromCanvas"
			@create:sticky="onCreateSticky"
			@delete:nodes="onDeleteNodes"
			@update:nodes:enabled="onToggleNodesDisabled"
			@update:nodes:pin="onPinNodes"
			@duplicate:nodes="onDuplicateNodes"
			@copy:nodes="onCopyNodes"
			@cut:nodes="onCutNodes"
			@replace:node="onClickReplaceNode"
			@run:workflow="runEntireWorkflow('main')"
			@save:workflow="onSaveWorkflow"
			@create:workflow="onCreateWorkflow"
			@viewport:change="onViewportChange"
			@selection:end="onSelectionEnd"
			@drag-and-drop="onDragAndDrop"
			@tidy-up="onTidyUp"
			@toggle:focus-panel="onToggleFocusPanel"
			@extract-workflow="onExtractWorkflow"
			@start-chat="onToggleChat"
		>
			<Suspense v-if="!isCanvasReadOnly">
				<LazySetupWorkflowCredentialsButton :class="$style.setupCredentialsButtonWrapper" />
			</Suspense>
			<EvaluationsCanvasInfoCard
				v-if="!isCanvasReadOnly"
				:class="$style.evaluationsCanvasInfoCardWrapper"
			/>
			<div v-if="!isCanvasReadOnly || canExecuteOnCanvas" :class="$style.executionButtons">
				<CanvasRunWorkflowButton
					v-if="isRunWorkflowButtonVisible"
					:waiting-for-webhook="isExecutionWaitingForWebhook"
					:disabled="isExecutionDisabled"
					:executing="isWorkflowRunning"
					:trigger-nodes="triggerNodes"
					:get-node-type="nodeTypesStore.getNodeType"
					:selected-trigger-node-name="workflowExecutionState.selectedTriggerNodeName"
					:embedded="isDemoRoute"
					@mouseenter="onRunWorkflowButtonMouseEnter"
					@mouseleave="onRunWorkflowButtonMouseLeave"
					@execute="runEntireWorkflow('main')"
					@select-trigger-node="workflowExecutionState.setSelectedTriggerNodeName"
				/>
				<template v-if="containsChatTriggerNodes">
					<CanvasChatButton
						v-if="isChatHubAvailable ? isChatHubPanelOpen : isLogsPanelOpen"
						variant="subtle"
						:label="i18n.baseText('chat.hide')"
						:class="$style.chatButton"
						@click="onToggleChat"
					/>
					<KeyboardShortcutTooltip
						v-else
						:label="i18n.baseText('chat.open')"
						:shortcut="{ keys: ['c'] }"
					>
						<CanvasChatButton
							:variant="isRunWorkflowButtonVisible ? 'subtle' : 'solid'"
							:label="i18n.baseText('chat.open')"
							:class="$style.chatButton"
							@click="onOpenChat"
						/>
					</KeyboardShortcutTooltip>
				</template>
				<CanvasStopCurrentExecutionButton
					v-if="isStopExecutionButtonVisible"
					:stopping="isStoppingExecution"
					:size="isRunButtonSplit ? 'xlarge' : 'large'"
					@click="onStopExecution"
				/>
				<CanvasStopWaitingForWebhookButton
					v-if="isStopWaitingForWebhookButtonVisible"
					:size="isRunButtonSplit ? 'xlarge' : 'large'"
					@click="onStopWaitingForWebhook"
				/>
			</div>

			<N8nCallout
				v-if="isReadOnlyEnvironment"
				theme="warning"
				icon="lock"
				:class="$style.readOnlyEnvironmentNotification"
			>
				{{ i18n.baseText('readOnlyEnv.cantEditOrRun') }}
			</N8nCallout>

			<N8nCanvasCollaborationPill
				v-if="collaborationStore.currentWriter && !collaborationStore.isCurrentTabWriter"
				:class="$style.canvasCenterPill"
				:is-another-tab="isWriterAnotherTab"
				:first-name="collaborationStore.currentWriter.user.firstName"
				:last-name="collaborationStore.currentWriter.user.lastName"
				@button-click="onAcquireEditingClick"
			/>

			<N8nCanvasThinkingPill
				v-if="builderStore.streaming && !builderStore.isHelpStreaming"
				:class="$style.canvasCenterPill"
				show-stop
				@stop="builderStore.abortStreaming"
			/>

			<Suspense>
				<LazyNodeCreation
					v-if="!isCanvasReadOnly"
					:create-node-active="nodeCreatorStore.isCreateNodeActive"
					:node-view-scale="viewportTransform.zoom"
					:focus-panel-active="focusPanelStore.focusPanelActive"
					@toggle-node-creator="onToggleNodeCreator"
					@add-nodes="onAddNodesAndConnections"
					@close="onNodeCreatorClose"
				/>
			</Suspense>
			<Suspense>
				<LazyNodeDetailsView
					v-if="!isNDVV2"
					:read-only="isCanvasReadOnly"
					:is-production-execution-preview="nodeHelpers.isProductionExecutionPreview.value"
					:renaming="false"
					@value-changed="onRenameNode($event.value as string)"
					@stop-execution="onStopExecution"
					@switch-selected-node="onSwitchActiveNode"
					@open-connection-node-creator="onOpenSelectiveNodeCreator"
				/>
			</Suspense>
			<Suspense>
				<LazyNodeDetailsViewV2
					v-if="isNDVV2"
					:read-only="isCanvasReadOnly"
					:is-production-execution-preview="nodeHelpers.isProductionExecutionPreview.value"
					@rename-node="onRenameNode"
					@stop-execution="onStopExecution"
					@switch-selected-node="onSwitchActiveNode"
					@open-connection-node-creator="onOpenSelectiveNodeCreator"
				/>
			</Suspense>
		</WorkflowCanvas>
		<FocusSidebar
			v-if="
				!isLoading && (experimentalNdvStore.isNdvInFocusPanelEnabled ? !isCanvasReadOnly : true)
			"
			:is-canvas-read-only="isCanvasReadOnly"
			@context-menu-action="onContextMenuAction"
		/>
	</div>
</template>

<style lang="scss" module>
@use '@n8n/design-system/css/common/var';

.wrapper {
	display: flex;
	width: 100%;
}

.executionButtons {
	position: absolute;
	display: flex;
	justify-content: center;
	align-items: center;
	gap: var(--spacing--xs);
	left: 50%;
	transform: translateX(-50%);
	bottom: var(--spacing--sm);
	width: auto;

	@include mixins.breakpoint('sm-only') {
		left: auto;
		right: var(--spacing--sm);
		transform: none;
	}

	.chatButton {
		align-self: stretch;
	}
}

.setupCredentialsButtonWrapper {
	position: absolute;
	left: var(--spacing--sm);
	top: var(--spacing--sm);
}

.evaluationsCanvasInfoCardWrapper {
	position: absolute;
	left: var(--spacing--lg);
	bottom: var(--spacing--lg);
	z-index: var.$index-popper;
}

.readOnlyEnvironmentNotification {
	position: absolute;
	bottom: 16px;
	left: 50%;
	transform: translateX(-50%);
}

.canvasCenterPill {
	position: absolute;
	left: 50%;
	top: 50%;
	transform: translate(-50%, -50%);
	z-index: 10;
}
</style>
