<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';

import type {
	ICredentialsDecryptedResponse,
	ICredentialsResponse,
	IUpdateInformation,
} from '@/Interface';

import CredentialIcon from '@/components/CredentialIcon.vue';
import type {
	CredentialInformation,
	ICredentialDataDecryptedObject,
	ICredentialsDecrypted,
	INode,
	INodeParameters,
	INodeProperties,
	ITelemetryTrackProperties,
} from 'n8n-workflow';
import { NodeHelpers } from 'n8n-workflow';

import CredentialConfig from '@/components/CredentialEdit/CredentialConfig.vue';
import CredentialInfo from '@/components/CredentialEdit/CredentialInfo.vue';
import CredentialSharing from '@/components/CredentialEdit/CredentialSharing.ee.vue';
import InlineNameEdit from '@/components/InlineNameEdit.vue';
import Modal from '@/components/Modal.vue';
import SaveButton from '@/components/SaveButton.vue';
import { useMessage } from '@/composables/useMessage';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useToast } from '@/composables/useToast';
import { CREDENTIAL_EDIT_MODAL_KEY, EnterpriseEditionFeature, MODAL_CONFIRM } from '@/constants';
import { getResourcePermissions } from '@/permissions';
import { useCredentialsStore } from '@/stores/credentials.store';
import { useNDVStore } from '@/stores/ndv.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useSettingsStore } from '@/stores/settings.store';
import { useUIStore } from '@/stores/ui.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import type { Project, ProjectSharingData } from '@/types/projects.types';
import { assert } from '@n8n/utils/assert';
import type { IMenuItem } from '@n8n/design-system';
import { createEventBus } from '@n8n/utils/event-bus';

import { useExternalHooks } from '@/composables/useExternalHooks';
import { useI18n } from '@/composables/useI18n';
import { useTelemetry } from '@/composables/useTelemetry';
import { useProjectsStore } from '@/stores/projects.store';
import { isExpression, isTestableExpression } from '@/utils/expressions';
import {
	getNodeAuthOptions,
	getNodeCredentialForSelectedAuthType,
	updateNodeAuthType,
} from '@/utils/nodeTypesUtils';
import { isCredentialModalState, isValidCredentialResponse } from '@/utils/typeGuards';

type Props = {
	modalName: string;
	activeId?: string;
	mode?: 'new' | 'edit';
};

const props = withDefaults(defineProps<Props>(), { mode: 'new', activeId: undefined });

const credentialsStore = useCredentialsStore();
const ndvStore = useNDVStore();
const settingsStore = useSettingsStore();
const uiStore = useUIStore();
const workflowsStore = useWorkflowsStore();
const nodeTypesStore = useNodeTypesStore();
const projectsStore = useProjectsStore();

const nodeHelpers = useNodeHelpers();
const externalHooks = useExternalHooks();
const toast = useToast();
const message = useMessage();
const i18n = useI18n();
const telemetry = useTelemetry();

const activeTab = ref('connection');
const authError = ref('');
const credentialId = ref('');
const credentialName = ref('');
const selectedCredential = ref('');
const credentialData = ref<ICredentialDataDecryptedObject>({});
const currentCredential = ref<ICredentialsResponse | ICredentialsDecryptedResponse | null>(null);
const modalBus = ref(createEventBus());
const isDeleting = ref(false);
const isSaving = ref(false);
const isTesting = ref(false);
const hasUnsavedChanges = ref(false);
const isSaved = ref(false);
const loading = ref(false);
const showValidationWarning = ref(false);
const testedSuccessfully = ref(false);
const isRetesting = ref(false);
const hasUserSpecifiedName = ref(false);
const isSharedWithChanged = ref(false);
const requiredCredentials = ref(false); // Are credentials required or optional for the node
const contentRef = ref<HTMLDivElement>();

const activeNodeType = computed(() => {
	const activeNode = ndvStore.activeNode;

	if (activeNode) {
		return nodeTypesStore.getNodeType(activeNode.type, activeNode.typeVersion);
	}
	return null;
});

const selectedCredentialType = computed(() => {
	if (props.mode !== 'new') {
		return null;
	}

	// If there is already selected type, use it
	if (selectedCredential.value !== '') {
		return credentialsStore.getCredentialTypeByName(selectedCredential.value) ?? null;
	} else if (requiredCredentials.value) {
		// Otherwise, use credential type that corresponds to the first auth option in the node definition
		const nodeAuthOptions = getNodeAuthOptions(activeNodeType.value);
		// But only if there is zero or one auth options available
		if (nodeAuthOptions.length > 0 && activeNodeType.value?.credentials) {
			return getNodeCredentialForSelectedAuthType(activeNodeType.value, nodeAuthOptions[0].value);
		} else {
			return activeNodeType.value?.credentials ? activeNodeType.value.credentials[0] : null;
		}
	}

	return null;
});

const credentialType = computed(() => {
	if (!credentialTypeName.value) {
		return null;
	}

	const type = credentialsStore.getCredentialTypeByName(credentialTypeName.value);

	if (!type) {
		return null;
	}

	return {
		...type,
		properties: getCredentialProperties(credentialTypeName.value),
	};
});

const credentialTypeName = computed(() => {
	if (props.mode === 'edit') {
		if (currentCredential.value) {
			return currentCredential.value.type;
		}

		return null;
	}
	if (selectedCredentialType.value) {
		return selectedCredentialType.value.name;
	}
	return `${props.activeId}`;
});

const isEditingManagedCredential = computed(() => {
	if (!props.activeId) return false;
	return credentialsStore.getCredentialById(props.activeId)?.isManaged ?? false;
});

const isCredentialTestable = computed(() => {
	if (isOAuthType.value || !requiredPropertiesFilled.value) {
		return false;
	}

	const hasUntestableExpressions = Object.values(credentialData.value).reduce(
		(accu: boolean, value: CredentialInformation) =>
			accu || (typeof value === 'string' && isExpression(value) && !isTestableExpression(value)),
		false,
	);
	if (hasUntestableExpressions) {
		return false;
	}

	const nodesThatCanTest = nodesWithAccess.value.filter((node) => {
		if (node.credentials) {
			// Returns a list of nodes that can test this credentials
			const eligibleTesters = node.credentials.filter((credential) => {
				return credential.name === credentialTypeName.value && credential.testedBy;
			});
			// If we have any node that can test, return true.
			return !!eligibleTesters.length;
		}
		return false;
	});

	return !!nodesThatCanTest.length || (!!credentialType.value && !!credentialType.value.test);
});

const nodesWithAccess = computed(() => {
	if (credentialTypeName.value) {
		return credentialsStore.getNodesWithAccess(credentialTypeName.value);
	}

	return [];
});

const parentTypes = computed(() => {
	if (credentialTypeName.value) {
		return getParentTypes(credentialTypeName.value);
	}

	return [];
});

const isOAuthType = computed(() => {
	return (
		!!credentialTypeName.value &&
		(((credentialTypeName.value === 'oAuth2Api' || parentTypes.value.includes('oAuth2Api')) &&
			(credentialData.value.grantType === 'authorizationCode' ||
				credentialData.value.grantType === 'pkce')) ||
			credentialTypeName.value === 'oAuth1Api' ||
			parentTypes.value.includes('oAuth1Api'))
	);
});

const allOAuth2BasePropertiesOverridden = computed(() => {
	if (credentialType.value?.__overwrittenProperties) {
		return (
			credentialType.value.__overwrittenProperties.includes('clientId') &&
			credentialType.value.__overwrittenProperties.includes('clientSecret')
		);
	}
	return false;
});

const isOAuthConnected = computed(() => isOAuthType.value && !!credentialData.value.oauthTokenData);
const credentialProperties = computed(() => {
	const type = credentialType.value;
	if (!type) {
		return [];
	}

	const properties = type.properties.filter((propertyData: INodeProperties) => {
		if (!displayCredentialParameter(propertyData)) {
			return false;
		}
		return (
			!type.__overwrittenProperties || !type.__overwrittenProperties.includes(propertyData.name)
		);
	});

	/**
	 * If after all credentials overrides are applied only "notice"
	 * properties are left, do not return them. This will avoid
	 * showing notices that refer to a property that was overridden.
	 */
	if (properties.every((p) => p.type === 'notice')) {
		return [];
	}

	return properties;
});

const requiredPropertiesFilled = computed(() => {
	for (const property of credentialProperties.value) {
		if (property.required !== true) {
			continue;
		}

		const credentialProperty = credentialData.value[property.name];

		if (property.type === 'string' && !credentialProperty) {
			return false;
		}

		if (property.type === 'number') {
			const containsExpression =
				typeof credentialProperty === 'string' && credentialProperty.startsWith('=');

			if (typeof credentialProperty !== 'number' && !containsExpression) {
				return false;
			}
		}
	}
	return true;
});

const credentialPermissions = computed(() => {
	return getResourcePermissions(
		(currentCredential.value as ICredentialsResponse)?.scopes ?? homeProject.value?.scopes,
	).credential;
});

const sidebarItems = computed(() => {
	const menuItems: IMenuItem[] = [
		{
			id: 'connection',
			label: i18n.baseText('credentialEdit.credentialEdit.connection'),
			position: 'top',
		},
		{
			id: 'sharing',
			label: i18n.baseText('credentialEdit.credentialEdit.sharing'),
			position: 'top',
		},
		{
			id: 'details',
			label: i18n.baseText('credentialEdit.credentialEdit.details'),
			position: 'top',
		},
	];

	return menuItems;
});

const defaultCredentialTypeName = computed(() => {
	let defaultName = credentialTypeName.value;
	if (!defaultName || defaultName === 'null') {
		if (activeNodeType.value?.credentials && activeNodeType.value.credentials.length > 0) {
			defaultName = activeNodeType.value.credentials[0].name;
		}
	}
	return defaultName ?? '';
});

const showSaveButton = computed(() => {
	return (
		(props.mode === 'new' || hasUnsavedChanges.value || isSaved.value) &&
		(credentialPermissions.value.create ?? credentialPermissions.value.update)
	);
});

const showSharingContent = computed(() => activeTab.value === 'sharing' && !!credentialType.value);

const homeProject = computed(() => {
	const { currentProject, personalProject } = projectsStore;
	return currentProject ?? personalProject;
});

onMounted(async () => {
	requiredCredentials.value =
		isCredentialModalState(uiStore.modalsById[CREDENTIAL_EDIT_MODAL_KEY]) &&
		uiStore.modalsById[CREDENTIAL_EDIT_MODAL_KEY].showAuthSelector === true;

	if (props.mode === 'new' && credentialTypeName.value) {
		credentialName.value = await credentialsStore.getNewCredentialName({
			credentialTypeName: defaultCredentialTypeName.value,
		});

		credentialData.value = {
			...credentialData.value,
			...(homeProject.value ? { homeProject: homeProject.value } : {}),
		};
	} else {
		await loadCurrentCredential();
	}

	if (credentialType.value) {
		for (const property of credentialType.value.properties) {
			if (
				!credentialData.value.hasOwnProperty(property.name) &&
				!credentialType.value.__overwrittenProperties?.includes(property.name)
			) {
				credentialData.value = {
					...credentialData.value,
					[property.name]: property.default as CredentialInformation,
				};
			}
		}
	}

	await externalHooks.run('credentialsEdit.credentialModalOpened', {
		credentialType: credentialTypeName.value,
		isEditingCredential: props.mode === 'edit',
		activeNode: ndvStore.activeNode,
	});

	setTimeout(async () => {
		if (credentialId.value) {
			if (!requiredPropertiesFilled.value && credentialPermissions.value.update) {
				// sharees can't see properties, so this check would always fail for them
				// if the credential contains required fields.
				showValidationWarning.value = true;
			} else {
				await retestCredential();
			}
		}
	}, 0);

	loading.value = false;
});

async function beforeClose() {
	let keepEditing = false;

	if (hasUnsavedChanges.value) {
		const displayName = credentialType.value ? credentialType.value.displayName : '';
		const confirmAction = await message.confirm(
			i18n.baseText('credentialEdit.credentialEdit.confirmMessage.beforeClose1.message', {
				interpolate: { credentialDisplayName: displayName },
			}),
			i18n.baseText('credentialEdit.credentialEdit.confirmMessage.beforeClose1.headline'),
			{
				cancelButtonText: i18n.baseText(
					'credentialEdit.credentialEdit.confirmMessage.beforeClose1.cancelButtonText',
				),
				confirmButtonText: i18n.baseText(
					'credentialEdit.credentialEdit.confirmMessage.beforeClose1.confirmButtonText',
				),
			},
		);
		keepEditing = confirmAction === MODAL_CONFIRM;
	} else if (credentialPermissions.value.update && isOAuthType.value && !isOAuthConnected.value) {
		const confirmAction = await message.confirm(
			i18n.baseText('credentialEdit.credentialEdit.confirmMessage.beforeClose2.message'),
			i18n.baseText('credentialEdit.credentialEdit.confirmMessage.beforeClose2.headline'),
			{
				cancelButtonText: i18n.baseText(
					'credentialEdit.credentialEdit.confirmMessage.beforeClose2.cancelButtonText',
				),
				confirmButtonText: i18n.baseText(
					'credentialEdit.credentialEdit.confirmMessage.beforeClose2.confirmButtonText',
				),
			},
		);
		keepEditing = confirmAction === MODAL_CONFIRM;
	}

	if (!keepEditing) {
		uiStore.activeCredentialType = null;
		return true;
	} else if (!requiredPropertiesFilled.value) {
		showValidationWarning.value = true;
		scrollToTop();
	} else if (isOAuthType.value) {
		scrollToBottom();
	}

	return false;
}

function displayCredentialParameter(parameter: INodeProperties): boolean {
	if (parameter.type === 'hidden') {
		return false;
	}

	if (parameter.displayOptions?.hideOnCloud && settingsStore.isCloudDeployment) {
		return false;
	}

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

	return nodeHelpers.displayParameter(credentialData.value as INodeParameters, parameter, '', null);
}

function getCredentialProperties(name: string): INodeProperties[] {
	const credentialTypeData = credentialsStore.getCredentialTypeByName(name);

	if (!credentialTypeData) {
		return [];
	}

	if (credentialTypeData.extends === undefined) {
		return credentialTypeData.properties;
	}

	const combineProperties = [] as INodeProperties[];
	for (const credentialsTypeName of credentialTypeData.extends) {
		const mergeCredentialProperties = getCredentialProperties(credentialsTypeName);
		NodeHelpers.mergeNodeProperties(combineProperties, mergeCredentialProperties);
	}

	// The properties defined on the parent credentials take precedence
	NodeHelpers.mergeNodeProperties(combineProperties, credentialTypeData.properties);

	return combineProperties;
}

async function loadCurrentCredential() {
	credentialId.value = props.activeId ?? '';

	try {
		const currentCredentials = await credentialsStore.getCredentialData({
			id: credentialId.value,
		});

		if (!currentCredentials) {
			throw new Error(
				i18n.baseText('credentialEdit.credentialEdit.couldNotFindCredentialWithId') +
					':' +
					credentialId.value,
			);
		}

		currentCredential.value = currentCredentials;

		credentialData.value = (currentCredentials.data as ICredentialDataDecryptedObject) || {};
		if (currentCredentials.sharedWithProjects) {
			credentialData.value = {
				...credentialData.value,
				sharedWithProjects: currentCredentials.sharedWithProjects,
			};
		}
		if (currentCredentials.homeProject) {
			credentialData.value = {
				...credentialData.value,
				homeProject: currentCredentials.homeProject,
			};
		}

		credentialName.value = currentCredentials.name;
	} catch (error) {
		toast.showError(
			error,
			i18n.baseText('credentialEdit.credentialEdit.showError.loadCredential.title'),
		);
		closeDialog();

		return;
	}
}

function onTabSelect(tab: string) {
	activeTab.value = tab;
	const credType: string = credentialType.value ? credentialType.value.name : '';
	const activeNode: INode | null = ndvStore.activeNode;

	telemetry.track('User viewed credential tab', {
		credential_type: credType,
		node_type: activeNode ? activeNode.type : null,
		tab,
		workflow_id: workflowsStore.workflowId,
		credential_id: credentialId.value,
		sharing_enabled: EnterpriseEditionFeature.Sharing,
	});
}

function onChangeSharedWith(sharedWithProjects: ProjectSharingData[]) {
	credentialData.value = {
		...credentialData.value,
		sharedWithProjects,
	};
	isSharedWithChanged.value = true;
	hasUnsavedChanges.value = true;
}

function onDataChange({ name, value }: IUpdateInformation) {
	// skip update if new value matches the current
	if (credentialData.value[name] === value) return;

	hasUnsavedChanges.value = true;

	const { oauthTokenData, ...credData } = credentialData.value;

	credentialData.value = {
		...credData,
		[name]: value as CredentialInformation,
	};
}

function closeDialog() {
	modalBus.value.emit('close');
}

function getParentTypes(name: string): string[] {
	const type = credentialsStore.getCredentialTypeByName(name);

	if (type?.extends === undefined) {
		return [];
	}

	const types: string[] = [];
	for (const typeName of type.extends) {
		types.push(typeName);
		types.push.apply(types, getParentTypes(typeName)); // eslint-disable-line prefer-spread
	}

	return types;
}

function onNameEdit(text: string) {
	hasUnsavedChanges.value = true;
	hasUserSpecifiedName.value = true;
	credentialName.value = text;
}

function scrollToTop() {
	setTimeout(() => {
		if (contentRef.value) {
			contentRef.value.scrollTop = 0;
		}
	}, 0);
}

function scrollToBottom() {
	setTimeout(() => {
		if (contentRef.value) {
			contentRef.value.scrollTop = contentRef.value.scrollHeight;
		}
	}, 0);
}

async function retestCredential() {
	if (isEditingManagedCredential.value) {
		return;
	}

	if (!isCredentialTestable.value || !credentialTypeName.value) {
		authError.value = '';
		testedSuccessfully.value = false;

		return;
	}

	const { ownedBy, sharedWithProjects, ...otherCredData } = credentialData.value;
	const details: ICredentialsDecrypted = {
		id: credentialId.value,
		name: credentialName.value,
		type: credentialTypeName.value,
		data: otherCredData,
	};

	isRetesting.value = true;
	await testCredential(details);
	isRetesting.value = false;
}

async function testCredential(credentialDetails: ICredentialsDecrypted) {
	const result = await credentialsStore.testCredential(credentialDetails);
	if (result.status === 'Error') {
		authError.value = result.message;
		testedSuccessfully.value = false;
	} else {
		authError.value = '';
		testedSuccessfully.value = true;
	}

	scrollToTop();
}

function usesExternalSecrets(data: Record<string, unknown>): boolean {
	return Object.entries(data).some(
		([, value]) => typeof value !== 'object' && /=.*\{\{[^}]*\$secrets\.[^}]+}}.*/.test(`${value}`),
	);
}

async function saveCredential(): Promise<ICredentialsResponse | null> {
	if (!requiredPropertiesFilled.value) {
		showValidationWarning.value = true;
		scrollToTop();
	} else {
		showValidationWarning.value = false;
	}

	isSaving.value = true;

	// Save only the none default data
	assert(credentialType.value);
	const data = NodeHelpers.getNodeParameters(
		credentialType.value.properties,
		credentialData.value as INodeParameters,
		false,
		false,
		null,
	);

	assert(credentialTypeName.value);
	const credentialDetails: ICredentialsDecrypted = {
		id: credentialId.value,
		name: credentialName.value,
		type: credentialTypeName.value,
		data: data as unknown as ICredentialDataDecryptedObject,
	};

	if (
		settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Sharing] &&
		credentialData.value.sharedWithProjects
	) {
		credentialDetails.sharedWithProjects = credentialData.value
			.sharedWithProjects as ProjectSharingData[];
	}

	if (credentialData.value.homeProject) {
		credentialDetails.homeProject = credentialData.value.homeProject as ProjectSharingData;
	}

	let credential: ICredentialsResponse | null = null;

	const isNewCredential = props.mode === 'new' && !credentialId.value;

	if (isNewCredential) {
		credential = await createCredential(credentialDetails, projectsStore.currentProject);
	} else {
		if (settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Sharing]) {
			credentialDetails.sharedWithProjects = credentialData.value
				.sharedWithProjects as ProjectSharingData[];
		}

		credential = await updateCredential(credentialDetails);
	}

	isSaving.value = false;
	if (credential) {
		credentialId.value = credential.id;
		currentCredential.value = credential;

		if (isCredentialTestable.value) {
			isTesting.value = true;
			// Add the full data including defaults for testing
			credentialDetails.data = credentialData.value;

			credentialDetails.id = credentialId.value;

			await testCredential(credentialDetails);
			isTesting.value = false;
		} else {
			authError.value = '';
			testedSuccessfully.value = false;
		}

		const trackProperties: ITelemetryTrackProperties = {
			credential_type: credentialDetails.type,
			workflow_id: workflowsStore.workflowId,
			credential_id: credential.id,
			is_complete: !!requiredPropertiesFilled.value,
			is_new: isNewCredential,
			uses_external_secrets: usesExternalSecrets(credentialDetails.data ?? {}),
		};

		if (isOAuthType.value) {
			trackProperties.is_valid = !!isOAuthConnected.value;
		} else if (isCredentialTestable.value) {
			trackProperties.is_valid = !!testedSuccessfully.value;
		}

		if (ndvStore.activeNode) {
			trackProperties.node_type = ndvStore.activeNode.type;
		}

		if (authError.value && authError.value !== '') {
			trackProperties.authError = authError.value;
		}

		/**
		 * For non-OAuth credentials we track saving on clicking the `Save` button, but for
		 * OAuth credentials we track saving at the end of the flow (BroastcastChannel event)
		 * so that the `is_valid` property is correct.
		 */
		if (!isOAuthType.value) {
			telemetry.track('User saved credentials', trackProperties);
		}

		await externalHooks.run('credentialEdit.saveCredential', trackProperties);
	}

	return credential;
}

const createToastMessagingForNewCredentials = (
	credentialDetails: ICredentialsDecrypted,
	project?: Project | null,
) => {
	let toastTitle = i18n.baseText('credentials.create.personal.toast.title');
	let toastText = '';

	if (!credentialDetails.sharedWithProjects) {
		toastText = i18n.baseText('credentials.create.personal.toast.text');
	}

	if (
		projectsStore.currentProject &&
		projectsStore.currentProject.id !== projectsStore.personalProject?.id
	) {
		toastTitle = i18n.baseText('credentials.create.project.toast.title', {
			interpolate: { projectName: project?.name ?? '' },
		});

		toastText = i18n.baseText('credentials.create.project.toast.text', {
			interpolate: { projectName: project?.name ?? '' },
		});
	}

	return {
		title: toastTitle,
		message: toastText,
	};
};

async function createCredential(
	credentialDetails: ICredentialsDecrypted,
	project?: Project | null,
): Promise<ICredentialsResponse | null> {
	let credential;

	try {
		credential = await credentialsStore.createNewCredential(credentialDetails, project?.id);
		hasUnsavedChanges.value = false;

		const { title, message } = createToastMessagingForNewCredentials(credentialDetails, project);

		toast.showMessage({
			title,
			message,
			type: 'success',
		});
	} catch (error) {
		toast.showError(
			error,
			i18n.baseText('credentialEdit.credentialEdit.showError.createCredential.title'),
		);

		return null;
	}

	await externalHooks.run('credential.saved', {
		credential_type: credentialDetails.type,
		credential_id: credential.id,
		is_new: true,
	});

	telemetry.track('User created credentials', {
		credential_type: credentialDetails.type,
		credential_id: credential.id,
		workflow_id: workflowsStore.workflowId,
	});

	return credential;
}

async function updateCredential(
	credentialDetails: ICredentialsDecrypted,
): Promise<ICredentialsResponse | null> {
	let credential: ICredentialsResponse | null = null;
	try {
		if (credentialPermissions.value.update) {
			credential = await credentialsStore.updateCredential({
				id: credentialId.value,
				data: credentialDetails,
			});
		}
		if (
			credentialPermissions.value.share &&
			isSharedWithChanged.value &&
			credentialDetails.sharedWithProjects
		) {
			credential = await credentialsStore.setCredentialSharedWith({
				credentialId: credentialDetails.id,
				sharedWithProjects: credentialDetails.sharedWithProjects,
			});
			isSharedWithChanged.value = false;
		}
		hasUnsavedChanges.value = false;
		isSaved.value = true;

		if (credential) {
			await externalHooks.run('credential.saved', {
				credential_type: credentialDetails.type,
				credential_id: credential.id,
				is_new: false,
			});
		}
	} catch (error) {
		toast.showError(
			error,
			i18n.baseText('credentialEdit.credentialEdit.showError.updateCredential.title'),
		);

		return null;
	}

	// Now that the credentials changed check if any nodes use credentials
	// which have now a different name
	nodeHelpers.updateNodesCredentialsIssues();

	return credential;
}

async function deleteCredential() {
	if (!currentCredential.value) {
		return;
	}

	const savedCredentialName = currentCredential.value.name;

	const deleteConfirmed = await message.confirm(
		i18n.baseText('credentialEdit.credentialEdit.confirmMessage.deleteCredential.message', {
			interpolate: { savedCredentialName },
		}),
		i18n.baseText('credentialEdit.credentialEdit.confirmMessage.deleteCredential.headline'),
		{
			confirmButtonText: i18n.baseText(
				'credentialEdit.credentialEdit.confirmMessage.deleteCredential.confirmButtonText',
			),
		},
	);

	if (deleteConfirmed !== MODAL_CONFIRM) {
		return;
	}

	try {
		isDeleting.value = true;
		await credentialsStore.deleteCredential({ id: credentialId.value });
		hasUnsavedChanges.value = false;
		isSaved.value = true;
	} catch (error) {
		toast.showError(
			error,
			i18n.baseText('credentialEdit.credentialEdit.showError.deleteCredential.title'),
		);
		isDeleting.value = false;

		return;
	}

	isDeleting.value = false;
	// Now that the credentials were removed check if any nodes used them
	nodeHelpers.updateNodesCredentialsIssues();
	credentialData.value = {};

	toast.showMessage({
		title: i18n.baseText('credentialEdit.credentialEdit.showMessage.title'),
		type: 'success',
	});
	closeDialog();
}

async function oAuthCredentialAuthorize() {
	let url;

	const credential = await saveCredential();
	if (!credential) {
		return;
	}

	const types = parentTypes.value;

	try {
		const credData = { id: credential.id, ...credentialData.value };
		if (credentialTypeName.value === 'oAuth2Api' || types.includes('oAuth2Api')) {
			if (isValidCredentialResponse(credData)) {
				url = await credentialsStore.oAuth2Authorize(credData);
			}
		} else if (credentialTypeName.value === 'oAuth1Api' || types.includes('oAuth1Api')) {
			if (isValidCredentialResponse(credData)) {
				url = await credentialsStore.oAuth1Authorize(credData);
			}
		}
	} catch (error) {
		toast.showError(
			error,
			i18n.baseText('credentialEdit.credentialEdit.showError.generateAuthorizationUrl.title'),
			i18n.baseText('credentialEdit.credentialEdit.showError.generateAuthorizationUrl.message'),
		);

		return;
	}

	const params =
		'scrollbars=no,resizable=yes,status=no,titlebar=noe,location=no,toolbar=no,menubar=no,width=500,height=700';
	const oauthPopup = window.open(url, 'OAuth Authorization', params);

	credentialData.value = {
		...credentialData.value,
		oauthTokenData: null as unknown as CredentialInformation,
	};

	const oauthChannel = new BroadcastChannel('oauth-callback');
	const receiveMessage = (event: MessageEvent) => {
		const successfullyConnected = event.data === 'success';

		const trackProperties: ITelemetryTrackProperties = {
			credential_type: credentialTypeName.value,
			workflow_id: workflowsStore.workflowId,
			credential_id: credentialId.value,
			is_complete: !!requiredPropertiesFilled.value,
			is_new: props.mode === 'new' && !credentialId.value,
			is_valid: successfullyConnected,
			uses_external_secrets: usesExternalSecrets(credentialData.value),
		};

		if (ndvStore.activeNode) {
			trackProperties.node_type = ndvStore.activeNode.type;
		}

		telemetry.track('User saved credentials', trackProperties);

		if (successfullyConnected) {
			oauthChannel.removeEventListener('message', receiveMessage);

			// Set some kind of data that status changes.
			// As data does not get displayed directly it does not matter what data.
			credentialData.value = {
				...credentialData.value,
				oauthTokenData: {} as CredentialInformation,
			};

			// Close the window
			if (oauthPopup) {
				oauthPopup.close();
			}
		}
	};
	oauthChannel.addEventListener('message', receiveMessage);
}

async function onAuthTypeChanged(type: string): Promise<void> {
	if (!activeNodeType.value?.credentials) {
		return;
	}
	const credentialsForType = getNodeCredentialForSelectedAuthType(activeNodeType.value, type);
	if (credentialsForType) {
		selectedCredential.value = credentialsForType.name;
		uiStore.activeCredentialType = credentialsForType.name;
		resetCredentialData();
		// Update current node auth type so credentials dropdown can be displayed properly
		updateNodeAuthType(ndvStore.activeNode, type);
		// Also update credential name but only if the default name is still used
		if (hasUnsavedChanges.value && !hasUserSpecifiedName.value) {
			const newDefaultName = await credentialsStore.getNewCredentialName({
				credentialTypeName: defaultCredentialTypeName.value,
			});
			credentialName.value = newDefaultName;
		}
	}
}

function resetCredentialData(): void {
	if (!credentialType.value) {
		return;
	}
	for (const property of credentialType.value.properties) {
		if (!credentialType.value.__overwrittenProperties?.includes(property.name)) {
			credentialData.value = {
				...credentialData.value,
				[property.name]: property.default as CredentialInformation,
			};
		}
	}

	const { currentProject, personalProject } = projectsStore;
	const scopes = currentProject?.scopes ?? personalProject?.scopes ?? [];
	const homeProject = currentProject ?? personalProject ?? {};

	credentialData.value = {
		...credentialData.value,
		scopes: scopes as unknown as CredentialInformation,
		homeProject,
	};
}
</script>

<template>
	<Modal
		:name="modalName"
		:custom-class="$style.credentialModal"
		:event-bus="modalBus"
		:loading="loading"
		:before-close="beforeClose"
		width="70%"
		height="80%"
	>
		<template #header>
			<div :class="$style.header">
				<div :class="$style.credInfo">
					<div :class="$style.credIcon">
						<CredentialIcon :credential-type-name="defaultCredentialTypeName" />
					</div>
					<InlineNameEdit
						:model-value="credentialName"
						:subtitle="credentialType ? credentialType.displayName : ''"
						:readonly="
							!credentialPermissions.update || !credentialType || isEditingManagedCredential
						"
						type="Credential"
						data-test-id="credential-name"
						@update:model-value="onNameEdit"
					/>
				</div>
				<div :class="$style.credActions">
					<n8n-icon-button
						v-if="currentCredential && credentialPermissions.delete"
						:title="i18n.baseText('credentialEdit.credentialEdit.delete')"
						icon="trash"
						type="tertiary"
						:disabled="isSaving"
						:loading="isDeleting"
						data-test-id="credential-delete-button"
						@click="deleteCredential"
					/>
					<SaveButton
						v-if="showSaveButton"
						:saved="!hasUnsavedChanges && !isTesting && !!credentialId"
						:is-saving="isSaving || isTesting"
						:saving-label="
							isTesting
								? i18n.baseText('credentialEdit.credentialEdit.testing')
								: i18n.baseText('credentialEdit.credentialEdit.saving')
						"
						data-test-id="credential-save-button"
						@click="saveCredential"
					/>
				</div>
			</div>
		</template>
		<template #content>
			<div :class="$style.container" data-test-id="credential-edit-dialog">
				<div v-if="!isEditingManagedCredential" :class="$style.sidebar">
					<n8n-menu
						mode="tabs"
						:items="sidebarItems"
						:transparent-background="true"
						@select="onTabSelect"
					></n8n-menu>
				</div>
				<div
					v-if="activeTab === 'connection' && credentialType"
					ref="contentRef"
					:class="$style.mainContent"
				>
					<CredentialConfig
						:credential-type="credentialType"
						:credential-properties="credentialProperties"
						:credential-data="credentialData"
						:credential-id="credentialId"
						:is-managed="isEditingManagedCredential"
						:show-validation-warning="showValidationWarning"
						:auth-error="authError"
						:tested-successfully="testedSuccessfully"
						:is-o-auth-type="isOAuthType"
						:is-o-auth-connected="isOAuthConnected"
						:is-retesting="isRetesting"
						:parent-types="parentTypes"
						:required-properties-filled="requiredPropertiesFilled"
						:credential-permissions="credentialPermissions"
						:all-o-auth2-base-properties-overridden="allOAuth2BasePropertiesOverridden"
						:mode="mode"
						:selected-credential="selectedCredential"
						:show-auth-type-selector="requiredCredentials"
						@update="onDataChange"
						@oauth="oAuthCredentialAuthorize"
						@retest="retestCredential"
						@scroll-to-top="scrollToTop"
						@auth-type-changed="onAuthTypeChanged"
					/>
				</div>
				<div v-else-if="showSharingContent" :class="$style.mainContent">
					<CredentialSharing
						:credential="currentCredential"
						:credential-data="credentialData"
						:credential-id="credentialId"
						:credential-permissions="credentialPermissions"
						:modal-bus="modalBus"
						@update:model-value="onChangeSharedWith"
					/>
				</div>
				<div v-else-if="activeTab === 'details' && credentialType" :class="$style.mainContent">
					<CredentialInfo :current-credential="currentCredential" />
				</div>
			</div>
		</template>
	</Modal>
</template>

<style module lang="scss">
.credentialModal {
	--dialog-max-width: 1200px;
	--dialog-close-top: 31px;
	--dialog-max-height: 750px;

	:global(.el-dialog__header) {
		padding-bottom: 0;
		border-bottom: var(--border-base);
	}

	:global(.el-dialog__body) {
		padding-top: var(--spacing-l);
		position: relative;
	}
}

.mainContent {
	flex: 1;
	overflow: auto;
	padding-bottom: 100px;
}

.sidebar {
	max-width: 170px;
	min-width: 170px;
	margin-right: var(--spacing-l);
	flex-grow: 1;

	ul {
		padding: 0 !important;
	}
}

.header {
	display: flex;
}

.container {
	display: flex;
	height: 100%;
}

.credInfo {
	display: flex;
	align-items: center;
	flex-direction: row;
	flex-grow: 1;
	margin-bottom: var(--spacing-l);
}

.credActions {
	display: flex;
	flex-direction: row;
	align-items: center;
	margin-right: var(--spacing-xl);
	margin-bottom: var(--spacing-l);

	> * {
		margin-left: var(--spacing-2xs);
	}
}

.credIcon {
	display: flex;
	align-items: center;
	margin-right: var(--spacing-xs);
}
</style>
