/**
 * 
 */


package gov.va.med.cds.persistence.hibernate;


import gov.va.med.cds.exception.ErrorCodeEnum;
import gov.va.med.cds.filter.EntryFilterInterface;
import gov.va.med.cds.filter.ParameterMapInterface;
import gov.va.med.cds.filter.QueryParameter;
import gov.va.med.cds.persistence.PersistenceException;
import gov.va.med.cds.persistence.QueryAssociationInterface;
import gov.va.med.cds.persistence.ReadException;
import gov.va.med.cds.persistence.hibernate.rpc.RpcParamStringBuilderInterface;
import gov.va.med.cds.response.strategy.VistaGenericStoredProcedureSingleResponseStrategyInterface;
import gov.va.med.cds.util.CipherUtilities;
import gov.va.med.cds.util.TimeoutUtil;

import java.net.SocketException;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;

import javax.sql.DataSource;

import org.hibernate.Session;
import org.springframework.orm.hibernate4.SessionFactoryUtils;
import org.springframework.util.StringUtils;


/**
 * @author DNS   egberb
 * 
 */
public class VistaGenericStoredProcedureQueryWork
    extends
        VistaStoredProcedureQueryWork
{

    private static final String STORED_PROCEDURE_NAME = "GenericObservationRead1.GSP";

    // Must match one of the predefined list of parameters. We will let the VistA system  validate the value so that CDS is not tightly couple 
    //to the allowable parameters.
    private static final String DOMAIN = "domain";

    // Date range used by the M API to constrain the results. Must be in format ????????.
    private static final String START = "start";

    private static final String STOP = "stop";

    private static final String RECORD_ID = "id";

    private static final String TEXT = "text";
    private static final String TEXT_TRUE = "true";
    private static final String TEXT_FALSE = "false";
    private static final String SHOW_DOCUMENTS = "1";
    private static final String NO_DOCUMENTS = "0";

    private static final byte[] KEY = { 104, 122, 80, 48, 57, 111, 76, 80, 50, 48, 79, 50, 75, 99, 66, 82 };

    private static final String CACHE_DATE_FORMAT = "yyyyMMddHHmmss";

    private static final String ACCESS_LEVEL = "3X";

    private static final byte[] EKEY = { 83, -88, 66, 69, 29, -48, 35, -9, 46, 117, -61, -13, 48, -85, 32, -65 };

    private String username; // injected through query strategy class.

    private String password; // encrypted and encoded and injected through query strategy class.

    private VistaGenericStoredProcedureSingleResponseStrategyInterface vistaGenericResponseStrategy;

    private RpcParamStringBuilderInterface rpcBuilder;


    /**
     * @param session
     * @param queryAssociation
     * @param entryFilter
     * @param personIdentifiers
     * @param parameterTransformerMap
     * @param queryMap
     * @param vistaResponseStrategy
     * @param applicationName
     * @param siteId
     */
    public VistaGenericStoredProcedureQueryWork( Session session, QueryAssociationInterface queryAssociation, EntryFilterInterface entryFilter,
                    List<String> personIdentifiers, Map<String, QueryParameterTransformerInterface> parameterTransformerMap,
                    Map<String, String> queryMap, VistaGenericStoredProcedureSingleResponseStrategyInterface vistaGenericResponseStrategy,
                    String applicationName, String siteId, String userName, String password, RpcParamStringBuilderInterface rpcBuilder )
    {
        super( session, queryAssociation, entryFilter, personIdentifiers, parameterTransformerMap, queryMap, null, applicationName, siteId );

        setUsername( userName );
        setPassword( password );
        setRpcBuilder( rpcBuilder );
        setVistaGenericResponseStrategy( vistaGenericResponseStrategy );

    }


    @Override
    protected String buildQueryString( String associationName, EntryFilterInterface aEntryFilter, List<String> personIdentifiers )
        throws ReadException
    {

        return this.queryMap.get( STORED_PROCEDURE_NAME );
    }


    @Override
    protected List<String> buildQueryParameterList( String query, String associationName, EntryFilterInterface aEntryFilter,
                    List<String> personIdentifiers )
    {

        List<String> spParameters = new ArrayList<String>();

        // Add the user to the list of params
        spParameters.add( this.username );

        // Decrypt the password and add timestamp and access level and encrpt it. Add the encrypted / base64 encoded password to the list of params
        try
        {

            SimpleDateFormat sdf = new SimpleDateFormat( CACHE_DATE_FORMAT );
            sdf.setTimeZone( TimeZone.getTimeZone( "GMT" ) );
            String datePlain = sdf.format( new Date() );

            //String decryptedPasswd = CipherUtilities.decrypt( EKEY, password );
            String decryptedPasswd = CipherUtilities.decrypt( EKEY, new byte[16], password );//change for fortify fix against CipherUtilities
            //String newEncryptPassword = CipherUtilities.encrypt( ACCESS_LEVEL + datePlain + decryptedPasswd, KEY);
            String newEncryptPassword = CipherUtilities.encrypt( ACCESS_LEVEL + datePlain + decryptedPasswd, KEY, new byte[16] );//change for fortify fix against CipherUtilities
          
            spParameters.add( newEncryptPassword );

        }
        catch ( Exception e )
        {
            throw new PersistenceException( ErrorCodeEnum.ROOT_CAUSE_MSG, e, "Error creating encrypted timestamp required by GSP." );
        }

        // Add the RPC naem to the parameters list.
        String rpcName = this.rpcBuilder.getRpcName();
        if ( rpcName == null | "".equals( rpcName ) )
        {
            throw new PersistenceException( ErrorCodeEnum.INVALID_OR_UNEXPECTED_QUERY_PARAM, "RPC NAME" );
        }
        spParameters.add( rpcName );

        // Map used to generate the M API parameter string.
        Map<String, String> mApiParams = new HashMap<String, String>();

        // extract patient identifier parameter from filter XML
        String patientArgName = rpcBuilder.getRpcPatientArgName();
        if ( patientArgName != null )
        {
            //add parameter when rpcBuilder returns a patient arg name (updates rpc will not use this param) - otherwise omit from parameter string
            mApiParams.put( patientArgName, extractVistaPatientIdentifier( personIdentifiers ) );
        }

        // extract start and end date filter parameters from filter XML. Only include them if there is a non-null date value.
        String dateRangeStart = extractVistaDateFromFilter( aEntryFilter.getStartDate(), DEFAULT_START_DATE );
        String dateRangeEnd = extractVistaDateFromFilter( aEntryFilter.getEndDate(), DEFAULT_END_DATE );

        if ( dateRangeEnd != null && !"".equals( dateRangeEnd ) )
        {
            mApiParams.put( STOP, dateRangeEnd );
        }

        if ( dateRangeStart != null && !"".equals( dateRangeStart ) )
        {
            mApiParams.put( START, dateRangeStart );
        }

        // extract domain from the domainEntryPoint filter element 
        if ( aEntryFilter.getDomainEntryPoint() == null || "".equals( aEntryFilter.getDomainEntryPoint() ) )
        {
            throw new PersistenceException( ErrorCodeEnum.INVALID_OR_UNEXPECTED_QUERY_PARAM, DOMAIN );
        }

        mApiParams.put( DOMAIN, aEntryFilter.getDomainEntryPoint() );

        // extract the record identifier element from the filter XML
        if ( aEntryFilter.getRecordIdentifiers() != null )
        {
            if ( aEntryFilter.getRecordIdentifiers().size() > 1 )
            {
                throw new PersistenceException( ErrorCodeEnum.INVALID_OR_UNEXPECTED_QUERY_PARAM, RECORD_ID );
            }
            else if ( aEntryFilter.getRecordIdentifiers().size() == 1 )
            {
                mApiParams.put( RECORD_ID, aEntryFilter.getRecordIdentifiers().get( 0 ) );
            }
        }

        // extract the optional parameters from the filter XML
        ParameterMapInterface parameterMap = aEntryFilter.getAdditionalParametersMap();
        String value = null;
        for ( String filterParameterName : parameterMap.getFilterParameterNames() )
        {
            //QueryParameter<?> queryParameter = parameterMap.getParameterValue( filterParameterName );
            QueryParameter<?> queryParameter = parameterMap.getParameterValue( filterParameterName );
            if ( queryParameter == null )
            {
                throw new PersistenceException( ErrorCodeEnum.INVALID_OR_UNEXPECTED_QUERY_PARAM, filterParameterName );
            }

            if ( "dateRange".equals( queryParameter.getType() ) )
            {
                throw new PersistenceException( ErrorCodeEnum.INVALID_OR_UNEXPECTED_QUERY_PARAM, filterParameterName );
            }
            else
            {
                if ( !( queryParameter.getValue() instanceof String ) )
                {
                    throw new PersistenceException( ErrorCodeEnum.INVALID_OR_UNEXPECTED_QUERY_PARAM, filterParameterName );
                }
                else
                {
                    if ( queryParameter.getName().equals( TEXT ) )
                    {
                        QueryParameter<String> textParameter = new QueryParameter<String>();
                        textParameter.setName( TEXT );
                        value = ( String )queryParameter.getValue();

                        if ( StringUtils.hasText( value ) )
                        {
                            if ( value.equalsIgnoreCase( TEXT_TRUE ) )
                            {

                                textParameter.setValue( SHOW_DOCUMENTS );

                            }
                            else if ( value.equalsIgnoreCase( TEXT_FALSE ) )
                            {

                                textParameter.setValue( NO_DOCUMENTS );

                            }
                            queryParameter = textParameter;
                        }

                    }

                    mApiParams.put( queryParameter.getName(), ( String )queryParameter.getValue() );
                }
            }
        }

        // build the parameter list from the filter	
        spParameters.add( rpcBuilder.build( mApiParams ) );

        return spParameters;

    }


    /*
     * (non-Javadoc)
     * 
     * @see java.lang.Runnable#run()
     */
    @Override
    public void run( )
    {
        CallableStatement cstmt = null;
        Connection pooledConnection = null;
        Connection wrappedConnection = null;
        ResultSet rs = null;

        try
        {
            String query = buildQueryString( queryAssociation.getAssociationName(), entryFilter, personIdentifiers );
            List<String> namedParameters = buildQueryParameterList( query, queryAssociation.getAssociationName(), entryFilter, personIdentifiers );
            List<Map<String, Object>> resultList = new ArrayList<Map<String, Object>>();

            DataSource dataSource = SessionFactoryUtils.getDataSource( session.getSessionFactory() );

            wrappedConnection = dataSource.getConnection();

            pooledConnection = pooledConnectionUnwrapper.unwrap( wrappedConnection );

            cstmt = pooledConnection.prepareCall( query );

            for ( int i = 0; i < namedParameters.size(); i++ )
            {
                cstmt.setString( i + 1, namedParameters.get( i ) );
            }

            long queryTimeout = TimeoutUtil.processTimeout( getTemplateTimeoutMap(), entryFilter, this.applicationName );
            cstmt.setQueryTimeout( ( int )queryTimeout );

            rs = cstmt.executeQuery();
            List<String> records = new ArrayList<String>();
            while ( rs.next() )
            {
                //GSP resultset has Data and Error columns
                String dataRecord = rs.getString( "Data" );
                String errorRecord = rs.getString( "Error" );
                if ( StringUtils.hasLength( errorRecord ) )
                {
                    /*
                     * we have an error - this call was not successful - the RPC is unknown or parameters invalid, etc. 
                     * This is not a Stored Procedure error - it is merely an embedded message that represents an error
                     * that resulted in an attempted call on an RPC at a Vista Site
                     */
                    addEmbeddedRpcError( errorRecord );
                }
                else
                {
                    records.add( dataRecord );
                }
                continue;
            }

            if ( ( records != null ) && ( records.size() > 0 ) )
            {
                this.results = vistaGenericResponseStrategy.formatResponse( records, extractVistaPatientIdentifier( personIdentifiers ), entryFilter,
                                siteId );
            }
        }
        catch ( SocketException se )
        {
            addException( se, applicationName );
        }
        catch ( Exception ex )
        {
            addException( ex, applicationName );
        }
        finally
        {
            if ( rs != null )
            {
                try
                {
                    rs.close();
                }
                catch ( Exception er )
                {
                    er.printStackTrace();
                }
                finally
                {
                    rs = null;
                }
            }
            if ( cstmt != null )
            {
                try
                {
                    cstmt.close();
                }
                catch ( SQLException ec )
                {
                    ec.printStackTrace();
                }
                finally
                {
                    cstmt = null;
                }
            }
            if ( pooledConnection != null )
            {
                try
                {
                    pooledConnection.close();
                }
                catch ( SQLException ep )
                {
                    ep.printStackTrace();
                }
                finally
                {
                    pooledConnection = null;
                }
            }
            if ( wrappedConnection != null )
            {
                try
                {
                    wrappedConnection.close();
                }
                catch ( SQLException ew )
                {
                    ew.printStackTrace();
                }
                finally
                {
                    wrappedConnection = null;
                }
            }
        }
    }


    private void addEmbeddedRpcError( String rpcErrorMessage )
    {
        //VISTA RPC returns an error message with double quotes ""MESSGE-TEXT"" the enclosing set of quotes needs to be removed.
        int begin = rpcErrorMessage.indexOf( '"' );
        int end = rpcErrorMessage.lastIndexOf( '"' );
        rpcErrorMessage = rpcErrorMessage.substring( begin + 1, end );

        /*To conform to the CDS Exception framework - we need to create a ROOT CAUSE exception for the embedded exception message
         * otherwise the message in the error section will be duplicated  - we could make either exception the root (our invented root or the embedded message)
         * but it reads better in the display message element to create an ERROR root as this embedded exception may be added in the error section as a WARNING or partial read.
         * We will get something like this in the display message element: 
         *             displayMessage: "..blah blah blah.....Root Cause Exception VISTA SITE ERROR. : RPC Unknown ...blah blah blah..."  
         */
        Exception rootCause = new Exception( "VISTA SITE ERROR" );
        addException( new Exception( rpcErrorMessage, rootCause ), applicationName );
    }


    public void setUsername( String username )
    {
        this.username = username;
    }


    public void setPassword( String password )
    {
        this.password = password;
    }


    public void setVistaGenericResponseStrategy( VistaGenericStoredProcedureSingleResponseStrategyInterface vistaGenericResponseStrategy )
    {
        this.vistaGenericResponseStrategy = vistaGenericResponseStrategy;
    }


    public void setRpcBuilder( RpcParamStringBuilderInterface rpcBuilder )
    {
        this.rpcBuilder = rpcBuilder;
    }

}
