package uk.ac.ox.zoo.seeg.abraid.mp.datamanager.process;

import org.apache.log4j.Logger;
import org.joda.time.DateTime;
import uk.ac.ox.zoo.seeg.abraid.mp.common.domain.DiseaseGroup;
import uk.ac.ox.zoo.seeg.abraid.mp.common.domain.DiseaseOccurrence;
import uk.ac.ox.zoo.seeg.abraid.mp.common.domain.Location;
import uk.ac.ox.zoo.seeg.abraid.mp.common.service.core.DiseaseService;
import uk.ac.ox.zoo.seeg.abraid.mp.common.service.core.ModelRunService;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

import static ch.lambdaj.Lambda.extract;
import static ch.lambdaj.Lambda.on;

/**
 * Determines whether the model run should execute.
 * Copyright (c) 2014 University of Oxford
 */
public class ModelRunGatekeeper {
    private static final Logger LOGGER = Logger.getLogger(ModelRunManager.class);
    private static final String DISEASE_GROUP_ID_MESSAGE = "MODEL RUN PREPARATION FOR DISEASE GROUP %d (%s)";
    private static final String NO_VALIDATION_PARAMETERS_THRESHOLDS =
            "Threshold (minNewLocationsTrigger, minEnvSuitability or minDistanceFromExtent) has not been defined";
    private static final String NEVER_BEEN_EXECUTED_BEFORE =
            "Model run has never been executed before for this disease group";
    private static final String WEEK_HAS_NOT_ELAPSED = "A week has not elapsed since last model run preparation on %s";
    private static final String WEEK_HAS_ELAPSED = "At least a week has elapsed since last model run preparation on %s";
    private static final String ENOUGH_NEW_LOCATIONS = "Number of new locations has exceeded minimum required";
    private static final String NOT_ENOUGH_NEW_LOCATIONS = "Number of new locations has not exceeded minimum value";
    private static final String STARTING_MODEL_RUN_PREP = "Starting model run preparation";
    private static final String NOT_STARTING_MODEL_RUN_PREP = "Model run preparation will not be executed";

    private DiseaseService diseaseService;
    private ModelRunService modelRunService;

    public ModelRunGatekeeper(DiseaseService diseaseService, ModelRunService modelRunService) {
        this.diseaseService = diseaseService;
        this.modelRunService = modelRunService;
    }

    /**
     * Determines whether model run preparation tasks should be carried out.
     * NB. This method is only ever called for disease groups that have automatic model runs enabled, as a result of
     * modelRunManager.getDiseaseGroupIdsForAutomaticModelRuns() in Main.
     *
     * @param diseaseGroupId The id of the disease group for which the model run is being prepared.
     * @return True if:
     *  - there is no lastModelRunPrepDate for the disease group (ie model has never been run) or
     *  - more than a week has passed since last run, or
     *  - there have been more new locations (more than a week ago) than the minimum required for the disease group.
     * False if any of the 3 thresholds required for performing the number of distinct new locations check
     * (minNewLocationsTrigger, minEnvSuitability, minDistanceFromExtent) are not specified for the disease group.
     */
    public boolean modelShouldRun(int diseaseGroupId) {
        DiseaseGroup diseaseGroup = diseaseService.getDiseaseGroupById(diseaseGroupId);
        LOGGER.info(String.format(DISEASE_GROUP_ID_MESSAGE, diseaseGroupId, diseaseGroup.getName()));

        boolean dueToRun = neverBeenRunOrWeekHasElapsed(diseaseGroup) || enoughNewLocations(diseaseGroup);

        LOGGER.info(dueToRun ? STARTING_MODEL_RUN_PREP : NOT_STARTING_MODEL_RUN_PREP);
        return dueToRun;
    }

    private boolean neverBeenRunOrWeekHasElapsed(DiseaseGroup diseaseGroup) {
        DateTime lastModelRunPrepDate = diseaseGroup.getLastModelRunPrepDate();
        if (lastModelRunPrepDate == null) {
            LOGGER.info(NEVER_BEEN_EXECUTED_BEFORE);
            return true;
        } else {
            DateTime comparisonDate = modelRunService.subtractDaysBetweenModelRuns(DateTime.now());
            DateTime lastModelRunPrepDay = lastModelRunPrepDate.withTimeAtStartOfDay();
            final boolean weekHasElapsed = !comparisonDate.isBefore(lastModelRunPrepDay);
            LOGGER.info(String.format(weekHasElapsed ? WEEK_HAS_ELAPSED : WEEK_HAS_NOT_ELAPSED, lastModelRunPrepDate));
            return weekHasElapsed;
        }
    }

    private boolean enoughNewLocations(DiseaseGroup diseaseGroup) {
        if (thresholdsDefined(diseaseGroup)) {
            long count = getDistinctLocationsCount(diseaseGroup.getId());
            int minimum = diseaseGroup.getMinNewLocationsTrigger();

            boolean hasEnoughNewLocations = count >= minimum;
            LOGGER.info(hasEnoughNewLocations ? ENOUGH_NEW_LOCATIONS : NOT_ENOUGH_NEW_LOCATIONS);
            return hasEnoughNewLocations;
        } else {
            LOGGER.info(NO_VALIDATION_PARAMETERS_THRESHOLDS);
            return false;
        }
    }

    private boolean thresholdsDefined(DiseaseGroup diseaseGroup) {
        return (diseaseGroup.getMinNewLocationsTrigger() != null) &&
               (diseaseGroup.getMinEnvironmentalSuitability() != null) &&
               (diseaseGroup.getMinDistanceFromDiseaseExtent() != null);
    }

    private long getDistinctLocationsCount(int diseaseGroupId) {
        DateTime endDate = modelRunService.subtractDaysBetweenModelRuns(DateTime.now());
        DateTime startDate = modelRunService.subtractDaysBetweenModelRuns(endDate);

        List<DiseaseOccurrence> occurrences =
                diseaseService.getDiseaseOccurrencesForTriggeringModelRun(diseaseGroupId, startDate, endDate);
        Set<Location> locations = new HashSet<>(extract(occurrences, on(DiseaseOccurrence.class).getLocation()));
        return locations.size();
    }
}

@@ -1284,25 +1284,16 @@
  String 
-WEEK_HAS_
 NOT_ELAP
@@ -1303,22 +1303,28 @@
  = %22
-A week has not
+Not enough days have
  ela
@@ -1401,25 +1401,16 @@
  String 
-WEEK_HAS_
 ELAPSED 
@@ -1416,27 +1416,24 @@
  = %22
-At least a week has
+Enough days have
  ela
@@ -2869,24 +2869,98 @@
 ease group.%0A
+     * Note that %22a week%22 is configurable (i.e. days between model runs).%0A
      * False
@@ -3457,38 +3457,46 @@
 = neverBeenRunOr
-WeekHa
+DaysBetweenRun
 sElapsed(disease
@@ -3697,14 +3697,22 @@
 unOr
-WeekHa
+DaysBetweenRun
 sEla
@@ -4178,22 +4178,30 @@
 boolean 
-weekHa
+daysBetweenRun
 sElapsed
@@ -4288,22 +4288,30 @@
 .format(
-weekHa
+daysBetweenRun
 sElapsed
@@ -4313,25 +4313,16 @@
 apsed ? 
-WEEK_HAS_
 ELAPSED 
@@ -4327,17 +4327,8 @@
 D : 
-WEEK_HAS_
 NOT_
@@ -4383,14 +4383,22 @@
 urn 
-weekHa
+daysBetweenRun
 sEla

Fix logging in ModelRunGatekeeper (week -> days between model runs)