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, Types } from 'mongoose';
import { LoginDto } from '../auth/login-dto';
import { CreateUserDto } from './dto/create-user.dto';
import { User } from './schemas/user.schema';
import { MailerService } from '../mailer/mailer.service';
import { IUser } from './interfaces/user.interface';
import { EmailChangeDto } from './dto/change-email.dto';
import { PendingStructureDto } from '../admin/dto/pending-structure.dto';
import { OwnerDto } from './dto/owner.dto';

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

  /**
   * Create a user account
   * @param createUserDto CreateUserDto
   */
  public async create(createUserDto: CreateUserDto): Promise<User | HttpStatus> {
    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.structuresLink = [];
    if (createUserDto.structuresLink) {
      createUserDto.structuresLink.forEach((structureId) => {
        createUser.structuresLink.push(Types.ObjectId(structureId));
      });
    }
    // createUser.email = createUserDto.email;
    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,})'); //NOSONAR
    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 findAll(): Promise<User[]> {
    return await this.userModel.find().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();
  }

  public async removeStructureIdFromUsers(structureId: Types.ObjectId): Promise<IUser[] | undefined> {
    return this.userModel
      .updateMany({ structuresLink: { $in: [structureId] } }, { $pull: { structuresLink: structureId } })
      .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;
  }

  /**
   * Send to all admins mail for aptic duplicated data
   */
  public async sendAdminApticStructureMail(structureName: string, duplicatedStructureName: string): Promise<any> {
    const config = this.mailerService.config;
    const ejsPath = this.mailerService.getTemplateLocation(config.templates.apticStructureDuplication.ejs);
    const jsonConfig = this.mailerService.loadJsonConfig(config.templates.apticStructureDuplication.json);

    const html = await ejs.renderFile(ejsPath, {
      config,
      name: structureName,
      duplicatedStructureName: duplicatedStructureName,
    });
    const admins = await this.getAdmins();
    admins.forEach((admin) => {
      this.mailerService.send(admin.email, jsonConfig.subject, html);
    });
  }

  /**
   * Send approval email for user
   * a new account.
   * @param user User
   */
  private async sendStructureClaimApproval(userEmail: string, structureName: string, status: boolean): Promise<any> {
    const config = this.mailerService.config;
    const ejsPath = this.mailerService.getTemplateLocation(config.templates.structureClaimValidation.ejs);
    const jsonConfig = this.mailerService.loadJsonConfig(config.templates.structureClaimValidation.json);

    const html = await ejs.renderFile(ejsPath, {
      config,
      status: status ? 'accepté' : 'refusée',
      name: structureName,
    });
    this.mailerService.send(userEmail, jsonConfig.subject, html);
  }

  /**
   * 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<User | HttpException> {
    const user = await this.findById(userId);
    if (user && user.validationToken === token) {
      user.validationToken = null;
      user.emailVerified = true;
      user.save();
      return user;
    } else {
      throw new HttpException('Invalid token', HttpStatus.UNAUTHORIZED);
    }
  }

  public async changeUserEmail(emailDto: EmailChangeDto): Promise<any> {
    const user = await this.findOne(emailDto.oldEmail);
    const alreadyUsed = await this.findOne(emailDto.newEmail);
    if (user) {
      if (!alreadyUsed) {
        const config = this.mailerService.config;
        const ejsPath = this.mailerService.getTemplateLocation(config.templates.changeEmail.ejs);
        const jsonConfig = this.mailerService.loadJsonConfig(config.templates.changeEmail.json);
        const token = crypto.randomBytes(64).toString('hex');
        const html = await ejs.renderFile(ejsPath, {
          config,
          token: token,
        });
        this.mailerService.send(user.email, jsonConfig.subject, html);
        user.changeEmailToken = token;
        user.newEmail = emailDto.newEmail;
        user.save();
        return user;
      }
      throw new HttpException('Email already used', HttpStatus.NOT_ACCEPTABLE);
    }
    throw new HttpException('Email sent if account exist', HttpStatus.UNAUTHORIZED);
  }

  public async verifyAndUpdateUserEmail(token: string): Promise<any> {
    const user = await this.userModel.findOne({ changeEmailToken: token }).exec();
    if (user) {
      user.email = user.newEmail;
      user.newEmail = null;
      user.changeEmailToken = null;
      user.save();
      return user;
    } 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();
  }

  /**
   * Send reset password email based on ejs template
   * @param email string
   */
  public async sendResetPasswordEmail(email: string): Promise<HttpException> {
    const user = await this.findOne(email);
    if (user) {
      const config = this.mailerService.config;
      const ejsPath = this.mailerService.getTemplateLocation(config.templates.resetPassword.ejs);
      const jsonConfig = this.mailerService.loadJsonConfig(config.templates.resetPassword.json);

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

      // Save token
      user.resetPasswordToken = token;
      user.save();
    }
    throw new HttpException('Email sent if account exist', HttpStatus.OK);
  }

  /**
   * Change password with the given token and password
   * Token existence and password strength are verified
   * @param password string
   * @param token string
   */
  public async validatePasswordResetToken(password: string, token: string): Promise<HttpException> {
    const user = await this.userModel.findOne({ resetPasswordToken: token }).exec();
    if (user) {
      if (!this.isStrongPassword(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
        );
      }
      user.password = await this.hashPassword(password);
      user.resetPasswordToken = null;
      user.save();
      throw new HttpException('Password Reset', HttpStatus.OK);
    }
    throw new HttpException('Invalid token', HttpStatus.UNAUTHORIZED);
  }

  public async getAdmins(): Promise<User[]> {
    return this.userModel.find({ role: 1 }).exec();
  }

  public async isStructureClaimed(structureId: string): Promise<IUser> {
    return this.userModel.findOne({ structuresLink: Types.ObjectId(structureId) }).exec();
  }

  public getStructureOwners(structureId: string): Promise<IUser[]> {
    return this.userModel.find({ structuresLink: Types.ObjectId(structureId) }).exec();
  }

  public async isUserAlreadyClaimedStructure(structureId: string, userEmail: string): Promise<boolean> {
    const user = await this.findOne(userEmail, true);
    if (user) {
      return user.pendingStructuresLink.includes(Types.ObjectId(structureId));
    }
    return false;
  }

  /**
   * Send to all admins validation email for structures
   * new account.
   */
  private async sendAdminStructureValidationMail(): Promise<any> {
    const config = this.mailerService.config;
    const ejsPath = this.mailerService.getTemplateLocation(config.templates.adminStructureClaim.ejs);
    const jsonConfig = this.mailerService.loadJsonConfig(config.templates.adminStructureClaim.json);

    const html = await ejs.renderFile(ejsPath, {
      config,
    });
    const admins = await this.getAdmins();
    admins.forEach((admin) => {
      this.mailerService.send(admin.email, jsonConfig.subject, html);
    });
  }

  public async updateStructureLinkedClaim(userEmail: string, idStructure: string): Promise<Types.ObjectId[]> {
    const stucturesLinked = this.updatePendingStructureLinked(userEmail, idStructure);
    this.sendAdminStructureValidationMail();

    return stucturesLinked;
  }

  public async updatePendingStructureLinked(userEmail: string, idStructure: string): Promise<Types.ObjectId[]> {
    const user = await this.findOne(userEmail, true);
    if (user) {
      if (!user.pendingStructuresLink.includes(Types.ObjectId(idStructure))) {
        user.pendingStructuresLink.push(Types.ObjectId(idStructure));
        await user.save();
        return user.pendingStructuresLink;
      }
      throw new HttpException('User already claimed this structure', HttpStatus.NOT_FOUND);
    }
    throw new HttpException('Invalid user', HttpStatus.NOT_FOUND);
  }

  public async removeFromPendingStructureLinked(userEmail: string, idStructure: string): Promise<Types.ObjectId[]> {
    const user = await this.findOne(userEmail, true);
    if (user) {
      if (user.pendingStructuresLink.includes(Types.ObjectId(idStructure))) {
        user.pendingStructuresLink = user.pendingStructuresLink.filter((structureId) => {
          return structureId === Types.ObjectId(idStructure);
        });
        await user.save();
        return user.pendingStructuresLink;
      }
      throw new HttpException('User already belong to this structure', HttpStatus.NOT_FOUND);
    }
    throw new HttpException('Invalid user', HttpStatus.NOT_FOUND);
  }

  public async updateStructureLinked(userEmail: string, idStructure: string): Promise<Types.ObjectId[]> {
    const user = await this.findOne(userEmail, true);
    if (user) {
      if (!user.structuresLink.includes(Types.ObjectId(idStructure))) {
        user.structuresLink.push(Types.ObjectId(idStructure));
        await user.save();
        return user.structuresLink;
      }
      throw new HttpException('User already belong to this structure', HttpStatus.NOT_FOUND);
    }
    throw new HttpException('Invalid user', HttpStatus.NOT_FOUND);
  }

  public async removeFromStructureLinked(userEmail: string, idStructure: string): Promise<Types.ObjectId[]> {
    const user = await this.findOne(userEmail, true);
    if (user) {
      if (user.structuresLink.includes(Types.ObjectId(idStructure))) {
        user.structuresLink = user.structuresLink.filter((structureId) => {
          return structureId == Types.ObjectId(idStructure);
        });
        await user.save();
        return user.structuresLink;
      }
      throw new HttpException('User already belong to this structure', HttpStatus.NOT_FOUND);
    }
    throw new HttpException('Invalid user', HttpStatus.NOT_FOUND);
  }

  /**
   * Return all pending attachments of all profiles
   */
  public async getPendingStructures(): Promise<PendingStructureDto[]> {
    const users = await this.userModel.find();
    const structuresPending = [];

    // For each user, if they have structures in pending, push them in tab and return this tab.
    users.forEach((user) => {
      if (user.pendingStructuresLink.length) {
        user.pendingStructuresLink.forEach((structureId) => {
          structuresPending.push({ userEmail: user.email, structureId: structureId });
        });
      }
    });
    return structuresPending;
  }

  /**
   * Validate or refuse a pending structure given a email and structure id
   */
  public async validatePendingStructure(
    userEmail: string,
    structureId: string,
    structureName: string,
    validate: boolean
  ): Promise<PendingStructureDto[]> {
    const user = await this.findOne(userEmail);
    // Get other users who have made the demand on the same structure
    const otherUsers = await this.userModel
      .find({ pendingStructuresLink: Types.ObjectId(structureId), email: { $ne: userEmail } })
      .exec();

    let status = false;
    if (!user) {
      throw new HttpException('User not found', HttpStatus.NOT_FOUND);
    }
    if (user.pendingStructuresLink.includes(Types.ObjectId(structureId))) {
      user.pendingStructuresLink = user.pendingStructuresLink.filter((item) => {
        return !Types.ObjectId(structureId).equals(item);
      });
      // If it's a validation case, push structureId into validated user structures
      if (validate) {
        user.structuresLink.push(Types.ObjectId(structureId));
        // Send validation email
        status = true;
        // For other users who have made the demand on the same structure
        if (otherUsers) {
          otherUsers.forEach((otherUser) => {
            // Remove the structure id from their demand
            otherUser.pendingStructuresLink = otherUser.pendingStructuresLink.filter((item) => {
              return !Types.ObjectId(structureId).equals(item);
            });
            // Send a rejection email
            this.sendStructureClaimApproval(otherUser.email, structureName, false);
            otherUser.save();
          });
        }
      }
      this.sendStructureClaimApproval(userEmail, structureName, status);
      await user.save();
      return this.getPendingStructures();
    } else {
      throw new HttpException(
        'Cannot validate strucutre. It might have been already validate, or the structure does`nt belong to the user',
        HttpStatus.NOT_FOUND
      );
    }
  }

  public async removeOutdatedStructureFromArray(structureId: string): Promise<void> {
    const users = await this.userModel.find({ structureOutdatedMailSent: Types.ObjectId(structureId) }).exec();
    users.forEach((user) => {
      user.structureOutdatedMailSent = user.structureOutdatedMailSent.filter((item) =>
        Types.ObjectId(structureId).equals(item)
      );
      user.save();
    });
  }

  public async verifyUserExist(email: string): Promise<boolean> {
    const user = await this.findOne(email);
    return user ? true : false;
  }

  public async deleteOne(email: string): Promise<User> {
    const user = await this.findOne(email);
    if (!user) {
      throw new HttpException('Invalid user email', HttpStatus.BAD_REQUEST);
    }
    return user.deleteOne();
  }

  public async deleteOneId(id: string): Promise<User> {
    const user = await this.userModel.findOne({ _id: id }).exec();
    if (!user) {
      throw new HttpException('Invalid user id', HttpStatus.BAD_REQUEST);
    }
    return user.deleteOne();
  }

  public async getStructureOwnersMails(structureId: string, emailUser: string): Promise<OwnerDto[]> {
    const users = await this.userModel
      .find({ structuresLink: Types.ObjectId(structureId), email: { $ne: emailUser } })
      .exec();
    const owners: OwnerDto[] = [];
    users.forEach((user) => {
      const userProfile = new OwnerDto();
      userProfile.email = user.email;
      userProfile.id = user._id;
      owners.push(userProfile);
    });
    return owners;
  }

  public async searchUsers(searchString: string) {
    return this.userModel.find({ email: new RegExp(searchString, 'i') }).exec();
  }
}