import jwt from 'jsonwebtoken'
import { E } from '../utils/utils'
import { ERROR } from '../const/code'
import { CloudBase } from '../cloudbase'
import { SYMBOL_CURRENT_ENV, SYMBOL_DEFAULT_ENV } from '../const/symbol'
import * as tcbapicaller from '../utils/tcbapirequester'
import * as tcbopenapicommonrequester from '../utils/tcbopenapicommonrequester'
import {
  IContextParam,
  ICustomReqOpts, IUserInfoQuery, IGetAuthContextResult, IGetUserInfoResult, IGetEndUserInfoResult
} from '../../types'

const checkCustomUserIdRegex = /^[a-zA-Z0-9_\-#@~=*(){}[\]:.,<>+]{4,32}$/

function validateUid(uid: string): void {
  if (typeof uid !== 'string') {
    throw E({ ...ERROR.INVALID_PARAM, message: 'uid must be a string' })
  }
  if (!checkCustomUserIdRegex.test(uid)) {
    throw E({ ...ERROR.INVALID_PARAM, message: `Invalid uid: "${uid}"` })
  }
}

export class Auth {
  private readonly cloudbase: CloudBase

  public constructor(cloudbase: CloudBase) {
    this.cloudbase = cloudbase
  }

  public async getAuthContext(context: IContextParam): Promise<IGetAuthContextResult> {
    const {
      TCB_UUID, LOGINTYPE, QQ_OPENID, QQ_APPID
    } = CloudBase.getCloudbaseContext(context)

    const result: any = {
      uid: TCB_UUID,
      loginType: LOGINTYPE
    }
    if (LOGINTYPE === 'QQ-MINI') {
      result.appId = QQ_APPID
      result.openId = QQ_OPENID
    }
    return result
  }

  public getClientIP(): string {
    const { TCB_SOURCE_IP } = CloudBase.getCloudbaseContext()
    return TCB_SOURCE_IP || ''
  }

  public getUserInfo(): IGetUserInfoResult {
    const {
      WX_OPENID,
      WX_APPID,
      TCB_UUID,
      TCB_CUSTOM_USER_ID,
      TCB_ISANONYMOUS_USER
    } = CloudBase.getCloudbaseContext()

    return {
      openId: WX_OPENID || '',
      appId: WX_APPID || '',
      uid: TCB_UUID || '',
      customUserId: TCB_CUSTOM_USER_ID || '',
      isAnonymous: TCB_ISANONYMOUS_USER === 'true'
    }
  }

  public async getEndUserInfo(uid?: string, opts?: ICustomReqOpts): Promise<IGetEndUserInfoResult> {
    const {
      WX_OPENID,
      WX_APPID,
      TCB_UUID,
      TCB_CUSTOM_USER_ID,
      TCB_ISANONYMOUS_USER
    } = CloudBase.getCloudbaseContext()

    const defaultUserInfo = {
      openId: WX_OPENID || '',
      appId: WX_APPID || '',
      uid: TCB_UUID || '',
      customUserId: TCB_CUSTOM_USER_ID || '',
      isAnonymous: TCB_ISANONYMOUS_USER === 'true'
    }

    if (uid === undefined) {
      return await Promise.resolve({
        userInfo: defaultUserInfo
      })
    }
    validateUid(uid)

    return await tcbapicaller.request({
      config: this.cloudbase.config,
      params: {
        action: 'auth.getUserInfoForAdmin',
        uuid: uid
      },
      method: 'post',
      opts,
      headers: {
        'content-type': 'application/json'
      }
    }).then(result => {
      if (result.code) {
        return result
      }

      return {
        userInfo: {
          ...defaultUserInfo,
          ...result.data
        },
        requestId: result.requestId
      }
    })
  }

  public async queryUserInfo(query: IUserInfoQuery, opts?: ICustomReqOpts): Promise<any> {
    const { uid, platform, platformId } = query
    return await tcbapicaller.request({
      config: this.cloudbase.config,
      params: {
        action: 'auth.getUserInfoForAdmin',
        uuid: uid,
        platform,
        platformId
      },
      method: 'post',
      opts,
      headers: {
        'content-type': 'application/json'
      }
    }).then(result => {
      if (result.code) {
        return result
      }

      return {
        userInfo: {
          ...result.data
        },
        requestId: result.requestId
      }
    })
  }

  public async getClientCredential(opts?: ICustomReqOpts): Promise<any> {
    // 如果有 accessKey 直接返回 accessKey，不用再去换取 token
    if (this.cloudbase.config.accessKey) {
      return this.cloudbase.config.accessKey
    }
    return await tcbopenapicommonrequester.request({
      config: this.cloudbase.config,
      method: 'POST',
      opts,
      headers: {
        'content-type': 'application/json'
      },
      path: '/auth/v1/token/clientCredential',
      data: {
        grant_type: 'client_credentials'
      }
    }).then(result => {
      return result.body
    })
  }

  public createTicket(uid: string, options: any = {}): string {
    validateUid(uid)

    const timestamp = new Date().getTime()
    const { TCB_ENV, SCF_NAMESPACE } = CloudBase.getCloudbaseContext()
    const { credentials } = this.cloudbase.config
    /* eslint-disable-next-line */
    const { env_id } = credentials

    let { envName } = this.cloudbase.config

    if (!envName) {
      throw E({ ...ERROR.INVALID_PARAM, message: 'no env in config' })
    }

    // 检查 credentials 是否包含 env
    if (!env_id) {
      throw E({
        ...ERROR.INVALID_PARAM,
        message:
          '当前私钥未包含env_id 信息， 请前往腾讯云云开发控制台，获取自定义登录最新私钥'
      })
    }

    // 使用symbol时替换为环境变量内的env
    if (envName === SYMBOL_CURRENT_ENV) {
      envName = TCB_ENV || SCF_NAMESPACE
    } else if (envName === SYMBOL_DEFAULT_ENV) {
      // nothing to do
    }

    // 检查 credentials env 和 init 指定 env 是否一致
    if (env_id && env_id !== envName) {
      throw E({
        ...ERROR.INVALID_PARAM,
        message: '当前私钥所属环境与 init 指定环境不一致！'
      })
    }

    if (!Reflect.has(options, 'allowInsecureKeySizes')) {
      options.allowInsecureKeySizes = true
    }

    const {
      refresh = 3600 * 1000,
      expire = timestamp + 7 * 24 * 60 * 60 * 1000
    } = options
    const token = jwt.sign(
      {
        alg: 'RS256',
        env: envName,
        iat: timestamp,
        exp: timestamp + 10 * 60 * 1000, // ticket十分钟有效
        uid,
        refresh,
        expire
      },
      credentials.private_key,
      {
        allowInsecureKeySizes: options.allowInsecureKeySizes === true,
        algorithm: 'RS256'
      }
    )

    return credentials.private_key_id + '/@@/' + token
  }
}

export function auth(cloudbase: CloudBase): Auth {
  return new Auth(cloudbase)
}
