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

// Java Classes
import gov.va.med.esr.common.builder.datatype.metadata.CX;
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.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.Person;
import gov.va.med.esr.common.model.person.PersonLockedReason;
import gov.va.med.esr.common.model.workload.WorkflowCaseInfo;
import gov.va.med.esr.messaging.service.AbstractMessagingService;
import gov.va.med.esr.messaging.service.MessageProcessServiceUtil;
import gov.va.med.esr.messaging.xml.HL7MessageException;
import gov.va.med.esr.service.UnknownLookupCodeException;
import gov.va.med.esr.service.UnknownLookupTypeException;
import gov.va.med.fw.hl7.InvalidMessageException;
import gov.va.med.fw.hl7.Message;
import gov.va.med.fw.hl7.constants.SegmentConstants;
import gov.va.med.fw.hl7.segment.PID;
import gov.va.med.fw.service.ServiceException;
import gov.va.med.fw.util.builder.Builder;
import gov.va.med.fw.util.builder.BuilderException;
import gov.va.med.fw.validation.ValidationMessage;
import gov.va.med.fw.validation.ValidationMessages;
import gov.va.med.fw.validation.ValidationServiceException;

import java.util.Date;
import java.util.Iterator;
import java.util.ResourceBundle;

import org.apache.commons.lang.Validate;

/**
 * Abstract class that should be extended by objects providing inbound message services.
 * 
 * @author Vu Le
 * @version 1.0
 */
public abstract class AbstractInboundProcessService extends AbstractMessagingService
   implements InboundProcessService {
   
   private Builder messageParser;

   private PersonFinder personFinder;

   private Builder siteIdentityBuilder;

   private String ruleErrorsPropertiesFile;

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

   public Builder getMessageParser() {
      return this.messageParser;
   }

   public void setMessageParser(Builder messageParser) {
      this.messageParser = messageParser;
   }

   public PersonFinder getPersonFinder() {
      return this.personFinder;
   }

   public void setPersonFinder(PersonFinder personFinder) {
      this.personFinder = personFinder;
   }

   public Builder getSiteIdentityBuilder() {
      return this.siteIdentityBuilder;
   }

   public void setSiteIdentityBuilder(Builder siteIdentityBuilder) {
      this.siteIdentityBuilder = siteIdentityBuilder;
   }

   /**
    * @return Returns the ruleErrorsPropertiesFile.
    */
   public String getRuleErrorsPropertiesFile() {
      return ruleErrorsPropertiesFile;
   }

   /**
    * @param ruleErrorsPropertiesFile
    *           The ruleErrorsPropertiesFile to set.
    */
   public void setRuleErrorsPropertiesFile(String ruleErrorsPropertiesFile) {
      this.ruleErrorsPropertiesFile = ruleErrorsPropertiesFile;
   }

   protected final Person buildPerson(Person person, Message message)
      throws BuilderException {
      return (Person)this.messageParser.build(new Object[] { person, message });
   }

   protected final SiteIdentity buildSiteIdentity(Person person, Message message)
      throws BuilderException, ServiceException {
      
      try {
         VAFacility sendingFacility = this.getSendingFacility(message);
         SiteIdentity siteIdentity = super.getSiteIdentity(person, sendingFacility);
         siteIdentity = (SiteIdentity)this.siteIdentityBuilder.build(new Object[] {
               siteIdentity, message.getSegment(SegmentConstants.PID) });
         siteIdentity.setPerson(person);
         siteIdentity.setVaFacility(sendingFacility);

         return siteIdentity;
      }
      catch( InvalidMessageException e ) {
         // ASSERT: Should not happen if the message is already parsed.
         throw new RuntimeException("Failed to get the PID segment", e);
      }
   }

   protected final InboundProcessException createInboundProcessException(Message message,
         String formattedBody, Person person, String errorText, Throwable internalError,
         MessageLogEntry initiatingMessage) {
      try {
         /*
          * let's make an extra check to see if there is a ValidationServiceException or a HL7MessageException in
          * the internalError, if so, use the ValidationMessages or the root cause message in it instead of the
          * passed in errorText
          */
         if (internalError != null
					&& !(internalError instanceof ValidationServiceException)) {
				Throwable t = internalError.getCause();
				while (t != null) {
					if (t instanceof ValidationServiceException) {
						errorText = getErrorMessageFromRuleException((ValidationServiceException) t);
					} else if (t instanceof HL7MessageException) {
						errorText = t.getMessage();
					}
					t = t.getCause();
				}
			}
         else if( internalError instanceof ValidationServiceException ) {
            errorText = getErrorMessageFromRuleException((ValidationServiceException)internalError);
         }
         return new InboundProcessException(this.createMessageLog(message, formattedBody,
               person, errorText, internalError, initiatingMessage), errorText,
               internalError);
      }
      catch( ServiceException e ) {
         if( logger.isErrorEnabled() ) {
            logger.error("Error while trying to create an InboundProcessException");
            logger.error("Original exception", internalError);
            logger.error("Caught exception", e);
         }

         throw new RuntimeException("Unable to create an InboundProcessException", e);
      }
      catch( RuntimeException e ) {
         if( logger.isErrorEnabled() ) {
            logger.error("Error while trying to create an InboundProcessException");
            logger.error("Original exception", internalError);
            logger.error("Caught exception", e);
         }

         throw e;
      }
   }

   private String getErrorMessageFromRuleException(ValidationServiceException ex) {
      StringBuffer errorMessages = new StringBuffer();
      ValidationMessages msgs = ex.getValidationMessages();
      Iterator itr = msgs.get();

      ResourceBundle bundle = ResourceBundle.getBundle(getRuleErrorsPropertiesFile());

      while( itr != null && itr.hasNext() ) {
         ValidationMessage msg = (ValidationMessage)itr.next();

         String errorMsg;
         try {
            errorMsg = bundle.getString(msg.getKey());
         }
         catch( Exception e ) {
            // In case of an exception, just use the key instead of
            // throwing up an exception
            errorMsg = msg.getKey();
         }

         errorMessages.append(errorMsg).append(" ");
      }
      return errorMessages.toString();

   }

   protected final InboundProcessException createInboundProcessException(Message message,
         String formattedBody, Person person, String errorText, Throwable internalError) {
      return this.createInboundProcessException(message, formattedBody, person,
            errorText, internalError, null);
   }

   protected final MessageLogEntry createMessageLog(Message message,
         String formattedBody, Person person, MessageLogEntry initiatingMessage)
         throws ServiceException {
      return this.createMessageLog(message, formattedBody, super
            .getMessageStatus(MessageStatus.COMPLETE), person, AckType.CODE_AA, null,
            null, initiatingMessage);
   }

   /**
    * Method to create a message log.
    * 
    * @param message
    * @param formattedBody
    * @param person
    * @return The message log.
    */
   protected final MessageLogEntry createMessageLog(Message message,
         String formattedBody, Person person) throws ServiceException {
      return this.createMessageLog(message, formattedBody, person, null);
   }

   protected final MessageLogEntry createMessageLog(Message message,
         String formattedBody, Person person, String errorText, Throwable internalError,
         MessageLogEntry initiatingMessage) throws ServiceException {

      if( this.logger.isDebugEnabled() ) {
         String error = internalError != null ? internalError.getClass().getName() : null;
         this.logger.debug("Creating message log entry for exception: " + error);
         this.logger.debug("Cause exception: ", internalError);
      }

      AckType.Code ackType = ( ( errorText != null ) && errorText.substring(0, 2).equals(
            AckType.CODE_AR.getName()) ) ? AckType.CODE_AR : AckType.CODE_AE;

      return this.createMessageLog(message, formattedBody, super
            .getMessageStatus(MessageStatus.ERROR), person, ackType, errorText,
            MessageProcessServiceUtil.formatInternalErrorText(internalError),
            initiatingMessage);
   }

   /**
    * Method to create a message log.
    * 
    * @param message
    * @param formattedBody
    * @param person
    * @param errorText
    * @param internalError
    * @return The message log.
    */
   protected final MessageLogEntry createMessageLog(Message message,
         String formattedBody, Person person, String errorText, Throwable internalError) {
      return this.createMessageLog(message, formattedBody, person, errorText, null);
   }

   protected abstract MessageLogEntry doProcessMessage(Person person, Message message,
         String formattedBody) throws InboundProcessException;

   protected final Person findPerson(Message message) throws ServiceException {
      Person person = personFinder.find(message);
      if( person == null ) {
         String messageId = "<Unable to determine Message Id>";
         try {
            messageId = message.getMessageID();
         }
         catch( InvalidMessageException e ) {
            // ignore, no need to compound the issue
         }
         throw new ServiceException("Unable to retrieve Person for Message ID: "
               + messageId);
      }
      return person;
   }

   /**
    * Return the sending facility.
    * 
    * @param message
    * @return The sending facility.
    * @throws ServiceException
    */
   protected final VAFacility getSendingFacility(Message message) throws ServiceException {
      String stationNumber = this.getSendingFacilityStationNumber(message);

      return ( stationNumber == null ) ? null : this.getLookupService()
            .getVaFacilityByStationNumber(stationNumber);
   }

   /**
    * Return the sending facility station number.
    * 
    * @param message
    * @return The sending facility station number.
    */
   protected final String getSendingFacilityStationNumber(Message message) {
      try {
         return message.getSendingFacility();
      }
      catch( InvalidMessageException e ) {
         // ASSERT: Should not happen if the message is already parsed.
         throw new RuntimeException(
               "Failed to obtain the station number from the MSH segment", e);
      }
   }

   public MessageLogEntry processMessage(Message message) throws InboundProcessException {
      
      Validate.notNull(message, "Message cannot be null");
      Person person = null;
      String formattedBody = null;

 	 try {
 	 	 StringBuffer logMessage = 
 			 new StringBuffer("Process message type ").append(message.getType()).append(" ID: ").append(message.getMessageID());

    	 if( logger.isInfoEnabled() ) {
	        logger.info(logMessage);
	     }

    	 //CCR11522 temporary removal of validation
         //formattedBody = super.formatMessage(message);
    	 // CCR 11547 turn it back on since xml formatter is now synchronized
    	 formattedBody = super.formatMessage(message);

         person = findPerson(message);

         if( person == null ) {
            throw new IllegalStateException("Unable to process message "
                  + message.getMessageType() + " since unknown Person");
         }
         
         
         if (person.isPersonLocked()
  	         && ( PersonLockedReason.PENDING_IDENTITY_TRAIT_UPDATES.getReason()
  	               .equals(person.getPersonLockedReason().getReason()))) {
  		   return processPendingTraits(person, message);
  	   	}
        //CCR 11776 -checks for deprecated messages and skips the processing when a deprecated identity traits is found for a dfn and station number.  
        //Logs the message in the log file.
        else if (person.isPersonLocked()
     	         && ( PersonLockedReason.PERSON_DEPRECATED.getReason()
     	  	               .equals(person.getPersonLockedReason().getReason()))) {
     	   return processDeprecatedTraits(person, message);
        }	   	 
  		else {
  			return this.doProcessMessage(person, message, formattedBody);
  		}
      }
      catch( Exception e ) {
         if( !( e instanceof InboundProcessException ) ) {
            throw this.createInboundProcessException( 
            		message, formattedBody, null, e.getMessage(), e );
         }
         else {
            throw (InboundProcessException)e;
         }
      }
   }
   
   // CCR11575 -- moved the logic into separate method to allow subclass override
   protected MessageLogEntry processPendingTraits(Person person, Message message) throws Exception 
   {
	   // Before we allow processing to continue, we need to make sure
	   // there is no "pending" updates to identity traits for this person. if so,
	   // it is AE scenario.
	   
	   // Create a DQ Workload case
		  createPendingIdentityTraitsWorkloadCase(message, person);
		
		  throw this.createInboundProcessException(message, null, person,
		        "AE: Target Person is locked because of reason '"
		          + person.getPersonLockedReason()
		          + "' so the message must be rejected with an AE.", null);
   }

   
   //CCR 11776 - skip the messages and create a log entry for deprecated dfn and station number
   protected MessageLogEntry processDeprecatedTraits(Person person, Message message) throws Exception {
		InboundProcessException e = this.createInboundProcessException(message, null, person,
		        "AE: Ignoring inbound message.  Target Person is locked because of deprecated DFN and station number", null);
		
		// Create a message log entry for the processed person
		MessageLogEntry mle = createMessageLog(message, null, person, e.getMessage(), e, getReferencedMessageLogEntry(message));
		return mle; 
	}
   

   /**
    * Get DFN from PID-3 segment. PID-3 is a CX datatype and DFN data has the "PI"
    * Identifier type.
    * 
    * @param pid
    * @return dfn value as String
    */
   protected String getDfnFromPID(PID pid) {
      CX[] cxs = CX.create(pid.getPatientIdentifierList(), pid.getRepeatDelimiter(), pid
            .getComponentDelimiter(), pid.getSubComponentDelimiter());

      String dfn = CX.getIdentifierByIdentifierType(cxs,
            SegmentConstants.IDENTIFIER_TYPE_PI);

      return dfn;
   }

   /**
    * Method to create a message log.
    * 
    * @param message
    * @param formattedBody
    * @param status
    * @param person
    * @param ackType
    * @param errorText
    * @param internalErrorText
    * @return The valid HL7 message log.
    */
   private final MessageLogEntry createMessageLog(Message message, String formattedBody,
         MessageStatus status, Person person, AckType.Code ackType, String errorText,
         String internalErrorText, MessageLogEntry initiatingMessage)
         throws ServiceException {
      VAFacility vaFacility = null;

      try {
         vaFacility = this.getSendingFacility(message);
      }
      catch( ServiceException e ) {
         this.logger.debug("Could not get the sending facility to log", e);
      }
      MessageLogEntry logEntry = MessageProcessServiceUtil.createMessageLog(super
            .getControlIdentifier(message), null, super.getMessageType(), status,
            vaFacility, message.getMessageData(), formattedBody, person, super
                  .getTransmissionDate(message), new Date(), getLookupService()
                  .getAckTypeByCode(ackType), errorText, internalErrorText);
      logEntry.setInitiatingMessage(initiatingMessage);
      return logEntry;
   }

   /**
    * @param message
    * @param person
    * @throws InvalidMessageException
    * @throws ServiceException
    * @throws UnknownLookupTypeException
    * @throws UnknownLookupCodeException
    */
   private void createPendingIdentityTraitsWorkloadCase(Message message, Person person)
         throws InvalidMessageException, ServiceException, UnknownLookupTypeException,
         UnknownLookupCodeException {
      // Create a DQ Workload case
      WorkflowCaseInfo caseInfo = getCaseInfo(person.getPersonEntityKey()
            .getKeyValueAsString(), message, null, MessageStatus.ERROR);
      // set the group type
      FunctionalGroup groupType = getLookupService().getFunctionalGroupByCode(
            FunctionalGroup.DQ.getCode());
      caseInfo.setGroupType(groupType);

      // There is no lookup code for Issue Type = Pending Identity Traits. So set the
      // error message.
      caseInfo.setErrorMessage("Identity Update Pending");
      // TODO WHAT IS THE CASE TYPE?
      caseInfo.setCaseType(getLookupService().getWkfCaseTypeByCode(
            WkfCaseType.CODE_APPLICATION_EXCEPTION.getName()));
      createWorkloadCase(caseInfo, CommonEntityKeyFactory.createPersonIdEntityKey(person
            .getPersonEntityKey().getKeyValueAsString()));
   }
}