const db = require('../db/models');
const UsersDBApi = require('../db/api/users');
const ValidationError = require('./notifications/errors/validation');
const ForbiddenError = require('./notifications/errors/forbidden');
const bcrypt = require('bcrypt');
const EmailAddressVerificationEmail = require('./email/list/addressVerification');
const InvitationEmail = require('./email/list/invitation');
const PasswordResetEmail = require('./email/list/passwordReset');
const EmailSender = require('./email');
const config = require('../config');
const helpers = require('../helpers');

class Auth {
  static async signup(email, password, options = {}, host) {
    const user = await UsersDBApi.findBy({ email });

    const hashedPassword = await bcrypt.hash(
      password,
      config.bcrypt.saltRounds,
    );

    if (user) {
      if (user.authenticationUid) {
        throw new ValidationError('auth.emailAlreadyInUse');
      }

      if (user.disabled) {
        throw new ValidationError('auth.userDisabled');
      }

      await UsersDBApi.updatePassword(user.id, hashedPassword, options);

      if (EmailSender.isConfigured) {
        await this.sendEmailAddressVerificationEmail(user.email, host);
      }

      const data = {
        user: {
          id: user.id,
          email: user.email,
        },
      };

      return helpers.jwtSign(data);
    }

    const newUser = await UsersDBApi.createFromAuth(
      {
        firstName: email.split('@')[0],
        password: hashedPassword,
        email: email,
      },
      options,
    );

    if (EmailSender.isConfigured) {
      await this.sendEmailAddressVerificationEmail(newUser.email, host);
    }

    const data = {
      user: {
        id: newUser.id,
        email: newUser.email,
      },
    };

    return helpers.jwtSign(data);
  }

  static async signin(email, password) {
    const user = await UsersDBApi.findBy({ email });

    if (!user) {
      throw new ValidationError('auth.userNotFound');
    }

    if (user.disabled) {
      throw new ValidationError('auth.userDisabled');
    }

    if (!user.password) {
      throw new ValidationError('auth.wrongPassword');
    }

    if (!EmailSender.isConfigured) {
      user.emailVerified = true;
    }

    if (!user.emailVerified) {
      throw new ValidationError('auth.userNotVerified');
    }

    const passwordsMatch = await bcrypt.compare(password, user.password);

    if (!passwordsMatch) {
      throw new ValidationError('auth.wrongPassword');
    }

    const data = {
      user: {
        id: user.id,
        email: user.email,
      },
    };

    return helpers.jwtSign(data);
  }

  static async sendEmailAddressVerificationEmail(email, host) {
    let link;
    try {
      const token = await UsersDBApi.generateEmailVerificationToken(email);
      link = `${host}/verify-email?token=${token}`;
    } catch (error) {
      console.error(error);
      throw new ValidationError('auth.emailAddressVerificationEmail.error');
    }

    const emailAddressVerificationEmail = new EmailAddressVerificationEmail(
      email,
      link,
    );

    return new EmailSender(emailAddressVerificationEmail).send();
  }

  static async sendPasswordResetEmail(email, type = 'register', host) {
    let link;

    try {
      const token = await UsersDBApi.generatePasswordResetToken(email);
      link = `${host}/password-reset?token=${token}`;
    } catch (error) {
      console.error(error);
      throw new ValidationError('auth.passwordReset.error');
    }

    let passwordResetEmail;
    if (type === 'register') {
      passwordResetEmail = new PasswordResetEmail(email, link);
    }
    if (type === 'invitation') {
      passwordResetEmail = new InvitationEmail(email, link);
    }

    return new EmailSender(passwordResetEmail).send();
  }

  static async verifyEmail(token, options = {}) {
    const user = await UsersDBApi.findByEmailVerificationToken(token, options);

    if (!user) {
      throw new ValidationError(
        'auth.emailAddressVerificationEmail.invalidToken',
      );
    }

    return UsersDBApi.markEmailVerified(user.id, options);
  }

  static async passwordUpdate(currentPassword, newPassword, options) {
    const currentUser = options.currentUser || null;
    if (!currentUser) {
      throw new ForbiddenError();
    }

    const currentPasswordMatch = await bcrypt.compare(
      currentPassword,
      currentUser.password,
    );

    if (!currentPasswordMatch) {
      throw new ValidationError('auth.wrongPassword');
    }

    const newPasswordMatch = await bcrypt.compare(
      newPassword,
      currentUser.password,
    );

    if (newPasswordMatch) {
      throw new ValidationError('auth.passwordUpdate.samePassword');
    }

    const hashedPassword = await bcrypt.hash(
      newPassword,
      config.bcrypt.saltRounds,
    );

    return UsersDBApi.updatePassword(currentUser.id, hashedPassword, options);
  }

  static async passwordReset(token, password, options = {}) {
    const user = await UsersDBApi.findByPasswordResetToken(token, options);

    if (!user) {
      throw new ValidationError('auth.passwordReset.invalidToken');
    }

    const hashedPassword = await bcrypt.hash(
      password,
      config.bcrypt.saltRounds,
    );

    return UsersDBApi.updatePassword(user.id, hashedPassword, options);
  }

  static async updateProfile(data, currentUser) {
    let transaction = await db.sequelize.transaction();

    try {
      await UsersDBApi.findBy({ id: currentUser.id }, { transaction });

      await UsersDBApi.update(currentUser.id, data, {
        currentUser,
        transaction,
      });

      await transaction.commit();
    } catch (error) {
      await transaction.rollback();
      throw error;
    }
  }
}

module.exports = Auth;
