// email.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0

'use strict';

const nodemailer = require('nodemailer');
const uuidv4 = require('uuid').v4;

const mongoose = require('mongoose');

const EmailBlacklist = mongoose.model('EmailBlacklist');
const EmailVerify = mongoose.model('EmailVerify');
const EmailLog = mongoose.model('EmailLog');

const disposableEmailDomains = require('disposable-email-provider-domains');
const emailValidator = require('email-validator');
const emailDomainCheck = require('email-domain-check');

const { SiteService, SiteError } = require('../../lib/site-lib');

class EmailService extends SiteService {

  constructor (dtp) {
    super(dtp, module.exports);
  }

  async start ( ) {
    await super.start();

    if (process.env.DTP_EMAIL_SERVICE !== 'enabled') {
      this.log.info("DTP_EMAIL_SERVICE is disabled, the system can't send email and will not try.");
      return;
    }

    const SMTP_PORT = parseInt(process.env.DTP_EMAIL_SMTP_PORT || '587', 10);
    this.log.info('creating SMTP transport', {
      host: process.env.DTP_EMAIL_SMTP_HOST,
      port: SMTP_PORT,
    });
    this.transport = nodemailer.createTransport({
      host: process.env.DTP_EMAIL_SMTP_HOST,
      port: SMTP_PORT,
      secure: process.env.DTP_EMAIL_SMTP_SECURE === 'enabled',
      auth: {
        user: process.env.DTP_EMAIL_SMTP_USER,
        pass: process.env.DTP_EMAIL_SMTP_PASS,
      },
      pool: true,
      maxConnections: parseInt(process.env.DTP_EMAIL_SMTP_POOL_MAX_CONN || '5', 10),
      maxMessages: parseInt(process.env.DTP_EMAIL_SMTP_POOL_MAX_MSGS || '5', 10),
    });

    this.templates = {
      html: {
        userEmail: this.loadAppTemplate('html', 'user-email.pug'),
        welcome: this.loadAppTemplate('html', 'welcome.pug'),
      },
      text: {
        userEmail: this.loadAppTemplate('text', 'user-email.pug'),
        welcome: this.loadAppTemplate('text', 'welcome.pug'),
      },
    };
  }

  async renderTemplate (templateId, templateType, templateModel) {
    this.log.debug('rendering email template', { templateId, templateType });
    return this.templates[templateType][templateId](templateModel);
  }

  async send (message) {
    const NOW = new Date();

    await this.checkEmailAddress(message.to);

    this.log.info('sending email', { to: message.to, subject: message.subject });
    const response = await this.transport.sendMail(message);

    const log = await EmailLog.create({
      created: NOW,
      from: message.from,
      to: message.to,
      to_lc: message.to.toLowerCase(),
      subject: message.subject,
      messageId: response.messageId,
    });

    return { response, log };
  }

  async checkEmailAddress (emailAddress) {
    this.log.debug('validating email address', { emailAddress });
    if (!emailValidator.validate(emailAddress)) {
      throw new Error('Email address is invalid');
    }

    const domainCheck = await emailDomainCheck(emailAddress);
    this.log.debug('email domain check', { domainCheck });
    if (!domainCheck) {
      throw new Error('Email address is invalid');
    }

    await this.isEmailBlacklisted(emailAddress);
  }

  async isEmailBlacklisted (emailAddress) {
    emailAddress = emailAddress.toLowerCase().trim();

    const domain = emailAddress.split('@')[1];
    this.log.debug('checking email domain for blacklist', { domain });
    if (disposableEmailDomains.domains.includes(domain)) {
      this.log.alert('blacklisted email domain blocked', { emailAddress, domain });
      throw new Error('Invalid email address');
    }

    const blacklistRecord = await EmailBlacklist.findOne({ email: emailAddress });
    if (blacklistRecord) {
      throw new Error('Email address has requested to not receive emails', { blacklistRecord });
    }

    return false;
  }

  async createVerificationToken (user) {
    const NOW = new Date();
    const verify = new EmailVerify();

    verify.created = NOW;
    verify.user = user._id;
    verify.token = uuidv4();

    await verify.save();

    this.log.info('created email verification token for user', { user: user._id });

    return verify.toObject();
  }

  async verifyToken (token) {
    const NOW = new Date();
    const { user: userService } = this.dtp.services;

    // fetch the token from the db
    const emailVerify = await EmailVerify
      .findOne({ token: token })
      .populate(this.populateEmailVerify)
      .lean();

    // verify that the token is at least valid (it exists)
    if (!emailVerify) {
      this.log.error('email verify token not found', { token });
      throw new SiteError(403, 'Email verification token is invalid');
    }

    // verify that it hasn't already been verified (user clicked link more than once)
    if (emailVerify.verified) {
      this.log.error('email verify token already claimed', { token });
      throw new SiteError(403, 'Email verification token is invalid');
    }

    this.log.info('marking user email verified', { userId: emailVerify.user._id });
    await userService.setEmailVerification(emailVerify.user, true);

    await EmailVerify.updateOne({ _id: emailVerify._id }, { $set: { verified: NOW } });
  }

  async removeVerificationTokensForUser (user) {
    this.log.info('removing all pending email address verification tokens for user', { user: user._id });
    await EmailVerify.deleteMany({ user: user._id });
  }
}

module.exports = {
  slug: 'email',
  name: 'email',
  create: (dtp) => { return new EmailService(dtp); },
};