/* global uba_user ubs_settings */
// eslint-disable-next-line camelcase
const me = uba_user
const UBA_COMMON = require('@unitybase/base').uba_common
const UB = require('@unitybase/ub')
const Session = UB.Session
const App = UB.App
const publicRegistration = require('./modules/publicRegistration').publicRegistration

me.publicRegistration = publicRegistration
me.entity.addMethod('publicRegistration')

// bypass HTTP logging for changePassword to hide sensitive information
App.registerEndpoint('changePassword', changePasswordEp, true, false, true)

me.entity.addMethod('changeLanguage')
me.entity.addMethod('setUDataKey')
me.entity.addMethod('changeOtherUserPassword')
me.entity.addMethod('getUserData')

me.on('insert:before', checkDuplicateUser)
me.on('update:before', checkDuplicateUser)
me.on('insert:before', fillFullNameIfMissing)
me.on('insert:after', ubaAuditNewUser)
me.on('update:before', denyBuildInUserRename)
me.on('update:after', ubaAuditModifyUser)
me.on('delete:after', ubaAuditDeleteUser)
me.on('delete:before', denyBuildInUserDeletion)

/**
 * Do not allow user with same name but in different case
 * @private
 * @param {ubMethodParams} ctx
 */
function checkDuplicateUser (ctx) {
  const params = ctx.mParams.execParams
  const newName = params.name
  const ID = params.ID
  if (newName) {
    const store = UB.Repository('uba_user').attrs('ID')
      .where('name', '=', newName.toLowerCase().trim())
      .whereIf(ID, 'ID', '<>', ID)
      .select()

    if (!store.eof) {
      throw new UB.UBAbort('<<<uba_user_errors.duplicateUserName>>>')
    }
    params.name = newName.toLowerCase().trim() // convert a username to lower case
  }
}

/**
 * Set fullName = name in case fullName is missing
 * Set lastPasswordChangeDate = maxDate in case user is domainUser
 * @private
 * @param {ubMethodParams} ctx
 */
function fillFullNameIfMissing (ctx) {
  const params = ctx.mParams.execParams
  const { name, firstName, middleName, lastName, fullName } = params
  const namePartsOrder = ({ firstName, middleName, lastName }) => {
    const lastNameFirst = ['uk', 'ru', 'az', 'ka', 'uz'].includes(Session.userLang)
    return lastNameFirst
      ? [lastName, firstName, middleName]
      : [firstName, middleName, lastName]
  }
  if (!fullName) {
    const formattedFullName = namePartsOrder({ firstName, middleName, lastName })
      .filter(value => !!value)
      .join(' ')
    if (formattedFullName) {
      params.fullName = formattedFullName
    } else {
      params.fullName = name
    }
  }
  if (params.name && params.name.indexOf('\\') !== -1) {
    // domain/ldap user password never expire on UB level
    params.lastPasswordChangeDate = new Date(2099, 12, 31)
  }
}

/**
 * Change user password
 * @param {number} userID
 * @param {string} userName Either userName or userID must be specified
 * @param  {string} password
 * @param {boolean} [needChangePassword=false] If true the password will be expired
 * @param {string} [oldPwdHash] Optional for optimisation
 * @param {boolean} [skipOnMatch=false] Optional - do not update password in case password hash is not changed
 * @param {boolean} [newerExpire=false] Optional - set lastChangeDate to infinity, so password newer expire
 * @returns {boolean} is password changed
 * @function changePassword
 * @memberOf uba_user_ns.prototype
 * @memberOfModule @unitybase/uba
 * @public
 */
me.changePassword = function (userID, userName, password, needChangePassword,
  oldPwdHash, skipOnMatch, newerExpire) {
  if (!(userID || userName) || !password) throw new Error('Invalid parameters')

  const store = UB.DataStore('uba_user')
  if (userID && (!userName || !oldPwdHash)) {
    UB.Repository('uba_user').attrs(['ID', 'name', 'uPasswordHashHexa']).where('[ID]', '=', userID).select(store)
    userName = store.get('name')
    oldPwdHash = store.get('uPasswordHashHexa')
  } else if (userName && (!userID || !oldPwdHash)) {
    UB.Repository('uba_user').attrs(['ID', 'name', 'uPasswordHashHexa']).where('[name]', '=', userName.toLowerCase()).select(store)
    userID = store.get('ID')
    oldPwdHash = store.get('uPasswordHashHexa')
  }
  let newPwd = password || ''
  if (skipOnMatch) {
    const newPwdHash = Session._buildPasswordHash(userName, newPwd)
    if (newPwdHash === oldPwdHash) {
      console.debug('Skip change password because skipOnMatch')
      return false
    }
  }

  // eslint-disable-next-line camelcase
  const passwordPolicy = ubs_settings
    ? {
        minLength: ubs_settings.loadKey('UBA.passwordPolicy.minLength', 3),
        checkCmplexity: ubs_settings.loadKey('UBA.passwordPolicy.checkCmplexity', false),
        checkDictionary: ubs_settings.loadKey('UBA.passwordPolicy.checkDictionary', false),
        allowMatchWithLogin: ubs_settings.loadKey('UBA.passwordPolicy.allowMatchWithLogin', false),
        checkPrevPwdNum: ubs_settings.loadKey('UBA.passwordPolicy.checkPrevPwdNum', 4)
      }
    : {}

  if (!needChangePassword) { // skip password policy validation in case user must change password on first logon
    // minLength
    if (passwordPolicy.minLength > 0) {
      if (newPwd.length < passwordPolicy.minLength) {
        throw new Error('<<<Password is too short>>>')
      }
    }

    // checkComplexity
    if (passwordPolicy.checkCmplexity) {
      if (!(/[A-Z]/.test(newPwd) && /[a-z]/.test(newPwd) &&
        /[0-9]/.test(newPwd) && /[~!@#$%^&*()_+|\\=\-/'":;<>.,[\]{}?]/.test(newPwd))
      ) {
        throw new Error('<<<Password is too simple>>>')
      }
    }
    // checkDictionary
    if (passwordPolicy.checkDictionary) {
      // todo - check password from dictionary
      // if (false) {
      //   throw new Error('<<<Password is dictionary word>>>')
      // }
    }

    // allowMatchWithLogin
    if (!passwordPolicy.allowMatchWithLogin) {
      if (newPwd.includes(userName)) {
        throw new Error('<<<Password matches with login>>>')
      }
    }
  }

  newPwd = Session._buildPasswordHash(userName, newPwd)
  // checkPrevPwdNum
  if (passwordPolicy.checkPrevPwdNum > 0) {
    UB.Repository('uba_prevPasswordsHash')
      .attrs('uPasswordHashHexa')
      .where('userID', '=', userID)
      .limit(passwordPolicy.checkPrevPwdNum)
      .orderBy('mi_createDate', 'desc').select(store)
    store.first()
    while (!store.eof) {
      if (store.get('uPasswordHashHexa') === newPwd) {
        throw new Error('<<<Previous password is not allowed>>>')
      }
      store.next()
    }
  }

  // since attribute uPasswordHashHexa is not defined in entity metadata
  // for security reason we need to execute SQL
  // It's always better to not use execSQL at all!
  store.execSQL(
    'update uba_user set uPasswordHashHexa=:newPwd:, lastPasswordChangeDate=:lastPasswordChangeDate: where id = :userID:',
    {
      newPwd,
      lastPasswordChangeDate: newerExpire
        ? new Date(9000, 0, 1)
        : needChangePassword ? new Date(2000, 1, 1) : new Date(),
      userID
    }
  )
  // store oldPwdHash
  if (oldPwdHash) {
    store.run('insert', {
      entity: 'uba_prevPasswordsHash',
      execParams: {
        userID,
        uPasswordHashHexa: oldPwdHash
      }
    })
  }
  return true
}

/**
 * Change (or set) user password for currently logged-in user.
 * Members of `Supervisor` role can change password for other users using uba_user.changeOtherUserPassword method
 *
 * If endpoint is called with URI parameter ?getPasswordPolicy=true - then result is password policies JSON
 *   { minLength: number, checkCmplexity: boolean}
 *
 * @private
 * @param {THTTPRequest}  req
 * @param {THTTPResponse} resp
 */
function changePasswordEp (req, resp) {
  if (req.parsedParameters.getPasswordPolicy) {
    const passwordPolicy = ubs_settings
      ? {
          minLength: ubs_settings.loadKey('UBA.passwordPolicy.minLength', 3),
          checkCmplexity: ubs_settings.loadKey('UBA.passwordPolicy.checkCmplexity', false)
        }
      : {}
    resp.writeEnd(passwordPolicy)
    resp.statusCode = 200
    return
  }
  const params = req.json()
  const newPwd = params.newPwd || ''
  const pwd = params.pwd || ''
  const needChangePassword = params.needChangePassword || false
  const store = UB.DataStore('uba_user')
  let dbPwdHash

  let failException = null
  const userID = Session.userID
  const userName = Session.uData.login
  let isPwdChanged = true
  try {
    if (!newPwd) throw new UB.ESecurityException('changePassword: newPwd parameter is required')
    UB.Repository('uba_user').attrs('name', 'uPasswordHashHexa').where('ID', '=', userID).select(store)
    if (store.eof) {
      throw new UB.ESecurityException('Can\'t load a user by ID')
    }
    dbPwdHash = store.get('uPasswordHashHexa')
    // check password
    const currentPwdHash = Session._buildPasswordHash(userName, pwd)
    if (currentPwdHash !== dbPwdHash) {
      throw new UB.ESecurityException('<<<Incorrect old password>>>')
    }
    isPwdChanged = me.changePassword(userID, userName, newPwd, needChangePassword, dbPwdHash)
  } catch (e) {
    failException = e
  }

  // make uba_audit record
  if (isPwdChanged && App.domainInfo.has('uba_audit')) {
    store.run('insert', {
      entity: 'uba_audit',
      execParams: {
        entity: 'uba_user',
        entityinfo_id: userID, // Session.userID,
        actionType: failException ? 'SECURITY_VIOLATION' : 'UPDATE',
        actionUser: Session.uData.login,
        actionTime: new Date(),
        remoteIP: Session.callerIP,
        targetUser: userName.toLowerCase(),
        toValue: failException
          ? JSON.stringify({ action: 'changePassword', reason: failException.message })
          : JSON.stringify({ action: 'changePassword' })
      }
    })
    App.dbCommit()
  }
  if (failException) throw failException
  resp.statusCode = 200
}

/**
 * Change (or set) user password for any user.
 * Call of this method should be restricted to a small number of roles/groups. By default, can be called by supervisor role
 *
 * @param {ubMethodParams} ctx
 * @param {string|number} ctx.mParams.execParams.forUser Name or ID of the user for whom you want to change the password
 * @param {string} ctx.mParams.execParams.newPwd         New password
 * @param {boolean} [ctx.mParams.execParams.needChangePassword] Indicates that the user must change the password at the first login
 * @param {boolean} [ctx.mParams.skipOnMatch=false] Optional - skip change password in case it match current (to be used in migrations only)
 * @param {boolean} [ctx.mParams.newerExpire=false] Optional - set lastChangeDate to infinity, so password newer expire
 * @memberOf uba_user_ns.prototype
 * @memberOfModule @unitybase/uba
 * @published
 */
function changeOtherUserPassword (ctx) {
  const { newPwd, needChangePassword, forUser, skipOnMatch, newerExpire } = ctx.mParams.execParams
  const store = UB.DataStore('uba_user')

  if (!newPwd) throw new Error('newPwd parameter is required')

  let failException = null
  const userID = UBA_COMMON.USERS.ANONYMOUS.ID
  let isPwdChanged = true
  try {
    UB.Repository('uba_user').attrs('ID', 'uPasswordHashHexa').where('[name]', '=', '' + forUser.toLowerCase()).select(store)
    if (store.eof) throw new Error('User not found')

    const userID = store.get('ID')
    const oldPwd = store.get('uPasswordHashHexa')
    isPwdChanged = me.changePassword(userID, forUser, newPwd, needChangePassword || false, oldPwd, skipOnMatch, newerExpire)
  } catch (e) {
    failException = e
  }

  // make uba_audit record
  if (isPwdChanged && App.domainInfo.has('uba_audit')) {
    store.run('insert', {
      entity: 'uba_audit',
      execParams: {
        entity: 'uba_user',
        entityinfo_id: userID,
        actionType: failException ? 'SECURITY_VIOLATION' : 'UPDATE',
        actionUser: Session.uData.login,
        actionTime: new Date(),
        remoteIP: Session.callerIP,
        targetUser: forUser.toLowerCase(),
        toValue: failException
          ? JSON.stringify({ action: 'changePassword', reason: failException.message })
          : JSON.stringify({ action: 'changePassword' })
      }
    })
    App.dbCommit()
  }
  if (failException) throw failException
}
me.changeOtherUserPassword = changeOtherUserPassword

/**
 * Change uba_user.uData JSON key to value
 * @param {string} key
 * @param {*} value
 */
function internalSetUDataKey (key, value) {
  const userID = Session.userID
  const user = UB.Repository('uba_user').attrs(['name', 'uData', 'mi_modifyDate']).where('ID', '=', userID).select()
  if (user.eof) {
    throw new Error('user is unknown or not logged in')
  }
  let newUData
  try {
    newUData = JSON.parse(user.get('uData'))
  } catch (e) {
    newUData = {}
  }
  if (!newUData) {
    newUData = {}
  }
  newUData[key] = value
  user.run('update', {
    execParams: {
      ID: userID,
      uData: JSON.stringify(newUData),
      mi_modifyDate: user.get('mi_modifyDate')
    }
  })
}

/**
 * Change (or set) current user language.
 * After call to this method UI must log out user and reload itself.
 *
 * @param {ubMethodParams} ctxt
 * @param {string} ctxt.mParams.newLang new user language
 * @memberOf uba_user_ns.prototype
 * @memberOfModule @unitybase/uba
 * @published
 */
function changeLanguage (ctxt) {
  const params = ctxt.mParams
  const newLang = params.newLang

  if (!newLang) {
    throw new Error('newLang parameter is required')
  }

  const supportedLangs = uba_user.entity.connectionConfig.supportLang
  if (supportedLangs.indexOf(newLang) < 0) {
    throw new Error(`Language "${newLang}" not supported`)
  }
  internalSetUDataKey('lang', newLang)
}
me.changeLanguage = changeLanguage

/**
 * Set key value inside `uba_user.uData` and store new JSON do DB.
 * All other uData JSON keys will remain unchanged.
 *
 * **WARNING** - overall length of uba_user.uData is 2000 characters, so only short values should be stored there
 *
 * @param {ubMethodParams} ctxt
 * @param {string} ctxt.mParams.key key to change
 * @param {string} ctxt.mParams.value new value
 * @memberOf uba_user_ns.prototype
 * @memberOfModule @unitybase/uba
 * @published
 */
function setUDataKey (ctxt) {
  const params = ctxt.mParams
  const key = params.key
  const value = params.value
  if (!key) throw new Error('key parameter is required')
  if (value === undefined) throw new Error('value parameter is required')

  internalSetUDataKey(key, value)
}
me.setUDataKey = setUDataKey

/**
 * After inserting new user - log event to uba_audit
 * @private
 * @param {ubMethodParams} ctx
 */
function ubaAuditNewUser (ctx) {
  if (!App.domainInfo.has('uba_audit')) return

  const params = ctx.mParams.execParams
  const store = UB.DataStore('uba_audit')
  store.run('insert', {
    execParams: {
      entity: 'uba_user',
      entityinfo_id: params.ID, // Session.userID,
      actionType: 'INSERT',
      actionUser: Session.uData.login,
      actionTime: new Date(),
      remoteIP: Session.callerIP,
      targetUser: params.name,
      toValue: JSON.stringify(params)
    }
  })
}

/**
 * After updating user - log event to uba_audit.
 * Logout a user if disabled is sets to 1
 * @private
 * @param {ubMethodParams} ctx
 */
function ubaAuditModifyUser (ctx) {
  const params = ctx.mParams.execParams
  if (params.disabled) {
    App.removeUserSessions(params.ID)
  }

  const store = UB.DataStore('uba_audit')
  const origStore = ctx.dataStore
  const origName = origStore.currentDataName
  let oldValues, oldName

  try {
    origStore.currentDataName = 'selectBeforeUpdate'
    oldValues = origStore.getAsTextInObjectNotation()
    oldName = origStore.get('name')
  } finally {
    origStore.currentDataName = origName
  }

  if (params.name) {
    store.run('insert', {
      execParams: {
        entity: 'uba_user',
        entityinfo_id: params.ID,
        actionType: 'DELETE',
        actionUser: Session.uData.login,
        actionTime: new Date(),
        remoteIP: Session.callerIP,
        targetUser: oldName,
        fromValue: oldValues,
        toValue: JSON.stringify(params)
      }
    })
    store.run('insert', {
      execParams: {
        entity: 'uba_user',
        entityinfo_id: params.ID,
        actionType: 'INSERT',
        actionUser: Session.uData.login,
        actionTime: new Date(),
        remoteIP: Session.callerIP,
        targetUser: params.name,
        fromValue: oldValues,
        toValue: JSON.stringify(params)
      }
    })
  } else {
    store.run('insert', {
      execParams: {
        entity: 'uba_user',
        entityinfo_id: params.ID,
        actionType: 'UPDATE',
        actionUser: Session.uData.login,
        actionTime: new Date(),
        remoteIP: Session.callerIP,
        targetUser: oldName,
        fromValue: oldValues,
        toValue: JSON.stringify(params)
      }
    })
  }
}

/**
 * After deleting user - log event to uba_audit
 * @private
 * @param {ubMethodParams} ctx
 */
function ubaAuditDeleteUser (ctx) {
  const params = ctx.mParams.execParams
  const store = UB.DataStore('uba_audit')
  const origStore = ctx.dataStore
  const origName = origStore.currentDataName
  let oldValues, oldName

  try {
    origStore.currentDataName = 'selectBeforeDelete'
    oldValues = origStore.getAsTextInObjectNotation()
    oldName = origStore.get('name')
  } finally {
    origStore.currentDataName = origName
  }

  store.run('insert', {
    execParams: {
      entity: 'uba_user',
      entityinfo_id: params.ID,
      actionType: 'DELETE',
      actionUser: Session.uData.login,
      actionTime: new Date(),
      remoteIP: Session.callerIP,
      targetUser: oldName,
      fromValue: oldValues
    }
  })
}

/**
 * Check if the user is a built-in user
 * @private
 * @param {string} userName
 * @returns {boolean}
 */
function isBuiltInUser (userName) {
  return Object.values(UBA_COMMON.USERS).some(r => r.NAME === userName)
}

/**
 * Check if the user is a service user defined by the security.disabledAccounts
 * configuration setting.
 * @private
 * @param {string} userName
 * @returns {boolean}
 */
function isDisabledUser (userName) {
  const disabledAccountsSetting = App.serverConfig.security.disabledAccounts
  return disabledAccountsSetting && new RegExp(disabledAccountsSetting).test(userName)
}

/**
 * Prevent deletion a build-in user
 * @private
 * @param {ubMethodParams} ctx
 */
function denyBuildInUserDeletion (ctx) {
  // Get name from "selectBeforeDelete" store
  const userName = ctx.dataStore.get('name')

  if (isBuiltInUser(userName)) {
    throw new UB.UBAbort(UB.i18n('<<<uba_user.deleteBuiltInUserProhibited>>>'))
  }

  if (isDisabledUser(userName)) {
    throw new UB.UBAbort(UB.i18n('<<<uba_user.deleteServiceUserProhibited>>>'))
  }
}

/**
 * Prevent renaming of a build-in user
 * @private
 * @param {ubMethodParams} ctx
 */
function denyBuildInUserRename (ctx) {
  if (ctx.mParams.execParams.name === undefined) {
    // Request does not contain "name", so name won't be changed
    return
  }

  // Get name from "selectBeforeDelete" store
  const userName = ctx.dataStore.get('name')

  if (userName === ctx.mParams.execParams.name) {
    // Username in execParams matches username in DB, no change
    return
  }

  if (isBuiltInUser(userName)) {
    throw new UB.UBAbort(UB.i18n('<<<uba_user.renameBuiltInUserProhibited>>>'))
  }

  if (isDisabledUser(userName)) {
    throw new UB.UBAbort(UB.i18n('<<<uba_user.renameServiceUserProhibited>>>'))
  }
}

/**
 * Rest endpoint what returns a `userData` - information about logged-in user.
 * For clients what uses an UBConnection userData is already available and can be obtained using `connection.userData()` function.
 * This endpoint is mostly for third-party integration.
 *
 * @example
GET /rest/uba_user/getUserData
 * @param {*} fake
 * @param {THTTPRequest} req
 * @param {THTTPResponse} resp
 * @function getUserData
 * @memberOf uba_user_ns.prototype
 * @memberOfModule @unitybase/uba
 * @published
 */
function getUserData (fake, req, resp) {
  resp.writeEnd(Session.uData)
  resp.statusCode = 200
}
me.getUserData = getUserData