// https://gist.github.com/reconbot/c888c0f5c4cc1ac60db14fa389259cec
import DataLoader from 'dataloader';
import { LRUCache } from 'lru-cache';
import type { Exchange, Operation } from 'urql';
import { map, pipe } from 'wonka';

interface BatchRequest {
	url: RequestInfo | URL;
	options?: RequestInit;
}

const batchFetch =
	(loader: DataLoader<BatchRequest, Response>): typeof fetch =>
	(url: RequestInfo | URL, options?: RequestInit) => {
		return loader.load({ url, options });
	};

const loadBatch = (fetcher: typeof fetch) => async (requests: Readonly<BatchRequest[]>) => {
	// if batch has just one item don't batch it
	if (requests.length === 1) return [await fetcher(requests[0].url, requests[0].options)];
	const requestBody = requests
		.map((req) => JSON.parse(req.options?.body?.toString() ?? '{}'))
		.map((body) => ({
			query: body.query,
			operationName: body.operationName,
			variables: body.variables,
			extensions: body.extensions,
		}));

	const response = await fetcher(requests[0].url, {
		...requests[0].options,
		body: JSON.stringify(requestBody),
	});

	const bodies: object[] = await response.json();
	const { status, statusText, ok, headers, url } = response;
	return bodies.map((body) => {
		return {
			url,
			headers,
			status,
			statusText,
			ok,
			json: async () => body,
			text: async () => JSON.stringify(body),
		} as Response;
	});
};

// You want to put your own logic here - I want to opt out of batching per `useQuery({ query, context: useMemo(() => ({ batch: false }), []) })`
// but you do you!
const shouldBatch = (operation: Operation): boolean => {
	return operation.kind === 'query' && (operation.context.batch ?? true);
};

export const batchFetchExchange =
	(options?: DataLoader.Options<BatchRequest, Response>, fetcher = fetch): Exchange =>
	({ forward }) => {
		const loader = new DataLoader(loadBatch(fetcher), {
			// short-lived cache
			cacheMap: new LRUCache<any, any>({ max: 2000, ttl: 60 * 1000 }),
			...options,
		});
		return (ops$) =>
			pipe(
				ops$,
				map((operation: Operation) => {
					const fetch = shouldBatch(operation) ? batchFetch(loader) : operation.context.fetch;
					return {
						...operation,
						context: {
							...operation.context,
							fetch,
						},
					};
				}),
				forward,
			);
	};
