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

import { getAuthToken } from "../core/auth.gen.js"
import type { QuerySerializerOptions } from "../core/bodySerializer.gen.js"
import { jsonBodySerializer } from "../core/bodySerializer.gen.js"
import {
  serializeArrayParam,
  serializeObjectParam,
  serializePrimitiveParam,
} from "../core/pathSerializer.gen.js"
import { getUrl } from "../core/utils.gen.js"
import type { Client, ClientOptions, Config, RequestOptions } from "./types.gen.js"

export const createQuerySerializer = <T = unknown>({
  allowReserved,
  array,
  object,
}: QuerySerializerOptions = {}) => {
  const querySerializer = (queryParams: T) => {
    const search: string[] = []
    if (queryParams && typeof queryParams === "object") {
      for (const name in queryParams) {
        const value = queryParams[name]

        if (value === undefined || value === null) {
          continue
        }

        if (Array.isArray(value)) {
          const serializedArray = serializeArrayParam({
            allowReserved,
            explode: true,
            name,
            style: "form",
            value,
            ...array,
          })
          if (serializedArray) search.push(serializedArray)
        } else if (typeof value === "object") {
          const serializedObject = serializeObjectParam({
            allowReserved,
            explode: true,
            name,
            style: "deepObject",
            value: value as Record<string, unknown>,
            ...object,
          })
          if (serializedObject) search.push(serializedObject)
        } else {
          const serializedPrimitive = serializePrimitiveParam({
            allowReserved,
            name,
            value: value as string,
          })
          if (serializedPrimitive) search.push(serializedPrimitive)
        }
      }
    }
    return search.join("&")
  }
  return querySerializer
}

/**
 * Infers parseAs value from provided Content-Type header.
 */
export const getParseAs = (contentType: string | null): Exclude<Config["parseAs"], "auto"> => {
  if (!contentType) {
    // If no Content-Type header is provided, the best we can do is return the raw response body,
    // which is effectively the same as the 'stream' option.
    return "stream"
  }

  const cleanContent = contentType.split(";")[0]?.trim()

  if (!cleanContent) {
    return
  }

  if (cleanContent.startsWith("application/json") || cleanContent.endsWith("+json")) {
    return "json"
  }

  if (cleanContent === "multipart/form-data") {
    return "formData"
  }

  if (
    ["application/", "audio/", "image/", "video/"].some((type) => cleanContent.startsWith(type))
  ) {
    return "blob"
  }

  if (cleanContent.startsWith("text/")) {
    return "text"
  }

  return
}

const checkForExistence = (
  options: Pick<RequestOptions, "auth" | "query"> & {
    headers: Headers
  },
  name?: string,
): boolean => {
  if (!name) {
    return false
  }
  if (
    options.headers.has(name) ||
    options.query?.[name] ||
    options.headers.get("Cookie")?.includes(`${name}=`)
  ) {
    return true
  }
  return false
}

export const setAuthParams = async ({
  security,
  ...options
}: Pick<Required<RequestOptions>, "security"> &
  Pick<RequestOptions, "auth" | "query"> & {
    headers: Headers
  }) => {
  for (const auth of security) {
    if (checkForExistence(options, auth.name)) {
      continue
    }

    const token = await getAuthToken(auth, options.auth)

    if (!token) {
      continue
    }

    const name = auth.name ?? "Authorization"

    switch (auth.in) {
      case "query":
        if (!options.query) {
          options.query = {}
        }
        options.query[name] = token
        break
      case "cookie":
        options.headers.append("Cookie", `${name}=${token}`)
        break
      case "header":
      default:
        options.headers.set(name, token)
        break
    }
  }
}

export const buildUrl: Client["buildUrl"] = (options) =>
  getUrl({
    baseUrl: options.baseUrl as string,
    path: options.path,
    query: options.query,
    querySerializer:
      typeof options.querySerializer === "function"
        ? options.querySerializer
        : createQuerySerializer(options.querySerializer),
    url: options.url,
  })

export const mergeConfigs = (a: Config, b: Config): Config => {
  const config = { ...a, ...b }
  if (config.baseUrl?.endsWith("/")) {
    config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1)
  }
  config.headers = mergeHeaders(a.headers, b.headers)
  return config
}

export const mergeHeaders = (
  ...headers: Array<Required<Config>["headers"] | undefined>
): Headers => {
  const mergedHeaders = new Headers()
  for (const header of headers) {
    if (!header || typeof header !== "object") {
      continue
    }

    const iterator = header instanceof Headers ? header.entries() : Object.entries(header)

    for (const [key, value] of iterator) {
      if (value === null) {
        mergedHeaders.delete(key)
      } else if (Array.isArray(value)) {
        for (const v of value) {
          mergedHeaders.append(key, v as string)
        }
      } else if (value !== undefined) {
        // assume object headers are meant to be JSON stringified, i.e. their
        // content value in OpenAPI specification is 'application/json'
        mergedHeaders.set(
          key,
          typeof value === "object" ? JSON.stringify(value) : (value as string),
        )
      }
    }
  }
  return mergedHeaders
}

type ErrInterceptor<Err, Res, Req, Options> = (
  error: Err,
  response: Res,
  request: Req,
  options: Options,
) => Err | Promise<Err>

type ReqInterceptor<Req, Options> = (request: Req, options: Options) => Req | Promise<Req>

type ResInterceptor<Res, Req, Options> = (
  response: Res,
  request: Req,
  options: Options,
) => Res | Promise<Res>

class Interceptors<Interceptor> {
  _fns: (Interceptor | null)[]

  constructor() {
    this._fns = []
  }

  clear() {
    this._fns = []
  }

  getInterceptorIndex(id: number | Interceptor): number {
    if (typeof id === "number") {
      return this._fns[id] ? id : -1
    } else {
      return this._fns.indexOf(id)
    }
  }
  exists(id: number | Interceptor) {
    const index = this.getInterceptorIndex(id)
    return !!this._fns[index]
  }

  eject(id: number | Interceptor) {
    const index = this.getInterceptorIndex(id)
    if (this._fns[index]) {
      this._fns[index] = null
    }
  }

  update(id: number | Interceptor, fn: Interceptor) {
    const index = this.getInterceptorIndex(id)
    if (this._fns[index]) {
      this._fns[index] = fn
      return id
    } else {
      return false
    }
  }

  use(fn: Interceptor) {
    this._fns = [...this._fns, fn]
    return this._fns.length - 1
  }
}

// `createInterceptors()` response, meant for external use as it does not
// expose internals
export interface Middleware<Req, Res, Err, Options> {
  error: Pick<Interceptors<ErrInterceptor<Err, Res, Req, Options>>, "eject" | "use">
  request: Pick<Interceptors<ReqInterceptor<Req, Options>>, "eject" | "use">
  response: Pick<Interceptors<ResInterceptor<Res, Req, Options>>, "eject" | "use">
}

// do not add `Middleware` as return type so we can use _fns internally
export const createInterceptors = <Req, Res, Err, Options>() => ({
  error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(),
  request: new Interceptors<ReqInterceptor<Req, Options>>(),
  response: new Interceptors<ResInterceptor<Res, Req, Options>>(),
})

const defaultQuerySerializer = createQuerySerializer({
  allowReserved: false,
  array: {
    explode: true,
    style: "form",
  },
  object: {
    explode: true,
    style: "deepObject",
  },
})

const defaultHeaders = {
  "Content-Type": "application/json",
}

export const createConfig = <T extends ClientOptions = ClientOptions>(
  override: Config<Omit<ClientOptions, keyof T> & T> = {},
): Config<Omit<ClientOptions, keyof T> & T> => ({
  ...jsonBodySerializer,
  headers: defaultHeaders,
  parseAs: "auto",
  querySerializer: defaultQuerySerializer,
  ...override,
})
