import { Logger } from '@n8n/backend-common';
import { Service } from '@n8n/di';
import {
	ICredentialContext,
	IExecuteData,
	IExecutionContext,
	INodeExecutionData,
	PlaintextExecutionContext,
	toCredentialContext,
	toExecutionContextEstablishmentHookParameter,
	toSecureArtifacts,
	Workflow,
} from 'n8n-workflow';

import { Cipher } from '@/encryption';
import { deepMerge } from '@/utils/deep-merge';

import { ExecutionContextHookRegistry } from './execution-context-hook-registry.service';

@Service()
export class ExecutionContextService {
	constructor(
		private readonly logger: Logger,
		private readonly executionContextHookRegistry: ExecutionContextHookRegistry,
		private readonly cipher: Cipher,
	) {}

	async decryptExecutionContext(context: IExecutionContext): Promise<PlaintextExecutionContext> {
		const { credentials: encCredentials, secureArtifacts: encSecureArtifacts, ...rest } = context;
		const result: PlaintextExecutionContext = { ...rest };
		if (encCredentials) {
			const decrypted = await this.cipher.decryptV2(encCredentials);
			result.credentials = toCredentialContext(decrypted);
		}
		if (encSecureArtifacts) {
			const decrypted = await this.cipher.decryptV2(encSecureArtifacts);
			result.secureArtifacts = toSecureArtifacts(decrypted);
		}
		return result;
	}

	/**
	 * Builds and encrypts a credential context for a manual editor-triggered execution.
	 *
	 * @param n8nAuthCookie - The JWT string extracted from the `n8n-auth` browser cookie.
	 * @returns Encrypted credential context string for storage in `IExecutionContext.credentials`.
	 */
	async buildManualExecutionCredentials(n8nAuthCookie: string): Promise<string> {
		const payload: ICredentialContext = {
			version: 1,
			identity: n8nAuthCookie,
			metadata: { source: 'manual-execution' },
		};
		return await this.cipher.encryptV2(payload);
	}

	async encryptExecutionContext(context: PlaintextExecutionContext): Promise<IExecutionContext> {
		const { credentials, secureArtifacts, ...rest } = context;
		const result: IExecutionContext = { ...rest };
		if (credentials) {
			result.credentials = await this.cipher.encryptV2(credentials);
		}
		if (secureArtifacts) {
			result.secureArtifacts = await this.cipher.encryptV2(secureArtifacts);
		}
		return result;
	}

	mergeExecutionContexts(
		baseContext: PlaintextExecutionContext,
		contextToMerge: Partial<PlaintextExecutionContext>,
	): PlaintextExecutionContext {
		return deepMerge(baseContext, contextToMerge);
	}

	// startItem is mutated to reflect any changes to trigger items made by the hooks
	async augmentExecutionContextWithHooks(
		workflow: Workflow,
		startItem: IExecuteData,
		contextToAugment: IExecutionContext,
	): Promise<{
		context: IExecutionContext;
		triggerItems: INodeExecutionData[] | null;
	}> {
		// Main input data is an array of items, each item represents an event that triggers the workflow execution
		// The 'main' selector selects the input name of the nodes, and the 0 index represents the runIndex,
		// 0 being the first run of this node in the workflow.

		let currentTriggerItems = startItem.data['main'][0];

		const contextEstablishmentHookParameters = {
			...(workflow.getNode(startItem.node.name)?.parameters ?? {}),
			...startItem.node.parameters,
		};

		// decrypt the context to work with plaintext data
		let context = await this.decryptExecutionContext(contextToAugment);

		// Run global hooks!
		for (const globalHook of this.executionContextHookRegistry.getGlobalHooks()) {
			// call the hook to let it modify the context and/or the main input data
			const result = await globalHook.execute({
				triggerNode: startItem.node,
				workflow,
				triggerItems: currentTriggerItems,
				context,
				options: {},
			});

			if (result.triggerItems !== undefined) {
				// Update trigger items in case they were modified by the hook
				currentTriggerItems = result.triggerItems;
			}

			if (result.contextUpdate) {
				// Merge any returned context fields into the execution context
				context = this.mergeExecutionContexts(context, result.contextUpdate);
			}
		}

		const startNodeParametersResult = toExecutionContextEstablishmentHookParameter(
			contextEstablishmentHookParameters,
		);

		if (!startNodeParametersResult || startNodeParametersResult.error) {
			if (startNodeParametersResult?.error) {
				this.logger.warn(
					`Failed to parse execution context establishment hook parameters for node ${startItem.node.name}: ${startNodeParametersResult.error.message}`,
				);
			}
			// no node specific execution establishment hooks found, we return early
			return {
				context: await this.encryptExecutionContext(context),
				triggerItems: currentTriggerItems,
			};
		}

		// startNodeParameters will hold the parameters of the start node
		// this can be the settings for the different hooks to be executed
		// for example to extract the bearer token from the start node data.
		const startNodeParameters = startNodeParametersResult.data;

		// based on startNodeParameters, startNodeType and currentTriggerItems we can now
		// iterate over the different hooks to extract specific data for the runtime context
		for (const hookParameters of startNodeParameters.contextEstablishmentHooks.hooks) {
			const hook = this.executionContextHookRegistry.getHookByName(hookParameters.hookName);

			if (!hook) {
				this.logger.warn(
					`Execution context establishment hook ${hookParameters.hookName} not found, skipping this hook`,
				);
				continue;
			}
			try {
				// call the hook to let it modify the context and/or the main input data
				const result = await hook.execute({
					triggerNode: startItem.node,
					workflow,
					triggerItems: currentTriggerItems,
					context,
					options: hookParameters,
				});

				if (result.triggerItems !== undefined) {
					// Update trigger items in case they were modified by the hook
					currentTriggerItems = result.triggerItems;
				}

				if (result.contextUpdate) {
					// Merge any returned context fields into the execution context
					context = this.mergeExecutionContexts(context, result.contextUpdate);
				}
			} catch (error) {
				this.logger.warn(
					`Failed to execute context establishment hook ${hookParameters.hookName}`,
					{ error },
				);
				if (!hookParameters.isAllowedToFail) {
					// If the hook is not allowed to fail, rethrow the error
					throw error;
				}
			}
		}

		return {
			context: await this.encryptExecutionContext(context),
			triggerItems: currentTriggerItems,
		};
	}
}
