package ru.yandex.webmaster3.storage.host.service;

import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListenableFutureTask;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Pair;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import ru.yandex.webmaster3.core.data.WebmasterHostId;
import ru.yandex.webmaster3.storage.mirrors.dao.MainMirrorsCHDao;
import ru.yandex.webmaster3.storage.mirrors.dao.NewMainMirrorCacheYDao;

@RequiredArgsConstructor(onConstructor_ = @Autowired)
@Service(value = "mirrorService2")
@Slf4j
public class MirrorService2 {

    private static final long CHECK_CACHE_DURATION_MINUTES = 2;
    private static final Object KEY = new Object();

    private final MainMirrorsCHDao mdbMainMirrorsCHDao;
    private final NewMainMirrorCacheYDao newMainMirrorCacheYDao;

    public void init() {
        getMainMirrors(Collections.emptySet()); // инициализируем загрузку кэша
    }

    private final ExecutorService executor = Executors.newFixedThreadPool(1);

    private final LoadingCache<Object, Future<Map<WebmasterHostId, WebmasterHostId>>> mainMirrorsCache =
            CacheBuilder.newBuilder()
                    .refreshAfterWrite(CHECK_CACHE_DURATION_MINUTES, TimeUnit.MINUTES)
                    .build(new MirrorCacheLoader());

    public Map<WebmasterHostId, WebmasterHostId> getMainMirrors(Set<WebmasterHostId> hosts) {
        return getMainMirrorsWithCache(hosts);
    }

    @Nullable
    public WebmasterHostId getMainMirror(WebmasterHostId webmasterHostId) {
        return getMainMirrorWithCache(webmasterHostId);
    }

    // не стоит использовать эти методы, в ручках, которые часто долбят. Подробнее в mdbMainMirrorsCHDao


    public Map<WebmasterHostId, WebmasterHostId> getMainMirrorsFromAllMirrors(Collection<WebmasterHostId> hosts) {
        return mdbMainMirrorsCHDao.getMainMirrorsFromAllMirrors(hosts);
    }

    @NotNull
    public WebmasterHostId getMainMirrorFromAllMirrors(WebmasterHostId webmasterHostId) {
        WebmasterHostId mainMirror = getMainMirrorsFromAllMirrors(List.of(webmasterHostId)).get(webmasterHostId);
        if (!webmasterHostId.equals(mainMirror)) {
            newMainMirrorCacheYDao.add(webmasterHostId, mainMirror);
        }
        return mainMirror;
    }
    ///

    public boolean isMainMirror(WebmasterHostId hostId) {
        return getMainMirror(hostId) == null;
    }

    private Map<WebmasterHostId, WebmasterHostId> getMainMirrorsWithCache(Set<WebmasterHostId> hosts) {
        if (mainMirrorsCache.getUnchecked(KEY).isDone()) {
            try {
                final Map<WebmasterHostId, WebmasterHostId> mirrors = mainMirrorsCache.getUnchecked(KEY).get();

                return hosts.stream()
                        .filter(mirrors::containsKey)
                        .map(hostId -> Pair.of(hostId, mirrors.get(hostId)))
                        .collect(Collectors.toMap(Pair::getKey, Pair::getValue));
            } catch (Exception e) {
                log.error("Error preparing snapshot cache", e);
                mainMirrorsCache.invalidate(KEY);
            }
        }
        return mdbMainMirrorsCHDao.getMainMirrors(hosts);
    }

    private WebmasterHostId getMainMirrorWithCache(WebmasterHostId webmasterHostId) {
        if (mainMirrorsCache.getUnchecked(KEY).isDone()) {
            try {
                return mainMirrorsCache.getUnchecked(KEY).get().get(webmasterHostId);
            } catch (Exception e) {
                log.error("Error preparing snapshot cache", e);
                mainMirrorsCache.invalidate(KEY);
            }
        }
        return mdbMainMirrorsCHDao.getMainMirror(webmasterHostId);
    }

    private final class MirrorCacheLoader extends CacheLoader<Object, Future<Map<WebmasterHostId, WebmasterHostId>>> {
        private String curVersion;

        @Override
        public Future<Map<WebmasterHostId, WebmasterHostId>> load(Object ign) {
            String tableVersion = mdbMainMirrorsCHDao.getTableVersion();
            return executor.submit(() -> loadMirrors(tableVersion));
        }

        @SneakyThrows
        public Map<WebmasterHostId, WebmasterHostId> loadMirrors(String tableVersion) {

            log.info("start load mirrors cache - {}", DateTime.now());
            Map<WebmasterHostId, WebmasterHostId> map;
            try {
                map = mdbMainMirrorsCHDao.collectAll(tableVersion);
            } catch (Exception e) {
                log.error("while load mirros cache {}", e.toString(), e);
                throw e;
            }

            log.info("finish success load mirrors cache, size - {}, date - {}", map.size(), DateTime.now());
            curVersion = tableVersion;
            return map;
        }


        @Override
        public ListenableFuture<Future<Map<WebmasterHostId, WebmasterHostId>>> reload(Object key,
                                                                                      Future<Map<WebmasterHostId,
                                                                                              WebmasterHostId>> oldValue) {
            String tableVersion = mdbMainMirrorsCHDao.getTableVersion();
            // обновляем кеш при смене базы
            log.debug("check reload mirrors cache");
            final ListenableFutureTask<Future<Map<WebmasterHostId, WebmasterHostId>>> task;
            log.debug("oldValue.isDone() == {}, curBase - {}, base - {}", oldValue.isDone(), curVersion, tableVersion);
            if (!oldValue.isDone() || Objects.equals(curVersion, tableVersion)) {
                log.debug("oldMirrors cache is actual");
                task = ListenableFutureTask.create(() -> oldValue);
                if (oldValue.isDone()) {
                    try {
                        oldValue.get().putAll(newMainMirrorCacheYDao.getAll());
                    } catch (Exception e) {
                        log.error(e.getMessage(), e);
                    }
                }
            } else {
                log.debug("start reload mirrors cache ");
                task = ListenableFutureTask.create(() -> {
                            try {
                                final Map<WebmasterHostId, WebmasterHostId> webmasterHostIdWebmasterHostIdMap =
                                        loadMirrors(tableVersion);
                                log.debug("finish reload mirrors cache ");
                                return CompletableFuture.completedFuture(webmasterHostIdWebmasterHostIdMap);
                            } catch (Exception e) {
                                return oldValue;
                            }
                        }
                );
            }
            executor.submit(task);
            return task;
        }
    }
}
