import { ValueOrPromise } from 'value-or-promise';
import { createGraphQLError, getOperationASTFromRequest, } from '@graphql-tools/utils';
import { fetch as defaultFetch } from '@whatwg-node/fetch';
import { createFormDataFromVariables } from './createFormDataFromVariables.js';
import { defaultPrintFn } from './defaultPrintFn.js';
import { handleEventStreamResponse } from './handleEventStreamResponse.js';
import { handleMultipartMixedResponse } from './handleMultipartMixedResponse.js';
import { isLiveQueryOperationDefinitionNode } from './isLiveQueryOperationDefinitionNode.js';
import { prepareGETUrl } from './prepareGETUrl.js';
export function buildHTTPExecutor(options) {
    const printFn = options?.print ?? defaultPrintFn;
    const executor = (request) => {
        const fetchFn = request.extensions?.fetch ?? options?.fetch ?? defaultFetch;
        let controller;
        let method = request.extensions?.method || options?.method;
        const operationAst = getOperationASTFromRequest(request);
        const operationType = operationAst.operation;
        if ((options?.useGETForQueries || request.extensions?.useGETForQueries) &&
            operationType === 'query') {
            method = 'GET';
        }
        let accept = 'application/graphql-response+json, application/json, multipart/mixed';
        if (operationType === 'subscription' || isLiveQueryOperationDefinitionNode(operationAst)) {
            method ||= 'GET';
            accept = 'text/event-stream';
        }
        else {
            method ||= 'POST';
        }
        const endpoint = request.extensions?.endpoint || options?.endpoint || '/graphql';
        const headers = { accept };
        if (options?.headers) {
            Object.assign(headers, typeof options?.headers === 'function' ? options.headers(request) : options?.headers);
        }
        if (request.extensions?.headers) {
            const { headers: headersFromExtensions, ...restExtensions } = request.extensions;
            Object.assign(headers, headersFromExtensions);
            request.extensions = restExtensions;
        }
        const query = printFn(request.document);
        let timeoutId;
        if (options?.timeout) {
            controller = new AbortController();
            timeoutId = setTimeout(() => {
                if (!controller?.signal.aborted) {
                    controller?.abort('timeout');
                }
            }, options.timeout);
        }
        const responseDetailsForError = {};
        return new ValueOrPromise(() => {
            switch (method) {
                case 'GET': {
                    const finalUrl = prepareGETUrl({
                        baseUrl: endpoint,
                        query,
                        variables: request.variables,
                        operationName: request.operationName,
                        extensions: request.extensions,
                    });
                    const fetchOptions = {
                        method: 'GET',
                        headers,
                        signal: controller?.signal,
                    };
                    if (options?.credentials != null) {
                        fetchOptions.credentials = options.credentials;
                    }
                    return fetchFn(finalUrl, fetchOptions, request.context, request.info);
                }
                case 'POST':
                    return new ValueOrPromise(() => createFormDataFromVariables({
                        query,
                        variables: request.variables,
                        operationName: request.operationName,
                        extensions: request.extensions,
                    }, {
                        File: options?.File,
                        FormData: options?.FormData,
                    }))
                        .then(body => {
                        if (typeof body === 'string' && !headers['content-type']) {
                            headers['content-type'] = 'application/json';
                        }
                        const fetchOptions = {
                            method: 'POST',
                            body,
                            headers,
                            signal: controller?.signal,
                        };
                        if (options?.credentials != null) {
                            fetchOptions.credentials = options.credentials;
                        }
                        return fetchFn(endpoint, fetchOptions, request.context, request.info);
                    })
                        .resolve();
            }
        })
            .then((fetchResult) => {
            responseDetailsForError.status = fetchResult.status;
            responseDetailsForError.statusText = fetchResult.statusText;
            if (timeoutId != null) {
                clearTimeout(timeoutId);
            }
            // Retry should respect HTTP Errors
            if (options?.retry != null && !fetchResult.status.toString().startsWith('2')) {
                throw new Error(fetchResult.statusText || `HTTP Error: ${fetchResult.status}`);
            }
            const contentType = fetchResult.headers.get('content-type');
            if (contentType?.includes('text/event-stream')) {
                return handleEventStreamResponse(fetchResult, controller);
            }
            else if (contentType?.includes('multipart/mixed')) {
                return handleMultipartMixedResponse(fetchResult, controller);
            }
            return fetchResult.text();
        })
            .then(result => {
            if (typeof result === 'string') {
                if (result) {
                    try {
                        const parsedResult = JSON.parse(result);
                        if (parsedResult.data == null &&
                            (parsedResult.errors == null || parsedResult.errors.length === 0)) {
                            return {
                                errors: [
                                    createGraphQLError('Unexpected empty "data" and "errors" fields', {
                                        extensions: {
                                            requestBody: {
                                                query,
                                                operationName: request.operationName,
                                            },
                                            responseDetails: responseDetailsForError,
                                        },
                                    }),
                                ],
                            };
                        }
                        return parsedResult;
                    }
                    catch (e) {
                        return {
                            errors: [
                                createGraphQLError(`Unexpected response: ${JSON.stringify(result)}`, {
                                    extensions: {
                                        requestBody: {
                                            query,
                                            operationName: request.operationName,
                                        },
                                        responseDetails: responseDetailsForError,
                                    },
                                    originalError: e,
                                }),
                            ],
                        };
                    }
                }
            }
            else {
                return result;
            }
        })
            .catch((e) => {
            if (typeof e === 'string') {
                return {
                    errors: [
                        createGraphQLError(e, {
                            extensions: {
                                requestBody: {
                                    query,
                                    operationName: request.operationName,
                                },
                                responseDetails: responseDetailsForError,
                            },
                        }),
                    ],
                };
            }
            else if (e.name === 'GraphQLError') {
                return {
                    errors: [e],
                };
            }
            else if (e.name === 'TypeError' && e.message === 'fetch failed') {
                return {
                    errors: [
                        createGraphQLError(`fetch failed to ${endpoint}`, {
                            extensions: {
                                requestBody: {
                                    query,
                                    operationName: request.operationName,
                                },
                                responseDetails: responseDetailsForError,
                            },
                            originalError: e,
                        }),
                    ],
                };
            }
            else if (e.name === 'AbortError' && controller?.signal?.reason) {
                return {
                    errors: [
                        createGraphQLError('The operation was aborted. reason: ' + controller.signal.reason, {
                            extensions: {
                                requestBody: {
                                    query,
                                    operationName: request.operationName,
                                },
                                responseDetails: responseDetailsForError,
                            },
                            originalError: e,
                        }),
                    ],
                };
            }
            else if (e.message) {
                return {
                    errors: [
                        createGraphQLError(e.message, {
                            extensions: {
                                requestBody: {
                                    query,
                                    operationName: request.operationName,
                                },
                                responseDetails: responseDetailsForError,
                            },
                            originalError: e,
                        }),
                    ],
                };
            }
            else {
                return {
                    errors: [
                        createGraphQLError('Unknown error', {
                            extensions: {
                                requestBody: {
                                    query,
                                    operationName: request.operationName,
                                },
                                responseDetails: responseDetailsForError,
                            },
                            originalError: e,
                        }),
                    ],
                };
            }
        })
            .resolve();
    };
    if (options?.retry != null) {
        return function retryExecutor(request) {
            let result;
            let attempt = 0;
            function retryAttempt() {
                attempt++;
                if (attempt > options.retry) {
                    if (result != null) {
                        return result;
                    }
                    return {
                        errors: [createGraphQLError('No response returned from fetch')],
                    };
                }
                return new ValueOrPromise(() => executor(request))
                    .then(res => {
                    result = res;
                    if (result?.errors?.length) {
                        return retryAttempt();
                    }
                    return result;
                })
                    .resolve();
            }
            return retryAttempt();
        };
    }
    return executor;
}
export { isLiveQueryOperationDefinitionNode };
