import Axios from 'axios'
import { ResCode } from './constant'

import type { AxiosResponse, AxiosRequestConfig } from 'axios'
import { ui18n } from '@/lang'

export type ResErrorHandler = <D, T extends ResData<D>>(
  response: AxiosResponse<T>,
  data?: T
) => void
export type ReqErrorHandler = <T>(
  err: Error,
  response: AxiosRequestConfig<T>
) => void
export type ResData<T> = {
  code: ResCode
  message: string
  data: T
  msg: string
}
export type Hook = {
  before?: (config: AxiosRequestConfig) => void
  after?: (config: AxiosRequestConfig) => void
}

export const axiosFactory = () => {
  const axiosRaw = Axios.create()
  const axiosConfig = {
    token: localStorage.getItem('token'),
    unTokenSet: [] as string[],
    unReqErrorSet: [] as string[],
    unResErrorSet: [] as string[],
    resErrorHandler: [] as ResErrorHandler[],
    reqErrorHandler: [] as ReqErrorHandler[],
    unLoadingSet: [] as string[],
    hook: [] as Hook[]
  }

  type AxiosConfig = typeof axiosConfig
  type ExponseApi<K extends keyof AxiosConfig> = {
    set: (val: AxiosConfig[K]) => void
  } & (AxiosConfig[K] extends Array<any>
    ? {
        add: (...val: AxiosConfig[K]) => void
        del: (...val: AxiosConfig[K]) => void
      }
    : { del: () => void })

  const getExponseApi = <K extends keyof AxiosConfig>(
    key: K
  ): ExponseApi<K> => {
    let axiosObj = axiosConfig[key] as any[]
    const apis: any = {
      set(val: AxiosConfig[K]) {
        axiosObj = axiosConfig[key] = val as any
      }
    }

    if (Array.isArray(axiosObj)) {
      apis.add = (...val: any[]) => {
        axiosObj.push(...val)
      }
      apis.del = (...val: any[]) => {
        if (val) {
          apis.set(axiosObj.filter((item: any) => !val?.includes(item)) as any)
        } else {
          axiosObj.length = 0
        }
      }
    } else {
      apis.del = () => {
        axiosConfig[key] = undefined as any
      }
    }
    return apis
  }

  const getToken = () => axiosConfig.token
  const setToken = (token: string) => {
    localStorage.setItem('token', token)
    axiosConfig.token = token
  }
  const delToken = () => {
    localStorage.removeItem('token')
    axiosConfig.token = null
  }

  const {
    set: setUnsetTokenURLS,
    add: addUnsetTokenURLS,
    del: delUnsetTokenURLS
  } = getExponseApi('unTokenSet')

  const {
    set: setResErrorHandler,
    add: addResErrorHandler,
    del: delResErrorHandler
  } = getExponseApi('resErrorHandler')

  const {
    set: setUnsetReqErrorURLS,
    add: addUnsetReqErrorURLS,
    del: delUnsetReqErrorURLS
  } = getExponseApi('unReqErrorSet')

  const {
    set: setReqErrorHandler,
    add: addReqErrorHandler,
    del: delReqErrorHandler
  } = getExponseApi('reqErrorHandler')

  const {
    set: setUnsetResErrorURLS,
    add: addUnsetResErrorURLS,
    del: delUnsetResErrorURLS
  } = getExponseApi('unResErrorSet')

  const { set: setHook, add: addHook, del: delHook } = getExponseApi('hook')

  const setDefaultURI = (url: string) => {
    axiosRaw.defaults.baseURL = url
  }

  const matchURL = (urls: string[], config: AxiosRequestConfig<any>) =>
    config && config.url && urls.includes(config.url)

  const callErrorHandler = (key: 'req' | 'res', ...args: any[]) => {
    Promise.resolve().then(() => {
      const api = `${key}ErrorHandler`
      ;(axiosConfig as any)[api].forEach((handler: any) => handler(...args))
    })
  }

  axiosRaw.interceptors.request.use(config => {
    for (const hook of axiosConfig.hook) {
      hook.before && hook.before(config)
    }

    if (!matchURL(axiosConfig.unTokenSet, config)) {
      if (!axiosConfig.token) {
        if (!matchURL(axiosConfig.unReqErrorSet, config)) {
          const error = new Error(ui18n.t('sys.NO_ACCESS'))
          callErrorHandler('req', error, config)
          throw error
        }
      } else {
        config.headers = {
          ...config.headers,
          token: axiosConfig.token
        }
      }
    }
    return config
  })

  axiosRaw.interceptors.response.use(
    (response: AxiosResponse<ResData<any>>) => {
      for (const hook of axiosConfig.hook) {
        hook.after && hook.after(response.config)
      }

      if (matchURL(axiosConfig.unResErrorSet, response.config)) {
        return response
      }

      if (response.status !== 200) {
        callErrorHandler('res', response)
        throw new Error(response.statusText)
      } else if (response.data.code !== ResCode.SUCCESS) {
        callErrorHandler('res', response, response.data)
        if (response.data.code === ResCode.TOKEN_INVALID) {
          delToken()
        }
        throw new Error(response?.data?.message)
      } else {
        return response.data.data
      }
    },
    err => {
      for (const hook of axiosConfig.hook) {
        hook.after && hook.after(err.config)
      }
      if (!matchURL(axiosConfig.unResErrorSet, err.config)) {
        callErrorHandler('res', err.response)
      }
      throw new Error(err.response ? err.response.statusText : err)
    }
  )

  type AxiosProcess = {
    getUri(config?: AxiosRequestConfig): string
    request<R = any, D = any>(config: AxiosRequestConfig<D>): Promise<R>
    get<R = any, D = any>(
      url: string,
      config?: AxiosRequestConfig<D>
    ): Promise<R>
    delete<R = any, D = any>(
      url: string,
      config?: AxiosRequestConfig<D>
    ): Promise<R>
    head<R = any, D = any>(
      url: string,
      config?: AxiosRequestConfig<D>
    ): Promise<R>
    options<R = any, D = any>(
      url: string,
      config?: AxiosRequestConfig<D>
    ): Promise<R>
    post<R = any, D = any>(
      url: string,
      data?: D,
      config?: AxiosRequestConfig<D>
    ): Promise<R>
    put<R = any, D = any>(
      url: string,
      data?: D,
      config?: AxiosRequestConfig<D>
    ): Promise<R>
    patch<R = any, D = any>(
      url: string,
      data?: D,
      config?: AxiosRequestConfig<D>
    ): Promise<R>
    postForm<R = any, D = any>(
      url: string,
      data?: D,
      config?: AxiosRequestConfig<D>
    ): Promise<R>
    putForm<R = any, D = unknown>(
      url: string,
      data?: D,
      config?: AxiosRequestConfig<D>
    ): Promise<R>
    patchForm<R = any, D = any>(
      url: string,
      data?: D,
      config?: AxiosRequestConfig<D>
    ): Promise<R>
  }

  interface AxiosInstanceProcess extends AxiosProcess {
    <R = any>(config: AxiosRequestConfig): Promise<R>
    <R = any>(url: string, config?: AxiosRequestConfig): Promise<R>
  }

  const axios: AxiosInstanceProcess = axiosRaw as any

  return {
    axios,
    getToken,
    setToken,
    delToken,
    setUnsetTokenURLS,
    addUnsetTokenURLS,
    delUnsetTokenURLS,
    setResErrorHandler,
    addResErrorHandler,
    delResErrorHandler,
    setUnsetReqErrorURLS,
    addUnsetReqErrorURLS,
    delUnsetReqErrorURLS,
    setReqErrorHandler,
    addReqErrorHandler,
    delReqErrorHandler,
    setUnsetResErrorURLS,
    addUnsetResErrorURLS,
    delUnsetResErrorURLS,
    setDefaultURI,
    setHook,
    addHook,
    delHook
  }
}

export default axiosFactory
