import type { AxiosInstance, AxiosPromise, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
import axios, { AxiosError } from 'axios'
import type { RequestOptions, Result, UploadFileParams } from '/#/axios'
import type { CreateAxiosOptions } from './axiosTransform'
import qs from 'qs'
import { AxiosCanceler } from './axiosCancel'
import { isFunction } from '/@/utils/is'
import { cloneDeep } from 'lodash-es'
import { ContentTypeEnum, RequestEnum } from '/@/enums/httpEnum'

/**
 * 覆盖实现，使用 uni.request 实现网络请求
 * @param config 请求配置
 * @returns 请求结果
 */
axios.defaults.adapter = function (config: InternalAxiosRequestConfig): AxiosPromise<any> {
    return new Promise((resolve, reject) => {
        const request = uni.request({
            method: config.method.toUpperCase() as RequestEnum,
            url: config.url,
            data: config.data || config.params,
            header: config.headers,
            responseType: config.responseType,
            timeout: config.timeout,
            dataType: 'json',
            success: (response) => {
                resolve({
                    config,
                    data: response.data,
                    headers: response.header,
                    status: response.statusCode,
                    statusText: '',
                })
            },
            fail: (response) => {
                try {
                    const { errMsg } = response
                    const isTimeout = errMsg.includes('timeout')
                    const error = AxiosError.from(
                        errMsg,
                        isTimeout ? AxiosError.ETIMEDOUT : AxiosError.ECONNABORTED,
                        config,
                        request,
                    )
                    reject(error)
                } catch (error) {
                    reject(error)
                }
            },
        })
    })
}

export * from './axiosTransform'

/**
 * @description:  axios module
 */
export class VAxios {
    private axiosInstance: AxiosInstance
    private readonly options: CreateAxiosOptions

    constructor(options: CreateAxiosOptions) {
        this.options = options
        this.axiosInstance = axios.create(options)
        this.setupInterceptors()
    }

    /**
     * @description:  Create axios instance
     */
    private createAxios(config: CreateAxiosOptions): void {
        this.axiosInstance = axios.create(config)
    }

    private getTransform() {
        const { transform } = this.options
        return transform
    }

    getAxios(): AxiosInstance {
        return this.axiosInstance
    }

    /**
     * @description: Reconfigure axios
     */
    configAxios(config: CreateAxiosOptions) {
        if (!this.axiosInstance) {
            return
        }
        this.createAxios(config)
    }

    /**
     * @description: Set general header
     */
    setHeader(headers: any): void {
        if (!this.axiosInstance) {
            return
        }
        Object.assign(this.axiosInstance.defaults.headers, headers)
    }

    /**
     * @description: Interceptor configuration
     */
    private setupInterceptors() {
        const transform = this.getTransform()
        if (!transform) {
            return
        }
        const { requestInterceptors, requestInterceptorsCatch, responseInterceptors, responseInterceptorsCatch } =
            transform

        const axiosCanceler = new AxiosCanceler()

        // Request interceptor configuration processing
        this.axiosInstance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
            // If cancel repeat request is turned on, then cancel repeat request is prohibited
            // @ts-expect-error
            const { ignoreCancelToken } = config.requestOptions
            const ignoreCancel =
                ignoreCancelToken !== undefined ? ignoreCancelToken : this.options.requestOptions?.ignoreCancelToken

            !ignoreCancel && axiosCanceler.addPending(config)
            if (requestInterceptors && isFunction(requestInterceptors)) {
                // @ts-expect-error
                config = requestInterceptors(config, this.options)
            }
            return config
        }, undefined)

        // Request interceptor error capture
        requestInterceptorsCatch &&
            isFunction(requestInterceptorsCatch) &&
            this.axiosInstance.interceptors.request.use(undefined, requestInterceptorsCatch)

        // Response result interceptor processing
        this.axiosInstance.interceptors.response.use((res: AxiosResponse<any>) => {
            res && axiosCanceler.removePending(res.config)
            if (responseInterceptors && isFunction(responseInterceptors)) {
                res = responseInterceptors(res)
            }
            return res
        }, undefined)

        // Response result interceptor error capture
        responseInterceptorsCatch &&
            isFunction(responseInterceptorsCatch) &&
            this.axiosInstance.interceptors.response.use(undefined, (error) => {
                // @ts-expect-error
                responseInterceptorsCatch(this.axiosInstance, error)
            })
    }

    /**
     * @description:  File Upload
     */
    uploadFile<T = any>(config: AxiosRequestConfig, params: UploadFileParams) {
        const formData = new window.FormData()
        const customFilename = params.name || 'file'

        if (params.filename) {
            formData.append(customFilename, params.file, params.filename)
        } else {
            formData.append(customFilename, params.file)
        }

        if (params.data) {
            Object.keys(params.data).forEach((key) => {
                const value = params.data![key]
                if (Array.isArray(value)) {
                    value.forEach((item) => {
                        formData.append(`${key}[]`, item)
                    })
                    return
                }

                formData.append(key, params.data![key])
            })
        }

        const transform = this.getTransform()
        const { requestOptions } = this.options
        const { transformRequestHook, requestCatchHook } = transform || {}

        return new Promise((resolve, reject) => {
            this.axiosInstance
                .request<any, AxiosResponse<Result>>({
                    ...config,
                    method: 'POST',
                    data: formData,
                    headers: {
                        'Content-Type': ContentTypeEnum.FORM_DATA,
                        ignoreCancelToken: true,
                    },
                })
                .then((res: AxiosResponse<Result>) => {
                    if (transformRequestHook && isFunction(transformRequestHook)) {
                        try {
                            const ret = transformRequestHook(res, requestOptions)
                            resolve(ret || true)
                        } catch (err) {
                            reject(err || new Error('request error!'))
                        }
                        return
                    }
                    resolve(res as unknown as Promise<T>)
                })
                .catch((e: Error | AxiosError) => {
                    if (requestCatchHook && isFunction(requestCatchHook)) {
                        reject(requestCatchHook(e, requestOptions))
                        return
                    }
                    if (axios.isAxiosError(e)) {
                        // rewrite error message from axios in here
                    }
                    reject(e)
                })
        })
    }

    // support form-data
    supportFormData(config: AxiosRequestConfig) {
        const headers = config.headers || this.options.headers
        const contentType = headers?.['Content-Type'] || headers?.['Content-Type']

        if (
            contentType !== ContentTypeEnum.FORM_URLENCODED ||
            !Reflect.has(config, 'data') ||
            config.method?.toUpperCase() === RequestEnum.GET
        ) {
            return config
        }

        return {
            ...config,
            data: qs.stringify(config.data, { arrayFormat: 'brackets' }),
        }
    }

    get<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
        return this.request({ ...config, method: 'GET' }, options)
    }

    post<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
        return this.request({ ...config, method: 'POST' }, options)
    }

    put<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
        return this.request({ ...config, method: 'PUT' }, options)
    }

    delete<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
        return this.request({ ...config, method: 'DELETE' }, options)
    }

    request<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
        let conf = cloneDeep(config) as CreateAxiosOptions
        const transform = this.getTransform()

        const { requestOptions } = this.options

        let opt: RequestOptions = Object.assign({}, requestOptions, options)

        const { beforeRequestHook, requestCatchHook, transformRequestHook, inject } = transform || {}
        if (inject && isFunction(inject)) {
            const result = inject(conf, opt)
            conf = result.config
            opt = result.options
        }
        if (beforeRequestHook && isFunction(beforeRequestHook)) {
            conf = beforeRequestHook(conf, opt)
        }

        conf.requestOptions = opt

        conf = this.supportFormData(conf)

        return new Promise((resolve, reject) => {
            this.axiosInstance
                .request<any, AxiosResponse<Result>>(conf)
                .then((res: AxiosResponse<Result>) => {
                    if (transformRequestHook && isFunction(transformRequestHook)) {
                        try {
                            const ret = transformRequestHook(res || ({} as AxiosResponse<Result>), opt)
                            resolve(ret)
                        } catch (err) {
                            reject(err || new Error('request error!'))
                        }
                        return
                    }
                    resolve(res as unknown as Promise<T>)
                })
                .catch((e: Error | AxiosError) => {
                    if (requestCatchHook && isFunction(requestCatchHook)) {
                        reject(requestCatchHook(e, opt))
                        return
                    }
                    if (axios.isAxiosError(e)) {
                        // rewrite error message from axios in here
                    }
                    reject(e)
                })
        })
    }
}
