/* 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 { jwt } = require('src/utils/pkgs.js');
const { copyObject, toNumber } = require('src/utils/index.js');

const { Success, InternalError, DataFormat, Forbidden, 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();
  };
};

/**
 * @param {boolean=} allowExpired
 * @return {import('koa').Middleware}
 */
 controller.authorization = allowExpired => {
  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;

      let decoded = {};
      let expired = false;

      try {
        decoded = jwt.verify(strs[1], config.server.jwt_secret);

        await joi
          .object({
            user_id: joi.string().required(),
          })
          .unknown()
          .validateAsync(decoded);
      } catch (err) {
        debug(`jwt token verify fail: ${util.inspect(err, false, null)}`);
        if (err instanceof jwt.TokenExpiredError) {
          decoded = jwt.decode(ctx.token.origin);
          expired = true;
        } else {
          throw err;
        }
      }

      ctx.token.user_id = decoded.user_id;
      ctx.token.sso = !!decoded.sso;

      if (expired) ctx.err(Forbidden, response.codeMessage.CodeTokenExpired);

      ctx.verified = true;
    } catch (err) {
      debug(`Token valid fail: ${util.inspect(err, false, null)}`);
      if (err instanceof response.APIError) {
        // 如果是過期的錯誤，判斷是否允許過期存取
        // @ts-ignore
        // eslint-disable-next-line
        if (err._object?.object?.code === response.codeMessage.CodeTokenExpired.code) {
          if (!!allowExpired) return next();
        }
      }
      throw err;
    }

    return next();
  };
};
