import { Client, PageCollection } from '@microsoft/microsoft-graph-client';
import { tryCatch } from '@activepieces/pieces-framework';
import { getGraphBaseUrl } from './microsoft-cloud';

type GraphRetryOptions = {
	maxRetries?: number;
	initialDelayMs?: number;
	maxDelayMs?: number;
};

export const createGraphClient = (accessToken: string, cloud?: string | null): Client => {
	return Client.initWithMiddleware({
		authProvider: {
			getAccessToken: () => Promise.resolve(accessToken),
		},
		baseUrl: getGraphBaseUrl(cloud),
	});
};

const delay = async (ms: number): Promise<void> =>
	new Promise((resolve) => setTimeout(resolve, ms));

const parseRetryAfterMs = (error: any): number | null => {
	const headers: Record<string, string> | undefined =
		error?.headers ?? error?.response?.headers ?? undefined;
	const retryAfter = headers?.['Retry-After'] ?? headers?.['retry-after'];
	if (!retryAfter) return null;
	const asNumber = Number(retryAfter);
	if (!Number.isNaN(asNumber)) return asNumber * 1000;
	// Retry-After can be HTTP-date; in that case, compute delta
	const retryDate = Date.parse(retryAfter);
	if (!Number.isNaN(retryDate)) {
		return Math.max(0, retryDate - Date.now());
	}
	return null;
};

const extractStatusCode = (error: any): number => {
	return (
		error?.statusCode ??
		error?.status ??
		error?.response?.status ??
		error?.response?.statusCode ??
		0
	);
};

const shouldRetry = (statusCode: number, code?: string): boolean => {
	// Retry on throttling and transient errors
	if (statusCode === 429 || statusCode === 503 || statusCode === 504) return true;
	// Optionally retry generic server errors
	if (statusCode >= 500 && statusCode < 600) return true;
	// Some SDKs surface codes like 'TooManyRequests'
	if (code && /too\s*many\s*requests/i.test(code)) return true;
	return false;
};

export const buildGraphErrorMessage = (error: any): string => {
	const status = extractStatusCode(error);
	const err = error?.body?.error ?? error?.error ?? {};
	const code = err?.code ?? error?.code;
	const message = err?.message ?? error?.message ?? 'Request failed';
	const inner = err?.innerError ?? err?.innererror ?? {};
	const requestId = inner?.['request-id'] ?? inner?.requestId ?? error?.requestId;
	return `Graph error (${status}${code ? ` ${code}` : ''})${requestId ? ` [request-id: ${requestId}]` : ''}: ${message}`;
};

export const resolveMeetingId = async ({
	client,
	identifierType,
	identifierValue,
}: {
	client: Client;
	identifierType: string;
	identifierValue: string;
}): Promise<string> => {
	if (identifierType === 'meetingId') return identifierValue;
	const filter =
		identifierType === 'joinWebUrl'
			? `JoinWebUrl eq '${identifierValue}'`
			: `joinMeetingIdSettings/joinMeetingId eq '${identifierValue}'`;
	const response = await client.api('/me/onlineMeetings').filter(filter).get();
	if (!response.value?.length) {
		throw new Error('No meeting found with the provided identifier.');
	}
	return response.value[0].id as string;
};

export const withGraphRetry = async <T>(
	requestFn: () => Promise<T>,
	options: GraphRetryOptions = {}
): Promise<T> => {
	const maxRetries = options.maxRetries ?? 3;
	const initialDelayMs = options.initialDelayMs ?? 1000;
	const maxDelayMs = options.maxDelayMs ?? 10000;

	let attempt = 0;
	let delayMs = initialDelayMs;

	while (attempt <= maxRetries) {
		try {
			return await requestFn();
		} catch (e: any) {
			const status = extractStatusCode(e);
			const code = e?.body?.error?.code ?? e?.error?.code ?? e?.code;
			if (attempt < maxRetries && shouldRetry(status, code)) {
				const retryAfterMs = parseRetryAfterMs(e);
				await delay(Math.min(retryAfterMs ?? delayMs, maxDelayMs));
				attempt++;
				if (!retryAfterMs) {
					delayMs = Math.min(delayMs * 2, maxDelayMs);
				}
				continue;
			}
			throw new Error(buildGraphErrorMessage(e));
		}
	}
	throw new Error("Unexpected error occured");
};

// Walks a paginated Graph list endpoint with retry, an item cap, and graceful failure.
// Returns whatever was collected before an error or the cap; the cap protects the 60s
// dropdown sandbox timeout from runaway pagination on accounts with very large lists.
export const paginateGraphList = async <T>(params: {
	client: Client;
	initialUrl: string;
	// Omit on endpoints that don't support OData $top (e.g. /me/joinedTeams).
	pageSize?: number;
	maxItems: number;
	expand?: string;
}): Promise<{ items: T[]; truncated: boolean; error: Error | null }> => {
	const { client, initialUrl, pageSize, maxItems, expand } = params;
	const items: T[] = [];
	const { error } = await tryCatch(async () => {
		let firstRequest = client.api(initialUrl);
		if (pageSize !== undefined) firstRequest = firstRequest.top(pageSize);
		if (expand) firstRequest = firstRequest.expand(expand);
		let response: PageCollection = await withGraphRetry(() => firstRequest.get());
		while (response.value && response.value.length > 0) {
			for (const item of response.value as T[]) {
				items.push(item);
				if (items.length >= maxItems) break;
			}
			if (items.length >= maxItems) break;
			const nextLink = response['@odata.nextLink'];
			if (!nextLink) break;
			response = await withGraphRetry(() => client.api(nextLink).get());
		}
	});
	return {
		items,
		truncated: items.length >= maxItems,
		error,
	};
};


