<script setup lang="ts">
import type {
	INodeParameters,
	INodeProperties,
	NodeParameterValue,
	NodeParameterValueType,
} from 'n8n-workflow';
import { ADD_FORM_NOTICE, deepCopy, NodeHelpers } from 'n8n-workflow';
import { computed, defineAsyncComponent, onErrorCaptured, ref, watch, type WatchSource } from 'vue';

import type { IUpdateInformation } from '@/Interface';

import AssignmentCollection from '@/components/AssignmentCollection/AssignmentCollection.vue';
import ButtonParameter from '@/components/ButtonParameter/ButtonParameter.vue';
import FilterConditions from '@/components/FilterConditions/FilterConditions.vue';
import ImportCurlParameter from '@/components/ImportCurlParameter.vue';
import MultipleParameter from '@/components/MultipleParameter.vue';
import ParameterInputFull from '@/components/ParameterInputFull.vue';
import ResourceMapper from '@/components/ResourceMapper/ResourceMapper.vue';
import { useI18n } from '@/composables/useI18n';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import {
	FORM_NODE_TYPE,
	FORM_TRIGGER_NODE_TYPE,
	KEEP_AUTH_IN_NDV_FOR_NODES,
	WAIT_NODE_TYPE,
} from '@/constants';
import { useNDVStore } from '@/stores/ndv.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';

import {
	getMainAuthField,
	getNodeAuthFields,
	isAuthRelatedParameter,
} from '@/utils/nodeTypesUtils';
import { captureException } from '@sentry/vue';
import { computedWithControl } from '@vueuse/core';
import { get, set } from 'lodash-es';
import { N8nIcon, N8nIconButton, N8nInputLabel, N8nNotice, N8nText } from '@n8n/design-system';
import { useRouter } from 'vue-router';

const LazyFixedCollectionParameter = defineAsyncComponent(
	async () => await import('./FixedCollectionParameter.vue'),
);
const LazyCollectionParameter = defineAsyncComponent(
	async () => await import('./CollectionParameter.vue'),
);

// Parameter issues are displayed within the inputs themselves, but some parameters need to show them in the label UI
const showIssuesInLabelFor = ['fixedCollection'];

type Props = {
	nodeValues: INodeParameters;
	parameters: INodeProperties[];
	path?: string;
	hideDelete?: boolean;
	indent?: boolean;
	isReadOnly?: boolean;
	hiddenIssuesInputs?: string[];
	entryIndex?: number;
};

const props = withDefaults(defineProps<Props>(), { path: '', hiddenIssuesInputs: () => [] });
const emit = defineEmits<{
	activate: [];
	valueChanged: [value: IUpdateInformation];
	parameterBlur: [value: string];
}>();

const nodeTypesStore = useNodeTypesStore();
const ndvStore = useNDVStore();

const nodeHelpers = useNodeHelpers();
const asyncLoadingError = ref(false);
const router = useRouter();
const workflowHelpers = useWorkflowHelpers({ router });
const i18n = useI18n();

onErrorCaptured((e, component) => {
	if (
		!['LazyFixedCollectionParameter', 'LazyCollectionParameter'].includes(
			component?.$options.name as string,
		)
	) {
		return;
	}
	asyncLoadingError.value = true;
	console.error(e);
	captureException(e, {
		tags: {
			asyncLoadingError: true,
		},
	});
	// Don't propagate the error further
	return false;
});

const nodeType = computed(() => {
	if (node.value) {
		return nodeTypesStore.getNodeType(node.value.type, node.value.typeVersion);
	}
	return null;
});

const filteredParameters = computedWithControl(
	[() => props.parameters, () => props.nodeValues] as WatchSource[],
	() => {
		const parameters = props.parameters.filter((parameter: INodeProperties) =>
			displayNodeParameter(parameter),
		);

		const activeNode = ndvStore.activeNode;

		if (activeNode && activeNode.type === FORM_TRIGGER_NODE_TYPE) {
			return updateFormTriggerParameters(parameters, activeNode.name);
		}

		if (activeNode && activeNode.type === FORM_NODE_TYPE) {
			return updateFormParameters(parameters, activeNode.name);
		}

		if (
			activeNode &&
			activeNode.type === WAIT_NODE_TYPE &&
			activeNode.parameters.resume === 'form'
		) {
			return updateWaitParameters(parameters, activeNode.name);
		}

		return parameters;
	},
);

const filteredParameterNames = computed(() => {
	return filteredParameters.value.map((parameter) => parameter.name);
});

const node = computed(() => ndvStore.activeNode);

const nodeAuthFields = computed(() => {
	return getNodeAuthFields(nodeType.value);
});

const credentialsParameterIndex = computed(() => {
	return filteredParameters.value.findIndex((parameter) => parameter.type === 'credentials');
});

const indexToShowSlotAt = computed(() => {
	if (credentialsParameterIndex.value !== -1) {
		return credentialsParameterIndex.value;
	}

	let index = 0;
	// For nodes that use old credentials UI, keep credentials below authentication field in NDV
	// otherwise credentials will use auth filed position since the auth field is moved to credentials modal
	const fieldOffset = KEEP_AUTH_IN_NDV_FOR_NODES.includes(nodeType.value?.name || '') ? 1 : 0;
	const credentialsDependencies = getCredentialsDependencies();

	filteredParameters.value.forEach((prop, propIndex) => {
		if (credentialsDependencies.has(prop.name)) {
			index = propIndex + fieldOffset;
		}
	});

	return Math.min(index, filteredParameters.value.length - 1);
});

const mainNodeAuthField = computed(() => {
	return getMainAuthField(nodeType.value || null);
});

watch(filteredParameterNames, (newValue, oldValue) => {
	if (newValue === undefined) {
		return;
	}
	// After a parameter does not get displayed anymore make sure that its value gets removed
	// Is only needed for the edge-case when a parameter gets displayed depending on another field
	// which contains an expression.
	for (const parameter of oldValue) {
		if (!newValue.includes(parameter)) {
			const parameterData = {
				name: `${props.path}.${parameter}`,
				node: ndvStore.activeNode?.name || '',
				value: undefined,
			};
			emit('valueChanged', parameterData);
		}
	}
});

function updateFormTriggerParameters(parameters: INodeProperties[], triggerName: string) {
	const workflow = workflowHelpers.getCurrentWorkflow();
	const connectedNodes = workflow.getChildNodes(triggerName);

	const hasFormPage = connectedNodes.some((nodeName) => {
		const node = workflow.getNode(nodeName);
		return node && node.type === FORM_NODE_TYPE;
	});

	if (hasFormPage) {
		const triggerParameters: INodeProperties[] = [];

		for (const parameter of parameters) {
			if (parameter.name === 'responseMode') {
				triggerParameters.push({
					displayName: 'On submission, the user will be taken to the next form node',
					name: 'formResponseModeNotice',
					type: 'notice',
					default: '',
				});

				continue;
			}

			if (parameter.name === ADD_FORM_NOTICE) continue;

			if (parameter.name === 'options') {
				const options = (parameter.options as INodeProperties[]).filter(
					(option) => option.name !== 'respondWithOptions',
				);
				triggerParameters.push({
					...parameter,
					options,
				});
				continue;
			}

			triggerParameters.push(parameter);
		}
		return triggerParameters;
	}

	return parameters;
}

function updateWaitParameters(parameters: INodeProperties[], nodeName: string) {
	const workflow = workflowHelpers.getCurrentWorkflow();
	const parentNodes = workflow.getParentNodes(nodeName);

	const formTriggerName = parentNodes.find(
		(node) => workflow.nodes[node].type === FORM_TRIGGER_NODE_TYPE,
	);
	if (!formTriggerName) return parameters;

	const connectedNodes = workflow.getChildNodes(formTriggerName);

	const hasFormPage = connectedNodes.some((nodeName) => {
		const node = workflow.getNode(nodeName);
		return node && node.type === FORM_NODE_TYPE;
	});

	if (hasFormPage) {
		const waitNodeParameters: INodeProperties[] = [];

		for (const parameter of parameters) {
			if (parameter.name === 'options') {
				const options = (parameter.options as INodeProperties[]).filter(
					(option) => option.name !== 'respondWithOptions' && option.name !== 'webhookSuffix',
				);
				waitNodeParameters.push({
					...parameter,
					options,
				});
				continue;
			}

			waitNodeParameters.push(parameter);
		}
		return waitNodeParameters;
	}

	return parameters;
}

function updateFormParameters(parameters: INodeProperties[], nodeName: string) {
	const workflow = workflowHelpers.getCurrentWorkflow();
	const parentNodes = workflow.getParentNodes(nodeName);

	const formTriggerName = parentNodes.find(
		(node) => workflow.nodes[node].type === FORM_TRIGGER_NODE_TYPE,
	);

	if (formTriggerName) return parameters.filter((parameter) => parameter.name !== 'triggerNotice');

	return parameters;
}

function onParameterBlur(parameterName: string) {
	emit('parameterBlur', parameterName);
}

function getCredentialsDependencies() {
	const dependencies = new Set();

	// Get names of all fields that credentials rendering depends on (using displayOptions > show)
	if (nodeType.value?.credentials) {
		for (const cred of nodeType.value.credentials) {
			if (cred.displayOptions?.show) {
				Object.keys(cred.displayOptions.show).forEach((fieldName) => dependencies.add(fieldName));
			}
		}
	}
	return dependencies;
}

function multipleValues(parameter: INodeProperties): boolean {
	return getArgument('multipleValues', parameter) === true;
}

function getArgument(
	argumentName: string,
	parameter: INodeProperties,
): string | string[] | number | boolean | undefined {
	if (parameter.typeOptions === undefined) {
		return undefined;
	}

	if (parameter.typeOptions[argumentName] === undefined) {
		return undefined;
	}

	return parameter.typeOptions[argumentName];
}

function getPath(parameterName: string): string {
	return (props.path ? `${props.path}.` : '') + parameterName;
}

function deleteOption(optionName: string): void {
	const parameterData = {
		name: getPath(optionName),
		value: undefined,
	};

	// TODO: If there is only one option it should delete the whole one

	emit('valueChanged', parameterData);
}

function mustHideDuringCustomApiCall(
	parameter: INodeProperties,
	nodeValues: INodeParameters,
): boolean {
	if (parameter?.displayOptions?.hide) return true;

	const MUST_REMAIN_VISIBLE = [
		'authentication',
		'resource',
		'operation',
		...Object.keys(nodeValues),
	];

	return !MUST_REMAIN_VISIBLE.includes(parameter.name);
}

function displayNodeParameter(
	parameter: INodeProperties,
	displayKey: 'displayOptions' | 'disabledOptions' = 'displayOptions',
): boolean {
	if (parameter.type === 'hidden') {
		return false;
	}

	if (
		nodeHelpers.isCustomApiCallSelected(props.nodeValues) &&
		mustHideDuringCustomApiCall(parameter, props.nodeValues)
	) {
		return false;
	}

	// Hide authentication related fields since it will now be part of credentials modal
	if (
		!KEEP_AUTH_IN_NDV_FOR_NODES.includes(node.value?.type || '') &&
		mainNodeAuthField.value &&
		(parameter.name === mainNodeAuthField.value?.name || shouldHideAuthRelatedParameter(parameter))
	) {
		return false;
	}

	if (parameter[displayKey] === undefined) {
		// If it is not defined no need to do a proper check
		return true;
	}

	const nodeValues: INodeParameters = {};
	let rawValues = props.nodeValues;
	if (props.path) {
		rawValues = get(props.nodeValues, props.path) as INodeParameters;
	}

	if (!rawValues) {
		return false;
	}
	// Resolve expressions
	const resolveKeys = Object.keys(rawValues);
	let key: string;
	let i = 0;
	let parameterGotResolved = false;
	do {
		key = resolveKeys.shift() as string;
		const value = rawValues[key];
		if (typeof value === 'string' && value?.charAt(0) === '=') {
			// Contains an expression that
			if (
				value.includes('$parameter') &&
				resolveKeys.some((parameterName) => value.includes(parameterName))
			) {
				// Contains probably an expression of a missing parameter so skip
				resolveKeys.push(key);
				continue;
			} else {
				// Contains probably no expression with a missing parameter so resolve
				try {
					nodeValues[key] = workflowHelpers.resolveExpression(
						value,
						nodeValues,
					) as NodeParameterValue;
				} catch (e) {
					// If expression is invalid ignore
					nodeValues[key] = '';
				}
				parameterGotResolved = true;
			}
		} else {
			// Does not contain an expression, add directly
			nodeValues[key] = rawValues[key];
		}
		// TODO: Think about how to calculate this best
		if (i++ > 50) {
			// Make sure we do not get caught
			break;
		}
	} while (resolveKeys.length !== 0);

	if (parameterGotResolved) {
		if (props.path) {
			rawValues = deepCopy(props.nodeValues);
			set(rawValues, props.path, nodeValues);
			return nodeHelpers.displayParameter(rawValues, parameter, props.path, node.value, displayKey);
		} else {
			return nodeHelpers.displayParameter(nodeValues, parameter, '', node.value, displayKey);
		}
	}

	return nodeHelpers.displayParameter(
		props.nodeValues,
		parameter,
		props.path,
		node.value,
		displayKey,
	);
}

function valueChanged(parameterData: IUpdateInformation): void {
	emit('valueChanged', parameterData);
}

function onNoticeAction(action: string) {
	if (action === 'activate') {
		emit('activate');
	}
}

function getParameterIssues(parameter: INodeProperties): string[] {
	if (!node.value || !showIssuesInLabelFor.includes(parameter.type)) {
		return [];
	}
	const issues = NodeHelpers.getParameterIssues(
		parameter,
		node.value.parameters,
		'',
		node.value,
		nodeType.value,
	);

	return issues.parameters?.[parameter.name] ?? [];
}

/**
 * Handles default node button parameter type actions
 * @param parameter
 */

function shouldHideAuthRelatedParameter(parameter: INodeProperties): boolean {
	// TODO: For now, hide all fields that are used in authentication fields displayOptions
	// Ideally, we should check if any non-auth field depends on it before hiding it but
	// since there is no such case, omitting it to avoid additional computation
	return isAuthRelatedParameter(nodeAuthFields.value, parameter);
}

function shouldShowOptions(parameter: INodeProperties): boolean {
	return parameter.type !== 'resourceMapper';
}

function getDependentParametersValues(parameter: INodeProperties): string | null {
	const loadOptionsDependsOn = getArgument('loadOptionsDependsOn', parameter) as
		| string[]
		| undefined;

	if (loadOptionsDependsOn === undefined) {
		return null;
	}

	// Get the resolved parameter values of the current node
	const currentNodeParameters = ndvStore.activeNode?.parameters;
	try {
		const resolvedNodeParameters = workflowHelpers.resolveParameter(currentNodeParameters);

		const returnValues: string[] = [];
		for (const parameterPath of loadOptionsDependsOn) {
			returnValues.push(get(resolvedNodeParameters, parameterPath) as string);
		}

		return returnValues.join('|');
	} catch (error) {
		return null;
	}
}

function getParameterValue<T extends NodeParameterValueType = NodeParameterValueType>(
	name: string,
): T {
	return nodeHelpers.getParameterValue(props.nodeValues, name, props.path) as T;
}
</script>

<template>
	<div class="parameter-input-list-wrapper">
		<div
			v-for="(parameter, index) in filteredParameters"
			:key="parameter.name"
			:class="{ indent }"
			data-test-id="parameter-item"
		>
			<slot v-if="indexToShowSlotAt === index" />

			<div
				v-if="multipleValues(parameter) === true && parameter.type !== 'fixedCollection'"
				class="parameter-item"
			>
				<MultipleParameter
					:parameter="parameter"
					:values="getParameterValue(parameter.name)"
					:node-values="nodeValues"
					:path="getPath(parameter.name)"
					:is-read-only="isReadOnly"
					@value-changed="valueChanged"
				/>
			</div>

			<ImportCurlParameter
				v-else-if="parameter.type === 'curlImport'"
				:is-read-only="isReadOnly"
				@value-changed="valueChanged"
			/>

			<N8nNotice
				v-else-if="parameter.type === 'notice'"
				:class="['parameter-item', parameter.typeOptions?.containerClass ?? '']"
				:content="i18n.nodeText().inputLabelDisplayName(parameter, path)"
				@action="onNoticeAction"
			/>

			<div v-else-if="parameter.type === 'button'" class="parameter-item">
				<ButtonParameter
					:parameter="parameter"
					:path="path"
					:value="getParameterValue(parameter.name)"
					:is-read-only="isReadOnly"
					@value-changed="valueChanged"
				/>
			</div>

			<div
				v-else-if="['collection', 'fixedCollection'].includes(parameter.type)"
				class="multi-parameter"
			>
				<N8nInputLabel
					:label="i18n.nodeText().inputLabelDisplayName(parameter, path)"
					:tooltip-text="i18n.nodeText().inputLabelDescription(parameter, path)"
					size="small"
					:underline="true"
					:input-name="parameter.name"
					color="text-dark"
				>
					<template
						v-if="
							showIssuesInLabelFor.includes(parameter.type) &&
							getParameterIssues(parameter).length > 0
						"
						#issues
					>
						<N8nTooltip>
							<template #content>
								<span v-for="(issue, i) in getParameterIssues(parameter)" :key="i">{{
									issue
								}}</span>
							</template>
							<N8nIcon icon="exclamation-triangle" size="small" color="danger" />
						</N8nTooltip>
					</template>
				</N8nInputLabel>
				<Suspense v-if="!asyncLoadingError">
					<template #default>
						<LazyCollectionParameter
							v-if="parameter.type === 'collection'"
							:parameter="parameter"
							:values="getParameterValue(parameter.name)"
							:node-values="nodeValues"
							:path="getPath(parameter.name)"
							:is-read-only="isReadOnly"
							@value-changed="valueChanged"
						/>
						<LazyFixedCollectionParameter
							v-else-if="parameter.type === 'fixedCollection'"
							:parameter="parameter"
							:values="getParameterValue(parameter.name)"
							:node-values="nodeValues"
							:path="getPath(parameter.name)"
							:is-read-only="isReadOnly"
							@value-changed="valueChanged"
						/>
					</template>
					<template #fallback>
						<N8nText size="small" class="async-notice">
							<N8nIcon icon="sync-alt" size="xsmall" :spin="true" />
							{{ i18n.baseText('parameterInputList.loadingFields') }}
						</N8nText>
					</template>
				</Suspense>
				<N8nText v-else size="small" color="danger" class="async-notice">
					<N8nIcon icon="exclamation-triangle" size="xsmall" />
					{{ i18n.baseText('parameterInputList.loadingError') }}
				</N8nText>
				<N8nIconButton
					v-if="hideDelete !== true && !isReadOnly && !parameter.isNodeSetting"
					type="tertiary"
					text
					size="mini"
					icon="trash"
					class="icon-button"
					:title="i18n.baseText('parameterInputList.delete')"
					@click="deleteOption(parameter.name)"
				></N8nIconButton>
			</div>
			<ResourceMapper
				v-else-if="parameter.type === 'resourceMapper'"
				:parameter="parameter"
				:node="node"
				:path="getPath(parameter.name)"
				:dependent-parameters-values="getDependentParametersValues(parameter)"
				:is-read-only="isReadOnly"
				input-size="small"
				label-size="small"
				@value-changed="valueChanged"
			/>
			<FilterConditions
				v-else-if="parameter.type === 'filter'"
				:parameter="parameter"
				:value="getParameterValue(parameter.name)"
				:path="getPath(parameter.name)"
				:node="node"
				:read-only="isReadOnly"
				@value-changed="valueChanged"
			/>
			<AssignmentCollection
				v-else-if="parameter.type === 'assignmentCollection'"
				:parameter="parameter"
				:value="getParameterValue(parameter.name)"
				:path="getPath(parameter.name)"
				:node="node"
				:is-read-only="isReadOnly"
				:default-type="parameter.typeOptions?.assignment?.defaultType"
				:disable-type="parameter.typeOptions?.assignment?.disableType"
				@value-changed="valueChanged"
			/>
			<div v-else-if="credentialsParameterIndex !== index" class="parameter-item">
				<N8nIconButton
					v-if="hideDelete !== true && !isReadOnly && !parameter.isNodeSetting"
					type="tertiary"
					text
					size="mini"
					icon="trash"
					class="icon-button"
					:title="i18n.baseText('parameterInputList.delete')"
					@click="deleteOption(parameter.name)"
				></N8nIconButton>

				<ParameterInputFull
					:parameter="parameter"
					:hide-issues="hiddenIssuesInputs.includes(parameter.name)"
					:value="getParameterValue(parameter.name)"
					:display-options="shouldShowOptions(parameter)"
					:path="getPath(parameter.name)"
					:is-read-only="
						isReadOnly ||
						(parameter.disabledOptions && displayNodeParameter(parameter, 'disabledOptions'))
					"
					:hide-label="false"
					:node-values="nodeValues"
					@update="valueChanged"
					@blur="onParameterBlur(parameter.name)"
				/>
			</div>
		</div>
		<div v-if="filteredParameters.length === 0" :class="{ indent }">
			<slot />
		</div>
	</div>
</template>

<style lang="scss">
.parameter-input-list-wrapper {
	--disabled-fill: var(--color-background-base);
	.icon-button {
		position: absolute;
		opacity: 0;
		top: 0;
		left: calc(-0.5 * var(--spacing-2xs));
		transition: opacity 100ms ease-in;
		Button {
			color: var(--color-icon-base);
		}
	}
	.icon-button > Button:hover {
		color: var(--color-icon-hover);
	}

	.indent > div {
		padding-left: var(--spacing-s);
	}

	.multi-parameter {
		position: relative;
		margin: var(--spacing-xs) 0;

		.parameter-info {
			display: none;
		}
	}

	.parameter-item {
		position: relative;
		margin: var(--spacing-xs) 0;
	}
	.parameter-item:hover > .icon-button,
	.multi-parameter:hover > .icon-button {
		opacity: 1;
	}

	.parameter-notice {
		background-color: var(--color-warning-tint-2);
		color: $custom-font-black;
		margin: 0.3em 0;
		padding: 0.7em;

		a {
			font-weight: var(--font-weight-bold);
		}
	}

	.async-notice {
		display: block;
		padding: var(--spacing-3xs) 0;
	}
}
</style>
