import {
	EVAL_VENDOR_SDK_INTERCEPTION_FLAG,
	type InstanceAiEvalExecutionRequest,
	type InstanceAiEvalNodeResult,
	type InstanceAiEvalExecutionResult,
} from '@n8n/api-types';
import { Logger } from '@n8n/backend-common';
import type { User } from '@n8n/db';
import { Service } from '@n8n/di';
import type { WorkflowJSON } from '@n8n/workflow-sdk';
import { normalizePinData } from '@n8n/workflow-sdk';
import {
	BinaryDataService,
	type EvalLlmMockHandler,
	type EvalMockHttpResponse,
	ExecutionLifecycleHooks,
	WorkflowExecute,
	synthesizeBinaryFixture,
} from 'n8n-core';
import {
	type IBinaryData,
	type IBinaryKeyData,
	type IDataObject,
	type IHttpRequestOptions,
	type INode,
	type INodeExecutionData,
	type IPinData,
	type IRun,
	type IRunExecutionData,
	type IWorkflowBase,
	type IWorkflowExecuteAdditionalData,
	createRunExecutionData,
	NodeHelpers,
	UserError,
	Workflow,
} from 'n8n-workflow';
import { randomUUID } from 'node:crypto';

import { NodeTypes } from '@/node-types';
import { PostHogClient } from '@/posthog';
import { getBase } from '@/workflow-execute-additional-data';
import { WorkflowFinderService } from '@/workflows/workflow-finder.service';

import { EvalMockedCredentialsHelper } from './eval-mocked-credentials-helper';
import { type InterceptedTurn, LlmWireServer } from './llm-wire-server';
import { createLlmMockHandler } from './mock-handler';
import { generatePinData } from './pin-data-generator';
import { patchNoProxyForLoopback } from './proxy-loopback';
import {
	buildVendorLlmRouting,
	detectBinaryDependencies,
	generateMockHints,
	identifyNodesForHints,
	identifyNodesForPinData,
	type MockHints,
	partitionAiRoots,
	type TriggerBinaryRequirement,
	type VendorLlmRouting,
} from './workflow-analysis';

// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------

/** Max output items per node kept in the artifact. The full count lives in `outputCount`. */
const MAX_OUTPUT_ITEMS_PER_NODE = 10;

// ---------------------------------------------------------------------------
// Service
// ---------------------------------------------------------------------------

// Executes workflows with LLM-based HTTP mocking. Phase 1 generates per-node
// mock hints (one LLM call); Phase 2 runs the workflow with a per-execution
// mock handler — additionalData is fresh, no global state mutated.
@Service()
export class EvalExecutionService {
	constructor(
		private readonly workflowFinderService: WorkflowFinderService,
		private readonly nodeTypes: NodeTypes,
		private readonly logger: Logger,
		private readonly postHogClient: PostHogClient,
		private readonly binaryDataService: BinaryDataService,
	) {}

	async executeWithLlmMock(
		workflowId: string,
		user: User,
		options: InstanceAiEvalExecutionRequest = {},
	): Promise<InstanceAiEvalExecutionResult> {
		const executionId = randomUUID();

		const workflowEntity = await this.workflowFinderService.findWorkflowForUser(workflowId, user, [
			'workflow:execute',
		]);

		if (!workflowEntity) {
			return this.errorResult(executionId, `Workflow ${workflowId} not found or not accessible`);
		}

		// Partition AI roots into "intercept via wire server" vs "leave pinned".
		// Default-on: every root with compatible sub-nodes gets intercepted;
		// callers can opt specific roots out via `pinNodes` (e.g. for A/B
		// comparison). Roots whose sub-nodes are incompatible auto-pin.
		let partitioned: ReturnType<typeof partitionAiRoots>;
		try {
			partitioned = partitionAiRoots(workflowEntity, options.pinNodes ?? []);
		} catch (error) {
			if (error instanceof UserError) {
				return this.errorResult(executionId, error.message);
			}
			throw error;
		}

		for (const entry of partitioned.autoPinned) {
			this.logger.debug(
				`[EvalMock] Auto-pinning AI root "${entry.root}" — sub-node "${entry.subNode}" (${entry.subNodeType}) is ${entry.reason}`,
			);
		}

		// Kill-switch: when interception is disabled, every root falls back to
		// the pinned path regardless of partition or explicit `pinNodes`.
		let interceptionEnabled = false;
		let unpinNodes = partitioned.unpinNodes;
		if (unpinNodes.length > 0) {
			interceptionEnabled = await this.isInterceptionEnabled(user);
			if (!interceptionEnabled) {
				this.logger.warn(
					'[EvalMock] Vendor SDK interception disabled by kill-switch — pinning all AI roots',
				);
				unpinNodes = [];
			}
		}

		const unpinSet = unpinNodes.length > 0 ? new Set(unpinNodes) : undefined;
		const hints = await this.analyzeWorkflow(workflowEntity, options.scenarioHints, unpinSet);
		const vendorLlmRouting = interceptionEnabled
			? buildVendorLlmRouting(workflowEntity, unpinNodes)
			: undefined;

		return await this.execute(
			workflowEntity,
			user,
			executionId,
			hints,
			options.scenarioHints,
			interceptionEnabled,
			vendorLlmRouting,
		);
	}

	// Default-on kill-switch: unset → enabled, explicit `false` → disabled, resolution error → disabled.
	private async isInterceptionEnabled(user: User): Promise<boolean> {
		try {
			const flags = await this.postHogClient.getFeatureFlags(user);
			return flags?.[EVAL_VENDOR_SDK_INTERCEPTION_FLAG] !== false;
		} catch (error) {
			this.logger.warn('[EvalMock] Failed to resolve vendor-SDK interception flag', {
				error: error instanceof Error ? error.message : String(error),
			});
			return false;
		}
	}

	// ── Phase 1: Workflow analysis ─────────────────────────────────────────

	private async analyzeWorkflow(
		workflowEntity: IWorkflowBase,
		scenarioHints?: string,
		unpinSet?: Set<string>,
	): Promise<MockHints> {
		// Phase 1: Generate mock hints for HTTP-interceptible nodes
		const hintNodes = identifyNodesForHints(workflowEntity);
		const nodeNames = hintNodes.map((n) => n.name);

		this.logger.debug(
			`[EvalMock] Generating hints for ${nodeNames.length} nodes: ${nodeNames.join(', ')}`,
		);

		const hints = await generateMockHints({
			workflow: workflowEntity,
			nodeNames,
			scenarioHints,
		});

		if (!hints.globalContext && nodeNames.length > 0) {
			this.logger.warn(
				'[EvalMock] Phase 1 hint generation returned empty — mock responses will lack cross-node consistency',
			);
		}

		this.logger.debug(
			`[EvalMock] Phase 1 result — globalContext: ${hints.globalContext ? 'present' : 'EMPTY'}, triggerContent keys: ${JSON.stringify(Object.keys(hints.triggerContent))}, nodeHints: ${Object.keys(hints.nodeHints).join(', ')}`,
		);

		// Phase 1.5: Generate pin data for nodes that bypass the HTTP mock layer
		const bypassNodes = identifyNodesForPinData(workflowEntity, unpinSet);
		const bypassNodeNames = bypassNodes.map((n) => n.name);

		if (bypassNodeNames.length > 0) {
			this.logger.debug(
				`[EvalMock] Generating pin data for ${bypassNodeNames.length} bypass nodes: ${bypassNodeNames.join(', ')}`,
			);
			hints.bypassPinData = await this.generateBypassPinData(
				workflowEntity,
				bypassNodeNames,
				hints.globalContext,
				scenarioHints,
			);
			this.logger.debug(
				`[EvalMock] Phase 1.5 result — pinned nodes: ${Object.keys(hints.bypassPinData).join(', ') || 'none'}`,
			);
		}

		return hints;
	}

	// ── Phase 1.5: Pin data for bypass nodes ─────────────────────────────

	/**
	 * Generate pin data for nodes that bypass the HTTP mock layer.
	 * Uses the existing LLM-based pin data generator with Phase 1's globalContext
	 * for cross-node data consistency.
	 */
	private async generateBypassPinData(
		workflowEntity: IWorkflowBase,
		bypassNodeNames: string[],
		globalContext: string,
		scenarioHints?: string,
	): Promise<IPinData> {
		if (bypassNodeNames.length === 0) return {};

		try {
			const dataDescription = [globalContext, scenarioHints].filter(Boolean).join('\n\n');
			const result = await generatePinData({
				workflow: workflowEntity as unknown as WorkflowJSON,
				nodeNames: bypassNodeNames,
				instructions: dataDescription ? { dataDescription } : undefined,
			});

			return normalizePinData(result as unknown as IPinData);
		} catch (error) {
			const errorMsg = error instanceof Error ? error.message : String(error);
			this.logger.error(`[EvalMock] Phase 1.5 pin data generation failed: ${errorMsg}`);
			return normalizePinData(
				Object.fromEntries(
					bypassNodeNames.map((nodeName) => [nodeName, [{ json: {} }]]),
				) as IPinData,
			);
		}
	}

	// ── Phase 2: Mock execution ────────────────────────────────────────────

	private async execute(
		workflowEntity: IWorkflowBase,
		user: User,
		executionId: string,
		hints: MockHints,
		scenarioHints?: string,
		interceptionEnabled = false,
		vendorLlmRouting?: VendorLlmRouting,
	): Promise<InstanceAiEvalExecutionResult> {
		const nodeResults: Record<string, InstanceAiEvalNodeResult> = {};

		const workflow = this.buildWorkflow(workflowEntity);
		const startNode = this.findStartNode(workflow);

		if (!startNode) {
			return this.errorResult(executionId, 'No trigger or start node found in the workflow');
		}

		const mockHandler = createLlmMockHandler({
			scenarioHints,
			globalContext: hints.globalContext,
			nodeHints: hints.nodeHints,
		});

		const additionalData = await getBase({
			userId: user.id,
			workflowId: workflowEntity.id,
			workflowSettings: workflowEntity.settings ?? {},
		});

		// try/finally wraps boot so a throw never leaks the server or NO_PROXY patch.
		let wireServer: LlmWireServer | undefined;
		let restoreNoProxy: (() => void) | undefined;
		let credentialsHelper: EvalMockedCredentialsHelper | undefined;
		try {
			let serverUrl: string | undefined;
			if (interceptionEnabled) {
				wireServer = new LlmWireServer({
					mockHandler,
					rootToSubNode: vendorLlmRouting?.rootToSubNode,
					onIntercept: (turn) => this.recordWireServerTurn(turn, nodeResults),
					logger: this.logger,
				});
				serverUrl = await wireServer.start();
				restoreNoProxy = patchNoProxyForLoopback();
				this.logger.debug(`[EvalMock] Wire server listening at ${serverUrl}`);
			}

			credentialsHelper = new EvalMockedCredentialsHelper(
				additionalData.credentialsHelper,
				serverUrl,
				this.logger,
				vendorLlmRouting?.subNodeToRoot,
			);
			additionalData.credentialsHelper = credentialsHelper;
			additionalData.evalLlmMockHandler = this.createInterceptingHandler(mockHandler, nodeResults);
			additionalData.hooks = new ExecutionLifecycleHooks('evaluation', executionId, workflowEntity);

			const binaryRequirement = detectBinaryDependencies(workflowEntity);
			const triggerPinData = this.buildTriggerPinData(
				startNode,
				hints.triggerContent,
				binaryRequirement,
			);
			const pinData: IPinData = { ...triggerPinData, ...hints.bypassPinData };
			const pinDataNodeNames = Object.keys(pinData);

			// Check config completeness before execution — detect missing required parameters
			this.checkNodeConfig(workflow, nodeResults, pinDataNodeNames);
			const executionData = this.buildExecutionData(startNode, pinData);

			// Mark the trigger node as pinned (it gets its output from pin data, not execution).
			if (Object.keys(triggerPinData).length > 0) {
				this.markNodeAsPinned(startNode.name, nodeResults);
			}
			for (const nodeName of Object.keys(hints.bypassPinData)) {
				this.markNodeAsPinned(nodeName, nodeResults);
			}

			const result = await this.runWorkflow(workflow, additionalData, executionData);
			return await this.buildResult(executionId, result, nodeResults, hints, credentialsHelper);
		} catch (error: unknown) {
			return this.buildPartialFailureResult(
				executionId,
				error,
				nodeResults,
				hints,
				credentialsHelper,
			);
		} finally {
			if (restoreNoProxy) restoreNoProxy();
			if (wireServer) {
				try {
					await wireServer.stop();
				} catch (error) {
					this.logger.warn('[EvalMock] Wire server teardown failed', {
						error: error instanceof Error ? error.message : String(error),
					});
				}
			}
		}
	}

	// ── Workflow construction ──────────────────────────────────────────────

	private buildWorkflow(workflowEntity: IWorkflowBase): Workflow {
		return new Workflow({
			id: workflowEntity.id,
			name: workflowEntity.name,
			nodes: workflowEntity.nodes,
			connections: workflowEntity.connections,
			active: false,
			nodeTypes: this.nodeTypes,
			staticData: workflowEntity.staticData,
			settings: workflowEntity.settings ?? {},
		});
	}

	/**
	 * Find the workflow's trigger/start node.
	 * Uses Workflow.getStartNode() first (handles trigger, poll, and STARTING_NODE_TYPES),
	 * then falls back to checking for webhook nodes which getStartNode() doesn't cover.
	 */
	private findStartNode(workflow: Workflow): INode | undefined {
		return workflow.getStartNode() ?? this.findWebhookNode(workflow);
	}

	private findWebhookNode(workflow: Workflow): INode | undefined {
		return Object.values(workflow.nodes).find((node) => {
			if (node.disabled) return false;
			const nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
			return nodeType !== undefined && 'webhook' in nodeType;
		});
	}

	/**
	 * Check each node for missing required parameters and record issues
	 * in nodeResults. This runs before execution so the report shows
	 * configuration completeness regardless of whether the node crashes.
	 */
	private checkNodeConfig(
		workflow: Workflow,
		nodeResults: Record<string, InstanceAiEvalNodeResult>,
		pinDataNodeNames: string[],
	): void {
		for (const node of Object.values(workflow.nodes)) {
			if (node.disabled) continue;
			const nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
			if (!nodeType) continue;

			const issues = NodeHelpers.getNodeParametersIssues(
				nodeType.description.properties,
				node,
				nodeType.description,
				pinDataNodeNames,
			);

			if (issues?.parameters && Object.keys(issues.parameters).length > 0) {
				const entry = (nodeResults[node.name] ??= {
					output: null,
					interceptedRequests: [],
					executionMode: 'real',
				});
				entry.configIssues = issues.parameters;
			}
		}
	}

	// ── Execution data ────────────────────────────────────────────────────

	/**
	 * Build pin data for the trigger/start node from LLM-generated content.
	 * Pin data provides the trigger's output — the node doesn't execute,
	 * since trigger nodes receive external events that don't fire in eval mode.
	 */
	private buildTriggerPinData(
		startNode: INode,
		triggerContent: Record<string, unknown>,
		binaryRequirement?: TriggerBinaryRequirement,
	): IPinData {
		const classifyBinaryFileType = (contentType: string): IBinaryData['fileType'] => {
			const lc = contentType.toLowerCase();
			if (lc.startsWith('image/')) return 'image';
			if (lc.startsWith('audio/')) return 'audio';
			if (lc.startsWith('video/')) return 'video';
			if (lc === 'application/pdf') return 'pdf';
			if (lc.startsWith('text/html')) return 'html';
			if (lc === 'application/json' || lc.startsWith('text/json')) return 'json';
			if (lc.startsWith('text/')) return 'text';
			return undefined;
		};

		if (Object.keys(triggerContent).length === 0 && !binaryRequirement) return {};

		const item: INodeExecutionData = { json: triggerContent as IDataObject };

		if (binaryRequirement) {
			const bytes = synthesizeBinaryFixture(
				binaryRequirement.contentType,
				binaryRequirement.filename,
			);
			const extension = binaryRequirement.filename.includes('.')
				? binaryRequirement.filename.slice(binaryRequirement.filename.lastIndexOf('.') + 1)
				: 'bin';
			const binary: IBinaryKeyData = {
				[binaryRequirement.propertyName]: {
					mimeType: binaryRequirement.contentType,
					fileName: binaryRequirement.filename,
					fileExtension: extension,
					fileType: classifyBinaryFileType(binaryRequirement.contentType),
					data: bytes.toString('base64'),
				},
			};
			item.binary = binary;
		}

		return { [startNode.name]: [item] };
	}

	/**
	 * Build execution data with the trigger node on the execution stack.
	 * We use processRunExecutionData() instead of run() because run() relies on
	 * getStartNode() which doesn't find webhook nodes (they define `webhook`,
	 * not `trigger`). This follows the same pattern as InstanceAiAdapterService.
	 * Pin data carries the trigger's output; the execution stack just marks where to start.
	 */
	private buildExecutionData(startNode: INode, pinData: IPinData): IRunExecutionData {
		return createRunExecutionData({
			startData: {},
			resultData: { pinData, runData: {} },
			executionData: {
				contextData: {},
				metadata: {},
				nodeExecutionStack: [
					{
						node: startNode,
						data: { main: [[{ json: {} }]] },
						source: null,
					},
				],
				waitingExecution: {},
				waitingExecutionSource: {},
			},
		});
	}

	private async runWorkflow(
		workflow: Workflow,
		additionalData: IWorkflowExecuteAdditionalData,
		executionData: IRunExecutionData,
	): Promise<IRun> {
		const workflowExecute = new WorkflowExecute(additionalData, 'evaluation', executionData);
		return await workflowExecute.processRunExecutionData(workflow);
	}

	// ── Request interception ─────────────────────────────────────────────

	/**
	 * Record a wire-server model turn against the AI root in `nodeResults`.
	 * Attribution mirrors `createInterceptingHandler` so vendor-SDK traffic
	 * and HTTP-helper traffic land in the same ledger shape — downstream
	 * consumers (eval UI, graders) don't need to special-case the two.
	 */
	private recordWireServerTurn(
		turn: InterceptedTurn,
		nodeResults: Record<string, InstanceAiEvalNodeResult>,
	): void {
		const entry = (nodeResults[turn.rootName] ??= {
			output: null,
			interceptedRequests: [],
			executionMode: 'mocked',
		});
		// Preserve a pre-set 'pinned' (bypass pass owns that classification);
		// otherwise the turn IS mocked, so upgrade from any other prior value
		// (e.g. 'real' from checkNodeConfig() pre-marking config-issue nodes).
		if (entry.executionMode !== 'pinned') {
			entry.executionMode = 'mocked';
		}
		entry.interceptedRequests.push({
			url: turn.url,
			method: turn.method,
			nodeType: turn.nodeType,
			requestBody: turn.requestBody,
			mockResponse: turn.mockResponse,
		});

		this.logger.debug(
			`[EvalMock] Wire server intercepted ${turn.method} ${turn.url} attributed to root "${turn.rootName}"`,
		);
	}

	/**
	 * Wraps the mock handler to collect intercepted request metadata for diagnostics.
	 */
	private createInterceptingHandler(
		mockHandler: EvalLlmMockHandler,
		nodeResults: Record<string, InstanceAiEvalNodeResult>,
	): EvalLlmMockHandler {
		return async (
			requestOptions: IHttpRequestOptions,
			node: INode,
		): Promise<EvalMockHttpResponse | undefined> => {
			// A node may make multiple HTTP requests — ensure it's marked as mocked.
			// checkNodeConfig may have pre-created the entry as 'real', so always override.
			const entry = (nodeResults[node.name] ??= {
				output: null,
				interceptedRequests: [],
				executionMode: 'mocked',
			});
			entry.executionMode = 'mocked';
			const response = await mockHandler(requestOptions, node);

			entry.interceptedRequests.push({
				url: requestOptions.url,
				method: requestOptions.method ?? 'GET',
				nodeType: node.type,
				requestBody: requestOptions.body,
				mockResponse: response?.body,
			});

			this.logger.debug(
				`[EvalMock] Intercepted ${requestOptions.method ?? 'GET'} ${requestOptions.url} from "${node.name}" (${node.type})`,
			);

			return response;
		};
	}

	/**
	 * Mark a node entry as pinned, preserving any config issues that
	 * `checkNodeConfig` may have already recorded on it. Used both for the
	 * trigger node (which receives its output from `triggerPinData`) and for
	 * each bypass node — the shape of the entry is identical, just the trigger
	 * is gated by the trigger-has-content branch above.
	 */
	private markNodeAsPinned(
		nodeName: string,
		nodeResults: Record<string, InstanceAiEvalNodeResult>,
	): void {
		const existing = nodeResults[nodeName];
		nodeResults[nodeName] = {
			output: null,
			interceptedRequests: [],
			executionMode: 'pinned',
			...(existing?.configIssues ? { configIssues: existing.configIssues } : {}),
		};
	}

	/**
	 * Build the failure result returned when execution threw partway through —
	 * preserves the accumulated `nodeResults`, `hints`, and credential
	 * diagnostics rather than discarding them like `errorResult` does. Lifted
	 * out of the `execute()` catch block so the inline expression count there
	 * stays within complexity bounds.
	 */
	private buildPartialFailureResult(
		executionId: string,
		error: unknown,
		nodeResults: Record<string, InstanceAiEvalNodeResult>,
		hints: MockHints,
		credentialsHelper: EvalMockedCredentialsHelper | undefined,
	): InstanceAiEvalExecutionResult {
		const message = error instanceof Error ? error.message : String(error);
		this.logger.error(`[EvalMock] Workflow execution failed: ${message}`);
		return {
			executionId,
			success: false,
			nodeResults,
			errors: [`Execution failed: ${message}`],
			hints,
			mockedCredentials: credentialsHelper?.mockedCredentials ?? [],
			rewrittenCredentials: credentialsHelper?.rewrittenCredentials ?? [],
		};
	}

	// ── Result extraction ─────────────────────────────────────────────────

	/**
	 * When binary data storage is filesystem/s3/db, `binary.<key>.data` is the
	 * mode marker (e.g. `'filesystem-v2'`) and the actual bytes live behind
	 * `binary.<key>.id`. Verifiers compare against the base64 payload, so read
	 * the stored bytes back and inline them on a shallow copy.
	 */
	private async hydrateBinaryData(items: INodeExecutionData[]): Promise<INodeExecutionData[]> {
		return await Promise.all(
			items.map(async (item) => {
				if (!item.binary) return item;
				const hydratedBinary: IBinaryKeyData = {};
				for (const [key, entry] of Object.entries(item.binary)) {
					if (entry.id) {
						try {
							const buffer = await this.binaryDataService.getAsBuffer(entry);
							hydratedBinary[key] = { ...entry, data: buffer.toString('base64') };
							continue;
						} catch (error) {
							this.logger.warn(
								`[EvalMock] Failed to hydrate binary "${key}" (${entry.id}): ${error instanceof Error ? error.message : String(error)}`,
							);
						}
					}
					hydratedBinary[key] = entry;
				}
				return { ...item, binary: hydratedBinary };
			}),
		);
	}

	private async buildResult(
		executionId: string,
		result: IRun,
		nodeResults: Record<string, InstanceAiEvalNodeResult>,
		hints: MockHints,
		credentialsHelper: EvalMockedCredentialsHelper,
	): Promise<InstanceAiEvalExecutionResult> {
		const errors: string[] = [];

		const runData = result.data?.resultData?.runData ?? {};
		for (const [nodeName, nodeRuns] of Object.entries(runData)) {
			// Nodes already in nodeResults were intercepted (mocked) or pinned.
			// Nodes appearing here for the first time executed for real (logic nodes).
			const entry = (nodeResults[nodeName] ??= {
				output: null,
				interceptedRequests: [],
				executionMode: 'real',
			});
			const lastRun = nodeRuns[nodeRuns.length - 1];
			if (lastRun?.startTime) {
				entry.startTime = lastRun.startTime;
			}
			if (lastRun?.data?.main) {
				// Capture output from all branches (Switch/IF nodes have multiple outputs)
				const flattened = lastRun.data.main
					.flat()
					.filter((item): item is INodeExecutionData => item !== null);
				entry.outputCount = flattened.length;
				const allOutputs = flattened.slice(0, MAX_OUTPUT_ITEMS_PER_NODE);
				if (allOutputs.length > 0) {
					entry.output = await this.hydrateBinaryData(allOutputs);
				}
			}
			if (lastRun?.error) {
				errors.push(`Node "${nodeName}": ${lastRun.error.message}`);
			}
		}

		const executionError = result.data?.resultData?.error;
		if (executionError) {
			errors.push(`Workflow error: ${executionError.message}`);
		}

		return {
			executionId,
			success: executionError === undefined && errors.length === 0,
			nodeResults,
			errors,
			hints,
			mockedCredentials: credentialsHelper.mockedCredentials,
			rewrittenCredentials: credentialsHelper.rewrittenCredentials,
		};
	}

	private errorResult(executionId: string, message: string): InstanceAiEvalExecutionResult {
		return {
			executionId,
			success: false,
			nodeResults: {},
			errors: [message],
			hints: {
				globalContext: '',
				triggerContent: {},
				nodeHints: {},
				warnings: [],
				bypassPinData: {},
			},
			mockedCredentials: [],
			rewrittenCredentials: [],
		};
	}
}
