import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import {
	extractReferencesInNodeExpressions,
	EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE,
	NodeHelpers,
} from 'n8n-workflow';
import type {
	ExtractableSubgraphData,
	ExtractableErrorResult,
	IConnections,
	INode,
} from 'n8n-workflow';
import { useToast } from './useToast';
import { useRouter } from 'vue-router';
import { VIEWS, WORKFLOW_EXTRACTION_NAME_MODAL_KEY } from '@/app/constants';
import { useHistoryStore } from '@/app/stores/history.store';
import { UpdateNodeGroupCommand } from '@/app/models/history';
import { useCanvasOperations } from './useCanvasOperations';
import { useSelectionValidation } from './useSelectionValidation';

import type { AddedNode, INodeUi, IWorkflowDb } from '@/Interface';
import type { WorkflowDataCreate } from '@n8n/rest-api-client/api/workflows';
import { useI18n } from '@n8n/i18n';
import { PUSH_NODES_OFFSET } from '@/app/utils/nodeViewUtils';
import { useUIStore } from '@/app/stores/ui.store';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useTelemetry } from './useTelemetry';
import { checkExhaustive } from '@/app/utils/typeGuards';
import isEqual from 'lodash/isEqual';
import uniq from 'lodash/uniq';
import { v4 as uuidv4 } from 'uuid';
import { sanitizeConnections } from '../utils/workflowUtils';

const CANVAS_HISTORY_OPTIONS = {
	trackBulk: false,
	trackHistory: true,
};

export function useWorkflowExtraction() {
	const uiStore = useUIStore();
	const workflowsStore = useWorkflowsStore();
	const workflowDocumentStore = injectWorkflowDocumentStore();
	const nodeTypesStore = useNodeTypesStore();
	const toast = useToast();
	const router = useRouter();
	const historyStore = useHistoryStore();
	const canvasOperations = useCanvasOperations();
	const i18n = useI18n();
	const telemetry = useTelemetry();
	const { isSelectionExtractable } = useSelectionValidation();

	function showError(message: string) {
		toast.showMessage({
			type: 'error',
			message,
			title: i18n.baseText('workflowExtraction.error.failure'),
			duration: 15 * 1000,
		});
	}

	function extractableErrorResultToMessage(result: ExtractableErrorResult) {
		switch (result.errorCode) {
			case 'Input Edge To Non-Root Node':
				return i18n.baseText('workflowExtraction.error.selectionGraph.inputEdgeToNonRoot', {
					interpolate: { node: result.node },
				});
			case 'Output Edge From Non-Leaf Node':
				return i18n.baseText('workflowExtraction.error.selectionGraph.outputEdgeFromNonLeaf', {
					interpolate: { node: result.node },
				});
			case 'Multiple Input Nodes':
				return i18n.baseText('workflowExtraction.error.selectionGraph.multipleInputNodes', {
					interpolate: { nodes: [...result.nodes].map((x) => `'${x}'`).join(', ') },
				});
			case 'Multiple Output Nodes':
				return i18n.baseText('workflowExtraction.error.selectionGraph.multipleOutputNodes', {
					interpolate: { nodes: [...result.nodes].map((x) => `'${x}'`).join(', ') },
				});
			case 'No Continuous Path From Root To Leaf In Selection':
				return i18n.baseText(
					'workflowExtraction.error.selectionGraph.noContinuousPathFromRootToLeaf',
					{ interpolate: { start: result.start, end: result.end } },
				);
		}
	}

	function makeExecuteWorkflowNode(
		workflowId: string,
		name: string,
		position: [number, number],
		variables: Map<string, string>,
	): Omit<INode, 'id'> {
		return {
			parameters: {
				workflowId: {
					__rl: true,
					value: workflowId,
					mode: 'list',
				},
				workflowInputs: {
					mappingMode: 'defineBelow',
					value: Object.fromEntries(variables.entries().map(([k, v]) => [k, `={{ ${v} }}`])),
					matchingColumns: [...variables.keys()],
					schema: [
						...variables.keys().map((x) => ({
							id: x,
							displayName: x,
							required: false,
							defaultMatch: false,
							display: true,
							canBeUsedToMatch: true,
							removed: false,
							// omitted type implicitly uses our `any` type
						})),
					],
					attemptToConvertTypes: false,
					convertFieldsToString: true,
				},
				options: {},
			},
			type: 'n8n-nodes-base.executeWorkflow',
			typeVersion: 1.2,
			position,
			name,
		};
	}

	function makeSubworkflow(
		newWorkflowName: string,
		{ start, end }: ExtractableSubgraphData,
		nodes: INodeUi[],
		connections: IConnections,
		selectionVariables: Map<string, string>,
		selectionChildrenVariables: Map<string, string>,
		startNodeName: string,
		returnNodeName: string,
	): WorkflowDataCreate {
		const newConnections = Object.fromEntries(
			Object.entries(connections).filter(([k]) => nodes.some((x) => x.name === k)),
		);
		if (end) {
			// this is necessary because the new workflow may crash looking for the
			// nodes in these connections
			delete newConnections[end];
		}

		const startNodeTarget = nodes.find((x) => x.name === start);
		const firstNode = startNodeTarget ?? nodes.sort((a, b) => a.position[1] - b.position[1])[0];
		const startNodePosition: [number, number] = [
			firstNode.position[0] - PUSH_NODES_OFFSET,
			firstNode.position[1],
		];

		const endNodeTarget = nodes.find((x) => x.name === end);
		const lastNode = endNodeTarget ?? nodes.sort((a, b) => b.position[1] - a.position[1])[0];
		const endNodePosition: [number, number] = [
			lastNode.position[0] + PUSH_NODES_OFFSET,
			lastNode.position[1],
		];

		const shouldInsertReturnNode = selectionChildrenVariables.size > 0;

		// Connect Start node to firstNode only if it accepts main input
		// Either because it's an explicit start target, or it accepts main connections
		const firstNodeType = nodeTypesStore.getNodeType(firstNode.type, firstNode.typeVersion);
		const shouldConnectStart =
			startNodeTarget !== undefined ||
			(firstNodeType && NodeHelpers.nodeAcceptsInputType(firstNodeType, 'main'));

		const startNodeConnection = shouldConnectStart
			? ({
					[startNodeName]: {
						main: [
							[
								{
									node: firstNode.name,
									type: 'main',
									index: 0,
								},
							],
						],
					},
				} satisfies IConnections)
			: {};

		const endNodeConnection =
			endNodeTarget && shouldInsertReturnNode
				? ({
						[endNodeTarget.name]: {
							main: [
								[
									{
										node: returnNodeName,
										type: 'main',
										index: 0,
									},
								],
							],
						},
					} satisfies IConnections)
				: {};

		const returnNode = shouldInsertReturnNode
			? [
					{
						parameters: {
							assignments: {
								assignments: [
									...selectionChildrenVariables.entries().map((x) => ({
										id: uuidv4(),
										name: x[0],
										value: `={{ ${x[1]} }}`,
										type: 'string',
									})),
								],
							},
							options: {},
						},
						type: 'n8n-nodes-base.set',
						typeVersion: 3.4,
						position: endNodePosition,
						id: uuidv4(),
						name: returnNodeName,
					} satisfies INode,
				]
			: [];
		const triggerParameters =
			selectionVariables.size === 0
				? {
						inputSource: 'passthrough',
					}
				: {
						workflowInputs: {
							values: [...selectionVariables.keys().map((k) => ({ name: k, type: 'any' }))],
						},
					};

		const triggerNode: INode = {
			id: uuidv4(),
			typeVersion: 1.1,
			name: startNodeName,
			type: EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE,
			position: startNodePosition,
			parameters: triggerParameters,
		};

		const result: WorkflowDataCreate = {
			name: newWorkflowName,
			nodes: [...nodes, ...returnNode, triggerNode],
			connections: {
				...newConnections,
				...startNodeConnection,
				...endNodeConnection,
			},
			settings: { executionOrder: 'v1' },
			projectId: workflowDocumentStore.value.homeProject?.id,
			parentFolderId: workflowDocumentStore.value.parentFolder?.id ?? undefined,
		};
		result.connections = sanitizeConnections(
			result.connections,
			result.nodes?.map((x) => x.name),
		);
		return result;
	}

	function computeAveragePosition(nodes: INode[]): [number, number] {
		const summedUp = nodes.reduce(
			(acc, v) => [acc[0] + v.position[0], acc[1] + v.position[1], acc[2] + 1],
			[0, 0, 0],
		);
		return [summedUp[0] / summedUp[2], summedUp[1] / summedUp[2]];
	}

	async function tryCreateWorkflow(workflowData: WorkflowDataCreate): Promise<IWorkflowDb | null> {
		try {
			const createdWorkflow = await workflowsStore.createNewWorkflow(workflowData);

			try {
				await workflowsStore.publishWorkflow(createdWorkflow.id, {
					versionId: createdWorkflow.versionId,
				});
			} catch (activationError) {
				console.error('Failed to activate extracted sub-workflow:', activationError);
			}

			const { href } = router.resolve({
				name: VIEWS.WORKFLOW,
				params: {
					workflowId: createdWorkflow.id,
				},
			});

			toast.showMessage({
				title: i18n.baseText('workflowExtraction.success.title'),
				message: i18n.baseText('workflowExtraction.success.message', {
					interpolate: { url: href },
				}),
				type: 'success',
				duration: 10 * 1000,
			});
			return createdWorkflow;
		} catch (e) {
			toast.showError(e, i18n.baseText('workflowExtraction.error.subworkflowCreationFailed'));
			return null;
		}
	}

	async function replaceSelectionWithNode(
		executeWorkflowNodeData: AddedNode,
		startId: string | undefined,
		endId: string | undefined,
		selection: INode[],
		selectionChildNodes: INode[],
	) {
		historyStore.startRecordingUndo();

		uiStore.resetLastInteractedWith();

		const executeWorkflowNode = (
			await canvasOperations.addNodes([executeWorkflowNodeData], {
				...CANVAS_HISTORY_OPTIONS,
				forcePosition: true,
			})
		)[0];

		addReplacementNodeToSelectionGroup(
			selection.map((node) => node.id),
			executeWorkflowNode.id,
		);

		if (endId)
			canvasOperations.replaceNodeConnections(endId, executeWorkflowNode.id, {
				...CANVAS_HISTORY_OPTIONS,
				replaceInputs: false,
				validateNodeGroups: false,
			});

		if (startId)
			canvasOperations.replaceNodeConnections(startId, executeWorkflowNode.id, {
				...CANVAS_HISTORY_OPTIONS,
				replaceOutputs: false,
				validateNodeGroups: false,
			});

		canvasOperations.deleteNodes(
			selection.map((x) => x.id),
			CANVAS_HISTORY_OPTIONS,
		);

		for (const node of selectionChildNodes) {
			const currentNode = workflowDocumentStore.value.allNodes.find((x) => x.id === node.id);

			if (isEqual(node, currentNode)) continue;

			canvasOperations.replaceNodeParameters(
				node.id,
				{ ...currentNode?.parameters },
				{ ...node.parameters },
				CANVAS_HISTORY_OPTIONS,
			);
		}

		uiStore.markStateDirty();
		historyStore.stopRecordingUndo();
	}

	function addReplacementNodeToSelectionGroup(selectionIds: string[], replacementNodeId: string) {
		const affectedGroupIds = uniq(
			selectionIds
				.map((nodeId) => workflowDocumentStore.value.getGroupForNode(nodeId)?.id)
				.filter((id): id is string => id !== undefined),
		);

		if (affectedGroupIds.length !== 1) return;

		const groupId = affectedGroupIds[0];
		const groupBefore = workflowDocumentStore.value.getGroupById(groupId);
		workflowDocumentStore.value.addNodesToGroup(groupId, [replacementNodeId]);
		const groupAfter = workflowDocumentStore.value.getGroupById(groupId);
		if (groupBefore && groupAfter) {
			historyStore.pushCommandToUndo(
				new UpdateNodeGroupCommand(
					{ ...groupBefore, nodeIds: [...groupBefore.nodeIds] },
					{ ...groupAfter, nodeIds: [...groupAfter.nodeIds] },
					Date.now(),
				),
			);
		}
	}

	function tryExtractNodesIntoSubworkflow(nodeIds: string[]): boolean {
		const result = isSelectionExtractable(nodeIds);

		if (!result.valid) {
			switch (result.reason) {
				case 'trigger-selected':
					showError(
						i18n.baseText('workflowExtraction.error.triggerSelected', {
							interpolate: {
								nodes: result.triggers.map((name) => `'${name}'`).join(', '),
							},
						}),
					);
					break;
				case 'invalid-subgraph':
					showError(
						i18n.baseText('workflowExtraction.error.selectionGraph.listHeader', {
							interpolate: {
								body: result.errors
									.map(extractableErrorResultToMessage)
									.map((x) => `- ${x}`)
									.join('<br>'),
							},
						}),
					);
					break;
				case 'multiple-input-branches':
					showError(
						i18n.baseText('workflowExtraction.error.inputNodeHasMultipleInputBranches', {
							interpolate: { node: result.node },
						}),
					);
					break;
				case 'multiple-output-branches':
					showError(
						i18n.baseText('workflowExtraction.error.outputNodeHasMultipleOutputBranches', {
							interpolate: { node: result.node },
						}),
					);
					break;
				default:
					checkExhaustive(result);
			}
			return false;
		}

		uiStore.openModalWithData({
			name: WORKFLOW_EXTRACTION_NAME_MODAL_KEY,
			data: { subGraph: result.subGraph, selection: result.subGraphData },
		});
		return true;
	}

	async function doExtractNodesIntoSubworkflow(
		selection: ExtractableSubgraphData,
		subGraph: INodeUi[],
		newWorkflowName: string,
	) {
		const { start, end } = selection;

		const allNodeNames = workflowDocumentStore.value.allNodes.map((x) => x.name) ?? [];

		let startNodeName = 'Start';
		const subGraphNames = subGraph.map((x) => x.name);
		while (subGraphNames.includes(startNodeName)) startNodeName += '_1';

		let returnNodeName = 'Return';
		while (subGraphNames.includes(returnNodeName)) returnNodeName += '_1';

		const directAfterEndNodeNames = end
			? (workflowDocumentStore.value
					.getChildNodes(end, 'main', 1)
					.map((x) => workflowDocumentStore.value.getNodeByName(x)?.name)
					.filter((x) => x !== undefined) ?? [])
			: [];

		const allAfterEndNodes = end
			? (workflowDocumentStore.value
					.getChildNodes(end, 'ALL')
					.map((x) => workflowDocumentStore.value.getNodeByName(x) ?? null)
					.filter((x) => x !== null) ?? [])
			: [];

		const { nodes, variables } = extractReferencesInNodeExpressions(
			subGraph,
			allNodeNames,
			startNodeName,
			start ? [start] : undefined,
		);

		let executeWorkflowNodeName = `Call ${newWorkflowName}`;
		while (allNodeNames.includes(executeWorkflowNodeName)) executeWorkflowNodeName += '_1';

		const { nodes: afterNodes, variables: afterVariables } = extractReferencesInNodeExpressions(
			allAfterEndNodes,
			allAfterEndNodes
				.map((x) => x.name)
				.concat(subGraphNames), // this excludes nodes that will remain in the parent workflow
			executeWorkflowNodeName,
			directAfterEndNodeNames,
		);

		const workflowData = makeSubworkflow(
			newWorkflowName,
			selection,
			nodes,
			workflowDocumentStore.value?.connectionsBySourceNode,
			variables,
			afterVariables,
			startNodeName,
			returnNodeName,
		);
		const createdWorkflow = await tryCreateWorkflow(workflowData);
		if (createdWorkflow === null) return false;

		const executeWorkflowPosition = computeAveragePosition(subGraph);
		const executeWorkflowNode = makeExecuteWorkflowNode(
			createdWorkflow.id,
			executeWorkflowNodeName,
			executeWorkflowPosition,
			variables,
		);
		await replaceSelectionWithNode(
			executeWorkflowNode,
			subGraph.find((x) => x.name === start)?.id,
			subGraph.find((x) => x.name === end)?.id,
			subGraph,
			afterNodes,
		);

		return true;
	}

	function trackStartExtractWorkflow(nodeCount: number, success: boolean) {
		telemetry.track('User started nodes to sub-workflow extraction', {
			node_count: nodeCount,
			success,
		});
	}

	function trackExtractWorkflow(nodeCount: number, success: boolean) {
		telemetry.track('User extracted nodes to sub-workflow', {
			node_count: nodeCount,
			success,
		});
	}

	/**
	 * This mutates the current workflow and creates a new one.
	 * Intended to be called from @WorkflowExtractionNameModal spawned
	 * by @tryExtractNodesIntoSubworkflow
	 */
	async function extractNodesIntoSubworkflow(
		selection: ExtractableSubgraphData,
		subGraph: INodeUi[],
		newWorkflowName: string,
	) {
		const success = await doExtractNodesIntoSubworkflow(selection, subGraph, newWorkflowName);
		trackExtractWorkflow(subGraph.length, success);
	}

	/**
	 * This starts the extraction process by checking whether the selection is extractable
	 * and spawning a pop up asking for a sub-workflow name.
	 * If confirmed, the modal calls @extractNodesIntoSubworkflow to handle the actual mutation
	 *
	 * @param nodeIds the ids to be extracted from the current workflow into a sub-workflow
	 */
	function extractWorkflow(nodeIds: string[]) {
		if (nodeIds.length === 0) return;

		const success = tryExtractNodesIntoSubworkflow(nodeIds);
		trackStartExtractWorkflow(nodeIds.length, success);
	}

	return {
		extractWorkflow,
		tryExtractNodesIntoSubworkflow,
		extractNodesIntoSubworkflow,
	};
}
