import axios, { AxiosInstance, AxiosError, AxiosRequestConfig, InternalAxiosRequestConfig, AxiosResponse } from 'axios'
import { showFullScreenLoading, tryHideFullScreenLoading } from '@/components/Loading/fullScreen'
import { LOGIN_URL } from '@/config'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ResultData } from '@/api/interface'
import { ResultEnum } from '@/enums/httpEnum'
import { checkStatus } from './helper/checkStatus'
import { AxiosCanceler } from './helper/axiosCancel'
import { useUserStore } from '@/stores/modules/user'
import router from '@/routers'
import { getToken } from '@/utils/token'
import { tansParams } from '@/utils/common'
import { nanoid } from 'nanoid'
import cache from '@/plugins/cache'
import { encryptBase64, encryptWithAes, generateAesKey, decryptWithAes, decryptBase64 } from '@/utils/crypto'
import { encrypt, decrypt } from '@/utils/jsEncrypt'
export interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig {
  loading?: boolean
  cancel?: boolean
  isEncrypt?: boolean
}

const config = {
  // 默认地址请求地址，可在 .env.** 文件中修改
  baseURL: import.meta.env.VITE_API_URL as string,
  // 设置超时时间
  timeout: ResultEnum.TIMEOUT as number,
  // 跨域时候允许携带凭证
  withCredentials: true
}
const encryptHeader = 'encrypt-key'
// 是否显示重新登录
export let isReLogin = { show: false }

export const globalHeaders = () => {
  return {
    Authorization: 'Bearer ' + getToken(),
    clientId: import.meta.env.VITE_APP_CLIENT_ID
  }
}
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
axios.defaults.headers['clientId'] = import.meta.env.VITE_APP_CLIENT_ID
const axiosCanceler = new AxiosCanceler()

class RequestHttp {
  service: AxiosInstance
  public constructor(config: AxiosRequestConfig) {
    // instantiation
    this.service = axios.create(config)

    /**
     * @description 请求拦截器
     * 客户端发送请求 -> [请求拦截器] -> 服务器
     * token校验(JWT) : 接受服务器返回的 token,存储到 vuex/pinia/本地储存当中
     */
    this.service.interceptors.request.use(
      (config: CustomAxiosRequestConfig) => {
        // 是否需要加密
        const isEncrypt = config.isEncrypt ?? (config.isEncrypt = false)
        // 重复请求不需要取消，在 api 服务中通过指定的第三个参数: { cancel: false } 来控制
        config.cancel ?? (config.cancel = true)
        config.cancel && axiosCanceler.addPending(config)
        // 当前请求不需要显示 loading，在 api 服务中通过指定的第三个参数: { loading: false } 来控制
        config.loading ?? (config.loading = true)
        config.loading && showFullScreenLoading()
        // 是否需要设置 token
        const isToken = (config.headers || {}).isToken === false
        // 是否需要防止数据重复提交
        const isRepeatSubmit = (config.headers || {}).repeatSubmit === false
        const requestId = nanoid()
        const userStore = useUserStore()
        if (getToken() && !isToken) {
          // 请求加上随机ID
          config.headers.set('RequestId', requestId)
          if (config.headers && typeof config.headers.set === 'function') {
            config.headers.set('Authorization', 'Bearer ' + userStore.token)
          }
        }
        // get请求映射params参数
        if (config.method === 'get' && config.params) {
          let url = config.url + '?' + tansParams(config.params)
          url = url.slice(0, -1)
          config.params = {}
          config.url = url
        }
        if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
          const requestObj = {
            url: config.url,
            data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
            time: new Date().getTime()
          }
          const sessionObj = cache.session.getJSON('sessionObj')
          if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
            cache.session.setJSON('sessionObj', requestObj)
          } else {
            const s_url = sessionObj.url // 请求地址
            const s_data = sessionObj.data // 请求数据
            const s_time = sessionObj.time // 请求时间
            const interval = 1000 // 间隔时间(ms)，小于此时间视为重复提交
            if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) {
              const message = '数据正在处理，请勿重复提交'
              console.warn(`[${s_url}]: ` + message)
              return Promise.reject(new Error(message))
            } else {
              cache.session.setJSON('sessionObj', requestObj)
            }
          }
        }
        // 当开启参数加密
        if (isEncrypt && (config.method === 'post' || config.method === 'put')) {
          // 生成一个 AES 密钥
          const aesKey = generateAesKey()
          config.headers[encryptHeader] = encrypt(encryptBase64(aesKey))
          config.data = typeof config.data === 'object' ? encryptWithAes(JSON.stringify(config.data), aesKey) : encryptWithAes(config.data, aesKey)
        }
        // FormData数据去请求头Content-Type
        if (config.data instanceof FormData) {
          delete config.headers['Content-Type']
        }
        return config
      },
      (error: AxiosError) => {
        console.log(error)
        return Promise.reject(error)
      }
    )

    /**
     * @description 响应拦截器
     *  服务器换返回信息 -> [拦截统一处理] -> 客户端JS获取到信息
     */
    this.service.interceptors.response.use(
      (response: AxiosResponse & { config: CustomAxiosRequestConfig }) => {
        const { config } = response
        axiosCanceler.removePending(config)
        // 加密后的 AES 秘钥
        const keyStr = response.headers[encryptHeader]
        // 加密
        if (keyStr != null && keyStr != '') {
          const data = response.data
          // 请求体 AES 解密
          const base64Str = decrypt(keyStr)
          // base64 解码 得到请求头的 AES 秘钥
          const aesKey = decryptBase64(base64Str.toString())
          // aesKey 解码 data
          const decryptData = decryptWithAes(data, aesKey)
          // 将结果 (得到的是 JSON 字符串) 转为 JSON
          response.data = JSON.parse(decryptData)
        }
        const { data, request } = response
        tryHideFullScreenLoading()
        if (request.responseType === 'blob' || request.responseType === 'arraybuffer') {
          return data
        }
        // 登录失效
        if (data.code == ResultEnum.OVERDUE) {
          const userStore = useUserStore()
          if (!isReLogin.show) {
            isReLogin.show = true
            ElMessageBox.confirm('登录状态已过期，您可以继续留在该页面，或者重新登录', '系统提示', {
              confirmButtonText: '重新登录',
              cancelButtonText: '取消',
              type: 'warning'
            })
              .then(() => {
                isReLogin.show = false
                userStore.logOut().then(() => {
                  router.replace(LOGIN_URL)
                })
              })
              .catch(() => {
                isReLogin.show = false
              })
          }
          return Promise.reject('无效的会话，或者会话已过期，请重新登录。')
        }
        // 全局错误信息拦截（防止下载文件的时候返回数据流，没有 code 直接报错）
        if (data.code && data.code == ResultEnum.ERROR) {
          ElMessage.error(data.msg)
          return Promise.reject(new Error(data.msg))
        }
        // 成功请求（在页面上除非特殊情况，否则不用处理失败逻辑）
        return data
      },
      async (error: AxiosError) => {
        const { response } = error
        tryHideFullScreenLoading()
        // 请求超时 && 网络错误单独判断，没有 response
        if (error.message.indexOf('timeout') !== -1) ElMessage.error('请求超时！请您稍后重试')
        if (error.message.indexOf('Network Error') !== -1) ElMessage.error('网络错误！请您稍后重试')
        // 根据服务器响应的错误状态码，做不同的处理
        if (response) checkStatus(response.status)
        // 服务器结果都没有返回(可能服务器错误可能客户端断网)，断网处理:可以跳转到断网页面
        if (!window.navigator.onLine) router.replace('/500')
        return Promise.reject(error)
      }
    )
  }

  /**
   * @description 常用请求方法封装
   */
  get<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {
    return this.service.get(url, { params, ..._object })
  }
  post<T>(url: string, params?: object | string, _object = {}): Promise<ResultData<T>> {
    return this.service.post(url, params, _object)
  }
  put<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {
    return this.service.put(url, params, _object)
  }
  delete<T>(url: string, params?: any, _object = {}): Promise<ResultData<T>> {
    return this.service.delete(url, { params, ..._object })
  }
  downloadGet(url: string, params?: object, _object = {}): Promise<BlobPart> {
    return this.service.get(url, { ..._object, responseType: 'blob' })
  }
  downloadPost(url: string, params?: object, _object = {}): Promise<BlobPart> {
    return this.service.post(url, params, { ..._object, responseType: 'blob' })
  }
}

export default new RequestHttp(config)
