import { DATA_VERSION, getSdkVersion, getBaseEndPoint, getEndPointInfo } from '../constants/common'
import {
  IRequestOptions,
  SDKRequestInterface,
  ResponseObject,
  IUploadRequestOptions,
  IRequestConfig,
  IFetchOptions,
} from '@cloudbase/adapter-interface'
import { utils, constants } from '@cloudbase/utilities'
import { KV } from '@cloudbase/types'
import {
  IGetAccessTokenResult,
  ICloudbaseRequestConfig,
  IAppendedRequestInfo,
  IRequestBeforeHook,
} from '@cloudbase/types/request'
import { ICloudbaseCache } from '@cloudbase/types/cache'
import { getLocalCache } from './cache'
import { Platform } from './adapter'
const { ERRORS } = constants
const { genSeqId, isFormData, formatUrl } = utils

// 下面几种 action 不需要 access token
const ACTIONS_WITHOUT_ACCESSTOKEN = [
  'auth.getJwt',
  'auth.logout',
  'auth.signInWithTicket',
  'auth.signInAnonymously',
  'auth.signIn',
  'auth.fetchAccessTokenWithRefreshToken',
  'auth.signUpWithEmailAndPassword',
  'auth.activateEndUserMail',
  'auth.sendPasswordResetEmail',
  'auth.resetPasswordWithToken',
  'auth.isUsernameRegistered',
]

function bindHooks(instance: SDKRequestInterface, name: string, hooks: IRequestBeforeHook[]) {
  const originMethod = instance[name]
  instance[name] = function (options: IRequestOptions) {
    const data = {}
    const headers = {}
    hooks.forEach((hook) => {
      const { data: appendedData, headers: appendedHeaders } = hook.call(instance, options)
      Object.assign(data, appendedData)
      Object.assign(headers, appendedHeaders)
    })
    const originData = options.data
    originData
      && (() => {
        if (isFormData(originData)) {
          Object.keys(data).forEach((key) => {
            (originData as FormData).append(key, data[key])
          })
          return
        }
        options.data = {
          ...originData,
          ...data,
        }
      })()
    options.headers = {
      ...(options.headers || {}),
      ...headers,
    }
    return (originMethod as Function).call(instance, options)
  }
}
function beforeEach(): IAppendedRequestInfo {
  const seqId = genSeqId()
  return {
    data: {
      seqId,
    },
    headers: {
      'X-SDK-Version': `@cloudbase/js-sdk/${getSdkVersion()}`,
      'x-seqid': seqId,
    },
  }
}
export interface ICloudbaseRequest {
  post: (options: IRequestOptions) => Promise<ResponseObject>
  upload: (options: IUploadRequestOptions) => Promise<ResponseObject>
  download: (options: IRequestOptions) => Promise<ResponseObject>
  request: (action: string, params: KV<any>, options?: KV<any>) => Promise<ResponseObject>
  send: (action: string, data: KV<any>) => Promise<any>
  fetch: (options: IFetchOptions) => Promise<ResponseObject>
}

/**
 * @class CloudbaseRequest
 */
export class CloudbaseRequest implements ICloudbaseRequest {
  config: ICloudbaseRequestConfig
  private reqClass: SDKRequestInterface
  // 请求失败是否抛出Error
  private throwWhenRequestFail = false
  // 持久化本地存储
  private localCache: ICloudbaseCache
  /**
   * 初始化
   * @param config
   */
  constructor(config: ICloudbaseRequestConfig & { throw?: boolean }) {
    this.config = config
    const reqConfig: IRequestConfig = {
      timeout: this.config.timeout,
      timeoutMsg: `[@cloudbase/js-sdk] 请求在${this.config.timeout / 1000}s内未完成，已中断`,
      restrictedMethods: ['post', 'put'],
    }
    this.reqClass = new Platform.adapter.reqClass(reqConfig)
    this.throwWhenRequestFail = config.throw || false
    this.localCache = getLocalCache(this.config.env)
    bindHooks(this.reqClass, 'post', [beforeEach])
    bindHooks(this.reqClass, 'upload', [beforeEach])
    bindHooks(this.reqClass, 'download', [beforeEach])
  }

  public async post(options: IRequestOptions): Promise<ResponseObject> {
    const res = await this.reqClass.post(options)
    return res
  }
  public async upload(options: IUploadRequestOptions): Promise<ResponseObject> {
    const res = await this.reqClass.upload(options)
    return res
  }
  public async download(options: IRequestOptions): Promise<ResponseObject> {
    const res = await this.reqClass.download(options)
    return res
  }

  public getBaseEndPoint() {
    return getBaseEndPoint(this.config.env)
  }

  public async getOauthAccessTokenV2(oauthClient: any): Promise<IGetAccessTokenResult> {
    const validAccessToken = await oauthClient.getAccessToken()
    const credentials = await oauthClient.getCredentials()
    return {
      accessToken: validAccessToken,
      accessTokenExpire: new Date(credentials.expires_at).getTime(),
    }
  }

  /* eslint-disable complexity */
  public async request(
    action: string,
    params: KV<any>,
    options?: {
      onUploadProgress?: Function
      pathname?: string
      parse?: boolean
      inQuery?: KV<any>
      search?: string
      defaultQuery?: KV<any>
    },
  ): Promise<ResponseObject> {
    const tcbTraceKey = `x-tcb-trace_${this.config.env}`
    let contentType = 'application/x-www-form-urlencoded'

    const tmpObj: KV<any> = {
      action,
      dataVersion: DATA_VERSION,
      env: this.config.env,
      ...params,
    }

    if (ACTIONS_WITHOUT_ACCESSTOKEN.indexOf(action) === -1) {
      const app = this.config._fromApp

      if (!app.oauthInstance) {
        throw new Error('you can\'t request without auth')
      }

      const { oauthInstance } = app
      const oauthClient = oauthInstance.oauth2client
      tmpObj.access_token = (await this.getOauthAccessTokenV2(oauthClient)).accessToken
    }

    // 拼body和content-type
    let payload
    if (action === 'storage.uploadFile') {
      payload = new FormData()
      Object.keys(payload).forEach((key) => {
        if (Object.prototype.hasOwnProperty.call(payload, key) && payload[key] !== undefined) {
          payload.append(key, tmpObj[key])
        }
      })
      contentType = 'multipart/form-data'
    } else {
      contentType = 'application/json;charset=UTF-8'
      payload = {}
      Object.keys(tmpObj).forEach((key) => {
        if (tmpObj[key] !== undefined) {
          payload[key] = tmpObj[key]
        }
      })
    }
    const opts: any = {
      headers: {
        'content-type': contentType,
      },
    }
    if (options?.onUploadProgress) {
      opts.onUploadProgress = options.onUploadProgress
    }

    if (this.config.region) {
      opts.headers['X-TCB-Region'] = this.config.region
    }

    const traceHeader = this.localCache.getStore(tcbTraceKey)
    if (traceHeader) {
      opts.headers['X-TCB-Trace'] = traceHeader
    }

    // 发出请求
    // 新的 url 需要携带 env 参数进行 CORS 校验
    // 请求链接支持添加动态 query 参数，方便用户调试定位请求
    const parse = options?.parse !== undefined ? options.parse : params.parse
    const inQuery = options?.inQuery !== undefined ? options.inQuery : params.inQuery
    const search = options?.search !== undefined ? options.search : params.search

    let formatQuery: Record<string, any> = {
      ...(options?.defaultQuery || {}),
      env: this.config.env,
    }
    // 尝试解析响应数据为 JSON
    parse && (formatQuery.parse = true)
    inQuery
      && (formatQuery = {
        ...inQuery,
        ...formatQuery,
      })

    const { baseUrl: BASE_URL, protocol: PROTOCOL } = getEndPointInfo(this.config.env, 'CLOUD_API')
    // 生成请求 url
    let newUrl
    if (options.pathname) {
      newUrl = formatUrl(
        PROTOCOL,
        `${getBaseEndPoint(this.config.env)?.replace(/^https?:/, '')}/${options.pathname}`,
        formatQuery,
      )
    } else {
      newUrl = formatUrl(PROTOCOL, BASE_URL, formatQuery)
    }

    if (search) {
      newUrl += search
    }

    const res: ResponseObject = await this.post({
      url: newUrl,
      data: payload,
      ...opts,
    })

    // 保存 trace header
    const resTraceHeader = res.header?.['x-tcb-trace']
    if (resTraceHeader) {
      this.localCache.setStore(tcbTraceKey, resTraceHeader)
    }

    if ((Number(res.status) !== 200 && Number(res.statusCode) !== 200) || !res.data) {
      throw new Error('network request error')
    }

    return res
  }

  public async fetch(options: IFetchOptions & { token?: string }): Promise<ResponseObject> {
    const { token, headers = {}, ...restOptions } = options
    const getAccessToken = async () => {
      if (token != null) {
        return token
      }
      const app = this.config._fromApp

      if (!app.oauthInstance) {
        throw new Error('you can\'t request without auth')
      }

      const { oauthInstance } = app
      const oauthClient = oauthInstance.oauth2client
      return (await this.getOauthAccessTokenV2(oauthClient)).accessToken
    }

    const doFetch = async () => this.reqClass.fetch({
      headers: {
        // 'Content-Type': 'application/json',
        // 'X-Request-Id': `${utils.generateRequestId()}`,
        // 'X-Request-Timestamp': `${Date.now()}`,
        'X-SDK-Version': `@cloudbase/js-sdk/${getSdkVersion()}`,
        Authorization: `Bearer ${await getAccessToken()}`,
        ...headers,
      },
      ...restOptions,
    })

    try {
      const result = await doFetch()
      return result
    } catch (err) {
      if (err?.code === 'ACCESS_TOKEN_EXPIRED') {
        // 如果是因为 token 过期失败，刷 token 后再试一次
        if (typeof this.config?._fromApp?.oauthInstance?.authApi?.refreshTokenForce !== 'function') {
          throw err
        }
        await this.config?._fromApp?.oauthInstance?.authApi?.refreshTokenForce()
        return doFetch()
      }
      // 其他原因向上抛出

      throw err
    }
  }

  public async send(action: string, data: KV<any> = {}, options: KV<any> = {}): Promise<any> {
    const response = await this.request(action, data, { ...options, onUploadProgress: data.onUploadProgress })

    if (response.data.code && this.throwWhenRequestFail) {
      throw new Error(JSON.stringify({
        code: ERRORS.OPERATION_FAIL,
        msg: `[${response.data.code}] ${response.data.message}`,
      }),)
    }

    return response.data
  }
}

const requestMap: KV<CloudbaseRequest> = {}

export function initRequest(config: ICloudbaseRequestConfig) {
  requestMap[config.env] = new CloudbaseRequest({
    ...config,
    throw: true,
  })
}

export function getRequestByEnvId(env: string): CloudbaseRequest {
  return requestMap[env]
}
