// ESM version of solid.js
// global: owaspPasswordStrengthTest, TextEncoder, crypto, fetch

(function () {
  'use strict'

  const PasswordValidator = function (passwordField, repeatedPasswordField) {
    if (
      passwordField === null || passwordField === undefined ||
      repeatedPasswordField === null || repeatedPasswordField === undefined
    ) {
      return
    }

    this.passwordField = passwordField
    this.repeatedPasswordField = repeatedPasswordField

    this.fetchDomNodes()
    this.bindEvents()

    this.currentStrengthLevel = 0
    this.errors = []
  }

  const FEEDBACK_SUCCESS = 'success'
  const FEEDBACK_WARNING = 'warning'
  const FEEDBACK_ERROR = 'error'

  const ICON_SUCCESS = 'glyphicon-ok'
  const ICON_WARNING = 'glyphicon-warning-sign'
  const ICON_ERROR = 'glyphicon-remove'

  const VALIDATION_SUCCESS = 'has-success'
  const VALIDATION_WARNING = 'has-warning'
  const VALIDATION_ERROR = 'has-error'

  const STRENGTH_PROGRESS_0 = 'progress-bar-danger level-0'
  const STRENGTH_PROGRESS_1 = 'progress-bar-danger level-1'
  const STRENGTH_PROGRESS_2 = 'progress-bar-warning level-2'
  const STRENGTH_PROGRESS_3 = 'progress-bar-success level-3'
  const STRENGTH_PROGRESS_4 = 'progress-bar-success level-4'

  /**
   * Prefetch all dom nodes at initialisation in order to gain time at execution since DOM manipulations
   * are really time consuming
   */
  PasswordValidator.prototype.fetchDomNodes = function () {
    this.form = this.passwordField.closest('form')

    this.disablePasswordChecks = this.passwordField.classList.contains('disable-password-checks')

    this.passwordGroup = this.passwordField.closest('.form-group')
    this.passwordFeedback = this.passwordGroup.querySelector('.form-control-feedback')
    this.passwordStrengthMeter = this.passwordGroup.querySelector('.progress-bar')
    this.passwordHelpText = this.passwordGroup.querySelector('.help-block')

    this.repeatedPasswordGroup = this.repeatedPasswordField.closest('.form-group')
    this.repeatedPasswordFeedback = this.repeatedPasswordGroup.querySelector('.form-control-feedback')
  }

  PasswordValidator.prototype.bindEvents = function () {
    this.passwordField.addEventListener('focus', this.resetPasswordFeedback.bind(this))
    this.passwordField.addEventListener('keyup', this.instantFeedbackForPassword.bind(this))
    this.repeatedPasswordField.addEventListener('keyup', this.validateRepeatedPassword.bind(this))
    this.passwordField.addEventListener('blur', this.validatePassword.bind(this))
  }

  /**
   * Events Listeners
   */

  PasswordValidator.prototype.resetPasswordFeedback = function () {
    this.errors = []
    this.resetValidation(this.passwordGroup)
    this.resetFeedbackIcon(this.passwordFeedback)
    if (!this.disablePasswordChecks) {
      this.displayPasswordErrors()
      this.instantFeedbackForPassword()
    }
  }

  /**
   * Validate password on the fly to provide the user a visual strength meter
   */
  PasswordValidator.prototype.instantFeedbackForPassword = function () {
    const passwordStrength = this.getPasswordStrength(this.passwordField.value)
    const strengthLevel = this.getStrengthLevel(passwordStrength)

    if (this.currentStrengthLevel === strengthLevel) {
      return
    }

    this.currentStrengthLevel = strengthLevel

    this.updateStrengthMeter()

    if (this.repeatedPasswordField.value !== '') {
      this.updateRepeatedPasswordFeedback()
    }
  }

  /**
   * Validate password and display the error(s) message(s)
   */
  PasswordValidator.prototype.validatePassword = function () {
    this.errors = []
    const password = this.passwordField.value

    if (!this.disablePasswordChecks) {
      const passwordStrength = this.getPasswordStrength(password)
      this.currentStrengthLevel = this.getStrengthLevel(passwordStrength)

      if (passwordStrength.errors) {
        this.addPasswordError(passwordStrength.errors)
      }

      this.checkLeakedPassword(password).then(this.handleLeakedPasswordResponse.bind(this))
    }

    this.setPasswordFeedback()
  }

  /**
   * Validate the repeated password upon typing
   */
  PasswordValidator.prototype.validateRepeatedPassword = function () {
    this.updateRepeatedPasswordFeedback()
  }

  /**
   * User Feedback manipulators
   */

  /**
   * Update the strength meter based on OWASP feedback
   */
  PasswordValidator.prototype.updateStrengthMeter = function () {
    this.resetStrengthMeter()

    this.passwordStrengthMeter.classList.add.apply(
      this.passwordStrengthMeter.classList,
      this.tokenize(this.getStrengthLevelProgressClass())
    )
  }

  PasswordValidator.prototype.setPasswordFeedback = function () {
    const feedback = this.getFeedbackFromLevel()
    this.updateStrengthMeter()
    this.displayPasswordErrors()
    this.setFeedbackForField(feedback, this.passwordField)
  }

  /**
   * Update the repeated password feedback icon and color
   */
  PasswordValidator.prototype.updateRepeatedPasswordFeedback = function () {
    const feedback = this.checkPasswordFieldsEquality() ? FEEDBACK_SUCCESS : FEEDBACK_ERROR
    this.setFeedbackForField(feedback, this.repeatedPasswordField)
  }

  /**
   * Display the given feedback on the field
   * @param {string} feedback success|error|warning
   * @param {HTMLElement} field
   */
  PasswordValidator.prototype.setFeedbackForField = function (feedback, field) {
    const formGroup = this.getFormGroupElementForField(field)
    const visualFeedback = this.getFeedbackElementForField(field)

    this.resetValidation(formGroup)
    this.resetFeedbackIcon(visualFeedback)

    visualFeedback.classList.remove('hidden')

    visualFeedback.classList
      .add
      .apply(
        visualFeedback.classList,
        this.tokenize(this.getFeedbackIconClass(feedback))
      )

    formGroup.classList
      .add
      .apply(
        formGroup.classList,
        this.tokenize(this.getValidationClass(feedback))
      )
  }

  /**
   * Password Strength Helpers
   */

  /**
   * Get OWASP feedback on the given password. Returns false if the password is empty
   * @param password
   * @returns {object|false}
   */
  PasswordValidator.prototype.getPasswordStrength = function (password) {
    if (password === '') {
      return false
    }
    return owaspPasswordStrengthTest.test(password)
  }

  /**
   * Get the password strength level based on password strength feedback object given by OWASP
   * @param passwordStrength
   * @returns {number}
   */
  PasswordValidator.prototype.getStrengthLevel = function (passwordStrength) {
    if (passwordStrength === false) {
      return 0
    }
    if (passwordStrength.requiredTestErrors.length !== 0) {
      return 1
    }

    if (passwordStrength.strong === false) {
      return 2
    }

    if (passwordStrength.isPassphrase === false || passwordStrength.optionalTestErrors.length !== 0) {
      return 3
    }

    return 4
  }

  PasswordValidator.prototype.LEVEL_TO_FEEDBACK_MAP = [
    FEEDBACK_ERROR,
    FEEDBACK_ERROR,
    FEEDBACK_WARNING,
    FEEDBACK_SUCCESS,
    FEEDBACK_SUCCESS
  ]

  /**
   * @returns {string}
   */
  PasswordValidator.prototype.getFeedbackFromLevel = function () {
    return this.LEVEL_TO_FEEDBACK_MAP[this.currentStrengthLevel]
  }

  PasswordValidator.prototype.LEVEL_TO_PROGRESS_MAP = [
    STRENGTH_PROGRESS_0,
    STRENGTH_PROGRESS_1,
    STRENGTH_PROGRESS_2,
    STRENGTH_PROGRESS_3,
    STRENGTH_PROGRESS_4
  ]

  /**
   * Get the CSS class for the meter based on the current level
   */
  PasswordValidator.prototype.getStrengthLevelProgressClass = function () {
    return this.LEVEL_TO_PROGRESS_MAP[this.currentStrengthLevel]
  }

  PasswordValidator.prototype.addPasswordError = function (error) {
    this.errors.push(...(Array.isArray(error) ? error : [error]))
  }

  PasswordValidator.prototype.displayPasswordErrors = function () {
    // Erase the error list content
    while (this.passwordHelpText.firstChild) {
      this.passwordHelpText.removeChild(this.passwordHelpText.firstChild)
    }

    // Add the errors in the stack to the DOM
    this.errors.map((error) => {
      const text = document.createTextNode(error)
      const paragraph = document.createElement('p')
      paragraph.appendChild(text)
      this.passwordHelpText.appendChild(paragraph)
    })
  }

  PasswordValidator.prototype.FEEDBACK_TO_ICON_MAP = []
  PasswordValidator.prototype.FEEDBACK_TO_ICON_MAP[FEEDBACK_SUCCESS] = ICON_SUCCESS
  PasswordValidator.prototype.FEEDBACK_TO_ICON_MAP[FEEDBACK_WARNING] = ICON_WARNING
  PasswordValidator.prototype.FEEDBACK_TO_ICON_MAP[FEEDBACK_ERROR] = ICON_ERROR

  /**
   * @param success|error|warning feedback
   */
  PasswordValidator.prototype.getFeedbackIconClass = function (feedback) {
    return this.FEEDBACK_TO_ICON_MAP[feedback]
  }

  PasswordValidator.prototype.FEEDBACK_TO_VALIDATION_MAP = []
  PasswordValidator.prototype.FEEDBACK_TO_VALIDATION_MAP[FEEDBACK_SUCCESS] = VALIDATION_SUCCESS
  PasswordValidator.prototype.FEEDBACK_TO_VALIDATION_MAP[FEEDBACK_WARNING] = VALIDATION_WARNING
  PasswordValidator.prototype.FEEDBACK_TO_VALIDATION_MAP[FEEDBACK_ERROR] = VALIDATION_ERROR

  /**
   * @param success|error|warning feedback
   */
  PasswordValidator.prototype.getValidationClass = function (feedback) {
    return this.FEEDBACK_TO_VALIDATION_MAP[feedback]
  }

  /**
   * Validators
   */

  /**
   * Check if both password fields are equal
   * @returns {boolean}
   */
  PasswordValidator.prototype.checkPasswordFieldsEquality = function () {
    return this.passwordField.value === this.repeatedPasswordField.value
  }

  /**
   * Check if the password is leaked
   * @param password
   */
  PasswordValidator.prototype.checkLeakedPassword = function (password) {
    const url = 'https://api.pwnedpasswords.com/range/'

    return new Promise(function (resolve, reject) {
      this.sha1(password).then((digest) => {
        const preFix = digest.slice(0, 5)
        let suffix = digest.slice(5, digest.length)
        suffix = suffix.toUpperCase()

        return fetch(url + preFix)
          .then(function (response) {
            return response.text()
          })
          .then(function (data) {
            resolve(data.indexOf(suffix) > -1)
          })
          .catch(function (err) {
            reject(err)
          })
      })
    }.bind(this))
  }

  PasswordValidator.prototype.handleLeakedPasswordResponse = function (hasPasswordLeaked) {
    if (hasPasswordLeaked === true) {
      this.currentStrengthLevel--
      this.addPasswordError('This password was exposed in a data breach. Please use a more secure alternative one!')
    }

    this.setPasswordFeedback()
  }

  /**
   * CSS Classes reseters
   */

  PasswordValidator.prototype.resetValidation = function (el) {
    const tokenizedClasses = this.tokenize(
      VALIDATION_ERROR,
      VALIDATION_WARNING,
      VALIDATION_SUCCESS
    )

    el.classList.remove.apply(
      el.classList,
      tokenizedClasses
    )
  }

  PasswordValidator.prototype.resetFeedbackIcon = function (el) {
    const tokenizedClasses = this.tokenize(
      ICON_ERROR,
      ICON_WARNING,
      ICON_SUCCESS
    )

    el.classList.remove.apply(
      el.classList,
      tokenizedClasses
    )
  }

  PasswordValidator.prototype.resetStrengthMeter = function () {
    const tokenizedClasses = this.tokenize(
      STRENGTH_PROGRESS_1,
      STRENGTH_PROGRESS_2,
      STRENGTH_PROGRESS_3,
      STRENGTH_PROGRESS_4
    )

    this.passwordStrengthMeter.classList.remove.apply(
      this.passwordStrengthMeter.classList,
      tokenizedClasses
    )
  }

  /**
   * Helpers
   */

  PasswordValidator.prototype.getFormGroupElementForField = function (field) {
    if (field === this.passwordField) {
      return this.passwordGroup
    }

    if (field === this.repeatedPasswordField) {
      return this.repeatedPasswordGroup
    }
  }

  PasswordValidator.prototype.getFeedbackElementForField = function (field) {
    if (field === this.passwordField) {
      return this.passwordFeedback
    }

    if (field === this.repeatedPasswordField) {
      return this.repeatedPasswordFeedback
    }
  }

  /**
   * Returns an array of strings ready to be applied on classList.add or classList.remove
   * @returns {string[]}
   */
  PasswordValidator.prototype.tokenize = function () {
    const tokenArray = []
    for (const i in arguments) {
      tokenArray.push(arguments[i])
    }
    return tokenArray.join(' ').split(' ')
  }

  PasswordValidator.prototype.sha1 = function (str) {
    const buffer = new TextEncoder('utf-8').encode(str)

    return crypto.subtle.digest('SHA-1', buffer).then((hash) => {
      return this.hex(hash)
    })
  }

  PasswordValidator.prototype.hex = function (buffer) {
    const hexCodes = []
    const view = new DataView(buffer)
    for (let i = 0; i < view.byteLength; i += 4) {
      const value = view.getUint32(i)
      const stringValue = value.toString(16)
      const padding = '00000000'
      const paddedValue = (padding + stringValue).slice(-padding.length)
      hexCodes.push(paddedValue)
    }
    return hexCodes.join('')
  }

  new PasswordValidator(
    document.getElementById('password'),
    document.getElementById('repeat_password')
  )
})()
