import type {
	IDataObject,
	IExecuteFunctions,
	ILoadOptionsFunctions,
	INodeExecutionData,
	INodePropertyOptions,
	INodeType,
	INodeTypeBaseDescription,
	INodeTypeDescription,
	JsonObject,
} from 'n8n-workflow';
import { NodeApiError, NodeConnectionTypes } from 'n8n-workflow';

import { oldVersionNotice } from '@utils/descriptions';

import {
	microsoftApiRequest,
	microsoftApiRequestAllItems,
	microsoftApiRequestAllItemsSkip,
} from './GenericFunctions';
import { tableFields, tableOperations } from './TableDescription';
import { workbookFields, workbookOperations } from './WorkbookDescription';
import { worksheetFields, worksheetOperations } from './WorksheetDescription';
import { generatePairedItemData } from '../../../../utils/utilities';

const versionDescription: INodeTypeDescription = {
	displayName: 'Microsoft Excel',
	name: 'microsoftExcel',
	icon: 'file:excel.svg',
	group: ['input'],
	version: 1,
	subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
	description: 'Consume Microsoft Excel API',
	defaults: {
		name: 'Microsoft Excel',
	},
	inputs: [NodeConnectionTypes.Main],
	outputs: [NodeConnectionTypes.Main],
	credentials: [
		{
			name: 'microsoftExcelOAuth2Api',
			required: true,
		},
	],
	properties: [
		oldVersionNotice,
		{
			displayName: 'Resource',
			name: 'resource',
			type: 'options',
			noDataExpression: true,
			options: [
				{
					name: 'Table',
					value: 'table',
					description: 'Represents an Excel table',
				},
				{
					name: 'Workbook',
					value: 'workbook',
					description:
						'Workbook is the top level object which contains related workbook objects such as worksheets, tables, ranges, etc',
				},
				{
					name: 'Worksheet',
					value: 'worksheet',
					description:
						'An Excel worksheet is a grid of cells. It can contain data, tables, charts, etc.',
				},
			],
			default: 'workbook',
		},
		...workbookOperations,
		...workbookFields,
		...worksheetOperations,
		...worksheetFields,
		...tableOperations,
		...tableFields,
	],
};
export class MicrosoftExcelV1 implements INodeType {
	description: INodeTypeDescription;

	constructor(baseDescription: INodeTypeBaseDescription) {
		this.description = {
			...baseDescription,
			...versionDescription,
		};
	}

	methods = {
		loadOptions: {
			// Get all the workbooks to display them to user so that he can
			// select them easily
			async getWorkbooks(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
				const qs: IDataObject = {
					select: 'id,name',
				};
				const returnData: INodePropertyOptions[] = [];
				const workbooks = await microsoftApiRequestAllItems.call(
					this,
					'value',
					'GET',
					"/drive/root/search(q='.xlsx')",
					{},
					qs,
				);
				for (const workbook of workbooks) {
					const workbookName = workbook.name;
					const workbookId = workbook.id;
					returnData.push({
						name: workbookName,
						value: workbookId,
					});
				}
				return returnData;
			},
			// Get all the worksheets to display them to user so that he can
			// select them easily
			async getworksheets(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
				const workbookId = this.getCurrentNodeParameter('workbook');
				const qs: IDataObject = {
					select: 'id,name',
				};
				const returnData: INodePropertyOptions[] = [];
				const worksheets = await microsoftApiRequestAllItems.call(
					this,
					'value',
					'GET',
					`/drive/items/${workbookId}/workbook/worksheets`,
					{},
					qs,
				);
				for (const worksheet of worksheets) {
					const worksheetName = worksheet.name;
					const worksheetId = worksheet.id;
					returnData.push({
						name: worksheetName,
						value: worksheetId,
					});
				}
				return returnData;
			},
			// Get all the tables to display them to user so that he can
			// select them easily
			async getTables(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
				const workbookId = this.getCurrentNodeParameter('workbook');
				const worksheetId = this.getCurrentNodeParameter('worksheet');
				const qs: IDataObject = {
					select: 'id,name',
				};
				const returnData: INodePropertyOptions[] = [];
				const tables = await microsoftApiRequestAllItems.call(
					this,
					'value',
					'GET',
					`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables`,
					{},
					qs,
				);
				for (const table of tables) {
					const tableName = table.name;
					const tableId = table.id;
					returnData.push({
						name: tableName,
						value: tableId,
					});
				}
				return returnData;
			},
		},
	};

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

		if (resource === 'table') {
			//https://docs.microsoft.com/en-us/graph/api/table-post-rows?view=graph-rest-1.0&tabs=http
			if (operation === 'addRow') {
				try {
					// TODO: At some point it should be possible to use item dependent parameters.
					//       Is however important to then not make one separate request each.
					const workbookId = this.getNodeParameter('workbook', 0) as string;
					const worksheetId = this.getNodeParameter('worksheet', 0) as string;
					const tableId = this.getNodeParameter('table', 0) as string;
					const additionalFields = this.getNodeParameter('additionalFields', 0);
					const body: IDataObject = {};

					if (additionalFields.index) {
						body.index = additionalFields.index as number;
					}

					// Get table columns to eliminate any columns not needed on the input
					responseData = await microsoftApiRequest.call(
						this,
						'GET',
						`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`,
						{},
						qs,
					);
					const columns = responseData.value.map((column: IDataObject) => column.name);

					const rows: any[][] = [];

					// Bring the items into the correct format
					for (const item of items) {
						const row = [];
						for (const column of columns) {
							row.push(item.json[column]);
						}
						rows.push(row);
					}

					body.values = rows;
					const { id } = await microsoftApiRequest.call(
						this,
						'POST',
						`/drive/items/${workbookId}/workbook/createSession`,
						{ persistChanges: true },
					);
					responseData = await microsoftApiRequest.call(
						this,
						'POST',
						`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows/add`,
						body,
						{},
						'',
						{ 'workbook-session-id': id },
					);
					await microsoftApiRequest.call(
						this,
						'POST',
						`/drive/items/${workbookId}/workbook/closeSession`,
						{},
						{},
						'',
						{ 'workbook-session-id': id },
					);

					const executionData = this.helpers.constructExecutionMetaData(
						this.helpers.returnJsonArray(responseData as IDataObject[]),
						{ itemData },
					);

					returnData.push(...executionData);
				} catch (error) {
					if (this.continueOnFail()) {
						const executionErrorData = this.helpers.constructExecutionMetaData(
							this.helpers.returnJsonArray({ error: error.message }),
							{ itemData },
						);
						returnData.push(...executionErrorData);
					} else {
						throw error;
					}
				}
			}
			//https://docs.microsoft.com/en-us/graph/api/table-list-columns?view=graph-rest-1.0&tabs=http
			if (operation === 'getColumns') {
				for (let i = 0; i < length; i++) {
					try {
						qs = {};
						const workbookId = this.getNodeParameter('workbook', i) as string;
						const worksheetId = this.getNodeParameter('worksheet', i) as string;
						const tableId = this.getNodeParameter('table', i) as string;
						const returnAll = this.getNodeParameter('returnAll', i);
						const rawData = this.getNodeParameter('rawData', i);
						if (rawData) {
							const filters = this.getNodeParameter('filters', i);
							if (filters.fields) {
								qs.$select = filters.fields;
							}
						}
						if (returnAll) {
							responseData = await microsoftApiRequestAllItemsSkip.call(
								this,
								'value',
								'GET',
								`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`,
								{},
								qs,
							);
						} else {
							qs.$top = this.getNodeParameter('limit', i);
							responseData = await microsoftApiRequest.call(
								this,
								'GET',
								`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`,
								{},
								qs,
							);
							responseData = responseData.value;
						}
						if (!rawData) {
							responseData = responseData.map((column: IDataObject) => ({ name: column.name }));
						} else {
							const dataProperty = this.getNodeParameter('dataProperty', i) as string;
							responseData = { [dataProperty]: responseData };
						}

						const executionData = this.helpers.constructExecutionMetaData(
							this.helpers.returnJsonArray(responseData as IDataObject[]),
							{ itemData: { item: i } },
						);

						returnData.push(...executionData);
					} catch (error) {
						if (this.continueOnFail()) {
							const executionErrorData = this.helpers.constructExecutionMetaData(
								this.helpers.returnJsonArray({ error: error.message }),
								{ itemData: { item: i } },
							);
							returnData.push(...executionErrorData);
							continue;
						}
						throw error;
					}
				}
			}
			//https://docs.microsoft.com/en-us/graph/api/table-list-rows?view=graph-rest-1.0&tabs=http
			if (operation === 'getRows') {
				for (let i = 0; i < length; i++) {
					qs = {};
					try {
						const workbookId = this.getNodeParameter('workbook', i) as string;
						const worksheetId = this.getNodeParameter('worksheet', i) as string;
						const tableId = this.getNodeParameter('table', i) as string;
						const returnAll = this.getNodeParameter('returnAll', i);
						const rawData = this.getNodeParameter('rawData', i);
						if (rawData) {
							const filters = this.getNodeParameter('filters', i);
							if (filters.fields) {
								qs.$select = filters.fields;
							}
						}
						if (returnAll) {
							responseData = await microsoftApiRequestAllItemsSkip.call(
								this,
								'value',
								'GET',
								`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows`,
								{},
								qs,
							);
						} else {
							const rowsQs = { ...qs };
							rowsQs.$top = this.getNodeParameter('limit', i);
							responseData = await microsoftApiRequest.call(
								this,
								'GET',
								`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows`,
								{},
								rowsQs,
							);
							responseData = responseData.value;
						}
						if (!rawData) {
							const columnsQs = { ...qs };
							columnsQs.$select = 'name';
							// TODO: That should probably be cached in the future
							let columns = await microsoftApiRequestAllItemsSkip.call(
								this,
								'value',
								'GET',
								`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`,
								{},
								columnsQs,
							);

							columns = (columns as IDataObject[]).map((column) => column.name);
							for (let index = 0; index < responseData.length; index++) {
								const object: IDataObject = {};
								for (let y = 0; y < columns.length; y++) {
									object[columns[y]] = responseData[index].values[0][y];
								}
								const executionData = this.helpers.constructExecutionMetaData(
									this.helpers.returnJsonArray({ ...object }),
									{ itemData: { item: index } },
								);

								returnData.push(...executionData);
							}
						} else {
							const dataProperty = this.getNodeParameter('dataProperty', i) as string;
							const executionData = this.helpers.constructExecutionMetaData(
								this.helpers.returnJsonArray({ [dataProperty]: responseData }),
								{ itemData: { item: i } },
							);

							returnData.push(...executionData);
						}
					} catch (error) {
						if (this.continueOnFail()) {
							const executionErrorData = this.helpers.constructExecutionMetaData(
								this.helpers.returnJsonArray({ error: error.message }),
								{ itemData: { item: i } },
							);
							returnData.push(...executionErrorData);
							continue;
						}
						throw error;
					}
				}
			}
			if (operation === 'lookup') {
				for (let i = 0; i < length; i++) {
					qs = {};
					try {
						const workbookId = this.getNodeParameter('workbook', i) as string;
						const worksheetId = this.getNodeParameter('worksheet', i) as string;
						const tableId = this.getNodeParameter('table', i) as string;
						const lookupColumn = this.getNodeParameter('lookupColumn', i) as string;
						const lookupValue = this.getNodeParameter('lookupValue', i) as string;
						const options = this.getNodeParameter('options', i);

						responseData = await microsoftApiRequestAllItemsSkip.call(
							this,
							'value',
							'GET',
							`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows`,
							{},
							{},
						);

						qs.$select = 'name';
						// TODO: That should probably be cached in the future
						let columns = await microsoftApiRequestAllItemsSkip.call(
							this,
							'value',
							'GET',
							`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`,
							{},
							qs,
						);
						columns = columns.map((column: IDataObject) => column.name);

						if (!columns.includes(lookupColumn)) {
							throw new NodeApiError(this.getNode(), responseData as JsonObject, {
								message: `Column ${lookupColumn} does not exist on the table selected`,
							});
						}

						result.length = 0;
						for (let index = 0; index < responseData.length; index++) {
							const object: IDataObject = {};
							for (let y = 0; y < columns.length; y++) {
								object[columns[y]] = responseData[index].values[0][y];
							}
							result.push({ ...object });
						}

						if (options.returnAllMatches) {
							responseData = result.filter((data: IDataObject) => {
								return data[lookupColumn]?.toString() === lookupValue;
							});
							const executionData = this.helpers.constructExecutionMetaData(
								this.helpers.returnJsonArray(responseData),
								{ itemData: { item: i } },
							);

							returnData.push(...executionData);
						} else {
							responseData = result.find((data: IDataObject) => {
								return data[lookupColumn]?.toString() === lookupValue;
							});
							const executionData = this.helpers.constructExecutionMetaData(
								this.helpers.returnJsonArray(responseData as IDataObject),
								{ itemData: { item: i } },
							);

							returnData.push(...executionData);
						}
					} catch (error) {
						if (this.continueOnFail()) {
							const executionErrorData = this.helpers.constructExecutionMetaData(
								this.helpers.returnJsonArray({ error: error.message }),
								{ itemData: { item: i } },
							);
							returnData.push(...executionErrorData);
							continue;
						}
						throw error;
					}
				}
			}
		}
		if (resource === 'workbook') {
			for (let i = 0; i < length; i++) {
				qs = {};
				try {
					//https://docs.microsoft.com/en-us/graph/api/worksheetcollection-add?view=graph-rest-1.0&tabs=http
					if (operation === 'addWorksheet') {
						const workbookId = this.getNodeParameter('workbook', i) as string;
						const additionalFields = this.getNodeParameter('additionalFields', i);
						const body: IDataObject = {};
						if (additionalFields.name) {
							body.name = additionalFields.name;
						}
						const { id } = await microsoftApiRequest.call(
							this,
							'POST',
							`/drive/items/${workbookId}/workbook/createSession`,
							{ persistChanges: true },
						);
						responseData = await microsoftApiRequest.call(
							this,
							'POST',
							`/drive/items/${workbookId}/workbook/worksheets/add`,
							body,
							{},
							'',
							{ 'workbook-session-id': id },
						);
						await microsoftApiRequest.call(
							this,
							'POST',
							`/drive/items/${workbookId}/workbook/closeSession`,
							{},
							{},
							'',
							{ 'workbook-session-id': id },
						);
					}
					if (operation === 'getAll') {
						const returnAll = this.getNodeParameter('returnAll', i);
						const filters = this.getNodeParameter('filters', i);
						if (filters.fields) {
							qs.$select = filters.fields;
						}
						if (returnAll) {
							responseData = await microsoftApiRequestAllItems.call(
								this,
								'value',
								'GET',
								"/drive/root/search(q='.xlsx')",
								{},
								qs,
							);
						} else {
							qs.$top = this.getNodeParameter('limit', i);
							responseData = await microsoftApiRequest.call(
								this,
								'GET',
								"/drive/root/search(q='.xlsx')",
								{},
								qs,
							);
							responseData = responseData.value;
						}
					}

					if (Array.isArray(responseData)) {
						const executionData = this.helpers.constructExecutionMetaData(
							this.helpers.returnJsonArray(responseData),
							{ itemData: { item: i } },
						);

						returnData.push(...executionData);
					} else if (responseData !== undefined) {
						const executionData = this.helpers.constructExecutionMetaData(
							this.helpers.returnJsonArray(responseData as IDataObject[]),
							{ itemData: { item: i } },
						);

						returnData.push(...executionData);
					}
				} catch (error) {
					if (this.continueOnFail()) {
						const executionErrorData = this.helpers.constructExecutionMetaData(
							this.helpers.returnJsonArray({ error: error.message }),
							{ itemData: { item: i } },
						);
						returnData.push(...executionErrorData);
						continue;
					}
					throw error;
				}
			}
		}
		if (resource === 'worksheet') {
			for (let i = 0; i < length; i++) {
				qs = {};
				try {
					//https://docs.microsoft.com/en-us/graph/api/workbook-list-worksheets?view=graph-rest-1.0&tabs=http
					if (operation === 'getAll') {
						const returnAll = this.getNodeParameter('returnAll', i);
						const workbookId = this.getNodeParameter('workbook', i) as string;
						const filters = this.getNodeParameter('filters', i);
						if (filters.fields) {
							qs.$select = filters.fields;
						}
						if (returnAll) {
							responseData = await microsoftApiRequestAllItems.call(
								this,
								'value',
								'GET',
								`/drive/items/${workbookId}/workbook/worksheets`,
								{},
								qs,
							);
						} else {
							qs.$top = this.getNodeParameter('limit', i);
							responseData = await microsoftApiRequest.call(
								this,
								'GET',
								`/drive/items/${workbookId}/workbook/worksheets`,
								{},
								qs,
							);
							responseData = responseData.value;
						}
					}
					//https://docs.microsoft.com/en-us/graph/api/worksheet-range?view=graph-rest-1.0&tabs=http
					if (operation === 'getContent') {
						const workbookId = this.getNodeParameter('workbook', i) as string;
						const worksheetId = this.getNodeParameter('worksheet', i) as string;
						const range = this.getNodeParameter('range', i) as string;
						const rawData = this.getNodeParameter('rawData', i);
						if (rawData) {
							const filters = this.getNodeParameter('filters', i);
							if (filters.fields) {
								qs.$select = filters.fields;
							}
						}

						responseData = await microsoftApiRequest.call(
							this,
							'GET',
							`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${range}')`,
							{},
							qs,
						);

						if (!rawData) {
							const keyRow = this.getNodeParameter('keyRow', i) as number;
							const dataStartRow = this.getNodeParameter('dataStartRow', i) as number;
							if (responseData.values === null) {
								throw new NodeApiError(this.getNode(), responseData as JsonObject, {
									message: 'Range did not return data',
								});
							}
							const keyValues = responseData.values[keyRow];
							for (let index = dataStartRow; index < responseData.values.length; index++) {
								const object: IDataObject = {};
								for (let y = 0; y < keyValues.length; y++) {
									object[keyValues[y]] = responseData.values[index][y];
								}
								const executionData = this.helpers.constructExecutionMetaData(
									this.helpers.returnJsonArray({ ...object }),
									{ itemData: { item: index } },
								);

								returnData.push(...executionData);
							}
						} else {
							const dataProperty = this.getNodeParameter('dataProperty', i) as string;
							const executionData = this.helpers.constructExecutionMetaData(
								this.helpers.returnJsonArray({ [dataProperty]: responseData }),
								{ itemData: { item: i } },
							);

							returnData.push(...executionData);
						}
					}
				} catch (error) {
					if (this.continueOnFail()) {
						const executionErrorData = this.helpers.constructExecutionMetaData(
							this.helpers.returnJsonArray({ error: error.message }),
							{ itemData: { item: i } },
						);
						returnData.push(...executionErrorData);
						continue;
					}
					throw error;
				}
			}
		}

		return [returnData];
	}
}
