import type {
	ActionResultRequestDto,
	CommunityNodeType,
	OptionsRequestDto,
	ResourceLocatorRequestDto,
	ResourceMapperFieldsRequestDto,
} from '@n8n/api-types';
import * as nodeTypesApi from '@n8n/rest-api-client/api/nodeTypes';
import {
	HTTP_REQUEST_NODE_TYPE,
	CREDENTIAL_ONLY_HTTP_NODE_VERSION,
	MODULE_ENABLED_NODES,
} from '@/app/constants';
import { STORES } from '@n8n/stores';
import type { NodeTypesByTypeNameAndVersion } from '@/Interface';
import { addHeaders, addNodeTranslation } from '@n8n/i18n';
import { omit } from '@/app/utils/typesUtils';
import type {
	INode,
	INodeInputConfiguration,
	INodeOutputConfiguration,
	INodeType,
	INodeTypeDescription,
	INodeTypeNameVersion,
	INodeTypes,
	NodeConnectionType,
} from 'n8n-workflow';
import { ERROR_TRIGGER_NODE_TYPE, NodeConnectionTypes, NodeHelpers } from 'n8n-workflow';
import { defineStore } from 'pinia';
import { useCredentialsStore } from '@/features/credentials/credentials.store';
import { useRootStore } from '@n8n/stores/useRootStore';
import * as utils from '@/app/utils/credentialOnlyNodes';
import { groupNodeTypesByNameAndType } from '@/app/utils/nodeTypes/nodeTypeTransforms';
import { computed, ref } from 'vue';
import { useActionsGenerator } from '@/features/shared/nodeCreator/composables/useActionsGeneration';
import { removePreviewToken } from '@/features/shared/nodeCreator/nodeCreator.utils';
import { useSettingsStore } from '@/app/stores/settings.store';
import { isDataWorkerEnabled } from '@/app/workers/isDataWorkerEnabled';
import type { WorkflowObjectAccessors } from '../types';

export type NodeTypesStore = ReturnType<typeof useNodeTypesStore>;

export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, () => {
	const nodeTypes = ref<NodeTypesByTypeNameAndVersion>({});

	const vettedCommunityNodeTypes = ref<Map<string, CommunityNodeType>>(new Map());

	const rootStore = useRootStore();

	const actionsGenerator = useActionsGenerator();

	const settingsStore = useSettingsStore();

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

	const communityNodeType = computed(() => {
		return (nodeTypeName: string) => {
			// First try direct lookup by map key (package name without preview token)
			const direct = vettedCommunityNodeTypes.value.get(nodeTypeName);
			if (direct) return direct;

			// Fallback: search by nodeDescription.name (handles preview token mismatch)
			const cleanedName = removePreviewToken(nodeTypeName);
			for (const communityNode of vettedCommunityNodeTypes.value.values()) {
				const descName = communityNode.nodeDescription?.name;
				if (descName === nodeTypeName || removePreviewToken(descName ?? '') === cleanedName) {
					return communityNode;
				}
			}

			return undefined;
		};
	});

	const officialCommunityNodeTypes = computed(() =>
		Array.from(vettedCommunityNodeTypes.value.values())
			.filter(({ isOfficialNode, isInstalled }) => isOfficialNode && !isInstalled)
			.map(({ nodeDescription }) => nodeDescription),
	);

	const unofficialCommunityNodeTypes = computed(() =>
		Array.from(vettedCommunityNodeTypes.value.values())
			.filter(({ isOfficialNode, isInstalled }) => !isOfficialNode && !isInstalled)
			.map(({ nodeDescription }) => nodeDescription),
	);

	const communityNodesAndActions = computed(() => {
		return actionsGenerator.generateMergedNodesAndActions(unofficialCommunityNodeTypes.value, []);
	});

	const allNodeTypes = computed(() => {
		return Object.values(nodeTypes.value).flatMap((nodeType) =>
			Object.keys(nodeType).map((version) => nodeType[Number(version)]),
		);
	});

	const allLatestNodeTypes = computed(() => {
		return Object.values(nodeTypes.value)
			.map((nodeVersions) => {
				const versionNumbers = Object.keys(nodeVersions).map(Number);
				return nodeVersions[Math.max(...versionNumbers)];
			})
			.filter(Boolean);
	});

	// Nodes defined with `hidden: true` that are still shown if their modules are enabled
	const moduleEnabledNodeTypes = computed<INodeTypeDescription[]>(() => {
		return MODULE_ENABLED_NODES.flatMap((node) => {
			const nodeVersions = nodeTypes.value[node.nodeType] ?? {};
			const versionNumbers = Object.keys(nodeVersions).map(Number);
			const latest = nodeVersions[Math.max(...versionNumbers)];

			if (latest?.hidden && settingsStore.isModuleActive(node.module)) {
				return {
					...latest,
					hidden: undefined,
				};
			}

			return [];
		});
	});

	const getNodeType = computed(() => {
		return (nodeTypeName: string, version?: number): INodeTypeDescription | null => {
			if (utils.isCredentialOnlyNodeType(nodeTypeName)) {
				return getCredentialOnlyNodeType.value(nodeTypeName, version);
			}

			const nodeVersions = nodeTypes.value[nodeTypeName];

			if (!nodeVersions) return null;

			const versionNumbers = Object.keys(nodeVersions).map(Number);
			const nodeType = nodeVersions[version ?? Math.max(...versionNumbers)];
			return nodeType ?? null;
		};
	});

	const getNodeVersions = computed(() => {
		return (nodeTypeName: string): number[] => {
			return Object.keys(nodeTypes.value[nodeTypeName] ?? {}).map(Number);
		};
	});

	const getCredentialOnlyNodeType = computed(() => {
		return (nodeTypeName: string, version?: number): INodeTypeDescription | null => {
			const credentialName = utils.getCredentialTypeName(nodeTypeName);
			const httpNode = getNodeType.value(
				HTTP_REQUEST_NODE_TYPE,
				version ?? CREDENTIAL_ONLY_HTTP_NODE_VERSION,
			);
			const credential = useCredentialsStore().getCredentialTypeByName(credentialName);
			return utils.getCredentialOnlyNodeType(httpNode, credential) ?? null;
		};
	});

	const isConfigNode = computed(() => {
		return (workflow: WorkflowObjectAccessors, node: INode, nodeTypeName: string): boolean => {
			if (!workflow.getNode(node.name)) {
				return false;
			}
			const nodeType =
				getNodeType.value(nodeTypeName) ?? communityNodeType.value(nodeTypeName)?.nodeDescription;
			if (!nodeType) {
				return false;
			}
			const outputs = NodeHelpers.getNodeOutputs(workflow, node, nodeType);
			const outputTypes = NodeHelpers.getConnectionTypes(outputs);
			return outputTypes
				? outputTypes.filter((output) => output !== NodeConnectionTypes.Main).length > 0
				: false;
		};
	});

	const isTriggerNode = computed(() => {
		return (nodeTypeName: string) => {
			const nodeType = getNodeType.value(nodeTypeName);
			return !!(nodeType && nodeType.group.includes('trigger'));
		};
	});

	const isToolNode = computed(() => {
		return (nodeTypeName: string) => {
			const nodeType = getNodeType.value(nodeTypeName);
			if (nodeType?.outputs && Array.isArray(nodeType.outputs)) {
				const outputTypes = nodeType.outputs.map(
					(output: NodeConnectionType | INodeOutputConfiguration) =>
						typeof output === 'string' ? output : output.type,
				);

				return outputTypes.includes(NodeConnectionTypes.AiTool);
			} else {
				return nodeType?.outputs.includes(NodeConnectionTypes.AiTool) ?? false;
			}
		};
	});

	const isModelNode = computed(() => {
		return (nodeTypeName: string) => {
			const nodeType = getNodeType.value(nodeTypeName);
			if (nodeType?.outputs && Array.isArray(nodeType.outputs)) {
				const outputTypes = nodeType.outputs.map(
					(output: NodeConnectionType | INodeOutputConfiguration) =>
						typeof output === 'string' ? output : output.type,
				);

				return outputTypes.includes(NodeConnectionTypes.AiLanguageModel);
			} else {
				return nodeType?.outputs.includes(NodeConnectionTypes.AiLanguageModel) ?? false;
			}
		};
	});

	const isCoreNodeType = computed(() => {
		return (nodeType: INodeTypeDescription) => {
			return nodeType.codex?.categories?.includes('Core Nodes');
		};
	});

	const visibleNodeTypes = computed(() => {
		return allLatestNodeTypes.value
			.concat(officialCommunityNodeTypes.value)
			.concat(moduleEnabledNodeTypes.value)
			.filter((nodeType) => !nodeType.hidden);
	});

	const nativelyNumberSuffixedDefaults = computed(() => {
		return allNodeTypes.value.reduce<string[]>((acc, cur) => {
			if (/\d$/.test(cur.defaults.name as string)) acc.push(cur.defaults.name as string);
			return acc;
		}, []);
	});

	const visibleNodeTypesByOutputConnectionTypeNames = computed(() => {
		const nodesByOutputType = visibleNodeTypes.value.reduce(
			(acc, node) => {
				const outputTypes = node.outputs;
				if (Array.isArray(outputTypes)) {
					outputTypes.forEach((value: NodeConnectionType | INodeOutputConfiguration) => {
						const outputType = typeof value === 'string' ? value : value.type;
						if (!acc[outputType]) {
							acc[outputType] = [];
						}
						acc[outputType].push(node.name);
					});
				} else {
					// If outputs is not an array, it must be a string expression
					// in which case we'll try to match all possible non-main output types that are supported
					const connectorTypes: NodeConnectionType[] = [
						NodeConnectionTypes.AiVectorStore,
						NodeConnectionTypes.AiChain,
						NodeConnectionTypes.AiDocument,
						NodeConnectionTypes.AiEmbedding,
						NodeConnectionTypes.AiLanguageModel,
						NodeConnectionTypes.AiMemory,
						NodeConnectionTypes.AiOutputParser,
						NodeConnectionTypes.AiTextSplitter,
						NodeConnectionTypes.AiTool,
					];
					connectorTypes.forEach((outputType: NodeConnectionType) => {
						if (outputTypes.includes(outputType)) {
							acc[outputType] = acc[outputType] || [];
							acc[outputType].push(node.name);
						}
					});
				}

				return acc;
			},
			{} as { [key: string]: string[] },
		);

		return nodesByOutputType;
	});

	const visibleNodeTypesByInputConnectionTypeNames = computed(() => {
		const nodesByOutputType = visibleNodeTypes.value.reduce(
			(acc, node) => {
				const inputTypes = node.inputs;
				if (Array.isArray(inputTypes)) {
					inputTypes.forEach(
						(value: NodeConnectionType | INodeOutputConfiguration | INodeInputConfiguration) => {
							const outputType = typeof value === 'string' ? value : value.type;
							if (!acc[outputType]) {
								acc[outputType] = [];
							}
							acc[outputType].push(node.name);
						},
					);
				}

				return acc;
			},
			{} as { [key: string]: string[] },
		);

		return nodesByOutputType;
	});

	const isConfigurableNode = computed(() => {
		return (
			workflow: WorkflowObjectAccessors,
			node: INode,
			nodeTypeName: string,
			nodeTypeVersion?: number,
		): boolean => {
			const nodeType = getNodeType.value(nodeTypeName, nodeTypeVersion);
			if (nodeType === null) {
				return false;
			}
			const inputs = NodeHelpers.getNodeInputs(workflow, node, nodeType);
			const inputTypes = NodeHelpers.getConnectionTypes(inputs);

			return inputTypes
				? inputTypes.filter((input) => input !== NodeConnectionTypes.Main).length > 0
				: false;
		};
	});

	// #endregion

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

	const setNodeTypes = (newNodeTypes: INodeTypeDescription[] = []) => {
		const groupedNodeTypes = groupNodeTypesByNameAndType(newNodeTypes);
		nodeTypes.value = {
			...nodeTypes.value,
			...groupedNodeTypes,
		};
	};

	const removeNodeTypes = (nodeTypesToRemove: INodeTypeDescription[]) => {
		nodeTypes.value = nodeTypesToRemove.reduce(
			(oldNodes, newNodeType) => omit(newNodeType.name, oldNodes),
			nodeTypes.value,
		);
	};

	// Read full node descriptions from the local SQLite database, falling back to
	// REST for any node not present locally (e.g. community/dynamic nodes).
	const loadNodesInformationFromLocalDb = async (
		nodeInfos: INodeTypeNameVersion[],
	): Promise<INodeTypeDescription[]> => {
		try {
			const { getNodeType } = await import('@/app/workers');
			const found: INodeTypeDescription[] = [];
			const missing: INodeTypeNameVersion[] = [];

			for (const nodeInfo of nodeInfos) {
				const nodeType = await getNodeType(nodeInfo.name, nodeInfo.version);
				if (nodeType) {
					found.push(nodeType);
				} else {
					missing.push(nodeInfo);
				}
			}

			if (missing.length) {
				found.push(...(await nodeTypesApi.getNodesInformation(rootStore.restApiContext, missing)));
			}

			return found;
		} catch (error) {
			return await nodeTypesApi.getNodesInformation(rootStore.restApiContext, nodeInfos);
		}
	};

	const getNodesInformation = async (
		nodeInfos: INodeTypeNameVersion[],
		replace = true,
	): Promise<INodeTypeDescription[]> => {
		// The local DB holds untranslated descriptions, so only read from it for
		// the English locale; other locales need per-node translations from REST.
		const nodesInformation =
			isDataWorkerEnabled() && rootStore.defaultLocale === 'en'
				? await loadNodesInformationFromLocalDb(nodeInfos)
				: await nodeTypesApi.getNodesInformation(rootStore.restApiContext, nodeInfos);

		nodesInformation.forEach((nodeInformation) => {
			if (nodeInformation.translation) {
				const nodeType = nodeInformation.name.replace('n8n-nodes-base.', '');

				addNodeTranslation({ [nodeType]: nodeInformation.translation }, rootStore.defaultLocale);
			}
		});
		if (replace) setNodeTypes(nodesInformation);

		return nodesInformation;
	};

	const getFullNodesProperties = async (
		nodesToBeFetched: INodeTypeNameVersion[],
		replaceNodeTypes = true,
	) => {
		const credentialsStore = useCredentialsStore();
		await credentialsStore.fetchCredentialTypes(true);
		if (replaceNodeTypes) {
			await getNodesInformation(nodesToBeFetched);
		}
	};

	// Sync the local SQLite database with the server (cheap incremental diff after
	// the initial load) and read all node descriptions back from it. Returns an
	// empty array if the database is unavailable so the caller can fall back to REST.
	const loadNodeTypesFromLocalDb = async (): Promise<INodeTypeDescription[]> => {
		try {
			const { loadNodeTypes, getAllNodeTypes } = await import('@/app/workers');
			await loadNodeTypes(rootStore.baseUrl);
			return await getAllNodeTypes();
		} catch (error) {
			return [];
		}
	};

	const getNodeTypes = async () => {
		const fetchedNodeTypes = isDataWorkerEnabled() ? await loadNodeTypesFromLocalDb() : [];
		const nodeTypes = fetchedNodeTypes.length
			? fetchedNodeTypes
			: await nodeTypesApi.getNodeTypes(rootStore.baseUrl);

		if (nodeTypes.length) {
			setNodeTypes(nodeTypes);
		}
	};

	const loadNodeTypesIfNotLoaded = async () => {
		if (Object.keys(nodeTypes.value).length === 0) {
			await getNodeTypes();
		}
	};

	const getNodeTranslationHeaders = async () => {
		const headers = await nodeTypesApi.getNodeTranslationHeaders(rootStore.restApiContext);

		if (headers) {
			addHeaders(headers, rootStore.defaultLocale);
		}
	};

	const getNodeParameterOptions = async (sendData: OptionsRequestDto) => {
		return await nodeTypesApi.getNodeParameterOptions(rootStore.restApiContext, sendData);
	};

	const getResourceLocatorResults = async (sendData: ResourceLocatorRequestDto) => {
		return await nodeTypesApi.getResourceLocatorResults(rootStore.restApiContext, sendData);
	};

	const getResourceMapperFields = async (sendData: ResourceMapperFieldsRequestDto) => {
		try {
			return await nodeTypesApi.getResourceMapperFields(rootStore.restApiContext, sendData);
		} catch (error) {
			return null;
		}
	};

	const getLocalResourceMapperFields = async (sendData: ResourceMapperFieldsRequestDto) => {
		try {
			return await nodeTypesApi.getLocalResourceMapperFields(rootStore.restApiContext, sendData);
		} catch (error) {
			return null;
		}
	};

	const getNodeParameterActionResult = async (sendData: ActionResultRequestDto) => {
		return await nodeTypesApi.getNodeParameterActionResult(rootStore.restApiContext, sendData);
	};

	const fetchCommunityNodePreviews = async () => {
		if (!settingsStore.isCommunityNodesFeatureEnabled) {
			return;
		}
		try {
			const communityNodeTypes = await nodeTypesApi.fetchCommunityNodeTypes(
				rootStore.restApiContext,
			);

			vettedCommunityNodeTypes.value = new Map(
				communityNodeTypes.map((nodeType) => [nodeType.name, nodeType]),
			);
		} catch (error) {
			vettedCommunityNodeTypes.value = new Map();
		}
	};

	const getCommunityNodeAttributes = async (nodeName: string) => {
		if (!settingsStore.isCommunityNodesFeatureEnabled) {
			return null;
		}

		try {
			return await nodeTypesApi.fetchCommunityNodeAttributes(
				rootStore.restApiContext,
				removePreviewToken(nodeName),
			);
		} catch (error) {
			return null;
		}
	};

	const getIsNodeInstalled = computed(() => {
		return (nodeTypeName: string) => {
			const cleanedNodeTypeName = removePreviewToken(nodeTypeName);
			return (
				!!getNodeType.value(cleanedNodeTypeName) ||
				!!communityNodeType.value(cleanedNodeTypeName)?.isInstalled
			);
		};
	});

	function getAllNodeTypes(): INodeTypes {
		const nodeTypes: INodeTypes = {
			nodeTypes: {},
			init: async (): Promise<void> => {},
			getByNameAndVersion: (nodeType: string, version?: number): INodeType | undefined => {
				const nodeTypeDescription =
					getNodeType.value(nodeType, version) ??
					communityNodeType.value(nodeType)?.nodeDescription ??
					null;
				if (nodeTypeDescription === null) {
					return undefined;
				}

				return {
					description: nodeTypeDescription,
					// As we do not have the trigger/poll functions available in the frontend
					// we use the information available to figure out what are trigger nodes
					// @ts-ignore
					trigger:
						(![ERROR_TRIGGER_NODE_TYPE].includes(nodeType) &&
							nodeTypeDescription.inputs.length === 0 &&
							!nodeTypeDescription.webhooks) ||
						undefined,
				};
			},
		} as unknown as INodeTypes;

		return nodeTypes;
	}

	// #endregion

	return {
		nodeTypes,
		allNodeTypes,
		allLatestNodeTypes,
		getNodeType,
		getNodeVersions,
		getCredentialOnlyNodeType,
		isConfigNode,
		isTriggerNode,
		isToolNode,
		isModelNode,
		isCoreNodeType,
		visibleNodeTypes,
		nativelyNumberSuffixedDefaults,
		visibleNodeTypesByOutputConnectionTypeNames,
		visibleNodeTypesByInputConnectionTypeNames,
		isConfigurableNode,
		communityNodesAndActions,
		communityNodeType,
		fetchCommunityNodePreviews,
		getResourceMapperFields,
		getLocalResourceMapperFields,
		getNodeParameterActionResult,
		getResourceLocatorResults,
		getNodeParameterOptions,
		getNodesInformation,
		getFullNodesProperties,
		getNodeTypes,
		getAllNodeTypes,
		loadNodeTypesIfNotLoaded,
		getNodeTranslationHeaders,
		setNodeTypes,
		removeNodeTypes,
		getCommunityNodeAttributes,
		getIsNodeInstalled,
	};
});
