/* eslint-disable no-bitwise */
const debug = require('debug')('ctrl:common')
const util = require('util')
const joi = require('joi')
const response = require('src/utils/response/index.js')
const config = require('src/config/index.js')
const sso = require('src/utils/sso/index.js')
const { copyObject } = require('src/utils/index.js')
const { get: getCacheInstance } = require('src/utils/cache.js')

const { Success, InternalError, DataFormat, Unauthorized } =
  response.resp

const controller = {}
module.exports = controller

/**
 * api reponse function
 * @param {import('src/utils/response/index.js').respObject} resp
 * @param {string|Object} body
 */
function responseFunc (resp, body) {
  /** @type {import('src/utils/response/index.js').respObject} */
  const copy = copyObject(response.checkStruct(resp) ? resp : Success)

  if (typeof body === 'string') copy.object.message = body
  else if (typeof body === 'object') copy.object = body

  // @ts-ignore
  if (!('obj' in this)) this.obj = {}
  this.obj.status = copy.status
  this.obj.object = copy.object
}

/**
 * api error response function
 * @param {import('src/utils/response/index.js').respObject} resp
 * @param {import('src/utils/response/index.js').codeMessage=} code
 */
function responseError (resp, code) {
  /** @type {import('src/utils/response/index.js').respObject} */
  const copy = copyObject(response.checkStruct(resp) ? resp : InternalError)

  if (code && typeof code === 'object' && 'message' in code && 'code' in code) {
    copy.object = code
  }

  const err = new response.APIError(copy.object.message, copy)
  throw err
}

controller.apiHandler = () => async (ctx, next) => {
  ctx.obj = {}
  ctx.token = {}

  ctx.resp = responseFunc.bind(ctx)
  ctx.err = responseError

  ctx.getBody = (key) => (ctx.request.body || {})[key]
  ctx.getFile = (key) => (ctx.request.files || {})[key]

  // run next
  try {
    await next()
  } catch (err) {
    debug(`Get API Throw Error: ${util.inspect(err, false, null)}`)
    // debug(err.stack);
    if (!(err instanceof response.APIError)) {
      ctx.resp(InternalError)
    } else {
      ctx.obj = err.object
    }

    if (process.env.NODE_ENV !== 'production') {
      ctx.obj.object.errorStack = err.stack
      ctx.obj.object.errorMessage = err.message
    }
  }

  if (Object.keys(ctx.obj).length > 0) {
    ctx.status = ctx.obj.status
    ctx.body = ctx.obj.object
  }
}

/**
 * data validate middleware
 * @param {{query?: any, header?: any, body?: any}} schema body,query and header is joi.Schema
 */
controller.validate = (schema) => {
  if (typeof schema !== 'object') responseError(InternalError)
  const v = {}
  if ('body' in schema) {
    v.body = joi.isSchema(schema.body)
      ? schema.body
      : joi.object(schema.body).unknown()
  }
  if ('header' in schema) {
    v.header = joi.isSchema(schema.header)
      ? schema.header
      : joi.object(schema.header).unknown()
  }
  if ('query' in schema) {
    v.query = joi.isSchema(schema.query)
      ? schema.query
      : joi.object(schema.query).unknown()
  }

  return async (ctx, next) => {
    try {
      await joi.object(v).unknown().validateAsync({
        query: ctx.query,
        header: ctx.headers,
        body: ctx.request.body
      })
    } catch (err) {
      debug(`data validate error: ${util.inspect(err, false, null)}`)
      responseError(DataFormat)
    }
    return next()
  }
}

/**
 * @return {import('koa').Middleware}
 */
controller.authorization = () => {
  return async (ctx, next) => {
    ctx.token = {}
    /** @type {string} */
    const token = ctx.get('authorization')

    if (!token) ctx.err(Unauthorized)

    try {
      const strs = token.split(/\s/)
      debug(`Get Header: ${token}`)
      if (strs.length !== 2 || !/^bearer$/i.test(strs[0])) { ctx.err(Unauthorized, response.codeMessage.CodeTokenInvalid) }

      [, ctx.token.origin] = strs

      const decoded = {}
      const cache = getCacheInstance()
      // cache not exists
      if (!cache.get(ctx.token.origin)) ctx.err(Unauthorized)

      try {
        // 可以考慮這邊做個cache 多久之內存取不會到keycloak驗證
        let userInfo = await sso.getUserInfo(ctx.token.origin)
        if (!userInfo) {
          // try refresh
          const oldToken = ctx.token.origin
          const refreshToken = cache.get(ctx.token.origin)
          if (!refreshToken) throw new Error('no cache data')
          const token = await sso.refreshToken(refreshToken)
          // set new cache
          cache.set(token.access_token, token.refresh_token, false)
          ctx.token.origin = token.access_token
          userInfo = await sso.getUserInfo(token.access_token)
          if (!userInfo) throw new Error('get user info fail')

          cache.del(oldToken)
          ctx.set('x-new-token', ctx.token.origin)
        }

        Object.assign(decoded, userInfo)
      } catch (err) {
        debug(`user info get fail ::: ${util.inspect(err, false, null)}`)
        ctx.err(Unauthorized)
      }

      ctx.token.user_id = decoded.username
      ctx.token.sso = true

      ctx.token.info = decoded

      ctx.verified = true
    } catch (err) {
      debug(`Token valid fail: ${util.inspect(err, false, null)}`)
      throw err
    }

    return next()
  }
}
