import Taro, { UploadTask } from '@tarojs/taro';

import { HttpError } from '@/http/error';
import { safeJsonParse } from '@/utils/common';

import { API, BASE_URL } from './api';
import { HTTP_STATUS, RESPONSE_ERROR_CODE } from './constant';
import interceptors from './interceptor';
import { IRequestResponse } from './type';
import { isTokenExpired, refreshToken } from './utils';

interface IRequestOption<T = BL.Anything> extends Taro.request.Option<T> {
  contentType?: 'application/json' | 'application/x-www-form-urlencoded';
  blRetryTime?: number;
}

interface IUploadOption extends Taro.uploadFile.Option {
  blRetryTime?: number;
  onProgress?: UploadTask.OnProgressUpdateCallback;
}

interceptors.forEach(interceptor => Taro.addInterceptor(interceptor));

class Http {
  private throwHttpError = (resp: Taro.request.SuccessCallbackResult<IRequestResponse>) => {
    const { statusCode, data } = resp;
    const errorCode = data?.code || 'null';
    const errorMsg = data?.msg || 'unknown';
    const traceId = data?.traceId;
    console.error(
      `http request fail, httpCode: ${statusCode}, errorCode: ${errorCode}, errorMsg: ${errorMsg}, traceId: ${traceId}`
    );
    throw new HttpError('request fail', errorCode, errorMsg, traceId);
  };

  private isNeedLoginError = (
    options: { blRetryTime?: number },
    resp: Taro.request.SuccessCallbackResult<IRequestResponse>
  ) => {
    if (options.blRetryTime && options.blRetryTime >= 3) {
      return false;
    }
    const { statusCode, data } = resp;
    return statusCode !== HTTP_STATUS.SUCCESS && data?.code === RESPONSE_ERROR_CODE.NEED_LOGIN;
  };

  private uploadFile = (options: IUploadOption) => {
    const { onProgress, ...option } = options;
    let offProgressUpdate: (() => void) | null = null;
    return new Promise((resolve, reject) => {
      option.success = resolve;
      option.fail = reject;
      const task = Taro.uploadFile(option);
      onProgress && task.onProgressUpdate(onProgress);
      onProgress && (offProgressUpdate = () => task.offProgressUpdate(onProgress));
    })
      .then((resp: Taro.uploadFile.SuccessCallbackResult) => {
        if (resp.statusCode === 200) {
          return safeJsonParse(resp.data.replace(/\uFEFF/g, ''));
        } else if (this.isNeedLoginError(option, resp as Taro.request.SuccessCallbackResult)) {
          option.blRetryTime = (option.blRetryTime || 0) + 1;
          return refreshToken().then(() => this.uploadFile(option));
        } else {
          this.throwHttpError(resp as Taro.request.SuccessCallbackResult);
        }
      })
      .finally(() => offProgressUpdate?.());
  };

  private request = (option: IRequestOption) => {
    return new Promise((resolve, reject) => {
      option.success = resolve;
      option.fail = reject;
      Taro.request(option);
    }).then((resp: Taro.request.SuccessCallbackResult) => {
      if (resp.statusCode === HTTP_STATUS.SUCCESS) {
        return resp.data;
      } else if (this.isNeedLoginError(option, resp)) {
        option.blRetryTime = (option.blRetryTime || 0) + 1;
        return refreshToken().then(() => this.request(option));
      } else {
        this.throwHttpError(resp);
      }
    });
  };

  private baseUpload = (params: IUploadOption) => {
    const { url, ...otherParams } = params;
    const option: IUploadOption = {
      // 默认超时时间三分钟
      timeout: 1000 * 60 * 3,
      ...otherParams,
      blRetryTime: 0,
      url: url.startsWith('http') ? url : BASE_URL + url,
      header: { 'content-type': 'multipart/form-data' },
    };

    return this.uploadFile(option);
  };

  private baseRequest = (params: IRequestOption, method: Taro.request.Option['method'] = 'GET') => {
    const { url, data, ...otherParams } = params;
    const contentType = params.contentType || 'application/json';
    const option: IRequestOption = {
      ...otherParams,
      blRetryTime: 0,
      url: BASE_URL + url,
      data,
      method: method,
      header: { 'content-type': contentType },
    };
    return this.request(option);
  };

  async init() {
    if (isTokenExpired()) {
      await refreshToken();
    }
  }

  get = <T = BL.Anything>(url: API, params?: Omit<IRequestOption<T>, 'url'>): Promise<T> => {
    const option = { url, ...params };
    return this.baseRequest(option);
  };

  post = <T = BL.Anything>(url: API, params?: Omit<IRequestOption<T>, 'url'>): Promise<T> => {
    const option = { url, ...params };
    return this.baseRequest(option, 'POST');
  };

  upload = <T = BL.Anything>(url: API | string, params?: Omit<IUploadOption, 'url'>): Promise<T> => {
    const option = { url, ...params } as IUploadOption;
    return this.baseUpload(option);
  };
}

export default new Http();
