

package gov.va.med.cds.tools.cleanup.errorq;


import gov.va.med.cds.tools.cleanup.errorq.exception.ErrorQCleanupException;

import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import ca.uhn.hl7v2.HL7Exception;
import ca.uhn.hl7v2.model.Message;
import ca.uhn.hl7v2.model.v24.datatype.EI;
import ca.uhn.hl7v2.model.v24.message.ORU_R01;
import ca.uhn.hl7v2.model.v24.message.RDE_O11;
import ca.uhn.hl7v2.model.v24.message.RDS_O13;
import ca.uhn.hl7v2.parser.EncodingNotSupportedException;
import ca.uhn.hl7v2.parser.PipeParser;
import ca.uhn.hl7v2.util.Terser;
import ca.uhn.hl7v2.validation.impl.ValidationContextImpl;


/**
 * 
 */

/**
 * @author susarlan
 * 
 */
public class ErrorQMessageParser
{
    private static Log logger = LogFactory.getLog( ErrorQMessageParser.class );

    private static final String T_SENDING_APPLICATION = "/MSH-3";
    private static final String T_MESSAGE_DATE_TIME = "/MSH-7";
    private static final String T_STATION_NUMBER = "/MSH-4-1";
    private static final String T_SENDING_FACILITY_ID = "/MSH-4-2";
    private static final String T_IDENTIFIER_TYPE = "/.PID-3(2)-5";
    private static final String T_ASSIGNING_FACILITY = "/.PID-3(2)-6-2";
    private static final String T_MESSAGE_CONTROL_ID = "/MSH-10";

    private Map<String, ErrorQHandlerInformation> sendingApplicationToErrorQHandlerInfoMap;
    private List<String> knownRejectableSendingAppList;
    private Map<String, String> facilityIdMap;
     

    /**
     * Used for tests
     * @param decodedHl7Message
     * @return
     */
    public String parseMessage( String decodedHl7Message )
    {
        return parseMessage( decodedHl7Message, new ErrorQueueCleanUpLogger() );
    }
    
    
    public String parseMessage( String decodedHl7Message, ErrorQueueCleanUpLogger anErrorQueueCleanUpLogger )
    {
		String returnValue;
		String sendingApp = ErrorQueueConstant.DEFAULT_MESSAGE_SENDING_APP;
		String methodName = "parseMessage( String decodedHl7Message ): ";

		logger.debug(methodName + "Begin.");
        
//		ErrorQueueThreadLocal.unset();
		ErrorQueueThreadLocal.set(sendingApp);
        
		try
        {
            //New code to check if the message from the errorq is a LAB message
        	if ( isLabMessage( decodedHl7Message ) )
            {
        		decodedHl7Message = handleEmptyLabObx5( decodedHl7Message );
            }
        	
        	
            // Parse the input request using HAPI
        	
            Message message = parseUsingHapi( decodedHl7Message );
            anErrorQueueCleanUpLogger.logMessageType( message );

            Terser terser = new Terser( message );

            String messageControlId = terser.get( T_MESSAGE_CONTROL_ID );
            sendingApp = terser.get( T_SENDING_APPLICATION );  // get the sending application info from HL7 (entry point)
           
            ErrorQueueThreadLocal.set(sendingApp);
            
            
            String messageDateTime = terser.get( T_MESSAGE_DATE_TIME );
            
            anErrorQueueCleanUpLogger.logParseInfo( messageControlId, sendingApp, messageDateTime );
            
            
            if ( logger.isDebugEnabled() )
            {
                logger.debug( methodName + " messageControlId: " + messageControlId + ", sendingApp: " + sendingApp );
            }
            
            if ( knownRejectableSendingAppList != null && knownRejectableSendingAppList.contains( sendingApp ) )
            {
                // ignore (a.k.a. remove) useless ping messages, MPI Move/Link messages, and others
                anErrorQueueCleanUpLogger.logCdsExceptionDetail( "Discarding message based on Sending Application" );
                return null;
            }
            
            
           
            // process only clinical messages in order to fix the missing assigning facility
            if ( !decodedHl7Message.contains( "MSA" ) && !sendingApp.trim().contains( "MPIF TRIGGER" ) )
            {
                decodedHl7Message = modifyHL7Msg( message, terser, messageControlId, anErrorQueueCleanUpLogger );
            }


            EntityIdentifier recordIdentifier;
            ErrorQHandlerInformation errorQHandlerInfo = sendingApplicationToErrorQHandlerInfoMap.get( sendingApp );
            
            try
            {
                if ( errorQHandlerInfo != null )
                {
                    recordIdentifier = getRecordIdentifier( message, errorQHandlerInfo.getHl7RecordIdSegment() );
                }
                else
                {
                    logger.error( methodName + "Unknown sending app (" + sendingApp + ") in sendingApplicationToErrorQHandlerInfoMap.  Unable to get record identifier for messageControlId: " + messageControlId );
                    throw new ErrorQCleanupException( "Unknown sending app (" + sendingApp + ") in sendingApplicationToErrorQHandlerInfoMap.  Unable to get record identifier for messageControlId: " + messageControlId );
                }
            }
            catch ( ErrorQCleanupException e )
            {
                throw e;
            }
            catch ( HL7Exception e )
            {
                logger.error( methodName + "HE1: unable to get record identifier for message: " + message );
                logger.error( methodName + "HE2: messageControlId: " + messageControlId + ", sendingApp: " + sendingApp );
                throw e;
            }
            catch ( RuntimeException e )
            {
                logger.error( methodName + "RE1: unable to get record identifier for message: " + message );
                logger.error( methodName + "RE2: messageControlId: " + messageControlId + ", sendingApp: " + sendingApp );
                throw e;
            }
            
            returnValue = errorQHandlerInfo.getMessageHandler().handleMessage( decodedHl7Message, errorQHandlerInfo.getHdrTableName(),
                            recordIdentifier, messageDateTime, anErrorQueueCleanUpLogger );
            

        }
        catch ( EncodingNotSupportedException ense )
        {
            anErrorQueueCleanUpLogger.logCdsExceptionDetail( decodedHl7Message );
            throw new ErrorQCleanupException( ense, ense.getMessage() );
        }
        catch ( HL7Exception e )
        {
            anErrorQueueCleanUpLogger.logCdsExceptionDetail( decodedHl7Message );
            throw new ErrorQCleanupException( e, e.getMessage() );
        }

        logger.debug( methodName + "End." );
        
        return returnValue;
    }

    
    private boolean isLabMessage( String hl7Message )
    {
    	return hl7Message.substring( 0, 15 ).contains( "LA7LAB" );
    }
        
    
    private String handleEmptyLabObx5( String hl7Message )
        throws HL7Exception
    {
        String methodName = "handleEmptyLabObx5( String hl7Message ): ";
        logger.debug( methodName + "Begin." );

        // sjb - non-java version
        // OBX\|\d*\|\|[^\|]*\|[^\|]*\|\"\"\|
        String regexPattern = "OBX\\|\\d*\\|\\|[^\\|]*\\|[^\\|]*\\|\"\"\\|"; //brings entire fragment that has all occurrences of ""

        Pattern pattern = Pattern.compile( regexPattern );
        Matcher matcher = pattern.matcher( hl7Message );

        String replacementString;
        while ( matcher.find() )
        {
            replacementString = matcher.toMatchResult().group();

            int idx = replacementString.lastIndexOf( '|' ) - 2;
            replacementString = replacementString.substring( 0, idx ) + "|";
            hl7Message = hl7Message.replaceFirst( regexPattern, replacementString );

            matcher = pattern.matcher( hl7Message );
        }

        logger.debug( methodName + "End." );

        return hl7Message;
    }


    private String handleCRinNTE( String hl7Message )
        throws HL7Exception
    {
        // sjb - non-java version
        // \r\n(NTE(.*\r\n)*?)OBX
        String regexPattern = "\\r(NTE(.*\\r(?!OBX))(.*\\r){1,}?(?=OBX))"; 
        // \r\n(NTE(.*\r\n(?!OBX))(.*\r\n){1,}?(?=OBX))  // works great no OBX in result
//        return handleCRinSegmentInMiddle( hl7Message, regexPattern );
        return handleCRinSegmentAtEnd( hl7Message, regexPattern );
    }


    private String handleCRinRXE( String hl7Message )
        throws HL7Exception
    {
        // sjb - non-java version
        // \r\n(RXE(.*\r\n)*?)RXR
        String regexPattern = "\\r(RXE(.*\\r)*?)(RXR|FT1)"; 
        
        return handleCRinSegmentInMiddle( hl7Message, regexPattern );
    }


    private String handleCRinOBX( String hl7Message )
        throws HL7Exception
    {
        // sjb - non-java version
        // OBX.*TX.COMMENT.*(\r\n.*)*
        String regexPattern = "OBX.*TX.COMMENT.*(\\r.*)*"; 
        
        return handleCRinSegmentAtEnd( hl7Message, regexPattern );
    }

    private String handleCRinSegmentAtEnd( String hl7Message, String regexPattern )
        throws HL7Exception
    {
        String methodName = "handleCRinSegmentAtEnd( String hl7Message ): ";
        logger.debug( methodName + "Begin." );
    
        Pattern pattern = Pattern.compile( regexPattern );
        Matcher matcher = pattern.matcher( hl7Message );
    
        String replacementString;
        while ( matcher.find() )
        {
            logger.info( "Found CR in segment - fixing..." );
            replacementString = matcher.toMatchResult().group( 0 );
//            int group1Start = matcher.start( 1 );
//            int group1End = matcher.end( 1 );
//            String badPart = hl7Message.substring( group1Start, group1End );
            
            String[] lines = replacementString.split( "\r" );
            StringBuilder newString = new StringBuilder( );
            for ( String string : lines )
            {
                newString.append( string ).append( ' ' );
            }
//            newString.setCharAt( newString.lastIndexOf( " " ), '\r' );
    
            hl7Message = hl7Message.replace( replacementString, newString.toString() );
            logger.info( "Found CR in segment - fixed" );
        }
    
        logger.debug( methodName + "End." );
    
        return hl7Message;
    }
    


    private String handleCRinSegmentInMiddle( String hl7Message, String regexPattern )
        throws HL7Exception
    {
        String methodName = "handleCRinSegmentInMiddle( String hl7Message ): ";
        logger.debug( methodName + "Begin." );

        Pattern pattern = Pattern.compile( regexPattern );
        Matcher matcher = pattern.matcher( hl7Message );

        String replacementString;
        while ( matcher.find() )
        {
            logger.info( "Found CR in segment - fixing..." );
            replacementString = matcher.group( 1 );
            int group1Start = matcher.start( 1 );
            int group1End = matcher.end( 1 );
            String badPart = hl7Message.substring( group1Start, group1End );
            
            String[] lines = replacementString.split( "\r" );
            StringBuilder newString = new StringBuilder( );
            for ( String string : lines )
            {
                newString.append( string ).append( ' ' );
            }
            newString.setCharAt( newString.lastIndexOf( " " ), '\r' );

            hl7Message = hl7Message.replace( badPart, newString.toString() );
//            matcher = pattern.matcher( hl7Message );
            logger.info( "Found CR in segment - fixed" );
//            matcher.reset( hl7Message );
        }

        logger.debug( methodName + "End." );

        return hl7Message;
    }


    /**
     * Parse the given decoded input HL7 request using HAPI parser
     * 
     * @param decodedHl7Request
     *            - Decoded input HL7 request
     * @throws HL7Exception 
     * @throws EncodingNotSupportedException 
     * 
     */
    private Message parseUsingHapi( String decodedHl7Request )
        throws EncodingNotSupportedException,
            HL7Exception
    {
        Message message = null;
        String methodName = "parseUsingHapi( String decodedHl7Request ): ";

        logger.debug( methodName + "Begin." );

        // Replace all /n with /r
        String request = decodedHl7Request.replace( '\n', '\r' );
        
        
        // Parse using PipeParser
        PipeParser hapiParser = new PipeParser();
        hapiParser.setValidationContext( new ValidationContextImpl() );
        try
        {
            request = handleCRinNTE( request );  // NTE to OBX (LAB)
            request = handleCRinRXE( request );  // RXE to RXR (Rx)
            request = handleCRinOBX( request );  // OBX to end (ALGY)

            message = hapiParser.parse( request );  // throws Encoding not supported on bad messages
        }
        catch ( EncodingNotSupportedException e )
        {
            // try again after fixing known possible problems with extra carriage returns
//            request = handleCRinNTE( request );  // NTE to OBX (LAB)
//            request = handleCRinRXE( request );  // RXE to RXR (Rx)
//            request = handleCRinOBX( request );  // OBX to end (ALGY)
            
            // let fail this time if not successful
            message = hapiParser.parse( request );
            
        }
//        message = hapiParser.parse( request );  // throws Encoding not supported on bad messages

        logger.debug( methodName + "End." );

        return message;
    }


    /**
     * Fix for avoiding CDS schema validation error when assigning facility id of pid is missing.
     * Populate it with sending facility id.
     * @param msg
     * @param terser
     * @return modified HL7 msg
     * @throws HL7Exception 
     */
    private String modifyHL7Msg( Message msg, Terser terser, String messageControlId, ErrorQueueCleanUpLogger anErrorQueueCleanUpLogger )
        throws HL7Exception
    {
        String sendingFacilityId = null;
        String sendingFacilityName = null;
        String assigningFacility = null;
        String messageIdentifier = null;
        String modifiedHl7Msg = null;
        String methodName = "modifyHL7Msg(Message msg, Terser terser): ";
        if(logger.isDebugEnabled()){
        	logger.debug( methodName + "Begin." );
        }

        sendingFacilityId = terser.get( T_STATION_NUMBER );
        sendingFacilityName = terser.get( T_SENDING_FACILITY_ID );
        anErrorQueueCleanUpLogger.logFacility( sendingFacilityId, sendingFacilityName );

        // fail fast
        if ( ( sendingFacilityId == null || sendingFacilityId.equals( "" ) ) && ( sendingFacilityName == null || sendingFacilityName.equals( "" ) ) )
        {
            tryToLogMessage( msg, anErrorQueueCleanUpLogger );
            throw new ErrorQCleanupException( "MSH 4.1 and MSH 4.2 in the message are empty" );
        }

        assigningFacility = terser.get( T_ASSIGNING_FACILITY );
        messageIdentifier = terser.get( T_IDENTIFIER_TYPE );
        
// TODO SJB - figure out this exception        
// java.lang.NullPointerException
//        at gov.va.med.cds.tools.cleanup.errorq.ErrorQMessageParser.modifyHL7Msg(ErrorQMessageParser.java:238)
        
        if ( ( messageIdentifier != null && messageIdentifier.equals( "PI" ) ) && ( assigningFacility == null || assigningFacility.equals( "" ) ) )
        {
            if ( ( sendingFacilityId == null || sendingFacilityId.equals( "" ) )
                            && ( sendingFacilityName != null && !sendingFacilityName.equals( "" ) ) )
            {
                logger.warn( methodName + "MSH 4.1 is empty and getting it from facilityId Map" );

                sendingFacilityId = ( String )getKeyFromValue( facilityIdMap, sendingFacilityName );

                if ( sendingFacilityId == null || sendingFacilityId.equals( "" ) )
                {
                    tryToLogMessage( msg, anErrorQueueCleanUpLogger );
                    throw new ErrorQCleanupException( "MSH 4.1 is empty and not in facilityId map and thus cannot populate the empty PID 3.6.2" );
                }
            }

            terser.set( T_ASSIGNING_FACILITY, sendingFacilityId );
        }
        else if ( ( messageIdentifier != null && messageIdentifier.equals( "PI" ) ) && ( sendingFacilityName == null || sendingFacilityName.equals( "" ) ) )
        {
            logger.warn( methodName + "MSH 4.2 is empty and getting it from facilityId Map" );

            sendingFacilityName = facilityIdMap.get( sendingFacilityId );

            // SJB
//            Check for 4.1 = "200" and pull real 4.1 from message control id and look it up and replace both.
            
            if ( sendingFacilityName == null && sendingFacilityId.equals( "200" ) )
            {
 
                try
                {
                    // this is an old north chicago message
                    sendingFacilityId = messageControlId.substring( 0, 3 );
                    sendingFacilityName = facilityIdMap.get( sendingFacilityId );
                    terser.set( T_ASSIGNING_FACILITY, sendingFacilityId );
                }
                catch ( Exception e )
                {
                }
            }
            if ( sendingFacilityName == null || sendingFacilityName.equals( "" ) )
            {
                tryToLogMessage( msg, anErrorQueueCleanUpLogger );
                throw new ErrorQCleanupException( "MSH 4.2 is empty and 4.1 (" + sendingFacilityId +") is not in facilityId map and thus cannot populate the empty MSH 4.2" );
                // CDS fails to store messages in audit clob store if there's not valid 4.1 and 4.2 in MSH
            }
            terser.set( T_SENDING_FACILITY_ID, sendingFacilityName );
        }

        PipeParser hapiParser = new PipeParser();
        hapiParser.setValidationContext( new ValidationContextImpl() );

        modifiedHl7Msg = hapiParser.encode( msg );

        if(logger.isDebugEnabled()){
        	logger.debug( methodName + "End." );
        }

        return modifiedHl7Msg;
    }

    
    private void tryToLogMessage( Message aMessage, ErrorQueueCleanUpLogger anErrorQueueCleanUpLogger )
    {
        String logMessage;
        try
        {
            PipeParser hapiParser = new PipeParser();
            hapiParser.setValidationContext( new ValidationContextImpl() );
            logMessage = hapiParser.encode( aMessage );
        }
        catch ( Exception e )
        {
            logMessage = "Exception trying to log parse message in order to log it: " + e.getMessage();
        }
        anErrorQueueCleanUpLogger.logCdsExceptionDetail( logMessage );
    }

    /**
     * Get the record identifier info from the hl7 segment OBR 3.1 to 3.4 for
     * allergies ORC 3.1 to 3.4 for Rx, Lab and Vitals
     * 
     * @param terser
     * @param hl7Segment
     * @return
     */
    private EntityIdentifier getRecordIdentifier( Message message, String hl7Segment )
        throws HL7Exception
    {
        String methodName = "getRecordIdentifier( Message message, String hl7Segment ): ";
        if( logger.isDebugEnabled()){
        	logger.debug( methodName + "Begin." );
        }

        EntityIdentifier recordIdentifier = new EntityIdentifier();

        EI ei = null;

        if ( message instanceof ORU_R01 )
        {
            ORU_R01 oruR01 = ( ORU_R01 )message;

            if ( hl7Segment.equalsIgnoreCase( "/ORC-3" ) )
            {
                ei = oruR01.getPATIENT_RESULT().getORDER_OBSERVATION().getORC().getFillerOrderNumber();
            }
            else if ( hl7Segment.equalsIgnoreCase( "/OBR-3" ) )
            {
                ei = oruR01.getPATIENT_RESULT().getORDER_OBSERVATION().getOBR().getFillerOrderNumber();
            }
        }
        else if ( message instanceof RDS_O13 )
        {
            RDS_O13 rds13 = ( RDS_O13 )message;
            ei = rds13.getORDER().getORC().getFillerOrderNumber();
        }
        else if ( message instanceof RDE_O11 )
        {
            RDE_O11 rdeO11 = ( RDE_O11 )message;
            ei = rdeO11.getORDER().getORC().getFillerOrderNumber();
        }

        if ( null != ei )
        {
            recordIdentifier.setIdentity( ei.getEntityIdentifier() != null ? ei.getEntityIdentifier().toString() : "" );
            recordIdentifier.setNamespaceId( ei.getNamespaceID() != null ? ei.getNamespaceID().toString() : "" );
            recordIdentifier.setUniversalId( ei.getUniversalID() != null ? ei.getUniversalID().toString() : "" );
            recordIdentifier.setUniversalIdType( ei.getUniversalIDType() != null ? ei.getUniversalIDType().toString() : "" );
        }
        if(logger.isDebugEnabled()){
        	logger.debug( methodName + "End." );
        }

        return recordIdentifier;
    }


    public void setSendingApplicationToErrorQHandlerInfoMap( Map<String, ErrorQHandlerInformation> sendingApplicationToErrorQHandlerInfoMap )
    {
        this.sendingApplicationToErrorQHandlerInfoMap = sendingApplicationToErrorQHandlerInfoMap;
    }



    public void setKnownRejectableSendingAppList( List<String> knownRejectableSendingAppList )
    {
        this.knownRejectableSendingAppList = knownRejectableSendingAppList;
    }



    public void setFacilityIdMap( Map<String, String> facilityIdMap )
    {
        this.facilityIdMap = facilityIdMap;
    }
    
    
    public static Object getKeyFromValue( Map<?, ?> hm, String value )
    {
        Object keyVal = null;
        for ( Object o : hm.keySet() )
        {
            if ( hm.get( o ).equals( value ) )
            {
                keyVal = o;
            }
        }
        return keyVal;
    }

}
