/**
 * Pure functions to turn engine v2 artifacts (graph, step outputs, step context)
 * back into engine v1 artifacts (`INode`, `Workflow`, run data, node execute context).
 */

import { deriveLoops, isBatchStepConfig } from '@n8n/engine';
import type { GraphNode, StepSlots, WorkflowGraph } from '@n8n/engine';
import { ExecuteContext, UnrecognizedNodeTypeError } from 'n8n-core';
import type {
	IConnections,
	IExecuteData,
	INode,
	INodeType,
	INodeTypes,
	IRunData,
	ISourceData,
	ITaskDataConnections,
	WorkflowExecuteMode,
} from 'n8n-workflow';
import { createRunExecutionData, Workflow } from 'n8n-workflow';

import {
	MAIN_CONNECTION_TYPE,
	MANUAL_TRIGGER_TYPE,
	SPLIT_IN_BATCHES_TYPE,
	SPLIT_IN_BATCHES_TYPE_VERSION,
} from './constants';
import { isTriggerStepConfig, isV1NodeStepConfig } from './guards';
import { fromStepInputs } from './io';
import type { CreateExecuteContextParams, V1Execution, V1NodeStepConfig } from './types';
import { emptyRun, forwardEdgesByTarget, nodeNamesById, toSourceSlots } from './v1-run-data';

export function toV1Execution(
	graph: WorkflowGraph,
	outputsByNode: Record<string, Record<number, StepSlots>>,
	activeNodeId: string,
	activeIteration: number,
): V1Execution {
	return {
		nodes: toV1Nodes(graph),
		connections: toV1Connections(graph),
		runData: toV1RunData(graph, outputsByNode, activeNodeId, activeIteration),
	};
}

function toV1Nodes(graph: WorkflowGraph): INode[] {
	return graph.nodes.flatMap((graphNode): INode[] => {
		if (graphNode.type === 'trigger') {
			return [toV1TriggerNode(graphNode)];
		}

		if (graphNode.type === 'batch') {
			return [toV1BatchNode(graphNode)];
		}

		if (!isV1NodeStepConfig(graphNode.config)) return [];

		return [toV1Node(graphNode, graphNode.config)];
	});
}

function toV1Connections(graph: WorkflowGraph): IConnections {
	const namesById = nodeNamesById(graph);

	const connections: IConnections = {};
	for (const edge of graph.edges) {
		const fromName = namesById.get(edge.from);
		const toName = namesById.get(edge.to);
		if (fromName === undefined || toName === undefined) continue;

		const outputs = (connections[fromName] ??= { [MAIN_CONNECTION_TYPE]: [] })[
			MAIN_CONNECTION_TYPE
		];
		const outputIndex = edge.outputIndex ?? 0;
		while (outputs.length <= outputIndex) outputs.push([]);
		outputs[outputIndex]!.push({
			node: toName,
			type: MAIN_CONNECTION_TYPE,
			index: edge.inputIndex ?? 0,
		});
	}

	return connections;
}

function toV1RunData(
	graph: WorkflowGraph,
	outputsByNode: Record<string, Record<number, StepSlots>>,
	activeNodeId: string,
	activeIteration: number,
): IRunData {
	const namesById = nodeNamesById(graph);
	const sourcesByNodeId = toV1Sources(graph);
	const activeLoop = deriveLoops(graph).find((loop) => loop.memberIds.has(activeNodeId));

	const runData: IRunData = {};
	for (const [completedNodeId, outputsByIteration] of Object.entries(outputsByNode)) {
		const nodeName = namesById.get(completedNodeId);
		if (nodeName === undefined) continue;

		const iterations = Object.keys(outputsByIteration).map(Number);
		if (iterations.length === 0) continue;

		let lastShown: number;
		if (!activeLoop) {
			lastShown = Math.max(...iterations);
		} else if (activeLoop.memberIds.has(completedNodeId)) {
			lastShown = activeIteration;
		} else {
			lastShown = 0;
		}

		const source = sourcesByNodeId.get(completedNodeId) ?? [];

		// A pass the node was skipped on gets an empty run rather than a gap.
		runData[nodeName] = Array.from({ length: lastShown + 1 }, (_, iteration) => {
			const outputs = outputsByIteration[iteration];
			if (outputs === undefined) return emptyRun(iteration, source);

			return {
				startTime: 0,
				executionTime: 0,
				executionIndex: iteration,
				source,
				data: { [MAIN_CONNECTION_TYPE]: fromStepInputs(outputs) },
			};
		});
	}

	return runData;
}

/** The execute path reports one pass, so no source names a `previousNodeRun`. */
export function toV1Sources(graph: WorkflowGraph): Map<string, Array<ISourceData | null>> {
	const namesById = nodeNamesById(graph);

	const sourcesByNodeId = new Map<string, Array<ISourceData | null>>();
	for (const [nodeId, edges] of forwardEdgesByTarget(graph)) {
		sourcesByNodeId.set(nodeId, toSourceSlots(edges, namesById));
	}
	return sourcesByNodeId;
}

export function toV1Workflow(
	workflowId: string,
	node: INode,
	execution: V1Execution,
	nodeTypes: INodeTypes,
): Workflow {
	const siblingNodes = execution.nodes.filter((executionNode) => executionNode.id !== node.id);

	return new Workflow({
		id: workflowId,
		nodes: [node, ...siblingNodes],
		connections: execution.connections,
		active: false,
		nodeTypes: tolerantNodeTypes(nodeTypes),
	});
}

/** An older graph carries no config, and graphs are immutable, so a stub stands in. */
function toV1TriggerNode(graphNode: GraphNode): INode {
	const config = isTriggerStepConfig(graphNode.config) ? graphNode.config : undefined;

	return {
		id: graphNode.id,
		name: graphNode.name,
		type: config?.nodeType ?? MANUAL_TRIGGER_TYPE,
		typeVersion: config?.typeVersion ?? 1,
		position: [0, 0],
		parameters: config?.parameters ?? {},
	};
}

function toV1BatchNode(graphNode: GraphNode): INode {
	const batchSize = isBatchStepConfig(graphNode.config) ? graphNode.config.batchSize : undefined;

	return {
		id: graphNode.id,
		name: graphNode.name,
		type: SPLIT_IN_BATCHES_TYPE,
		typeVersion: SPLIT_IN_BATCHES_TYPE_VERSION,
		position: [0, 0],
		parameters: batchSize === undefined ? {} : { batchSize },
	};
}

export function toV1Node(graphNode: GraphNode, config: V1NodeStepConfig): INode {
	return {
		id: graphNode.id,
		name: graphNode.name,
		type: config.nodeType,
		typeVersion: config.typeVersion,
		position: [0, 0],
		parameters: config.parameters,
		continueOnFail: config.continueOnFail,
	};
}

/**
 * In v1, an uninstalled node type _fails the whole execution upfront_, at
 * `Workflow` construction. In v2, where each step builds its own `Workflow`,
 * preserving that failure would mean failing at every step of the execution.
 * Instead, for fault isolation, v2 maps this to `undefined`, which `Workflow`
 * tolerates by design (node kept, name and data visible to expressions, only
 * parameter defaults skipped), so only the missing node type step fails.
 */
function tolerantNodeTypes(nodeTypes: INodeTypes): INodeTypes {
	return {
		getByName: (type) => nodeTypes.getByName(type),
		getKnownTypes: () => nodeTypes.getKnownTypes(),
		getByNameAndVersion: (type, version) => {
			try {
				return nodeTypes.getByNameAndVersion(type, version);
			} catch (error) {
				if (error instanceof UnrecognizedNodeTypeError) return undefined as unknown as INodeType;
				throw error;
			}
		},
	};
}

export function toV1ExecuteContext({
	node,
	workflow,
	execution,
	source,
	additionalData,
	stepContext,
	itemsByConnection,
}: CreateExecuteContextParams): ExecuteContext {
	const firstInput = itemsByConnection[0] ?? [];
	const connectionInputData = firstInput.length > 0 ? firstInput : [{ json: {} }];
	const inputData: ITaskDataConnections = {
		main: [connectionInputData, ...itemsByConnection.slice(1)],
	};

	const runExecutionData = createRunExecutionData({
		startData: {},
		resultData: { runData: execution.runData },
		executionData: {
			contextData: {},
			nodeExecutionStack: [],
			metadata: {},
			waitingExecution: {},
			waitingExecutionSource: null,
		},
	});

	const executeData: IExecuteData = {
		node,
		data: inputData,
		source: source.length > 0 ? { main: source } : null,
	};
	const mode: WorkflowExecuteMode = stepContext.mode === 'production' ? 'trigger' : 'manual';

	return new ExecuteContext(
		workflow,
		node,
		additionalData,
		mode,
		runExecutionData,
		stepContext.iteration,
		connectionInputData,
		inputData,
		executeData,
		[],
	);
}
