package com.hubspot.singularity.smtp;

import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import javax.inject.Singleton;
import javax.mail.Address;
import javax.mail.Message.RecipientType;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeUtility;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Optional;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.inject.Inject;
import com.hubspot.mesos.JavaUtils;
import com.hubspot.singularity.config.SMTPConfiguration;
import com.hubspot.singularity.sentry.SingularityExceptionNotifier;

import io.dropwizard.lifecycle.Managed;

@Singleton
public class SingularitySmtpSender implements Managed {

  private static final Logger LOG = LoggerFactory.getLogger(SingularitySmtpSender.class);

  private final Optional<SMTPConfiguration> maybeSmtpConfiguration;
  private final Optional<ThreadPoolExecutor> mailSenderExecutorService;
  private final SingularityExceptionNotifier exceptionNotifier;

  @Inject
  public SingularitySmtpSender(Optional<SMTPConfiguration> maybeSmtpConfiguration, SingularityExceptionNotifier exceptionNotifier) {
    this.maybeSmtpConfiguration = maybeSmtpConfiguration;
    this.exceptionNotifier = exceptionNotifier;

    if (maybeSmtpConfiguration.isPresent()) {
      this.mailSenderExecutorService = Optional.of(JavaUtils.newFixedTimingOutThreadPool(maybeSmtpConfiguration.get().getMailMaxThreads(), TimeUnit.SECONDS.toMillis(1), "SingularitySMTPSender-%d"));
    } else {
      this.mailSenderExecutorService = Optional.absent();
    }
  }

  @Override
  public void start() {}

  @Override
  public void stop() {
    if (mailSenderExecutorService.isPresent()) {
      MoreExecutors.shutdownAndAwaitTermination(mailSenderExecutorService.get(), 1, TimeUnit.SECONDS);
    }
  }

  public void queueMail(final List<String> toList, final List<String> ccList, final String subject, final String body) {
    if (toList.isEmpty()) {
      LOG.warn("Couldn't queue email {} because no to address is present", subject);
      return;
    }

    if (!mailSenderExecutorService.isPresent()) {
      LOG.warn(String.format("Couldn't queue email %s because no SMTP configuration is present", getEmailLogFormat(toList, subject)));
      return;
    }

    final Runnable cmd = new Runnable() {

      @Override
      public void run() {
        sendMail(toList, ccList, subject, body);
      }
    };

    LOG.debug("Queuing an email to {}/{} (subject: {})", toList, ccList, subject);

    mailSenderExecutorService.get().submit(cmd);
  }

  private String getEmailLogFormat(List<String> toList, String subject) {
    return String.format("[to: %s, subject: %s]", toList, subject);
  }

  private Session createSession(SMTPConfiguration smtpConfiguration, boolean useAuth) {
    Properties properties = System.getProperties();

    properties.setProperty("mail.smtp.host", smtpConfiguration.getHost());
    properties.setProperty("mail.smtp.port", Integer.toString(smtpConfiguration.getPort()));

    if (useAuth) {
      properties.setProperty("mail.smtp.auth", "true");
      return Session.getInstance(properties, new SMTPAuthenticator(smtpConfiguration.getUsername().get(), smtpConfiguration.getPassword().get()));
    } else {
      return Session.getInstance(properties);
    }
  }

  private void sendMail(List<String> toList, List<String> ccList, String subject, String body) {
    final SMTPConfiguration smtpConfiguration = maybeSmtpConfiguration.get();

    boolean useAuth = false;

    if (smtpConfiguration.getUsername().isPresent() && smtpConfiguration.getPassword().isPresent()) {
      useAuth = true;
    } else if (smtpConfiguration.getUsername().isPresent() || smtpConfiguration.getPassword().isPresent()) {
      LOG.warn("Not using smtp authentication because username ({}) or password ({}) was not present", smtpConfiguration.getUsername().isPresent(), smtpConfiguration.getPassword().isPresent());
    }

    try {
      final Session session = createSession(maybeSmtpConfiguration.get(), useAuth);

      MimeMessage message = new MimeMessage(session);

      Address[] toArray = getAddresses(toList);
      message.addRecipients(RecipientType.TO, toArray);

      if (!ccList.isEmpty()) {
        Address[] ccArray = getAddresses(ccList);
        message.addRecipients(RecipientType.CC, ccArray);
      }

      message.setFrom(new InternetAddress(smtpConfiguration.getFrom()));

      message.setSubject(MimeUtility.encodeText(subject, "utf-8", null));
      message.setContent(body, "text/html; charset=utf-8");

      LOG.trace("Sending a message to {} - {}", Arrays.toString(toArray), getEmailLogFormat(toList, subject));

      Transport.send(message);
    } catch (Throwable t) {
      LOG.warn("Unable to send message {}", getEmailLogFormat(toList, subject), t);
      exceptionNotifier.notify(t, ImmutableMap.of("subject", subject));
    }
  }

  private Address[] getAddresses(List<String> toList) {
    List<InternetAddress> addresses = Lists.newArrayListWithCapacity(toList.size());

    for (String to : toList) {
      try {
        addresses.add(new InternetAddress(to));
      } catch (AddressException t) {
        LOG.warn("Invalid address {} - ignoring", to, t);
      }
    }

    return addresses.toArray(new InternetAddress[addresses.size()]);
  }
}
