<script setup lang="ts">
import { useResolvedExpression } from '@/app/composables/useResolvedExpression';
import { injectNDVStore } from '@/features/ndv/shared/ndv.store';
import { useBinaryDataAccessTooltip } from '@/features/ndv/shared/composables/useBinaryDataAccessTooltip';
import useEnvironmentsStore from '@/features/settings/environments.ee/environments.store';
import type { IUpdateInformation } from '@/Interface';
import { useI18n } from '@n8n/i18n';
import type { AssignmentValue, INodeProperties } from 'n8n-workflow';
import { computed, ref, watch } from 'vue';
import InputTriple from '../InputTriple/InputTriple.vue';
import ParameterInputFull from '../ParameterInputFull.vue';
import ParameterInputHint from '../ParameterInputHint.vue';
import ParameterIssues from '../ParameterIssues.vue';
import TypeSelect from './TypeSelect.vue';

import { removeExpressionPrefix } from '@/app/utils/expressions';
import { propertyNameFromExpression } from '@/app/utils/mappingUtils';
import { N8nIconButton, N8nTooltip } from '@n8n/design-system';
import { typeFromExpression } from '../../utils/assignmentCollection.utils';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';

interface Props {
	path: string;
	modelValue: AssignmentValue;
	issues: string[];
	hideType?: boolean;
	disableType?: boolean;
	isReadOnly?: boolean;
	index?: number;
}

const props = defineProps<Props>();

const assignment = ref<AssignmentValue>(props.modelValue);

watch(
	() => props.modelValue,
	(newValue) => {
		assignment.value = newValue;
	},
);

const emit = defineEmits<{
	'update:model-value': [value: AssignmentValue];
	remove: [];
}>();

const i18n = useI18n();
const ndvStore = injectNDVStore();
const environmentsStore = useEnvironmentsStore();
const { binaryDataAccessTooltip } = useBinaryDataAccessTooltip();
const workflowDocumentStore = injectWorkflowDocumentStore();

const assignmentTypeToNodeProperty = (
	type: string,
): Partial<INodeProperties> & Pick<INodeProperties, 'type'> => {
	switch (type) {
		case 'boolean':
			return {
				type: 'options',
				default: false,
				options: [
					{ name: 'false', value: false },
					{ name: 'true', value: true },
				],
			};
		case 'array':
		case 'object':
		case 'binary':
			return { type: 'string' };
		case 'any':
			return { type: 'string' };
		default:
			return { type } as INodeProperties;
	}
};

const nameParameter = computed<INodeProperties>(() => ({
	name: 'name',
	displayName: 'Name',
	default: '',
	requiresDataPath: 'single',
	placeholder: 'name',
	type: 'string',
}));

const valueParameter = computed<INodeProperties>(() => {
	const placeholder =
		assignment.value.type === 'binary'
			? i18n.baseText('assignment.binaryData.placeholder')
			: 'value';
	return {
		name: 'value',
		displayName: 'Value',
		default: '',
		placeholder,
		...assignmentTypeToNodeProperty(assignment.value.type ?? 'string'),
	};
});

const value = computed(() => assignment.value.value);

const resolvedAdditionalExpressionData = computed(() => {
	return { $vars: environmentsStore.variablesAsObject };
});

const { resolvedExpressionString, isExpression } = useResolvedExpression({
	expression: value,
	additionalData: resolvedAdditionalExpressionData,
});

const hint = computed(() => resolvedExpressionString.value);

const highlightHint = computed(() => Boolean(hint.value && ndvStore.value.getHoveringItem));

const onAssignmentNameChange = (update: IUpdateInformation): void => {
	assignment.value.name = update.value as string;
};

const onAssignmentTypeChange = (update: string): void => {
	assignment.value.type = update;

	if (update === 'boolean' && !isExpression.value) {
		assignment.value.value = false;
	}
};

const onAssignmentValueChange = (update: IUpdateInformation): void => {
	assignment.value.value = update.value as string;
};

const onRemove = (): void => {
	emit('remove');
};

const onBlur = (): void => {
	emit('update:model-value', assignment.value);
};

const onValueDrop = async (droppedExpression: string) => {
	if (props.disableType) {
		return;
	}

	const droppedValue = removeExpressionPrefix(droppedExpression);
	assignment.value.type = await typeFromExpression(
		droppedValue,
		workflowDocumentStore.value.documentId,
	);

	if (!assignment.value.name) {
		assignment.value.name = propertyNameFromExpression(droppedValue);
	}
};
</script>

<template>
	<div
		:class="{
			[$style.wrapper]: true,
			[$style.hasIssues]: issues.length > 0,
			[$style.hasHint]: !!hint,
		}"
		data-test-id="assignment"
	>
		<N8nIconButton
			variant="ghost"
			v-if="!isReadOnly"
			size="small"
			icon="grip-vertical"
			:class="[$style.iconButton, $style.defaultTopPadding, 'drag-handle']"
		/>
		<N8nIconButton
			variant="ghost"
			v-if="!isReadOnly"
			size="small"
			icon="trash-2"
			data-test-id="assignment-remove"
			:class="[$style.iconButton, $style.extraTopPadding]"
			@click="onRemove"
		/>

		<div :class="$style.inputs">
			<InputTriple middle-width="100px">
				<template #left>
					<ParameterInputFull
						display-options
						hide-label
						hide-hint
						options-position="top"
						:is-read-only="isReadOnly"
						:parameter="nameParameter"
						:value="assignment.name"
						:path="`${path}.name`"
						data-test-id="assignment-name"
						@update="onAssignmentNameChange"
						@blur="onBlur"
					/>
				</template>
				<template v-if="!hideType" #middle="{ isStacked }">
					<div :class="$style.typeSelectWrapper">
						<N8nTooltip placement="left" :disabled="assignment.type !== 'binary'">
							<template #content>
								{{ binaryDataAccessTooltip }}
							</template>
							<TypeSelect
								:model-value="assignment.type ?? 'string'"
								:is-read-only="disableType || isReadOnly"
								:is-stacked="isStacked"
								@update:model-value="onAssignmentTypeChange"
							/>
						</N8nTooltip>
					</div>
				</template>
				<template #right="{ isStacked }">
					<div :class="$style.value">
						<ParameterInputFull
							display-options
							hide-label
							hide-issues
							hide-hint
							is-assignment
							:is-read-only="isReadOnly"
							:options-position="isStacked ? 'bottom' : 'top'"
							:parameter="valueParameter"
							:value="assignment.value"
							:path="`${path}.value`"
							data-test-id="assignment-value"
							@update="onAssignmentValueChange"
							@blur="onBlur"
							@drop="onValueDrop"
						/>
						<ParameterInputHint
							v-if="resolvedExpressionString"
							data-test-id="parameter-expression-preview-value"
							:class="[$style.hint]"
							:highlight="highlightHint"
							:hint="hint"
							single-line
						/>
					</div>
				</template>
			</InputTriple>
		</div>

		<div :class="$style.status">
			<ParameterIssues v-if="issues.length > 0" :issues="issues" />
		</div>
	</div>
</template>

<style lang="scss" module>
.wrapper {
	position: relative;
	display: flex;
	align-items: flex-end;
	gap: var(--spacing--4xs);

	&.hasIssues {
		--input--border-color: var(--color--danger);
	}

	&.hasHint {
		padding-bottom: var(--spacing--sm);
	}

	&:hover {
		.iconButton {
			opacity: 1;
		}
	}
}

.inputs {
	display: flex;
	flex-direction: column;
	flex-grow: 1;
	min-width: 0;

	> div {
		flex-grow: 1;
	}
}

.value {
	position: relative;

	.hint {
		position: absolute;
		bottom: calc(var(--spacing--sm) * -1);
		left: 0;
		right: 0;
		font-family: monospace;
	}
}

.iconButton {
	position: absolute;
	left: 0;
	opacity: 0;
	transition: opacity 100ms ease-in;
	color: var(--icon--color);
}
.extraTopPadding {
	top: calc(20px + var(--spacing--sm));
}

.defaultTopPadding {
	top: var(--spacing--sm);
}

.status {
	align-self: flex-start;
	padding-top: 28px;
}

.statusIcon {
	padding-left: var(--spacing--4xs);
}

.typeSelectWrapper {
	height: 100%;

	> :deep(span) {
		height: 100%;
	}
}
</style>
