// api/monitoring/client/operations/getClients.methods.ts
import { IExecuteFunctions, INodeExecutionData, IDataObject } from 'n8n-workflow';
import { logger } from '../../../../helpers/logger';
import { handleApiError } from '../../../../helpers/errorHandler';
import { handlePagination } from '../../../../helpers/pagination';
import { apiRequest } from '../../../../helpers/apiRequest';

/**
 * Pagination using the last_client_mac cursor strategy required by the
 * Aruba Central v2 wireless client API.  The standard offset-based approach
 * causes a 500 error when the offset reaches 10,000, so subsequent pages must
 * be fetched by passing the last_client_mac returned in the previous response
 * while keeping offset=0 throughout.
 */
async function handleWirelessClientPagination(
	context: IExecuteFunctions,
	endpoint: string,
	qs: IDataObject,
	limit: number,
	pageSize: number = 100,
): Promise<INodeExecutionData[]> {
	const allItems: IDataObject[] = [];

	// Build the base query — offset stays 0 for every call
	const pageQs: IDataObject = { ...qs, offset: 0, limit: pageSize };
	delete pageQs.last_client_mac; // ensure we start fresh

	while (true) {
		logger.debug(
			'monitoring:client:wirelessPagination',
			`Fetching page, last_client_mac=${pageQs.last_client_mac ?? 'none'}, collected=${allItems.length}`,
		);

		const response = await apiRequest.call(context, 'GET', endpoint, {}, pageQs);

		const items: IDataObject[] = (response as IDataObject)?.clients as IDataObject[];

		if (!Array.isArray(items) || items.length === 0) {
			break;
		}

		if (limit > 0) {
			const remaining = limit - allItems.length;
			if (remaining <= 0) break;
			allItems.push(...items.slice(0, remaining));
			if (allItems.length >= limit) break;
		} else {
			allItems.push(...items);
		}

		const lastMac = (response as IDataObject)?.last_client_mac;

		// null (or missing) signals the final page
		if (lastMac === null || lastMac === undefined || lastMac === '') {
			break;
		}

		pageQs.last_client_mac = lastMac as string;
	}

	logger.debug(
		'monitoring:client:wirelessPagination',
		`Complete. Total items retrieved: ${allItems.length}`,
	);

	return allItems.map((item) => ({ json: item }));
}

/**
 * Valid client types for unified client operations
 */
const CLIENT_TYPES = {
	WIRELESS: 'WIRELESS',
	WIRED: 'WIRED',
} as const;

/**
 * Valid client statuses for unified client operations
 */
const CLIENT_STATUSES = {
	CONNECTED: 'CONNECTED',
	FAILED_TO_CONNECT: 'FAILED_TO_CONNECT',
} as const;

/**
 * Validates that only one of the mutually exclusive parameters is provided
 */
function validateMutuallyExclusiveParams(
	params: Record<string, any>,
	exclusiveParams: string[],
): string | null {
	const providedParams = exclusiveParams.filter(
		(param) => params[param] && params[param].toString().trim(),
	);

	if (providedParams.length > 1) {
		return `You can only specify one of: ${exclusiveParams.join(', ')}. Found: ${providedParams.join(', ')}`;
	}

	return null;
}

/**
 * Validates client type and status combination
 */
function validateClientTypeAndStatus(clientType: string, clientStatus: string): string | null {
	if (clientStatus === CLIENT_STATUSES.FAILED_TO_CONNECT && clientType !== CLIENT_TYPES.WIRELESS) {
		return 'Failed to connect status is not supported for wired clients';
	}

	return null;
}

/**
 * Get a list of Connected wireless clients from Aruba Central
 *
 * @param this The n8n execution context
 * @returns Formatted list of wireless clients
 */
export async function getWirelessClients(this: IExecuteFunctions): Promise<INodeExecutionData[]> {
	try {
		logger.debug('monitoring:client:getWirelessClients', 'Getting wireless clients');

		const returnAll = this.getNodeParameter('returnAll', 0, false) as boolean;
		const limit = this.getNodeParameter('limit', 0, 50) as number;

		const additionalFields = this.getNodeParameter('additionalFields', 0, {}) as IDataObject;

		const qs: IDataObject = { ...additionalFields };

		logger.debug('monitoring:client:getWirelessClients', `Query parameters: ${JSON.stringify(qs)}`);

		return await handlePagination.call(
			this,
			'/monitoring/v1/clients/wireless',
			'GET',
			{},
			qs,
			{ path: ['clients'], fallbackPaths: [['data'], ['items']] },
			returnAll,
			returnAll ? 0 : limit,
			limit,
		);
	} catch (error) {
		return handleApiError.call(this, error, 'Failed to get wireless clients');
	}
}

/**
 * Get a list of Connected wired clients from Aruba Central
 *
 * @param this The n8n execution context
 * @returns Formatted list of wired clients
 */
export async function getWiredClients(this: IExecuteFunctions): Promise<INodeExecutionData[]> {
	try {
		logger.debug('monitoring:client:getWiredClients', 'Getting wired clients');

		const returnAll = this.getNodeParameter('returnAll', 0, false) as boolean;
		const limit = this.getNodeParameter('limit', 0, 50) as number;

		const additionalFields = this.getNodeParameter('additionalFields', 0, {}) as IDataObject;

		const qs: IDataObject = { ...additionalFields };

		logger.debug('monitoring:client:getWiredClients', `Query parameters: ${JSON.stringify(qs)}`);

		return await handlePagination.call(
			this,
			'/monitoring/v1/clients/wired',
			'GET',
			{},
			qs,
			{ path: ['clients'], fallbackPaths: [['data'], ['items']] },
			returnAll,
			returnAll ? 0 : limit,
			limit,
		);
	} catch (error) {
		return handleApiError.call(this, error, 'Failed to get wired clients');
	}
}

/**
 * Get a list of unified clients from Aruba Central
 * This is a unified form of the wired and wireless client APIs
 *
 * @param this The n8n execution context
 * @returns Formatted list of unified clients
 */
export async function getUnifiedClients(this: IExecuteFunctions): Promise<INodeExecutionData[]> {
	try {
		logger.debug('monitoring:client:getUnifiedClients', 'Getting unified clients');

		const returnAll = this.getNodeParameter('returnAll', 0, false) as boolean;
		const limit = this.getNodeParameter('limit', 0, 50) as number;

		const clientType = this.getNodeParameter('client_type', 0) as string;
		const clientStatus = this.getNodeParameter('client_status', 0) as string;
		const timerange = this.getNodeParameter('timerange', 0) as string;

		// Validate client type and status combination
		const statusError = validateClientTypeAndStatus(clientType, clientStatus);
		if (statusError) {
			throw new Error(statusError);
		}

		const additionalFields = this.getNodeParameter('additionalFields', 0, {}) as IDataObject;

		// Validate mutually exclusive parameters
		const exclusiveError = validateMutuallyExclusiveParams(additionalFields as Record<string, any>, [
			'group',
			'swarm_id',
			'cluster_id',
			'network',
			'site',
			'label',
		]);
		if (exclusiveError) {
			throw new Error(exclusiveError);
		}

		// Validate wireless-only parameters
		if (clientType !== CLIENT_TYPES.WIRELESS) {
			if ((additionalFields.band as string)?.trim()) {
				throw new Error('Band filter is only supported for wireless clients');
			}
			if (additionalFields.show_signal_db !== undefined) {
				throw new Error('Show signal DB is only supported for wireless clients');
			}
		}

		// Validate wired-only parameters
		if (clientType !== CLIENT_TYPES.WIRED) {
			if ((additionalFields.stack_id as string)?.trim()) {
				throw new Error('Stack ID filter is only supported for wired clients');
			}
		}

		const qs: IDataObject = {
			client_type: clientType,
			client_status: clientStatus,
			timerange,
			...additionalFields,
		};

		logger.debug(
			'monitoring:client:getUnifiedClients',
			`Getting ${clientType.toLowerCase()} clients with status ${clientStatus}: ${JSON.stringify(qs)}`,
		);

		// The v2 wireless client endpoint returns a 500 error when the offset
		// reaches 10,000.  Use the last_client_mac cursor strategy instead of
		// standard offset pagination when fetching all wireless clients.
		if (clientType === CLIENT_TYPES.WIRELESS && returnAll) {
			return await handleWirelessClientPagination(this, '/monitoring/v2/clients', qs, 0, limit);
		}

		return await handlePagination.call(
			this,
			'/monitoring/v2/clients',
			'GET',
			{},
			qs,
			{ path: ['clients'], fallbackPaths: [['data'], ['items']] },
			returnAll,
			returnAll ? 0 : limit,
			limit,
		);
	} catch (error) {
		return handleApiError.call(this, error, 'Failed to get unified clients');
	}
}
