/**
 * 
 */


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


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

import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;

import javax.annotation.Resource;
import javax.jms.BytesMessage;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.Session;
import javax.jms.TextMessage;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.dao.DataAccessException;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.core.MessageCreator;

import ca.uhn.hl7v2.HL7Exception;
import ca.uhn.hl7v2.parser.EncodingNotSupportedException;


public abstract class AbstractErrorQListener
    implements
        MessageListener, BeanFactoryAware
{
    private static Log logger = LogFactory.getLog( AbstractErrorQListener.class );

    private ErrorQMessageParser messageParser;
    protected JmsTemplate jmsTemplate;
    private Map<String, String> exceptionsMap;
    private Map<String, String> reprocessRejectionMap;
    private List<String> reprocessList;
    private List<String> removeFromQueueList;
    private BeanFactory beanFactory; // this is used to get instances of the logger
    private AtomicCounter messageCounter;
    
    protected static final String TEMPLATE_ID = "TEMPLATE_ID";
    protected static final String REQUEST_ID = "REQUEST_ID";
    protected static final String NO_TEMPLATE_ID_PROVIDED = "NO_TEMPLATE_ID";
    protected static final String NO_REQUEST_ID_PROVIDED = "NO_REQUEST_ID";
    
    
 	protected String templateId = null;
	protected String requestId = null;
	protected int jmsMessagePriority = -1;
	
 
    public void onMessage( Message message )
    {
        messageCounter.incrementCount();
        
        byte[] messageBytes = null;
        String decodedMessage = null;
        String jmsMessageId = null;
        String methodName = "onMessage(Message message): ";
        if( logger.isDebugEnabled()){
        	 logger.debug( methodName + "Begin." );
        }

        ErrorQueueCleanUpLogger errorQueueCleanUpLogger = ( ErrorQueueCleanUpLogger )beanFactory.getBean("errorQueueCleanUpLogger"); 
        errorQueueCleanUpLogger.logStart( message );

        try
        {
            if ( message instanceof BytesMessage )
            {
                BytesMessage bytesMsg = ( BytesMessage )message;
                int msgLength = ( int )bytesMsg.getBodyLength();
                messageBytes = new byte[msgLength];
                bytesMsg.readBytes( messageBytes );
            }
            else if ( message instanceof TextMessage )
            {
                TextMessage textMsg = ( TextMessage )message;
                messageBytes = textMsg.getText().getBytes();
            }
            else
            {
                logger.fatal( methodName + "Message is not of type BytesMessage or TextMessage" );
                throw new ErrorQCleanupException( "Unknown message type" );
            }

            jmsMessageId = message.getJMSCorrelationID();
            templateId = message.getStringProperty( TEMPLATE_ID );
            requestId = message.getStringProperty( REQUEST_ID );
            templateId = (templateId != null) ? templateId : NO_TEMPLATE_ID_PROVIDED;
            requestId = (requestId != null) ? requestId : NO_REQUEST_ID_PROVIDED;
            decodedMessage = decodeMessage( messageBytes );
            errorQueueCleanUpLogger.logDecoded( );

            // Graph messages are Patient Correlation Messages
            if ( decodedMessage.contains( "<graph>" ) )
            {
                // log message type of graph
                errorQueueCleanUpLogger.logStyle( "graph" );
                errorQueueCleanUpLogger.logCdsExceptionDetail( "Discarding MPI correlation message" );
            }
            else
            {
                // parse message for handling by clinical or non-clinical message handler
                errorQueueCleanUpLogger.logStyle( "regular" );
                messageParser.parseMessage( decodedMessage, errorQueueCleanUpLogger );
            }
            errorQueueCleanUpLogger.logComplete( );
        }
        catch ( ErrorQCleanupException e )
        {
            logger.error( "ErrorQCleanupException occurred for the message ", e );
            errorQueueCleanUpLogger.logError( e );
            errorQueueCleanUpLogger.logExceptionType( jmsMessageId );
            processApplicationException( message, new String( messageBytes ), e, errorQueueCleanUpLogger );
        }
        catch ( RuntimeException e )
        {
            logger.error( "RuntimeException occurred for the message ", e );
            errorQueueCleanUpLogger.logError( e );
            sendLowPriorityMessageForReprocessing( new String( messageBytes ), errorQueueCleanUpLogger );
        }
        catch ( Exception e )
        {
            logger.error( "Exception occurred for the message ", e );
            errorQueueCleanUpLogger.logError( e );
            sendLowPriorityMessageForReprocessing( new String( messageBytes ), errorQueueCleanUpLogger);
        }
        catch ( Error e )
        {
            logger.error( "Error occurred for the message ", e );
            errorQueueCleanUpLogger.logError( e );
            sendLowPriorityMessageForReprocessing( new String( messageBytes ), errorQueueCleanUpLogger);
            throw e;
        }
        catch ( Throwable e )
        {
            logger.error( "Throwable occurred for the message ", e );
            errorQueueCleanUpLogger.logError( e );
            sendLowPriorityMessageForReprocessing( new String( messageBytes ), errorQueueCleanUpLogger);
            throw new Error( e );
        }

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



    /**
     * Used to populate the failure reason in the failed jms Message in case of application exceptions (errorq/message mediator/cds) 
     * to stop reprocessing. 
     * Reprocessing is done in case of system exceptions.
     * Only incorrect message format and errorq related exceptions information is stored in the exceptionsmap since the other
     *  cds/message mediator exceptions are runtime based and too descriptive to get a concise level of detail.
     * Thus in such cases, need to look in the cds application log table for more details 
     * about the cause(s) based on the request id in the failure reason.
     * 
     * @param rte
     * @return
     */
    private void processApplicationException( Message message, String msg, RuntimeException rte, ErrorQueueCleanUpLogger errorQueueCleanUpLogger)
    {
        String exceptionMsg = null;
        String runtimeExceptionMessage = rte.getMessage();
        boolean messageProcessed = false;
        
        
        if ( rte.getCause() instanceof ResponseException ) //exceptions from Message Mediator/CDS
        {
        	if( logger.isDebugEnabled()){
        		logger.debug( "processApplicationException - ResponseException." );
        	}
            
            for ( String token : removeFromQueueList ) 
            {
                if ( runtimeExceptionMessage.contains( token ) ) 
                {
                    // do not requeue
                	if( logger.isDebugEnabled()){
                		logger.debug( "Removing message from queue due to: " + token );
                	}
                    errorQueueCleanUpLogger.logRemovedFromQueue();
                    messageProcessed = true;
                    break;
                }
            }
            

            if ( ! messageProcessed ) 
            {
            	for ( String tokenKey : reprocessRejectionMap.keySet() ) 
            	{
            		if ( runtimeExceptionMessage.contains( tokenKey ) )
            		{
            			sendWithConversionToPreventReprocessing( message, reprocessRejectionMap.get(tokenKey), errorQueueCleanUpLogger);
                        if(logger.isDebugEnabled()){
                        	logger.debug( "Replaced message on queue as processed with error due to: " + tokenKey );
                        }
            			messageProcessed = true;
            			break;
            		}	
    			}
            }
            
        	// grr i hate all these nested if blocks

			if ( ! messageProcessed ) 
			{
				for ( String token : reprocessList ) 
				{
					if ( runtimeExceptionMessage.contains( token ) ) 
					{
						sendLowPriorityMessageForReprocessing( msg, errorQueueCleanUpLogger);
                        if( logger.isDebugEnabled()){
                        	logger.debug( "Replaced message on queue for reprocessing due to: " + token );
                        }
                        messageProcessed = true;
						break;
					}
				}
			}

			if ( ! messageProcessed )
            {
                if ( runtimeExceptionMessage.contains( "HL7_SUPPORT_HAPI_ENCODE_EXCEPTION" )
                                || runtimeExceptionMessage.contains( "ca.uhn.hl7v2.HL7Exception: Can't encode group" )
                                || runtimeExceptionMessage.contains( "gov.va.med.cds.hapi.HL7SupportException" ) )
                {
                    exceptionMsg = exceptionsMap.get( "HL7_SUPPORT_HAPI_ENCODE_EXCEPTION" );
                }
                else if ( runtimeExceptionMessage.contains( "gov.va.med.cds.xml.schema.SchemaValidationException" )
                                || runtimeExceptionMessage.contains( "SCHEMA_VALIDATION_FAILED" ) )
                {
                    exceptionMsg = exceptionsMap.get( "SchemaValidationException" );
                }
                else if ( runtimeExceptionMessage.contains( "HDRII_OPERATION_FAILED" ) )
                {
                    exceptionMsg = exceptionsMap.get( "HDRII_OPERATION_FAILED" );
                }
                else
                {
                    if ( runtimeExceptionMessage.contains( "Unable to process message" ) && runtimeExceptionMessage.contains( "ErrorCode" )
                                    && runtimeExceptionMessage.contains( "DisplayMessage" ) )
                    {
                        exceptionMsg = runtimeExceptionMessage.substring( runtimeExceptionMessage.indexOf( "ErrorCode" ), runtimeExceptionMessage.indexOf( "DisplayMessage" ) );
                    }
                    else
                    {
                        exceptionMsg = "Failure to process the message in CDS or message mediator. Look for the cause in cds application log table";
                    }

                }
                sendWithConversionToPreventReprocessing( message, exceptionMsg, errorQueueCleanUpLogger);
            }

        }
        else
        //exceptions from errorq cleanup code
        {
            exceptionMsg = exceptionsMap.get( runtimeExceptionMessage );

            if ( exceptionMsg != null && !exceptionMsg.equals( "" ) )
            {
                sendWithConversionToPreventReprocessing( message, exceptionMsg, errorQueueCleanUpLogger);
            }
            else
            {
                if ( rte.getCause() instanceof EncodingNotSupportedException )
                {
                    exceptionMsg = exceptionsMap.get( "EncodingNotSupportedException" );
                    sendWithConversionToPreventReprocessing( message, exceptionMsg, errorQueueCleanUpLogger);
                }
                else if ( rte.getCause() instanceof HL7Exception )
                {
                    exceptionMsg = exceptionsMap.get( "HL7Exception" );
                    sendWithConversionToPreventReprocessing( message, exceptionMsg, errorQueueCleanUpLogger);
                }
                else if ( rte.getCause() instanceof DataAccessException )
                {
                    exceptionMsg = exceptionsMap.get( "DataAccessException" );
                    sendWithConversionToPreventReprocessing( message, exceptionMsg, errorQueueCleanUpLogger);
                }
                else
                {
                    exceptionMsg = "Failure to process the message in ErrorQ cleanup process: " + runtimeExceptionMessage ;
                    sendWithConversionToPreventReprocessing( message, exceptionMsg, errorQueueCleanUpLogger);
                }
            }
        }
    }


    private String decodeMessage( byte[] messageBytes )
    {
        String methodName = "decodeMessage( byte[] messageBytes )";
        String decodedMessage;

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

        // remove non-printable characters from  HL7 messages only 
        ByteBuffer messageBuffer = ByteBuffer.wrap( messageBytes );
        messageBuffer = MllpUtil.decode( new ByteBuffer[] { messageBuffer }, true, Charset.forName( "ISO-8859-1" ), Charset.defaultCharset() );

        if ( messageBuffer != null )
        {
            decodedMessage = new String( messageBuffer.array() );
        }
        else
        {
            decodedMessage = new String( ByteBuffer.wrap( messageBytes ).array() );
        }

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

        return decodedMessage;
    }


    /**
     * Used in case of application exceptions and thus avoiding the reprocessing of the failed message
     * by setting a flag that the message is already processed along with failure reason.
     * @param message
     * @param reason
     */
    abstract void sendWithConversionToPreventReprocessing( Message message, final String reason, ErrorQueueCleanUpLogger errorQueueCleanUpLogger);


    /**
     * Used in the case of system exceptions and error scenarios.
     * Puts the failed message back on the errorq for reprocessing.
     * @param strMessage
     */
    private void sendLowPriorityMessageForReprocessing( final String strMessage, ErrorQueueCleanUpLogger errorQueueCleanUpLogger)
    {
    	final String s = strMessage;
    	final String sendingApp = ErrorQueueThreadLocal.get();
    	//this.jmsTemplate.setExplicitQosEnabled(true);
    	this.jmsTemplate.setPriority(getJmsMessagePriority());
        

        this.jmsTemplate.send( new MessageCreator()
        {
            public javax.jms.Message createMessage( Session session )
                throws JMSException
            {
                javax.jms.Message message = session.createTextMessage( s );
                message.clearProperties();
                message.setStringProperty( TEMPLATE_ID, templateId );
                message.setStringProperty( REQUEST_ID, requestId );
                message.setStringProperty(ErrorQueueConstant.SENDING_APP, sendingApp);
  //              message.setJMSPriority( 0 );
                return message;
            }
        } );
        
        errorQueueCleanUpLogger.logRequeued( true );
    }

    protected int getJmsMessagePriority() {
    	if( jmsMessagePriority == -1){
    		try {
				String str = System.getProperty(ErrorQueueConstant.MESSAGE_PRIORITY);
				jmsMessagePriority = Integer.parseInt(str);
			} catch (NumberFormatException e) {
				logger.error("JMS Message Priority is not found in command line");
				jmsMessagePriority = ErrorQueueConstant.JMS_MESSAGE_DEFAULT_PRIORITY;
			}
    	}
    		
		return jmsMessagePriority;
	}
    @Resource
    public void setReprocessRejectionMap( Map<String, String> reprocessRejectionMap )
    {
        this.reprocessRejectionMap = reprocessRejectionMap;
    }


    public void setReprocessList( List<String> reprocessList )
    {
        this.reprocessList = reprocessList;
    }


    public void setRemoveFromQueueList( List<String> removeFromQueueList )
    {
        this.removeFromQueueList = removeFromQueueList;
    }

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


    public void setJmsTemplate( JmsTemplate jmsTemplate )
    {
        this.jmsTemplate = jmsTemplate;
    }


    public void setExceptionsMap( Map<String, String> exceptionsMap )
    {
        this.exceptionsMap = exceptionsMap;
    }


    public void setBeanFactory( BeanFactory beanFactory )
        throws BeansException
    {
        this.beanFactory = beanFactory;
    }


    public void setMessageCounter( AtomicCounter messageCounter )
    {
        this.messageCounter = messageCounter;
    }
}
