// This file is auto-generated by @hey-api/openapi-ts

import { createSseClient } from '../core/serverSentEvents.gen';
import type { HttpMethod } from '../core/types.gen';
import { getValidRequestBody } from '../core/utils.gen';
import type { Client, Config, RequestOptions, ResolvedRequestOptions } from './types.gen';
import {
  buildUrl,
  createConfig,
  createInterceptors,
  getParseAs,
  mergeConfigs,
  mergeHeaders,
  setAuthParams,
} from './utils.gen';

type ReqInit = Omit<RequestInit, 'body' | 'headers'> & {
  body?: any;
  headers: ReturnType<typeof mergeHeaders>;
};

export const createClient = (config: Config = {}): Client => {
  let _config = mergeConfigs(createConfig(), config);

  const getConfig = (): Config => ({ ..._config });

  const setConfig = (config: Config): Config => {
    _config = mergeConfigs(_config, config);
    return getConfig();
  };

  const interceptors = createInterceptors<Request, Response, unknown, ResolvedRequestOptions>();

  const beforeRequest = async (options: RequestOptions) => {
    const opts = {
      ..._config,
      ...options,
      fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
      headers: mergeHeaders(_config.headers, options.headers),
      serializedBody: undefined,
    };

    if (opts.security) {
      await setAuthParams({
        ...opts,
        security: opts.security,
      });
    }

    if (opts.requestValidator) {
      await opts.requestValidator(opts);
    }

    if (opts.body !== undefined && opts.bodySerializer) {
      opts.serializedBody = opts.bodySerializer(opts.body);
    }

    // remove Content-Type header if body is empty to avoid sending invalid requests
    if (opts.body === undefined || opts.serializedBody === '') {
      opts.headers.delete('Content-Type');
    }

    const url = buildUrl(opts);

    return { opts, url };
  };

  const request: Client['request'] = async (options) => {
    // @ts-expect-error
    const { opts, url } = await beforeRequest(options);
    const requestInit: ReqInit = {
      redirect: 'follow',
      ...opts,
      body: getValidRequestBody(opts),
    };

    let request = new Request(url, requestInit);

    for (const fn of interceptors.request.fns) {
      if (fn) {
        request = await fn(request, opts);
      }
    }

    // fetch must be assigned here, otherwise it would throw the error:
    // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
    const _fetch = opts.fetch!;
    let response: Response;

    try {
      response = await _fetch(request);
    } catch (error) {
      // Handle fetch exceptions (AbortError, network errors, etc.)
      let finalError = error;

      for (const fn of interceptors.error.fns) {
        if (fn) {
          finalError = (await fn(error, undefined as any, request, opts)) as unknown;
        }
      }

      finalError = finalError || ({} as unknown);

      if (opts.throwOnError) {
        throw finalError;
      }

      // Return error response
      return opts.responseStyle === 'data'
        ? undefined
        : {
            error: finalError,
            request,
            response: undefined as any,
          };
    }

    for (const fn of interceptors.response.fns) {
      if (fn) {
        response = await fn(response, request, opts);
      }
    }

    const result = {
      request,
      response,
    };

    if (response.ok) {
      const parseAs =
        (opts.parseAs === 'auto'
          ? getParseAs(response.headers.get('Content-Type'))
          : opts.parseAs) ?? 'json';

      if (response.status === 204 || response.headers.get('Content-Length') === '0') {
        let emptyData: any;
        switch (parseAs) {
          case 'arrayBuffer':
          case 'blob':
          case 'text':
            emptyData = await response[parseAs]();
            break;
          case 'formData':
            emptyData = new FormData();
            break;
          case 'stream':
            emptyData = response.body;
            break;
          case 'json':
          default:
            emptyData = {};
            break;
        }
        return opts.responseStyle === 'data'
          ? emptyData
          : {
              data: emptyData,
              ...result,
            };
      }

      let data: any;
      switch (parseAs) {
        case 'arrayBuffer':
        case 'blob':
        case 'formData':
        case 'text':
          data = await response[parseAs]();
          break;
        case 'json': {
          // Some servers return 200 with no Content-Length and empty body.
          // response.json() would throw; read as text and parse if non-empty.
          const text = await response.text();
          data = text ? JSON.parse(text) : {};
          break;
        }
        case 'stream':
          return opts.responseStyle === 'data'
            ? response.body
            : {
                data: response.body,
                ...result,
              };
      }

      if (parseAs === 'json') {
        if (opts.responseValidator) {
          await opts.responseValidator(data);
        }

        if (opts.responseTransformer) {
          data = await opts.responseTransformer(data);
        }
      }

      return opts.responseStyle === 'data'
        ? data
        : {
            data,
            ...result,
          };
    }

    const textError = await response.text();
    let jsonError: unknown;

    try {
      jsonError = JSON.parse(textError);
    } catch {
      // noop
    }

    const error = jsonError ?? textError;
    let finalError = error;

    for (const fn of interceptors.error.fns) {
      if (fn) {
        finalError = (await fn(error, response, request, opts)) as string;
      }
    }

    finalError = finalError || ({} as string);

    if (opts.throwOnError) {
      throw finalError;
    }

    // TODO: we probably want to return error and improve types
    return opts.responseStyle === 'data'
      ? undefined
      : {
          error: finalError,
          ...result,
        };
  };

  const makeMethodFn = (method: Uppercase<HttpMethod>) => (options: RequestOptions) =>
    request({ ...options, method });

  const makeSseFn = (method: Uppercase<HttpMethod>) => async (options: RequestOptions) => {
    const { opts, url } = await beforeRequest(options);
    return createSseClient({
      ...opts,
      body: opts.body as BodyInit | null | undefined,
      headers: opts.headers as unknown as Record<string, string>,
      method,
      onRequest: async (url, init) => {
        let request = new Request(url, init);
        for (const fn of interceptors.request.fns) {
          if (fn) {
            request = await fn(request, opts);
          }
        }
        return request;
      },
      serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined,
      url,
    });
  };

  return {
    buildUrl,
    connect: makeMethodFn('CONNECT'),
    delete: makeMethodFn('DELETE'),
    get: makeMethodFn('GET'),
    getConfig,
    head: makeMethodFn('HEAD'),
    interceptors,
    options: makeMethodFn('OPTIONS'),
    patch: makeMethodFn('PATCH'),
    post: makeMethodFn('POST'),
    put: makeMethodFn('PUT'),
    request,
    setConfig,
    sse: {
      connect: makeSseFn('CONNECT'),
      delete: makeSseFn('DELETE'),
      get: makeSseFn('GET'),
      head: makeSseFn('HEAD'),
      options: makeSseFn('OPTIONS'),
      patch: makeSseFn('PATCH'),
      post: makeSseFn('POST'),
      put: makeSseFn('PUT'),
      trace: makeSseFn('TRACE'),
    },
    trace: makeMethodFn('TRACE'),
  } as Client;
};
