import { DATA_VERSION, getSdkVersion, getBaseEndPoint, getEndPointInfo } from '../constants/common'
import {
  IRequestOptions,
  SDKRequestInterface,
  ResponseObject,
  IUploadRequestOptions,
  IRequestConfig,
  IFetchOptions,
} from '@cloudbase/adapter-interface'
import { utils, constants, langEvent } from '@cloudbase/utilities'
import { EndPointKey, KV } from '@cloudbase/types'
import { ICustomReqOpts } from '@cloudbase/types/functions'
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 IGateWayOptions {
  name?: string
  data?: any
  path?: string
  method: string
  header?: {}
  url?: string
}
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 & { auth: ICloudbaseRequestConfig['auth'] } = {
      timeout: this.config.timeout,
      timeoutMsg: `[@cloudbase/js-sdk] 请求在${this.config.timeout / 1000}s内未完成，已中断`,
      restrictedMethods: ['post', 'put'],
      auth: config.auth,
    }
    this.reqClass = new Platform.adapter.reqClass(reqConfig)
    this.throwWhenRequestFail = config.throw || false
    this.localCache = getLocalCache(this.config.env)

    if (this.config.endPointMode !== 'GATEWAY') {
      bindHooks(this.reqClass, 'post', [beforeEach])
      bindHooks(this.reqClass, 'upload', [beforeEach])
      bindHooks(this.reqClass, 'download', [beforeEach])
    }

    langEvent.bus.on(langEvent.LANG_CHANGE_EVENT, (params) => {
      this.config.i18n = params.data?.i18n || this.config.i18n
    })
  }

  public async getAccessToken(token = null) {
    // eslint-disable-next-line eqeqeq
    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
  }

  public getDefaultHeaders() {
    return {
      [this.config.i18n?.LANG_HEADER_KEY]: this.config.i18n?.lang,
      'X-SDK-Version': `@cloudbase/js-sdk/${getSdkVersion()}`,
    }
  }

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

  public getBaseEndPoint(endPointKey: EndPointKey = 'CLOUD_API') {
    return getBaseEndPoint(this.config.env, endPointKey)
  }

  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>
      headers?: KV<string>
      endPointMode?: 'CLOUD_API' | 'GATEWAY'
    },
    customReqOpts?: ICustomReqOpts,
  ): Promise<ResponseObject> {
    try {
      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,
          ...this.getDefaultHeaders(),
          ...options?.headers,
        },
      }
      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 endPointMode = options?.endPointMode || this.config.endPointMode || 'CLOUD_API'

      const url = getEndPointInfo(this.config.env, endPointMode)
      let BASE_URL = url.baseUrl
      const PROTOCOL = url.protocol

      if (endPointMode === 'GATEWAY') {
        if (/^((database)\.)|(auth\.wsWebSign)/.test(action)) {
          const url = getEndPointInfo(this.config.env, 'CLOUD_API')
          BASE_URL = `${url.baseUrl.match(/\/\/([^/?#]*)/)[0]}/web`
        }
      }

      // 生成请求 url
      let newUrl
      if (options.pathname) {
        newUrl = formatUrl(
          PROTOCOL,
          `${getBaseEndPoint(this.config.env, endPointMode)?.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,
        },
        customReqOpts,
      )

      // 保存 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
    } catch (err) {
      try {
        err.requestId = err.requestId || options?.headers['x-request-id'] || options?.headers['X-Request-Id'] || ''
      } catch (error) {}
      throw err
    }
  }

  public async fetch(options: IFetchOptions & { token?: string; withCredentials?: boolean; customReqOpts?: ICustomReqOpts },): Promise<ResponseObject> {
    const { token, headers = {}, withCredentials = true, ...restOptions } = options

    const doFetch = async () => {
      const authHeaders: Record<string, string> = {}
      if (withCredentials) {
        authHeaders.Authorization = `Bearer ${await this.getAccessToken(token)}`
      }
      return 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()}`,
          ...authHeaders,
          ...this.getDefaultHeaders(),
          ...headers,
        },
        ...restOptions,
      })
    }

    try {
      const result = await doFetch()
      return result
    } catch (err) {
      try {
        err.requestId = err.requestId || headers['x-request-id'] || headers['X-Request-Id'] || ''
      } catch (error) {}
      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> = {},
    customReqOpts?: ICustomReqOpts,
  ): Promise<any> {
    const response = await this.request(
      action,
      data,
      { ...options, onUploadProgress: data.onUploadProgress },
      customReqOpts,
    )

    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
  }

  public async gateWay(options: IGateWayOptions, customReqOpts?: ICustomReqOpts) {
    const { url, name, data, path = '', method, header = {} } = options

    if ((!name || !path) && !url) {
      throw new Error(JSON.stringify({
        code: ERRORS.INVALID_PARAMS,
        msg: '[gateWay] invalid function name or path or url',
      }),)
    }
    let jsonData
    try {
      jsonData = data ? JSON.stringify(data) : ''
    } catch (e) {
      throw new Error(JSON.stringify({
        code: ERRORS.INVALID_PARAMS,
        msg: '[gateWay] invalid data',
      }),)
    }

    const requestId = utils.generateRequestId()
    const { baseUrl, protocol } = getEndPointInfo(this.config.env, 'GATEWAY')
    let endpoint = `${protocol}${baseUrl}${url || `/${path}/${name}`}`

    const isGetAndHead = ['GET', 'HEAD'].includes(method?.toUpperCase?.())

    if (isGetAndHead) {
      try {
        let dataParse = {}
        try {
          dataParse = JSON.parse(data as any)
        } catch (error) {
          dataParse = data || {}
        }
        endpoint = `${endpoint}${endpoint.includes('?') ? '&' : '?'}${Object.keys(dataParse)
          .map(key => `${key}=${dataParse[key]}`)
          .join('&')}`
      } catch (error) {
        //
      }
    }

    const response = await this.fetch({
      method: method || 'POST',
      headers: {
        'Content-Type': 'application/json; charset=utf-8',
        'X-Request-Id': requestId,
        ...header,
      },
      ...(isGetAndHead ? {} : { body: jsonData }),
      url: endpoint,
      customReqOpts,
    })

    return { requestId, ...response, data: await 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]
}
