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

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

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 && opts.bodySerializer) {
      opts.serializedBody = opts.bodySerializer(opts.body)
    }

    // remove Content-Type header if body is empty to avoid sending invalid requests
    if (opts.serializedBody === 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: opts.serializedBody,
    }

    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 = await _fetch(request)

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

    const result = {
      request,
      response,
    }

    if (response.ok) {
      if (response.status === 204 || response.headers.get("Content-Length") === "0") {
        return opts.responseStyle === "data"
          ? {}
          : {
              data: {},
              ...result,
            }
      }

      const parseAs =
        (opts.parseAs === "auto" ? getParseAs(response.headers.get("Content-Type")) : opts.parseAs) ?? "json"

      let data: any
      switch (parseAs) {
        case "arrayBuffer":
        case "blob":
        case "formData":
        case "json":
        case "text":
          data = await response[parseAs]()
          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 makeMethod = (method: Required<Config>["method"]) => {
    const fn = (options: RequestOptions) => request({ ...options, method })
    fn.sse = 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,
        url,
      })
    }
    return fn
  }

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