package hirondelle.web4j.webmaster;

import hirondelle.web4j.model.AppException;
import hirondelle.web4j.readconfig.Config;
import hirondelle.web4j.util.Util;
import hirondelle.web4j.util.WebUtil;

import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.logging.Logger;

import javax.mail.Authenticator;
import javax.mail.Message;
import javax.mail.PasswordAuthentication;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;

/**
 Default implementation of {@link Emailer}.
 
 <P>Uses these <tt>init-param</tt> settings in <tt>web.xml</tt>:
 <ul>
 <li><tt>Webmaster</tt> : the email address of the webmaster.
 <li><tt>MailServerConfig</tt> : configuration data to be passed to the mail server, as a list of name=value pairs.
 Each name=value pair appears on a single line by itself. Used for <tt>mail.host</tt> settings, and so on. 
 The special value <tt>NONE</tt> indicates that emails are suppressed, and will not be sent. 
 <li><tt>MailServerCredentials</tt> : user name and password for access to the outgoing mail server. 
 The user name is separated from the password by a pipe character '|'.
 The special value <tt>NONE</tt> means that no credentials are needed (often the case when the wep app
 and the outgoing mail server reside on the same network). 
 </ul> 
 
 <P>Example <tt>web.xml</tt> settings, using a Gmail account:
<PRE>    &lt;init-param&gt;
      &lt;param-name&gt;Webmaster&lt;/param-name&gt;
      &lt;param-value&gt;myaccount@gmail.com&lt;/param-value&gt; 
    &lt;/init-param&gt;
    
    &lt;init-param&gt;
      &lt;param-name&gt;MailServerConfig&lt;/param-name&gt;
      &lt;param-value&gt;
        mail.smtp.host=smtp.gmail.com       
        mail.smtp.auth=true
        mail.smtp.port=465
        mail.smtp.socketFactory.port=465
        mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
      &lt;/param-value&gt; 
    &lt;/init-param&gt;

    &lt;init-param&gt;
      &lt;param-name&gt;MailServerCredentials&lt;/param-name&gt;
      &lt;param-value&gt;myaccount@gmail.com|mypassword&lt;/param-value&gt; 
    &lt;/init-param&gt;
  </pre>
*/
public final class EmailerImpl implements Emailer {

  public void sendFromWebmaster(List<String> aToAddresses, String aSubject, String aBody) throws AppException {
    if (isMailEnabled()) {
      validateState(getWebmasterEmailAddress(), aToAddresses, aSubject, aBody);
      fLogger.fine("Sending email using request thread.");
      sendEmail(getWebmasterEmailAddress(), aToAddresses, aSubject, aBody);
    }
    else {
      fLogger.fine("Mailing is disabled, since mail server is configured as " + Util.quote(Config.NONE));
    }
  }

  // PRIVATE 

  private Config fConfig = new Config();
  private static final Logger fLogger = Util.getLogger(EmailerImpl.class);

  private boolean isMailEnabled() {
    return  fConfig.isEnabled(fConfig.getMailServerConfig());
  }
  
  private boolean areCredentialsEnabled() {
    return fConfig.isEnabled(fConfig.getMailServerCredentials());
  }

  /** Return the mail server config in the form of a Properties object. */
  private Properties getMailServerConfigProperties() {
    Properties result = new Properties();
    String rawValue = fConfig.getMailServerConfig();
    /*  Example data: mail.smtp.host = smtp.blah.com */
    if(Util.textHasContent(rawValue)){
      List<String> lines = getAsLines(rawValue);
      for(String line : lines){
        int delimIdx = line.indexOf("=");
        String name = line.substring(0,delimIdx);
        String value = line.substring(delimIdx+1);
        if(isMissing(name) || isMissing(value)){
          throw new RuntimeException(
            "This line for the MailServerConfig setting in web.xml does not have the expected form: " + Util.quote(line)
          );
        }
        result.put(name.trim(), value.trim());
      }
    }
    return result;
  }
  
  private List<String> getAsLines(String aRawValue){
    List<String> result = new  ArrayList<String>();
    StringTokenizer parser = new StringTokenizer(aRawValue, "\n\r");
    while ( parser.hasMoreTokens() ) {
      result.add( parser.nextToken().trim() );
    }
    return result;
  }
  
  private static boolean isMissing(String aText){
    return ! Util.textHasContent(aText);
  }
  
  private String getWebmasterEmailAddress() {
    return fConfig.getWebmaster();
  }

  private void validateState(String aFrom, List<String> aToAddresses, String aSubject, String aBody) throws AppException {
    AppException ex = new AppException();
    if (!WebUtil.isValidEmailAddress(aFrom)) {
      ex.add("From-Address is not a valid email address.");
    }
    if (!Util.textHasContent(aSubject)) {
      ex.add("Email subject has no content.");
    }
    if (!Util.textHasContent(aBody)) {
      ex.add("Email body has no content.");
    }
    if (aToAddresses.isEmpty()){
      ex.add("To-Address is empty.");
    }
    for(String email: aToAddresses){
      if (!WebUtil.isValidEmailAddress(email)) {
        ex.add("To-Address is not a valid email address: " + Util.quote(email));
      }
    }
    if (ex.isNotEmpty()) {
      fLogger.severe("Cannot send email : " + ex);
      throw ex;
    }
  }

  private void sendEmail(String aFrom, List<String> aToAddresses, String aSubject, String aBody) throws AppException {
    fLogger.fine("Sending mail from " + Util.quote(aFrom));
    fLogger.fine("Sending mail to " + Util.quote(aToAddresses));
    Properties props = getMailServerConfigProperties();
    //fLogger.fine("Properties: " + props);
    try {
      Authenticator auth = getAuthenticator();
      //fLogger.fine("Authenticator: " + auth);
      Session session = Session.getDefaultInstance(props, auth);
      //session.setDebug(true);
      MimeMessage message = new MimeMessage(session);
      message.setFrom(new InternetAddress(aFrom));
      for(String toAddr: aToAddresses){
        message.addRecipient(Message.RecipientType.TO, new InternetAddress(toAddr));
      }
      message.setSubject(aSubject);
      message.setText(aBody);
      Transport.send(message); // thread-safe? throttling makes the question irrelevant 
    }
    catch (Throwable ex) {
      fLogger.severe("CANNOT SEND EMAIL: " + ex);
      throw new AppException("Cannot send email", ex);
    }
    fLogger.fine("Mail is sent.");
  }

  private Authenticator getAuthenticator(){
    Authenticator  result = null;
    if( areCredentialsEnabled() ){
      result = new SMTPAuthenticator();
    }
    return result;
  }
  
  private static final class SMTPAuthenticator extends Authenticator {
    SMTPAuthenticator() {}
    public PasswordAuthentication getPasswordAuthentication()   {
      PasswordAuthentication result = null;
      /** Format is pipe separated : bob|passwd. */
      String rawValue = new Config().getMailServerCredentials();
      int delimIdx = rawValue.indexOf("|");
      if(delimIdx != -1){
        String userName = rawValue.substring(0,delimIdx);
        String password = rawValue.substring(delimIdx+1);
        result = new PasswordAuthentication(userName, password);
      }
      else {
        throw new RuntimeException("Missing pipe separator between user name and password: " + rawValue);
      }
      return result;
    }
  }
}
