/*****************************************************************************************
 * Copyright © 2004 EDS. All rights reserved
 ****************************************************************************************/
package gov.va.med.esr.messaging.service.inbound;

// Java Classes
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import org.apache.commons.lang.ClassUtils;
import org.apache.commons.lang.Validate;

import gov.va.med.fw.hl7.BatchMessage;
import gov.va.med.fw.hl7.InvalidMessageException;
import gov.va.med.fw.hl7.Message;
import gov.va.med.fw.service.ServiceException;
import gov.va.med.fw.util.StopWatchLogger;

import gov.va.med.esr.common.model.lookup.AckType;
import gov.va.med.esr.common.model.messaging.MessageLogEntry;
import gov.va.med.esr.messaging.service.outbound.OutboundProcessService;

/**
 * Abstract class that should be extended by objects providing inbound message
 * services.
 * 
 * @author Vu Le
 * @version 1.0
 */
public class UnsolicitedInboundProcessService extends
		AbstractInboundMessagingService {
	
	private OutboundProcessService aaAckService;

	private OutboundProcessService aeAckService;

	private InboundProcessService messageProcessService;

	private boolean allowDuplicateInboundMessages = false;

	/**
	 * Default constructor.
	 */
	public UnsolicitedInboundProcessService() {
		super();
	}

	
	public OutboundProcessService getAaAckService() {
		return this.aaAckService;
	}

	public void setAaAckService(OutboundProcessService aaAckService) {
		this.aaAckService = aaAckService;
	}

	public OutboundProcessService getAeAckService() {
		return this.aeAckService;
	}

	public void setAeAckService(OutboundProcessService aeAckService) {
		this.aeAckService = aeAckService;
	}

	public InboundProcessService getMessageProcessService() {
		return this.messageProcessService;
	}

	public void setMessageProcessService(
			InboundProcessService messageProcessService) {
		this.messageProcessService = messageProcessService;
	}

	/**
	 * Method to process message.
	 * 
	 * @param message
	 *            The message
	 */
	public MessageLogEntry processMessage(Message message)
			throws InboundProcessException {
		Validate.notNull(message, "Message cannot be null");

		StopWatchLogger watch = null;
		if (logger.isDebugEnabled()) {
			watch = new StopWatchLogger(ClassUtils
					.getShortClassName(getClass())
					+ " processMessage");
			if (watch != null) {
				watch.start();
			}
		}

		List messageLogEntries = null;
		try {
			initAuditIdFromMessage(message);
			if (message instanceof BatchMessage) {
				messageLogEntries = processBatchMessage((BatchMessage) message);
			} else {
				// There will be only one MessageLogEntry in the list
				messageLogEntries = processSingleMessage(message);
			}

			if (messageLogEntries != null && !messageLogEntries.isEmpty()) {
				this.sendAcknowledgement(message, messageLogEntries);
			}
		} catch (Throwable e) {
			// Means the whole batch failed without processing any message. Dump
			// the whole batch data for troubleshooting purposes.
			logger.error("Could not process Unsolicitated batch message.  "
					+ message.getMessageData(), e);

		} finally {
			if (logger.isDebugEnabled()) {
				try {
					StringBuffer info = new StringBuffer(
							"Total time to process ");
					info.append(
							messageLogEntries == null ? 0 : messageLogEntries
									.size()).append(" ").append(
							getMessageProcessService().getMessageType()
									.getCode()).append(" ID: ").append(
							message.getMessageID());
					if (watch != null) {
						watch.stopAndLog(info.toString());
					}
				} catch (Exception e) {
				}
			}
		}
		return null;
	}

	/**
	 * Method to process a batch message.
	 * 
	 * @param batch
	 * @return The list of message log entries.
	 */
	private List processBatchMessage(BatchMessage batch) {
		try {

			String batchControlIdentifier = super.getControlIdentifier(batch);
			Date batchTransmissionDate = super.getTransmissionDate(batch);

			StopWatchLogger watch = null;
			if (logger.isDebugEnabled()) {
				watch = new StopWatchLogger("BatchMessage.getMessages");
				if (watch != null) {
					watch.start();
				}
			}

			List messages = batch.getMessages();

			if (logger.isDebugEnabled() && watch != null)
				watch.stopAndLog();

			List messageLogEntries = new ArrayList(messages.size());

			for (int index = 0; index < messages.size(); index++) {
				Message message = (Message) messages.get(index);
				MessageLogEntry mle = this.processSingleMessage(
						batchControlIdentifier, batchTransmissionDate, message);
				if (mle != null) {
					messageLogEntries.add(mle);
				}
			}

			return messageLogEntries;
		} catch (InvalidMessageException e) {
			// No Message log entry can be logged at this batch level. So fail
			// the whole batch and throw the exception.
			throw new RuntimeException(
					"Failed extracting list of messages from incoming batch message: ",
					e);
		}
	}

	/**
	 * Method to process a single message.
	 * 
	 * @param message
	 * @return The list of message log entries.
	 */
	private List processSingleMessage(Message message) {
		List messageLogEntries = new ArrayList(1);
		MessageLogEntry mle = this.processSingleMessage(null, null, message);
		if (mle != null) {
			messageLogEntries.add(mle);
		}

		return messageLogEntries;
	}

	/**
	 * Method to process single message.
	 * <li>Calls appropriate service(e.g Z07ProcessService) to do the dirty
	 * stuff</li>
	 * <li>Calls Messaging Service to persist the MessageLogEntry in the
	 * transaction log</li>
	 * 
	 * @param batchControlIdentifier
	 * @param batchTransmissionDate
	 * @param message
	 * @return The message log entry.
	 */
	protected final MessageLogEntry processSingleMessage(
			String batchControlIdentifier, Date batchTransmissionDate,
			Message message) {
		MessageLogEntry logEntry = null;

		if (!allowDuplicateInboundMessages) {
			try {
				String messageControlNumber = message.getMessageID();
				String sendingFacility = message.getSendingFacility();

				if (super.getMessagingService().hasProcessedInboundMessage(
						messageControlNumber, sendingFacility)) {
					logger
							.warn("Ignoring processSingleMessage for DUPLICATE incoming ORU with [messageType="
									+ message.getMessageType()
									+ " ,stationNumber="
									+ sendingFacility
									+ " ,messageControlNumber="
									+ messageControlNumber + "]");
					return null;
				}
			} catch (Exception e) {
				if (logger.isErrorEnabled()) {
					logger
							.error(
									"Unable to determine if already processed inbound messages due to exception...so processing message anyway",
									e);
				}
			}
		}

		try {
			// Call appropriate service(e.g Z07ProcessService) to do the dirty
			// stuff
			//CodeCR12654
			//logEntry = this.messageProcessService.processMessage(message);
			//Adding retry mechanism to override HibernateOptimisticLockingFailureException for given number of MAX retries
			logEntry = retryProcessMessage(message, 0);
		} catch (InboundProcessException e) {
			logEntry = e.getLogEntry();
			if (logger.isInfoEnabled()) {
				logger.info(
						"Exception processing unsolicitated inbound message "
								+ message, e);
			}
		} catch (Throwable ex) {
			try {
				logEntry = super.createErrorMessageLogEntry(message, ex,
						getMessageProcessService().getMessageType());
			} catch (ServiceException e) {
				// Could neither process the message nor create a
				// MessageLogEntry object to persist into the transaction log.
				// Dump the message data for troubleshooting.
				logger.error("Could not process Solicited message nor log "
						+ "into transaction log: " + message.getMessageData(),
						e);
			}
		}

		if (logEntry != null) {
			logEntry.setBatchControlIdentifier(batchControlIdentifier);

			if (logEntry.getTransmissionDate() == null) {
				logEntry.setTransmissionDate(batchTransmissionDate);
			}
			// Call Messaging Service to persist the MessageLogEntry in the
			// transaction log
			if (logger.isDebugEnabled()) {
				logger
						.debug("Persisting message into transaction log for Message Control Number "
								+ logEntry.getControlIdentifier());
			}
			super.logMessage(logEntry);
		}

		return logEntry;
	}

	//CodeCR12654 - Adding retry mechanism to override HibernateOptimisticLockingFailureException for given number of MAX retries
	private MessageLogEntry retryProcessMessage(Message message, int retryTimes) 
	throws InboundProcessException, org.springframework.orm.hibernate3.HibernateOptimisticLockingFailureException {
		MessageLogEntry logEntry = null;
		try {
			logger.info("Invoking retryProcessMessage with retryTimes as "+retryTimes);
			logEntry = this.messageProcessService.processMessage(message);
		} catch (org.springframework.orm.hibernate3.HibernateOptimisticLockingFailureException ex) {
			logger.info("HibernateOptimisticLockingFailureException received, error is "+ex.getMessage());
			if (retryTimes < getRetryAppMessagingCount()) {
				retryTimes += 1;
				logEntry = retryProcessMessage(message, retryTimes);
			} else {
				logger.info("HibernateOptimisticLockingFailureException exceeds MAX count - " + getRetryAppMessagingCount());
				throw ex;
			}
		}
		return logEntry;
	}	
	
	private void sendAcknowledgement(Message message, List messageLogEntries) {
		List errorEntries = new ArrayList();
		boolean hasErrorEntries = false;
		// Find all the error entries
		for (int index = 0; index < messageLogEntries.size(); index++) {
			MessageLogEntry logEntry = (MessageLogEntry) messageLogEntries
					.get(index);
			String ackType = logEntry.getAckType() == null ? null : logEntry
					.getAckType().getCode();

			if (!AckType.CODE_AA.getName().equals(ackType)) {
				hasErrorEntries = true;
				errorEntries.add(logEntry);
				// break;
			}
		}

		try {
			if (!hasErrorEntries) {
				// one ACK for whole message
				this.aaAckService.processMessage(message);
			} else {
				// ok, we know there was at least one exception...must report on
				// each MSH
				// individually (in one outbound message)
				this.aeAckService.processMessage(new Object[] { message,
				/* messageLogEntries */errorEntries });
			}
		} catch (ServiceException e) {
			// Could not send acknowledgement.
			if (super.logger.isErrorEnabled()) {
				super.logger.error("Could not send the acknowledgement", e);
			}

			// Set all the ack dates of the message log entries to null
			for (int index = 0; index < messageLogEntries.size(); index++) {
				((MessageLogEntry) messageLogEntries.get(index))
						.setAckDate(null);
			}
		}
	}

	public boolean isAllowDuplicateInboundMessages() {
		return allowDuplicateInboundMessages;
	}

	public void setAllowDuplicateInboundMessages(
			boolean allowDuplicateInboundMessages) {
		this.allowDuplicateInboundMessages = allowDuplicateInboundMessages;
	}

}