/********************************************************************
 * Copyright © 2006 VHA. All rights reserved
 ********************************************************************/
// Package
package gov.va.med.fw.scheduling;

// Java classes
import gov.va.med.fw.batchprocess.BatchProcessDetail;
import gov.va.med.fw.batchprocess.BatchProcessInvoker;
import gov.va.med.fw.batchprocess.BatchProcessService;
import gov.va.med.fw.security.LoginManager;
import gov.va.med.fw.util.ReflectionException;
import gov.va.med.fw.util.Reflector;

import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.util.List;

import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.ClassUtils;
import org.apache.commons.lang.StringUtils;
import org.quartz.InterruptableJob;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.UnableToInterruptJobException;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.scheduling.quartz.QuartzJobBean;

/**
 * Base class for all Quartz based jobs.  This allows for consistent pre- and post- operations
 * to the execution of the jobs.  The jobs themselves are tied to the serviceName and methodName.
 * 
 * If a BatchProcessInvoker is "detected", its custom implementation will create a JMS Message to
 * indicate a job needs to be executed immediately.  This allows for targeting job execution to
 * dedicated servers (ie, physical JMS queue is located on subset of nodes in cluster)
 * without worrying about inclusion/exclusion of Quartz scheduler.
 *
 * @author DNS   LEV
 */
public class StatelessScheduledService extends QuartzJobBean implements Serializable, InterruptableJob {

	public static final String ARGUMENTS_METHOD = "methodArguments";
	private static final String INVOKE_METHOD = "invoke";
	private static final String INTERRUPT_METHOD = "interrupt";
	
	/**
	 * An instance of serialVersionUID
	 */
	private static final long serialVersionUID = -5611593072858343064L;

	/**
	 * An instance of serviceName
	 */
	private String serviceName = null;
	
	/**
	 * An instance of methodName - default is invoke.
	 */
	private String methodName = INVOKE_METHOD;
	
	/**
	 * An instance of applicationContext
	 */
	private ApplicationContext applicationContext = null;
	
	/**
	 * An instance of methodArguments
	 */
	private Object[] methodArguments = null;
	
	/**
	 * An instance of interruptMethod, default value is interrupt
	 */
	private String interruptMethod = INTERRUPT_METHOD;
	
	private BatchProcessInvoker batchProcessInvoker;
	private BatchProcessService batchProcessService;
	private LoginManager loginManager;

	/**
	 * A default constructor
	 */
	public StatelessScheduledService() {
		super();
	}

	/**
	 * @param applicationContext
	 * @throws BeansException
	 */
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.applicationContext = applicationContext;
	}
	
	/**
	 * @return Returns the applicationContext.
	 */
	public ApplicationContext getApplicationContext() {
		return applicationContext;
	}
	
	/**
	 * @param methodName The methodName to set.
	 */
	public void setMethodName(String methodName) {
		this.methodName = methodName;
	}

	/**
	 * @param methodArguments The methodArguments to set.
	 */
	public void setMethodArguments(Object[] methodArguments) {
		this.methodArguments = methodArguments;
	}

	/**
	 * @param serviceName The serviceName to set.
	 */
	public void setServiceName(String serviceName) {
		this.serviceName = serviceName;
	}

	/**
	 * @return Returns the serviceName.
	 */
	public String getServiceName() {
		return serviceName;
	}

	/**
	 * @return Returns the interruptMethod.
	 */
	public String getInterruptMethod() {
		return interruptMethod;
	}
	
	/**
	 * @param interruptMethod The interruptMethod to set.
	 */
	public void setInterruptMethod(String interruptMethod) {
		this.interruptMethod = interruptMethod;
	}		

	/**
	 * @see org.quartz.InterruptableJob#interrupt()
	 */
	public void interrupt() throws UnableToInterruptJobException {
		
		try {
			Object service = getApplicationContext().getBean( getServiceName() );
			Reflector.invoke( service, interruptMethod, null);
		}
		catch( NoSuchMethodException e ) {
			UnableToInterruptJobException ex = new UnableToInterruptJobException( "A interrupt method to run is missing");
			ex.initCause(e);
			throw ex;
		}
		catch( ReflectionException e ) {
			UnableToInterruptJobException ex = new UnableToInterruptJobException( "A interrupt method to run is in accessible");
			ex.initCause(e);
			throw ex;
		}
		catch( InvocationTargetException e ) {
			UnableToInterruptJobException ex = new UnableToInterruptJobException( "Exception thrown in a interrupt method " + interruptMethod);
			ex.initCause(e);
			throw ex;
		}		
		catch( Exception e ) {
			// Any runtime exception such as TransactionTimeoutException
			UnableToInterruptJobException ex = new UnableToInterruptJobException( "Exception thrown in a interrupt method " + interruptMethod);
			ex.initCause(e);
			throw ex;
		}
		catch( Error e ) {
			// Any error such as OutOfMemoryError
			UnableToInterruptJobException ex = new UnableToInterruptJobException( "Exception thrown in a interrupt method " + interruptMethod);
			ex.initCause(e);
			throw ex;
		}
	}
	
	/**
	 * @see org.springframework.scheduling.quartz.QuartzJobBean#executeInternal(org.quartz.JobExecutionContext)
	 */
	protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
		
		// An application context is set automatically by a Spring managed SchedulerFactoryBean
		// into a SchdulerContext, which is a map.  A QuartzJobBean, in turn, looks for properties
		// in this Job class whose names are the same as keys in a ScheduleContext then set the 
		// appropriate values.  Refer to QuartzJobBean for more info
		
		// First check if all required properties are set
		if( this.applicationContext == null || 
			 this.methodName == null ||
			 this.serviceName == null ) {
			
			throw new JobExecutionException( "Missing required properties to run a job" );
		}
		
		// execution context could come from either trigger or job.....therefore check context
		Object[] currentArguments = methodArguments;
		Object[] contextArguments = null;
		if(context.getMergedJobDataMap().containsKey(ScheduledProcessInvocationContext.INVOCATION_ARGUMENTS)) {
			// client can either have these as typed or one generic Object.....try typed first
			if(context.getMergedJobDataMap().get(ScheduledProcessInvocationContext.INVOCATION_ARGUMENTS) instanceof List)
				contextArguments = ((List) context.getMergedJobDataMap().get(ScheduledProcessInvocationContext.INVOCATION_ARGUMENTS)).toArray();
			else
				contextArguments = (Object[]) context.getMergedJobDataMap().get(ScheduledProcessInvocationContext.INVOCATION_ARGUMENTS);
			if(methodArguments == null) {
				currentArguments = contextArguments;
			} else if(isNotJobData(contextArguments)) {
				// some usages may pass dynamic data in on the Job's JobDataMap (which would get copied onto methodArguments already)
				currentArguments = ArrayUtils.addAll(methodArguments, contextArguments);
			}
		}
		
		try {
			// wrap current arguments in a ScheduledProcessInvocationContext
			ScheduledProcessInvocationContext invContext = new ScheduledProcessInvocationContext();
			
			if(currentArguments != null)
				invContext.setInvocationArguments(currentArguments.length == 1 ? currentArguments[0] : currentArguments);
			String executionContext = (String) context.getMergedJobDataMap().get(
					ScheduledProcessInvocationContext.EXECUTION_CONTEXT);
			if(StringUtils.isNotBlank(executionContext))
				invContext.setExecutionContext(executionContext);
			
			/* notice we do not set the JobDetailBeanName on invContext, this is because it may not exist (logical name that is not
			 * an actual Spring bean)
			 */
			invContext.setServiceName(this.serviceName);
			invContext.setMethodName(this.methodName);
			
			invContext.setJobName(context.getJobDetail().getName());
			invContext.setJobGroup(context.getJobDetail().getGroup());

			doExecuteJob(invContext);
		}
		catch( NoSuchMethodException e ) {
			throw new JobExecutionException( "A service method to run is missing", e, false );
		}
		catch( ReflectionException e ) {
			throw new JobExecutionException( "A service method to run is in accessible", e, false );
		}
		catch( InvocationTargetException e ) {
			throw new JobExecutionException( "Failed to run a job in: " + methodName, e, false );
		}
		catch( Exception e ) {
			// Any runtime exception such as TransactionTimeoutException
			throw new JobExecutionException( "Failed to run a job in: " + methodName, e, false );
		}
		catch( Error e ) {
			// Any error such as OutOfMemoryError
			JobExecutionException je = new JobExecutionException( "Failed to run a job in: " + methodName, null, false );
			je.initCause( e );
			throw je;
		}
	}
	
	/** Subclasses can override */
	protected void doExecuteJob(ScheduledProcessInvocationContext invContext)
		throws Exception {
		if(!containsBatchProcessInvoker()) {
			if(!this.containsBatchProcessService())
				throw new IllegalStateException("Missing BatchProcessService to execute job");
			this.getBatchProcessService().executeJob(invContext);
		} else {
			String jobName = invContext.getJobName();
			BatchProcessDetail detail = getBatchProcessInvoker().getBatchProcessDetail(jobName);
			if(detail == null) {
				// could be for dynamically scheduled job
				detail = new BatchProcessDetail();
				detail.setJobName(invContext.getJobName());
				detail.setGroupName(invContext.getJobGroup());
			}
			
			try {
				this.getLoginManager().loginAnonymous(Thread.currentThread().getName());
				getBatchProcessInvoker().invokeBatchProcessWithEvent(detail, invContext);
			} finally {
				this.getLoginManager().logout();
			}
		}
	}
	
	/**
	 * @param args
	 * @return
	 */
	private boolean isNotJobData(Object[] args) {
		if(methodArguments.length != args.length)
			return true;
		for(int i=0; i<args.length; i++)
			if(!args[i].equals(methodArguments[i]))
				return true;
		return false;
	}

	
	private boolean containsBatchProcessInvoker() {
		try {
			getBatchProcessInvoker();
			return true;
		} catch(Exception e) {
			return false;
		}
	}

	private boolean containsBatchProcessService() {
		try {
			this.getBatchProcessService();
			return true;
		} catch(Exception e) {
			return false;
		}
	}
	
	
	/* below are needed since this bean's lifecycle is not managed by Spring (is created by Quartz) */
	
	private BatchProcessInvoker getBatchProcessInvoker() {
		if (batchProcessInvoker == null) {
			try {
				batchProcessInvoker = (BatchProcessInvoker) getApplicationContext().getBean(ClassUtils.getShortClassName(BatchProcessInvoker.class), BatchProcessInvoker.class);
			} catch(IllegalStateException e) {
				/*
				 * Ensure it is refreshed if get IllegalStateException and try one more time...(seen this exception in clustered environment).
				 * See Spring AbstractRefreshableApplicationContext.getBeanFactory() method that is called from this chain
				 */
				if(getApplicationContext() instanceof ConfigurableApplicationContext) {
					((ConfigurableApplicationContext)getApplicationContext()).refresh();
					batchProcessInvoker = (BatchProcessInvoker) getApplicationContext().getBean(ClassUtils.getShortClassName(BatchProcessInvoker.class), BatchProcessInvoker.class);
				} else
					throw e;				
			}
		}
		return batchProcessInvoker;
	}
	
	private BatchProcessService getBatchProcessService() {
		if (batchProcessService == null) {
			try {
				batchProcessService = (BatchProcessService) getApplicationContext().getBean(ClassUtils.getShortClassName(BatchProcessService.class), BatchProcessService.class);
			} catch(IllegalStateException e) {
				/*
				 * Ensure it is refreshed if get IllegalStateException and try one more time...(seen this exception in clustered environment).
				 * See Spring AbstractRefreshableApplicationContext.getBeanFactory() method that is called from this chain
				 */
				if(getApplicationContext() instanceof ConfigurableApplicationContext) {
					((ConfigurableApplicationContext)getApplicationContext()).refresh();
					batchProcessService = (BatchProcessService) getApplicationContext().getBean(ClassUtils.getShortClassName(BatchProcessService.class), BatchProcessService.class);
				} else
					throw e;				
			}
		}
		return batchProcessService;
	}		
	
	private LoginManager getLoginManager() {
		if (loginManager == null) {
			try {
				loginManager = (LoginManager) getApplicationContext().getBean(ClassUtils.getShortClassName(LoginManager.class), LoginManager.class);
			} catch(IllegalStateException e) {
				/*
				 * Ensure it is refreshed if get IllegalStateException and try one more time...(seen this exception in clustered environment).
				 * See Spring AbstractRefreshableApplicationContext.getBeanFactory() method that is called from this chain
				 */
				if(getApplicationContext() instanceof ConfigurableApplicationContext) {
					((ConfigurableApplicationContext)getApplicationContext()).refresh();
					loginManager = (LoginManager) getApplicationContext().getBean(ClassUtils.getShortClassName(LoginManager.class), LoginManager.class);
				} else
					throw e;				
			}
		}
		return loginManager;
	}	
}