import { ref, onScopeDispose, shallowRef, unref, getCurrentInstance } from 'vue'
import type { Ref, ShallowRef } from 'vue'
import api from '@/api'
import request from '@/plugins/request'
import type { AxiosRequestConfig, Method } from 'axios'
import type { ApiKeys, ExtractApiParams, ExtractApiResponse } from '@/api/api'

interface ReturnType<K extends ApiKeys> {
  loading: Ref<boolean>
  error: ShallowRef<unknown>
  result: ShallowRef<ExtractApiResponse<K>>
  fetch(data?: ExtractApiParams<K>, config?: AxiosRequestConfig): Promise<ExtractApiResponse<K>>
  cancel: () => void
  reset: () => void
}

type HttpMethod = Exclude<Lowercase<Method>, 'purge' | 'link' | 'unlink'>

function isConfig(x?: string | AxiosRequestConfig): x is AxiosRequestConfig {
  return typeof x === 'object'
}

function useFetch<K extends ApiKeys>(key: K): ReturnType<K>
function useFetch<K extends ApiKeys>(key: K, config: AxiosRequestConfig, method?: HttpMethod): ReturnType<K>
function useFetch<K extends ApiKeys>(key: K, method: HttpMethod): ReturnType<K>
function useFetch<K extends ApiKeys>(k: K, ...args: any[]) {
  const loading = ref(false)
  const result = shallowRef<ExtractApiResponse<K> | null>(null)
  const error = shallowRef<unknown>()
  let abortcontroller: AbortController | null
  let method: HttpMethod = 'post'
  const config: AxiosRequestConfig = {}

  if (args.length > 0) {
    if (isConfig(args[0])) {
      Object.assign(config, args[0])
    } else {
      method = args[0] || method
    }
  }

  if (args.length > 1) {
    method = args[1]
  }

  const fetch = async (data?: ExtractApiParams<K>, fetchConfig: AxiosRequestConfig = {}) => {
    try {
      if (unref(loading)) {
        cancel()
      }

      abortcontroller = new AbortController()

      loading.value = true

      const apiConfig = api[k]

      const option: AxiosRequestConfig & { url: string } = {
        ...config,
        ...fetchConfig,
        ...{ signal: abortcontroller.signal },
        ...(isConfig(apiConfig) ? apiConfig : { url: apiConfig }),
      }

      const useData = ['post', 'put', 'patch'].includes(method.toLocaleLowerCase())

      option.data = { ...option.data, ...((useData && data) || {}) }
      option.params = { ...option.params, ...((!useData && data) || {}) }

      result.value = await request[method](option.url, useData ? option.data : option, option)
      return result.value
    } catch (failed) {
      error.value = failed
      return Promise.reject(failed)
    } finally {
      loading.value = false
    }
  }

  const cancel = () => {
    if (!abortcontroller) {
      return
    }
    if (!abortcontroller.signal.aborted) {
      abortcontroller.abort()
      abortcontroller = null
    }
  }

  const reset = () => {
    cancel()
    result.value = null
    error.value = null
  }

  if (getCurrentInstance()) {
    onScopeDispose(cancel)
  }

  return {
    loading,
    error,
    result,
    fetch,
    cancel,
    reset,
  }
}

export default useFetch
