/*
 * Copyright (C) 2019 StApps
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation, version 3.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program. If not, see <https://www.gnu.org/licenses/>.
 */
import * as nodemailer from 'nodemailer';
import {MailOptions} from 'nodemailer/lib/sendmail-transport';
import {deleteUndefinedProperties, isProductiveEnvironment, RecursivePartial} from './common.js';
import {VerifiableTransport} from './transport.js';

/**
 * A configuration of the transport used to send mails via SMTP
 */
export interface SMTPConfig {
  /**
   * Auth configuration
   */
  auth: {
    /**
     * Password for login
     */
    password: string;
    /**
     * User for login
     */
    user: string;
  };
  /**
   * List of "carbon copy" recipients
   */
  cc?: string[];
  /**
   * SMTP host server
   */
  host: string;
  /**
   * SMTP port
   */
  port: number;
  /**
   * List of recipients
   */
  recipients: string[];
  /**
   * Whether or not to establish a secure connection
   */
  secure?: boolean;
  /**
   * Sender configuration
   */
  sender: {
    /**
     * Mail of sender
     */
    mail: string;
    /**
     * Name of sender
     */
    name?: string;
  };
}

/**
 * An implementation of mail transport via SMTP
 */
export class SMTP extends VerifiableTransport {
  /**
   * Singleton instance
   */
  private static _instance: SMTP;

  /**
   * List of all mail addresses to send in cc
   */
  private readonly cc: string[];

  /**
   * Who is using this service
   */
  private readonly from: {
    /**
     * Mail of sender
     */
    mail: string;
    /**
     * Name of sender
     */
    name?: string;
  };

  /**
   * List of all mail addresses to send to
   */
  private readonly recipients: string[];

  /**
   * Connection to SMTP server
   */
  private readonly transportAgent: nodemailer.Transporter;

  /**
   * Set to true if the transport was verified
   */
  private verified: boolean;

  /**
   * Get Singleton Instance of the SMTP Transport
   *
   * If no config is given it is assumed it will be given via environment variables
   * SMTP_AUTH_USER:      SMTP username
   * SMTP_AUTH_PASSWORD:  SMTP password
   * SMTP_HOST:           SMTP host
   * SMTP_PORT:           SMTP port
   * SMTP_RECIPIENTS:     comma seperated list of recipients
   * SMTP_CC:             comma seperated list of recipients for the carbon copy (CC)
   * SMTP_SENDER_MAIL:    sender of the mail
   * SMTP_SENDER_NAME:    name of the sender
   * SMTP_SECURE:         `true` to enable tls
   * @param config SMTP config for instance
   */
  public static getInstance(config?: SMTPConfig): SMTP | undefined {
    // if an instance of SMTP already exists
    if (SMTP._instance !== undefined) {
      return SMTP._instance;
    }

    // monitoring is not required -> SMTP init can fail
    if (!isProductiveEnvironment() || process.env.ALLOW_NO_TRANSPORT === 'true') {
      try {
        SMTP._instance = new SMTP(config);
      } catch {
        // eslint-disable-next-line no-console
        console.warn('SMTP config failed.');

        return;
      }
    } else {
      // monitoring is required -> SMTP will throw error if config is invalid
      SMTP._instance = new SMTP(config);
    }

    return SMTP._instance;
  }

  /**
   * This checks email addresses to be compatible with new standards. RFC 3490 introduced some features that are not
   * implemented by every mail transfer agent (MTA).
   *
   * For more information please consider reading
   * https://stackoverflow.com/a/2071250
   * @param address Address to validate
   */
  public static isValidEmailAddress(address: string): boolean {
    // tslint:disable-next-line:max-line-length
    return /^(([^<>()\[\].,;:\s@"]+(\.[^<>()\[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i.test(
      address,
    );
  }

  /**
   * Checks a list of mail addresses for validity
   * @param recipients List of recipients to check
   */
  public static isValidRecipientsList(recipients: string[] | undefined): boolean {
    return Array.isArray(recipients) && recipients.length > 0 && recipients.every(SMTP.isValidEmailAddress);
  }

  /**
   * Creates an SMTP connection.
   *
   * WARNING: This class is supposed to be used as a singleton. You should never call `new SMTP()`
   * @param smtpConfig SMTP config
   */
  private constructor(smtpConfig?: SMTPConfig) {
    // create a partial config from environment variables that can overwrite the given config
    const environmentConfig: RecursivePartial<SMTPConfig> = {
      auth: {
        password: process.env.SMTP_AUTH_PASSWORD,
        user: process.env.SMTP_AUTH_USER,
      },
      cc: process.env.SMTP_CC === undefined ? [] : (process.env.SMTP_CC as string).split(','),
      host: process.env.SMTP_HOST,
      port: process.env.SMTP_PORT === undefined ? undefined : Number.parseInt(process.env.SMTP_PORT, 10),
      recipients: process.env.SMTP_RECIPIENTS === undefined ? [] : process.env.SMTP_RECIPIENTS.split(','),
      secure: process.env.SMTP_SECURE === undefined ? false : process.env.SMTP_SECURE === 'true',
      sender: {
        mail: process.env.SMTP_SENDER_MAIL,
        name: process.env.SMTP_SENDER_NAME,
      },
    };

    const config = {
      ...smtpConfig,
      // deleting undefined properties so the actual config doesn't get overwritten by undefined values
      ...(deleteUndefinedProperties(environmentConfig) as object),
    } as SMTPConfig;

    if (config.host === undefined) {
      throw new TypeError(
        'SMTP configuration needs a host. Add it to the config or use environment variables (SMTP_HOST).',
      );
    }

    if (config.port === undefined || Number.isNaN(config.port)) {
      throw new TypeError(
        'SMTP configuration needs a port. Add it to the config or use environment variables (SMTP_PORT).',
      );
    }

    if (typeof config.auth !== 'object') {
      throw new TypeError(
        'SMTP configuration needs an auth object.' +
          'Add it to the config or use environment variables (SMTP_AUTH_USER, SMTP_AUTH_PASSWORD).',
      );
    }

    if (config.auth.user === undefined) {
      throw new TypeError(
        'SMTP auth configuration needs a user. Add it to the config or use environment variables (SMTP_AUTH_USER).',
      );
    }

    if (config.auth.password === undefined) {
      throw new TypeError(
        'SMTP auth configuration needs a password.' +
          'Add it to the config or use environment variables (SMTP_AUTH_PASSWORD).',
      );
    }

    if (Array.isArray(config.recipients) && config.recipients.length === 0) {
      throw new Error(
        'SMTP configuration needs recipients. Add it to the config or use environment variables (SMTP_RECIPIENTS).',
      );
    }

    if (config.sender.mail === undefined) {
      throw new TypeError(
        'SMTP configuration needs a sender. Add it to the config or use environment variables (SMTP_SENDER_MAIL).',
      );
    }

    super();

    if (SMTP.isValidRecipientsList(config.recipients)) {
      this.recipients = config.recipients;
    } else {
      throw new Error('Invalid recipients found');
    }

    if (config.cc === undefined) {
      this.cc = [];
    } else {
      if (SMTP.isValidRecipientsList(config.cc)) {
        this.cc = config.cc;
      } else {
        throw new Error('Invalid cc recipients found');
      }
    }

    this.from = config.sender;

    this.verified = false;

    // creating transport with configuration
    this.transportAgent = nodemailer.createTransport({
      auth: {
        pass: config.auth.password,
        user: config.auth.user,
      },
      host: config.host,
      port: config.port,
      secure: config.secure === undefined ? false : config.secure,
    });
  }

  /**
   * Check if instance was verified at least once
   */
  public isVerified(): boolean {
    return this.verified;
  }

  /**
   * Sends a preconfigured mail with recipients and sender configured on
   * creation of the class (set by environment or on creation of this class)
   * @param subject Subject of the mail
   * @param message message of the mail
   */
  public async send(subject: string, message: string): Promise<string> {
    return this.sendMail({
      cc: this.cc,
      // use an address block if name is available, mail otherwise
      from: typeof this.from.name === 'string' ? this.from.mail : `${this.from.name} <${this.from.mail}>`,
      subject: subject,
      text: message,
      to: this.recipients,
    });
  }

  /**
   * Sends a mail object
   * @param mail Mail to send
   */
  public async sendMail(mail: MailOptions): Promise<string> {
    // info is the response of the smtp server
    let info = await this.transportAgent.sendMail(mail);

    // it can be undefined (empty response)
    if (info === undefined) {
      info = 'Successfully sent mail';
    }

    // or not of type string
    if (typeof info !== 'string') {
      info = JSON.stringify(info);
    }

    return info;
  }

  /**
   * Verify authentication with SMTP server
   * @throws error if you are in a productive environment and don't set ALLOW_NO_TRANSPORT to true
   * @returns true if the transport is valid
   */
  public async verify(): Promise<boolean> {
    let verificationSuccessfull = false;

    try {
      verificationSuccessfull = await this.transportAgent.verify();
    } catch (error) {
      if (!isProductiveEnvironment() || process.env.ALLOW_NO_TRANSPORT !== 'true') {
        throw error;
      }

      // eslint-disable-next-line no-console
      console.warn(
        'SMTP verification error was ignored, because tranport failures are allowed:',
        (error as Error).message,
      );
    }

    if (!verificationSuccessfull) {
      if (!isProductiveEnvironment() || process.env.ALLOW_NO_TRANSPORT !== 'true') {
        throw new Error(
          'Verification of SMTP transport failed.' +
            'If you want to ignore this error set' +
            '`NODE_ENV=dev` or `ALLOW_NO_TRANSPORT=true`',
        );
      }

      // eslint-disable-next-line no-console
      console.warn('SMTP verification error was ignored, because tranport failures are allowed.');
    }
    this.verified = verificationSuccessfull;

    return verificationSuccessfull;
  }
}
