import http from 'http'

/* eslint-disable-next-line */
import { parse } from 'url'

import { sign } from '@cloudbase/signature-nodejs'

import { ICloudBaseConfig, ICustomParam, ICustomReqOpts } from '../../types'
import { IRequestInfo, IReqHooks, IReqOpts } from '../../types/internal'

import { ERROR } from '../const/code'
import { SYMBOL_CURRENT_ENV, SYMBOL_DEFAULT_ENV } from '../const/symbol'

import { generateTracingInfo, ITracingInfo } from './tracing'
import * as utils from './utils'
import { CloudBase } from '../cloudbase'
import { checkIsInScf, checkIsInternalAsync, getCurrRunEnvTag } from './cloudplatform'
import { buildUrl } from './tcbapiendpoint'

import { extraRequest } from './request'

import { getWxCloudToken } from './wxCloudToken'

import { version } from './version'

const { E, second, processReturn } = utils

export function getEnvIdFromContext(): string {
  const { TCB_ENV, SCF_NAMESPACE } = CloudBase.getCloudbaseContext()
  return TCB_ENV || SCF_NAMESPACE || ''
}

interface ITencentCloudCredentials {
  secretId: string
  secretKey: string
  sessionToken?: string
}

export function getCredentialsOnDemand(credentials: ITencentCloudCredentials): ITencentCloudCredentials {
  const { secretId, secretKey } = credentials
  let newCredentials: ITencentCloudCredentials = credentials

  // 原本这里只在SCF云函数环境下，运行支持任意环境通过环境变量传递密钥
  if (!secretId || !secretKey) {
    // 尝试从环境变量中读取
    const {
      TENCENTCLOUD_SECRETID,
      TENCENTCLOUD_SECRETKEY,
      TENCENTCLOUD_SESSIONTOKEN
    } = CloudBase.getCloudbaseContext()

    if (TENCENTCLOUD_SECRETID && TENCENTCLOUD_SECRETKEY) {
      newCredentials = {
        secretId: TENCENTCLOUD_SECRETID,
        secretKey: TENCENTCLOUD_SECRETKEY,
        sessionToken: TENCENTCLOUD_SESSIONTOKEN
      }
    }

    // 注意：CBR 环境下，已经禁止该方式获取临时密钥，这里实际是不会成功的
    // if (checkIsInCBR()) {
    //   const tmpSecret = await getTmpSecret()
    //   newCredentials = {
    //     secretId: tmpSecret.id,
    //     secretKey: tmpSecret.key,
    //     sessionToken: tmpSecret.token
    //   }
    //   return newCredentials
    // }

    // if (await checkIsInTencentCloud()) {
    //   const tmpSecret = await getTmpSecret()
    //   newCredentials = {
    //     secretId: tmpSecret.id,
    //     secretKey: tmpSecret.key,
    //     sessionToken: tmpSecret.token
    //   }
    //   return newCredentials
    // }
  }
  return newCredentials
}

export async function prepareCredentials(): Promise<void> {
  const opts = this.opts

  // CrossAccountInfo: 跨账号调用
  const getCrossAccountInfo = opts.getCrossAccountInfo || this.config.getCrossAccountInfo
  /* istanbul ignore if */
  if (getCrossAccountInfo) {
    const crossAccountInfo = await getCrossAccountInfo()
    const { credential } = crossAccountInfo
    const { secretId, secretKey, token } = credential || {}
    this.config = {
      ...this.config,
      secretId,
      secretKey,
      sessionToken: token
    }
    if (!this.config.secretId || !this.config.secretKey) {
      throw E({
        ...ERROR.INVALID_PARAM,
        message: 'missing secretId or secretKey of tencent cloud'
      })
    }

    // 替换掉原函数，缓存数据，这里缓存是否起作用，取决于 this 实例是否复用
    // 另一处获取 authorization 的代码可以服用吃这里的缓存
    this.opts.getCrossAccountInfo = async () => await Promise.resolve(crossAccountInfo)
  } else {
    const { secretId, secretKey, sessionToken } = this.config
    const credentials = getCredentialsOnDemand({ secretId, secretKey, sessionToken })
    this.config = {
      ...this.config,
      secretId: credentials.secretId,
      secretKey: credentials.secretKey,
      sessionToken: credentials.sessionToken
    }
    if (!this.config.secretId || !this.config.secretKey) {
      throw E({
        ...ERROR.INVALID_PARAM,
        message: 'missing secretId or secretKey of tencent cloud, please set secretId and secretKey in config'
      })
    }
  }
}

export class TcbApiHttpRequester {
  private readonly args: IRequestInfo
  private readonly config: ICloudBaseConfig
  private readonly opts: ICustomReqOpts
  private readonly defaultTimeout = 15000
  private readonly timestamp: number = new Date().valueOf()
  private readonly tracingInfo: ITracingInfo

  /* eslint-disable no-undef */
  private slowWarnTimer: NodeJS.Timer = null
  /* eslint-enable no-undef */

  private readonly hooks: IReqHooks = {}

  public constructor(args: IRequestInfo) {
    this.args = args
    this.config = args.config
    this.opts = args.opts || {}
    this.tracingInfo = generateTracingInfo(args.config?.context?.eventID)
  }

  public async request(): Promise<any> {
    // 如果没有配置 accessKey，则通过密钥获取签名，这里先检查密钥是否存在
    if (!this.config.accessKey) {
      // 检查密钥是否存在
      await this.prepareCredentials()
    }
    const params = await this.makeParams()
    // console.log('params', params)
    const opts = this.makeReqOpts(params)
    // console.log('opts', opts)
    const action = this.getAction()
    const key = {
      functions: 'function_name',
      database: 'collectionName',
      wx: 'apiName'
    }[action.split('.')[0]]

    const argopts: any = this.opts
    const config = this.config

    // 注意：必须初始化为 null
    let retryOptions: any = null
    if (argopts.retryOptions) {
      retryOptions = argopts.retryOptions
    } else if (config.retries && typeof config.retries === 'number') {
      retryOptions = { retries: config.retries }
    }

    return await extraRequest(opts, {
      debug: config.debug,
      op: `${action}:${this.args.params[key]}@${params.envName}`,
      seqId: this.tracingInfo.seqId,
      retryOptions,
      timingsMeasurerOptions: config.timingsMeasurerOptions || {}
    }).then((response: any) => {
      this.slowWarnTimer && clearTimeout(this.slowWarnTimer)
      const { body } = response
      if (response.statusCode === 200) {
        let result: any
        try {
          result = typeof body === 'string' ? JSON.parse(body) : body
          if (this.hooks && this.hooks.handleData) {
            result = this.hooks.handleData(result, null, response, body)
          }
        } catch (e) {
          result = body
        }
        return result
      } else {
        const e = E({
          code: response.statusCode,
          message: `${response.statusCode} ${http.STATUS_CODES[response.statusCode]} | [${opts.url}]`
        })
        throw e
      }
    })
  }

  public setHooks(hooks: IReqHooks) {
    Object.assign(this.hooks, hooks)
  }

  public setSlowWarning(timeout: number) {
    const action = this.getAction()
    const { seqId } = this.tracingInfo
    this.slowWarnTimer = setTimeout(() => {
      /* istanbul ignore next */
      const msg = `[TCB][WARN] Your current request ${action
                || ''} is longer than 3s, it may be due to the network or your query performance | [${seqId}]`
      /* istanbul ignore next */
      console.warn(msg)
    }, timeout)
  }

  private getAction(): string {
    return this.args.params.action
  }

  private async makeParams(): Promise<any> {
    const { TCB_SESSIONTOKEN } = CloudBase.getCloudbaseContext()
    const args = this.args
    const opts = this.opts
    const config = this.config

    const crossAuthorizationData
            = opts.getCrossAccountInfo && (await opts.getCrossAccountInfo()).authorization

    const { wxCloudApiToken, wxCloudbaseAccesstoken } = getWxCloudToken()

    const params: ICustomParam = {
      ...args.params,
      envName: config.envName || '',
      wxCloudApiToken,
      wxCloudbaseAccesstoken,
      tcb_sessionToken: TCB_SESSIONTOKEN || '',
      sessionToken: config.sessionToken,
      crossAuthorizationToken: crossAuthorizationData
        ? Buffer.from(JSON.stringify(crossAuthorizationData)).toString('base64')
        : ''
    }

    if (!params.envName) {
      if (checkIsInScf()) {
        params.envName = getEnvIdFromContext()
        console.warn(`[TCB][WARN] 当前未指定env，将默认使用当前函数所在环境的环境：${params.envName}！`)
      } else {
        console.warn('[TCB][WARN] 当前未指定env，将默认使用第一个创建的环境！')
      }
    }

    // 取当前云函数环境时，替换为云函数下环境变量
    if (params.envName === SYMBOL_CURRENT_ENV) {
      params.envName = getEnvIdFromContext()
    } else if (params.envName === SYMBOL_DEFAULT_ENV) {
      // 这里传空字符串没有可以跟不传的情况做一个区分
      params.envName = ''
    }

    utils.filterUndefined(params)

    return params
  }

  private makeReqOpts(params: any): IReqOpts {
    const config = this.config
    const args = this.args

    const url = buildUrl({
      envId: params.envName || '',
      region: this.config.region,
      protocol: this.config.protocol || 'https',
      serviceUrl: this.config.serviceUrl,
      seqId: this.tracingInfo.seqId,
      isInternal: this.args.isInternal
    })

    const method = this.args.method || 'get'

    const timeout = this.args.opts?.timeout || this.config.timeout || this.defaultTimeout

    const opts: IReqOpts = {
      url,
      method,
      timeout,
      // 优先取config，其次取模块，最后取默认
      headers: this.getHeaders(method, url, params),
      proxy: config.proxy,
      type: this.opts?.type || 'json'
    }

    if (typeof config.keepalive === 'undefined' && !checkIsInScf()) {
      // 非云函数环境下，默认开启 keepalive
      opts.keepalive = true
    } else {
      /** eslint-disable-next-line */
      opts.keepalive = typeof config.keepalive === 'boolean' && config.keepalive
    }

    if (args.method === 'post') {
      if (args.isFormData) {
        opts.formData = params
        opts.encoding = null
      } else {
        opts.body = params
        opts.json = true
      }
    } else {
      /* istanbul ignore next */
      opts.qs = params
    }

    return opts
  }

  private async prepareCredentials(): Promise<void> {
    prepareCredentials.bind(this)()
  }

  private getHeaders(method: string, url: string, params: any): any {
    const config = this.config
    const { context, secretId, secretKey, accessKey } = config
    const args = this.args

    const { TCB_SOURCE } = CloudBase.getCloudbaseContext()
    // Note: 云函数被调用时可能调用端未传递 SOURCE，TCB_SOURCE 可能为空
    const SOURCE = `${context?.extendedContext?.source || TCB_SOURCE || ''},${args.opts.runEnvTag}`

    // 注意：因为 url.parse 和 url.URL 存在差异，因 url.parse 已被废弃，这里可能会需要改动。
    // 因 @cloudbase/signature-nodejs sign 方法目前内部使用 url.parse 解析 url，
    // 如果这里需要改动，需要注意与 @cloudbase/signature-nodejs 的兼容性
    // 否则将导致签名存在问题
    const parsedUrl = parse(url)
    // const parsedUrl = new URL(url)

    let requiredHeaders = {
      'User-Agent': `tcb-node-sdk/${version}`,
      'X-TCB-Source': SOURCE,
      'X-Client-Timestamp': this.timestamp,
      'X-SDK-Version': `tcb-node-sdk/${version}`,
      Host: parsedUrl.host
    }

    if (config.version) {
      requiredHeaders['X-SDK-Version'] = config.version
    }

    if (this.tracingInfo.trace) {
      requiredHeaders['X-TCB-Tracelog'] = this.tracingInfo.trace
    }

    const region = this.config.region || process.env.TENCENTCLOUD_REGION || ''

    if (region) {
      requiredHeaders['X-TCB-Region'] = region
    }

    requiredHeaders = { ...config.headers, ...args.headers, ...requiredHeaders }

    const { authorization, timestamp } = sign({
      secretId,
      secretKey,
      method,
      url,
      params,
      headers: requiredHeaders,
      withSignedParams: true,
      timestamp: second() - 1
    })

    /* eslint-disable @typescript-eslint/dot-notation */
    // 优先使用 accessKey，否则使用签名
    requiredHeaders['Authorization'] = accessKey ? `Bearer ${accessKey}` : authorization
    requiredHeaders['X-Signature-Expires'] = 600
    requiredHeaders['X-Timestamp'] = timestamp

    return { ...requiredHeaders }
  }
}

const handleWxOpenApiData = (res: any, err: any, response: any, body: any): any => {
  // wx.openApi 调用时，需用content-type区分buffer or JSON
  const { headers } = response
  let transformRes = res
  if (headers['content-type'] === 'application/json; charset=utf-8') {
    transformRes = JSON.parse(transformRes.toString()) // JSON错误时buffer转JSON
  }
  return transformRes
}

export async function request(args: IRequestInfo): Promise<any> {
  // console.log('args', args)
  if (typeof args.isInternal === 'undefined') {
    args.isInternal = await checkIsInternalAsync()
  }

  args.opts = args.opts || {}
  args.opts.runEnvTag = await getCurrRunEnvTag()
  const requester = new TcbApiHttpRequester(args)
  const { action } = args.params

  if (action === 'wx.openApi' || action === 'wx.wxPayApi') {
    requester.setHooks({ handleData: handleWxOpenApiData })
  }

  if (action.startsWith('database') && process.env.SILENCE !== 'true') {
    requester.setSlowWarning(3000)
  }

  const result = await requester.request()
  if (result?.code) {
    return processReturn(result)
  }
  return result
}
