<script setup lang="ts">
import {
	computed,
	defineAsyncComponent,
	nextTick,
	onBeforeMount,
	onBeforeUnmount,
	onMounted,
	ref,
	useCssModule,
} from 'vue';
import { useRoute, useRouter } from 'vue-router';
import WorkflowCanvas from '@/components/canvas/WorkflowCanvas.vue';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useUIStore } from '@/stores/ui.store';
import CanvasRunWorkflowButton from '@/components/canvas/elements/buttons/CanvasRunWorkflowButton.vue';
import { useI18n } from '@/composables/useI18n';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useRunWorkflow } from '@/composables/useRunWorkflow';
import type {
	AddedNodesAndConnections,
	IExecutionResponse,
	INodeUi,
	IUpdateInformation,
	IWorkflowDataUpdate,
	IWorkflowDb,
	IWorkflowTemplate,
	NodeCreatorOpenSource,
	ToggleNodeCreatorOptions,
	XYPosition,
} from '@/Interface';
import type { Connection, ViewportTransform } from '@vue-flow/core';
import type {
	CanvasConnectionCreateData,
	CanvasNode,
	CanvasNodeMoveEvent,
	ConnectStartEvent,
} from '@/types';
import { CanvasNodeRenderType } from '@/types';
import {
	CHAT_TRIGGER_NODE_TYPE,
	EnterpriseEditionFeature,
	MAIN_HEADER_TABS,
	MANUAL_CHAT_TRIGGER_NODE_TYPE,
	MODAL_CONFIRM,
	NODE_CREATOR_OPEN_SOURCES,
	START_NODE_TYPE,
	STICKY_NODE_TYPE,
	VALID_WORKFLOW_IMPORT_URL_REGEX,
	VIEWS,
	WORKFLOW_LM_CHAT_MODAL_KEY,
} from '@/constants';
import { useSourceControlStore } from '@/stores/sourceControl.store';
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
import { useExternalHooks } from '@/composables/useExternalHooks';
import { TelemetryHelpers, NodeConnectionType, jsonParse } from 'n8n-workflow';
import type { IDataObject, ExecutionSummary, IConnection, IWorkflowBase } from 'n8n-workflow';
import { useToast } from '@/composables/useToast';
import { useSettingsStore } from '@/stores/settings.store';
import { useCredentialsStore } from '@/stores/credentials.store';
import useEnvironmentsStore from '@/stores/environments.ee.store';
import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store';
import { useRootStore } from '@/stores/root.store';
import { historyBus } from '@/models/history';
import { useCanvasOperations } from '@/composables/useCanvasOperations';
import { useExecutionsStore } from '@/stores/executions.store';
import { useCanvasStore } from '@/stores/canvas.store';
import { useMessage } from '@/composables/useMessage';
import { useTitleChange } from '@/composables/useTitleChange';
import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import { useTelemetry } from '@/composables/useTelemetry';
import { useHistoryStore } from '@/stores/history.store';
import { useProjectsStore } from '@/stores/projects.store';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useExecutionDebugging } from '@/composables/useExecutionDebugging';
import { useUsersStore } from '@/stores/users.store';
import { sourceControlEventBus } from '@/event-bus/source-control';
import { useTagsStore } from '@/stores/tags.store';
import { usePushConnectionStore } from '@/stores/pushConnection.store';
import { useNDVStore } from '@/stores/ndv.store';
import { getNodeViewTab } from '@/utils/canvasUtils';
import CanvasStopCurrentExecutionButton from '@/components/canvas/elements/buttons/CanvasStopCurrentExecutionButton.vue';
import CanvasStopWaitingForWebhookButton from '@/components/canvas/elements/buttons/CanvasStopWaitingForWebhookButton.vue';
import CanvasClearExecutionDataButton from '@/components/canvas/elements/buttons/CanvasClearExecutionDataButton.vue';
import { nodeViewEventBus } from '@/event-bus';
import * as NodeViewUtils from '@/utils/nodeViewUtils';
import { tryToParseNumber } from '@/utils/typesUtils';
import { useTemplatesStore } from '@/stores/templates.store';
import { createEventBus } from 'n8n-design-system';
import type { PinDataSource } from '@/composables/usePinnedData';
import { useClipboard } from '@/composables/useClipboard';

const LazyNodeCreation = defineAsyncComponent(
	async () => await import('@/components/Node/NodeCreation.vue'),
);

const LazyNodeDetailsView = defineAsyncComponent(
	async () => await import('@/components/NodeDetailsView.vue'),
);

const $style = useCssModule();
const router = useRouter();
const route = useRoute();
const i18n = useI18n();
const telemetry = useTelemetry();
const externalHooks = useExternalHooks();
const toast = useToast();
const message = useMessage();
const { titleReset, titleSet } = useTitleChange();
const workflowHelpers = useWorkflowHelpers({ router });
const nodeHelpers = useNodeHelpers();

const nodeTypesStore = useNodeTypesStore();
const uiStore = useUIStore();
const workflowsStore = useWorkflowsStore();
const sourceControlStore = useSourceControlStore();
const nodeCreatorStore = useNodeCreatorStore();
const settingsStore = useSettingsStore();
const credentialsStore = useCredentialsStore();
const environmentsStore = useEnvironmentsStore();
const externalSecretsStore = useExternalSecretsStore();
const rootStore = useRootStore();
const executionsStore = useExecutionsStore();
const canvasStore = useCanvasStore();
const npsSurveyStore = useNpsSurveyStore();
const historyStore = useHistoryStore();
const projectsStore = useProjectsStore();
const usersStore = useUsersStore();
const tagsStore = useTagsStore();
const pushConnectionStore = usePushConnectionStore();
const ndvStore = useNDVStore();
const templatesStore = useTemplatesStore();

const canvasEventBus = createEventBus();

const { runWorkflow, stopCurrentExecution, stopWaitingForWebhook } = useRunWorkflow({ router });
const {
	updateNodePosition,
	updateNodesPosition,
	revertUpdateNodePosition,
	renameNode,
	revertRenameNode,
	setNodeActive,
	setNodeSelected,
	toggleNodesDisabled,
	revertToggleNodeDisabled,
	toggleNodesPinned,
	setNodeParameters,
	deleteNode,
	deleteNodes,
	copyNodes,
	cutNodes,
	duplicateNodes,
	revertDeleteNode,
	addNodes,
	revertAddNode,
	createConnection,
	revertCreateConnection,
	deleteConnection,
	revertDeleteConnection,
	setNodeActiveByName,
	addConnections,
	importWorkflowData,
	fetchWorkflowDataFromUrl,
	resetWorkspace,
	initializeWorkspace,
	editableWorkflow,
	editableWorkflowObject,
	lastClickPosition,
} = useCanvasOperations({ router });
const { applyExecutionData } = useExecutionDebugging();
useClipboard({ onPaste: onClipboardPaste });

const isLoading = ref(true);
const isBlankRedirect = ref(false);
const readOnlyNotification = ref<null | { visible: boolean }>(null);

const isProductionExecutionPreview = ref(false);
const isExecutionPreview = ref(false);

const canOpenNDV = ref(true);
const hideNodeIssues = ref(false);

const workflowId = computed<string>(() => route.params.name as string);
const workflow = computed(() => workflowsStore.workflowsById[workflowId.value]);

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

const isCanvasReadOnly = computed(() => {
	return isDemoRoute.value || isReadOnlyEnvironment.value;
});

const fallbackNodes = computed<INodeUi[]>(() =>
	isLoading.value || isCanvasReadOnly.value
		? []
		: [
				{
					id: CanvasNodeRenderType.AddNodes,
					name: CanvasNodeRenderType.AddNodes,
					type: CanvasNodeRenderType.AddNodes,
					typeVersion: 1,
					position: [0, 0],
					parameters: {},
				},
			],
);

/**
 * Initialization
 */

async function initializeData() {
	const loadPromises = (() => {
		if (settingsStore.isPreviewMode && isDemoRoute.value) return [];

		const promises: Array<Promise<unknown>> = [
			workflowsStore.fetchActiveWorkflows(),
			credentialsStore.fetchAllCredentials(),
			credentialsStore.fetchCredentialTypes(true),
		];

		if (settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Variables]) {
			promises.push(environmentsStore.fetchAllVariables());
		}

		if (settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.ExternalSecrets]) {
			promises.push(externalSecretsStore.fetchAllSecrets());
		}

		if (nodeTypesStore.allNodeTypes.length === 0) {
			promises.push(nodeTypesStore.getNodeTypes());
		}

		return promises;
	})();

	try {
		await Promise.all(loadPromises);
	} catch (error) {
		toast.showError(
			error,
			i18n.baseText('nodeView.showError.mounted1.title'),
			i18n.baseText('nodeView.showError.mounted1.message') + ':',
		);
		return;
	}
}

async function initializeRoute() {
	// In case the workflow got saved we do not have to run init
	// as only the route changed but all the needed data is already loaded
	if (route.params.action === 'workflowSave') {
		uiStore.stateIsDirty = false;
		return;
	}

	// This function is called on route change as well, so we need to do the following:
	// - if the redirect is blank, then do nothing
	// - if the route is the template import view, then open the template
	// - if the user is leaving the current view without saving the changes, then show a confirmation modal
	if (isBlankRedirect.value) {
		isBlankRedirect.value = false;
	} else if (route.name === VIEWS.TEMPLATE_IMPORT) {
		const templateId = route.params.id;
		await openWorkflowTemplate(templateId.toString());
	} else {
		historyStore.reset();

		// If there is no workflow id, treat it as a new workflow
		if (!workflowId.value || isNewWorkflowRoute.value) {
			if (route.meta?.nodeView === true) {
				await initializeWorkspaceForNewWorkflow();
			}
			return;
		}

		await initializeWorkspaceForExistingWorkflow(workflowId.value);
	}

	nodeHelpers.updateNodesInputIssues();
	nodeHelpers.updateNodesCredentialsIssues();
	nodeHelpers.updateNodesParameterIssues();

	await loadCredentials();
}

async function initializeWorkspaceForNewWorkflow() {
	resetWorkspace();

	await workflowsStore.getNewWorkflowData(undefined, projectsStore.currentProjectId);
	workflowsStore.makeNewWorkflowShareable();

	uiStore.nodeViewInitialized = true;
}

async function initializeWorkspaceForExistingWorkflow(id: string) {
	resetWorkspace();

	try {
		const workflowData = await workflowsStore.fetchWorkflow(id);

		await openWorkflow(workflowData);
		await initializeDebugMode();

		if (workflowData.meta?.onboardingId) {
			trackOpenWorkflowFromOnboardingTemplate();
		}

		await projectsStore.setProjectNavActiveIdByWorkflowHomeProject(workflow.value.homeProject);
	} catch (error) {
		toast.showError(error, i18n.baseText('openWorkflow.workflowNotFoundError'));

		void router.push({
			name: VIEWS.NEW_WORKFLOW,
		});
	} finally {
		uiStore.nodeViewInitialized = true;
	}
}

/**
 * Workflow
 */

async function openWorkflow(data: IWorkflowDb) {
	resetWorkspace();
	titleSet(workflow.value.name, 'IDLE');

	await initializeWorkspace(data);

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

	// @TODO Check why this is needed when working on executions
	// const selectedExecution = executionsStore.activeExecution;
	// if (selectedExecution?.workflowId !== data.id) {
	// 	executionsStore.activeExecution = null;
	// 	workflowsStore.currentWorkflowExecutions = [];
	// } else {
	// 	executionsStore.activeExecution = selectedExecution;
	// }

	fitView();
}

function trackOpenWorkflowFromOnboardingTemplate() {
	telemetry.track(
		`User opened workflow from onboarding template with ID ${workflow.value.meta?.onboardingId}`,
		{
			workflow_id: workflowId.value,
		},
		{
			withPostHog: true,
		},
	);
}

/**
 * Templates
 */

async function openWorkflowTemplate(templateId: string) {
	resetWorkspace();

	canvasStore.startLoading();
	canvasStore.setLoadingText(i18n.baseText('nodeView.loadingTemplate'));

	workflowsStore.currentWorkflowExecutions = [];
	executionsStore.activeExecution = null;

	let data: IWorkflowTemplate | undefined;
	try {
		void externalHooks.run('template.requested', { templateId });

		data = await templatesStore.getFixedWorkflowTemplate(templateId);
		if (!data) {
			throw new Error(
				i18n.baseText('nodeView.workflowTemplateWithIdCouldNotBeFound', {
					interpolate: { templateId },
				}),
			);
		}
	} catch (error) {
		toast.showError(error, i18n.baseText('nodeView.couldntImportWorkflow'));
		await router.replace({ name: VIEWS.NEW_WORKFLOW });
		return;
	}

	trackOpenWorkflowTemplate(templateId);

	isBlankRedirect.value = true;
	await router.replace({ name: VIEWS.NEW_WORKFLOW, query: { templateId } });

	const convertedNodes = data.workflow.nodes.map(workflowsStore.convertTemplateNodeToNodeUi);

	workflowsStore.setConnections(data.workflow.connections);
	await addNodes(convertedNodes);
	await workflowsStore.getNewWorkflowData(data.name, projectsStore.currentProjectId);
	workflowsStore.addToWorkflowMetadata({ templateId });

	uiStore.stateIsDirty = true;

	canvasStore.stopLoading();

	void externalHooks.run('template.open', {
		templateId,
		templateName: data.name,
		workflow: data.workflow,
	});

	fitView();
}

function trackOpenWorkflowTemplate(templateId: string) {
	telemetry.track(
		'User inserted workflow template',
		{
			source: 'workflow',
			template_id: tryToParseNumber(templateId),
			wf_template_repo_session_id: templatesStore.previousSessionId,
		},
		{
			withPostHog: true,
		},
	);
}

/**
 * Nodes
 */

const triggerNodes = computed(() => {
	return editableWorkflow.value.nodes.filter(
		(node) => node.type === START_NODE_TYPE || 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;
});

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) {
	deleteNode(id, { trackHistory: true });
}

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

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

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 onSetNodeActive(id: string) {
	setNodeActive(id);
}

function onSetNodeSelected(id?: string) {
	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 (getNodeViewTab(route) !== MAIN_HEADER_TABS.WORKFLOW) {
		return;
	}

	if (!checkIfEditingIsAllowed()) {
		return;
	}

	let workflowData: IWorkflowDataUpdate | 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',
				),
				dangerouslyUseHTMLString: true,
			},
		);

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

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

	if (!workflowData) {
		return;
	}

	const result = await importWorkflowData(workflowData, 'paste', false);
	selectNodes(result.nodes?.map((node) => node.id) ?? []);
}

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);

	selectNodes(newIds);
}

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

	toggleNodesPinned(ids, source);
}

async function onSaveWorkflow() {
	const saved = await workflowHelpers.saveCurrentWorkflow();
	if (saved) {
		canvasEventBus.emit('saved:workflow');
	}
}

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

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

async function onSaveFromWithinNDV() {
	if (ndvStore.activeNodeName) {
		toast.showMessage({
			title: i18n.baseText('generic.workflowSaved'),
			type: 'success',
		});
	}
}

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

function onRenameNode(parameterData: IUpdateInformation) {
	if (parameterData.name === 'name' && parameterData.oldValue) {
		void renameNode(parameterData.oldValue as string, parameterData.value as string);
	}
}

async function onOpenRenameNodeModal(id: string) {
	const currentName = workflowsStore.getNodeById(id)?.name ?? '';
	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();

		const promptResponse = await promptResponsePromise;

		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);
}

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

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

/**
 * Credentials
 */

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

	if (workflow.value) {
		options = { workflowId: workflow.value.id };
	} 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) {
	setTimeout(() => {
		nodeCreatorStore.openNodeCreatorForConnectingNode({
			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 importWorkflowExact({ workflow: workflowData }: { workflow: IWorkflowDataUpdate }) {
	if (!workflowData.nodes || !workflowData.connections) {
		throw new Error('Invalid workflow object');
	}

	resetWorkspace();

	await initializeWorkspace({
		...workflowData,
		nodes: NodeViewUtils.getFixedNodesList<INodeUi>(workflowData.nodes),
	} as IWorkflowDb);

	fitView();
}

async function onImportWorkflowDataEvent(data: IDataObject) {
	const workflowData = data.data as IWorkflowDataUpdate;
	await importWorkflowData(workflowData, 'file');

	fitView();
	selectNodes(workflowData.nodes?.map((node) => node.id) ?? []);
}

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

	await importWorkflowData(workflowData, 'url');

	fitView();
	selectNodes(workflowData.nodes?.map((node) => node.id) ?? []);
}

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

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

/**
 * Node creator
 */

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

	await addNodes(nodes, { dragAndDrop, position, trackHistory: true });
	await nextTick();

	const offsetIndex = editableWorkflow.value.nodes.length - nodes.length;
	const mappedConnections: CanvasConnectionCreateData[] = connections.map(({ from, to }) => {
		const fromNode = editableWorkflow.value.nodes[offsetIndex + from.nodeIndex];
		const toNode = editableWorkflow.value.nodes[offsetIndex + to.nodeIndex];

		return {
			source: fromNode.id,
			target: toNode.id,
			data: {
				source: {
					index: from.outputIndex ?? 0,
					type: NodeConnectionType.Main,
				},
				target: {
					index: to.inputIndex ?? 0,
					type: NodeConnectionType.Main,
				},
			},
		};
	});

	addConnections(mappedConnections);

	void nextTick(() => {
		uiStore.resetLastInteractedWith();
	});
}

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

async function onSwitchActiveNode(nodeName: string) {
	setNodeActiveByName(nodeName);
}

async function onOpenSelectiveNodeCreator(node: string, connectionType: NodeConnectionType) {
	nodeCreatorStore.openSelectiveNodeCreator({ node, connectionType });
}

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

function onOpenNodeCreator(options: ToggleNodeCreatorOptions) {
	nodeCreatorStore.openNodeCreator(options);
}

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

function onClickConnectionAdd(connection: Connection) {
	nodeCreatorStore.openNodeCreatorForConnectingNode({
		connection,
		eventSource: NODE_CREATOR_OPEN_SOURCES.NODE_CONNECTION_ACTION,
	});
}

/**
 * Executions
 */

const isStoppingExecution = ref(false);

const isWorkflowRunning = computed(() => uiStore.isActionActive.workflowRunning);
const isExecutionWaitingForWebhook = computed(() => workflowsStore.executionWaitingForWebhook);

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

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

const isStopExecutionButtonVisible = computed(
	() => isWorkflowRunning.value && !isExecutionWaitingForWebhook.value,
);
const isStopWaitingForWebhookButtonVisible = computed(
	() => isWorkflowRunning.value && isExecutionWaitingForWebhook.value,
);
const isClearExecutionButtonVisible = computed(
	() =>
		!isReadOnlyRoute.value &&
		!isReadOnlyEnvironment.value &&
		!isWorkflowRunning.value &&
		!allTriggerNodesDisabled.value &&
		workflowExecutionData.value,
);

const workflowExecutionData = computed(() => workflowsStore.workflowExecutionData);

async function onRunWorkflow() {
	trackRunWorkflow();

	await runWorkflow({});
}

function trackRunWorkflow() {
	void workflowHelpers.getWorkflowDataToSave().then((workflowData) => {
		const telemetryPayload = {
			workflow_id: workflowId.value,
			node_graph_string: JSON.stringify(
				TelemetryHelpers.generateNodesGraph(
					workflowData as IWorkflowBase,
					workflowHelpers.getNodeTypes(),
					{ isCloudDeployment: settingsStore.isCloudDeployment },
				).nodeGraph,
			),
		};
		telemetry.track('User clicked execute workflow button', telemetryPayload);
		void externalHooks.run('nodeView.onRunWorkflow', telemetryPayload);
	});
}

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

	trackRunWorkflowToNode(node);
	await runWorkflow({ destinationNode: node.name, source: 'Node.executeNode' });
}

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

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

async function openExecution(executionId: string) {
	canvasStore.startLoading();
	resetWorkspace();

	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!`);
	}

	await initializeData();
	await initializeWorkspace(data.workflowData);
	workflowsStore.setWorkflowExecutionData(data);

	uiStore.stateIsDirty = false;
	canvasStore.stopLoading();

	fitView();

	canvasEventBus.emit('open:execution', data);

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

	telemetry.track('User opened read-only execution', {
		workflow_id: data.workflowData.id,
		execution_mode: data.mode,
		execution_finished: data.finished,
	});
}

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: `<a data-action="reload">${i18n.baseText('nodeView.refresh')}</a> ${i18n.baseText(
				'nodeView.toSeeTheLatestStatus',
			)}.<br/> <a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.wait/" target="_blank">${i18n.baseText(
				'nodeView.moreInfo',
			)}</a>`,
			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();
}

async function onClearExecutionData() {
	workflowsStore.workflowExecutionData = null;
	nodeHelpers.updateNodesExecutionIssues();
}

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

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

/**
 * Chat
 */

const chatTriggerNode = computed(() => {
	return editableWorkflow.value.nodes.find((node) => node.type === CHAT_TRIGGER_NODE_TYPE);
});

const containsChatTriggerNodes = computed(() => {
	return (
		!isExecutionWaitingForWebhook.value &&
		!!editableWorkflow.value.nodes.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 workflowsStore.pinDataByNodeName(chatTriggerNode.value.name);
});

async function onOpenChat() {
	uiStore.openModal(WORKFLOW_LM_CHAT_MODAL_KEY);

	const payload = {
		workflow_id: workflowId.value,
	};

	void externalHooks.run('nodeView.onOpenChat', payload);
	telemetry.track('User clicked chat open button', payload);
}

/**
 * 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('enableNodeToggle', onRevertToggleNodeDisabled);
}

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('enableNodeToggle', onRevertToggleNodeDisabled);
}

/**
 * Source control
 */

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

		if (workflowId.value !== null && !uiStore.stateIsDirty) {
			const workflowData = await workflowsStore.fetchWorkflow(workflowId.value);
			if (workflowData) {
				titleSet(workflowData.name, 'IDLE');
				await openWorkflow(workflowData);
			}
		}
	} catch (error) {
		console.error(error);
	}
}

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

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

/**
 * Post message events
 */

function addPostMessageEventBindings() {
	window.addEventListener('message', onPostMessageReceived);

	if (window.parent) {
		window.parent.postMessage(
			JSON.stringify({ command: 'n8nReady', version: rootStore.versionCli }),
			'*',
		);
	}
}

function removePostMessageEventBindings() {
	window.removeEventListener('message', onPostMessageReceived);
}

async function onPostMessageReceived(messageEvent: MessageEvent) {
	if (
		!messageEvent ||
		typeof messageEvent.data !== 'string' ||
		!messageEvent.data?.includes?.('"command"')
	) {
		return;
	}
	try {
		const json = JSON.parse(messageEvent.data);
		if (json && json.command === 'openWorkflow') {
			try {
				await importWorkflowExact(json);
				canOpenNDV.value = json.canOpenNDV ?? true;
				hideNodeIssues.value = json.hideNodeIssues ?? false;
				isExecutionPreview.value = false;
			} catch (e) {
				if (window.top) {
					window.top.postMessage(
						JSON.stringify({
							command: 'error',
							message: i18n.baseText('openWorkflow.workflowImportError'),
						}),
						'*',
					);
				}
				toast.showError(e, i18n.baseText('openWorkflow.workflowImportError'));
			}
		} else if (json && json.command === 'openExecution') {
			try {
				// If this NodeView is used in preview mode (in iframe) it will not have access to the main app store
				// so everything it needs has to be sent using post messages and passed down to child components
				isProductionExecutionPreview.value = json.executionMode !== 'manual';

				await openExecution(json.executionId);
				canOpenNDV.value = json.canOpenNDV ?? true;
				hideNodeIssues.value = json.hideNodeIssues ?? false;
				isExecutionPreview.value = true;
			} catch (e) {
				if (window.top) {
					window.top.postMessage(
						JSON.stringify({
							command: 'error',
							message: i18n.baseText('nodeView.showError.openExecution.title'),
						}),
						'*',
					);
				}
				toast.showMessage({
					title: i18n.baseText('nodeView.showError.openExecution.title'),
					message: (e as Error).message,
					type: 'error',
				});
			}
		} else if (json?.command === 'setActiveExecution') {
			executionsStore.activeExecution = (await executionsStore.fetchExecution(
				json.executionId,
			)) as ExecutionSummary;
		}
	} catch (e) {}
}

/**
 * Permission checks
 */

function checkIfEditingIsAllowed(): boolean {
	if (readOnlyNotification.value?.visible) {
		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',
			dangerouslyUseHTMLString: true,
		}) as unknown as { visible: boolean };

		return false;
	}

	return true;
}

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

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

/**
 * Debug mode
 */

async function initializeDebugMode() {
	if (route.name === VIEWS.EXECUTION_DEBUG) {
		titleSet(workflowsStore.workflowName, 'DEBUG');

		if (!workflowsStore.isInDebugMode) {
			await applyExecutionData(route.params.executionId as string);
			workflowsStore.isInDebugMode = true;
		}

		canvasEventBus.on('saved:workflow', onSaveFromWithinExecutionDebug);
	}
}

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

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

/**
 * Canvas
 */

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

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

function fitView() {
	setTimeout(() => canvasEventBus.emit('fitView'));
}

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

/**
 * Mouse events
 */

function onClickPane(position: CanvasNode['position']) {
	lastClickPosition.value = [position.x, position.y];
	canvasStore.newNodeInsertPosition = [position.x, position.y];
	uiStore.isCreateNodeActive = false;
}

/**
 * Custom Actions
 */

function registerCustomActions() {
	// @TODO Implement these
	// this.registerCustomAction({
	// 	key: 'openNodeDetail',
	// 	action: ({ node }: { node: string }) => {
	// 		this.nodeSelectedByName(node, true);
	// 	},
	// });
	//
	// this.registerCustomAction({
	// 	key: 'openSelectiveNodeCreator',
	// 	action: this.openSelectiveNodeCreator,
	// });
	//
	// this.registerCustomAction({
	// 	key: 'showNodeCreator',
	// 	action: () => {
	// 		this.ndvStore.activeNodeName = null;
	//
	// 		void this.$nextTick(() => {
	// 			this.showTriggerCreator(NODE_CREATOR_OPEN_SOURCES.TAB);
	// 		});
	// 	},
	// });
}

/**
 * Lifecycle
 */

onBeforeMount(() => {
	if (!isDemoRoute.value) {
		pushConnectionStore.pushConnect();
	}
});

onMounted(async () => {
	canvasStore.startLoading();
	titleReset();
	resetWorkspace();

	void initializeData().then(() => {
		void initializeRoute()
			.then(() => {
				// Once view is initialized, pick up all toast notifications
				// waiting in the store and display them
				toast.showNotificationForViews([VIEWS.WORKFLOW, VIEWS.NEW_WORKFLOW]);
			})
			.finally(() => {
				isLoading.value = false;
				canvasStore.stopLoading();
			});

		void usersStore.showPersonalizationSurvey();

		checkIfRouteIsAllowed();
	});

	addUndoRedoEventBindings();
	addPostMessageEventBindings();
	addSourceControlEventBindings();
	addImportEventBindings();
	addExecutionOpenedEventBindings();
	addWorkflowSavedEventBindings();

	registerCustomActions();

	// @TODO: This currently breaks since front-end hooks are still not updated to work with pinia store
	void externalHooks.run('nodeView.mount').catch(() => {});
});

onBeforeUnmount(() => {
	removeUndoRedoEventBindings();
	removePostMessageEventBindings();
	removeSourceControlEventBindings();
	removeImportEventBindings();
	removeExecutionOpenedEventBindings();
	removeWorkflowSavedEventBindings();
});
</script>

<template>
	<WorkflowCanvas
		v-if="editableWorkflow && editableWorkflowObject && !isLoading"
		:workflow="editableWorkflow"
		:workflow-object="editableWorkflowObject"
		:fallback-nodes="fallbackNodes"
		:event-bus="canvasEventBus"
		:read-only="isCanvasReadOnly"
		@update:nodes:position="onUpdateNodesPosition"
		@update:node:position="onUpdateNodePosition"
		@update:node:active="onSetNodeActive"
		@update:node:selected="onSetNodeSelected"
		@update:node:enabled="onToggleNodeDisabled"
		@update:node:name="onOpenRenameNodeModal"
		@update:node:parameters="onUpdateNodeParameters"
		@click:node:add="onClickNodeAdd"
		@run:node="onRunWorkflowToNode"
		@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"
		@run:workflow="onRunWorkflow"
		@save:workflow="onSaveWorkflow"
		@create:workflow="onCreateWorkflow"
		@viewport-change="onViewportChange"
	>
		<div :class="$style.executionButtons">
			<CanvasRunWorkflowButton
				:waiting-for-webhook="isExecutionWaitingForWebhook"
				:disabled="isExecutionDisabled"
				:executing="isWorkflowRunning"
				@mouseenter="onRunWorkflowButtonMouseEnter"
				@mouseleave="onRunWorkflowButtonMouseLeave"
				@click="onRunWorkflow"
			/>
			<CanvasChatButton v-if="containsChatTriggerNodes" @click="onOpenChat" />
			<CanvasStopCurrentExecutionButton
				v-if="isStopExecutionButtonVisible"
				:stopping="isStoppingExecution"
				@click="onStopExecution"
			/>
			<CanvasStopWaitingForWebhookButton
				v-if="isStopWaitingForWebhookButtonVisible"
				@click="onStopWaitingForWebhook"
			/>
			<CanvasClearExecutionDataButton
				v-if="isClearExecutionButtonVisible"
				@click="onClearExecutionData"
			/>
		</div>
		<Suspense>
			<LazyNodeCreation
				v-if="!isCanvasReadOnly"
				:create-node-active="uiStore.isCreateNodeActive"
				:node-view-scale="viewportTransform.zoom"
				@toggle-node-creator="onOpenNodeCreator"
				@add-nodes="onAddNodesAndConnections"
			/>
		</Suspense>
		<Suspense>
			<LazyNodeDetailsView
				:workflow-object="editableWorkflowObject"
				:read-only="isCanvasReadOnly"
				:is-production-execution-preview="isProductionExecutionPreview"
				:renaming="false"
				@value-changed="onRenameNode"
				@stop-execution="onStopExecution"
				@switch-selected-node="onSwitchActiveNode"
				@open-connection-node-creator="onOpenSelectiveNodeCreator"
				@save-keyboard-shortcut="onSaveWorkflow"
			/>
			<!--
				:renaming="renamingActive"
			-->
		</Suspense>
	</WorkflowCanvas>
</template>

<style lang="scss" module>
.executionButtons {
	position: absolute;
	display: flex;
	justify-content: center;
	align-items: center;
	left: 50%;
	transform: translateX(-50%);
	bottom: var(--spacing-l);
	width: auto;

	@media (max-width: $breakpoint-2xs) {
		bottom: 150px;
	}

	button {
		display: flex;
		justify-content: center;
		align-items: center;
		margin-left: 0.625rem;

		&:first-child {
			margin: 0;
		}
	}
}
</style>
