/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import type { BaseChatMemory } from '@langchain/classic/memory';
import {
	limitWaitTimeOption,
	sendAndWaitWebhooksDescription,
} from 'n8n-nodes-base/dist/utils/sendAndWait/descriptions';
import {
	SEND_AND_WAIT_WAITING_TOOLTIP,
	sendAndWaitWebhook,
} from 'n8n-nodes-base/dist/utils/sendAndWait/utils';
import {
	CHAT_TRIGGER_NODE_TYPE,
	CHAT_WAIT_USER_REPLY,
	FREE_TEXT_CHAT_RESPONSE_TYPE,
	NodeConnectionTypes,
	NodeOperationError,
	SEND_AND_WAIT_OPERATION,
} from 'n8n-workflow';
import type {
	IExecuteFunctions,
	INodeExecutionData,
	INodeTypeDescription,
	INodeType,
	NodeTypeAndVersion,
	INode,
	IDataObject,
} from 'n8n-workflow';

import {
	configureInputs,
	configureWaitTillDate,
	getChatMessage,
	getSendAndWaitPropertiesForChatNode,
} from './util';

export class Chat implements INodeType {
	description: INodeTypeDescription = {
		usableAsTool: true,
		displayName: 'Chat',
		name: 'chat',
		icon: 'fa:comments',
		iconColor: 'black',
		group: ['input'],
		version: [1, 1.1, 1.2],
		defaultVersion: 1.2,
		description: 'Send a message into the chat',
		defaults: {
			name: 'Chat',
		},
		builderHint: {
			relatedNodes: [
				{
					nodeType: '@n8n/n8n-nodes-langchain.chatTrigger',
					relationHint:
						'Required trigger for this node to work - must set responseMode to "responseNodes"',
				},
			],
		},
		codex: {
			categories: ['Core Nodes', 'HITL'],
			subcategories: {
				HITL: ['Human in the Loop'],
			},
			alias: ['human', 'wait', 'hitl', 'respond', 'approve', 'confirm', 'send', 'message'],
			resources: {
				primaryDocumentation: [
					{
						url: 'https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-langchain.respondtochat/',
					},
				],
			},
		},
		inputs: `={{ (${configureInputs})($parameter) }}`,
		outputs: [NodeConnectionTypes.Main],
		waitingNodeTooltip: SEND_AND_WAIT_WAITING_TOOLTIP,
		webhooks: sendAndWaitWebhooksDescription,
		properties: [
			{
				displayName:
					"Verify you're using a chat trigger with the 'Response Mode' option set to 'Using Response Nodes'",
				name: 'generalNotice',
				type: 'notice',
				default: '',
			},
			{
				displayName: 'Operation',
				name: 'operation',
				type: 'options',
				default: 'send',
				noDataExpression: true,
				options: [
					{
						name: 'Send Message',
						value: 'send',
						action: 'Send a message',
					},
					{
						name: 'Send and Wait for Response',
						value: SEND_AND_WAIT_OPERATION,
						action: 'Send message and wait for response',
					},
				],
				displayOptions: {
					show: {
						'@version': [{ _cnd: { gte: 1.1 } }],
					},
				},
			},
			{
				displayName: 'Message',
				name: 'message',
				type: 'string',
				default: '',
				required: true,
				typeOptions: {
					rows: 4,
				},
			},
			{
				displayName: 'Wait for User Reply',
				name: CHAT_WAIT_USER_REPLY,
				type: 'boolean',
				default: true,
				noDataExpression: true,
				displayOptions: {
					show: {
						'@version': [{ _cnd: { lt: 1.1 } }],
					},
				},
			},
			...getSendAndWaitPropertiesForChatNode(),
			{
				displayName: 'Options',
				name: 'options',
				type: 'collection',
				placeholder: 'Add Option',
				default: {},
				displayOptions: {
					hide: {
						'@tool': [true],
					},
				},
				options: [
					{
						displayName: 'Add Memory Input Connection',
						name: 'memoryConnection',
						type: 'boolean',
						default: false,
						displayOptions: {
							hide: {
								'/responseType': ['approval'],
							},
						},
					},
					{
						...limitWaitTimeOption,
						displayOptions: {
							show: {
								[`/${CHAT_WAIT_USER_REPLY}`]: [true],
							},
						},
					},
					{
						...limitWaitTimeOption,
						displayOptions: {
							show: {
								'/operation': [SEND_AND_WAIT_OPERATION],
							},
						},
					},
				],
			},
			{
				displayName: 'Options',
				name: 'options',
				type: 'collection',
				placeholder: 'Add Option',
				default: {},
				options: [limitWaitTimeOption],
				displayOptions: {
					show: {
						'@tool': [true],
						[`/${CHAT_WAIT_USER_REPLY}`]: [true],
					},
				},
			},
			{
				displayName: 'Options',
				name: 'options',
				type: 'collection',
				placeholder: 'Add Option',
				default: {},
				options: [limitWaitTimeOption],
				displayOptions: {
					show: {
						'@tool': [true],
						'/operation': [SEND_AND_WAIT_OPERATION],
					},
				},
			},
		],
	};

	webhook = sendAndWaitWebhook;

	async onMessage(
		context: IExecuteFunctions,
		data: INodeExecutionData,
	): Promise<INodeExecutionData[][]> {
		const options = context.getNodeParameter('options', 0, {}) as {
			memoryConnection?: boolean;
		};

		const nodeVersion = context.getNode().typeVersion;
		let waitForReply;
		if (nodeVersion >= 1.1) {
			const operation = context.getNodeParameter('operation', 0, 'sendMessage');
			waitForReply = operation === SEND_AND_WAIT_OPERATION;
		} else {
			waitForReply = context.getNodeParameter(CHAT_WAIT_USER_REPLY, 0, true) as boolean;
		}

		if (!waitForReply) {
			const inputData = context.getInputData();
			return [inputData];
		}

		if (options.memoryConnection) {
			const memory = (await context.getInputConnectionData(NodeConnectionTypes.AiMemory, 0)) as
				| BaseChatMemory
				| undefined;

			const message = data.json?.chatInput;

			if (memory && message) {
				await memory.chatHistory.addUserMessage(message as string);
			}
		}

		if (nodeVersion < 1.1) {
			return [[data]];
		}

		const responseType = context.getNodeParameter(
			'responseType',
			0,
			FREE_TEXT_CHAT_RESPONSE_TYPE,
		) as string;
		const isFreeText = responseType === FREE_TEXT_CHAT_RESPONSE_TYPE;

		if (nodeVersion <= 1.1) {
			return [
				[
					{
						...data,
						json: {
							// put everything under the `data` key to be consistent
							// with other HITL nodes
							data: {
								...data.json,
								// if the response type is not "Free Text" and the
								// user has typed something - we assume it's
								// disapproval
								approved: isFreeText ? undefined : false,
							},
						},
					},
				],
			];
		}

		let nestedData: IDataObject = {};
		if (typeof data.json.data === 'object') {
			nestedData = {
				...data.json.data,
			};
		}

		// if the response type is not "Free Text" and the
		// user has typed something - we assume it's
		// disapproval
		if (!isFreeText) {
			nestedData.approved = false;
		}

		return [
			[
				{
					...data,
					json: {
						// for v1.2+, don't nest under the `data` key so that Chat
						// node can be connected to the AI Agent directly
						// (it expects `$json.chatInput` field)
						...data.json,
						data: Object.keys(nestedData).length > 0 ? nestedData : undefined,
					},
				},
			],
		];
	}

	async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
		const connectedNodes = this.getParentNodes(this.getNode().name, {
			includeNodeParameters: true,
		});

		let chatTrigger: INode | NodeTypeAndVersion | undefined | null = connectedNodes.find(
			(node) => node.type === CHAT_TRIGGER_NODE_TYPE && !node.disabled,
		);

		if (!chatTrigger) {
			try {
				// try to get chat trigger from workflow if node working as a tool
				chatTrigger = this.getChatTrigger();
			} catch (error) {}
		}

		if (!chatTrigger) {
			throw new NodeOperationError(
				this.getNode(),
				'Workflow must be started from a chat trigger node',
			);
		}

		const parameters = chatTrigger.parameters as {
			mode?: 'hostedChat' | 'webhook';
			options: { responseMode: 'lastNode' | 'responseNodes' | 'streaming' | 'responseNode' };
		};

		if (parameters.mode === 'webhook') {
			throw new NodeOperationError(
				this.getNode(),
				'"Embedded chat" is not supported, change the "Mode" in the chat trigger node to the "Hosted Chat"',
			);
		}

		if (parameters.options.responseMode !== 'responseNodes') {
			throw new NodeOperationError(
				this.getNode(),
				'"Response Mode" in the chat trigger node must be set to "Using Response Nodes"',
			);
		}

		const message = getChatMessage(this);
		const options = this.getNodeParameter('options', 0, {}) as {
			memoryConnection?: boolean;
		};

		if (options.memoryConnection) {
			const memory = (await this.getInputConnectionData(NodeConnectionTypes.AiMemory, 0)) as
				| BaseChatMemory
				| undefined;

			if (memory) {
				const text = typeof message === 'string' ? message : message.text;
				await memory.chatHistory.addAIMessage(text);
			}
		}

		const waitTill = configureWaitTillDate(this);

		await this.putExecutionToWait(waitTill);
		return [[{ json: {}, sendMessage: message }]];
	}
}
