/// <reference lib="es2022" preserve="true" />
/// <reference lib="dom" preserve="true" />
import type { Dispatcher } from 'undici';
import type { ErrorData } from '../error/error.model.js';
import type { CommonLogger } from '../log/commonLogger.js';
import type { AnyObject, NumberOfMilliseconds, Promisable, Reviver, UnixTimestampMillis } from '../types.js';
import type { HttpMethod, HttpStatusFamily } from './http.model.js';
export interface FetcherNormalizedCfg extends Required<Omit<FetcherCfg, 'dispatcher' | 'name'>>, Omit<FetcherRequest, 'started' | 'fullUrl' | 'logRequest' | 'logRequestBody' | 'logResponse' | 'logResponseBody' | 'debug' | 'redirect' | 'credentials' | 'throwHttpErrors' | 'errorData'> {
    logger: CommonLogger;
    searchParams: Record<string, any>;
    name?: string;
}
export type FetcherBeforeRequestHook = (req: FetcherRequest) => Promisable<void>;
export type FetcherAfterResponseHook = <BODY = unknown>(res: FetcherResponse<BODY>) => Promisable<void>;
export type FetcherBeforeRetryHook = <BODY = unknown>(res: FetcherResponse<BODY>) => Promisable<void>;
/**
 * Allows to mutate the error.
 * Cannot cancel/prevent the error - AfterResponseHook can be used for that instead.
 */
export type FetcherOnErrorHook = (err: Error) => Promisable<void>;
/**
 * FetcherCfg: configuration of the Fetcher instance. One per instance.
 * FetcherOptions: options for a single request. One per request.
 */
export interface FetcherCfg {
    /**
     * Should **not** contain trailing slash.
     */
    baseUrl?: string;
    /**
     * "Name" of the fetcher.
     * Accessible inside HttpRequestError, to be able to construct a good fingerprint.
     * If name is not provided - baseUrl is used to identify a Fetcher.
     */
    name?: string;
    /**
     * Default rule is that you **are allowed** to mutate req, res, res.retryStatus
     * properties of hook function arguments.
     * If you throw an error from the hook - it will be re-thrown as-is.
     */
    hooks?: {
        /**
         * Allows to mutate req.
         */
        beforeRequest?: FetcherBeforeRequestHook[];
        /**
         * Allows to mutate res.
         * If you set `res.err` - it will be thrown.
         */
        afterResponse?: FetcherAfterResponseHook[];
        /**
         * Allows to mutate res.retryStatus to override retry behavior.
         */
        beforeRetry?: FetcherBeforeRetryHook[];
        onError?: FetcherOnErrorHook[];
    };
    /**
     * If Fetcher has an error - `errorData` object will be appended to the error data.
     * Like this:
     *
     * _errorDataAppend(err, cfg.errorData)
     *
     * So you, for example, can append a `fingerprint` to any error thrown from this fetcher.
     */
    errorData?: ErrorData | undefined;
    /**
     * If true - enables all possible logging.
     */
    debug?: boolean;
    logRequest?: boolean;
    logRequestBody?: boolean;
    logResponse?: boolean;
    logResponseBody?: boolean;
    /**
     * Controls if `baseUrl` should be included in logs (both success and error).
     *
     * Defaults to `true` on ServerSide and `false` on ClientSide.
     *
     * Reasoning.
     *
     * ClientSide often uses one main "backend host".
     * Not including baseUrl improves Sentry error grouping.
     *
     * ServerSide often uses one Fetcher instance per 3rd-party API.
     * Not including baseUrl can introduce confusion of "which API is it?".
     */
    logWithBaseUrl?: boolean;
    /**
     * Default to true.
     * Set to false to strip searchParams from url when logging (both success and error)
     */
    logWithSearchParams?: boolean;
    /**
     * Defaults to `console`.
     */
    logger?: CommonLogger;
    throwHttpErrors?: boolean;
    /**
     * Pass an Undici Dispatcher.
     * (Node.js only)
     *
     * @experimental
     */
    dispatcher?: Dispatcher;
}
export interface FetcherRetryStatus {
    retryAttempt: number;
    retryTimeout: NumberOfMilliseconds;
    retryStopped: boolean;
}
export interface FetcherRetryOptions {
    count: number;
    timeout: NumberOfMilliseconds;
    timeoutMax: NumberOfMilliseconds;
    timeoutMultiplier: number;
}
export interface FetcherRequest extends Omit<FetcherOptions, 'method' | 'headers' | 'baseUrl' | 'url'> {
    /**
     * inputUrl is only the part that was passed in the request,
     * without baseUrl or searchParams.
     */
    inputUrl: string;
    /**
     * fullUrl includes baseUrl and searchParams.
     */
    fullUrl: string;
    init: RequestInitNormalized;
    responseType: FetcherResponseType;
    timeoutSeconds: number;
    retry: FetcherRetryOptions;
    retryPost: boolean;
    retry3xx: boolean;
    retry4xx: boolean;
    retry5xx: boolean;
    started: UnixTimestampMillis;
}
export interface FetcherGraphQLOptions extends FetcherOptions {
    query: string;
    variables?: AnyObject;
    /**
     * When querying singular entities, it may be convenient to specify 1st level object to unwrap.
     * Example:
     * {
     *   homePage: { ... }
     * }
     *
     * unwrapObject: 'homePage'
     *
     * would return the contents of `{ ... }`
     */
    unwrapObject?: string;
}
/**
 * FetcherCfg: configuration of the Fetcher instance. One per instance.
 * FetcherOptions: options for a single request. One per request.
 */
export interface FetcherOptions {
    method?: HttpMethod;
    /**
     * If defined - this `url` will override the original given `url`.
     * baseUrl (and searchParams) will still modify it.
     */
    url?: string;
    baseUrl?: string;
    /**
     * Default: 30.
     *
     * Timeout applies to both get the response and retrieve the body (e.g `await res.json()`),
     * so both should finish within this single timeout (not each).
     */
    timeoutSeconds?: number;
    /**
     * AbortSignal to allow the caller to abort the request.
     * If `timeoutSeconds` is also set, the signals are combined via `AbortSignal.any()`,
     * so the request aborts on whichever fires first.
     */
    signal?: AbortSignal;
    /**
     * Supports all the types that RequestInit.body supports.
     *
     * Useful when you want to e.g pass FormData.
     */
    body?: Blob | BufferSource | FormData | URLSearchParams | string;
    /**
     * Same as `body`, but also conveniently sets the
     * Content-Type header to `text/plain`
     */
    text?: string;
    /**
     * Same as `body`, but:
     * 1. JSON.stringifies the passed variable
     * 2. Conveniently sets the Content-Type header to `application/json`
     */
    json?: any;
    /**
     * Same as `body`, but:
     * 1. Transforms the passed plain js object into URLSearchParams and passes it to `body`
     * 2. Conveniently sets the Content-Type header to `application/x-www-form-urlencoded`
     */
    form?: FormData | URLSearchParams | AnyObject;
    credentials?: RequestCredentials;
    /**
     * Default to 'follow'.
     * 'error' would throw on redirect.
     * 'manual' will not throw, but return !ok response with 3xx status.
     */
    redirect?: RequestRedirect;
    /**
     * Default to false.
     * When set to true, the request will not be aborted when the page is unloaded.
     * Useful for sending analytics or tracking events that need to complete even if the user navigates away.
     */
    keepalive?: boolean;
    headers?: Record<string, any>;
    responseType?: FetcherResponseType;
    searchParams?: Record<string, any>;
    /**
     * Default is 2 retries (3 tries in total).
     * Pass `retry: { count: 0 }` to disable retries.
     */
    retry?: Partial<FetcherRetryOptions>;
    /**
     * Defaults to false.
     * Set to true to allow retrying `post` requests.
     */
    retryPost?: boolean;
    /**
     * Defaults to false.
     */
    retry3xx?: boolean;
    /**
     * Defaults to false.
     */
    retry4xx?: boolean;
    /**
     * Defaults to true.
     */
    retry5xx?: boolean;
    jsonReviver?: Reviver;
    logRequest?: boolean;
    logRequestBody?: boolean;
    logResponse?: boolean;
    logResponseBody?: boolean;
    /**
     * If true - enables all possible logging.
     */
    debug?: boolean;
    /**
     * If provided - will be used instead of `globalThis.fetch`.
     * Can be used e.g to pass a `fetch` function from `undici` (in Node.js).
     *
     * This function IS called from `Fetcher.callNativeFetch`, so
     * when `callNativeFetch` is mocked - fetchFn is NOT called.
     */
    fetchFn?: FetchFunction;
    /**
     * Allows to provide a fetch function that is NOT mocked by `Fetcher.callNativeFetch`.
     *
     * By default - consider `fetchFn`, that's what you would need most of the time.
     *
     * If you want to pass a fetch function that is NOT mockable - use `overrideFetchFn`.
     * Example of where it is useful: in backend resourceTestService, which still needs to call
     * native fetch, while allowing unit tests' fetch calls to be mocked.
     */
    overrideFetchFn?: FetchFunction;
    /**
     * Default to true.
     * Set to false to not throw on `!Response.ok`, but simply return `Response.body` as-is (json parsed, etc).
     */
    throwHttpErrors?: boolean;
    /**
     * If Fetcher has an error - `errorData` object will be appended to the error data.
     * Like this:
     *
     * _errorDataAppend(err, cfg.errorData)
     *
     * So you, for example, can append a `fingerprint` to any error thrown from this fetcher.
     */
    errorData?: ErrorData;
    /**
     * Allows to mutate the error.
     * Cannot cancel/prevent the error - AfterResponseHook can be used for that instead.
     */
    onError?: FetcherOnErrorHook;
    /**
     * If provided - will be passed further to HttpRequestError if error happens,
     * allowing to construct an errorGroup/fingerprint to be able to group errors
     * related to "this type of request".
     */
    requestName?: string;
}
export type RequestInitNormalized = Omit<RequestInit, 'method' | 'headers'> & {
    method: HttpMethod;
    headers: Record<string, any>;
    dispatcher?: Dispatcher;
    keepalive?: boolean;
};
export interface FetcherSuccessResponse<BODY = unknown> {
    ok: true;
    err: undefined;
    fetchResponse: Response;
    body: BODY;
    req: FetcherRequest;
    statusCode: number;
    statusFamily?: HttpStatusFamily;
    retryStatus: FetcherRetryStatus;
    signature: string;
}
export interface FetcherErrorResponse<BODY = unknown> {
    ok: false;
    err: Error;
    fetchResponse?: Response;
    body?: BODY;
    req: FetcherRequest;
    statusCode?: number;
    statusFamily?: HttpStatusFamily;
    retryStatus: FetcherRetryStatus;
    signature: string;
}
export type FetcherResponse<BODY = unknown> = FetcherSuccessResponse<BODY> | FetcherErrorResponse<BODY>;
export type FetcherResponseType = 'json' | 'text' | 'void' | 'arrayBuffer' | 'blob' | 'readableStream';
/**
 * Signature for the `fetch` function.
 * Used to be able to override and provide a different implementation,
 * e.g when mocking.
 */
export type FetchFunction = (url: string, init: RequestInit) => Promise<Response>;
/**
 * A subset of RequestInit that would match both:
 *
 * 1. RequestInit from dom types
 * 2. RequestInit from undici types
 */
export interface RequestInitLike {
    method?: string;
    referrer?: string;
    keepalive?: boolean;
}
/**
 * A subset of Response type that matches both dom and undici types.
 */
export interface ResponseLike {
    ok: boolean;
    status: number;
    statusText: string;
}
export type GraphQLResponse<DATA> = GraphQLSuccessResponse<DATA> | GraphQLErrorResponse;
export interface GraphQLSuccessResponse<DATA> {
    data: DATA;
    errors: never;
}
export interface GraphQLErrorResponse {
    data: never;
    errors: GraphQLFormattedError[];
}
/**
 * Copy-pasted from `graphql` package, slimmed down.
 * See: https://spec.graphql.org/draft/#sec-Errors
 */
export interface GraphQLFormattedError {
    message: string;
}
