package net.joelinn.quartz.jobstore;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import net.joelinn.quartz.jobstore.jedis.JedisClusterCommandsWrapper;
import org.quartz.Calendar;
import org.quartz.*;
import org.quartz.impl.matchers.GroupMatcher;
import org.quartz.impl.matchers.StringMatcher;
import org.quartz.spi.OperableTrigger;
import org.quartz.spi.SchedulerSignaler;
import org.quartz.spi.TriggerFiredBundle;
import org.quartz.spi.TriggerFiredResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;

/**
 * Joe Linn
 * 8/22/2015
 */
public class RedisClusterStorage extends AbstractRedisStorage<JedisClusterCommandsWrapper> {
    private static final Logger logger = LoggerFactory.getLogger(RedisClusterStorage.class);

    public RedisClusterStorage(RedisJobStoreSchema redisSchema, ObjectMapper mapper, SchedulerSignaler signaler, String schedulerInstanceId, int lockTimeout) {
        super(redisSchema, mapper, signaler, schedulerInstanceId, lockTimeout);
    }

    /**
     * Store a job in Redis
     *
     * @param jobDetail       the {@link JobDetail} object to be stored
     * @param replaceExisting if true, any existing job with the same group and name as the given job will be overwritten
     * @param jedis           a thread-safe Redis connection
     * @throws ObjectAlreadyExistsException
     */
    @Override
    @SuppressWarnings("unchecked")
    public void storeJob(JobDetail jobDetail, boolean replaceExisting, JedisClusterCommandsWrapper jedis) throws ObjectAlreadyExistsException {
        final String jobHashKey = redisSchema.jobHashKey(jobDetail.getKey());
        final String jobDataMapHashKey = redisSchema.jobDataMapHashKey(jobDetail.getKey());
        final String jobGroupSetKey = redisSchema.jobGroupSetKey(jobDetail.getKey());

        if (!replaceExisting && jedis.exists(jobHashKey)) {
            throw new ObjectAlreadyExistsException(jobDetail);
        }

        jedis.hmset(jobHashKey, (Map<String, String>) mapper.convertValue(jobDetail, new TypeReference<HashMap<String, String>>() {
        }));
        jedis.del(jobDataMapHashKey);
        if (jobDetail.getJobDataMap() != null && !jobDetail.getJobDataMap().isEmpty()) {
            jedis.hmset(jobDataMapHashKey, getStringDataMap(jobDetail.getJobDataMap()));
        }

        jedis.sadd(redisSchema.jobsSet(), jobHashKey);
        jedis.sadd(redisSchema.jobGroupsSet(), jobGroupSetKey);
        jedis.sadd(jobGroupSetKey, jobHashKey);
    }

    /**
     * Remove the given job from Redis
     *
     * @param jobKey the job to be removed
     * @param jedis  a thread-safe Redis connection
     * @return true if the job was removed; false if it did not exist
     */
    @Override
    public boolean removeJob(JobKey jobKey, JedisClusterCommandsWrapper jedis) throws JobPersistenceException {
        final String jobHashKey = redisSchema.jobHashKey(jobKey);
        final String jobBlockedKey = redisSchema.jobBlockedKey(jobKey);
        final String jobDataMapHashKey = redisSchema.jobDataMapHashKey(jobKey);
        final String jobGroupSetKey = redisSchema.jobGroupSetKey(jobKey);
        final String jobTriggerSetKey = redisSchema.jobTriggersSetKey(jobKey);

        // remove the job and any associated data
        Long delJobHashKeyResponse = jedis.del(jobHashKey);
        // remove the blocked job key
        jedis.del(jobBlockedKey);
        // remove the job's data map
        jedis.del(jobDataMapHashKey);
        // remove the job from the set of all jobs
        jedis.srem(redisSchema.jobsSet(), jobHashKey);
        // remove the job from the set of blocked jobs
        jedis.srem(redisSchema.blockedJobsSet(), jobHashKey);
        // remove the job from its group
        jedis.srem(jobGroupSetKey, jobHashKey);
        // retrieve the keys for all triggers associated with this job, then delete that set
        Set<String> jobTriggerSetResponse = jedis.smembers(jobTriggerSetKey);
        jedis.del(jobTriggerSetKey);
        Long jobGroupSetSizeResponse = jedis.scard(jobGroupSetKey);
        if (jobGroupSetSizeResponse == 0) {
            // The group now contains no jobs. Remove it from the set of all job groups.
            jedis.srem(redisSchema.jobGroupsSet(), jobGroupSetKey);
        }

        // remove all triggers associated with this job
        for (String triggerHashKey : jobTriggerSetResponse) {
            // get this trigger's TriggerKey
            final TriggerKey triggerKey = redisSchema.triggerKey(triggerHashKey);
            final String triggerGroupSetKey = redisSchema.triggerGroupSetKey(triggerKey);
            unsetTriggerState(triggerHashKey, jedis);
            // remove the trigger from the set of all triggers
            jedis.srem(redisSchema.triggersSet(), triggerHashKey);
            // remove the trigger's group from the set of all trigger groups
            jedis.srem(redisSchema.triggerGroupsSet(), triggerGroupSetKey);
            // remove this trigger from its group
            jedis.srem(triggerGroupSetKey, triggerHashKey);
            // delete the trigger
            jedis.del(triggerHashKey);
        }
        return delJobHashKeyResponse == 1;
    }

    /**
     * Store a trigger in redis
     *
     * @param trigger         the trigger to be stored
     * @param replaceExisting true if an existing trigger with the same identity should be replaced
     * @param jedis           a thread-safe Redis connection
     * @throws JobPersistenceException
     * @throws ObjectAlreadyExistsException
     */
    @Override
    public void storeTrigger(OperableTrigger trigger, boolean replaceExisting, JedisClusterCommandsWrapper jedis) throws JobPersistenceException {
        final String triggerHashKey = redisSchema.triggerHashKey(trigger.getKey());
        final String triggerGroupSetKey = redisSchema.triggerGroupSetKey(trigger.getKey());
        final String jobTriggerSetKey = redisSchema.jobTriggersSetKey(trigger.getJobKey());

        if (!(trigger instanceof SimpleTrigger) && !(trigger instanceof CronTrigger)) {
            throw new UnsupportedOperationException("Only SimpleTrigger and CronTrigger are supported.");
        }
        final boolean exists = jedis.exists(triggerHashKey);
        if (exists && !replaceExisting) {
            throw new ObjectAlreadyExistsException(trigger);
        }

        Map<String, String> triggerMap = mapper.convertValue(trigger, new TypeReference<HashMap<String, String>>() {
        });
        triggerMap.put(TRIGGER_CLASS, trigger.getClass().getName());

        jedis.hmset(triggerHashKey, triggerMap);
        jedis.sadd(redisSchema.triggersSet(), triggerHashKey);
        jedis.sadd(redisSchema.triggerGroupsSet(), triggerGroupSetKey);
        jedis.sadd(triggerGroupSetKey, triggerHashKey);
        jedis.sadd(jobTriggerSetKey, triggerHashKey);
        if (trigger.getCalendarName() != null && !trigger.getCalendarName().isEmpty()) {
            final String calendarTriggersSetKey = redisSchema.calendarTriggersSetKey(trigger.getCalendarName());
            jedis.sadd(calendarTriggersSetKey, triggerHashKey);
        }
        if (trigger.getJobDataMap() != null && !trigger.getJobDataMap().isEmpty()) {
            final String triggerDataMapHashKey = redisSchema.triggerDataMapHashKey(trigger.getKey());
            jedis.hmset(triggerDataMapHashKey, getStringDataMap(trigger.getJobDataMap()));
        }

        if (exists) {
            // We're overwriting a previously stored instance of this trigger, so clear any existing trigger state.
            unsetTriggerState(triggerHashKey, jedis);
        }

        Boolean triggerPausedResponse = jedis.sismember(redisSchema.pausedTriggerGroupsSet(), triggerGroupSetKey);
        Boolean jobPausedResponse = jedis.sismember(redisSchema.pausedJobGroupsSet(), redisSchema.jobGroupSetKey(trigger.getJobKey()));

        if (triggerPausedResponse || jobPausedResponse) {
            final long nextFireTime = trigger.getNextFireTime() != null ? trigger.getNextFireTime().getTime() : -1;
            final String jobHashKey = redisSchema.jobHashKey(trigger.getJobKey());
            if (isBlockedJob(jobHashKey, jedis)) {
                setTriggerState(RedisTriggerState.PAUSED_BLOCKED, (double) nextFireTime, triggerHashKey, jedis);
            } else {
                setTriggerState(RedisTriggerState.PAUSED, (double) nextFireTime, triggerHashKey, jedis);
            }
        } else if (trigger.getNextFireTime() != null) {
            setTriggerState(RedisTriggerState.WAITING, (double) trigger.getNextFireTime().getTime(), triggerHashKey, jedis);
        }
    }

    /**
     * Remove (delete) the <code>{@link Trigger}</code> with the given key.
     *
     * @param triggerKey          the key of the trigger to be removed
     * @param removeNonDurableJob if true, the job associated with the given trigger will be removed if it is non-durable
     *                            and has no other triggers
     * @param jedis               a thread-safe Redis connection
     * @return true if the trigger was found and removed
     */
    @Override
    protected boolean removeTrigger(TriggerKey triggerKey, boolean removeNonDurableJob, JedisClusterCommandsWrapper jedis) throws JobPersistenceException, ClassNotFoundException {
        final String triggerHashKey = redisSchema.triggerHashKey(triggerKey);
        final String triggerGroupSetKey = redisSchema.triggerGroupSetKey(triggerKey);

        if (!jedis.exists(triggerHashKey)) {
            return false;
        }

        OperableTrigger trigger = retrieveTrigger(triggerKey, jedis);

        final String jobHashKey = redisSchema.jobHashKey(trigger.getJobKey());
        final String jobTriggerSetKey = redisSchema.jobTriggersSetKey(trigger.getJobKey());

        // remove the trigger from the set of all triggers
        jedis.srem(redisSchema.triggersSet(), triggerHashKey);
        // remove the trigger from its trigger group set
        jedis.srem(triggerGroupSetKey, triggerHashKey);
        // remove the trigger from the associated job's trigger set
        jedis.srem(jobTriggerSetKey, triggerHashKey);

        if (jedis.scard(triggerGroupSetKey) == 0) {
            // The trigger group set is empty. Remove the trigger group from the set of trigger groups.
            jedis.srem(redisSchema.triggerGroupsSet(), triggerGroupSetKey);
        }

        if (removeNonDurableJob) {
            Long jobTriggerSetKeySizeResponse = jedis.scard(jobTriggerSetKey);
            Boolean jobExistsResponse = jedis.exists(jobHashKey);
            if (jobTriggerSetKeySizeResponse == 0 && jobExistsResponse) {
                JobDetail job = retrieveJob(trigger.getJobKey(), jedis);
                if (!job.isDurable()) {
                    // Job is not durable and has no remaining triggers. Delete it.
                    removeJob(job.getKey(), jedis);
                    signaler.notifySchedulerListenersJobDeleted(job.getKey());
                }
            }
        }

        if (isNullOrEmpty(trigger.getCalendarName())) {
            jedis.srem(redisSchema.calendarTriggersSetKey(trigger.getCalendarName()), triggerHashKey);
        }
        unsetTriggerState(triggerHashKey, jedis);
        jedis.del(triggerHashKey);
        return true;
    }

    /**
     * Unsets the state of the given trigger key by removing the trigger from all trigger state sets.
     *
     * @param triggerHashKey the redis key of the desired trigger hash
     * @param jedis          a thread-safe Redis connection
     * @return true if the trigger was removed, false if the trigger was stateless
     * @throws JobPersistenceException if the unset operation failed
     */
    @Override
    public boolean unsetTriggerState(String triggerHashKey, JedisClusterCommandsWrapper jedis) throws JobPersistenceException {
        boolean removed = false;
        List<Long> responses = new ArrayList<>(RedisTriggerState.values().length);
        for (RedisTriggerState state : RedisTriggerState.values()) {
            responses.add(jedis.zrem(redisSchema.triggerStateKey(state), triggerHashKey));
        }
        for (Long response : responses) {
            removed = response == 1;
            if (removed) {
                jedis.del(redisSchema.triggerLockKey(redisSchema.triggerKey(triggerHashKey)));
                break;
            }
        }
        return removed;
    }

    /**
     * Store a {@link Calendar}
     *
     * @param name            the name of the calendar
     * @param calendar        the calendar object to be stored
     * @param replaceExisting if true, any existing calendar with the same name will be overwritten
     * @param updateTriggers  if true, any existing triggers associated with the calendar will be updated
     * @param jedis           a thread-safe Redis connection
     * @throws JobPersistenceException
     */
    @Override
    public void storeCalendar(String name, Calendar calendar, boolean replaceExisting, boolean updateTriggers, JedisClusterCommandsWrapper jedis) throws JobPersistenceException {
        final String calendarHashKey = redisSchema.calendarHashKey(name);
        if (!replaceExisting && jedis.exists(calendarHashKey)) {
            throw new ObjectAlreadyExistsException(String.format("Calendar with key %s already exists.", calendarHashKey));
        }
        Map<String, String> calendarMap = new HashMap<>();
        calendarMap.put(CALENDAR_CLASS, calendar.getClass().getName());
        try {
            calendarMap.put(CALENDAR_JSON, mapper.writeValueAsString(calendar));
        } catch (JsonProcessingException e) {
            throw new JobPersistenceException("Unable to serialize calendar.", e);
        }

        jedis.hmset(calendarHashKey, calendarMap);
        jedis.sadd(redisSchema.calendarsSet(), calendarHashKey);

        if (updateTriggers) {
            final String calendarTriggersSetKey = redisSchema.calendarTriggersSetKey(name);
            Set<String> triggerHashKeys = jedis.smembers(calendarTriggersSetKey);
            for (String triggerHashKey : triggerHashKeys) {
                OperableTrigger trigger = retrieveTrigger(redisSchema.triggerKey(triggerHashKey), jedis);
                long removed = jedis.zrem(redisSchema.triggerStateKey(RedisTriggerState.WAITING), triggerHashKey);
                trigger.updateWithNewCalendar(calendar, misfireThreshold);
                if (removed == 1) {
                    setTriggerState(RedisTriggerState.WAITING, (double) trigger.getNextFireTime().getTime(), triggerHashKey, jedis);
                }
            }
        }
    }

    /**
     * Remove (delete) the <code>{@link Calendar}</code> with the given name.
     *
     * @param calendarName the name of the calendar to be removed
     * @param jedis        a thread-safe Redis connection
     * @return true if a calendar with the given name was found and removed
     */
    @Override
    public boolean removeCalendar(String calendarName, JedisClusterCommandsWrapper jedis) throws JobPersistenceException {
        final String calendarTriggersSetKey = redisSchema.calendarTriggersSetKey(calendarName);

        if (jedis.scard(calendarTriggersSetKey) > 0) {
            throw new JobPersistenceException(String.format("There are triggers pointing to calendar %s, so it cannot be removed.", calendarName));
        }
        final String calendarHashKey = redisSchema.calendarHashKey(calendarName);
        Long deleteResponse = jedis.del(calendarHashKey);
        jedis.srem(redisSchema.calendarsSet(), calendarHashKey);

        return deleteResponse == 1;
    }

    /**
     * Get the keys of all of the <code>{@link Job}</code> s that have the given group name.
     *
     * @param matcher the matcher with which to compare group names
     * @param jedis   a thread-safe Redis connection
     * @return the set of all JobKeys which have the given group name
     */
    @Override
    public Set<JobKey> getJobKeys(GroupMatcher<JobKey> matcher, JedisClusterCommandsWrapper jedis) {
        Set<JobKey> jobKeys = new HashSet<>();
        if (matcher.getCompareWithOperator() == StringMatcher.StringOperatorName.EQUALS) {
            final String jobGroupSetKey = redisSchema.jobGroupSetKey(new JobKey("", matcher.getCompareToValue()));
            final Set<String> jobs = jedis.smembers(jobGroupSetKey);
            if (jobs != null) {
                for (final String job : jobs) {
                    jobKeys.add(redisSchema.jobKey(job));
                }
            }
        } else {
            List<Set<String>> jobGroups = new ArrayList<>();
            for (final String jobGroupSetKey : jedis.smembers(redisSchema.jobGroupsSet())) {
                if (matcher.getCompareWithOperator().evaluate(redisSchema.jobGroup(jobGroupSetKey), matcher.getCompareToValue())) {
                    jobGroups.add(jedis.smembers(jobGroupSetKey));
                }
            }
            for (Set<String> jobGroup : jobGroups) {
                if (jobGroup != null) {
                    for (final String job : jobGroup) {
                        jobKeys.add(redisSchema.jobKey(job));
                    }
                }
            }
        }
        return jobKeys;
    }

    /**
     * Get the names of all of the <code>{@link Trigger}</code> s that have the given group name.
     *
     * @param matcher the matcher with which to compare group names
     * @param jedis   a thread-safe Redis connection
     * @return the set of all TriggerKeys which have the given group name
     */
    @Override
    public Set<TriggerKey> getTriggerKeys(GroupMatcher<TriggerKey> matcher, JedisClusterCommandsWrapper jedis) {
        Set<TriggerKey> triggerKeys = new HashSet<>();
        if (matcher.getCompareWithOperator() == StringMatcher.StringOperatorName.EQUALS) {
            final String triggerGroupSetKey = redisSchema.triggerGroupSetKey(new TriggerKey("", matcher.getCompareToValue()));
            final Set<String> triggers = jedis.smembers(triggerGroupSetKey);
            if (triggers != null) {
                for (final String trigger : triggers) {
                    triggerKeys.add(redisSchema.triggerKey(trigger));
                }
            }
        } else {
            List<Set<String>> triggerGroups = new ArrayList<>();
            for (final String triggerGroupSetKey : jedis.smembers(redisSchema.triggerGroupsSet())) {
                if (matcher.getCompareWithOperator().evaluate(redisSchema.triggerGroup(triggerGroupSetKey), matcher.getCompareToValue())) {
                    triggerGroups.add(jedis.smembers(triggerGroupSetKey));
                }
            }
            for (Set<String> triggerGroup : triggerGroups) {
                if (triggerGroup != null) {
                    for (final String trigger : triggerGroup) {
                        triggerKeys.add(redisSchema.triggerKey(trigger));
                    }
                }
            }
        }
        return triggerKeys;
    }

    /**
     * Get the current state of the identified <code>{@link Trigger}</code>.
     *
     * @param triggerKey the key of the desired trigger
     * @param jedis      a thread-safe Redis connection
     * @return the state of the trigger
     */
    @Override
    public Trigger.TriggerState getTriggerState(TriggerKey triggerKey, JedisClusterCommandsWrapper jedis) {
        final String triggerHashKey = redisSchema.triggerHashKey(triggerKey);
        Map<RedisTriggerState, Double> scores = new HashMap<>(RedisTriggerState.values().length);
        for (RedisTriggerState redisTriggerState : RedisTriggerState.values()) {
            scores.put(redisTriggerState, jedis.zscore(redisSchema.triggerStateKey(redisTriggerState), triggerHashKey));
        }
        for (Map.Entry<RedisTriggerState, Double> entry : scores.entrySet()) {
            if (entry.getValue() != null) {
                return entry.getKey().getTriggerState();
            }
        }
        return Trigger.TriggerState.NONE;
    }

    /**
     * Pause the trigger with the given key
     *
     * @param triggerKey the key of the trigger to be paused
     * @param jedis      a thread-safe Redis connection
     * @throws JobPersistenceException if the desired trigger does not exist
     */
    @Override
    public void pauseTrigger(TriggerKey triggerKey, JedisClusterCommandsWrapper jedis) throws JobPersistenceException {
        final String triggerHashKey = redisSchema.triggerHashKey(triggerKey);
        Boolean exists = jedis.exists(triggerHashKey);
        Double completedScore = jedis.zscore(redisSchema.triggerStateKey(RedisTriggerState.COMPLETED), triggerHashKey);
        String nextFireTimeResponse = jedis.hget(triggerHashKey, TRIGGER_NEXT_FIRE_TIME);
        Double blockedScore = jedis.zscore(redisSchema.triggerStateKey(RedisTriggerState.BLOCKED), triggerHashKey);

        if (!exists) {
            return;
        }
        if (completedScore != null) {
            // doesn't make sense to pause a completed trigger
            return;
        }

        final long nextFireTime = nextFireTimeResponse == null
                || nextFireTimeResponse.isEmpty() ? -1 : Long.parseLong(nextFireTimeResponse);
        if (blockedScore != null) {
            setTriggerState(RedisTriggerState.PAUSED_BLOCKED, (double) nextFireTime, triggerHashKey, jedis);
        } else {
            setTriggerState(RedisTriggerState.PAUSED, (double) nextFireTime, triggerHashKey, jedis);
        }
    }

    /**
     * Pause all of the <code>{@link Trigger}s</code> in the given group.
     *
     * @param matcher matcher for the trigger groups to be paused
     * @param jedis   a thread-safe Redis connection
     * @return a collection of names of trigger groups which were matched and paused
     * @throws JobPersistenceException
     */
    @Override
    public Collection<String> pauseTriggers(GroupMatcher<TriggerKey> matcher, JedisClusterCommandsWrapper jedis) throws JobPersistenceException {
        Set<String> pausedTriggerGroups = new HashSet<>();
        if (matcher.getCompareWithOperator() == StringMatcher.StringOperatorName.EQUALS) {
            final String triggerGroupSetKey = redisSchema.triggerGroupSetKey(new TriggerKey("", matcher.getCompareToValue()));
            final long addResult = jedis.sadd(redisSchema.pausedTriggerGroupsSet(), triggerGroupSetKey);
            if (addResult > 0) {
                for (final String trigger : jedis.smembers(triggerGroupSetKey)) {
                    pauseTrigger(redisSchema.triggerKey(trigger), jedis);
                }
                pausedTriggerGroups.add(redisSchema.triggerGroup(triggerGroupSetKey));
            }
        } else {
            Map<String, Set<String>> triggerGroups = new HashMap<>();
            for (final String triggerGroupSetKey : jedis.smembers(redisSchema.triggerGroupsSet())) {
                if (matcher.getCompareWithOperator().evaluate(redisSchema.triggerGroup(triggerGroupSetKey), matcher.getCompareToValue())) {
                    triggerGroups.put(triggerGroupSetKey, jedis.smembers(triggerGroupSetKey));
                }
            }
            for (final Map.Entry<String, Set<String>> entry : triggerGroups.entrySet()) {
                if (jedis.sadd(redisSchema.pausedJobGroupsSet(), entry.getKey()) > 0) {
                    // This trigger group was not paused. Pause it now.
                    pausedTriggerGroups.add(redisSchema.triggerGroup(entry.getKey()));
                    for (final String triggerHashKey : entry.getValue()) {
                        pauseTrigger(redisSchema.triggerKey(triggerHashKey), jedis);
                    }
                }
            }
        }
        return pausedTriggerGroups;
    }

    /**
     * Pause all of the <code>{@link Job}s</code> in the given group - by pausing all of their
     * <code>Trigger</code>s.
     *
     * @param groupMatcher the mather which will determine which job group should be paused
     * @param jedis        a thread-safe Redis connection
     * @return a collection of names of job groups which have been paused
     * @throws JobPersistenceException
     */
    @Override
    public Collection<String> pauseJobs(GroupMatcher<JobKey> groupMatcher, JedisClusterCommandsWrapper jedis) throws JobPersistenceException {
        Set<String> pausedJobGroups = new HashSet<>();
        if (groupMatcher.getCompareWithOperator() == StringMatcher.StringOperatorName.EQUALS) {
            final String jobGroupSetKey = redisSchema.jobGroupSetKey(new JobKey("", groupMatcher.getCompareToValue()));
            if (jedis.sadd(redisSchema.pausedJobGroupsSet(), jobGroupSetKey) > 0) {
                pausedJobGroups.add(redisSchema.jobGroup(jobGroupSetKey));
                for (String job : jedis.smembers(jobGroupSetKey)) {
                    pauseJob(redisSchema.jobKey(job), jedis);
                }
            }
        } else {
            Map<String, Set<String>> jobGroups = new HashMap<>();
            for (final String jobGroupSetKey : jedis.smembers(redisSchema.jobGroupsSet())) {
                if (groupMatcher.getCompareWithOperator().evaluate(redisSchema.jobGroup(jobGroupSetKey), groupMatcher.getCompareToValue())) {
                    jobGroups.put(jobGroupSetKey, jedis.smembers(jobGroupSetKey));
                }
            }
            for (final Map.Entry<String, Set<String>> entry : jobGroups.entrySet()) {
                if (jedis.sadd(redisSchema.pausedJobGroupsSet(), entry.getKey()) > 0) {
                    // This job group was not already paused. Pause it now.
                    pausedJobGroups.add(redisSchema.jobGroup(entry.getKey()));
                    for (final String jobHashKey : entry.getValue()) {
                        pauseJob(redisSchema.jobKey(jobHashKey), jedis);
                    }
                }
            }
        }
        return pausedJobGroups;
    }

    /**
     * Resume (un-pause) a {@link Trigger}
     *
     * @param triggerKey the key of the trigger to be resumed
     * @param jedis      a thread-safe Redis connection
     */
    @Override
    public void resumeTrigger(TriggerKey triggerKey, JedisClusterCommandsWrapper jedis) throws JobPersistenceException {
        final String triggerHashKey = redisSchema.triggerHashKey(triggerKey);
        Boolean exists = jedis.sismember(redisSchema.triggersSet(), triggerHashKey);
        Double isPaused = jedis.zscore(redisSchema.triggerStateKey(RedisTriggerState.PAUSED), triggerHashKey);
        Double isPausedBlocked = jedis.zscore(redisSchema.triggerStateKey(RedisTriggerState.PAUSED_BLOCKED), triggerHashKey);

        if (!exists) {
            // Trigger does not exist.  Nothing to do.
            return;
        }
        if (isPaused == null && isPausedBlocked == null) {
            // Trigger is not paused. Nothing to do.
            return;
        }
        OperableTrigger trigger = retrieveTrigger(triggerKey, jedis);
        final String jobHashKey = redisSchema.jobHashKey(trigger.getJobKey());
        final Date nextFireTime = trigger.getNextFireTime();

        if (nextFireTime != null) {
            if (isBlockedJob(jobHashKey, jedis)) {
                setTriggerState(RedisTriggerState.BLOCKED, (double) nextFireTime.getTime(), triggerHashKey, jedis);
            } else {
                setTriggerState(RedisTriggerState.WAITING, (double) nextFireTime.getTime(), triggerHashKey, jedis);
            }
        }
        applyMisfire(trigger, jedis);
    }

    /**
     * Resume (un-pause) all of the <code>{@link Trigger}s</code> in the given group.
     *
     * @param matcher matcher for the trigger groups to be resumed
     * @param jedis   a thread-safe Redis connection
     * @return the names of trigger groups which were resumed
     */
    @Override
    public Collection<String> resumeTriggers(GroupMatcher<TriggerKey> matcher, JedisClusterCommandsWrapper jedis) throws JobPersistenceException {
        Set<String> resumedTriggerGroups = new HashSet<>();
        if (matcher.getCompareWithOperator() == StringMatcher.StringOperatorName.EQUALS) {
            final String triggerGroupSetKey = redisSchema.triggerGroupSetKey(new TriggerKey("", matcher.getCompareToValue()));
            jedis.srem(redisSchema.pausedJobGroupsSet(), triggerGroupSetKey);
            Set<String> triggerHashKeysResponse = jedis.smembers(triggerGroupSetKey);
            for (String triggerHashKey : triggerHashKeysResponse) {
                OperableTrigger trigger = retrieveTrigger(redisSchema.triggerKey(triggerHashKey), jedis);
                resumeTrigger(trigger.getKey(), jedis);
                resumedTriggerGroups.add(trigger.getKey().getGroup());
            }
        } else {
            for (final String triggerGroupSetKey : jedis.smembers(redisSchema.triggerGroupsSet())) {
                if (matcher.getCompareWithOperator().evaluate(redisSchema.triggerGroup(triggerGroupSetKey), matcher.getCompareToValue())) {
                    resumedTriggerGroups.addAll(resumeTriggers(GroupMatcher.triggerGroupEquals(redisSchema.triggerGroup(triggerGroupSetKey)), jedis));
                }
            }
        }
        return resumedTriggerGroups;
    }

    /**
     * Resume (un-pause) all of the <code>{@link Job}s</code> in the given group.
     *
     * @param matcher the matcher with which to compare job group names
     * @param jedis   a thread-safe Redis connection
     * @return the set of job groups which were matched and resumed
     */
    @Override
    public Collection<String> resumeJobs(GroupMatcher<JobKey> matcher, JedisClusterCommandsWrapper jedis) throws JobPersistenceException {
        Set<String> resumedJobGroups = new HashSet<>();
        if (matcher.getCompareWithOperator() == StringMatcher.StringOperatorName.EQUALS) {
            final String jobGroupSetKey = redisSchema.jobGroupSetKey(new JobKey("", matcher.getCompareToValue()));
            Long unpauseResponse = jedis.srem(redisSchema.pausedJobGroupsSet(), jobGroupSetKey);
            Set<String> jobsResponse = jedis.smembers(jobGroupSetKey);
            if (unpauseResponse > 0) {
                resumedJobGroups.add(redisSchema.jobGroup(jobGroupSetKey));
            }
            for (String job : jobsResponse) {
                resumeJob(redisSchema.jobKey(job), jedis);
            }
        } else {
            for (final String jobGroupSetKey : jedis.smembers(redisSchema.jobGroupsSet())) {
                if (matcher.getCompareWithOperator().evaluate(redisSchema.jobGroup(jobGroupSetKey), matcher.getCompareToValue())) {
                    resumedJobGroups.addAll(resumeJobs(GroupMatcher.jobGroupEquals(redisSchema.jobGroup(jobGroupSetKey)), jedis));
                }
            }
        }
        return resumedJobGroups;
    }

    /**
     * Inform the <code>JobStore</code> that the scheduler is now firing the
     * given <code>Trigger</code> (executing its associated <code>Job</code>),
     * that it had previously acquired (reserved).
     *
     * @param triggers a list of triggers
     * @param jedis    a thread-safe Redis connection
     * @return may return null if all the triggers or their calendars no longer exist, or
     * if the trigger was not successfully put into the 'executing'
     * state.  Preference is to return an empty list if none of the triggers
     * could be fired.
     */
    @Override
    public List<TriggerFiredResult> triggersFired(List<OperableTrigger> triggers, JedisClusterCommandsWrapper jedis) throws JobPersistenceException, ClassNotFoundException {
        List<TriggerFiredResult> results = new ArrayList<>();
        for (OperableTrigger trigger : triggers) {
            final String triggerHashKey = redisSchema.triggerHashKey(trigger.getKey());
            logger.debug(String.format("Trigger %s fired.", triggerHashKey));
            Boolean triggerExistsResponse = jedis.exists(triggerHashKey);
            Double triggerAcquiredResponse = jedis.zscore(redisSchema.triggerStateKey(RedisTriggerState.ACQUIRED), triggerHashKey);
            if (!triggerExistsResponse || triggerAcquiredResponse == null) {
                // the trigger does not exist or the trigger is not acquired
                if (!triggerExistsResponse) {
                    logger.debug(String.format("Trigger %s does not exist.", triggerHashKey));
                } else {
                    logger.debug(String.format("Trigger %s was not acquired.", triggerHashKey));
                }
                continue;
            }
            Calendar calendar = null;
            final String calendarName = trigger.getCalendarName();
            if (calendarName != null) {
                calendar = retrieveCalendar(calendarName, jedis);
                if (calendar == null) {
                    continue;
                }
            }

            final Date previousFireTime = trigger.getPreviousFireTime();
            trigger.triggered(calendar);

            JobDetail job = retrieveJob(trigger.getJobKey(), jedis);
            TriggerFiredBundle triggerFiredBundle = new TriggerFiredBundle(job, trigger, calendar, false, new Date(), previousFireTime, previousFireTime, trigger.getNextFireTime());

            // handling jobs for which concurrent execution is disallowed
            if (isJobConcurrentExecutionDisallowed(job.getJobClass())) {
                final String jobHashKey = redisSchema.jobHashKey(trigger.getJobKey());
                final String jobTriggerSetKey = redisSchema.jobTriggersSetKey(job.getKey());
                for (String nonConcurrentTriggerHashKey : jedis.smembers(jobTriggerSetKey)) {
                    Double score = jedis.zscore(redisSchema.triggerStateKey(RedisTriggerState.WAITING), nonConcurrentTriggerHashKey);
                    if (score != null) {
                        setTriggerState(RedisTriggerState.BLOCKED, score, nonConcurrentTriggerHashKey, jedis);
                        // setting trigger state removes locks, so re-lock
                        lockTrigger(redisSchema.triggerKey(nonConcurrentTriggerHashKey), jedis);
                    } else {
                        score = jedis.zscore(redisSchema.triggerStateKey(RedisTriggerState.PAUSED), nonConcurrentTriggerHashKey);
                        if (score != null) {
                            setTriggerState(RedisTriggerState.PAUSED_BLOCKED, score, nonConcurrentTriggerHashKey, jedis);
                            // setting trigger state removes locks, so re-lock
                            lockTrigger(redisSchema.triggerKey(nonConcurrentTriggerHashKey), jedis);
                        }
                    }
                }
                jedis.set(redisSchema.jobBlockedKey(job.getKey()), schedulerInstanceId);
                jedis.sadd(redisSchema.blockedJobsSet(), jobHashKey);
            }

            // release the fired trigger
            if (trigger.getNextFireTime() != null) {
                final long nextFireTime = trigger.getNextFireTime().getTime();
                jedis.hset(triggerHashKey, TRIGGER_NEXT_FIRE_TIME, Long.toString(nextFireTime));
                logger.debug(String.format("Releasing trigger %s with next fire time %s. Setting state to WAITING.", triggerHashKey, nextFireTime));
                setTriggerState(RedisTriggerState.WAITING, (double) nextFireTime, triggerHashKey, jedis);
            } else {
                jedis.hset(triggerHashKey, TRIGGER_NEXT_FIRE_TIME, "");
                unsetTriggerState(triggerHashKey, jedis);
            }
            jedis.hset(triggerHashKey, TRIGGER_PREVIOUS_FIRE_TIME, Long.toString(System.currentTimeMillis()));

            results.add(new TriggerFiredResult(triggerFiredBundle));
        }
        return results;
    }

    /**
     * Inform the <code>JobStore</code> that the scheduler has completed the
     * firing of the given <code>Trigger</code> (and the execution of its
     * associated <code>Job</code> completed, threw an exception, or was vetoed),
     * and that the <code>{@link JobDataMap}</code>
     * in the given <code>JobDetail</code> should be updated if the <code>Job</code>
     * is stateful.
     *
     * @param trigger         the trigger which was completed
     * @param jobDetail       the job which was completed
     * @param triggerInstCode the status of the completed job
     * @param jedis           a thread-safe Redis connection
     */
    @Override
    public void triggeredJobComplete(OperableTrigger trigger, JobDetail jobDetail, Trigger.CompletedExecutionInstruction triggerInstCode, JedisClusterCommandsWrapper jedis) throws JobPersistenceException, ClassNotFoundException {
        final String jobHashKey = redisSchema.jobHashKey(jobDetail.getKey());
        final String jobDataMapHashKey = redisSchema.jobDataMapHashKey(jobDetail.getKey());
        final String triggerHashKey = redisSchema.triggerHashKey(trigger.getKey());
        logger.debug(String.format("Job %s completed.", jobHashKey));
        if (jedis.exists(jobHashKey)) {
            // job was not deleted during execution
            if (isPersistJobDataAfterExecution(jobDetail.getJobClass())) {
                // update the job data map
                JobDataMap jobDataMap = jobDetail.getJobDataMap();
                jedis.del(jobDataMapHashKey);
                if (jobDataMap != null && !jobDataMap.isEmpty()) {
                    jedis.hmset(jobDataMapHashKey, getStringDataMap(jobDataMap));
                }
            }
            if (isJobConcurrentExecutionDisallowed(jobDetail.getJobClass())) {
                // unblock the job
                jedis.srem(redisSchema.blockedJobsSet(), jobHashKey);
                jedis.del(redisSchema.jobBlockedKey(jobDetail.getKey()));

                final String jobTriggersSetKey = redisSchema.jobTriggersSetKey(jobDetail.getKey());
                for (String nonConcurrentTriggerHashKey : jedis.smembers(jobTriggersSetKey)) {
                    Double score = jedis.zscore(redisSchema.triggerStateKey(RedisTriggerState.BLOCKED), nonConcurrentTriggerHashKey);
                    if (score != null) {
                        setTriggerState(RedisTriggerState.WAITING, score, nonConcurrentTriggerHashKey, jedis);
                    } else {
                        score = jedis.zscore(redisSchema.triggerStateKey(RedisTriggerState.PAUSED_BLOCKED), nonConcurrentTriggerHashKey);
                        if (score != null) {
                            setTriggerState(RedisTriggerState.PAUSED, score, nonConcurrentTriggerHashKey, jedis);
                        }
                    }
                }
                signaler.signalSchedulingChange(0L);
            }
        } else {
            // unblock the job, even if it has been deleted
            jedis.srem(redisSchema.blockedJobsSet(), jobHashKey);
        }

        if (jedis.exists(triggerHashKey)) {
            // trigger was not deleted during job execution
            if (triggerInstCode == Trigger.CompletedExecutionInstruction.DELETE_TRIGGER) {
                if (trigger.getNextFireTime() == null) {
                    // double-check for possible reschedule within job execution, which would cancel the need to delete
                    if (isNullOrEmpty(jedis.hget(triggerHashKey, TRIGGER_NEXT_FIRE_TIME))) {
                        removeTrigger(trigger.getKey(), jedis);
                    }
                } else {
                    removeTrigger(trigger.getKey(), jedis);
                    signaler.signalSchedulingChange(0L);
                }
            } else if (triggerInstCode == Trigger.CompletedExecutionInstruction.SET_TRIGGER_COMPLETE) {
                setTriggerState(RedisTriggerState.COMPLETED, (double) System.currentTimeMillis(), triggerHashKey, jedis);
                signaler.signalSchedulingChange(0L);
            } else if (triggerInstCode == Trigger.CompletedExecutionInstruction.SET_TRIGGER_ERROR) {
                logger.debug(String.format("Trigger %s set to ERROR state.", triggerHashKey));
                final double score = trigger.getNextFireTime() != null ? (double) trigger.getNextFireTime().getTime() : 0;
                setTriggerState(RedisTriggerState.ERROR, score, triggerHashKey, jedis);
                signaler.signalSchedulingChange(0L);
            } else if (triggerInstCode == Trigger.CompletedExecutionInstruction.SET_ALL_JOB_TRIGGERS_ERROR) {
                final String jobTriggersSetKey = redisSchema.jobTriggersSetKey(jobDetail.getKey());
                for (String errorTriggerHashKey : jedis.smembers(jobTriggersSetKey)) {
                    final String nextFireTime = jedis.hget(errorTriggerHashKey, TRIGGER_NEXT_FIRE_TIME);
                    final double score = isNullOrEmpty(nextFireTime) ? 0 : Double.parseDouble(nextFireTime);
                    setTriggerState(RedisTriggerState.ERROR, score, errorTriggerHashKey, jedis);
                }
                signaler.signalSchedulingChange(0L);
            } else if (triggerInstCode == Trigger.CompletedExecutionInstruction.SET_ALL_JOB_TRIGGERS_COMPLETE) {
                final String jobTriggerSetKey = redisSchema.jobTriggersSetKey(jobDetail.getKey());
                for (String completedTriggerHashKey : jedis.smembers(jobTriggerSetKey)) {
                    setTriggerState(RedisTriggerState.COMPLETED, (double) System.currentTimeMillis(), completedTriggerHashKey, jedis);
                }
                signaler.signalSchedulingChange(0L);
            }
        }
    }
}