import type {
	ICredentialMap,
	ICredentialsDecryptedResponse,
	ICredentialsResponse,
	ICredentialsState,
	ICredentialTypeMap,
	INodeUi,
	IUsedCredential,
} from '@/Interface';
import * as credentialsApi from '@/api/credentials';
import * as credentialsEeApi from '@/api/credentials.ee';
import { EnterpriseEditionFeature, STORES } from '@/constants';
import { i18n } from '@/plugins/i18n';
import type { ProjectSharingData } from '@/types/projects.types';
import { makeRestApiRequest } from '@/utils/apiUtils';
import { getAppNameFromCredType } from '@/utils/nodeTypesUtils';
import { splitName } from '@/utils/projects.utils';
import { isEmpty, isPresent } from '@/utils/typesUtils';
import type {
	ICredentialsDecrypted,
	ICredentialType,
	INodeCredentialTestResult,
} from 'n8n-workflow';
import { defineStore } from 'pinia';
import { computed, ref } from 'vue';
import { useNodeTypesStore } from './nodeTypes.store';
import { useRootStore } from './root.store';
import { useSettingsStore } from './settings.store';

const DEFAULT_CREDENTIAL_NAME = 'Unnamed credential';
const DEFAULT_CREDENTIAL_POSTFIX = 'account';
const TYPES_WITH_DEFAULT_NAME = ['httpBasicAuth', 'oAuth2Api', 'httpDigestAuth', 'oAuth1Api'];

export type CredentialsStore = ReturnType<typeof useCredentialsStore>;

export const useCredentialsStore = defineStore(STORES.CREDENTIALS, () => {
	const state = ref<ICredentialsState>({ credentialTypes: {}, credentials: {} });

	// ---------------------------------------------------------------------------
	// #region Computed
	// ---------------------------------------------------------------------------

	const credentialTypesById = computed(() => {
		return state.value.credentialTypes;
	});

	const allCredentialTypes = computed(() => {
		return Object.values(state.value.credentialTypes).sort((a, b) =>
			a.displayName.localeCompare(b.displayName),
		);
	});

	const allCredentials = computed(() => {
		return Object.values(state.value.credentials).sort((a, b) => a.name.localeCompare(b.name));
	});

	const allCredentialsByType = computed(() => {
		const credentials = allCredentials.value;
		const types = allCredentialTypes.value;
		return types.reduce(
			(accu: { [type: string]: ICredentialsResponse[] }, type: ICredentialType) => {
				accu[type.name] = credentials.filter(
					(cred: ICredentialsResponse) => cred.type === type.name,
				);
				return accu;
			},
			{},
		);
	});

	const allUsableCredentialsByType = computed(() => {
		const credentials = allCredentials.value;
		const types = allCredentialTypes.value;

		return types.reduce(
			(accu: { [type: string]: ICredentialsResponse[] }, type: ICredentialType) => {
				accu[type.name] = credentials.filter((cred: ICredentialsResponse) => {
					return cred.type === type.name;
				});

				return accu;
			},
			{},
		);
	});

	const allUsableCredentialsForNode = computed(() => {
		return (node: INodeUi): ICredentialsResponse[] => {
			let credentials: ICredentialsResponse[] = [];
			const nodeType = useNodeTypesStore().getNodeType(node.type, node.typeVersion);
			if (nodeType?.credentials) {
				nodeType.credentials.forEach((cred) => {
					credentials = credentials.concat(allUsableCredentialsByType.value[cred.name]);
				});
			}
			return credentials.sort((a, b) => {
				const aDate = new Date(a.updatedAt);
				const bDate = new Date(b.updatedAt);
				return aDate.getTime() - bDate.getTime();
			});
		};
	});

	const getCredentialTypeByName = computed(() => {
		return (type: string): ICredentialType | undefined => state.value.credentialTypes[type];
	});

	const getCredentialById = computed(() => {
		return (id: string): ICredentialsResponse => state.value.credentials[id];
	});

	const getCredentialByIdAndType = computed(() => {
		return (id: string, type: string): ICredentialsResponse | undefined => {
			const credential = state.value.credentials[id];
			return !credential || credential.type !== type ? undefined : credential;
		};
	});

	const getCredentialsByType = computed(() => {
		return (credentialType: string): ICredentialsResponse[] => {
			return allCredentialsByType.value[credentialType] || [];
		};
	});

	const getUsableCredentialByType = computed(() => {
		return (credentialType: string): ICredentialsResponse[] => {
			return allUsableCredentialsByType.value[credentialType] || [];
		};
	});

	const getNodesWithAccess = computed(() => {
		return (credentialTypeName: string) => {
			const credentialType = getCredentialTypeByName.value(credentialTypeName);
			if (!credentialType) {
				return [];
			}
			const nodeTypesStore = useNodeTypesStore();

			return (credentialType.supportedNodes ?? [])
				.map((nodeType) => nodeTypesStore.getNodeType(nodeType))
				.filter(isPresent);
		};
	});

	const getScopesByCredentialType = computed(() => {
		return (credentialTypeName: string) => {
			const credentialType = getCredentialTypeByName.value(credentialTypeName);
			if (!credentialType) {
				return [];
			}

			const scopeProperty = credentialType.properties.find((p) => p.name === 'scope');

			if (
				!scopeProperty ||
				!scopeProperty.default ||
				typeof scopeProperty.default !== 'string' ||
				scopeProperty.default === ''
			) {
				return [];
			}

			let { default: scopeDefault } = scopeProperty;

			// disregard expressions for display
			scopeDefault = scopeDefault.replace(/^=/, '').replace(/\{\{.*\}\}/, '');

			if (/ /.test(scopeDefault)) return scopeDefault.split(' ');

			if (/,/.test(scopeDefault)) return scopeDefault.split(',');

			return [scopeDefault];
		};
	});

	const getCredentialOwnerName = computed(() => {
		return (credential: ICredentialsResponse | IUsedCredential | undefined): string => {
			const { name, email } = splitName(credential?.homeProject?.name ?? '');

			return name
				? email
					? `${name} (${email})`
					: name
				: (email ?? i18n.baseText('credentialEdit.credentialSharing.info.sharee.fallback'));
		};
	});

	const getCredentialOwnerNameById = computed(() => {
		return (credentialId: string): string => {
			const credential = getCredentialById.value(credentialId);

			return getCredentialOwnerName.value(credential);
		};
	});

	const httpOnlyCredentialTypes = computed(() => {
		return allCredentialTypes.value.filter(
			(credentialType) => credentialType.httpRequestNode && !credentialType.httpRequestNode.hidden,
		);
	});

	// #endregion

	// ---------------------------------------------------------------------------
	// #region Methods
	// ---------------------------------------------------------------------------

	const setCredentialTypes = (credentialTypes: ICredentialType[]) => {
		state.value.credentialTypes = credentialTypes.reduce(
			(accu: ICredentialTypeMap, cred: ICredentialType) => {
				accu[cred.name] = cred;

				return accu;
			},
			{},
		);
	};

	const addCredentials = (credentials: ICredentialsResponse[]) => {
		credentials.forEach((cred: ICredentialsResponse) => {
			if (cred.id) {
				state.value.credentials[cred.id] = { ...state.value.credentials[cred.id], ...cred };
			}
		});
	};

	const setCredentials = (credentials: ICredentialsResponse[]) => {
		state.value.credentials = credentials.reduce(
			(accu: ICredentialMap, cred: ICredentialsResponse) => {
				if (cred.id) {
					accu[cred.id] = cred;
				}
				return accu;
			},
			{},
		);
	};

	const upsertCredential = (credential: ICredentialsResponse) => {
		if (credential.id) {
			state.value.credentials = {
				...state.value.credentials,
				[credential.id]: {
					...state.value.credentials[credential.id],
					...credential,
				},
			};
		}
	};

	const fetchCredentialTypes = async (forceFetch: boolean) => {
		if (allCredentialTypes.value.length > 0 && !forceFetch) {
			return;
		}
		const rootStore = useRootStore();
		const credentialTypes = await credentialsApi.getCredentialTypes(rootStore.baseUrl);
		setCredentialTypes(credentialTypes);
	};

	const fetchAllCredentials = async (
		projectId?: string,
		includeScopes = true,
	): Promise<ICredentialsResponse[]> => {
		const rootStore = useRootStore();

		const filter = {
			projectId,
		};

		const credentials = await credentialsApi.getAllCredentials(
			rootStore.restApiContext,
			isEmpty(filter) ? undefined : filter,
			includeScopes,
		);
		setCredentials(credentials);
		return credentials;
	};

	const fetchAllCredentialsForWorkflow = async (
		options: { workflowId: string } | { projectId: string },
	): Promise<ICredentialsResponse[]> => {
		const rootStore = useRootStore();

		const credentials = await credentialsApi.getAllCredentialsForWorkflow(
			rootStore.restApiContext,
			options,
		);
		setCredentials(credentials);
		return credentials;
	};

	const getCredentialData = async ({
		id,
	}: {
		id: string;
	}): Promise<ICredentialsResponse | ICredentialsDecryptedResponse | undefined> => {
		const rootStore = useRootStore();
		return await credentialsApi.getCredentialData(rootStore.restApiContext, id);
	};

	const createNewCredential = async (
		data: ICredentialsDecrypted,
		projectId?: string,
	): Promise<ICredentialsResponse> => {
		const rootStore = useRootStore();
		const settingsStore = useSettingsStore();
		const credential = await credentialsApi.createNewCredential(
			rootStore.restApiContext,
			data,
			projectId,
		);

		if (data?.homeProject && !credential.homeProject) {
			credential.homeProject = data.homeProject as ProjectSharingData;
		}

		if (settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Sharing]) {
			upsertCredential(credential);
			if (data.sharedWithProjects) {
				await setCredentialSharedWith({
					credentialId: credential.id,
					sharedWithProjects: data.sharedWithProjects,
				});
			}
		} else {
			upsertCredential(credential);
		}
		return credential;
	};

	const updateCredential = async (params: {
		data: ICredentialsDecrypted;
		id: string;
	}): Promise<ICredentialsResponse> => {
		const { id, data } = params;
		const rootStore = useRootStore();
		const credential = await credentialsApi.updateCredential(rootStore.restApiContext, id, data);

		upsertCredential(credential);

		return credential;
	};

	const deleteCredential = async ({ id }: { id: string }) => {
		const rootStore = useRootStore();
		const deleted = await credentialsApi.deleteCredential(rootStore.restApiContext, id);
		if (deleted) {
			const { [id]: deletedCredential, ...rest } = state.value.credentials;
			state.value.credentials = rest;
		}
	};

	const oAuth2Authorize = async (data: ICredentialsResponse): Promise<string> => {
		const rootStore = useRootStore();
		return await credentialsApi.oAuth2CredentialAuthorize(rootStore.restApiContext, data);
	};

	const oAuth1Authorize = async (data: ICredentialsResponse): Promise<string> => {
		const rootStore = useRootStore();
		return await credentialsApi.oAuth1CredentialAuthorize(rootStore.restApiContext, data);
	};

	const testCredential = async (
		data: ICredentialsDecrypted,
	): Promise<INodeCredentialTestResult> => {
		const rootStore = useRootStore();
		return await credentialsApi.testCredential(rootStore.restApiContext, { credentials: data });
	};

	const getNewCredentialName = async (params: { credentialTypeName: string }): Promise<string> => {
		try {
			const { credentialTypeName } = params;
			let newName = DEFAULT_CREDENTIAL_NAME;
			if (!TYPES_WITH_DEFAULT_NAME.includes(credentialTypeName)) {
				const cred = getCredentialTypeByName.value(credentialTypeName);
				newName = cred ? getAppNameFromCredType(cred.displayName) : '';
				newName =
					newName.length > 0 ? `${newName} ${DEFAULT_CREDENTIAL_POSTFIX}` : DEFAULT_CREDENTIAL_NAME;
			}
			const rootStore = useRootStore();
			const res = await credentialsApi.getCredentialsNewName(rootStore.restApiContext, newName);
			return res.name;
		} catch (e) {
			return DEFAULT_CREDENTIAL_NAME;
		}
	};

	const setCredentialSharedWith = async (payload: {
		sharedWithProjects: ProjectSharingData[];
		credentialId: string;
	}): Promise<ICredentialsResponse> => {
		if (useSettingsStore().isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Sharing]) {
			await credentialsEeApi.setCredentialSharedWith(
				useRootStore().restApiContext,
				payload.credentialId,
				{
					shareWithIds: payload.sharedWithProjects.map((project) => project.id),
				},
			);

			state.value.credentials[payload.credentialId] = {
				...state.value.credentials[payload.credentialId],
				sharedWithProjects: payload.sharedWithProjects,
			};
		}
		return state.value.credentials[payload.credentialId];
	};

	const getCredentialTranslation = async (credentialType: string): Promise<object> => {
		const rootStore = useRootStore();
		return await makeRestApiRequest(rootStore.restApiContext, 'GET', '/credential-translation', {
			credentialType,
		});
	};

	// #endregion

	return {
		state,
		getCredentialOwnerName,
		getCredentialsByType,
		getCredentialById,
		getCredentialTypeByName,
		getCredentialByIdAndType,
		getNodesWithAccess,
		getUsableCredentialByType,
		credentialTypesById,
		httpOnlyCredentialTypes,
		getScopesByCredentialType,
		getCredentialOwnerNameById,
		allUsableCredentialsForNode,
		allCredentials,
		allCredentialTypes,
		allUsableCredentialsByType,
		setCredentialTypes,
		addCredentials,
		setCredentials,
		deleteCredential,
		upsertCredential,
		fetchCredentialTypes,
		fetchAllCredentials,
		fetchAllCredentialsForWorkflow,
		createNewCredential,
		updateCredential,
		getCredentialData,
		oAuth1Authorize,
		oAuth2Authorize,
		getNewCredentialName,
		testCredential,
		getCredentialTranslation,
		setCredentialSharedWith,
	};
});

/**
 * Helper function for listening to credential changes in the store
 */
export const listenForCredentialChanges = (opts: {
	store: CredentialsStore;
	onCredentialCreated?: (credential: ICredentialsResponse) => void;
	onCredentialUpdated?: (credential: ICredentialsResponse) => void;
	onCredentialDeleted?: (credentialId: string) => void;
}) => {
	const { store, onCredentialCreated, onCredentialDeleted, onCredentialUpdated } = opts;
	const listeningForActions = ['createNewCredential', 'updateCredential', 'deleteCredential'];

	return store.$onAction((result) => {
		const { name, after, args } = result;
		after(async (returnValue) => {
			if (!listeningForActions.includes(name)) {
				return;
			}

			switch (name) {
				case 'createNewCredential':
					const createdCredential = returnValue as unknown as ICredentialsResponse;
					onCredentialCreated?.(createdCredential);
					break;

				case 'updateCredential':
					const updatedCredential = returnValue as unknown as ICredentialsResponse;
					onCredentialUpdated?.(updatedCredential);
					break;

				case 'deleteCredential':
					const credentialId = args[0].id;
					onCredentialDeleted?.(credentialId);
					break;
			}
		});
	});
};
