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

// Java Classes
import gov.va.med.esr.common.model.CommonEntityKeyFactory;
import gov.va.med.esr.common.model.lookup.AckType;
import gov.va.med.esr.common.model.lookup.FunctionalGroup;
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.NameType;
import gov.va.med.esr.common.model.lookup.SSNType;
import gov.va.med.esr.common.model.lookup.VAFacility;
import gov.va.med.esr.common.model.lookup.WkfCaseType;
import gov.va.med.esr.common.model.messaging.MessageLogEntry;
import gov.va.med.esr.common.model.messaging.SiteIdentity;
import gov.va.med.esr.common.model.person.Name;
import gov.va.med.esr.common.model.person.Person;
import gov.va.med.esr.common.model.person.SSN;
import gov.va.med.esr.common.model.person.id.PersonIdEntityKey;
import gov.va.med.esr.common.model.workload.WorkflowCaseInfo;
import gov.va.med.esr.messaging.util.MessagingWorkloadCaseHelper;
import gov.va.med.esr.service.LookupService;
import gov.va.med.esr.service.MessagingService;
import gov.va.med.esr.service.PersonIdentityTraits;
import gov.va.med.esr.service.trigger.BulletinTrigger;
import gov.va.med.esr.service.trigger.BulletinTriggerEvent;
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.hl7.MessageParser;
import gov.va.med.fw.hl7.constants.SegmentConstants;
import gov.va.med.fw.hl7.segment.PID;
import gov.va.med.fw.service.AbstractComponent;
import gov.va.med.fw.service.ServiceException;
import gov.va.med.fw.service.trigger.TriggerRouter;
import gov.va.med.fw.util.StopWatchLogger;
import gov.va.med.fw.util.StringUtils;
import gov.va.med.fw.util.builder.Builder;
import gov.va.med.fw.util.builder.BuilderException;

import java.io.Serializable;
import java.util.Date;
import java.util.Map;
import java.util.Set;

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

   public static final String SPACE = " ";
   public static final int MAX_RETRY_APP_MESSAGING_COUNT = 3;

   private Builder dateBuilder;

   private LookupService lookupService;

   private Builder messageFormatter;

   private String messageTypeCode;

   private MessagingService messagingService;

   private TriggerRouter triggerRouter;

   private Map inboundAEMessageMap;

   private MessagingWorkloadCaseHelper messagingWorkloadCaseHelper;
   
   private int retryAppMessagingCount = 0;

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

   public Builder getDateBuilder() {
      return this.dateBuilder;
   }

   public void setDateBuilder(Builder dateBuilder) {
      this.dateBuilder = dateBuilder;
   }

   public LookupService getLookupService() {
      return this.lookupService;
   }

   public void setLookupService(LookupService lookupService) {
      this.lookupService = lookupService;
   }

   public Builder getMessageFormatter() {
      return this.messageFormatter;
   }

   public void setMessageFormatter(Builder xmlFormatter) {
      this.messageFormatter = xmlFormatter;
   }

   public String getMessageTypeCode() {
      return this.messageTypeCode;
   }

   public void setMessageTypeCode(String messageTypeCode) {
      this.messageTypeCode = messageTypeCode;
   }

   public MessagingService getMessagingService() {
      return this.messagingService;
   }

   public void setMessagingService(MessagingService messagingService) {
      this.messagingService = messagingService;
   }

   /**
    * @return Returns the triggerRouter.
    */
   public TriggerRouter getTriggerRouter() {
      return triggerRouter;
   }

   /**
    * @param triggerRouter
    *           The triggerRouter to set.
    */
   public void setTriggerRouter(TriggerRouter triggerRouter) {
      this.triggerRouter = triggerRouter;
   }

   protected final Date buildDate(String value) throws BuilderException {
      return (Date)this.dateBuilder.build(value);
   }

   public Map getInboundAEMessageMap() {
      return this.inboundAEMessageMap;
   }

   public void setInboundAEMessageMap(Map inboundAEMessageMap) {
      this.inboundAEMessageMap = inboundAEMessageMap;
   }

   /**
    * Method to format a message.
    * 
    * @param message
    * @return The validated object.
    * @throws BuilderException
    */
   protected final String formatMessage(Message message) throws BuilderException {
      
      StopWatchLogger watch = null;
      if( logger.isDebugEnabled() ) {
         watch = new StopWatchLogger("AbstractMessagingService formatMessage");
         watch.start();
      }

      String formattedMesssage = doGetFormattedMessageBody(message);

      if( logger.isDebugEnabled() ) {
         String messageId = null;
         String messageType = null;
         try {
            messageId = message.getMessageID();
            messageType = message.getType();
         }
         catch( InvalidMessageException e ) {}
         if (watch != null) {
        	 watch.stopAndLog( "Total time to format and validate message with message Id "
               + messageId + " and message type " + messageType );
      	 }
      }
      return formattedMesssage;
   }
   
   /**
    * Gets the formatted message body and includes a call to the messageFormatter build method
    * which transforms the HL7 message to XML and validates it against a schema.  This method
    * was created so that child classes could override it and skip the transformation to XML.
    *
    * @param message Hibernate object for HL7 message
    *
    * @return String with the formatted message body.
    * @throws BuilderException If there errors occur in transforming or validating the message.
    */
   protected String doGetFormattedMessageBody(Message message) throws BuilderException {
       return MessageProcessServiceUtil.getFormattedMessageBody( 
               messageFormatter.build(message.getMessageData() ) );
   }

   protected final String getControlIdentifier(Message message) {
      try {
         return message.getMessageID();
      }
      catch( InvalidMessageException e ) {
         // ASSERT: Should not happen if the message is already parsed.
         throw new RuntimeException("Failed to obtain the control identifier", e);
      }
   }

   /**
    * Method to retrieve a message log by control identifier.
    * 
    * @param messageControlIdentifier
    * @return The message log.
    * @throws ServiceException
    */
   protected final MessageLogEntry getMessageLogEntry(String messageControlIdentifier)
         throws ServiceException {
      return this.messagingService.getMessageLogEntry(messageControlIdentifier);
   }

   /**
    * Return the message status.
    * 
    * @param code
    * @return The message status.
    */
   protected final MessageStatus getMessageStatus(MessageStatus.Code code) {
      try {
         return (MessageStatus)this.lookupService.getByCode(MessageStatus.class, code);
      }
      catch( ServiceException e ) {
         // ASSERT: Should not happen if the database is loaded.
         throw new RuntimeException("Could not load the message status: " + code, e);
      }
   }

   public final MessageType getMessageType() {
      try {
         return (MessageType)this.lookupService.getByCode(MessageType.class, this
               .getMessageTypeCode());
      }
      catch( ServiceException e ) {
         // ASSERT: Should not happen if the database is loaded.
         throw new RuntimeException("Could not load the message type: "
               + this.getMessageTypeCode(), e);
      }
   }

   /**
    * Method to save the acknowledgment information in a message log.
    * 
    * @param message
    * @return The message log.
    * @throws ServiceException
    */
   protected final boolean logAcknowledgment(Message message,
         MessageLogEntry initiatingMessage) throws ServiceException {
	   
	   //If no initiating message, nothing to update
      if( initiatingMessage == null ) { 
    	  return false;
      }
      
      if(initiatingMessage.getAckDate() != null) {
    	  logger.warn("Ignoring logAcknowledgment for incoming ACK/ORF for initiating MessageLogEntry with " + 
					"[control id=" + initiatingMessage.getControlIdentifier() +
					"] " +
					"because this initiating message was already ACK'ed/ORF'ed on " +
					initiatingMessage.getAckDate());    	  
    	  return false;
      }
      /*
       * Include retransmit statuses here since ESR could receive a response *way later* than we sent the
       * originating message (could even receive a response after we've retransmit or failed retransmitting).
       * 
       * These scenarios must be supported.
       */
      if(!MessageStatus.AWAITING_ACKNOWLEDGEMENT.getCode().equals(initiatingMessage.getStatus().getCode()) &&
    		  !MessageStatus.RETRANSMIT.getCode().equals(initiatingMessage.getStatus().getCode()) &&
    		  !MessageStatus.RETRANSMISSION_FAILED.getCode().equals(initiatingMessage.getStatus().getCode())) {
    	  
    	  logger.warn("Ignoring logAcknowledgment for incoming ACK/ORF for initiating MessageLogEntry with " + 
					"[control id=" + initiatingMessage.getControlIdentifier() +
				    "] " +
					"because this initiating message is not in AWAITING_ACKNOWLEDGEMENT or any Retransmit status");    	  
    	  return false;
    }

      
      String ackType = null;
      String errorText = null;
      try {
         ackType = HL7MessageUtils.getAcknowledgmentCode(message);
         errorText = HL7MessageUtils.getErrorText(message);
      }
      catch( InvalidMessageException e ) {
         throw new ServiceException("Failed to parse message due to ", e);
      }

      initiatingMessage.setErrorText(errorText);
      initiatingMessage.setAckDate(new Date());
      initiatingMessage.setAckType(getLookupService().getAckTypeByCode(
            AckType.Code.getByCode(ackType)));

      // Create cases only for AEs and Not for ARs
      if( ackType.equals(AckType.CODE_AE.getName()) ) {
         initiatingMessage.setStatus(getMessageStatus(MessageStatus.ERROR));

         try {
            // Verify the error message is in the list for creating AE/AR cases
            String errorMessage = HL7MessageUtils.getErrorMessage(message);

            if( inboundAEMessageMap != null ) {
               // Check group type EE or DQ (not used to create cases)
               String caseGroupType = (String)inboundAEMessageMap.get(errorMessage);
               if( caseGroupType != null ) {

                  WorkflowCaseInfo caseInfo = getCaseInfo(
                        initiatingMessage.getPersonId(), message, initiatingMessage,
                        MessageStatus.ERROR);
                  // set the group type
                  FunctionalGroup groupType = getLookupService()
                        .getFunctionalGroupByCode(caseGroupType);
                  caseInfo.setGroupType(groupType);
                  createWorkloadCase(caseInfo, CommonEntityKeyFactory
                        .createPersonIdEntityKey(initiatingMessage.getPersonId()
                              .toString()));
               }
            }
         }
         catch( InvalidMessageException e ) {
            throw new ServiceException(
                  "logAcknowledgment method indicates case creation but can not create trigger event");
         }
      }
      else if( ackType.equals(AckType.CODE_AR.getName()) ) {
         initiatingMessage.setStatus(getMessageStatus(MessageStatus.ERROR));
      }
      else {
         initiatingMessage.setStatus(getMessageStatus(MessageStatus.COMPLETE));
      }

      logMessage(initiatingMessage);
      return true;
   }

   protected void createWorkloadCase(WorkflowCaseInfo caseInfo,
         PersonIdEntityKey personKey) throws ServiceException {
      messagingWorkloadCaseHelper.createWorkloadCase(caseInfo, personKey);
   }

   protected WorkflowCaseInfo getCaseInfo(Serializable personId, Message message,
         MessageLogEntry referencedMessage, MessageStatus.Code status)
         throws InvalidMessageException, ServiceException {
      WorkflowCaseInfo caseInfo = new WorkflowCaseInfo();
      if( personId == null )
         throw new RuntimeException(
               "Unable to create a WorkflowMessageCaseInfo because the personId is null");

      caseInfo.setPersonEntityKey(CommonEntityKeyFactory
            .createPersonIdEntityKey(personId));

      caseInfo.setTransmissionSite(getLookupService().getVaFacilityByStationNumber(
            message.getSendingFacility()));

      caseInfo.setMessageID(message.getMessageID());

      String ackCode = HL7MessageUtils.getAcknowledgmentCode(message);
      if( ackCode != null ) {
    	  
    	  if (referencedMessage != null) {
    		  // use referenced message control id instead
    		  // ESR_CodeCR7337 -- but only if the referenced message exists. It's none existant
    		  // for a PendingIdentityTraitsWorkloadCase.
    		  caseInfo.setMessageID(referencedMessage.getControlIdentifier());
    	  }
    	  
         /*
          * yet another special case for AE/AR....since VistA is not consistent with their
          * MSH-2 value (sometimes it is stationNumber and sometimes it is 724). Same goes
          * for BHS-2. So, therefore, try one. If it is 724 (wrong number for HEC), use
          * the other one.
          */
         String sendingFacility = message.getSendingFacility();
         if( VAFacility.CODE_HEC_INCORRECT.getName().equals(sendingFacility) ) {
            sendingFacility = message.getSendingFacility(SegmentConstants.BHS);
            caseInfo.setTransmissionSite(getLookupService().getVaFacilityByStationNumber(
                  sendingFacility));
         }

         caseInfo.setCaseType(lookupService
               .getWkfCaseTypeByCode(WkfCaseType.CODE_APPLICATION_EXCEPTION.getName()));

         caseInfo.setErrorMessage(HL7MessageUtils.getErrorText(message));
      }
      return caseInfo;
   }

   /**
    * Returns the referenced MessageLogEntry
    * 
    * @param message
    * @return
    * @throws ServiceException
    */
   protected final MessageLogEntry getReferencedMessageLogEntry(Message message)
         throws ServiceException {

      try {
         String id = HL7MessageUtils.getMessageID(message);
         return id == null ? null : getMessageLogEntry(id);
      }
      catch( InvalidMessageException e ) {
         throw new ServiceException("Failed to obtain a message control ID", e);
      }
   }

   /**
    * Return the transmisssion date from the message.
    * 
    * @param message
    * @return The transmission date.
    */
   protected final Date getTransmissionDate(Message message) {

      Date date = null;
      try {
         date = buildDate(HL7MessageUtils.getTransmissionDate(message));
      }
      catch( Exception e ) {
         // Ignore to return null
      }
      return date;
   }

   /**
    * @param person
    * @param vaFacility
    * @return
    * @throws ServiceException
    */
   protected final SiteIdentity getSiteIdentity(Person person, VAFacility vaFacility)
         throws ServiceException {
      return this.messagingService.getIdentity(person, vaFacility);
   }

   /**
    * Send SolictedZ11NoMatch Bulletin for the oruz11 message
    * 
    * @param message
    */
   protected void sendSolictedZ11NoMatchBulletin(Message message) {
      if( message == null ) // message.getType().equals(Message.))
         return;
      // create bulletin (TBL 882) Solicited Z11 didn't match on the person
      try {

         BulletinTriggerEvent bte = new BulletinTriggerEvent(
               BulletinTrigger.DataType.SOLICITED_Z11_NO_MATCH);
         // get VPID from psdelegate service
         String vpid = getVPID(message);
         
         //If could not rerieve VPID from message - PID segment could be null in case of NO DATA ON FILE
         if(vpid != null)
         {
        	 bte.addField(BulletinTriggerEvent.PersonIdentityFields.VPID, getVPID(message));         
         	getTriggerRouter().processTriggerEvent(bte);
         }else 
         {
        	 try {
					throw new RuntimeException(
							"Could not create Z11Bulletin because the VPID could " +
							"not be extracted from message data for message ID: "
									+ message.getMessageID());
				} catch (InvalidMessageException e) {
					logger.error("Could not retrieve message Id for message while sending Z11 bulletin: "
							+ message.getMessageData(), e);
				}
         }
      }
      catch( ServiceException e ) {
         logger.error("InvalidMessageException", e);
      }
   }

   /**
    * Method to log the message.
    * 
    * @param logEntry
    *           The message log entry.
    */
   protected final void logMessage(MessageLogEntry logEntry) {
      try {
         this.messagingService.logMessage(logEntry);
      }
      catch( ServiceException e ) {
         if( super.logger.isErrorEnabled() ) {
            super.logger.error("Could not save to the HL7 Message Log", e);
         }

         if( super.logger.isWarnEnabled() ) {
            super.logger.warn("Message log entry: " + logEntry.toString());
         }
      }
   }

   /**
    * Fetch Person Identity Traits from PSIM (through Messaging Service)
    * 
    * @param message
    * @return
    */
   protected String getVPID(Message message) {

      PID pidSegment;
      try {
         pidSegment = (PID)message.getSegment(SegmentConstants.PID);
         
         if(pidSegment == null)
        	 return null;
         
         String lastName = null;
         String firstName = null;
         String middleName = null;

         String patientName = pidSegment.getPatientName();

         // parse name
         String[] nameElements = MessageParser.parseElement(patientName, pidSegment
               .getComponentDelimiter());
         if( nameElements != null ) {
            lastName = ( nameElements.length > 0 ) ? nameElements[0] : null;
            firstName = ( nameElements.length > 1 ) ? nameElements[1] : null;
            middleName = ( nameElements.length > 2 ) ? nameElements[2] : null;
         }
         // Parse ssn
         String SSNText = ( pidSegment.getSSN() != null ) ? pidSegment.getSSN() : null;
         // Create Person Identity Traits

         PersonIdentityTraits searchCriteriaForTraits = new PersonIdentityTraits();

         // populate name
         Name legalName = new Name();
         legalName.setFamilyName(StringUtils.trimToNull(lastName));
         legalName.setGivenName(StringUtils.trimToNull(firstName));
         legalName.setMiddleName(StringUtils.trimToNull(middleName));
         legalName
               .setType(lookupService.getNameTypeByCode(NameType.LEGAL_NAME.getName()));
         searchCriteriaForTraits.addName(legalName);

         // populate ssn ssn is always required
         SSN ssnObj = new SSN();
         ssnObj.setType(getLookupService().getSSNTypeByCode(
               SSNType.CODE_ACTIVE.getName()));
         ssnObj.setSsnText(StringUtils.trimToNull(SSNText));
         searchCriteriaForTraits.setSsn(ssnObj);

         // execute search
         Set identityTraits = getMessagingService().getMatchingTraits(
               searchCriteriaForTraits);

         if( identityTraits != null ) {
            if( identityTraits.size() > 1 ) {
               logger
                     .error("Expecting a single person identity traits from PSIM and found multiple. Traits:"
                           + searchCriteriaForTraits.toString());
            }

            if( identityTraits.size() > 0 ) {
               PersonIdentityTraits traits = (PersonIdentityTraits)identityTraits
                     .iterator().next();
               return traits.getVpid().getVPID();
            }
         }
      }
      catch( InvalidMessageException e ) {
         logger.error("Message Parsing Failed", e);
      }
      catch( ServiceException e ) {
         logger.error("Identity Traits ftech from PSIM failed", e);
      }

      return "";
   }

   /**
    * @return Returns the messagingWorkloadCaseHelper.
    */
   public MessagingWorkloadCaseHelper getMessagingWorkloadCaseHelper() {
      return messagingWorkloadCaseHelper;
   }

   /**
    * @param messagingWorkloadCaseHelper
    *           The messagingWorkloadCaseHelper to set.
    */
   public void setMessagingWorkloadCaseHelper(
         MessagingWorkloadCaseHelper messagingWorkloadCaseHelper) {
      this.messagingWorkloadCaseHelper = messagingWorkloadCaseHelper;
   } 
   
	/**
	 * Create a MessageLogEntry for the exception and specific message type
	 * 
	 * @param message
	 * @param e
	 * @param messageType
	 * @return
	 * @throws ServiceException
	 */
	protected MessageLogEntry createErrorMessageLogEntry(Message message, Throwable e, MessageType messageType)
			throws ServiceException {

		MessageLogEntry logEntry = null;
		String facility;
		
		try {
			facility = message.getSendingFacility();
		} catch (InvalidMessageException ex) {
			throw new ServiceException("Could not get Sending facility from message ", ex);
		}
		
		VAFacility vaFacilty = getLookupService().getVaFacilityByCode(facility);

		logEntry = MessageProcessServiceUtil.createMessageLog(
				getControlIdentifier(message), null, messageType,
				getMessageStatus(MessageStatus.ERROR), vaFacilty, message
						.getMessageData(), null, null,
				getTransmissionDate(message), new Date(), getLookupService()
						.getAckTypeByCode(AckType.CODE_AE), e.getMessage(),
				MessageProcessServiceUtil.formatInternalErrorText(e));

		return logEntry;

	}
	
   public int getRetryAppMessagingCount() {
	   if (this.retryAppMessagingCount <= 0) {
		   return MAX_RETRY_APP_MESSAGING_COUNT;
	   } else {
		   return this.retryAppMessagingCount;
	   }
   }

   public void setRetryAppMessagingCount(int retryAppMessagingCount) {
      this.retryAppMessagingCount = retryAppMessagingCount;
   }	
}