import jwt from 'jsonwebtoken';
import moment from 'moment-timezone';
import type {
	IExecuteFunctions,
	ICredentialsDecrypted,
	ICredentialTestFunctions,
	IDataObject,
	ILoadOptionsFunctions,
	INodeCredentialTestResult,
	INodeExecutionData,
	INodePropertyOptions,
	INodeType,
	INodeTypeDescription,
	IRequestOptions,
} from 'n8n-workflow';
import { NodeConnectionTypes, NodeOperationError, SEND_AND_WAIT_OPERATION } from 'n8n-workflow';

import {
	// attachmentFields,
	// attachmentOperations,
	// incomingWebhookFields,
	// incomingWebhookOperations,
	// mediaFields,
	// mediaOperations,
	memberFields,
	memberOperations,
	messageFields,
	messageOperations,
	spaceFields,
	spaceIdProperty,
	spaceOperations,
} from './descriptions';
import {
	createSendAndWaitMessageBody,
	googleApiRequest,
	googleApiRequestAllItems,
	validateJSON,
} from './GenericFunctions';
import type { IMessage, IMessageUi } from './MessageInterface';
import { configureWaitTillDate } from '../../../utils/sendAndWait/configureWaitTillDate.util';
import { sendAndWaitWebhooksDescription } from '../../../utils/sendAndWait/descriptions';
import {
	getSendAndWaitProperties,
	SEND_AND_WAIT_WAITING_TOOLTIP,
	sendAndWaitWebhook,
} from '../../../utils/sendAndWait/utils';

export class GoogleChat implements INodeType {
	description: INodeTypeDescription = {
		displayName: 'Google Chat',
		name: 'googleChat',
		icon: 'file:googleChat.svg',
		group: ['input'],
		version: 1,
		subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
		description: 'Consume Google Chat API',
		defaults: {
			name: 'Google Chat',
		},
		usableAsTool: true,
		inputs: [NodeConnectionTypes.Main],
		outputs: [NodeConnectionTypes.Main],
		waitingNodeTooltip: SEND_AND_WAIT_WAITING_TOOLTIP,
		webhooks: sendAndWaitWebhooksDescription,
		credentials: [
			{
				name: 'googleApi',
				required: true,
				testedBy: 'testGoogleTokenAuth',
				displayOptions: {
					show: {
						authentication: ['serviceAccount'],
					},
				},
			},
			{
				name: 'googleChatOAuth2Api',
				required: true,
				displayOptions: {
					show: {
						authentication: ['oAuth2'],
					},
				},
			},
		],
		properties: [
			{
				displayName: 'Authentication',
				name: 'authentication',
				type: 'options',
				options: [
					{
						// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
						name: 'OAuth2 (recommended)',
						value: 'oAuth2',
					},
					{
						name: 'Service Account',
						value: 'serviceAccount',
					},
				],
				default: 'serviceAccount',
			},
			{
				displayName: 'Resource',
				name: 'resource',
				required: true,
				noDataExpression: true,
				type: 'options',
				options: [
					// {
					// 	name: 'Attachment',
					// 	value: 'attachment',
					// },
					// {
					// 	name: 'Incoming Webhook',
					// 	value: 'incomingWebhook',
					// },
					// {
					// 	name: 'Media',
					// 	value: 'media',
					// },
					{
						name: 'Member',
						value: 'member',
					},
					{
						name: 'Message',
						value: 'message',
					},
					{
						name: 'Space',
						value: 'space',
					},
				],
				default: 'message',
			},
			// ...attachmentOperations,
			// ...attachmentFields,
			// ...incomingWebhookOperations,
			// ...incomingWebhookFields,
			// ...mediaOperations,
			// ...mediaFields,
			...memberOperations,
			...memberFields,
			...messageOperations,
			...messageFields,
			...spaceOperations,
			...spaceFields,
			...getSendAndWaitProperties([spaceIdProperty], 'message', undefined, {
				noButtonStyle: true,
				defaultApproveLabel: '✅ Approve',
				defaultDisapproveLabel: '❌ Decline',
			}).filter((p) => p.name !== 'subject'),
		],
	};

	webhook = sendAndWaitWebhook;

	methods = {
		loadOptions: {
			// Get all the spaces to display them to user so that they can
			// select them easily
			async getSpaces(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
				const returnData: INodePropertyOptions[] = [];
				const spaces = await googleApiRequestAllItems.call(this, 'spaces', 'GET', '/v1/spaces');
				for (const space of spaces) {
					returnData.push({
						name: space.displayName,
						value: space.name,
					});
				}
				return returnData;
			},
		},
		credentialTest: {
			async testGoogleTokenAuth(
				this: ICredentialTestFunctions,
				credential: ICredentialsDecrypted,
			): Promise<INodeCredentialTestResult> {
				const scopes = ['https://www.googleapis.com/auth/chat.bot'];

				const now = moment().unix();

				const email = (credential.data!.email as string).trim();
				const privateKey = (credential.data!.privateKey as string).replace(/\\n/g, '\n').trim();

				try {
					const signature = jwt.sign(
						{
							iss: email,
							sub: credential.data!.delegatedEmail || email,
							scope: scopes.join(' '),
							aud: 'https://oauth2.googleapis.com/token',
							iat: now,
							exp: now,
						},
						privateKey,
						{
							algorithm: 'RS256',
							header: {
								kid: privateKey,
								typ: 'JWT',
								alg: 'RS256',
							},
						},
					);

					const options: IRequestOptions = {
						headers: {
							'Content-Type': 'application/x-www-form-urlencoded',
						},
						method: 'POST',
						form: {
							grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
							assertion: signature,
						},
						uri: 'https://oauth2.googleapis.com/token',
						json: true,
					};

					const response = await this.helpers.request(options);

					if (!response.access_token) {
						return {
							status: 'Error',
							message: JSON.stringify(response),
						};
					}
				} catch (err) {
					return {
						status: 'Error',
						message: `${err.message}`,
					};
				}

				return {
					status: 'OK',
					message: 'Connection successful!',
				};
			},
		},
	};

	async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
		const items = this.getInputData();
		const returnData: INodeExecutionData[] = [];
		const length = items.length;
		const qs: IDataObject = {};
		let responseData;
		const resource = this.getNodeParameter('resource', 0);
		const operation = this.getNodeParameter('operation', 0);

		if (resource === 'message' && operation === SEND_AND_WAIT_OPERATION) {
			const spaceId = this.getNodeParameter('spaceId', 0) as string;
			const body = createSendAndWaitMessageBody(this);

			await googleApiRequest.call(this, 'POST', `/v1/${spaceId}/messages`, body);

			const waitTill = configureWaitTillDate(this);

			await this.putExecutionToWait(waitTill);
			return [this.getInputData()];
		}

		for (let i = 0; i < length; i++) {
			try {
				if (resource === 'media') {
					if (operation === 'download') {
						// ----------------------------------------
						//             media: download
						// ----------------------------------------

						// https://developers.google.com/chat/reference/rest/v1/media/download

						const resourceName = this.getNodeParameter('resourceName', i) as string;

						const endpoint = `/v1/media/${resourceName}?alt=media`;

						// Return the data as a buffer
						const encoding = null;

						responseData = await googleApiRequest.call(
							this,
							'GET',
							endpoint,
							undefined,
							undefined,
							undefined,
							undefined,
							encoding,
						);

						const newItem: INodeExecutionData = {
							json: items[i].json,
							binary: {},
						};

						if (items[i].binary !== undefined) {
							// Create a shallow copy of the binary data so that the old
							// data references which do not get changed still stay behind
							// but the incoming data does not get changed.
							Object.assign(newItem.binary!, items[i].binary);
						}

						items[i] = newItem;

						const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i);

						items[i].binary![binaryPropertyName] = await this.helpers.prepareBinaryData(
							responseData as Buffer,
							endpoint,
						);
					}
				} else if (resource === 'space') {
					if (operation === 'get') {
						// ----------------------------------------
						//             space: get
						// ----------------------------------------

						// https://developers.google.com/chat/reference/rest/v1/spaces/get

						const spaceId = this.getNodeParameter('spaceId', i) as string;

						responseData = await googleApiRequest.call(this, 'GET', `/v1/${spaceId}`);
					} else if (operation === 'getAll') {
						// ----------------------------------------
						//             space: getAll
						// ----------------------------------------

						// https://developers.google.com/chat/reference/rest/v1/spaces/list

						const returnAll = this.getNodeParameter('returnAll', 0);
						if (returnAll) {
							responseData = await googleApiRequestAllItems.call(
								this,
								'spaces',
								'GET',
								'/v1/spaces',
							);
						} else {
							const limit = this.getNodeParameter('limit', i);
							qs.pageSize = limit;

							responseData = await googleApiRequest.call(this, 'GET', '/v1/spaces', undefined, qs);
							responseData = responseData.spaces;
						}
					}
				} else if (resource === 'member') {
					if (operation === 'get') {
						// ----------------------------------------
						//             member: get
						// ----------------------------------------

						// https://developers.google.com/chat/reference/rest/v1/spaces.members/get

						const memberId = this.getNodeParameter('memberId', i) as string;

						responseData = await googleApiRequest.call(this, 'GET', `/v1/${memberId}`);
					} else if (operation === 'getAll') {
						// ----------------------------------------
						//             member: getAll
						// ----------------------------------------

						// https://developers.google.com/chat/reference/rest/v1/spaces.members/list

						const spaceId = this.getNodeParameter('spaceId', i) as string;

						const returnAll = this.getNodeParameter('returnAll', 0);
						if (returnAll) {
							responseData = await googleApiRequestAllItems.call(
								this,
								'memberships',
								'GET',
								`/v1/${spaceId}/members`,
								undefined,
								qs,
							);
						} else {
							const limit = this.getNodeParameter('limit', i);
							qs.pageSize = limit;

							responseData = await googleApiRequest.call(
								this,
								'GET',
								`/v1/${spaceId}/members`,
								undefined,
								qs,
							);
							responseData = responseData.memberships;
						}
					}
				} else if (resource === 'message') {
					if (operation === 'create') {
						// ----------------------------------------
						//             message: create
						// ----------------------------------------

						// https://developers.google.com/chat/reference/rest/v1/spaces.messages/create

						const spaceId = this.getNodeParameter('spaceId', i) as string;

						// get additional fields for threadKey and requestId
						const additionalFields = this.getNodeParameter('additionalFields', i);
						if (additionalFields.threadKey) {
							qs.threadKey = additionalFields.threadKey;
						}
						if (additionalFields.requestId) {
							qs.requestId = additionalFields.requestId;
						}

						let message: IMessage = {};
						const jsonParameters = this.getNodeParameter('jsonParameters', i);
						if (jsonParameters) {
							const messageJson = this.getNodeParameter('messageJson', i);

							if (messageJson instanceof Object) {
								// if it is an object
								message = messageJson as IMessage;
							} else {
								// if it is a string
								if (validateJSON(messageJson as string) !== undefined) {
									message = JSON.parse(messageJson as string) as IMessage;
								} else {
									throw new NodeOperationError(
										this.getNode(),
										'Message (JSON) must be a valid json',
										{ itemIndex: i },
									);
								}
							}
						} else {
							const messageUi = this.getNodeParameter('messageUi', i) as IMessageUi;
							if (messageUi.text && messageUi.text !== '') {
								message.text = messageUi.text;
							} else {
								throw new NodeOperationError(this.getNode(), 'Message Text must be provided.', {
									itemIndex: i,
								});
							}
							// 	// TODO: get cards from the UI
							// if (messageUi?.cards?.metadataValues && messageUi?.cards?.metadataValues.length !== 0) {
							// 	const cards = messageUi.cards.metadataValues as IDataObject[]; // TODO: map cards to messageUi.cards.metadataValues
							// 	message.cards = cards;
							// }
						}

						const body: IDataObject = {};
						Object.assign(body, message);

						responseData = await googleApiRequest.call(
							this,
							'POST',
							`/v1/${spaceId}/messages`,
							body,
							qs,
						);
					} else if (operation === 'delete') {
						// ----------------------------------------
						//             message: delete
						// ----------------------------------------

						// https://developers.google.com/chat/reference/rest/v1/spaces.messages/delete

						const messageId = this.getNodeParameter('messageId', i) as string;

						responseData = await googleApiRequest.call(this, 'DELETE', `/v1/${messageId}`);
					} else if (operation === 'get') {
						// ----------------------------------------
						//             message: get
						// ----------------------------------------

						// https://developers.google.com/chat/reference/rest/v1/spaces.messages/get

						const messageId = this.getNodeParameter('messageId', i) as string;

						responseData = await googleApiRequest.call(this, 'GET', `/v1/${messageId}`);
					} else if (operation === 'update') {
						// ----------------------------------------
						//             message: update
						// ----------------------------------------

						// https://developers.google.com/chat/reference/rest/v1/spaces.messages/update

						const messageId = this.getNodeParameter('messageId', i) as string;

						let message: IMessage = {};
						const jsonParameters = this.getNodeParameter('jsonParameters', i);
						if (jsonParameters) {
							const updateFieldsJson = this.getNodeParameter('updateFieldsJson', i);

							if (updateFieldsJson instanceof Object) {
								// if it is an object
								message = updateFieldsJson as IMessage;
							} else {
								// if it is a string
								if (validateJSON(updateFieldsJson as string) !== undefined) {
									message = JSON.parse(updateFieldsJson as string) as IMessage;
								} else {
									throw new NodeOperationError(
										this.getNode(),
										'Update Fields (JSON) must be a valid json',
										{ itemIndex: i },
									);
								}
							}
						} else {
							const updateFieldsUi = this.getNodeParameter('updateFieldsUi', i) as IDataObject;
							if (updateFieldsUi.text) {
								message.text = updateFieldsUi.text as string;
							}
							// // TODO: get cards from the UI
							// if (updateFieldsUi.cards) {
							// 	message.cards = updateFieldsUi.cards as IDataObject[];
							// }
						}

						const body: IDataObject = {};
						Object.assign(body, message);

						// get update mask
						let updateMask = '';
						if (message.text) {
							updateMask += 'text,';
						}
						if (message.cards) {
							updateMask += 'cards,';
						}
						updateMask = updateMask.slice(0, -1); // remove trailing comma
						qs.updateMask = updateMask;

						responseData = await googleApiRequest.call(this, 'PUT', `/v1/${messageId}`, body, qs);
					}
				} else if (resource === 'attachment') {
					if (operation === 'get') {
						// ----------------------------------------
						//             attachment: get
						// ----------------------------------------

						// https://developers.google.com/chat/reference/rest/v1/spaces.messages.attachments/get

						const attachmentName = this.getNodeParameter('attachmentName', i) as string;

						responseData = await googleApiRequest.call(this, 'GET', `/v1/${attachmentName}`);
					}
				} else if (resource === 'incomingWebhook') {
					if (operation === 'create') {
						// ----------------------------------------
						//             incomingWebhook: create
						// ----------------------------------------

						// https://developers.google.com/chat/how-tos/webhooks

						const uri = this.getNodeParameter('incomingWebhookUrl', i) as string;

						// get additional fields for threadKey
						const additionalFields = this.getNodeParameter('additionalFields', i);
						if (additionalFields.threadKey) {
							qs.threadKey = additionalFields.threadKey;
						}

						let message: IMessage = {};
						const jsonParameters = this.getNodeParameter('jsonParameters', i);
						if (jsonParameters) {
							const messageJson = this.getNodeParameter('messageJson', i);

							if (messageJson instanceof Object) {
								// if it is an object
								message = messageJson as IMessage;
							} else {
								// if it is a string
								if (validateJSON(messageJson as string) !== undefined) {
									message = JSON.parse(messageJson as string) as IMessage;
								} else {
									throw new NodeOperationError(
										this.getNode(),
										'Message (JSON) must be a valid json',
										{ itemIndex: i },
									);
								}
							}
						} else {
							const messageUi = this.getNodeParameter('messageUi', i) as IMessageUi;
							if (messageUi.text && messageUi.text !== '') {
								message.text = messageUi.text;
							} else {
								throw new NodeOperationError(this.getNode(), 'Message Text must be provided.', {
									itemIndex: i,
								});
							}
						}

						const body: IDataObject = {};
						Object.assign(body, message);

						responseData = await googleApiRequest.call(this, 'POST', '', body, qs, uri, true);
					}
				}

				const executionData = this.helpers.constructExecutionMetaData(
					this.helpers.returnJsonArray(responseData as IDataObject),
					{ itemData: { item: i } },
				);
				returnData.push(...executionData);
			} catch (error) {
				if (this.continueOnFail()) {
					// Return the actual reason as error
					if (operation === 'download') {
						items[i].json = { error: error.message };
					} else {
						const executionErrorData = this.helpers.constructExecutionMetaData(
							this.helpers.returnJsonArray({ error: error.message }),
							{ itemData: { item: i } },
						);
						returnData.push(...executionErrorData);
					}
					continue;
				}
				throw error;
			}
		}

		if (operation === 'download') {
			// For file downloads the files get attached to the existing items
			return [items];
		} else {
			// For all other ones does the output get replaced
			return [returnData];
		}
	}
}
