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

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

controller.getAppVersion = () => async (ctx, next) => {
  // appVersion Format x.y.z (major.minor.patch)
  const appVersion = ctx.get('x-app-version');
  const appBuildNumber = toNumber(ctx.get('x-app-buildnumber'), 0);
  const appPlatform = ctx.get('x-app-platform');

  Object.assign(ctx.state, { appVersion, appBuildNumber, appPlatform });

  return next();
};
