/* global UB */

const Vue = require('vue')
// vuex required for type checking
// eslint-disable-next-line no-unused-vars
const Vuex = require('vuex')
const { required, minValue, maxValue } = require('vuelidate/lib/validators/index')
const { i18n } = require('@unitybase/ub-pub')

const { mapInstanceFields, validateWithErrorText } = require('./helpers')

const MAX_INT_VALUE = 2 ** 31 - 1
const MIN_INT_VALUE = -(2 ** 31)

const attrCaptionsMixin = {
  computed: {
    attributeCaptions () {
      const { attributeCaptions } = this.$options
      if (typeof attributeCaptions === 'function') {
        return attributeCaptions.call(this)
      }
      return attributeCaptions
    }
  },

  methods: {
    getCustomFormCaptionByPath (path) {
      return path.reduce(getPropByKey, this.attributeCaptions)
    },

    getAttributeCaption (path) {
      return this.getCustomFormCaptionByPath(path) ?? getEntityAttributeCaption(this.entitySchema, path[0])
    }
  },

  attributeCaptions: {}
}

/**
 * Abstraction for easy managing of validation of some form. This class uses a `Vue`
 * instance with configured `Vuelidate` state to provide reactivity of form data.
 */
class Validator {
  constructor (vueInstance) {
    this._vueInstance = vueInstance
  }

  /**
   * Create a instance for form data validation based on some options defined as Vue mixin.
   * Default behavior is to check entity schema attributes with `allowNull=true` and `defaultView=true`.
   *
   * `customValidationMixin` can extend default behavior by it own rules.
   *
   * @param {object} params
   * @param {Vuex} params.store Store
   * @param {UBEntity} params.entitySchema Entity schema
   * @param {string[]} params.masterFieldList Field list of master entity
   * @param {Vue} [customValidationMixin={}] Custom validations what extends default
   */
  static initializeWithCustomOptions ({
    store,
    entitySchema,
    masterFieldList,
    customValidationMixin = {}
  }) {
    const requiredAttributesNames = entitySchema
      .filterAttribute(attr => attr.defaultView && !attr.allowNull && masterFieldList.includes(attr.code))
      .map(attr => attr.name)

    const intAttributesNames = entitySchema.filterAttribute(attr => attr.dataType === 'Int').map(attr => attr.name)

    const defaultValidationMixin = {
      computed: {
        ...mapInstanceFields(entitySchema.getAttributeNames()),

        entitySchema () {
          return entitySchema
        }
      },

      validations () {
        const requiredAttrs = requiredAttributesNames.map(attr => [attr, { required }])
        const validationObject = Object.fromEntries(requiredAttrs)
        if (intAttributesNames.length) {
          for (const attributeName of intAttributesNames) {
            validationObject[attributeName] = validationObject[attributeName] ?? {}
            validationObject[attributeName].minValue = validateWithErrorText(
              i18n('numberExceedsMinLimit', MIN_INT_VALUE),
              minValue(MIN_INT_VALUE)
            )
            validationObject[attributeName].maxValue = validateWithErrorText(
              i18n('numberExceedsMaxLimit', MAX_INT_VALUE),
              maxValue(MAX_INT_VALUE)
            )
          }
        }
        return validationObject
      }
    }

    const vueInstance = new Vue({
      store,
      mixins: [
        attrCaptionsMixin,
        defaultValidationMixin,
        customValidationMixin
      ]
    })

    return new Validator(vueInstance)
  }

  /**
   * Returns the current state of validation. The method is useful when you have dynamic validation
   *
   * @returns {object} Vuelidate object
   */
  getValidationState () {
    return this._vueInstance.$v
  }

  /**
   * Get caption by attribute name from `attributeCaptions` sections. If it
   * is not defined, default locale i18n(`${entity}.${attributeName}`) will be returned
   *
   * @param {string} attributeName
   * @returns {string}
   */
  getAttributeCaption (attributeName) {
    const attributePath = attributeName.split('.')
    const caption = this._vueInstance.getAttributeCaption(attributePath)
    return UB.i18n(caption)
  }

  /**
   * Get error text for some first failed validation rule of the attribute
   *
   * @param {string} attributeName
   * @returns {string | null}
   */
  getErrorForAttribute (attributeName) {
    const attributePath = attributeName.split('.')
    const attrValidationState = attributePath.reduce(getPropByKey, this._vueInstance.$v)

    if (!attrValidationState || !attrValidationState.$error) {
      return null
    }

    for (const param in attrValidationState.$params) {
      if (attrValidationState[param] === false) {
        const ruleParams = attrValidationState.$params[param]
        const errorText = ruleParams ? ruleParams.$errorText : null
        if (errorText) {
          return UB.i18n(errorText)
        }
      }
    }

    return UB.i18n('requiredField')
  }

  /**
   * Check if the attribute has the required rule in the configured validation
   *
   * @param {string} attributeName
   * @returns {boolean}
   */
  getIsAttributeRequired (attributeName) {
    const attributePath = attributeName.split('.')
    const attrValidationState = attributePath.reduce(getPropByKey, this._vueInstance.$v)
    return attrValidationState && 'required' in attrValidationState
  }

  /**
   * Validates form data with the Vuelidate help. If validation is failed `UBAbortError` will be thrown
   *
   * @param {object} [params = {}]
   * @param {boolean} [params.showErrorModal = true] To display error modal if validation is failed
   * @param {string} [params.errorMsgTemplate = 'validationError'] Error message template for the error modal
   * @throws {UB.UBAbortError}
   */
  validateForm ({
    showErrorModal = true,
    errorMsgTemplate = 'validationError'
  } = {}) {
    const { $v } = this._vueInstance

    $v.$touch()
    if (!$v.$error) {
      return
    }

    const invalidFieldsCaptions = new Set()
    for (const { path } of $v.$flattenParams()) {
      const attrValidation = path.reduce(getPropByKey, $v)
      if (attrValidation.$error) {
        // for array validation remove $each.XXX: aaa.bbb.$each.125.ccc.ddd ->  aa.bbb.$each.125.ccc.ddd
        const attrCaption = this.getAttributeCaption(path.filter((f, idx, arr) => (f !== '$each') && (arr[idx - 1] !== '$each')).join('.'))
        invalidFieldsCaptions.add(attrCaption)
      }
    }
    const errMsg = UB.i18n(errorMsgTemplate, [...invalidFieldsCaptions].join(', '))

    if (showErrorModal) {
      UB.showErrorWindow(errMsg)
    }

    throw new UB.UBAbortError(errMsg)
  }

  /**
   * Reset validation state
   */
  reset () {
    this._vueInstance.$v.$reset()
  }
}

/**
 * Helper function that returns an object property by the key. This method is useful for
 * passing to the `reduce()` array function to get some object nested property
 *
 * @param {object|null} obj
 * @param {string} key
 * @returns {object|null}
 */
function getPropByKey (obj, key) {
  return obj ? obj[key] : null
}

/**
 * Get entity attribute caption in the current locale
 *
 * @param {UBEntity} entitySchema
 * @param {string} attr
 * @returns {string}
 */
function getEntityAttributeCaption (entitySchema, attr) {
  const localeString = `${entitySchema.code}.${attr}`
  return UB.i18n(localeString) === localeString ? attr : UB.i18n(localeString)
}

const validationMixin = {
  inject: {
    entitySchema: {
      from: 'entitySchema',
      default: {}
    }
  },

  mixins: [
    attrCaptionsMixin
  ],

  provide () {
    return {
      validator: this.validator
    }
  },

  computed: {
    validator () {
      return new Validator(this)
    }
  }
}

module.exports = {
  Validator,
  validationMixin
}