import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import * as bcrypt from 'bcrypt';
import * as ejs from 'ejs';
import * as crypto from 'crypto';
import { Model } from 'mongoose';
import { LoginDto } from '../auth/login-dto';
import { CreateUserDto } from './create-user.dto';
import { User } from './user.schema';
import { MailerService } from '../mailer/mailer.service';
import { IUser } from './user.interface';

@Injectable()
export class UsersService {
  constructor(@InjectModel(User.name) private userModel: Model<IUser>, private mailerService: MailerService) {}

  /**
   * Create a user account
   * @param createUserDto CreateUserDto
   */
  public async create(createUserDto: CreateUserDto): Promise<User> {
    const userInDb = await this.findOne(createUserDto.email);
    if (userInDb) {
      throw new HttpException('User already exists', HttpStatus.BAD_REQUEST);
    }
    if (!this.isStrongPassword(createUserDto.password)) {
      throw new HttpException(
        'Weak password, it must contain ne lowercase alphabetical character, one uppercase alphabetical character, one numeric character, one special character and be eight characters or longer',
        HttpStatus.UNPROCESSABLE_ENTITY
      );
    }
    let createUser = new this.userModel(createUserDto);
    createUser.password = await this.hashPassword(createUser.password);
    // Send verification email
    createUser = await this.verifyUserMail(createUser);
    createUser.save();
    return await this.findOne(createUserDto.email);
  }

  /**
   * Verify password strenth with the following rule:
   * - The string must contain at least 1 lowercase alphabetical character
   * - The string must contain at least 1 uppercase alphabetical character
   * - The string must contain at least 1 numeric character
   * - The string must contain at least one special character, reserved RegEx characters are escaped to avoid conflict
   * - The string must be eight characters or longer
   * @param password string
   */
  private isStrongPassword(password: string): boolean {
    const strongRegex = new RegExp('^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,})');
    return strongRegex.test(password);
  }

  private async comparePassword(attempt: string, password: string): Promise<boolean> {
    return await bcrypt.compare(attempt, password);
  }

  private async hashPassword(password: string): Promise<string> {
    return await bcrypt.hash(password, process.env.SALT);
  }

  public async findOne(mail: string, passwordQuery?: boolean): Promise<IUser | undefined> {
    if (passwordQuery) {
      return this.userModel.findOne({ email: mail }).exec();
    }
    return this.userModel.findOne({ email: mail }).select('-password').exec();
  }

  public async findById(id: string, passwordQuery?: boolean): Promise<IUser | undefined> {
    if (passwordQuery) {
      return this.userModel.findById(id).exec();
    }
    return this.userModel.findById(id).select('-password').exec();
  }

  /**
   * Return a user after credential checking.
   * Use for login action
   * @param param LoginDto
   */
  public async findByLogin({ email, password }: LoginDto): Promise<User> {
    const user = await this.findOne(email, true);

    if (!user) {
      throw new HttpException('Invalid credentials', HttpStatus.UNAUTHORIZED);
    }

    // compare passwords
    const areEqual = await this.comparePassword(password, user.password);

    if (!areEqual) {
      throw new HttpException('Invalid credentials', HttpStatus.UNAUTHORIZED);
    }

    return user;
  }

  /**
   * Generate activation token and send it to user by email, in order to validate
   * a new account.
   * @param user User
   */
  private async verifyUserMail(user: IUser): Promise<any> {
    const config = this.mailerService.config;
    const ejsPath = this.mailerService.getTemplateLocation(config.templates.verify.ejs);
    const jsonConfig = this.mailerService.loadJsonConfig(config.templates.verify.json);

    const token = crypto.randomBytes(64).toString('hex');
    const html = await ejs.renderFile(ejsPath, {
      config,
      token: token,
      userId: user._id,
    });
    this.mailerService.send(user.email, jsonConfig.subject, html);

    // Save token
    user.validationToken = token;
    return user;
  }

  /**
   * Check that the given token is associated to userId. If it's true, validate user account.
   * @param userId string
   * @param token string
   */
  public async validateUser(userId: string, token: string): Promise<any> {
    const user = await this.findById(userId);
    if (user && user.validationToken === token) {
      user.validationToken = null;
      user.emailVerified = true;
      user.save();
    } else {
      throw new HttpException('Invalid token', HttpStatus.UNAUTHORIZED);
    }
  }

  public async changeUserPassword(userId: string, oldPassword: string, newPassword: string): Promise<any> {
    const user = await this.findById(userId, true);
    const arePasswordEqual = await this.comparePassword(oldPassword, user.password);
    if (!arePasswordEqual) {
      throw new HttpException('Invalid credentials', HttpStatus.UNAUTHORIZED);
    }
    if (!this.isStrongPassword(newPassword)) {
      throw new HttpException(
        'Weak password, it must contain ne lowercase alphabetical character, one uppercase alphabetical character, one numeric character, one special character and be eight characters or longer',
        HttpStatus.UNPROCESSABLE_ENTITY
      );
    }
    user.password = await this.hashPassword(newPassword);
    user.save();
  }
}