package eu.dnetlib.data.collective.manager.scheduling;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;

import eu.dnetlib.common.interfaces.nh.IBlackboardMessage.Action;
import eu.dnetlib.common.interfaces.ws.IDriverService;
import eu.dnetlib.data.collective.manager.WorkerServiceManager;
import eu.dnetlib.data.collective.manager.nh.ActivityListener;
import eu.dnetlib.data.collective.manager.nh.JobInfo;
import eu.dnetlib.data.collective.manager.nh.ServiceActivity;
import eu.dnetlib.data.collective.manager.nh.ServiceJobListener;
import eu.dnetlib.data.collective.manager.nh.JobInfo.JOBSTATUS;
import eu.dnetlib.data.collective.manager.profile.AbstractInstance;
import eu.dnetlib.data.collective.manager.scheduling.InstanceScheduling.TRIGGER_TYPE;
import eu.dnetlib.data.collective.manager.utils.ServiceRegistry;
import eu.dnetlib.data.collective.worker.WorkerServiceParameters;
import eu.dnetlib.enabling.is.registry.rmi.ISRegistryException;
import eu.dnetlib.enabling.tools.blackboard.ActionStatus;
import eu.dnetlib.enabling.tools.blackboard.BlackboardClientHandler;
import eu.dnetlib.enabling.tools.blackboard.BlackboardJob;
import eu.dnetlib.enabling.tools.blackboard.BlackboardJobRegistry;
import eu.dnetlib.miscutils.datetime.DateUtils;

/**
 * @author Jochen Schirrwagen (jochen.schirrwagen@uni-bielefeld.de)
 * @param <T> worker service manager
 * @param <E> instance data structure
 *
 */
public class AbstractServiceJob<T extends WorkerServiceManager<IDriverService, AbstractInstance> , E extends AbstractInstance> extends QuartzJobBean{

	private static Log log = LogFactory.getLog(AbstractServiceJob.class);
	private T workerServiceManager;
	private E instance;
	private BlackboardClientHandler bbClientHandler;
	private BlackboardJobRegistry bbJobRegistry;
	private Action bbMsgAction;
	private TRIGGER_TYPE triggerType;
	private ExecutorService executor = Executors.newCachedThreadPool(); 
    private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
    
	/* (non-Javadoc)
	 * @see org.springframework.scheduling.quartz.QuartzJobBean#executeInternal(org.quartz.JobExecutionContext)
	 */
	protected void executeInternal(JobExecutionContext arg0)
			throws JobExecutionException {
 		log.debug("---executingInternal---");
 		
		ServiceRegistry<IDriverService> registry = this.getWorkerServiceManager().getServiceRegistry();
		if (registry.size() == 0){
			log.warn("cannot create instance jobs, because no worker services available!");
			return;
		}
		
 		if (bbMsgAction.name().equals(Action.CANCEL.name())){
 			List<ServiceActivity> serviceActivities = workerServiceManager.getServiceActivityRegistry().getServiceActivities(instance.getResourceId());
 			if (serviceActivities.isEmpty()){
 				log.warn("instance " + instance.getResourceId() + " has no active service activities.");
 			}
 			for (ServiceActivity activity: serviceActivities){
 				updateBlackboardMessage(activity.getJob(), instance.getLastUpdateErrorMessage());
 				
 				ServiceJobListener<ActivityListener> hsJobListener = new ServiceJobListener<ActivityListener>();				
 				hsJobListener.setWorkerServiceManager(workerServiceManager);
 				bbJobRegistry.registerJobListener(activity.getJob(), hsJobListener); // is this line necessary? because the job will be resend.
 				bbClientHandler.assign(activity.getJob());
 			}
 		}else{
 			if (!instance.isOngoing()){
 				if (this.triggerType == TRIGGER_TYPE.CRON){
 					try {
 						long instanceMinIntervalInMinutes = Long.parseLong(instance.getScheduling().getCronScheduling().getMinInterval()); 
 						if (instanceMinIntervalInMinutes > 1440){
 							// fix: subtract a full day from the minInterval to keep the interval period
 							instanceMinIntervalInMinutes = instanceMinIntervalInMinutes - 1440;
 						}
 						long minIntervallInMillis = instanceMinIntervalInMinutes * 60000;
						if ( (DateUtils.now() - sdf.parse(instance.getDateOfCreation()).getTime()) < minIntervallInMillis){
							log.debug("minimal intervall of " + instance.getScheduling().getCronScheduling().getMinInterval() + " minutes doesn't allow to execute job for instance-id: " + instance.getResourceId());
							return;
						}
					} catch (NumberFormatException e) {
						log.error("invalid date format in profile");
						return;
					} catch (ParseException e) {
						log.error("invalid date format in profile");
						return;
					}
 				}
 				instance.setOngoing(true);
 				instance.setLastUpdateErrorMessage(""); // added Sept. 09 by JS - start the new job with an empty error message
 				String resourceId = registry.next();
 				log.debug("assigning job to : " + resourceId);
 				BlackboardJob job = createBlackboardMessage(resourceId);
 				job.getParameters().putAll(workerServiceManager.createJobParameters(instance.getResourceId(), bbMsgAction));
 				ServiceActivity serviceActivity = new ServiceActivity(job, resourceId, instance.getResourceId(), job.getParameters().get(WorkerServiceParameters.PARAM_NAME_ACTION_ID));
 				JobInfo jobInfo = serviceActivity.getJobInfo();
 				jobInfo.setActive(true);
 				jobInfo.setJobStatus(JOBSTATUS.STARTING);
 				//jobInfo.setElapsedTime("0");
 				jobInfo.setStartDate(DateUtils.now_ISO8601());
 				try{
 					Callable<String> pingService = new PingService(registry.getService(resourceId));
 					Future<String> result = executor.submit(pingService);
 	 				if (result.get(10, TimeUnit.SECONDS) != null){
 	 					// test if service is alive by pinging the service
 	 	 				workerServiceManager.getServiceActivityRegistry().addWorkerServiceActivity(
 	 	 						job.getParameters().get(WorkerServiceParameters.PARAM_NAME_ACTION_ID), serviceActivity);
 	 	 				log.debug("putting activitiy with id " + job.getParameters().get(WorkerServiceParameters.PARAM_NAME_ACTION_ID) + " into the activity-cache.");
 	 	 				
 	 	 				ServiceJobListener<ActivityListener> hsJobListener = new ServiceJobListener<ActivityListener>();
 	 	 				
 	 	 				hsJobListener.setWorkerServiceManager(workerServiceManager);
 	 	 				bbJobRegistry.registerJobListener(job, hsJobListener);
 	 	 				bbClientHandler.assign(job);
 	 	 				this.workerServiceManager.getUiInstanceService().update(serviceActivity);
 	 				}else{
 	 					updateOnFailure(job, serviceActivity); 	 					
 	 				}
 				}catch(Exception e){
 					log.fatal("unable to ping the worker service. Probably it is not alive. Aborting the current job.", e);
 					updateOnFailure(job, serviceActivity);
 				}
 			} 			
 		}		
	}
	
	private void updateOnFailure(BlackboardJob aJob, ServiceActivity aServiceActivity){
		try {
			aJob.setError("aborting current job, because no worker service available!");
			aJob.setActionStatus(ActionStatus.FAILED);
			instance.setOngoing(false);
			instance.setLastUpdateErrorMessage(aJob.getError());
			JobInfo jobInfo = aServiceActivity.getJobInfo();
			jobInfo.setActive(false);
			jobInfo.setJobStatus(JOBSTATUS.IDLE);
			this.workerServiceManager.updateAll(instance, aServiceActivity);
		} catch (ISRegistryException isRegException) {
			log.fatal("unable to update resource profile for id: " + instance.getResourceId(), isRegException);
		}		
	}
	
	/**
	 * creates a new blackboard job assigning to a service with this resource id
	 * @param aResourceId service resource id
	 * @return blackboard job
	 */
	protected BlackboardJob createBlackboardMessage(String aResourceId){
		BlackboardJob job = bbClientHandler.newJob(aResourceId);
		job.setAction(bbMsgAction.name());
		job.setActionStatus(ActionStatus.ASSIGNED);
		job.setId("bb-" + UUID.randomUUID().toString());		
		return job;
	}
	
	/**
	 * updates a blackboard-message with action and actionstatus in order to resend the message
	 * @param aJob the blackboard-message to update
	 * @param errorMsg the errorMsg to set - optional
	 */
	protected void updateBlackboardMessage(BlackboardJob aJob, String errorMsg){
		aJob.setAction(bbMsgAction.name());
		aJob.setActionStatus(ActionStatus.ASSIGNED);
		if (errorMsg.length() > 0){
			aJob.setError(instance.getLastUpdateErrorMessage());			
		}
	}

	/**
	 * @param workerServiceManager
	 */
	public void setWorkerServiceManager(T workerServiceManager) {
		this.workerServiceManager = workerServiceManager;
	}


	/**
	 * @return the worker service manager
	 */
	public T getWorkerServiceManager() {
		return workerServiceManager;
	}

	/**
	 * @param bbClientHandler the bbClientHandler to set
	 */
	public void setBbClientHandler(BlackboardClientHandler bbClientHandler) {
		this.bbClientHandler = bbClientHandler;
	}


	/**
	 * @return the bbClientHandler
	 */
	public BlackboardClientHandler getBbClientHandler() {
		return bbClientHandler;
	}

	/**
	 * @param bbJobRegistry the bbJobRegistry to set
	 */
	public void setBbJobRegistry(BlackboardJobRegistry bbJobRegistry) {
		this.bbJobRegistry = bbJobRegistry;
	}

	/**
	 * @return the bbJobRegistry
	 */
	public BlackboardJobRegistry getBbJobRegistry() {
		return bbJobRegistry;
	}

	/**
	 * @param instance the instance to set
	 */
	public void setInstance(E instance) {
		this.instance = instance;
	}

	/**
	 * @return the instance
	 */
	public E getInstance() {
		return instance;
	}

	public void setBbMsgAction(Action bbMsgAction) {
		this.bbMsgAction = bbMsgAction;
	}

	public Action getBbMsgAction() {
		return bbMsgAction;
	}

	/**
	 * @param triggerType the triggerType to set
	 */
	public void setTriggerType(TRIGGER_TYPE triggerType) {
		this.triggerType = triggerType;
	}

	/**
	 * @return the triggerType
	 */
	public TRIGGER_TYPE getTriggerType() {
		return triggerType;
	}
	
	class PingService implements Callable<String>{

		private IDriverService service;
		
		public PingService(IDriverService aService) {
			this.service = aService;
		}
		
		@Override
		public String call() throws Exception {
			return service.identify();
		}		
	}

}
