

package gov.va.med.cds.template;


import gov.va.med.cds.clinicaldata.Operation;
import gov.va.med.cds.clinicaldata.TemplateMetaDataInterface;
import gov.va.med.cds.exception.ErrorCodeEnum;
import gov.va.med.cds.request.RequestValidatorInterface;
import gov.va.med.cds.request.ResponseValidatorInterface;
import gov.va.med.cds.request.ValidationException;
import gov.va.med.cds.request.WriteRequestInterface;
import gov.va.med.cds.xml.schema.SchemaHelperInterface;
import gov.va.med.cds.xml.schema.SchemaValidationException;

import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;

import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Validator;

import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.dom4j.Document;
import org.xml.sax.SAXException;


public class TemplateManager
    implements
        TemplateMetaDataProviderInterface,
        RequestValidatorInterface,
        ResponseValidatorInterface,
        TemplateManagerMBeanInterface
{
    private static final Log LOGGER = LogFactory.getLog( TemplateManager.class );
    private TemplateCacheInterface templateCache = null;
    private TemplateCachePersistenceInterface templateCachePersistence = null;
    private TemplateServiceInterface templateService = null;


    public TemplateManager( TemplateServiceInterface aTemplateService, TemplateCachePersistenceInterface aTemplateCachePersistence,
                    TemplateCacheInterface aTemplateCache )
        throws Exception
    {
        templateService = aTemplateService;
        templateCachePersistence = aTemplateCachePersistence;
        templateCache = aTemplateCache;

        initialize();
    }


    /**
     * validation for a WRITE request
     */
    public TemplateMetaDataInterface validateWriteRequest( WriteRequestInterface aWriteRequest )
    {
        if ( aWriteRequest == null )
        {
            throw new ValidationException( ErrorCodeEnum.WRITE_REQUEST_XML_NULL );
        }

        String requestTemplateId = aWriteRequest.getTemplateId();
        String requestXml = ( String )aWriteRequest.getRequestData();

        Validator validator = null;
        TemplateMetaDataInterface templateMetaData = null;

        // attempt to access the registered metadata
        try
        {
            validator = getTemplateValidator( requestTemplateId );
            templateMetaData = getTemplateMetaData( requestTemplateId );
        }
        catch ( TemplateCacheException e ) // if template was not found...
        {
            throw new ValidationException( ErrorCodeEnum.WRITE_REQUEST_TEMPLATE_ID_INVALID, requestTemplateId,
                            ( ExceptionUtils.getRootCause( e ) == null ? e.getMessage() : ExceptionUtils.getRootCause( e ).getMessage() ) );
        }

        // There are 3 unique attributes that represent the request type during a write.
        // apiRequestType is the request type that is indicated by the method called on the CDS API
        // requestRequestType is the request type defined by the value held in the XML instance passed to CDS in the
        // request
        // registeredRequestType is the request type registered within the metadata for the template XSD
        // The request type should contain one value from the set of (CREATE, UPDATE, APPEND, DELETE)
        // dont have any of these attributes in the xml so cant do
        // 1st we must compare the apiRequestType to the requestRequestType
        // 2nd we must compare the requestRequestType to the the registeredRequestType

        // this next validation will cover the second requestType validation mentioned above
        // attempt to validate the xml instance against the xsd template schema
        try
        {
            validator.validate( new StreamSource( new StringReader( requestXml ) ) );
        }
        catch ( SAXException e )
        {
            throw new SchemaValidationException( ErrorCodeEnum.SCHEMA_VALIDATION_FAILED, e, e.getMessage() );
        }
        catch ( IOException e )
        {
            throw new SchemaValidationException( ErrorCodeEnum.SCHEMA_VALIDATION_FAILED, e, e.getMessage() );
        }

        return templateMetaData;
    }


    /**
     * validation for a READ template
     */
    public TemplateMetaDataInterface validateReadTemplateId( String templateId )
    {
        TemplateMetaDataInterface templateMetaData = null;

        try
        {
            templateMetaData = getTemplateMetaData( templateId );
        }
        catch ( TemplateMetaDataProviderException e )
        {
            throw new ValidationException( ErrorCodeEnum.INVALID_TEMPLATE, e, templateId, ( ExceptionUtils.getRootCause( e ) == null ? e.getMessage()
                            : ExceptionUtils.getRootCause( e ).getMessage() ) );
        }

        // compare the request type of READ to the registered Metadata template request type
        if ( !templateMetaData.getOperation().name().equals( Operation.Read.name() ) )
        {
            throw new ValidationException( ErrorCodeEnum.REQUEST_TYPE_MISMATCH_OPERATION_VS_INSTANCE, Operation.Read.name(), templateMetaData
                            .getOperation().name() );
        }

        return templateMetaData;
    }


    /*
     * (non-Javadoc)
     * 
     * @see gov.va.med.cds.template.ResponseValidatorInterface#validate(gov.va.med.cds.request.ReadResponseInterface)
     */
    public void validateReadResponse( Document aReadResponse, String responseTemplateId )
    {
        String readResponseXML = aReadResponse.asXML();

        // attempt to validate the xml instance against the xsd template schema
        try
        {
            Validator validator = getTemplateValidator( responseTemplateId );
            validator.validate( new StreamSource( new StringReader( readResponseXML ) ) );
        }
        catch ( SAXException e )
        {
            throw new SchemaValidationException( ErrorCodeEnum.SCHEMA_VALIDATION_FAILED, e, e.getMessage() );
        }
        catch ( IOException e )
        {
            throw new SchemaValidationException( ErrorCodeEnum.SCHEMA_VALIDATION_FAILED, e, e.getMessage() );
        }
    }


    public synchronized Validator getTemplateValidator( String aTemplateId )
    {
        SchemaHelperInterface schemaHelper = getSchemaHelper( aTemplateId );
        Validator validator = schemaHelper.getValidator();

        return validator;
    }


    /**
     * 
     * @see gov.va.med.cds.xml.schema.SchemaSymbolMapProviderInterface#getSymbolMap(java.lang.String)
     */
    public synchronized Map<String, List<String>> getSymbolMap( String aTemplateId )
    {
        SchemaHelperInterface schemaHelper;
        try
        {
            schemaHelper = getSchemaHelper( aTemplateId );
        }
        catch ( TemplateCacheException e )
        {
            throw new ValidationException( ErrorCodeEnum.CANNOT_CREATE_TEMPLATE_SCHEMA_SYMBOL_MAP, e,
                            "Could not get a schema helper for the template id: " + aTemplateId, ExceptionUtils.getRootCause( e ).getMessage() );
        }

        Map<String, List<String>> symbolMap = schemaHelper.getSymbolMap();

        return symbolMap;
    }


    /**
     * 
     * @see gov.va.med.cds.template.TemplateMetaDataProviderInterface#getTemplateMetaData(java.lang.String)
     */
    public synchronized TemplateMetaDataInterface getTemplateMetaData( String aTemplateId )
        throws TemplateCacheException
    {
        TemplateMetaDataInterface metaData = templateCache.getTemplateMetaData( aTemplateId );
        if ( metaData == null )
        {
            // first check the template cache persistence for this template
            templateCachePersistence.loadTemplateIntoCache( templateCache, aTemplateId );
            metaData = templateCache.getTemplateMetaData( aTemplateId );
            if ( metaData == null )
            {
                retrieveAndPersistTemplateFromService( aTemplateId );
                templateCachePersistence.loadTemplateIntoCache( templateCache, aTemplateId );
                metaData = templateCache.getTemplateMetaData( aTemplateId );
            }
        }
        if ( metaData == null )
        {
             throw new TemplateMetaDataProviderException( ErrorCodeEnum.CANNOT_LOAD_TEMPLATE_METADATA, aTemplateId, "MetaData of the template id is null" );
        }

        return metaData;
    }


    public SchemaHelperInterface getSchemaHelper( String aTemplateId )
    {
        SchemaHelperInterface schemaHelper = templateCache.getSchemaHelper( aTemplateId );

        if ( schemaHelper == null )
        {
            // first check the template cache persistence for this template
            templateCachePersistence.loadTemplateIntoCache( templateCache, aTemplateId );
            schemaHelper = templateCache.getSchemaHelper( aTemplateId );
            if ( schemaHelper == null )
            {
                // next check the template repository for this template
                retrieveAndPersistTemplateFromService( aTemplateId );
                templateCachePersistence.loadTemplateIntoCache( templateCache, aTemplateId );

                schemaHelper = templateCache.getSchemaHelper( aTemplateId );
            }

            if ( schemaHelper == null )
            {
                throw new ValidationException( ErrorCodeEnum.INVALID_TEMPLATE, aTemplateId );
            }
        }

        return schemaHelper;
    }


    private void retrieveAndPersistTemplateFromService( String aTemplateId )
    {
        try
        {
            TemplateMetaDataInterface templateMetaData = templateService.getTemplateMetaData( aTemplateId );

            if ( templateMetaData != null )
            {
                templateMetaData.setDateUpdated( new Date() );
                templateCachePersistence.saveTemplateMetaDataToPersistence( templateMetaData );
            }
        }
        catch (TemplateServiceException e)
        {
            //Ignore exception to continue loading other templates (error captured in client)
            if (LOGGER.isDebugEnabled()) 
            {
                LOGGER.debug("TemplateServiceException in retrieveAndPersistTemplateFromService, template ID: "
                                + aTemplateId
                                + " Exception Message: "
                                + e.getMessage());
            }           
        }
        catch ( Exception e )
        {
            //Ignore exception to continue loading other templates
            LOGGER.warn("Exception in retrieveAndPersistTemplateFromService, template ID: "
                    + aTemplateId
                    + " Exception Message: "
                    + e.getMessage());
        }
    }


    /**
     * Preload all schemas as schema type systems to allow traversal ability to all schemas. Loads the templates from
     * templateService and persist into db
     */
    private void initialize( )
    {
        try
        {
            templateService.initialize();
            persistTemplatesFromService();
        }
        catch ( Exception e )
        {
            // Vhim Template Service unavailable
            LOGGER.warn( "Template Service Initialization failed" );
        }

        // Load cache from locally persisted templates in tfrlocal
        templateCachePersistence.loadAllTemplatesIntoCache( templateCache );

    }


    /**
     * reinitializes the template cache
     * ** Control this method as it can alter templateCache during getTemplateMetaData processing and give template not found error
     */
    public synchronized void reinitializeTemplateCache( )
    {
        TemplateCacheInterface tempCache = templateCache;
        try
        {
            clearTemplateCache();
            initialize();
        }
        catch ( Exception e )
        {
            templateCache = tempCache; // TODO remove this, cause it doesn't do anything
        }
    }


    /**
     * Management method used to clear out cache from memory and as the TemplateManager is accessed, templates will
     * reload as they are used
     * ** Control this method as it can alter templateCache during getTemplateMetaData processing and give template not found error
     */
    public synchronized void clearTemplateCache( )
    {
        TemplateCacheInterface tempCache = templateCache;
        try
        {
            if ( null != templateCache )
            {
                if ( !templateCache.isEmpty() )
                {
                    templateCache.clear();
                }
            }
        }
        catch ( TemplateServiceException e )
        {
            templateCache = tempCache;
        }
        catch ( TemplateCacheException e )
        {
            templateCache = tempCache;
        }
    }


    /**
     * Management method used to reset cache size from its current size.
     * 
     * If <newSize> is increasing then existing or current templates in memory cache will remain and new ones added as
     * used/accessed until capacity is reached and then least used is discarded as new templates are accessed.
     * 
     * If <newSize> is decreasing then method will reinitialize the cache to current size and load to that capacity -
     * templates accessed that are not in cache will be loaded as least used is discarded.
     * 
     * ** Control this method as it can alter templateCache during getTemplateMetaData processing and give template not found error
     */
    public synchronized void resizeTemplateCache( int newSize )
    {
        if ( getTemplateCacheThreshold() > newSize )
        {
            this.templateCache.reSize( newSize );
            // reducing templates in memory cache so reinitialize
            reinitializeTemplateCache();
        }
        else
        {
            this.templateCache.reSize( newSize );
        }
    }


    /**
     * Management method used to examine current cache size
     * 
     * @return int - Template Cache current capacity
     */
    public int getNumberOfTemplatesLoadedIntoTemplateCache( )
    {
        return templateCache.getNumberOfTemplatesLoadedIntoTemplateCache();
    }


    public List<String> getTemplateCacheTemplateIds( )
    {
        List<String> list = new ArrayList<String>( templateCache.getTemplateCacheTemplateIds() );
        return list;
    }


    public int getTemplateCacheThreshold( )
    {
        return templateCache.getTemplateCacheThreshold();
    }


    private void persistTemplatesFromService( )
    {
        Collection<String> templateIds = null;
        Collection<String> vhimVersions = templateService.getVhimVersions();

        for ( String vhimVersionName : vhimVersions )
        {
            templateIds = templateService.getActiveTemplateIds( vhimVersionName );

            if ( LOGGER.isDebugEnabled() )
            {
                LOGGER.debug( "looping on vhim version: " + vhimVersionName );
            }

            for ( String templateId : templateIds )
            {
                if ( LOGGER.isDebugEnabled() )
                {
                    LOGGER.debug( "\tRetrieving and Persisting template from service, template ID: " + templateId );
                }
                retrieveAndPersistTemplateFromService( templateId );
            }
        }
    }


    public void setTemplateCache( TemplateCacheInterface aTemplateCache )
    {
        templateCache = aTemplateCache;
    }

}
