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

import java.util.ArrayList;
import java.util.Enumeration;

import javax.annotation.Resource;
import javax.jms.JMSException;
import javax.jms.Message;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.jdbc.core.JdbcTemplate;


/**
 * This method has all of the SQL for updating log records in the database (for the error queue cleanup process).
 * 
 * TODO Make some unit tests for this class.
 * 
 * @author DNS   brushs
 *
 */
public class ErrorQueueCleanUpLogger
{
    private static Log logger = LogFactory.getLog( ErrorQueueCleanUpLogger.class );
    private JdbcTemplate hdrJdbcTemplate;
    private long logId = 0;
    

    public void logComplete( )
    {
        StringBuilder sqlQuery = new StringBuilder();
        sqlQuery.append( "update LOGGER.cds_errorq_process_log " );
        sqlQuery.append( " set success = 'T', last_process_date = sysdate " );
        sqlQuery.append( ", num_times_processed = num_times_processed + 1 " );
        sqlQuery.append( " where errorq_process_log_id = ? " );
        
        logUpdate( sqlQuery.toString(), logId );
    }

    
    private void logUpdate( String updateString, Object... args )
    {
        logger.debug( "trying to log with SQL: " + updateString + "(" + args + ")" );
        try
        {
            hdrJdbcTemplate.update( updateString, args );
        }
        catch ( DataIntegrityViolationException e )
        {
            // unique constraint violation - only occurs from logParseInfo
            // logParseInfo will retry if it gets this exception so throw it
            logger.fatal( "DataIntegrityViolationException at database.", e );
            throw e;
        }
        catch ( Exception e )  
        {
            logger.fatal( "Could not log to database.", e );
        }
    }

    
    private void deleteLog( long aLogId )
    {
        StringBuilder sqlQuery = new StringBuilder();
        sqlQuery.append( "delete from LOGGER.cds_errorq_process_log " );
        sqlQuery.append( " where errorq_process_log_id = ?" );

        logUpdate( sqlQuery.toString(), aLogId );
    }
    
    
    private void logUpdateProperty( String... propertyNameValues )
    {
        StringBuilder sqlQuery = new StringBuilder();
        sqlQuery.append( "update LOGGER.cds_errorq_process_log " );
        sqlQuery.append( " set last_process_date = sysdate" );

        ArrayList<Object> values = new ArrayList<Object>();
        for ( int i = 0; i < propertyNameValues.length; i = i+2 )
        {
            sqlQuery.append( ", " ).append( propertyNameValues[i] ).append( " = ? " );
        }
        for ( int i = 1; i < propertyNameValues.length; i = i+2 )
        {
            values.add( propertyNameValues[i] );
        }

        sqlQuery.append( " where errorq_process_log_id = ? " );
        values.add( logId );
        
        logUpdate( sqlQuery.toString(), values.toArray() );
    }

    
    public void logError( Throwable e )
    {
        String message = ( e == null ) ? "no exception given" : e.getMessage();
        if ( message.length() > 4000 )
        {
            message = message.substring( 0, 4000 );
        }
        
        StringBuilder sqlQuery = new StringBuilder();
        sqlQuery.append( "update LOGGER.cds_errorq_process_log " );
        sqlQuery.append( " set success = 'F', last_process_date = sysdate " );
        sqlQuery.append( ", num_times_processed = num_times_processed + 1 " );
        sqlQuery.append( ", eq_process_exception_detail = ?" );
        sqlQuery.append( " where errorq_process_log_id = ?" );

        logUpdate( sqlQuery.toString(), message, logId );
    }


    public void logCdsExceptionDetail( String message )
    {
        message = ( message == null ) ? "no detail given" : message.substring(0, Math.min(message.length(), 4000));
        
        StringBuilder sqlQuery = new StringBuilder();
        sqlQuery.append( "update LOGGER.cds_errorq_process_log " );
        sqlQuery.append( " set cds_exception_detail = ?" );
        sqlQuery.append( " where errorq_process_log_id = ?" );

        logUpdate( sqlQuery.toString(), message, logId );
    }

    
    public long logStart( Message message )
    {
        
        // do something with properties
        try
        {
            logger.debug( "message dest: " + message.getJMSDestination() );
            Enumeration<?> names = message.getPropertyNames();

            while ( names.hasMoreElements() )
            {
                String name = names.nextElement().toString();
                logger.debug( "property: " + name + ", value: " + message.getObjectProperty( name ) );
            }
        }
        catch ( JMSException e )
        {
        }
        
        String messageType = message.getClass().getName();
        StringBuilder sqlQuery = new StringBuilder();
        sqlQuery.append( "insert into LOGGER.cds_errorq_process_log " );
        sqlQuery.append( " columns ( " );
        sqlQuery.append( "errorq_process_log_id, message_type, first_process_date" );
        sqlQuery.append( " ) " );
        sqlQuery.append( " values ( " );
        sqlQuery.append( "?, ?, sysdate" );
        sqlQuery.append( " ) " );

        try
        {
            logId = hdrJdbcTemplate.queryForObject( "select seq_errorq_process_log_id.nextval from dual", Long.class );
            logger.debug( "logId: " + logId );

            logger.debug( "trying to log with SQL: " + sqlQuery );
            hdrJdbcTemplate.update( sqlQuery.toString(), new Object[] { logId, messageType } );
        }
        catch ( Exception e )  
        {
            logger.fatal( "Could not log to database.", e );
        }
        
        return logId;
    }
    
    
    public void logMessageType( ca.uhn.hl7v2.model.Message message )
    {
        String messageType = ( message != null ) ? message.getClass().getName() : "null message";
        StringBuilder sqlQuery = new StringBuilder();
        sqlQuery.append( "update LOGGER.cds_errorq_process_log " );
        sqlQuery.append( " set message_type = message_type || ' / ' || ? " );
        sqlQuery.append( " where errorq_process_log_id = ?" );
        
        logUpdate( sqlQuery.toString(), messageType, logId );
    }


    public void logStyle( String style )
    {
        logUpdateProperty( "message_style", style );
    }

    
    public void addLogStyle( String style )
    {
        StringBuilder sqlQuery = new StringBuilder();
        sqlQuery.append( "update LOGGER.cds_errorq_process_log " );
        sqlQuery.append( " set message_style = message_style || ' / ' || ? " );
        sqlQuery.append( " where errorq_process_log_id = ?" );
        
        logUpdate( sqlQuery.toString(), style, logId );
    }

    
    public void logDecoded( )
    {
        logUpdateProperty( "decoded", "T" );
    }

    
    public void logExceptionType( String aValue )
    {
        if ( aValue != null ) logUpdateProperty( "exception_type", aValue );
    }

    
    public void logDuplicate( boolean isDuplicate )
    {
        logUpdateProperty( "duplicate", isDuplicate ? "Y" : "N" );
    }

    
    public void logFacility( String sendingFacilityId, String sendingFacilityName )
    {
        logUpdateProperty( "facility_id", sendingFacilityId, "facility_name", sendingFacilityName );
    }
    
    
    public void logFacilityId( String sendingFacilityId )
    {
        logUpdateProperty( "facility_id", sendingFacilityId );
    }
    
    
    public void logPsimNationalId( String nid )
    {
        StringBuilder sqlQuery = new StringBuilder();
        sqlQuery.append( "update LOGGER.cds_errorq_process_log " );
        sqlQuery.append( " set domain_name = domain_name || ' NID: ' || ? " );
        sqlQuery.append( " where errorq_process_log_id = ?" );
        
        logUpdate( sqlQuery.toString(), nid, logId );
    }
    
    
    public void logPsimLocalId( String lid )
    {
        StringBuilder sqlQuery = new StringBuilder();
        sqlQuery.append( "update LOGGER.cds_errorq_process_log " );
        sqlQuery.append( " set domain_name = domain_name || ' LID: ' || ? " );
        sqlQuery.append( " where errorq_process_log_id = ?" );
        
        logUpdate( sqlQuery.toString(), lid, logId );
    }
    
    
    public void logPsimCorrelation( String correlationStatus )
    {
        logUpdateProperty( "exception_type", correlationStatus );
    }
    
    
    public void logParseInfo( String messageControlId, String sendingApp, String messageDateTime )
    {
        logParseInfo( messageControlId, sendingApp, messageDateTime, 1 );
    }
    
    
    /**
     * In order to reduce the messages in the log table, if the messageControlId exists, update that
     * record instead of creating a new one.  Because of multi-threading at the database, there
     * could be a slight race condition, which is resolved by trying again, up to 3 times.
     * 
     * @param messageControlId
     * @param sendingApp
     * @param messageDateTime
     * @param tryNum
     */
    private void logParseInfo( String messageControlId, String sendingApp, String messageDateTime, int tryNum )
    {
        StringBuilder sqlQuery = new StringBuilder();
        sqlQuery.append( "select count(errorq_process_log_id) " );
        sqlQuery.append( " from LOGGER.cds_errorq_process_log " );
        sqlQuery.append( " where REQUEST_ID = ? and SENDING_APP = ? and MESSAGE_DATE_TIME = ?" );

        try
        {
            logger.debug( "trying to log with SQL: " + sqlQuery );
            // getting count because query fails with org.springframework.dao.EmptyResultDataAccessException: if doesn't exist
            long logCount = hdrJdbcTemplate.queryForObject( sqlQuery.toString(), new Object[] { messageControlId, sendingApp, messageDateTime } , Long.class );
            
            if ( logCount == 0 )
            {
                // new log record - threading could cause contention at database
                try
                {
                    logger.debug( "trying to log parse info - try: " + tryNum );
                    logUpdateProperty( "REQUEST_ID", messageControlId, "SENDING_APP", sendingApp, "MESSAGE_DATE_TIME", messageDateTime );
                }
                catch ( DataIntegrityViolationException e )
                {
                    if ( tryNum <= 3 )
                    {
                        logger.warn( "trying again to log parse info - try: " + tryNum );
                        logParseInfo( messageControlId, sendingApp, messageDateTime, tryNum +1 );
                    }
                    else
                    {
                        logger.fatal( "Could not log parse info after " + tryNum + " tries." );
                    }
                }
            }
            else if ( logCount == 1 )
            {
                logger.debug( "log parse info try " + tryNum + ": record already exists: (requestId, sendingApp, messageDateTime) " + messageControlId + ", " + sendingApp + ", " + messageDateTime );

                sqlQuery = new StringBuilder();
                sqlQuery.append( "select errorq_process_log_id " );
                sqlQuery.append( " from LOGGER.cds_errorq_process_log " );
                sqlQuery.append( " where REQUEST_ID = ? and SENDING_APP = ? and MESSAGE_DATE_TIME = ?" );
                long existingLogId = hdrJdbcTemplate.queryForObject( sqlQuery.toString(), new Object[] { messageControlId, sendingApp, messageDateTime }, Long.class  );
                
                deleteLog( logId );

                logId = existingLogId;
            }
            else if ( logCount > 1 )
            {
                logger.error( "multiple log records already exists: (requestId, sendingApp, messageDateTime) " + messageControlId + ", " + sendingApp + ", " + messageDateTime );

                logUpdateProperty( "REQUEST_ID", messageControlId, "SENDING_APP", sendingApp, "MESSAGE_DATE_TIME", messageDateTime ); 
            }
        }
        catch ( Exception e )  
        {
            logger.fatal( "Had some problem.", e );
        }
    }

    
    public void logRemovedFromQueue( )
    {
        logUpdateProperty( "queued_for_reprocess", "R" );
    }

    
    public void logRequeued( boolean requeued )
    {
        logUpdateProperty( "queued_for_reprocess", requeued ? "Y" : "N" );
    }
    


    
    
    // for the main running statistics
    
    public void logProcessFinish( long aProcessLogId, long messagesProcessed )
    {
        StringBuilder sqlQuery = new StringBuilder();
        sqlQuery.append( "update LOGGER.cds_eq_process_run_log " );
        sqlQuery.append( " set finish_date = sysdate " );
        sqlQuery.append( ", processed_count = ? " );
//        , num_duplicates = nd, num_success = ns, num_failed = nf" );
        sqlQuery.append( " where eq_process_run_log_id = ? " );
        
        logUpdate( sqlQuery.toString(), messagesProcessed, aProcessLogId );
    }

    
    public long logProcessStart( String configFile )
    {
        long processLogId = 0;

        StringBuilder sqlQuery = new StringBuilder();
        sqlQuery.append( "insert into LOGGER.cds_eq_process_run_log " );
        sqlQuery.append( " columns ( " );
        sqlQuery.append( "eq_process_run_log_id, config_file, start_date " );
        sqlQuery.append( " ) " );
        sqlQuery.append( " values ( " );
        sqlQuery.append( "?, ?, sysdate" );
        sqlQuery.append( " ) " );
        
        try
        {
            processLogId = hdrJdbcTemplate.queryForObject( "select seq_eq_process_run_log_id.nextval from dual", Long.class  );
            logger.debug( "process logId: " + processLogId );

            logger.debug( "trying to log with SQL: " + sqlQuery );
            hdrJdbcTemplate.update( sqlQuery.toString(), new Object[] { processLogId, configFile } );
        }
        catch ( Exception e )  
        {
            logger.fatal( "Could not log to database.", e );
        }

        return processLogId;
    }

    
    @Resource
    public void setHdrJdbcTemplate( JdbcTemplate hdrJdbcTemplate )
    {
        this.hdrJdbcTemplate = hdrJdbcTemplate;
    }


}
