import { IExecuteFunctions, IDataObject } from 'n8n-workflow';
import { apiRequest, apiRequestAllItems } from '../helpers/apiRequest';
import { handleProxMoxError, validateParameters } from '../helpers/errorHandler';
import { logger } from '../helpers/logger';

export async function executeBackupOperation(
	this: IExecuteFunctions,
	operation: string,
	i: number,
): Promise<IDataObject | IDataObject[]> {
	try {
		logger.operation('backup', operation, 'start', { operation });

		let response: IDataObject | IDataObject[];

		switch (operation) {
			case 'create':
				response = await createBackup.call(this, i);
				break;

			case 'restore':
				response = await restoreBackup.call(this, i);
				break;

			case 'getAll':
				response = await getAllBackups.call(this, i);
				break;

			case 'delete':
				response = await deleteBackup.call(this, i);
				break;

			case 'getJobs':
				response = await getBackupJobs.call(this, i);
				break;

			default:
				throw new Error(`Unknown backup operation: ${operation}`);
		}

		logger.operation('backup', operation, 'success', { operation });
		return response;
	} catch (error) {
		logger.operation('backup', operation, 'error', { operation, error: error.message });
		return handleProxMoxError.call(this, error, operation, 'backup');
	}
}

async function waitForTask(
	this: IExecuteFunctions,
	nodeName: string,
	taskId: string,
	timeout: number = 300,
): Promise<IDataObject> {
	const startTime = Date.now();
	const timeoutMs = timeout * 1000;

	logger.debug('task', `Waiting for task ${taskId} on node ${nodeName}`, { taskId, timeout });

	while (Date.now() - startTime < timeoutMs) {
		try {
			const taskStatus = await apiRequest.call(
				this,
				'GET',
				`/nodes/${nodeName}/tasks/${taskId}/status`,
			);

			if (taskStatus.status === 'stopped') {
				if (taskStatus.exitstatus === 'OK') {
					logger.debug('task', `Task ${taskId} completed successfully`);
					return taskStatus;
				} else {
					throw new Error(`Task failed: ${taskStatus.exitstatus}`);
				}
			}

			// Wait 5 seconds before checking again (backups can take a while)
			await new Promise((resolve) => setTimeout(resolve, 5000));
		} catch (error) {
			logger.error('task', `Error checking task status: ${error.message}`);
			throw error;
		}
	}

	throw new Error(`Task ${taskId} timed out after ${timeout} seconds`);
}

async function createBackup(this: IExecuteFunctions, i: number): Promise<IDataObject> {
	const nodeName = this.getNodeParameter('nodeName', i) as string;
	const vmId = this.getNodeParameter('vmId', i) as number;
	const storage = this.getNodeParameter('storage', i) as string;
	const waitForTask = this.getNodeParameter('waitForTask', i, true) as boolean;
	const taskTimeout = this.getNodeParameter('taskTimeout', i, 300) as number;
	const backupOptions = this.getNodeParameter('backupOptions', i, {}) as IDataObject;

	validateParameters.call(
		this,
		{ nodeName, vmId, storage },
		['nodeName', 'vmId', 'storage'],
		'create backup',
	);

	const body: IDataObject = {
		vmid: vmId,
		storage,
		...backupOptions,
	};

	// Set default backup options
	if (!body.compress) body.compress = 'zstd';
	if (!body.mode) body.mode = 'snapshot';
	if (body.includeRam) body.vmstate = 1;
	if (body.remove === undefined) body.remove = 1;

	logger.debug('backup', 'Creating backup', { nodeName, vmId, storage, options: backupOptions });

	const response = await apiRequest.call(this, 'POST', `/nodes/${nodeName}/vzdump`, body);

	if (waitForTask && response.data) {
		const taskResult = await waitForTask.call(this, nodeName, response.data, taskTimeout);
		return { ...response, taskResult };
	}

	return response;
}

async function restoreBackup(this: IExecuteFunctions, i: number): Promise<IDataObject> {
	const nodeName = this.getNodeParameter('nodeName', i) as string;
	const archive = this.getNodeParameter('archive', i) as string;
	const newVmId = this.getNodeParameter('newVmId', i) as number;
	const waitForTask = this.getNodeParameter('waitForTask', i, true) as boolean;
	const taskTimeout = this.getNodeParameter('taskTimeout', i, 300) as number;
	const restoreOptions = this.getNodeParameter('restoreOptions', i, {}) as IDataObject;

	validateParameters.call(
		this,
		{ nodeName, archive, newVmId },
		['nodeName', 'archive', 'newVmId'],
		'restore backup',
	);

	const body: IDataObject = {
		archive,
		vmid: newVmId,
		...restoreOptions,
	};

	// Set default restore options
	if (!body.storage) body.storage = 'local-lvm';
	if (body.start) body.start = 1;
	if (body.unique) body.unique = 1;
	if (body.force) body.force = 1;

	logger.debug('backup', 'Restoring backup', {
		nodeName,
		archive,
		newVmId,
		options: restoreOptions,
	});

	// Determine if it's a VM or Container backup based on the archive name
	const isContainer = archive.includes('vzdump-lxc-') || archive.includes('-ct-');
	const endpoint = isContainer ? `/nodes/${nodeName}/lxc` : `/nodes/${nodeName}/qemu`;

	const response = await apiRequest.call(this, 'POST', endpoint, body);

	if (waitForTask && response.data) {
		const taskResult = await waitForTask.call(this, nodeName, response.data, taskTimeout);
		return { ...response, taskResult };
	}

	return response;
}

async function getAllBackups(this: IExecuteFunctions, i: number): Promise<IDataObject[]> {
	const nodeName = this.getNodeParameter('nodeName', i) as string;
	const returnAll = this.getNodeParameter('returnAll', i) as boolean;
	const filterOptions = this.getNodeParameter('filterOptions', i, {}) as IDataObject;

	validateParameters.call(this, { nodeName }, ['nodeName'], 'get all backups');

	let qs: IDataObject = {
		content: filterOptions.content || 'backup',
	};

	// Add additional filters
	if (filterOptions.vmid) {
		qs.vmid = filterOptions.vmid;
	}

	logger.debug('backup', 'Getting all backups', { nodeName, filterOptions });

	// Get backups from all storages on the node
	const storagesResponse = await apiRequest.call(this, 'GET', `/nodes/${nodeName}/storage`);

	// Handle different response formats
	let storages: IDataObject[];
	if (Array.isArray(storagesResponse)) {
		storages = storagesResponse;
	} else if (storagesResponse && typeof storagesResponse === 'object' && storagesResponse.data) {
		storages = Array.isArray(storagesResponse.data)
			? storagesResponse.data
			: [storagesResponse.data];
	} else {
		logger.error('backup', 'Unexpected storages response format', { storagesResponse });
		throw new Error('Failed to get storage list - unexpected response format');
	}

	const allBackups: IDataObject[] = [];

	for (const storage of storages) {
		if (storage.content && (storage.content as string).includes('backup')) {
			try {
				const backups = await apiRequest.call(
					this,
					'GET',
					`/nodes/${nodeName}/storage/${storage.storage}/content`,
					{},
					qs,
				);
				if (Array.isArray(backups)) {
					allBackups.push(...backups.map((backup) => ({ ...backup, storage: storage.storage })));
				}
			} catch (error) {
				logger.warn(
					'backup',
					`Failed to get backups from storage ${storage.storage}: ${error.message}`,
				);
			}
		}
	}

	if (returnAll) {
		return allBackups;
	} else {
		const limit = this.getNodeParameter('limit', i) as number;
		return allBackups.slice(0, limit);
	}
}

async function deleteBackup(this: IExecuteFunctions, i: number): Promise<IDataObject> {
	const nodeName = this.getNodeParameter('nodeName', i) as string;
	const storage = this.getNodeParameter('storage', i) as string;
	const backupFile = this.getNodeParameter('backupFile', i) as string;

	validateParameters.call(
		this,
		{ nodeName, storage, backupFile },
		['nodeName', 'storage', 'backupFile'],
		'delete backup',
	);

	// The volume ID format is typically storage:path
	const volumeId = `${storage}:${backupFile}`;

	logger.debug('backup', 'Deleting backup', { nodeName, storage, backupFile, volumeId });

	return await apiRequest.call(
		this,
		'DELETE',
		`/nodes/${nodeName}/storage/${storage}/content/${encodeURIComponent(volumeId)}`,
	);
}

async function getBackupJobs(this: IExecuteFunctions, i: number): Promise<IDataObject[]> {
	const returnAll = this.getNodeParameter('returnAll', i) as boolean;

	logger.debug('backup', 'Getting backup jobs');

	if (returnAll) {
		return await apiRequestAllItems.call(this, 'GET', '/cluster/backup', {});
	} else {
		const limit = this.getNodeParameter('limit', i) as number;
		const response = await apiRequest.call(this, 'GET', '/cluster/backup', {}, { limit });
		return Array.isArray(response) ? response.slice(0, limit) : [response];
	}
}
