/*****************************************************************************************
 * Copyright © 2004 EDS. All rights reserved
 ****************************************************************************************/

package gov.va.med.esr.messaging.service.inbound;

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

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

import gov.va.med.fw.hl7.AckMessage;
import gov.va.med.fw.hl7.BatchMessage;
import gov.va.med.fw.hl7.HL7MessageUtils;
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.lookup.MessageStatus;
import gov.va.med.esr.common.model.lookup.MessageType;
import gov.va.med.esr.common.model.lookup.VAFacility;
import gov.va.med.esr.common.model.messaging.MessageLogEntry;
import gov.va.med.esr.messaging.constants.HL7Constants;
import gov.va.med.esr.messaging.service.MessageProcessServiceUtil;
import gov.va.med.esr.service.LookupService;

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

	/**
	 * An instance of messageProcessService
	 */
	private InboundProcessService messageProcessService;

	/**
	 * An instance of solicitedQueryResponses
	 */
	private Map solicitedQueryResponses;

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

	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();
			}
		}

		try {
			initAuditIdFromMessage(message);

			if (message instanceof BatchMessage) {
				this.processBatchMessage((BatchMessage) message);
			}
			else if (message instanceof AckMessage) {
				processAckMessage((AckMessage) message);
			}
			else {
				this.processSingleMessage(null, null, message);
			}
		}
		catch (Throwable e) {
			// Log the exception and we are done.
			logger.error("Failed to process Solicitated message "
					+ message.getMessageData(), e);
		}
		finally {
			if (logger.isDebugEnabled()) {
				try {
					StringBuffer info = new StringBuffer("Total time to process ")
							.append(
									getMessageProcessService() == null ? message
											.getMessageType() : getMessageProcessService()
											.getMessageType().getCode()).append(" ID: ")
							.append(super.getControlIdentifier(message));

					if (watch != null) {
						watch.stopAndLog(info.toString());
					}
				}
				catch (Exception e) {
				}
			}
		}
		return null;
	}

	private void processAckMessage(AckMessage ack)
			throws InboundProcessException {
		String referencedControlNumber = null;
		try {
			// get referenced message control number
			referencedControlNumber = ack.getReferencedControlNumber();

			if (ack.isBatch()) {
				// this case could be a batch of AE or a batched up singular
				// query response
				BatchMessage batch = ack.getAsBatch();
				if (ack.isQueryResponse()) {
					/*
					 * Note: This is very nonstandard but apparently VistA will wrap
					 * up some query responses in a batch header. This logic is in
					 * place to handle that strange VistA behavior (query response
					 * wrapped as a batch ACK).
					 */
					// unwrap the singular query response from the batch
					Message msg = (Message) batch.getMessages().get(0);

					// get the first and only Message
					// get referenced MessageLogEntry for this solicited message and
					// update it
					MessageLogEntry referencedLogEntry = getReferencedMessageLogEntry(msg);
					boolean ackOk = logAcknowledgment(msg, referencedLogEntry);

					// process query response data
					MessageLogEntry logEntry = null;
					String batchControlIdentifier = getControlIdentifier(batch);
					Date batchTransmissionDate = getTransmissionDate(batch);
					try {
						// get the appropriate solicitedQueryResponses service
						// based on query response message type (eg, ORFZ07)
						InboundProcessService ips = (InboundProcessService) getSolicitedQueryResponses()
								.get(batch.getType());
						if (ips == null) {
							MessageType messageType = null;
							try {
								messageType = getLookupService().getMessageTypeByCode(
										batch.getType() + "-E");
							}
							catch (Exception e) {
								logger.error("Received an exception", e);
								// TODO: REVISIT THIS LOGIC HERE
							}
							LookupService lookupService = getLookupService();
							VAFacility vaFacility = lookupService
									.getVaFacilityByCode(msg.getSendingFacility());

							AckType.Code code = AckType.Code.getByCode(HL7MessageUtils
									.getAcknowledgmentCode(msg));
							AckType ackType = lookupService.getAckTypeByCode(code);

							// Create a log entry with detail message data
							logEntry = MessageProcessServiceUtil
									.createMessageLog(
											getControlIdentifier(msg),
											batchControlIdentifier,
											messageType,
											lookupService
													.getMessageStatusByCode(MessageStatus.ERROR),
											vaFacility,
											msg.getMessageData(),
											null,
											batchTransmissionDate,
											new Date(),
											ackType,
											null,
											"Can not find the InboundProcessService for the solicited (ACK) query response "
													+ "that came in as a batch message for messageType: "
													+ batch.getType());
						}
						else {
							if(ackOk) {
								logEntry = ips.processMessage(msg);
							} else {
								logEntry = this.createErrorMessageLogEntry(msg,
										new IllegalStateException("Ignored this message since initiating message was already updated by an ACK or ORF"),
										ips.getMessageType());
							}
						}
					}
					catch (InboundProcessException e) {
						logEntry = e.getLogEntry();
					}

					if(ackOk) {
						logSingleMessage(logEntry, batchControlIdentifier,
								batchTransmissionDate);
					}
				}
				else {
					// normal batch of ACK's to individual responses to each MSH
					// (eg, AE)
					List messages = batch.getMessages();

					for (int index = 0; index < messages.size(); index++) {
						Message message = (Message) messages.get(index);
						// get referenced MessageLogEntry for this solicited
						// message and update it
						MessageLogEntry referencedLogEntry = getReferencedMessageLogEntry(message);
						logAcknowledgment(message, referencedLogEntry);
					}
				}
			}
			else {
				// if ACK is not batch, then this is a single ACK to the entire
				// batch that was sent out (eg, AA)
				MessageLogEntry referencedLogEntry = getMessagingService()
						.getMessageLogEntryByBatchControlNumber(
								referencedControlNumber);
				// update it and we're done
				logAcknowledgment(ack, referencedLogEntry);
			}
		}
		catch (Exception e) {
			throw new InboundProcessException(null,
					"Failed to process the ACK message for referenced control number: "
							+ referencedControlNumber, e);
		}

	}

	private MessageLogEntry logSingleMessage(MessageLogEntry logEntry,
			String batchControlIdentifier, Date transmissionDate) {
		if (logEntry != null) {
			logEntry.setBatchControlIdentifier(batchControlIdentifier);

			if (logEntry.getTransmissionDate() == null) {
				logEntry.setTransmissionDate(transmissionDate);
			}

			super.logMessage(logEntry);
		}
		return logEntry;
	}

	/**
	 * Method to process batch message.
	 * 
	 * @param batch
	 */
	private void processBatchMessage(BatchMessage batch) {

		List messages = null;
		String batchControlIdentifier = null;
		try {

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

			messages = batch.getMessages();
			for (int index = 0; index < messages.size(); index++) {
				Message message = (Message) messages.get(index);
				processSingleMessage(batchControlIdentifier, batchTransmissionDate,
						message);
			}
		}
		catch (InvalidMessageException e) {
			throw new RuntimeException(
					"Failed process batch message due to an exception ", e);
		}
	}

	/**
	 * Method to process single message.
	 * 
	 * @param batchControlIdentifier
	 * @param batchTransmissionDate
	 * @param message
	 * @return The message log entry.
	 */
	protected final MessageLogEntry processSingleMessage(
			String batchControlIdentifier, Date batchTransmissionDate,
			Message message) {
		MessageLogEntry logEntry = null;

		boolean ackOk = false;
		try {
			// get referenced MessageLogEntry for this solicited message and
			// update it
			MessageLogEntry referencedLogEntry = getReferencedMessageLogEntry(message);

			if (referencedLogEntry == null) {
				Exception ex = new ServiceException(
						"Solicited Message can not be tied to an existing MessageLogEntry with control id:"
								+ message.getMessageID());
				logEntry = super.createErrorMessageLogEntry(message, ex,
						getMessageProcessService().getMessageType());

				// If the original message reference is not found send a bulletin
				if (HL7Constants.ORFZ11.equals(message.getType()))
					sendSolictedZ11NoMatchBulletin(message);

			}
			else {

				// log acknowledgement
				ackOk = logAcknowledgment(message, referencedLogEntry);
				if(ackOk) {
					//CodeCR12654
					//logEntry = this.messageProcessService.processMessage(message);
					//Adding retry mechanism to override HibernateOptimisticLockingFailureException for given number of MAX retries
					logEntry = retryProcessMessage(message, 0);
				} else {
					logEntry = this.createErrorMessageLogEntry(message,
							new IllegalStateException("Ignored this message since initiating message was already updated by an ACK or ORF"),
							this.messageProcessService.getMessageType());
				}
			}

		}
		catch (InboundProcessException e) {
			logger.info("Exception processing Solicitated message ", e);
			logEntry = e.getLogEntry();

		}
		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(ackOk) {
			return logSingleMessage(logEntry, batchControlIdentifier,
					batchTransmissionDate);
		}
		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;
	}
	
	/**
	 * @return Returns the solicitedQueryResponses.
	 */
	public Map getSolicitedQueryResponses() {
		return solicitedQueryResponses;
	}

	/**
	 * @param solicitedQueryResponses
	 *           The solicitedQueryResponses to set.
	 */
	public void setSolicitedQueryResponses(Map solicitedQueryResponses) {
		this.solicitedQueryResponses = solicitedQueryResponses;
	}
}