package ru.yandex.qe.dispenser.domain.dao.base_resources;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
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 javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.inject.Inject;

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import ru.yandex.qe.dispenser.domain.dao.bot.bigorder.BigOrderCache;
import ru.yandex.qe.dispenser.domain.dao.campaign.CampaignCache;
import ru.yandex.qe.dispenser.domain.dao.campaign.CampaignOwningCostCache;
import ru.yandex.qe.dispenser.domain.util.FunctionalUtils;

@Component
public class CacheRefreshScheduler {

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

    private final long initialDelaySeconds;
    private final long delaySeconds;
    private final long randomDelayCapSeconds;
    private final BaseResourceCache baseResourceCache;
    private final BaseResourceTypeCache baseResourceTypeCache;
    private final CampaignCache campaignCache;
    private final CampaignOwningCostCache campaignOwningCostCache;
    private final BigOrderCache botBigOrderCache;
    private final PerCampaignMappingsCache perCampaignMappingsCache;
    private final PerCampaignLimitsCache perCampaignLimitsCache;
    private final ScheduledExecutorService schedulerExecutorService;
    private final Random random = new Random();

    private volatile ScheduledFuture<?> scheduledFuture;

    @Inject
    public CacheRefreshScheduler(@Value("${cache.refresh.scheduler.initial.delay.seconds}") long initialDelaySeconds,
                                 @Value("${cache.refresh.scheduler.delay.seconds}") long delaySeconds,
                                 @Value("${cache.refresh.scheduler.random.delay.cap.seconds}") long randomDelayCapSeconds,
                                 BaseResourceCache baseResourceCache,
                                 BaseResourceTypeCache baseResourceTypeCache,
                                 CampaignCache campaignCache,
                                 BigOrderCache botBigOrderCache,
                                 PerCampaignMappingsCache perCampaignMappingsCache,
                                 PerCampaignLimitsCache perCampaignLimitsCache,
                                 CampaignOwningCostCache campaignOwningCostCache) {
        this.initialDelaySeconds = initialDelaySeconds;
        this.delaySeconds = delaySeconds;
        this.randomDelayCapSeconds = randomDelayCapSeconds;
        this.baseResourceCache = baseResourceCache;
        this.baseResourceTypeCache = baseResourceTypeCache;
        this.campaignCache = campaignCache;
        this.botBigOrderCache = botBigOrderCache;
        this.perCampaignMappingsCache = perCampaignMappingsCache;
        this.perCampaignLimitsCache = perCampaignLimitsCache;
        this.campaignOwningCostCache = campaignOwningCostCache;
        ThreadFactory threadFactory = new ThreadFactoryBuilder()
                .setDaemon(true)
                .setNameFormat("cache-refresh-scheduler-pool-%d")
                .setUncaughtExceptionHandler((t, e) -> LOG.error("Uncaught exception in thread " + t, e))
                .build();
        ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(2, threadFactory);
        scheduledThreadPoolExecutor.setRemoveOnCancelPolicy(true);
        this.schedulerExecutorService = scheduledThreadPoolExecutor;
    }

    @PostConstruct
    public void postConstruct() {
        scheduledFuture = schedulerExecutorService.scheduleWithFixedDelay(() -> {
                    try {
                        doRefresh();
                    } catch (final Throwable e) {
                        FunctionalUtils.throwIfUnrecoverable(e);
                        LOG.error("Cache refresh failure", e);
                    }
                }, initialDelaySeconds + random.nextInt((int) randomDelayCapSeconds),
                delaySeconds + random.nextInt((int) randomDelayCapSeconds), TimeUnit.SECONDS);
    }

    @PreDestroy
    public void preDestroy() {
        if (scheduledFuture != null) {
            scheduledFuture.cancel(true);
            scheduledFuture = null;
        }
        schedulerExecutorService.shutdown();
        boolean terminated = false;
        try {
            terminated = schedulerExecutorService.awaitTermination(1, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        if (!terminated) {
            schedulerExecutorService.shutdownNow();
        }
    }

    public static <T> T wrapException(Callable<T> callable) {
        try {
            return callable.call();
        } catch (ExecutionException e) {
            if (e.getCause() instanceof RuntimeException) {
                throw (RuntimeException) e.getCause();
            }
            if (e.getCause() instanceof IOException) {
                throw new UncheckedIOException((IOException) e.getCause());
            }
            if (e.getCause() != null) {
                throw new RuntimeException(e.getCause());
            }
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private void doRefresh() {
        LOG.info("Cache refreshing started...");
        tryRefresh(baseResourceCache::refresh);
        tryRefresh(baseResourceTypeCache::refresh);
        tryRefresh(campaignCache::refresh);
        tryRefresh(botBigOrderCache::refresh);
        tryRefresh(perCampaignMappingsCache::refresh);
        tryRefresh(perCampaignLimitsCache::refresh);
        tryRefresh(campaignOwningCostCache::refresh);
        LOG.info("Cache refreshing successfully over");
    }

    private void tryRefresh(Runnable r) {
        try {
            r.run();
        } catch (Exception e) {
            LOG.error("Cache refresh failure", e);
        }
    }

}
