import { merge } from 'es-toolkit/compat';
import { hasFiles } from './form.js';
import { HttpResponseError } from './http/errors.js';
import { fetchHttpClient } from './http/fetchClient.js';
/**
 * The configured HTTP client.
 */
let httpClient = fetchHttpClient;
/**
 * The configured base URL.
 */
let baseURL = undefined;
/**
 * The configured default timeout.
 */
let timeout = undefined;
/**
 * The configured credentials mode.
 */
let credentials = 'same-origin';
/**
 * The request fingerprint resolver.
 */
let requestFingerprintResolver = (config) => `${config.method}:${config.baseURL ?? baseURL ?? ''}${config.url}`;
/**
 * The precognition success resolver.
 */
let successResolver = (response) => response.status === 204 && response.headers['precognition-success'] === 'true';
/**
 * The abort controller cache.
 */
const abortControllers = {};
/**
 * The precognitive HTTP client instance.
 */
export const client = {
    get: (url, data = {}, config = {}) => request(mergeConfig('get', url, data, config)),
    post: (url, data = {}, config = {}) => request(mergeConfig('post', url, data, config)),
    patch: (url, data = {}, config = {}) => request(mergeConfig('patch', url, data, config)),
    put: (url, data = {}, config = {}) => request(mergeConfig('put', url, data, config)),
    delete: (url, data = {}, config = {}) => request(mergeConfig('delete', url, data, config)),
    useHttpClient(newHttpClient) {
        httpClient = newHttpClient;
        return client;
    },
    withBaseURL(url) {
        baseURL = url;
        return client;
    },
    withTimeout(duration) {
        timeout = duration;
        return client;
    },
    withCredentials(value) {
        credentials = typeof value === 'string' ? value : (value ? 'include' : 'omit');
        return client;
    },
    fingerprintRequestsUsing(callback) {
        requestFingerprintResolver = callback === null
            ? () => null
            : callback;
        return client;
    },
    determineSuccessUsing(callback) {
        successResolver = callback;
        return client;
    },
    withXsrfCookieName(name) {
        fetchHttpClient.setXsrfCookieName(name);
        return client;
    },
    withXsrfHeaderName(name) {
        fetchHttpClient.setXsrfHeaderName(name);
        return client;
    },
};
/**
 * Merge the client specified arguments with the provided configuration.
 */
const mergeConfig = (method, url, data, config) => ({
    url,
    method,
    ...config,
    ...(['get', 'delete'].includes(method) ? {
        params: merge({}, data, config?.params),
    } : {
        data: merge({}, data, config?.data),
    }),
});
/**
 * Send and handle a new request.
 */
const request = (userConfig = {}) => {
    const config = [
        resolveConfig,
        abortMatchingRequests,
        refreshAbortController,
    ].reduce((config, callback) => callback(config), userConfig);
    if ((config.onBefore ?? (() => true))() === false) {
        return Promise.resolve(null);
    }
    (config.onStart ?? (() => null))();
    return httpClient.request({
        method: config.method,
        url: config.url,
        baseURL: config.baseURL ?? baseURL,
        data: config.data,
        params: config.params,
        headers: config.headers,
        signal: config.signal,
        timeout: config.timeout,
        credentials,
    }).then(async (response) => {
        if (config.precognitive) {
            validatePrecognitionResponse(response);
        }
        const status = response.status;
        let payload = response;
        if (config.precognitive && config.onPrecognitionSuccess && successResolver(response)) {
            payload = await Promise.resolve(config.onPrecognitionSuccess(response) ?? payload);
        }
        if (config.onSuccess && isSuccess(status)) {
            payload = await Promise.resolve(config.onSuccess(payload) ?? payload);
        }
        const statusHandler = resolveStatusHandler(config, status)
            ?? ((response) => response);
        return statusHandler(payload) ?? payload;
    }, (error) => {
        if (isNotServerGeneratedError(error)) {
            return Promise.reject(error);
        }
        const httpError = error;
        if (config.precognitive) {
            validatePrecognitionResponse(httpError.response);
        }
        const statusHandler = resolveStatusHandler(config, httpError.response.status)
            ?? ((_, error) => Promise.reject(error));
        return statusHandler(httpError.response, httpError);
    }).finally(config.onFinish ?? (() => null));
};
/**
 * Resolve the configuration.
 */
const resolveConfig = (config) => {
    const only = config.only ?? config.validate;
    return {
        ...config,
        timeout: config.timeout ?? timeout,
        precognitive: config.precognitive !== false,
        fingerprint: typeof config.fingerprint === 'undefined'
            ? requestFingerprintResolver(config, httpClient)
            : config.fingerprint,
        headers: {
            ...config.headers,
            'Accept': 'application/json',
            'Content-Type': resolveContentType(config),
            ...config.precognitive !== false ? {
                Precognition: true,
            } : {},
            ...only ? {
                'Precognition-Validate-Only': Array.from(only).join(),
            } : {},
        },
    };
};
/**
 * Determine if the status is successful.
 */
const isSuccess = (status) => status >= 200 && status < 300;
/**
 * Abort an existing request with the same configured fingerprint.
 */
const abortMatchingRequests = (config) => {
    if (typeof config.fingerprint !== 'string') {
        return config;
    }
    abortControllers[config.fingerprint]?.abort();
    delete abortControllers[config.fingerprint];
    return config;
};
/**
 * Create and configure the abort controller for a new request.
 */
const refreshAbortController = (config) => {
    if (typeof config.fingerprint !== 'string'
        || config.signal
        || !config.precognitive) {
        return config;
    }
    abortControllers[config.fingerprint] = new AbortController;
    return {
        ...config,
        signal: abortControllers[config.fingerprint].signal,
    };
};
/**
 * Ensure that the response is a Precognition response.
 */
const validatePrecognitionResponse = (response) => {
    if (response.headers?.precognition !== 'true') {
        throw Error('Did not receive a Precognition response. Ensure you have the Precognition middleware in place for the route.');
    }
};
/**
 * Determine if the error was not triggered by a server response.
 */
const isNotServerGeneratedError = (error) => {
    return !(error instanceof HttpResponseError) || typeof error.response?.status !== 'number';
};
/**
 * Resolve the handler for the given HTTP response status.
 */
const resolveStatusHandler = (config, code) => ({
    401: config.onUnauthorized,
    403: config.onForbidden,
    404: config.onNotFound,
    409: config.onConflict,
    422: config.onValidationError,
    423: config.onLocked,
}[code]);
/**
 * Resolve the request's "Content-Type" header.
 */
const resolveContentType = (config) => (config.headers?.['Content-Type']
    ?? config.headers?.['Content-type']
    ?? config.headers?.['content-type']
    ?? (hasFiles(config.data) ? 'multipart/form-data' : 'application/json'));
/**
 * Resolve the url from a potential callback.
 */
export const resolveUrl = (url) => typeof url === 'string'
    ? url
    : url();
/**
 * Resolve the method from a potential callback.
 */
export const resolveMethod = (method) => typeof method === 'string'
    ? method.toLowerCase()
    : method();
