/**
 * 
 */


package gov.va.med.cds.filter;


import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Map;

import org.dom4j.Element;

import gov.va.med.cds.common.person.correlation.PersonIdentifierInterface;
import gov.va.med.cds.exception.ErrorCodeEnum;
import gov.va.med.cds.request.ValidationException;


/**
 * @author susarlan
 *
 */
public class FilterQueryMetadata
    implements
        EntryFilterInterface
{
    public static final int DEFAULT_MAX_PAGE_SIZE = 500;

    public static final int DEFAULT_PAGE_NUMBER = 1;

    protected final static String XS_DATE_FORMAT_PATTERN = "yyyy-MM-dd";

    private final static String DATABASE_DATE_FORMAT_PATTERN = "yyyyMMddHHmmss";

    private String domainEntryPoint = null;
    private String queryName = null;
    private boolean countOnly = false;
    private String xpathQuery = null;
    private String jsonPathQuery = null;
    private Calendar startDate;
    private Calendar endDate;

    private String requestId = null;
    private String templateId = null;
    private Long timeout = null;

    private List<String> recordIdentifiers = null;
    private boolean useDates = false;
    private boolean isPatientCentricFilter = true;
    private String facilityId = null;

    private ParameterMap additionalParametersMap = null;

    private String[] elementNames = { "queryTimeoutSeconds", "domainEntryPoint", "otherQueryParameters", "recordIdentifiers", "xpathQuery",
                    "jsonPathQuery", "endDate", "startDate", "orderBy", "xpathFilter", "xpathExtractor" };
    private String[] unsupportedKeyWrds = { "SELECT", "DELETE", "UPDATE", "CREATE", "DROP", "ALTER", "COMMIT", "ROLLBACK", "TRUNCATE" };
    ArrayList<String> entryFilterElementNames = null;

    private String xpathExtractExpression = null;
    private String xpathFilterExpression = null;

    private int pageNumber = DEFAULT_PAGE_NUMBER;
    private int pageSize = DEFAULT_MAX_PAGE_SIZE;

    private String xpathExtractExpressionNamespace;

    private String xpathFilterExpressionNamespace;


    public String getXpathFilterExpression( )
    {
        return xpathFilterExpression;
    }


    public String getXpathFilterExpressionNamespace( )
    {
        return xpathFilterExpressionNamespace;
    }


    public String getXpathExtractExpression( )
    {
        return xpathExtractExpression;
    }


    public String getXpathExtractExpressionNamespace( )
    {
        return xpathExtractExpressionNamespace;
    }


    public String getDomainEntryPoint( )
    {
        return domainEntryPoint;
    }


    public Calendar getEndDate( )
    {
        return endDate;
    }


    public String getQueryName( )
    {
        return queryName;
    }


    @Override
    public boolean isCountOnly( )
    {
        return this.countOnly;
    }


    @Override
    public int getPageSize( )
    {
        return this.pageSize;
    }


    @Override
    public int getPageNumber( )
    {
        return this.pageNumber;
    }


    @Override
    public boolean isPagingFilter( )
    {
        return this.getPageSize() > 0 && this.getPageNumber() >= 0;
    }


    public List<String> getRecordIdentifiers( )
    {
        return recordIdentifiers;
    }


    public Calendar getStartDate( )
    {
        return startDate;
    }


    public String getXpathQuery( )
    {
        return xpathQuery;
    }


    public String getJsonPathQuery( )
    {
        return jsonPathQuery;
    }


    public boolean useDates( )
    {
        return useDates;
    }


    public ParameterMapInterface getAdditionalParametersMap( )
    {
        return additionalParametersMap;
    }


    protected FilterQueryMetadata( Element entryFilter, String templateId, String requestId )
    {
        initElementNames();
        extractMetadata( entryFilter );
        setRequestId( requestId );
        setTemplateId( templateId );
    }


    protected void initElementNames( )
    {
        this.entryFilterElementNames = new ArrayList<String>();
        for ( String name : elementNames )
        {
            entryFilterElementNames.add( name );
        }

    }


    private void extractMetadata( Element entryFilter )
    {
        DateFormat databaseDateFormat = new SimpleDateFormat( DATABASE_DATE_FORMAT_PATTERN );
        startDate = extractDate( "18410101000000", databaseDateFormat );
        endDate = extractDate( "30001231235959", databaseDateFormat );

        queryName = entryFilter.attributeValue( "queryName" );
        countOnly = Boolean.parseBoolean( entryFilter.attributeValue( "countOnly" ) );

        // paging capabilities
        pageNumber = ( entryFilter.attributeValue( "pageNumber" ) != null ) ? Integer.parseInt( entryFilter.attributeValue( "pageNumber" ) )
                        : DEFAULT_PAGE_NUMBER;
        pageSize = ( entryFilter.attributeValue( "pageSize" ) != null && Integer.parseInt( entryFilter.attributeValue( "pageSize" ) ) < DEFAULT_MAX_PAGE_SIZE ) ? Integer
                        .parseInt( entryFilter.attributeValue( "pageSize" ) ) : DEFAULT_MAX_PAGE_SIZE;

        String patientCentricFilterStr = entryFilter.attributeValue( "isPatientCentric" );
        if ( patientCentricFilterStr != null )
        {
            this.isPatientCentricFilter = Boolean.parseBoolean( entryFilter.attributeValue( "isPatientCentric" ) );
        }

        String facilityIdStr = entryFilter.attributeValue( "facilityId" );
        if ( facilityIdStr != null )
        {
            this.facilityId = facilityIdStr;
        }

        //determine if this is a specialized EntryFilter (wraps domainEntryPoint and optionalParameters)
        Element specializedEntryFilter = extractSpecializedEntryPointFilter( entryFilter );
        domainEntryPoint = specializedEntryFilter.element( "domainEntryPoint" ).getTextTrim();

        extractTimeout( entryFilter );
        extractStartAndEndDates( entryFilter );
        extractRecordIdentifiers( entryFilter );
        extractXpathQuery( entryFilter );
        extractJsonPathQuery( entryFilter );
        extractAdditionalParameters( specializedEntryFilter );
        extractXpathExpressions( entryFilter );
    }


    private void extractXpathExpressions( Element entryFilter )
    {
        Element xpathQueryElement = entryFilter.element( "xpathExtractor" );
        if ( xpathQueryElement != null )
        {
            Element xpathElement = xpathQueryElement.element( "expression" );
            if ( xpathElement == null )
            {
                throw new ValidationException( ErrorCodeEnum.INVALID_XPATH_QUERY );
            }

            this.xpathExtractExpression = xpathElement.getTextTrim();
            this.xpathExtractExpressionNamespace = xpathQueryElement.attributeValue( "extractorNamespace" );

        }
        Element xpathFilterElement = entryFilter.element( "xpathFilter" );
        if ( xpathFilterElement != null )
        {
            Element xpathElement = xpathFilterElement.element( "expression" );
            if ( xpathElement == null )
            {
                throw new ValidationException( ErrorCodeEnum.INVALID_XPATH_QUERY );
            }
            this.xpathFilterExpression = xpathElement.getTextTrim();
            this.xpathFilterExpressionNamespace = xpathFilterElement.attributeValue( "filterNamespace" );
        }

    }


    private Element extractSpecializedEntryPointFilter( Element entryFilter )
    {

        //return a specializedEntryFilter element if one exists - otherwise just return entryFilter
        @SuppressWarnings( "unchecked" )
        List<Element> elements = entryFilter.elements();
        for ( Element element : elements )
        {
            if ( !this.entryFilterElementNames.contains( element.getName() ) )
            {
                return element;
            }
        }
        return entryFilter;
    }


    private void extractStartAndEndDates( Element entryFilter )
    {
        Element startDateElement = entryFilter.element( "startDate" );
        if ( startDateElement == null )
        {
            extractStartAndEndDateCalendars( entryFilter );
        }
        else
        {
            if ( startDateElement.getTextTrim().length() == 10 )
            {
                extractStartAndEndDateCalendars( entryFilter );
            }
            else
            {
                extractStartAndEndDateTimestampCalendars( entryFilter );
            }
        }
    }


    private void extractStartAndEndDateTimestampCalendars( Element entryFilter )
    {
        useDates = true;

        if ( entryFilter.element( "startDate" ) != null )
        {
            String startDateString = entryFilter.element( "startDate" ).getTextTrim();

            startDate = extractXsDate( startDateString );
        }

        if ( entryFilter.element( "endDate" ) != null )
        {
            String endDateString = entryFilter.element( "endDate" ).getTextTrim();

            endDate = extractXsDate( endDateString );
            /// alter the end date to include data timestamped with the end date in the query results.
            /// for example - a specified end date of 1970-04-20 should include a record with the utc observation date
            /// of 197004200910+0000 
            endDate.add( Calendar.DATE, 1 );
            endDate.add( Calendar.MILLISECOND, -1 );
        }
    }


    private Calendar extractXsDate( String dateString )
    {
        try
        {
            return javax.xml.bind.DatatypeConverter.parseDateTime( dateString );
        }
        catch ( Exception e )
        {
            throw new ValidationException( ErrorCodeEnum.CANNOT_PARSE_DATE, dateString );
        }
    }


    private void extractStartAndEndDateCalendars( Element entryFilter )
    {
        DateFormat xmlDateFormat = new SimpleDateFormat( XS_DATE_FORMAT_PATTERN );

        Element startDateElement = entryFilter.element( "startDate" );
        if ( startDateElement != null )
        {
            useDates = true;
            String startDateString = startDateElement.getTextTrim();

            startDate = extractDate( startDateString, xmlDateFormat );
            startDate.set( Calendar.HOUR_OF_DAY, 0 );
            startDate.set( Calendar.MINUTE, 0 );
            startDate.set( Calendar.SECOND, 0 );
            startDate.add( Calendar.DATE, -1 );
        }

        /* Do domain specific processing.
         * This design is temporary and was suggested by Narasa.
         * It will be refactored in 3.2 version.
        */
        if ( startDateElement == null && domainEntryPoint.equals( "Appointment" ) )
        {
            // compute today -90
            useDates = true;
            SimpleDateFormat sdf = new SimpleDateFormat( "yyyy-MM-dd" );

            Calendar cal = new GregorianCalendar();
            cal.add( Calendar.DAY_OF_YEAR, -90 );
            String todayMinus90 = sdf.format( cal.getTime() );

            startDate = extractDate( todayMinus90, xmlDateFormat );
            startDate.set( Calendar.HOUR_OF_DAY, 0 );
            startDate.set( Calendar.MINUTE, 0 );
            startDate.set( Calendar.SECOND, 0 );
            startDate.add( Calendar.DATE, -1 );

        }

        Element endDateElement = entryFilter.element( "endDate" );
        if ( endDateElement != null )
        {
            useDates = true;
            String endDateString = endDateElement.getTextTrim();
            endDate = extractDate( endDateString, xmlDateFormat );
            endDate.set( Calendar.HOUR_OF_DAY, 23 );
            endDate.set( Calendar.MINUTE, 59 );
            endDate.set( Calendar.SECOND, 59 );
            endDate.add( Calendar.DATE, +1 );
        }

        /* Do domain specific processing.
         * This design is temporary and was suggested by Narasa.
         * It will be refactored in 3.2 version.
        */
        if ( endDateElement == null && domainEntryPoint.equals( "Appointment" ) )
        {
            useDates = true;
            SimpleDateFormat sdf = new SimpleDateFormat( "yyyy-MM-dd" );
            Calendar cal = new GregorianCalendar();
            cal.add( Calendar.DAY_OF_YEAR, 120 );
            String todayPlus120 = sdf.format( cal.getTime() );

            endDate = extractDate( todayPlus120, xmlDateFormat );
            endDate.set( Calendar.HOUR_OF_DAY, 23 );
            endDate.set( Calendar.MINUTE, 59 );
            endDate.set( Calendar.SECOND, 59 );
            endDate.add( Calendar.DATE, +1 );
        }
    }


    private void extractTimeout( Element entryFilter )
    {
        Element timeoutElement = entryFilter.element( "queryTimeoutSeconds" );
        if ( timeoutElement != null )
        {
            try
            {
                timeout = new Long( timeoutElement.getTextTrim() );
            }
            catch ( NumberFormatException nfe )
            {
                throw new ValidationException( ErrorCodeEnum.CANNOT_PARSE_TIMEOUT );
            }
        }
    }


    protected void extractAdditionalParameters( Element entryFilter )
    {
        Element additionalParametersElement = entryFilter.element( "otherQueryParameters" );
        if ( additionalParametersElement != null )
        {
            for ( String unsupportedValue : unsupportedKeyWrds )
            {
                String param = additionalParametersElement.getStringValue();
                if ( param.toUpperCase().contains( unsupportedValue ) )
                {
                    throw new ValidationException( ErrorCodeEnum.INVALID_OTHER_QUERY_PARAMS, param );
                }
            }
            additionalParametersMap = new ParameterMap( additionalParametersElement );
        }
    }


    private void extractXpathQuery( Element entryFilter )
    {
        Element xpathQueryElement = entryFilter.element( "xpathQuery" );
        if ( xpathQueryElement != null )
        {
            Element xpathElement = xpathQueryElement.element( "xpath" );
            if ( xpathElement == null )
            {
                throw new ValidationException( ErrorCodeEnum.INVALID_XPATH_QUERY );
            }
            xpathQuery = xpathElement.getTextTrim();
        }
    }


    private void extractJsonPathQuery( Element entryFilter )
    {
        Element jsonPathQueryElement = entryFilter.element( "jsonPathQuery" );
        if ( jsonPathQueryElement != null )
        {
            Element jsonPathElement = jsonPathQueryElement.element( "jsonPath" );
            if ( jsonPathElement == null )
            {
                throw new ValidationException( ErrorCodeEnum.INVALID_JSONPATH_QUERY );
            }
            jsonPathQuery = jsonPathElement.getTextTrim();
        }
    }


    @SuppressWarnings( "unchecked" )
    private void extractRecordIdentifiers( Element entryFilter )
    {
        List<Element> recordIdentifierElements = entryFilter.elements( "recordIdentifiers" );
        if ( recordIdentifierElements != null )
        {
            recordIdentifiers = extractRecordIdentifiers( recordIdentifierElements );
        }
    }


    protected Calendar extractDate( String dateString, DateFormat dateFormatter )
    {
        try
        {
            Date dateFromString = dateFormatter.parse( dateString );
            Calendar cal = new GregorianCalendar();
            cal.setTime( dateFromString );
            return cal;
        }
        catch ( ParseException e )
        {
            throw new ValidationException( ErrorCodeEnum.CANNOT_PARSE_DATE, dateString );
        }
    }


    private List<String> extractRecordIdentifiers( List<Element> recordIdentifierElements )
    {
        List<String> extractedRecordIdentifiers = new ArrayList<String>();
        for ( Element recordIdentifierElement : recordIdentifierElements )
        {
            String concatenateRecordIdentifier = getRecordIdentifierField( recordIdentifierElement, "identity" ) + "/"
                            + getRecordIdentifierField( recordIdentifierElement, "namespaceId" ) + "/"
                            + getRecordIdentifierField( recordIdentifierElement, "universalId" ) + "/"
                            + getRecordIdentifierField( recordIdentifierElement, "universalIdType" );

            extractedRecordIdentifiers.add( concatenateRecordIdentifier );
        }

        return extractedRecordIdentifiers;
    }


    private String getRecordIdentifierField( Element recordIdentifierElement, String fieldName )
    {
        String fieldValue = "";
        Element recordIdentifierFieldElement = recordIdentifierElement.element( fieldName );
        if ( recordIdentifierFieldElement != null )
        {
            fieldValue = recordIdentifierFieldElement.getTextTrim();
        }
        return fieldValue;
    }


    /*    
    	private String getDefaultStartDate( String domainEntryPoint )
        {
            String todayMinus90 = "";
            if ( domainEntryPoint.equals( "Appointment" ) )
            {
                // compute today -90
                SimpleDateFormat sdf = new SimpleDateFormat( "yyyyMMdd000000" );
                Calendar cal = new GregorianCalendar();
                cal.add( Calendar.DAY_OF_YEAR, -90 );
                todayMinus90 = sdf.format( cal.getTime() );
            }

            return todayMinus90;
        }
    */

    @Override
    public String getRequestId( )
    {
        return this.requestId;
    }


    public void setRequestId( String requestId )
    {
        this.requestId = requestId;
    }


    @Override
    public String getTemplateId( )
    {
        return this.templateId;
    }


    public void setTemplateId( String templateId )
    {
        this.templateId = templateId;
    }


    public Long getTimeout( )
    {
        return this.timeout;
    }


    @Override
    public boolean isPatientCentricFilter( )
    {
        return this.isPatientCentricFilter;
    }


    @Override
    public boolean isSiteCentricFilter( )
    {
        boolean siteCentric = false;
        if ( !isPatientCentricFilter() )
        {
            siteCentric = ( facilityId != null ) ? true : false;
        }
        return siteCentric;
    }


    @Override
    public String getFacilityId( )
    {
        return this.facilityId;
    }


    @Override
    public boolean hasRecordIdentifiers( )
    {
        return ( this.recordIdentifiers != null && this.recordIdentifiers.size() != 0 );
    }


    @Override
    public boolean containsXpathFilterExpression( )
    {
        return ( getXpathFilterExpression() != null && getXpathFilterExpression().length() > 0 );
    }


    public void setAdditionalParametersMap( ParameterMap additionalParametersMap )
    {
        this.additionalParametersMap = additionalParametersMap;
    }
 
    public Map<String, Object> getDataMap()
    {
		 return null;
	}
	public List<PersonIdentifierInterface> getPersonIdentifiers()
	{
		return null;
	}
}
