import * as bcrypt from 'bcryptjs';
import { DocumentQuery } from 'mongoose';
import { UserProfile } from 'shared';

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';

import { IUserProfileDbModel, UserProfileDbModel } from '../database/models/user-profile.db.model';

/**
 * Responsible of authenticating users, it uses JWT for the authentication process.
 */
@Injectable()
export class AuthService {
  constructor(private jwtService: JwtService) {}

  /**
   * Checks if the provided username and password valid, if so, returns the user match. If not, returns null.
   * @param email
   * @param password
   */
  authenticate(email: string, password: string): Promise<IUserProfileDbModel> {
    return UserProfileDbModel.findOne({ email }).then(user => {
      // If this user does not exist, throw the same error for security reasons
      if (!user)
        throw new UnauthorizedException('Email or password are invalid!');

      return bcrypt.compare(password, user.password).then(match => {
        // The password do not match the one saved on the database
        if (!match)
          throw new UnauthorizedException('Email or password are invalid!');
        return user;
      });
    });
  }

  getUserFromDB(
    email: string,
  ): DocumentQuery<IUserProfileDbModel, IUserProfileDbModel, unknown> {
    return UserProfileDbModel.findOne({ email });
  }

  getUserFromToken(
    token: string,
  ): DocumentQuery<IUserProfileDbModel, IUserProfileDbModel, unknown> {
    // Decode the token
    const decodedUser = this.jwtService.verify(token) as IUserProfileDbModel;

    if (decodedUser) {
      // If the user has been decoded successfully, check it against the database
      return UserProfileDbModel.findById(decodedUser._id);
    }
  }

  userHasRoles(user: UserProfile, ...roles: string[]): boolean {
    // If the user don't have this amount of roles, don't pass
    if (user.roles.length < roles.length) return false;

    // Go through each role, and check if the user has it, if not, return false
    for (const role of roles) if (!user.roles.includes(role)) return false;

    return true;
  }

  /**
   * Generates a JWT token with the specified user data.
   * @param user
   */
  generateToken(user: UserProfile): string {
    return this.jwtService.sign(user);
  }

  /**
   * Decodes a JWT token and returns the user found.
   * @param token
   */
  decodeToken(token: string): UserProfile {
    return this.jwtService.verify(token) as UserProfile;
  }
}