package ru.yandex.qe.dispenser.quartz.monitoring;

import java.util.Collections;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.inject.Inject;

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.TriggerKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

import ru.yandex.qe.dispenser.domain.util.FunctionalUtils;

public class TriggerAutoRestoreManager {

    private static final Logger LOG = LoggerFactory.getLogger(TriggerAutoRestoreManager.class);
    private static final long INITIAL_DELAY_SECONDS = 300L;
    private static final long DELAY_SECONDS = 600L;

    private final SchedulersStatusProvider statusProvider;
    private final Set<Scheduler> schedulers;
    private final PlatformTransactionManager transactionManager;
    private final ScheduledExecutorService schedulerExecutorService;
    private final Random random = new Random();

    private volatile ScheduledFuture<?> scheduledFuture;

    @Inject
    public TriggerAutoRestoreManager(final SchedulersStatusProvider statusProvider, final Set<Scheduler> schedulers,
                                     final PlatformTransactionManager transactionManager) {
        this.statusProvider = statusProvider;
        this.schedulers = schedulers;
        this.transactionManager = transactionManager;
        final ThreadFactory threadFactory = new ThreadFactoryBuilder()
                .setDaemon(true)
                .setNameFormat("trigger-auto-restore-pool-%d")
                .setUncaughtExceptionHandler((t, e) -> LOG.error("Uncaught exception in thread " + t, e))
                .build();
        final ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1, threadFactory);
        scheduledThreadPoolExecutor.setRemoveOnCancelPolicy(true);
        this.schedulerExecutorService = scheduledThreadPoolExecutor;
    }

    @PostConstruct
    public void postConstruct() {
        // Quartz checks should not be scheduled through quartz itself obviously
        LOG.info("Starting trigger auto-restore manager...");
        scheduledFuture = schedulerExecutorService.scheduleWithFixedDelay(() -> {
            try {
                LOG.info("Searching for triggers to restore...");
                doRestore();
                LOG.info("Finished searching for triggers to restore");
            } catch (final Throwable e) {
                FunctionalUtils.throwIfUnrecoverable(e);
                LOG.error("Trigger auto-restore failure", e);
            }
        }, INITIAL_DELAY_SECONDS, DELAY_SECONDS + random.nextInt((int) DELAY_SECONDS), TimeUnit.SECONDS);
        LOG.info("Trigger auto-restore manager started successfully");
    }

    @PreDestroy
    public void preDestroy() {
        LOG.info("Stopping trigger auto-restore manager...");
        if (scheduledFuture != null) {
            scheduledFuture.cancel(true);
            scheduledFuture = null;
        }
        schedulerExecutorService.shutdown();
        try {
            schedulerExecutorService.awaitTermination(1, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        schedulerExecutorService.shutdownNow();
        LOG.info("Trigger auto-restore manager stopped successfully");
    }

    private void doRestore() {
        final QuartzStatus status = statusProvider.getStatus();
        final Map<String, Set<TriggerStatus>> errorsByScheduler = status.getSchedulerStatuses().stream()
                .collect(Collectors.toMap(SchedulerStatus::getSchedulerName, s -> s.getJobStatuses().stream()
                        .flatMap(j -> j.getTriggerStatuses().stream().filter(t -> t.getState() == TriggerState.ERROR)).collect(Collectors.toSet())));
        for (final Scheduler scheduler : schedulers) {
            final String schedulerName = getSchedulerName(scheduler);
            final Set<TriggerStatus> errorTriggers = errorsByScheduler.getOrDefault(schedulerName, Collections.emptySet());
            if (!errorTriggers.isEmpty()) {
                LOG.info("Restoring triggers in {} from error state: {}", schedulerName, errorTriggers);
            }
            if (isJobStoreClustered(scheduler)) {
                doInTx(() -> {
                    errorTriggers.forEach(triggerStatus -> resetTriggerStatus(scheduler, triggerStatus));
                });
            } else {
                errorTriggers.forEach(triggerStatus -> resetTriggerStatus(scheduler, triggerStatus));
            }
        }
    }

    private void resetTriggerStatus(final Scheduler scheduler, final TriggerStatus triggerStatus) {
        try {
            scheduler.resetTriggerFromErrorState(new TriggerKey(triggerStatus.getTriggerName(), triggerStatus.getGroupName()));
        } catch (SchedulerException e) {
            throw new RuntimeException(e);
        }
    }

    private String getSchedulerName(final Scheduler scheduler) {
        try {
            return scheduler.getSchedulerName();
        } catch (SchedulerException e) {
            throw new RuntimeException(e);
        }
    }

    private boolean isJobStoreClustered(final Scheduler scheduler) {
        try {
            return scheduler.getMetaData().isJobStoreClustered();
        } catch (SchedulerException e) {
            throw new RuntimeException(e);
        }
    }

    private void doInTx(final Runnable body) {
        final TransactionStatus transactionStatus = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
        boolean success = false;
        try {
            body.run();
            success = true;
        } finally {
            if (transactionStatus != null) {
                if (success) {
                    this.transactionManager.commit(transactionStatus);
                } else {
                    try {
                        this.transactionManager.rollback(transactionStatus);
                    } catch (TransactionException ex) {
                        LOG.error("Failed to rollback restoration transaction", ex);
                    }
                }
            }
        }
    }

}
