// Utilities for working with passwords
import crypto from 'crypto';
import bcrypt from 'bcrypt';
import moment from 'moment';
import { decrypt } from './encryption';
import { model as User } from '../models/user';

const BCRYPT_SALT_ROUNDS = 10;

// Hash a plain text password
export function bcryptHash (passwordToHash) {
  return bcrypt.hash(passwordToHash, BCRYPT_SALT_ROUNDS); // returns a promise
}

// Check if a plain text password matches a hash
export function bcryptCompare (passwordToCheck, hashedPassword) {
  return bcrypt.compare(passwordToCheck, hashedPassword); // returns a promise
}

// Return the encrypted version of a password (using sha1) given a salt
// Used for legacy passwords that have not yet been migrated to bcrypt
export function sha1Encrypt (password, salt) {
  return crypto
    .createHmac('sha1', salt)
    .update(password)
    .digest('hex');
}

// Create a salt, default length is 10
export function sha1MakeSalt (len = 10) {
  return crypto
    .randomBytes(Math.ceil(len / 2))
    .toString('hex')
    .substring(0, len);
}

// Compare the password for an user
// Works with bcrypt and sha1 independently
// An async function is used so that a promise is always returned
// even for comparing sha1 hashed passwords that use a sync method
export async function compare (user, passwordToCheck) {
  if (!user || !passwordToCheck) throw new Error('user and passwordToCheck are required parameters.');

  const { passwordHashMethod } = user.auth.local;
  const passwordHash = user.auth.local.hashed_password;
  const passwordSalt = user.auth.local.salt; // Only used for SHA1

  if (passwordHashMethod === 'bcrypt') {
    return bcryptCompare(passwordToCheck, passwordHash);
  // default to sha1 if the user has a salt but no passwordHashMethod
  } if (passwordHashMethod === 'sha1' || (!passwordHashMethod && passwordSalt)) {
    return passwordHash === sha1Encrypt(passwordToCheck, passwordSalt);
  }
  throw new Error('Invalid password hash method.');
}

// Convert an user to use bcrypt from sha1 for password hashing
// needs to save the user separately.
// NOTE: before calling this method it should be verified that the supplied plain text password
// is indeed hashed with sha1 and is valid
export async function convertToBcrypt (user, plainTextPassword) {
  if (!user || !plainTextPassword) throw new Error('user and plainTextPassword are required parameters.');

  user.auth.local.salt = undefined;
  user.auth.local.passwordHashMethod = 'bcrypt';
  user.auth.local.hashed_password = await bcryptHash(plainTextPassword); // eslint-disable-line camelcase, max-len
}

// Returns the user if a valid password reset code is supplied, otherwise false
export async function validatePasswordResetCodeAndFindUser (code) {
  let isCodeValid = true;

  let userId;
  let user;
  let decryptedPasswordResetCode;

  // wrapping the code in a try to be able to handle the error here
  try {
    decryptedPasswordResetCode = JSON.parse(decrypt(code || 'invalid')); // also catches missing code
    userId = decryptedPasswordResetCode.userId;
    const { expiresAt } = decryptedPasswordResetCode;

    if (moment(expiresAt).isBefore(moment())) throw new Error();
  } catch (err) {
    isCodeValid = false;
  }

  if (isCodeValid) {
    user = await User.findById(userId).exec();
    // check if user is found
    if (!user) {
      isCodeValid = false;
    } else if (code !== user.auth.local.passwordResetCode) {
      // Make sure only the last code can be used
      isCodeValid = false;
    }
  }

  return isCodeValid ? user : false;
}
