const util = require("util")
const get = require("lodash.get")
const set = require("lodash.set")
const defaultsDeep = require("lodash.defaultsdeep")

const yup = require("yup")

const optionsSchema = yup.object({
  async: yup.bool(),
  cast: yup.bool(),
  errorFull: yup.bool(),
  errorDepth: yup.number().positive().integer(),
  defaults: yup.object(),
  validate: yup.object(),
  properties: yup.object().shape({
    required: yup.bool(),
    validate: yup.object(),
  }),
  required: yup.array().of(yup.string()),
})

const defaultOptions = {
  async: false,
  cast: false,
  errorFull: false,
  errorDepth: 2,
  defaults: {},
  validate: {},
  required: [],
  properties: {},
}

function mergeSchemas(...schemas) {
  const [first, ...rest] = schemas.filter((schema) => !!schema)
  const merged = rest.reduce(
    (mergedSchemas, schema) => mergedSchemas.concat(schema),
    first
  )
  return merged
}

module.exports = function createOptions(options = {}, funcName) {
  defaultsDeep(options, defaultOptions)
  optionsSchema.validateSync(options)

  const { validate } = options

  const { properties } = options
  const { required } = options

  for (const [k, prop] of Object.entries(properties)) {
    const key = k.split(".").join(".fields.")
    const validator = get(validate, key)
    const { validate: validateProp, required: requiredProp } = prop
    if (requiredProp) {
      required.push(k)
    }
    set(validate, key, mergeSchemas(validator, validateProp))
  }

  for (const k of required) {
    const key = k.split(".").join(".fields.")
    const validator = get(validate, key)
    if (validator) {
      set(validate, key, validator.required())
    } else {
      set(validate, key, yup.mixed().required())
    }
  }

  const { cast, errorFull, errorDepth } = options
  const validator = yup.object(validate)

  if (options.async) {
    return async (opts) => {
      defaultsDeep(opts, options.defaults)
      try {
        await validator.validate(opts)
      } catch (e) {
        if (funcName) {
          e.message += ` calling function "${funcName}"`
        }
        if (!errorFull) {
          e.value = util.inspect(e.value, { depth: errorDepth })
        }
        throw e
      }
      if (cast) {
        Object.assign(opts, validator.cast(opts))
      }
    }
  }

  return (opts) => {
    defaultsDeep(opts, options.defaults)
    try {
      validator.validateSync(opts)
    } catch (e) {
      if (funcName) {
        e.message += ` calling function "${funcName}"`
      }
      if (!errorFull) {
        e.value = util.inspect(e.value, { depth: errorDepth })
      }
      throw e
    }
    if (cast) {
      Object.assign(opts, validator.cast(opts))
    }
  }
}
