/* eslint-disable @typescript-eslint/no-explicit-any */
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, CancelTokenSource } from 'axios'
import type { Fn } from '@datago/utils'

export interface UseRequestReturn<T> {
  isLoading: boolean

  isFinished: boolean

  isAborted: boolean

  statusCode: number | null

  response: AxiosResponse | null

  error: any

  data: T | null

  abort: Fn

  execute: (throwOnFailed?: boolean) => Promise<any>

  // methods
  get(): UseRequestReturn<T>
  post(payload?: unknown, contentType?: ContentType): UseRequestReturn<T>
  patch(payload?: unknown, contentType?: ContentType): UseRequestReturn<T>
  put(payload?: unknown, contentType?: ContentType): UseRequestReturn<T>
  delete(payload?: unknown, contentType?: ContentType): UseRequestReturn<T>
}

export interface UseRequestOptions {
  instance?: AxiosInstance
  /**
   * Will automatically run Request when `useRequest` is used
   *
   *  @default true
   */
  immediate?: boolean
}

type RequestInterceptUse = AxiosInstance['interceptors']['request']['use']
type ResponseInterceptUse = AxiosInstance['interceptors']['response']['use']
export interface CreateRequestOptions {
  baseURL?: string
  timeout?: number
  requestIntercept?: (use: RequestInterceptUse) => void
  responseIntercept?: (use: ResponseInterceptUse) => void
}

export type RequestMethod = 'get' | 'post' | 'patch' | 'put' | 'delete'

export type ContentType = 'json' | 'text' | 'formData' | 'form'

const contentTypeMapping: Record<string, string> = {
  json: 'application/json',
  text: 'text/plain',
  formData: 'multipart/form-data',
  form: 'application/x-www-form-urlencoded',
}

function isRequestOption(obj: object): obj is UseRequestOptions {
  return ['immediate', 'beforeRequest', 'afterRequest', 'onRequestError'].some((key) => key in obj)
}

export function createRequest(options: CreateRequestOptions) {
  const { requestIntercept, responseIntercept, ..._options } = options
  const instance = axios.create(_options)
  if (requestIntercept) {
    requestIntercept(instance.interceptors.request.use.bind(instance.interceptors.request))
  }
  if (responseIntercept) {
    responseIntercept(instance.interceptors.response.use.bind(instance.interceptors.response))
  }

  function useFactoryRequest(url: string, ...args: any[]) {
    let options = { instance }
    let requestConfig = {}

    if (args.length > 0) {
      if (isRequestOption(args[0])) {
        options = { ...options, ...args[0] }
      } else {
        requestConfig = {
          ...args[0],
        }
      }
    }

    if (args.length > 1 && isRequestOption(args[1])) {
      options = { ...options, ...args[1] }
    }

    return useRequest(url, requestConfig, options)
  }

  return useFactoryRequest as typeof useRequest
}

export function useRequest<T>(url: string): UseRequestReturn<T>
export function useRequest<T>(url: string, useRequestOptions: UseRequestOptions): UseRequestReturn<T>
export function useRequest<T>(
  url: string,
  config: AxiosRequestConfig<T>,
  useRequestOptions?: UseRequestOptions
): UseRequestReturn<T>
export function useRequest<T>(url: string, ...args: any[]): UseRequestReturn<T> {
  let options: UseRequestOptions = { immediate: true }
  let config: AxiosRequestConfig = {}

  type InterConfig = { method: RequestMethod; payload: unknown; contentType?: ContentType }
  const interConfig: InterConfig = { method: 'get', payload: undefined as unknown }

  if (args.length > 0) {
    if (isRequestOption(args[0])) {
      options = { ...options, ...args[0] }
    } else {
      config = args[0]
    }
  }

  if (args.length > 1) {
    if (isRequestOption(args[1])) {
      options = { ...options, ...args[1] }
    }
  }

  const { instance = axios } = options

  const cancelTokenSource: CancelTokenSource = axios.CancelToken.source()

  config.cancelToken = cancelTokenSource.token

  const shell: UseRequestReturn<T> = {
    isLoading: false,
    isFinished: false,
    isAborted: false,

    statusCode: null,
    response: null,
    data: null,
    error: null,

    abort,
    execute,

    get: setMethod('get'),
    post: setMethod('post'),
    patch: setMethod('patch'),
    put: setMethod('put'),
    delete: setMethod('delete'),
  }

  function abort() {
    shell.isAborted = true
    shell.isFinished = true
    shell.isLoading = false

    cancelTokenSource.cancel()
  }

  function execute(throwOnFailed = false) {
    shell.isLoading = true
    shell.isFinished = false
    shell.isAborted = false
    shell.statusCode = null
    shell.error = null

    const defaultConfig: AxiosRequestConfig = {
      method: interConfig.method,
      headers: {},
    }

    if (interConfig.payload) {
      const headers = defaultConfig.headers as Record<string, any>
      if (interConfig.contentType) {
        headers['Content-Type'] = contentTypeMapping[interConfig.contentType] ?? interConfig.contentType
      }
      defaultConfig.data = interConfig.payload
    }
    return new Promise((resolve, reject) => {
      instance(url, {
        ...defaultConfig,
        ...config,
        headers: {
          ...defaultConfig.headers,
          ...config.headers,
        },
      })
        .then((response) => {
          shell.response = response
          shell.statusCode = response.status
          shell.data = response.data

          resolve(response)
        })
        .catch((error) => {
          shell.error = error

          if (throwOnFailed) {
            reject(error)
          } else {
            resolve(undefined)
          }
        })
        .finally(() => {
          shell.isLoading = false
          shell.isFinished = true
        })
    })
  }

  function setMethod(method: RequestMethod) {
    return (payload?: unknown, contentType?: ContentType) => {
      if (!shell.isLoading) {
        interConfig.method = method
        interConfig.payload = payload
        interConfig.contentType = contentType

        return shell as any
      }
      return undefined
    }
  }

  if (options.immediate) {
    setTimeout(execute, 0)
  }

  return shell as UseRequestReturn<T>
}
