import type { IStartRunData, IWorkflowDb } from '@/Interface';
import type {
	IExecutionPushResponse,
	IExecutionResponse,
	IExecutionsStopData,
} from '@/features/execution/executions/executions.types';

import type {
	IRunData,
	ITaskData,
	IPinData,
	StartNodeData,
	INode,
	IDataObject,
	IWorkflowBase,
	IDestinationNode,
} from 'n8n-workflow';
import {
	createRunExecutionData,
	NodeConnectionTypes,
	TelemetryHelpers,
	BINARY_MODE_COMBINED,
} from 'n8n-workflow';
import { retry } from '@n8n/utils/retry';
import { computed, getCurrentInstance, type Ref } from 'vue';

import { useToast } from '@/app/composables/useToast';
import { useMessage } from '@/app/composables/useMessage';
import { useNodeHelpers } from '@/app/composables/useNodeHelpers';

import {
	CHAT_NODE_TYPE,
	CHAT_TOOL_NODE_TYPE,
	CHAT_HITL_TOOL_NODE_TYPE,
	CHAT_TRIGGER_NODE_TYPE,
	IN_PROGRESS_EXECUTION_ID,
	MODAL_CONFIRM,
	RESPOND_TO_WEBHOOK_NODE_TYPE,
} from '@/app/constants';

import { useRootStore } from '@n8n/stores/useRootStore';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useWorkflowExecutionStateStore } from '@/app/stores/workflowExecutionState.store';
import {
	injectWorkflowDocumentStore,
	type WorkflowDocumentStore,
} from '@/app/stores/workflowDocument.store';
import { displayForm } from '@/features/execution/executions/executions.utils';
import { useExternalHooks } from '@/app/composables/useExternalHooks';
import { useWorkflowHelpers } from '@/app/composables/useWorkflowHelpers';
import type { useRouter } from 'vue-router';
import { isEmpty } from '@/app/utils/typesUtils';
import { useI18n } from '@n8n/i18n';
import get from 'lodash/get';
import { useExecutionsStore } from '@/features/execution/executions/executions.store';
import { useTelemetry } from './useTelemetry';
import { useSettingsStore } from '@/app/stores/settings.store';
import { useUIStore } from '@/app/stores/ui.store';
import { usePushConnectionStore } from '@/app/stores/pushConnection.store';
import { useNodeDirtiness } from '@/app/composables/useNodeDirtiness';
import { useCanvasOperations } from './useCanvasOperations';
import { chatEventBus } from '@n8n/chat/event-buses';
import { useAgentRequestStore } from '@n8n/stores/useAgentRequestStore';
import { useWorkflowSaving } from './useWorkflowSaving';
import { useDocumentTitle } from './useDocumentTitle';
import { useEditorContext } from './useEditorContext';
import { useChat } from '@n8n/chat/composables';
import type { WorkflowObjectAccessors } from '../types';

export function useRunWorkflow(useRunWorkflowOpts: {
	router: ReturnType<typeof useRouter>;
	/**
	 * Binds this instance to a specific workflow document. Pass this from
	 * async, non-setup callers (e.g. push handlers) where `inject()` can't
	 * resolve the current document; otherwise it falls back to the injected
	 * document store for normal setup-context callers.
	 */
	workflowDocumentStore?: Readonly<Ref<WorkflowDocumentStore>>;
}) {
	const workflowHelpers = useWorkflowHelpers();
	const i18n = useI18n();
	const toast = useToast();
	const message = useMessage();
	const telemetry = useTelemetry();
	const externalHooks = useExternalHooks();
	const settingsStore = useSettingsStore();
	const uiStore = useUIStore();
	const agentRequestStore = useAgentRequestStore();

	const rootStore = useRootStore();
	const pushConnectionStore = usePushConnectionStore();
	const workflowsStore = useWorkflowsStore();
	const workflowDocumentStore =
		useRunWorkflowOpts.workflowDocumentStore ?? injectWorkflowDocumentStore();
	const workflowExecutionState = computed(() =>
		useWorkflowExecutionStateStore(workflowDocumentStore.value.documentId),
	);
	const nodeHelpers = useNodeHelpers();

	// Editor hosts (e.g. the Instance AI artifact preview) can suppress execution
	// error toasts for their embedded editor. Resolved at setup; `inject` can't
	// run in the async, non-setup callers (push handlers), so fall back to
	// showing toasts when there is no active component instance.
	const editorContext = getCurrentInstance() ? useEditorContext() : undefined;
	const workflowSaving = useWorkflowSaving({
		router: useRunWorkflowOpts.router,
	});
	const executionsStore = useExecutionsStore();
	const { dirtinessByName } = useNodeDirtiness(() => workflowDocumentStore.value.documentId);
	const { startChat } = useCanvasOperations();
	const chatStore = useChat();

	function sortNodesByYPosition(nodes: string[]) {
		return [...nodes].sort((a, b) => {
			const nodeA = workflowDocumentStore.value.getNodeByName(a)?.position ?? [0, 0];
			const nodeB = workflowDocumentStore.value.getNodeByName(b)?.position ?? [0, 0];

			const nodeAYPosition = nodeA[1];
			const nodeBYPosition = nodeB[1];

			if (nodeAYPosition === nodeBYPosition) return 0;

			return nodeAYPosition > nodeBYPosition ? 1 : -1;
		});
	}

	// Starts to execute a workflow on server
	async function runWorkflowApi(runData: IStartRunData): Promise<IExecutionPushResponse> {
		if (!pushConnectionStore.isConnected) {
			// Do not start if the connection to server is not active
			// because then it can not receive the data as it executes.
			throw new Error(i18n.baseText('workflowRun.noActiveConnectionToTheServer'));
		}

		// Set the execution as started, but still waiting for the execution to be retrieved
		workflowExecutionState.value.setActiveExecutionId(null);

		let response: IExecutionPushResponse;
		try {
			response = await workflowsStore.runWorkflow(runData);
		} catch (error) {
			workflowExecutionState.value.setActiveExecutionId(undefined);
			throw error;
		}

		const workflowExecutionIdIsNew =
			workflowExecutionState.value.previousExecutionId !== response.executionId;
		const workflowExecutionIdIsPending = workflowExecutionState.value.activeExecutionId === null;
		if (response.executionId && workflowExecutionIdIsNew && workflowExecutionIdIsPending) {
			workflowExecutionState.value.setActiveExecutionId(response.executionId);
		}

		if (response.waitingForWebhook === true) {
			workflowExecutionState.value.setExecutionWaitingForWebhook(true);
		}

		return response;
	}

	async function runWorkflow(options: {
		destinationNode?: IDestinationNode;
		triggerNode?: string;
		rerunTriggerNode?: boolean;
		nodeData?: ITaskData;
		source?: string;
		sessionId?: string;
	}): Promise<IExecutionPushResponse | undefined> {
		if (workflowExecutionState.value.activeExecutionId) {
			return;
		}

		toast.clearAllStickyNotifications();

		try {
			// Get the direct parents of the node
			let directParentNodes: string[] = [];
			if (options.destinationNode !== undefined) {
				directParentNodes = workflowDocumentStore.value.getParentNodes(
					options.destinationNode.nodeName,
					NodeConnectionTypes.Main,
					-1,
				);
			}

			const runData = workflowExecutionState.value.activeExecutionRunData;

			const isNewWorkflow = !workflowsStore.isWorkflowSaved[workflowDocumentStore.value.workflowId];

			// With N8N_WORKFLOWS_AUTOSAVE_DISABLED=true the editor no longer
			// force-saves before executing, so canvas-only edits would be
			// dropped by the executor (it only ever runs the DB copy). Prompt
			// the user to save first so the run reflects the canvas. ADO-5328.
			if (!isNewWorkflow && uiStore.stateIsDirty && !settingsStore.isAutosaveEnabled) {
				const response = await message.confirm(i18n.baseText('workflowRun.saveBeforeRun.message'), {
					title: i18n.baseText('workflowRun.saveBeforeRun.headline'),
					type: 'info',
					confirmButtonText: i18n.baseText('workflowRun.saveBeforeRun.confirmButtonText'),
					cancelButtonText: i18n.baseText('workflowRun.saveBeforeRun.cancelButtonText'),
					showClose: true,
				});

				if (response !== MODAL_CONFIRM) {
					return undefined;
				}

				const saved = await workflowSaving.saveCurrentWorkflow({
					id: workflowDocumentStore.value.workflowId,
				});
				if (!saved) {
					return undefined;
				}
			} else if (isNewWorkflow || (uiStore.stateIsDirty && settingsStore.isAutosaveEnabled)) {
				await workflowSaving.saveCurrentWorkflow({ id: workflowDocumentStore.value.workflowId });
			}

			const workflowData = workflowDocumentStore.value.serialize();

			if (
				rootStore.binaryDataMode === 'default' &&
				workflowData.settings?.binaryMode === BINARY_MODE_COMBINED
			) {
				toast.showMessage({
					title: i18n.baseText('workflowRun.showError.unsupportedExecutionLogic.title'),
					message: i18n.baseText('workflowRun.showError.unsupportedExecutionLogic.description'),
					type: 'error',
				});
				return undefined;
			}

			const consolidatedData = consolidateRunDataAndStartNodes(
				directParentNodes,
				runData,
				workflowData.pinData,
				workflowDocumentStore.value.getWorkflowObjectAccessorSnapshot(),
			);

			const { startNodeNames } = consolidatedData;
			const destinationNodeType = options.destinationNode
				? (workflowDocumentStore.value.getNodeByName(options.destinationNode.nodeName)?.type ?? '')
				: '';

			let executedNode: string | undefined;
			let triggerToStartFrom: IStartRunData['triggerToStartFrom'];
			if (
				startNodeNames.length === 0 &&
				directParentNodes.length === 0 &&
				'destinationNode' in options &&
				options.destinationNode !== undefined
			) {
				executedNode = options.destinationNode.nodeName;
				startNodeNames.push(options.destinationNode.nodeName);
			} else if (options.triggerNode && options.nodeData && !options.rerunTriggerNode) {
				// starts execution from downstream nodes of trigger node
				startNodeNames.push(
					...workflowDocumentStore.value.getChildNodes(
						options.triggerNode,
						NodeConnectionTypes.Main,
						1,
					),
				);
			} else if (options.destinationNode) {
				executedNode = options.destinationNode.nodeName;
			}

			if (options.triggerNode) {
				triggerToStartFrom = {
					name: options.triggerNode,
					data: options.nodeData,
				};
			}

			// If the destination node is specified, check if it is a chat trigger node or has a chat parent
			if (
				options.destinationNode &&
				(workflowDocumentStore.value.checkIfNodeHasChatParent(options.destinationNode.nodeName) ||
					workflowDocumentStore.value.checkIfToolNodeHasChatParent(
						options.destinationNode.nodeName,
					) ||
					destinationNodeType === CHAT_TRIGGER_NODE_TYPE) &&
				options.source !== 'RunData.ManualChatMessage' &&
				options.source !== 'RunData.ManualChatTrigger'
			) {
				const startNode = workflowDocumentStore.value.getStartNode(
					options.destinationNode.nodeName,
				);
				if (startNode && startNode.type === CHAT_TRIGGER_NODE_TYPE) {
					// Check if the chat node has input data or pin data
					const chatHasInputData =
						nodeHelpers.getNodeInputData(startNode, 0, 0, 'input')?.length > 0;
					const chatHasPinData = !!workflowData.pinData?.[startNode.name];

					// If the chat node has no input data or pin data, open the chat modal
					// and halt the execution
					if (!chatHasInputData && !chatHasPinData) {
						workflowExecutionState.value.setChatPartialExecutionDestinationNode(
							options.destinationNode.nodeName,
						);
						startChat();
						return;
					}
				}

				startChat();
			}

			const triggers = workflowData.nodes.filter(
				(node) => !node.disabled && node.type.toLowerCase().includes('trigger'),
			);
			const chatTriggerNode = triggers.find((node) => node.type === CHAT_TRIGGER_NODE_TYPE);
			const chatTriggerNodeOptions = chatTriggerNode?.parameters?.options as IDataObject;
			if (
				options.triggerNode === chatTriggerNode?.name &&
				chatTriggerNodeOptions?.responseMode === 'responseNodes'
			) {
				const responseNodes = workflowData.nodes.filter(
					(node) =>
						!node.disabled &&
						(node.type === CHAT_NODE_TYPE ||
							node.type === CHAT_TOOL_NODE_TYPE ||
							node.type === CHAT_HITL_TOOL_NODE_TYPE ||
							node.type === RESPOND_TO_WEBHOOK_NODE_TYPE),
				);
				if (!responseNodes?.length) {
					toast.showMessage({
						title: i18n.baseText('workflowRun.showWarning.noChatResponseNodes.title'),
						message: i18n.baseText('workflowRun.showWarning.noChatResponseNodes.description'),
						type: 'warning',
					});
				}
			}

			//if no destination node is specified
			//and execution is not triggered from chat
			//and there are other triggers in the workflow
			//disable chat trigger node to avoid modal opening and webhook creation
			if (
				!options.destinationNode &&
				options.source !== 'RunData.ManualChatMessage' &&
				chatTriggerNode
			) {
				const otherTriggers = triggers.filter((node) => node.type !== CHAT_TRIGGER_NODE_TYPE);
				if (otherTriggers.length) {
					chatTriggerNode.disabled = true;
				}
			}

			// partial executions must have a destination node
			const isPartialExecution = options.destinationNode !== undefined;

			// TODO: this will be redundant once we cleanup the partial execution v1
			const startNodes: StartNodeData[] = sortNodesByYPosition(startNodeNames)
				.map((name) => {
					// Find for each start node the source data
					let sourceData = get(runData, [name, 0, 'source', 0], null);
					if (sourceData === null) {
						const parentNodes = workflowDocumentStore.value.getParentNodes(
							name,
							NodeConnectionTypes.Main,
							1,
						);
						const executeData = workflowHelpers.executeData(
							workflowDocumentStore.value.connectionsBySourceNode,
							parentNodes,
							name,
							NodeConnectionTypes.Main,
							0,
						);
						sourceData = get(executeData, ['source', NodeConnectionTypes.Main, 0], null);
					}
					return {
						name,
						sourceData,
					};
				})
				// If a destination node is specified and it has chat parent, we don't want to include it in the start nodes
				.filter((node) => {
					if (
						options.destinationNode &&
						workflowDocumentStore.value.checkIfNodeHasChatParent(options.destinationNode.nodeName)
					) {
						return node.name !== options.destinationNode.nodeName;
					}
					return true;
				});

			const startRunData: IStartRunData = {
				workflowId: workflowData.id!,
				runData: isPartialExecution
					? (runData ?? undefined) // For partial execution, backend decides what run data to use and what to ignore.
					: undefined, // if it's a full execution we don't want to send any run data
				startNodes,
				triggerToStartFrom,
				chatSessionId: options.sessionId,
			};

			if ('destinationNode' in options) {
				startRunData.destinationNode = options.destinationNode;
				const nodeId = workflowDocumentStore.value.getNodeByName(
					options.destinationNode?.nodeName ?? '',
				)?.id;
				if (workflowDocumentStore.value.workflowId && nodeId) {
					const agentRequest = agentRequestStore.getAgentRequest(
						workflowDocumentStore.value.workflowId,
						nodeId,
					);

					if (agentRequest) {
						startRunData.agentRequest = {
							query: agentRequest.query ?? {},
							tool: {
								name: agentRequest.toolName ?? '',
							},
						};
					}
				}
			}

			if (startRunData.runData) {
				const nodeNames = Object.entries(dirtinessByName.value).flatMap(([nodeName, dirtiness]) =>
					dirtiness ? [nodeName] : [],
				);

				startRunData.dirtyNodeNames = nodeNames.length > 0 ? nodeNames : undefined;
			}

			// Init the execution data to represent the start of the execution
			// that data which gets reused is already set and data of newly executed
			// nodes can be added as it gets pushed in
			const executionData: IExecutionResponse = {
				id: IN_PROGRESS_EXECUTION_ID,
				finished: false,
				mode: 'manual',
				status: 'running',
				createdAt: new Date(),
				startedAt: new Date(),
				stoppedAt: undefined,
				workflowId: workflowDocumentStore.value.workflowId,
				executedNode,
				triggerNode: triggerToStartFrom?.name,
				data: createRunExecutionData({
					resultData: {
						runData: startRunData.runData ?? {},
						pinData: workflowData.pinData,
					},
				}),
				workflowData: {
					id: workflowDocumentStore.value.workflowId,
					name: workflowData.name!,
					active: workflowData.active!,
					createdAt: 0,
					updatedAt: 0,
					...workflowData,
				} as IWorkflowDb,
			};
			workflowExecutionState.value.setWorkflowExecutionData(executionData);
			nodeHelpers.updateNodesExecutionIssues();

			useDocumentTitle().setDocumentTitle(workflowDocumentStore.value.name, 'EXECUTING');
			const runWorkflowApiResponse = await runWorkflowApi(startRunData);

			if (
				!chatStore?.ws &&
				chatTriggerNodeOptions?.responseMode === 'responseNodes' &&
				options.source !== 'RunData.ManualChatMessage' &&
				options.source !== 'RunData.ManualChatTrigger' &&
				runWorkflowApiResponse.executionId
			) {
				chatEventBus.emit('setupWebsocket', runWorkflowApiResponse.executionId);
			}
			const pinData = workflowData.pinData ?? {};

			const getTestUrl = (() => {
				return (node: INode) => {
					const path =
						node.parameters.path ||
						(node.parameters.options as IDataObject)?.path ||
						node.webhookId;
					return `${rootStore.formTestUrl}/${path as string}`;
				};
			})();

			try {
				await displayForm({
					nodes: workflowData.nodes,
					runData: workflowExecutionState.value.activeExecution?.data?.resultData?.runData,
					destinationNode: options.destinationNode?.nodeName,
					triggerNode: options.triggerNode,
					pinData,
					directParentNodes,
					source: options.source,
					getTestUrl,
				});
			} catch (error) {}

			await externalHooks.run('workflowRun.runWorkflow', {
				nodeName: options.destinationNode?.nodeName,
				source: options.source,
			});

			return runWorkflowApiResponse;
		} catch (error) {
			console.error(error);
			workflowExecutionState.value.setWorkflowExecutionData(null);
			useDocumentTitle().setDocumentTitle(workflowDocumentStore.value.name, 'ERROR');
			if (editorContext?.executionErrorToasts.value !== false) {
				toast.showError(error, i18n.baseText('workflowRun.showError.title'));
			}
			return undefined;
		}
	}

	function consolidateRunDataAndStartNodes(
		directParentNodes: string[],
		runData: IRunData | null,
		pinData: IPinData | undefined,
		workflow: WorkflowObjectAccessors,
	): { runData: IRunData | undefined; startNodeNames: string[] } {
		const startNodeNames = new Set<string>();
		let newRunData: IRunData | undefined;

		if (runData !== null && Object.keys(runData).length !== 0) {
			newRunData = {};
			// Go over the direct parents of the node
			for (const directParentNode of directParentNodes) {
				// Go over the parents of that node so that we can get a start
				// node for each of the branches
				const parentNodes = workflow.getParentNodes(directParentNode, NodeConnectionTypes.Main);

				// Add also the enabled direct parent to be checked
				if (workflow.getNode(directParentNode)?.disabled) continue;

				parentNodes.push(directParentNode);

				for (const parentNode of parentNodes) {
					// We want to execute nodes that don't have run data neither pin data
					// in addition, if a node failed we want to execute it again
					if (
						(!runData[parentNode]?.length && !pinData?.[parentNode]?.length) ||
						runData[parentNode]?.[0]?.error !== undefined
					) {
						// When we hit a node which has no data we stop and set it
						// as a start node the execution from and then go on with other
						// direct input nodes
						startNodeNames.add(parentNode);
						break;
					}
					if (runData[parentNode] && !runData[parentNode]?.[0]?.error) {
						newRunData[parentNode] = runData[parentNode]?.slice(0, 1);
					}
				}
			}

			if (isEmpty(newRunData)) {
				// If there is no data for any of the parent nodes make sure
				// that run data is empty that it runs regularly
				newRunData = undefined;
			}
		}

		return { runData: newRunData, startNodeNames: [...startNodeNames] };
	}

	async function stopCurrentExecution() {
		const executionId = workflowExecutionState.value.activeExecutionId;
		let stopData: IExecutionsStopData | undefined;

		if (!executionId) {
			return;
		}

		try {
			stopData = await executionsStore.stopCurrentExecution(executionId);
		} catch (error) {
			// Execution stop might fail when the execution has already finished. Let's treat this here.
			const execution = await workflowsStore.getExecution(executionId);

			if (execution === undefined) {
				// execution finished but was not saved (e.g. due to low connectivity)
				toast.showMessage({
					title: i18n.baseText('nodeView.showMessage.stopExecutionCatch.unsaved.title'),
					message: i18n.baseText('nodeView.showMessage.stopExecutionCatch.unsaved.message'),
					type: 'success',
				});
			} else if (execution?.finished) {
				// execution finished before it could be stopped
				const executedData = {
					id: execution.id,
					data: execution.data,
					workflowData: workflowDocumentStore.value.getSnapshot(),
					finished: execution.finished,
					mode: execution.mode,
					startedAt: execution.startedAt,
					stoppedAt: execution.stoppedAt,
				} as IExecutionResponse;
				// Clear the active id so setWorkflowExecutionData's else branch sets
				// displayedExecutionId to the freshly-fetched finished id.
				workflowExecutionState.value.setActiveExecutionId(undefined);
				workflowExecutionState.value.setWorkflowExecutionData(executedData);
				toast.showMessage({
					title: i18n.baseText('nodeView.showMessage.stopExecutionCatch.title'),
					message: i18n.baseText('nodeView.showMessage.stopExecutionCatch.message'),
					type: 'success',
				});
			} else {
				toast.showError(error, i18n.baseText('nodeView.showError.stopExecution.title'));
			}
		} finally {
			// Wait for websocket event to update the execution status to 'canceled'
			const markedAsStopped = await retry(
				async () => {
					const execution = await workflowsStore.getExecution(executionId);
					if (!['running', 'waiting'].includes(execution?.status as string)) {
						workflowExecutionState.value.markExecutionAsStopped(stopData);
						return true;
					}

					return false;
				},
				250,
				20,
			);

			if (!markedAsStopped) {
				workflowExecutionState.value.markExecutionAsStopped(stopData);
			}
		}
	}

	async function stopWaitingForWebhook() {
		try {
			await workflowsStore.removeTestWebhook(workflowDocumentStore.value.workflowId);
		} catch (error) {
			toast.showError(error, i18n.baseText('nodeView.showError.stopWaitingForWebhook.title'));
			return;
		}
	}

	async function runEntireWorkflow(source: 'node' | 'main', triggerNode?: string) {
		const workflowData = workflowDocumentStore.value.serialize();
		const telemetryPayload = {
			workflow_id: workflowDocumentStore.value.workflowId,
			node_graph_string: JSON.stringify(
				TelemetryHelpers.generateNodesGraph(
					workflowData as IWorkflowBase,
					workflowHelpers.getNodeTypes(),
					{ isCloudDeployment: settingsStore.isCloudDeployment },
				).nodeGraph,
			),
			button_type: source,
		};
		telemetry.track('User clicked execute workflow button', telemetryPayload);
		void externalHooks.run('nodeView.onRunWorkflow', telemetryPayload);

		let resolvedTriggerNode = triggerNode ?? workflowExecutionState.value.selectedTriggerNodeName;

		// When no trigger is explicitly selected (e.g. chat trigger is the only trigger
		// and the Run button doesn't offer it for selection), resolve it from the workflow.
		if (!resolvedTriggerNode) {
			const triggers = workflowDocumentStore.value.allNodes.filter(
				(node) => !node.disabled && node.type.toLowerCase().includes('trigger'),
			);
			if (triggers.length === 1) {
				resolvedTriggerNode = triggers[0].name;
			}
		}

		void runWorkflow({
			triggerNode: resolvedTriggerNode,
		});
	}

	return {
		consolidateRunDataAndStartNodes,
		runEntireWorkflow,
		runWorkflow,
		runWorkflowApi,
		stopCurrentExecution,
		stopWaitingForWebhook,
		sortNodesByYPosition,
	};
}
