import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { email as Email } from '@prisma/client';
import { TemplatePayloadDto } from './dto/template-payload.dto';
import { plainToInstance } from 'class-transformer';
import { RegistrationDataDto } from './dto/registration-data.dto';
import { resolve } from 'path';
import { encode } from 'html-entities';
import * as nodemailer from 'nodemailer';
import * as schedule from 'node-schedule';
import * as ejs from 'ejs';

@Injectable()
export class EmailService {
  private job: schedule.Job;
  private transporter: nodemailer.Transporter;

  constructor(
    private prisma: PrismaService,
  ) {
    const { SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS } = process.env;
    const options = {
      pool: true,
      host: SMTP_HOST,
      port: SMTP_PORT,
      secure: false, // upgrade later with STARTTLS
      tls: { ciphers: 'SSLv3' },
      auth: {
        user: SMTP_USER,
        pass: SMTP_PASS,
      },
    } as nodemailer.TransportOptions;
    this.transporter = nodemailer.createTransport(options);
    // Test the connection
    // this.verify();
  }

  // verify connection configuration
  async verify(): Promise<void> {
    return this.transporter.verify(function (error, success) {
      if (error) {
        console.log(new Date()+' - Email server failed verification test');
        console.log(error);
        process.exit(1);
      }
      console.log(new Date()+' - Email server is ready to take our messages');
    });
  }

  /**
   * Create a new email item, to be sent at a later point
   * @param object payload The name of the user
   */
  async create(payload: TemplatePayloadDto) {
    try {
      const email = await this.prisma.email.create({
        data: {
          created_date: new Date(),
          payload: payload as object,
          meta: {},
          processed: false
        }
      });
      return email
    } catch (err) {
      console.log(new Date()+' - Error in EmailService.create', err)
    }
  }

  /**
  * Retrieves all email items depending on their processed value
  * @param boolean processed Toggle results that have / have not been processed
  */
  async find(processed: boolean = false): Promise<Email[] | undefined> {
    try {
      const emails = await this.prisma.email.findMany({
          where: {
            processed
          },
          orderBy: {
            email_id: 'asc'
          }
      });
      return emails;
    } catch (err) {
      console.log(new Date()+' - Error in EmailService.find', err)
    }
  }

  /**
   * Updates an email item after it has been processing
   * @param number id The id of the email item
   * @param object meta The result from processing the item
   */
  async update(email_id: number, meta: object) {
    try {
      const email = await this.prisma.email.update({
        where: {
          email_id
        },
        data: {
          meta,
          processed: true
        },
      })
      return email
    } catch (err) {
      console.log(new Date()+' - Error in EmailService.update', err)
    }
  }

  startProcessing() {
    // schedule job to run at every 1 minute to process any emails waiting to be sent
    let timings;
    // 1 minute prod
    if (['staging', 'production'].includes(process.env.NODE_ENV)) timings = '*/1 * * * *';
    // 20 seconds dev
    else  timings = '5 * * * * *';
    // start processing records
    this.job = schedule.scheduleJob(timings, () => this.sendScheduled().catch((err) => console.log(err)));
  }

  stopProcessing() {
    // stop processing the scheduled job
    this.job.cancel();
  }

  registrationTitleReference(template: string): string {
    const titles = {
      registration: 'Welcome to IPW'
    }
    //@ts-ignore
    return titles[template];
  }

  encodeDataEntries(data: RegistrationDataDto) {
    const keys= Object.keys(data);
    for (let index = 0, n = keys.length; index < n; index++) {
      const key = keys[index];
      //@ts-ignore
      data[key] = encode(data[key]);
    }
    return data;
  }

  /**
  * Send the email via SMTP
  * @param object item The result from processing the item
  */
  async send(payload: TemplatePayloadDto) {
    try {
      // TODO should likely be encoding the data before adding it into the database?

      // send mail with defined transport object
      const { to, cc, bcc, template, data } = payload;
      // Get the template subject
      const subject = this.registrationTitleReference(template);
      // Encode the data values
      const locals = this.encodeDataEntries(data);
      // Generate the text content
      // TODO: find a way to copy these to the dist directory
      const text: string = await ejs.renderFile(await resolve(__dirname, 'template', `${template}.text.ejs`), locals);
      const html: string = await ejs.renderFile(resolve(__dirname, 'template', `${template}.html.ejs`), locals);
      // Build the mail options
      const sendmailPayload: nodemailer.SendMailOptions = {
        from: process.env.SMTP_FROM,
        to,
        subject,
        text,
        html
      };
      if (cc && cc.length > 0) sendmailPayload.cc = cc;
      if (bcc && bcc.length > 0) sendmailPayload.bcc = bcc;

      // Send the email
      const result = await this.transporter.sendMail(sendmailPayload);

      // success
      return result;
    } catch (err) {
      // handle errors
      console.log(new Date()+' - Error in EmailService.send', err)
      throw err;
    }
  }

  async toggleProcessing(processing: boolean) {
    await this.prisma.cronjob.update({
      where: { cronjob_id: 1 },
      data: { processing }
    });
  }

  async isProcessing() {
    const cronjob = await this.prisma.cronjob.findUnique({
      where: { cronjob_id: 1 },
      select: { processing: true }
    });
    return cronjob?.processing || false;
  }

  async sendScheduled(): Promise<void> {
    try {
      // get all unprocessed contact form submittions
      const formSubmissions = await this.find();
      if (!formSubmissions) return;
      // ensure that it is not already processing emails
      const isProcessing = await this.isProcessing();
      if (isProcessing) return;
      // get the count of emails to process
      const countOfFormSubmissions = formSubmissions.length;
      if (countOfFormSubmissions === 0) return;
      // start processing emails
      await this.toggleProcessing(true);
      const { NODE_ENV } = process.env;
      if (NODE_ENV === 'development') console.log(new Date()+' - processing sheduled emails now');
      // itterate over the emails
      if (NODE_ENV === 'development') console.log(`${new Date()} - found ${countOfFormSubmissions} emails to process`);
      for (let i = 0, n = countOfFormSubmissions; i < n; i++) {
        // access the submission values
        const submission = formSubmissions[i];
        if (!submission) continue;
        try {
          // a santity check
          // https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#reading-a-json-field
          if (
            !submission ||
            !submission.payload ||
            typeof submission?.payload !== 'object'
          ) continue;

          // cast object to class
          // https://stackoverflow.com/a/73949601/2110294
          // https://github.com/typestack/class-transformer
          const payload = plainToInstance(TemplatePayloadDto, submission.payload);
          const countOfPayloadKeys = Object.keys(payload).length;
          if (countOfPayloadKeys === 0) {
            await this.update(submission.email_id, { error: 'Failed to process the email as the payload is empty.' });
            return;
          }

          // send the mail
          const result = await this.send(payload);
          if (NODE_ENV === 'development') console.log(new Date()+' - result of the submission:', result);

          // update the record with the result
          await this.update(submission.email_id, { result });
        } catch (err) {
          await this.update(submission.email_id, { error: err });
        }
      }
      // processing is complete
      // await this.toggleProcessing(false);
    } catch (err) {
      console.log(new Date()+'Fatal error while sending emails')
      console.log(err);
      // processing is complete
      await this.toggleProcessing(false);
      // TODO: notify admin of the fatal error
      //await this.update(submission.email_id, { error: err });
      // Return to calling function
      throw err;
    }
  }
}
