package eu.dnetlib.msro.cron;

import java.util.Date;

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

import eu.dnetlib.enabling.locators.UniqueServiceLocator;
import eu.dnetlib.miscutils.datetime.DateUtils;
import eu.dnetlib.msro.workflows.procs.ProcessRegistry;
import eu.dnetlib.msro.workflows.procs.WorkflowExecutor;
import eu.dnetlib.msro.workflows.procs.WorkflowProcess;
import eu.dnetlib.rmi.enabling.ISLookUpException;
import eu.dnetlib.rmi.enabling.ISLookUpService;

public class ScheduledWorkflowLauncher {

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

	private static final DateUtils dateUtils = new DateUtils();

	private WorkflowExecutor workflowExecutor;

	private ProcessRegistry processRegistry;

	private int windowSize = 1800000; // 30 minutes

	@Autowired
	private UniqueServiceLocator serviceLocator;

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

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

		try {
			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 = NumberUtils.toInt(arr[2].trim(), 0);
				final Date lastExecutionDate = calculateLastExecutionDate(id);

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

			}
		} catch (final ISLookUpException e) {
			log.error("Error executing query " + query);
		}

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

	private 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 wfId) {
		for (final WorkflowProcess p : processRegistry.findProcsByOtherId(wfId)) {
			switch (p.getStatus()) {
			case CREATED:
				return true;
			case EXECUTING:
				return true;
			default:
				break;
			}
		}
		return false;
	}

	private Date calculateLastExecutionDate(final String id) {
		final String query = "for $x in doc('/db/DRIVER/WorkflowDSResources/WorkflowDSResourceType/" + StringUtils.substringBefore(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 ProcessRegistry getProcessRegistry() {
		return processRegistry;
	}

	@Required
	public void setProcessRegistry(final ProcessRegistry processRegistry) {
		this.processRegistry = processRegistry;
	}

	public int getWindowSize() {
		return windowSize;
	}

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

}
