/*
 * Created on Mar 11, 2008 (C) Copyright TANDBERG Television Ltd.
 */

package com.tandbergtv.watchpoint.pmm.action;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import java.util.Random;

import javax.mail.BodyPart;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.Message.RecipientType;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

import org.apache.log4j.Logger;
import org.jbpm.graph.def.ActionHandler;
import org.jbpm.graph.exe.ExecutionContext;

import com.tandbergtv.workflow.util.ApplicationProperties;

/**
 * Action handler that sends an email.
 * 
 * @author Vijay Silva
 */
public class EmailAction implements ActionHandler {

	/* Serialization Id */
	private static final long serialVersionUID = 1518660516183353428L;

	private static final Logger logger = Logger.getLogger(EmailAction.class);

	/* The Mail Service Name from which to get the mail server information */
	private static final String MAIL_SERVICE_NAME = "java:/Mail";

	/* The Place Holder start marker */
	private static final String PLACEHOLDER_START = "<![";

	/* The Place Holder end marker */
	private static final String PLACEHOLDER_END = "]>";

	/* The Application Property Key for the workflow URL */
	private static final String WORKFLOW_APPlICATION_URL_KEY = "application.url";

	/** The Key to use to display the token Id */
	public static final String TOKEN_ID_KEY = "_tokenId";

	/** The Key to use to display the Work Order Id */
	public static final String PROCESS_ID = "_workOrderNumber";

	/** The Key to use to display the Workflow System URL */
	public static final String WORKFLOW_URL = "_workflowURL";

	/** The Configuration Property Name for the recipient list */
	public static final String TO_PROPERTY_NAME = "Mail.To";

	/** The Configuration Property Name for the 'cc' recipient list */
	public static final String CC_PROPERTY_NAME = "Mail.CC";

	/** The Configuration Property Name for the 'bcc' recipient list */
	public static final String BCC_PROPERTY_NAME = "Mail.BCC";

	/** The Configuration Property Name for the email subject */
	public static final String SUBJECT_PROPERTY_NAME = "Mail.Subject";

	/** The Configuration Property Name for the email body text */
	public static final String TEXT_PROPERTY_NAME = "Mail.Text";

	/** The Configuration Property Name for the files to attach */
	public static final String ATTACHMENTS_PROPERTY_NAME = "Mail.Attachments";

	/** The name of the properties file resource containing the required properties */
	private static final String CONFIGURATION_FILE = "template-actions/emailNotification.properties";

	/* The number of attempts to make to deliver the email message */
	private static final int MAX_ATTEMPTS = 5;

	/* The Key to use when getting Email Properties */
	private String configurationKey;

	/* The number of retry attempts to make in case the SMTP connection fails */
	private int deliveryAttempts = 3;

	/**
	 * Default Constructor
	 */
	public EmailAction() {
	}

	/**
	 * Sends an email to a specified sender
	 * 
	 * @see org.jbpm.graph.def.ActionHandler#execute(org.jbpm.graph.exe.ExecutionContext)
	 */
	public void execute(ExecutionContext executionContext) throws Exception {
		this.sendEmail(executionContext);
	}

	/*
	 * Sends an Email using the specified configuration
	 */
	protected void sendEmail(ExecutionContext context) throws ActionException {
		MimeMessage message = this.buildMessage(context);

		if (this.deliveryAttempts < 1) {
			this.deliveryAttempts = 1;
		} else if (this.deliveryAttempts > MAX_ATTEMPTS) {
			this.deliveryAttempts = MAX_ATTEMPTS;
		}

		boolean attemptSuccessful = false;
		for (int attempt = 1; attempt <= this.deliveryAttempts; attempt++) {
			try {
				Transport.send(message);
				attemptSuccessful = true;
				break;
			} catch (MessagingException e) {
				String msg = "Failed to deliver email notification message for attempt #" + attempt;
				logger.warn(msg, e);
				if (attempt != this.deliveryAttempts) {
					this.delay();
				}
			}
		}

		if (!attemptSuccessful) {
			String msg = "Failed to deliver email notification message, giving up after "
					+ Integer.toString(this.deliveryAttempts) + " attempts.";
			throw new ActionException(msg);
		}
	}

	/**
	 * Build the Message to send
	 * 
	 * @param context The Execution Context
	 * @return The MimeMessage built
	 * @throws ActionException
	 */
	protected MimeMessage buildMessage(ExecutionContext context) throws ActionException {
		MimeMessage message = null;

		/* Read the Message Properties */
		Properties properties = this.readConfiguration();
		try {
			Session session = this.getMailSession(context);
			message = new MimeMessage(session);

			String to = this.getProperty(properties, TO_PROPERTY_NAME);
			String cc = this.getProperty(properties, CC_PROPERTY_NAME);
			String bcc = this.getProperty(properties, BCC_PROPERTY_NAME);
			String subject = this.getProperty(properties, SUBJECT_PROPERTY_NAME);
			String text = this.getProperty(properties, TEXT_PROPERTY_NAME);
			String attachments = this.getProperty(properties, ATTACHMENTS_PROPERTY_NAME);

			/* Set the message properties */
			message.setRecipients(RecipientType.TO, this.replacePlaceholderValues(context, to));
			message.setRecipients(RecipientType.CC, this.replacePlaceholderValues(context, cc));
			message.setRecipients(RecipientType.BCC, this.replacePlaceholderValues(context, bcc));
			message.setSubject(this.replacePlaceholderValues(context, subject));

			/* Set the message content / attachments */
			text = this.replacePlaceholderValues(context, text);
			attachments = this.replacePlaceholderValues(context, attachments);
			boolean hasAttachments = (attachments != null && attachments.trim().length() > 0);

			if (!hasAttachments) {
				message.setText(text);
			} else {
				Multipart content = new MimeMultipart();

				/* Add the Message Text */
				BodyPart textPart = new MimeBodyPart();
				textPart.setText(text);
				content.addBodyPart(textPart);

				/* Add each attachment */
				String[] attachmentPaths = attachments.split(",");
				for (String attachmentPath : attachmentPaths) {
					if (attachmentPath != null && attachmentPath.trim().length() > 0) {
						BodyPart attachmentPart = this.buildAttachment(attachmentPath);
						content.addBodyPart(attachmentPart);
					}
				}

				/* Set the content of the mail message */
				message.setContent(content);
			}
		} catch (MessagingException e) {
			String msg = "Failed to compose the email notification message.";
			throw new ActionException(msg, e);
		}

		return message;
	}

	/**
	 * Build the Mail BodyPart that contains the attachment file
	 * 
	 * @param attachmentFilePath The path to the file to attach
	 * @return The Mail BodyPart for the attachment
	 * @throws ActionException Exception adding the path.
	 */
	protected BodyPart buildAttachment(String attachmentFilePath) throws ActionException {
		try {
			MimeBodyPart attachmentPart = new MimeBodyPart();
			attachmentPart.attachFile(attachmentFilePath.trim());

			return attachmentPart;
		} catch (Exception e) {
			String msg = "Failed to create email attachment for file: " + attachmentFilePath
					+ ", error: " + e.getMessage();
			throw new ActionException(msg, e);
		}
	}

	/* Delay the current thread by a random number of seconds less than 2 minutes */
	private void delay() {
		int delay = new Random().nextInt(120000);
		try {
			Thread.sleep(delay);
		} catch (InterruptedException e) {
			logger.warn("Interrupted while delaying before email retry.", e);
		}
	}

	/* Get the Mail Session used to transmit the message */
	private Session getMailSession(ExecutionContext executionContext) throws ActionException {
		try {
			Context context = new InitialContext();
			return (Session) context.lookup(MAIL_SERVICE_NAME);
		} catch (NamingException e) {
			throw new ActionException("Failed to get the Mail Service[" + MAIL_SERVICE_NAME
					+ "] using JNDI.", e);
		} catch (Exception e) {
			throw new ActionException("Unexpected error when getting the Mail Service["
					+ MAIL_SERVICE_NAME + "] using JNDI.", e);
		}
	}

	/* Replaces all place holders with appropriate value in the input string */
	private String replacePlaceholderValues(ExecutionContext context, final String value)
			throws ActionException {
		if (value == null)
			return "";

		/* replace all place holders */
		int index = 0;
		StringBuilder buf = new StringBuilder();
		while (index < value.length()) {
			/* find the PLACEHOLDER_START and PLACEHOLDER_END strings */
			int startIndex = value.indexOf(PLACEHOLDER_START, index);
			int endIndex = (startIndex != -1) ? value.indexOf(PLACEHOLDER_END, startIndex) : -1;

			if (startIndex != -1) {
				/* Append the message up to the PARAM_START */
				buf.append(value.substring(index, startIndex));

				if (endIndex != -1) {
					/* Found the PARAM_END */
					String parameter = value.substring(startIndex + PLACEHOLDER_START.length(),
							endIndex);
					String replacement = this.getReplacementValue(context, parameter);
					buf.append(replacement);
					index = endIndex + PLACEHOLDER_END.length();
				} else {
					/* No PARAM_END, skip PARAM_START */
					index = startIndex + PLACEHOLDER_START.length();
				}
			} else {
				/* No PARAM_START, go to end of the message */
				buf.append(value.substring(index, value.length()));
				index = value.length();
			}
		}

		return buf.toString();
	}

	/*
	 * Replace the specified value by using the variable value in the execution context, or using
	 * the special replacement keys
	 */
	private String getReplacementValue(ExecutionContext context, String key) throws ActionException {
		String value = null;

		if (key == null) {
			value = "";
		} else if (key.equals(TOKEN_ID_KEY)) {
			value = Long.toString(context.getToken().getId());
		} else if (key.equals(PROCESS_ID)) {
			value = Long.toString(context.getProcessInstance().getId());
		} else if (key.equals(WORKFLOW_URL)) {
			try {
				ApplicationProperties properties = ApplicationProperties.getInstance();
				value = properties.getProperty(WORKFLOW_APPlICATION_URL_KEY);
			} catch (Exception e) {
				String message = "Failed to get the Workflow Application Properties key: "
						+ WORKFLOW_APPlICATION_URL_KEY;
				throw new ActionException(message, e);
			}
		} else {
			Object variableValue = context.getVariable(key);
			value = (variableValue != null) ? variableValue.toString() : "";
		}

		return value;
	}

	// ========================================================================
	// ===================== CONFIGURATION MANAGEMENT
	// ========================================================================

	/* Get the property value for the given property name */
	private String getProperty(Properties properties, String name) {
		String key = this.configurationKey + "." + name;
		return properties.getProperty(key);
	}

	/* Read the configuration for this notification */
	private Properties readConfiguration() throws ActionException {
		InputStream stream = null;
		try {
			ClassLoader classloader = this.getClass().getClassLoader();
			stream = classloader.getResourceAsStream(CONFIGURATION_FILE);
			Properties properties = new Properties();
			properties.load(stream);
			return properties;
		} catch (IOException ioe) {
			String msg = "Failed to load the configuration: " + CONFIGURATION_FILE;
			throw new ActionException(msg, ioe);
		} finally {
			try {
				if (stream != null)
					stream.close();
			} catch (Exception e) {
				String msg = "Failed to close the stream for file: " + CONFIGURATION_FILE
						+ ", ignoring.";
				logger.warn(msg, e);
			}
		}
	}
}
