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

import java.time.Clock;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

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

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.jetbrains.annotations.NotNull;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.impl.matchers.GroupMatcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ru.yandex.monlib.metrics.labels.Labels;
import ru.yandex.monlib.metrics.primitives.Rate;
import ru.yandex.monlib.metrics.registry.MetricRegistry;
import ru.yandex.qe.dispenser.domain.util.FunctionalUtils;
import ru.yandex.qe.dispenser.solomon.SolomonHolder;

public class SchedulersStatusProviderImpl implements SchedulersStatusProvider {

    private static final Logger LOG = LoggerFactory.getLogger(SchedulersStatusProviderImpl.class);

    private static final long INITIAL_DELAY = 15L;
    private static final long DELAY = 60L;

    private final Set<Scheduler> schedulers;
    private static final String STOPPED_SCHEDULERS = "scheduler.stopped_schedulers";
    private static final String TRIGGERS_WITH_ERROR = "scheduler.triggers_with_error";
    private static final String STATUS_LAST_SUCCESS = "scheduler.status.time_since_last_success_end";
    private static final String STATUS_LAST_START = "scheduler.status.time_since_last_start";
    private static final String STATUS_ERROR_RATE = "scheduler.status.error_rate";

    private volatile long stoppedSchedulers = 0L;
    private volatile long triggersWithError = 0L;

    private final Rate statusErrorRate;
    private volatile long statusLastStart;
    private volatile long statusLastSuccess;
    private final Clock clock = Clock.systemDefaultZone();

    private final ScheduledThreadPoolExecutor scheduledExecutorService;

    @Inject
    public SchedulersStatusProviderImpl(final Set<Scheduler> schedulers, final SolomonHolder solomonHolder) {
        this.schedulers = schedulers;
        final MetricRegistry rootRegistry = solomonHolder.getRootRegistry();
        rootRegistry.lazyGaugeInt64(STOPPED_SCHEDULERS, Labels.of(), () -> stoppedSchedulers);
        rootRegistry.lazyGaugeInt64(TRIGGERS_WITH_ERROR, Labels.of(), () -> triggersWithError);

        statusErrorRate = rootRegistry.rate(STATUS_ERROR_RATE);
        statusLastStart = clock.millis();
        statusLastSuccess = clock.millis();

        rootRegistry.lazyGaugeInt64(STATUS_LAST_START, () -> clock.millis() - statusLastStart);
        rootRegistry.lazyGaugeInt64(STATUS_LAST_SUCCESS, () -> clock.millis() - statusLastSuccess);

        final ThreadFactory threadFactory = new ThreadFactoryBuilder()
                .setDaemon(true)
                .setNameFormat("schedulers-status-cache-pool-%d")
                .setUncaughtExceptionHandler((t, e) -> LOG.error("Uncaught exception in thread " + t, e))
                .build();
        scheduledExecutorService = new ScheduledThreadPoolExecutor(1, threadFactory);
        scheduledExecutorService.setRemoveOnCancelPolicy(true);

        scheduledExecutorService.scheduleWithFixedDelay(() -> {
            statusLastStart = clock.millis();
            boolean success = false;
            try {
                final QuartzStatus quartzStatus = getStatus();

                stoppedSchedulers = quartzStatus.getStoppedSchedulers();
                triggersWithError = quartzStatus.getTriggersWithError();
                success = true;
            } catch (Throwable e) {
                LOG.error("Failed to update quartz schedulers status", e);
                FunctionalUtils.throwIfUnrecoverable(e);
            } finally {
                if (success) {
                    statusLastSuccess = clock.millis();
                } else {
                    stoppedSchedulers = 0;
                    triggersWithError = 0;
                    statusErrorRate.inc();
                }
            }
        }, INITIAL_DELAY, DELAY, TimeUnit.SECONDS);
    }

    @PreDestroy
    public void preDestroy() {
        LOG.info("Stopping scheduler status provider...");
        scheduledExecutorService.shutdown();
        try {
            scheduledExecutorService.awaitTermination(1, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        scheduledExecutorService.shutdownNow();
        LOG.info("Scheduler status provider stopped successfully");
    }

    @NotNull
    @Override
    public QuartzStatus getStatus() {
        try {
            return getStatusImpl(schedulers);
        } catch (SchedulerException e) {
            throw new RuntimeException(e);
        }
    }

    private static QuartzStatus getStatusImpl(final Set<Scheduler> schedulers) throws SchedulerException {
        final Set<SchedulerStatus> result = new HashSet<>();
        long stoppedSchedulers = 0L;
        long triggersWithError = 0L;
        for (final Scheduler scheduler : schedulers) {
            final String schedulerName = scheduler.getSchedulerName();
            final boolean schedulerStarted = scheduler.isStarted();
            if (!schedulerStarted) {
                stoppedSchedulers++;
            }
            final List<String> jobGroupNames = scheduler.getJobGroupNames();
            final Set<JobStatus> schedulerJobStatuses = new HashSet<>();
            for (final String jobGroupName : jobGroupNames) {
                final Set<JobKey> jobKeys = scheduler.getJobKeys(GroupMatcher.jobGroupEquals(jobGroupName));
                for (final JobKey jobKey : jobKeys) {
                    final List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobKey);
                    final Set<TriggerStatus> schedulerTriggerStatuses = new HashSet<>();
                    for (final Trigger trigger : triggers) {
                        final Trigger.TriggerState triggerState = scheduler.getTriggerState(trigger.getKey());
                        final TriggerStatus triggerStatus = new TriggerStatus(trigger.getKey().getName(), trigger.getKey().getGroup(),
                                TriggerState.from(triggerState));
                        schedulerTriggerStatuses.add(triggerStatus);
                        if (!triggerStatus.getState().isOk()) {
                            triggersWithError++;
                        }
                    }
                    final JobStatus jobStatus = new JobStatus(jobKey.getName(), jobKey.getGroup(), schedulerTriggerStatuses);
                    schedulerJobStatuses.add(jobStatus);
                }
            }
            final SchedulerStatus status = new SchedulerStatus(schedulerName, schedulerStarted, schedulerJobStatuses);
            result.add(status);
        }
        return new QuartzStatus(result, stoppedSchedulers, triggersWithError);
    }

}
