import pickBy from 'lodash/pickBy';
import {
	type INodeExecutionData,
	NodeConnectionTypes,
	type IExecuteFunctions,
	type INodeType,
	type INodeTypeDescription,
	type ITriggerFunctions,
	type ITriggerResponse,
} from 'n8n-workflow';

import {
	INPUT_SOURCE,
	WORKFLOW_INPUTS,
	JSON_EXAMPLE,
	VALUES,
	TYPE_OPTIONS,
	PASSTHROUGH,
	FALLBACK_DEFAULT_VALUE,
} from '../../../utils/workflowInputsResourceMapping/constants';
import { getFieldEntries } from '../../../utils/workflowInputsResourceMapping/GenericFunctions';

export class ExecuteWorkflowTrigger implements INodeType {
	description: INodeTypeDescription = {
		displayName: 'Execute Workflow Trigger',
		name: 'executeWorkflowTrigger',
		icon: 'node:sub-workflow-trigger',
		iconColor: 'black',
		group: ['trigger'],
		version: [1, 1.1, 1.2],
		description:
			'Helpers for calling other n8n workflows. Used for designing modular, microservice-like workflows.',
		eventTriggerDescription: '',
		maxNodes: 1,
		defaults: {
			name: 'When Executed by Another Workflow',
		},
		inputs: [],
		outputs: [NodeConnectionTypes.Main],
		hints: [
			{
				message:
					"This workflow isn't set to accept any input data. Fill out the workflow input schema or change the workflow to accept any data passed to it.",
				// This condition checks if we have no input fields, which gets a bit awkward:
				// For WORKFLOW_INPUTS: keys() only contains `VALUES` if at least one value is provided
				// For JSON_EXAMPLE: We remove all whitespace and check if we're left with an empty object. Note that we already error if the example is not valid JSON
				displayCondition:
					`={{$parameter['${INPUT_SOURCE}'] === '${WORKFLOW_INPUTS}' && !$parameter['${WORKFLOW_INPUTS}'].keys().length ` +
					`|| $parameter['${INPUT_SOURCE}'] === '${JSON_EXAMPLE}' && $parameter['${JSON_EXAMPLE}'].toString().replaceAll(' ', '').replaceAll('\\n', '') === '{}' }}`,
				whenToDisplay: 'always',
				location: 'ndv',
			},
		],
		properties: [
			{
				displayName: 'Events',
				name: 'events',
				type: 'hidden',
				noDataExpression: true,
				options: [
					{
						name: 'Workflow Call',
						value: 'worklfow_call',
						description: 'When executed by another workflow using Execute Workflow Trigger',
						action: 'When executed by Another Workflow',
					},
				],
				default: 'worklfow_call',
			},
			{
				displayName:
					"When an ‘execute workflow’ node calls this workflow, the execution starts here. Any data passed into the 'execute workflow' node will be output by this node.",
				name: 'notice',
				type: 'notice',
				default: '',
				displayOptions: {
					show: { '@version': [{ _cnd: { eq: 1 } }] },
				},
			},
			{
				displayName: 'This node is out of date. Please upgrade by removing it and adding a new one',
				name: 'outdatedVersionWarning',
				type: 'notice',
				displayOptions: { show: { '@version': [{ _cnd: { eq: 1 } }] } },
				default: '',
			},
			{
				// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
				displayName: 'Input data mode',
				name: INPUT_SOURCE,
				type: 'options',
				options: [
					{
						// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
						name: 'Define using fields below',
						value: WORKFLOW_INPUTS,
						description: 'Provide input fields via UI',
					},
					{
						// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
						name: 'Define using JSON example',
						value: JSON_EXAMPLE,
						description: 'Generate a schema from an example JSON object',
					},
					{
						// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
						name: 'Accept all data',
						value: PASSTHROUGH,
						description: 'Use all incoming data from the parent workflow',
					},
				],
				default: WORKFLOW_INPUTS,
				noDataExpression: true,
				displayOptions: {
					show: { '@version': [{ _cnd: { gte: 1.1 } }] },
				},
			},
			{
				displayName:
					'Provide an example object to infer fields and their types.<br>To allow any type for a given field, set the value to null.',
				name: `${JSON_EXAMPLE}_notice`,
				type: 'notice',
				default: '',
				displayOptions: {
					show: { '@version': [{ _cnd: { gte: 1.1 } }], inputSource: [JSON_EXAMPLE] },
				},
			},
			{
				displayName: 'JSON Example',
				name: JSON_EXAMPLE,
				type: 'json',
				default: JSON.stringify(
					{
						aField: 'a string',
						aNumber: 123,
						thisFieldAcceptsAnyType: null,
						anArray: [],
					},
					null,
					2,
				),
				noDataExpression: true,
				displayOptions: {
					show: { '@version': [{ _cnd: { gte: 1.1 } }], inputSource: [JSON_EXAMPLE] },
				},
			},
			{
				displayName: 'Workflow Input Schema',
				name: WORKFLOW_INPUTS,
				placeholder: 'Add field',
				type: 'fixedCollection',
				description:
					'Define expected input fields. If no inputs are provided, all data from the calling workflow will be passed through.',
				typeOptions: {
					multipleValues: true,
					sortable: true,
					minRequiredFields: 1,
				},
				displayOptions: {
					show: { '@version': [{ _cnd: { gte: 1.1 } }], inputSource: [WORKFLOW_INPUTS] },
				},
				default: {},
				options: [
					{
						name: VALUES,
						displayName: 'Values',
						values: [
							{
								displayName: 'Name',
								name: 'name',
								type: 'string',
								default: '',
								placeholder: 'e.g. fieldName',
								description:
									'A unique name for this workflow input, used to reference it from another workflows',
								required: true,
								noDataExpression: true,
							},
							{
								displayName: 'Type',
								name: 'type',
								type: 'options',
								description:
									"Expected data type for this input value. Determines how this field's values are stored, validated, and displayed.",
								options: TYPE_OPTIONS,
								required: true,
								default: 'string',
								noDataExpression: true,
							},
						],
					},
				],
			},
			{
				displayName: 'Items to Return',
				name: 'returnOutput',
				type: 'options',
				noDataExpression: true,
				default: 'lastRunOnly',
				description:
					'Choose what to send back when the last node ran multiple times (for example, after a Loop Over Items)',
				options: [
					{
						name: 'All Items From Every Run',
						value: 'allRuns',
						description: 'Send every item the last node produced, across all its runs',
					},
					{
						name: 'Items From the Last Run Only',
						value: 'lastRunOnly',
						description: "Send only the items from the last node's final run",
					},
				],
				displayOptions: {
					show: {
						'@version': [{ _cnd: { lt: 1.2 } }],
					},
				},
			},
		],
	};

	async trigger(this: ITriggerFunctions): Promise<ITriggerResponse> {
		// ExecuteWorkflowTrigger is triggered by the ExecuteWorkflow node
		// No setup or teardown is required, as the triggering is handled externally
		return {};
	}

	async execute(this: IExecuteFunctions) {
		const inputData = this.getInputData();
		const inputSource = this.getNodeParameter(INPUT_SOURCE, 0, PASSTHROUGH) as string;

		// Note on the data we receive from ExecuteWorkflow caller:
		//
		// The ExecuteWorkflow node typechecks all fields explicitly provided by the user here via the resourceMapper
		// and removes all fields that are in the schema, but `removed` in the resourceMapper.
		//
		// In passthrough and legacy node versions, inputData will line up since the resourceMapper is empty,
		// in which case all input is passed through.
		// In other cases we will already have matching types and fields provided by the resource mapper,
		// so we just need to be permissive on this end,
		// while ensuring we provide default values for fields in our schema, which are removed in the resourceMapper.

		if (inputSource === PASSTHROUGH) {
			return [inputData];
		} else {
			const newParams = getFieldEntries(this);
			const newKeys = new Set(newParams.fields.map((x) => x.name));
			const itemsInSchema: INodeExecutionData[] = inputData.map(({ json, binary }, index) => ({
				json: {
					...Object.fromEntries(newParams.fields.map((x) => [x.name, FALLBACK_DEFAULT_VALUE])),
					// Need to trim to the expected schema to support legacy Execute Workflow callers passing through all their data
					// which we do not want to expose past this node.
					...pickBy(json, (_value, key) => newKeys.has(key)),
				},
				index,
				binary,
			}));

			return [itemsInSchema];
		}
	}
}
