package eu.dnetlib.msro.cron;

import java.util.Date;

import javax.annotation.Resource;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.quartz.CronExpression;
import org.springframework.beans.factory.annotation.Required;

import com.googlecode.sarasvati.GraphProcess;

import eu.dnetlib.enabling.is.lookup.rmi.ISLookUpException;
import eu.dnetlib.enabling.is.lookup.rmi.ISLookUpService;
import eu.dnetlib.enabling.locators.UniqueServiceLocator;
import eu.dnetlib.miscutils.datetime.DateUtils;
import eu.dnetlib.msro.workflows.sarasvati.loader.WorkflowExecutor;
import eu.dnetlib.msro.workflows.sarasvati.registry.GraphProcessRegistry;

public class ScheduledWorkflowLauncher {

	private static final Log log = LogFactory.getLog(ScheduledWorkflowLauncher.class);

	private static final DateUtils dateUtils = new DateUtils();

	private WorkflowExecutor workflowExecutor;

	private GraphProcessRegistry graphProcessRegistry;

	private int windowSize = 1800000; // 30 minutes

	private final static int millisInMinute = 60000;

	@Resource
	private UniqueServiceLocator serviceLocator;

	public void verifySheduledWorkflows() {
		log.debug("Verifying scheduled workflows - START");

		final String query = "for $x in collection('/db/DRIVER/MetaWorkflowDSResources/MetaWorkflowDSResourceType') " +
				"where $x//CONFIGURATION/@status='EXECUTABLE' " +
				"and $x//SCHEDULING/@enabled='true' " +
				"return concat($x//RESOURCE_IDENTIFIER/@value, ' @@@ ', $x//SCHEDULING/CRON, ' @@@ ', $x//SCHEDULING/MININTERVAL)";

		try {
			final int defaultMinInterval = windowSize / millisInMinute;
			for (final String s : serviceLocator.getService(ISLookUpService.class).quickSearchProfile(query)) {
				final String[] arr = s.split("@@@");
				final String id = arr[0].trim();
				final String cron = arr[1].trim();
				final int minInterval = Math.max(NumberUtils.toInt(arr[2].trim(), 0), defaultMinInterval);
				final Date lastExecutionDate = calculateLastExecutionDate(id);

				if (isReady(id, cron, minInterval, lastExecutionDate, new Date()) && !isAlreadyRunning(id)) {
					try {
						workflowExecutor.startMetaWorkflow(id, false);
					} catch (final Exception e) {
						log.debug("Error launching scheduled wf: " + id, e);
					}
				}
			}
		} catch (final ISLookUpException e) {
			log.error("Error executing query " + query);
		}

		log.debug("Verifying scheduled workflows - END");
	}

	protected boolean isReady(final String id, final String cron, final int minInterval, final Date lastExecutionDate, final Date now) {
		if (CronExpression.isValidExpression(cron)) {
			final int minIntervalMillis = minInterval * 60000; // minutes to millis;

			final boolean res;
			if (lastExecutionDate != null) {
				final int elapsed = Math.round(now.getTime() - lastExecutionDate.getTime());
				res = (elapsed > minIntervalMillis) && verifyCron(cron, now);
			} else {
				res = verifyCron(cron, now);
			}

			if (log.isDebugEnabled()) {
				log.debug("**************************************************************");
				log.debug("META WORKFLOW ID       : " + id);
				log.debug("NOW                    : " + now);
				log.debug("LAST EXECUTION DATE    : " + lastExecutionDate);
				log.debug("MIN INTERVAL (minutes) : " + minInterval);
				log.debug("REAL MIN INTERVAL (ms) : " + minIntervalMillis);
				log.debug("WINDOW SIZE (ms)       : " + windowSize);
				log.debug("MUST BE EXECUTED       : " + res);
				log.debug("**************************************************************");
			}

			return res;
		}

		return false;
	}

	private boolean verifyCron(final String cronExpression, final Date now) {
		try {
			final CronExpression cron = new CronExpression(cronExpression);

			final Date date = new Date(now.getTime() - windowSize);

			final Date cronDate = cron.getNextValidTimeAfter(date);

			if (log.isDebugEnabled()) {
				log.debug("NEXT EXECUTION DATE: " + cronDate);
				log.debug("FIRED              : " + (cronDate.getTime() < now.getTime()));
			}
			return cronDate.getTime() < now.getTime();
		} catch (final Exception e) {
			log.error("Error calculating next cron event: " + cronExpression, e);
			return false;
		}
	}

	private boolean isAlreadyRunning(final String metaWfId) {
		final String query = "doc('/db/DRIVER/MetaWorkflowDSResources/MetaWorkflowDSResourceType/" + StringUtils.substringBefore(metaWfId, "_")
				+ "')//WORKFLOW/@id/string()";

		try {
			for (final String profileId : serviceLocator.getService(ISLookUpService.class).quickSearchProfile(query)) {
				if (profileId.length() > 0) {
					for (final GraphProcess p : graphProcessRegistry.findProcessesByResource(profileId)) {
						switch (p.getState()) {
						case Created:
							return true;
						case Executing:
							return true;
						default:
							break;
						}
					}
				}
			}
		} catch (final ISLookUpException e) {
			log.error("Error executing query " + query);
		}
		return false;
	}

	private Date calculateLastExecutionDate(final String id) {
		final String query = "for $id in doc('/db/DRIVER/MetaWorkflowDSResources/MetaWorkflowDSResourceType/" + StringUtils.substringBefore(id, "_")
				+ "')//WORKFLOW/@id/string() " +
				"for $x in doc(concat('/db/DRIVER/WorkflowDSResources/WorkflowDSResourceType/', substring-before($id, '_'))) " +
				"where $x//LAST_EXECUTION_STATUS = 'SUCCESS' " +
				"return $x//LAST_EXECUTION_DATE/text() ";

		long time = 0;
		try {
			for (final String s : serviceLocator.getService(ISLookUpService.class).quickSearchProfile(query)) {
				if (s.length() > 0) {
					final Date d = dateUtils.parse(s);
					if (time < d.getTime()) {
						time = d.getTime();
					}
				}
			}
		} catch (final ISLookUpException e) {
			log.error("Error executing query " + query);
		} catch (final Exception e) {
			log.error("Error calculating date", e);
		}

		return time > 0 ? new Date(time) : null;
	}

	public WorkflowExecutor getWorkflowExecutor() {
		return workflowExecutor;
	}

	@Required
	public void setWorkflowExecutor(final WorkflowExecutor workflowExecutor) {
		this.workflowExecutor = workflowExecutor;
	}

	public GraphProcessRegistry getGraphProcessRegistry() {
		return graphProcessRegistry;
	}

	@Required
	public void setGraphProcessRegistry(final GraphProcessRegistry graphProcessRegistry) {
		this.graphProcessRegistry = graphProcessRegistry;
	}

	public int getWindowSize() {
		return windowSize;
	}

	@Required
	public void setWindowSize(final int windowSize) {
		this.windowSize = windowSize;
	}

}
