import {
	AI_NODE_CREATOR_VIEW,
	AI_OTHERS_NODE_CREATOR_VIEW,
	AI_UNCATEGORIZED_CATEGORY,
	CUSTOM_API_CALL_KEY,
	NODE_CREATOR_OPEN_SOURCES,
	REGULAR_NODE_CREATOR_VIEW,
	TRIGGER_NODE_CREATOR_VIEW,
} from '@/app/constants';
import type {
	ActionsRecord,
	INodeCreateElement,
	NodeCreatorOpenSource,
	NodeFilterType,
	SimplifiedNodeType,
	ToggleNodeCreatorOptions,
} from '@/Interface';
import { STORES } from '@n8n/stores';
import { defineStore } from 'pinia';

import { useExternalHooks } from '@/app/composables/useExternalHooks';
import { useTelemetry } from '@/app/composables/useTelemetry';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useUIStore } from '@/app/stores/ui.store';
import { useRouteWorkflowId } from '@/app/composables/useWorkflowId';
import type { TelemetryNdvType } from '@/app/types/telemetry';
import { getNodeIconSource } from '@/app/utils/nodeIcon';
import { isVueFlowConnection } from '@/app/utils/typeGuards';
import type { PartialBy } from '@/app/utils/typeHelpers';
import { useNDVStore } from '@/features/ndv/shared/ndv.store';
import { CanvasConnectionMode } from '@/features/workflows/canvas/canvas.types';
import {
	createCanvasConnectionHandleString,
	parseCanvasConnectionHandleString,
} from '@/features/workflows/canvas/canvas.utils';
import type { Connection } from '@vue-flow/core';
import get from 'lodash/get';
import type { IDataObject, NodeConnectionType } from 'n8n-workflow';
import { NodeConnectionTypes, isCommunityPackageName } from 'n8n-workflow';
import { computed, nextTick, ref } from 'vue';
import { useGetNodeCreatorFilter } from './composables/useGetNodeCreatorFilter';
import { useViewStacks, type ViewStack } from './composables/useViewStacks';
import { prepareCommunityNodeDetailsViewStack, transformNodeType } from './nodeCreator.utils';
import {
	createWorkflowDocumentId,
	useWorkflowDocumentStore,
} from '@/app/stores/workflowDocument.store';

export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => {
	const routeWorkflowId = useRouteWorkflowId();
	const ndvStore = computed(() => useNDVStore(createWorkflowDocumentId(routeWorkflowId.value)));
	const uiStore = useUIStore();
	const nodeTypesStore = useNodeTypesStore();
	const telemetry = useTelemetry();
	const externalHooks = useExternalHooks();
	const { getNodeCreatorFilter } = useGetNodeCreatorFilter();

	const selectedView = ref<NodeFilterType>(TRIGGER_NODE_CREATOR_VIEW);
	const mergedNodes = ref<SimplifiedNodeType[]>([]);
	const actions = ref<ActionsRecord<typeof mergedNodes.value>>({});

	const openSource = ref<NodeCreatorOpenSource>('');

	const isCreateNodeActive = ref<boolean>(false);

	const openingContext = ref<null | 'replacement'>(null);
	const pendingInitialViewStack = ref<ViewStack | null>(null);

	const nodePanelSessionId = ref<string>('');

	const allNodeCreatorNodes = computed(() =>
		Object.values(mergedNodes.value).map((i) => transformNodeType(i)),
	);

	function setMergeNodes(nodes: SimplifiedNodeType[]) {
		mergedNodes.value = nodes;
	}

	function removeNodeFromMergedNodes(nodeName: string) {
		mergedNodes.value = mergedNodes.value.filter((n) => n.name !== nodeName);
	}

	function setActions(nodes: ActionsRecord<typeof mergedNodes.value>) {
		actions.value = nodes;
	}

	function setSelectedView(view: NodeFilterType) {
		selectedView.value = view;
	}

	function setOpenSource(view: NodeCreatorOpenSource) {
		openSource.value = view;
	}

	function openSelectiveNodeCreator({
		connectionType,
		node,
		creatorView,
		connectionIndex = 0,
		workflowId,
	}: {
		connectionType: NodeConnectionType;
		node: string;
		creatorView?: NodeFilterType;
		connectionIndex?: number;
		workflowId: string;
	}) {
		const nodeName = node ?? ndvStore.value.activeNodeName;
		const nodeData = nodeName
			? (useWorkflowDocumentStore(createWorkflowDocumentId(workflowId)).getNodeByName(nodeName) ??
				null)
			: null;

		ndvStore.value.unsetActiveNodeName();

		setTimeout(() => {
			if (creatorView) {
				setNodeCreatorState({
					createNodeActive: true,
					nodeCreatorView: creatorView,
					connectionType,
					workflowId,
				});
			} else if (connectionType && nodeData) {
				openNodeCreatorForConnectingNode({
					connection: {
						source: nodeData.id,
						sourceHandle: createCanvasConnectionHandleString({
							mode: 'inputs',
							type: connectionType,
							index: connectionIndex,
						}),
					},
					eventSource: NODE_CREATOR_OPEN_SOURCES.NOTICE_ERROR_MESSAGE,
					workflowId,
				});
			}
		});
	}

	function setNodeCreatorState({
		source,
		createNodeActive,
		nodeCreatorView,
		connectionType,
		workflowId,
	}: ToggleNodeCreatorOptions & { workflowId: string }) {
		if (!nodeCreatorView) {
			nodeCreatorView =
				useWorkflowDocumentStore(createWorkflowDocumentId(workflowId)).workflowTriggerNodes.length >
				0
					? REGULAR_NODE_CREATOR_VIEW
					: TRIGGER_NODE_CREATOR_VIEW;
		}
		// Default to the trigger tab in node creator if there's no trigger node yet
		setSelectedView(nodeCreatorView);

		isCreateNodeActive.value = createNodeActive;
		if (createNodeActive && source) {
			setOpenSource(source);
		}

		void externalHooks.run('nodeView.createNodeActiveChanged', {
			source,
			mode: getMode(nodeCreatorView),
			connectionType,
			createNodeActive,
		});

		if (createNodeActive) {
			onCreatorOpened({
				source,
				mode: getMode(nodeCreatorView),
				connectionType,
				workflow_id: workflowId,
			});
		}
	}

	function openNodeCreatorForConnectingNode({
		connection,
		eventSource,
		nodeCreatorView,
		workflowId,
	}: {
		connection: PartialBy<Connection, 'target' | 'targetHandle'>;
		eventSource?: NodeCreatorOpenSource;
		nodeCreatorView?: NodeFilterType;
		workflowId: string;
	}) {
		// Get the node and set it as active that new nodes
		// which get created get automatically connected
		// to it.
		const sourceNode = useWorkflowDocumentStore(createWorkflowDocumentId(workflowId)).getNodeById(
			connection.source,
		);
		if (!sourceNode) {
			return;
		}

		const { type, mode } = parseCanvasConnectionHandleString(connection.sourceHandle);

		uiStore.lastSelectedNode = sourceNode.name;

		if (isVueFlowConnection(connection)) {
			uiStore.lastInteractedWithNodeConnection = connection;
		}
		uiStore.lastInteractedWithNodeHandle = connection.sourceHandle ?? null;
		uiStore.lastInteractedWithNodeId = sourceNode.id;

		const isOutput = mode === CanvasConnectionMode.Output;
		const isScopedConnection = type !== NodeConnectionTypes.Main && !nodeCreatorView;
		const view = isScopedConnection ? AI_UNCATEGORIZED_CATEGORY : nodeCreatorView;
		setNodeCreatorState({
			source: eventSource,
			createNodeActive: true,
			nodeCreatorView: view,
			connectionType: type,
			workflowId,
		});

		// TODO: The animation is a bit glitchy because we're updating view stack immediately
		// after the node creator is opened
		if (isScopedConnection) {
			useViewStacks()
				.gotoCompatibleConnectionView(
					type,
					isOutput,
					getNodeCreatorFilter(sourceNode.name, type, sourceNode),
				)
				.catch(() => {});
		}
	}

	async function openNodeCreatorWithNode(workflowId: string, nodeName: string) {
		const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(workflowId));
		const nodeData = nodeName ? (workflowDocumentStore.getNodeByName(nodeName) ?? null) : null;
		if (!nodeData) {
			return;
		}
		ndvStore.value.unsetActiveNodeName();
		const nodeType =
			nodeTypesStore.getNodeType(nodeData?.type) ??
			nodeTypesStore.communityNodeType(nodeData?.type)?.nodeDescription;
		if (!nodeType) {
			return;
		}
		setNodeCreatorState({
			workflowId,
			createNodeActive: true,
		});
		// wait for the node creator state to be set
		await nextTick();
		const nodeActions = actions.value[nodeType.name];
		const viewStack = prepareCommunityNodeDetailsViewStack(
			{
				key: nodeType.name,
				properties: nodeType,
				type: 'node',
				subcategory: '*',
			},
			getNodeIconSource(nodeType.name, null, workflowDocumentStore.getExpressionHandler()),
			'Regular',
			nodeActions ?? [],
		);
		useViewStacks().pushViewStack(viewStack, { resetStacks: true });
	}

	function openNodeCreatorForTriggerNodes(workflowId: string, source: NodeCreatorOpenSource) {
		ndvStore.value.unsetActiveNodeName();
		setSelectedView(TRIGGER_NODE_CREATOR_VIEW);
		setNodeCreatorState({
			workflowId,
			source,
			createNodeActive: true,
			nodeCreatorView: TRIGGER_NODE_CREATOR_VIEW,
		});
	}

	function openNodeCreatorForRegularNodes(workflowId: string, source: NodeCreatorOpenSource) {
		ndvStore.value.unsetActiveNodeName();
		setSelectedView(REGULAR_NODE_CREATOR_VIEW);
		setNodeCreatorState({
			workflowId,
			source,
			createNodeActive: true,
			nodeCreatorView: REGULAR_NODE_CREATOR_VIEW,
		});
	}

	function openNodeCreatorForActions(
		workflowId: string,
		node: string,
		eventSource?: NodeCreatorOpenSource,
	) {
		const actionNode = allNodeCreatorNodes.value.find((i) => i.key === node);

		if (!actionNode) {
			return;
		}

		const nodeActions = actions.value[actionNode.key];

		const transformedActions = nodeActions?.map((a) =>
			transformNodeType(a, actionNode.properties.displayName, 'action'),
		);

		ndvStore.value.unsetActiveNodeName();
		setSelectedView(REGULAR_NODE_CREATOR_VIEW);
		pendingInitialViewStack.value = {
			subcategory: '*',
			title: actionNode.properties.displayName,
			nodeIcon: {
				type: 'icon',
				name: 'check-check',
			},
			rootView: 'Regular',
			mode: 'actions',
			items: transformedActions,
		};
		setNodeCreatorState({
			workflowId,
			source: eventSource,
			createNodeActive: true,
			nodeCreatorView: REGULAR_NODE_CREATOR_VIEW,
		});
	}

	function consumePendingInitialViewStack(): ViewStack | null {
		const stack = pendingInitialViewStack.value;
		pendingInitialViewStack.value = null;
		return stack;
	}

	function resetNodesPanelSession() {
		nodePanelSessionId.value = `nodes_panel_session_${new Date().valueOf()}`;
	}

	function trackNodeCreatorEvent(event: string, properties: IDataObject = {}) {
		telemetry.track(event, {
			...properties,
			nodes_panel_session_id: nodePanelSessionId.value,
		});
	}

	function onCreatorOpened({
		source,
		mode,
		connectionType,
		workflow_id,
	}: {
		source?: string;
		mode: string;
		connectionType?: NodeConnectionType;
		workflow_id?: string;
	}) {
		resetNodesPanelSession();
		trackNodeCreatorEvent('User opened nodes panel', {
			source,
			mode,
			connectionType,
			workflow_id,
		});
	}

	function onNodeFilterChanged({
		newValue,
		filteredNodes,
		filterMode,
		subcategory,
		title,
	}: {
		newValue: string;
		filteredNodes: INodeCreateElement[];
		filterMode: NodeFilterType;
		subcategory?: string;
		title?: string;
	}) {
		if (!newValue.length) {
			return;
		}
		const { results_count, trigger_count, regular_count, community_count } = filteredNodes.reduce(
			(accu, node) => {
				if (!('properties' in node)) {
					return accu;
				}
				const isCustomAction =
					'actionKey' in node.properties && node.properties.actionKey === CUSTOM_API_CALL_KEY;
				if (isCustomAction) {
					return accu;
				}
				const isTrigger = node.key.includes('Trigger');

				const nodeName = get(node, 'properties.name', null);
				const isCommunityNode = nodeName && isCommunityPackageName(nodeName);
				return {
					results_count: accu.results_count + 1,
					trigger_count: accu.trigger_count + (isTrigger ? 1 : 0),
					regular_count: accu.regular_count + (isTrigger ? 0 : 1),
					community_count: accu.community_count + (isCommunityNode ? 1 : 0),
				};
			},
			{
				results_count: 0,
				trigger_count: 0,
				regular_count: 0,
				community_count: 0,
			},
		);
		trackNodeCreatorEvent('User entered nodes panel search term', {
			search_string: newValue,
			filter_mode: getMode(filterMode),
			category_name: subcategory,
			results_count,
			trigger_count,
			regular_count,
			community_count,
			title,
		});
	}

	function onCategoryExpanded(properties: { category_name: string; workflow_id: string }) {
		trackNodeCreatorEvent('User viewed node category', { ...properties, is_subcategory: false });
	}

	function onViewActions(properties: {
		app_identifier: string;
		actions: string[];
		regular_action_count: number;
		trigger_action_count: number;
	}) {
		trackNodeCreatorEvent('User viewed node actions', properties);
	}

	function onActionsCustomAPIClicked(properties: { app_identifier: string }) {
		trackNodeCreatorEvent('User clicked custom API from node actions', properties);
	}

	function onSubcategorySelected(properties: { subcategory: string }) {
		trackNodeCreatorEvent('User viewed node category', {
			category_name: properties.subcategory,
			is_subcategory: true,
		});
	}

	function onNodeAddedToCanvas(properties: {
		node_id: string;
		node_type: string;
		node_version: number;
		is_auto_add?: boolean;
		workflow_id: string;
		drag_and_drop?: boolean;
		input_node_type?: string;
		resource?: string;
		operation?: string;
		action?: string;
		next_view_shown?: TelemetryNdvType;
	}) {
		trackNodeCreatorEvent('User added node to workflow canvas', properties);
	}

	function getMode(mode: NodeFilterType): string {
		if (mode === AI_NODE_CREATOR_VIEW || mode === AI_OTHERS_NODE_CREATOR_VIEW) {
			return 'ai';
		}

		if (mode === TRIGGER_NODE_CREATOR_VIEW) {
			return 'trigger';
		}

		return 'regular';
	}

	return {
		isCreateNodeActive,
		openingContext,
		openSource,
		selectedView,
		mergedNodes,
		actions,
		allNodeCreatorNodes,
		setSelectedView,
		setOpenSource,
		setActions,
		setMergeNodes,
		removeNodeFromMergedNodes,
		setNodeCreatorState,
		openSelectiveNodeCreator,
		openNodeCreatorForConnectingNode,
		openNodeCreatorForTriggerNodes,
		openNodeCreatorForRegularNodes,
		openNodeCreatorForActions,
		consumePendingInitialViewStack,
		onCreatorOpened,
		onNodeFilterChanged,
		onCategoryExpanded,
		onActionsCustomAPIClicked,
		onViewActions,
		onSubcategorySelected,
		onNodeAddedToCanvas,
		openNodeCreatorWithNode,
	};
});
