import type {
	IBinaryKeyData,
	IDataObject,
	IExecuteFunctions,
	INodeExecutionData,
	INodeProperties,
	JsonObject,
} from 'n8n-workflow';
import { NodeApiError, NodeOperationError } from 'n8n-workflow';
import { microsoftApiRequest } from '../../transport';
import { messageRLC } from '../../descriptions';
import { updateDisplayOptions } from '@utils/utilities';

export const properties: INodeProperties[] = [
	messageRLC,
	{
		displayName: 'Input Data Field Name',
		name: 'binaryPropertyName',
		hint: 'The name of the input field containing the binary file data to be attached',
		type: 'string',
		required: true,
		default: 'data',
		placeholder: 'e.g. data',
	},
	{
		displayName: 'Options',
		name: 'options',
		type: 'collection',
		placeholder: 'Add Option',
		default: {},
		options: [
			{
				displayName: 'File Name',
				name: 'fileName',
				description:
					'Filename of the attachment. If not set will the file-name of the binary property be used, if it exists.',
				type: 'string',
				default: '',
			},
		],
	},
];

const displayOptions = {
	show: {
		resource: ['messageAttachment'],
		operation: ['add'],
	},
};

export const description = updateDisplayOptions(displayOptions, properties);

export async function execute(this: IExecuteFunctions, index: number, items: INodeExecutionData[]) {
	let responseData;

	const messageId = this.getNodeParameter('messageId', index, undefined, {
		extractValue: true,
	}) as string;

	const binaryPropertyName = this.getNodeParameter('binaryPropertyName', 0);
	const options = this.getNodeParameter('options', index);

	if (items[index].binary === undefined) {
		throw new NodeOperationError(this.getNode(), 'No binary data exists on item!');
	}

	if (
		items[index].binary &&
		(items[index].binary as IDataObject)[binaryPropertyName] === undefined
	) {
		throw new NodeOperationError(
			this.getNode(),
			`No binary data property "${binaryPropertyName}" does not exists on item!`,
			{ itemIndex: index },
		);
	}

	const binaryData = (items[index].binary as IBinaryKeyData)[binaryPropertyName];
	const dataBuffer = await this.helpers.getBinaryDataBuffer(index, binaryPropertyName);

	const fileName = options.fileName === undefined ? binaryData.fileName : options.fileName;

	if (!fileName) {
		throw new NodeOperationError(
			this.getNode(),
			'File name is not set. It has either to be set via "Additional Fields" or has to be set on the binary property!',
			{ itemIndex: index },
		);
	}

	// Check if the file is over 3MB big
	if (dataBuffer.length > 3e6) {
		// Maximum chunk size is 4MB
		const chunkSize = 4e6;
		const body: IDataObject = {
			AttachmentItem: {
				attachmentType: 'file',
				name: fileName,
				size: dataBuffer.length,
			},
		};

		// Create upload session
		responseData = await microsoftApiRequest.call(
			this,
			'POST',
			`/messages/${messageId}/attachments/createUploadSession`,
			body,
		);
		const uploadUrl = responseData.uploadUrl;

		if (uploadUrl === undefined) {
			throw new NodeApiError(this.getNode(), responseData as JsonObject, {
				message: 'Failed to get upload session',
			});
		}

		for (let bytesUploaded = 0; bytesUploaded < dataBuffer.length; bytesUploaded += chunkSize) {
			// Upload the file chunk by chunk
			const nextChunk = Math.min(bytesUploaded + chunkSize, dataBuffer.length);
			const contentRange = `bytes ${bytesUploaded}-${nextChunk - 1}/${dataBuffer.length}`;

			const data = dataBuffer.subarray(bytesUploaded, nextChunk);

			responseData = await this.helpers.request(uploadUrl, {
				method: 'PUT',
				headers: {
					'Content-Type': 'application/octet-stream',
					'Content-Length': data.length,
					'Content-Range': contentRange,
				},
				body: data,
			});
		}
	} else {
		const body: IDataObject = {
			'@odata.type': '#microsoft.graph.fileAttachment',
			name: fileName,
			contentBytes: binaryData.data,
		};

		responseData = await microsoftApiRequest.call(
			this,
			'POST',
			`/messages/${messageId}/attachments`,
			body,
			{},
		);
	}

	const executionData = this.helpers.constructExecutionMetaData(
		this.helpers.returnJsonArray({ success: true }),
		{ itemData: { item: index } },
	);

	return executionData;
}
