package ru.yandex.webmaster3.storage.metrika;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.Sets;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import ru.yandex.webmaster3.core.metrika.counters.CounterBinding;
import ru.yandex.webmaster3.storage.metrika.dao.MetrikaCrawlStateData;
import ru.yandex.webmaster3.storage.metrika.dao.MetrikaCrawlStateYDao;
import ru.yandex.webmaster3.storage.metrika.data.MetrikaCounterCrawlStateEnum;
import ru.yandex.webmaster3.storage.metrika.data.MetrikaDomainCrawlState;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import static java.util.stream.Collectors.mapping;

/**
 * @author leonidrom
 */
@Slf4j
@Service("metrikaCrawlStateService")
public class MetrikaCrawlStateService {
    private static final int CACHE_EXPIRATION_DURATION = 5; // minutes

    private final MetrikaCrawlStateYDao metrikaCrawlStateYDao;

    private final LoadingCache<String, MetrikaDomainCrawlState> domainCrawlStateCache = CacheBuilder.newBuilder()
            .expireAfterWrite(CACHE_EXPIRATION_DURATION, TimeUnit.MINUTES)
            .build(CacheLoader.from(this::getDomainCrawlState));


    @Autowired
    public MetrikaCrawlStateService(
            MetrikaCrawlStateYDao metrikaCrawlStateYDao) {
        this.metrikaCrawlStateYDao = metrikaCrawlStateYDao;
    }

    @NotNull
    public MetrikaDomainCrawlState getDomainCrawlState(String domain) {
        List<MetrikaCrawlStateData> states = metrikaCrawlStateYDao.getStatesForDomain(domain);
        return collectDomainStates(domain, states);
    }

    public MetrikaDomainCrawlState getDomainCrawlStateCached(String domain) {
        return domainCrawlStateCache.getUnchecked(domain);
    }

    public Map<String, MetrikaDomainCrawlState> getDomainsCrawlState(Collection<String> domains, Map<String, List<CounterBinding>> domainsBindings) {
        // disabled - состояние обхода по счетчику по умолчанию, в общем случае оно в MetrikaCrawlStateYDao не хранится
        Map<String, MetrikaDomainCrawlState> domainToStatesWithoutDisabled = collectDomainsStates(metrikaCrawlStateYDao.getStatesForDomains(domains));

        Map<String, MetrikaDomainCrawlState> res = new HashMap<>();
        domainsBindings.forEach((domain, bindings) -> {
            MetrikaDomainCrawlState crawlStateWithoutDisabled = domainToStatesWithoutDisabled.getOrDefault(domain, new MetrikaDomainCrawlState(domain));
            MetrikaDomainCrawlState fullCrawlState = new MetrikaDomainCrawlState(domain);
            // поскольку disabled состояние в базе не хранится, то надо пройтись по всем привязанным счетчикам
            bindings.forEach(b -> {
                if (!b.getCounterBindingState().isApproved()) {
                    return;
                }
                var counterState = crawlStateWithoutDisabled.getMetrikaCounterCrawlState(b.getCounterId());
                fullCrawlState.add(b.getCounterId(), counterState);
            });

            res.put(domain, fullCrawlState);
        });

        return res;
    }

    public Map<String, MetrikaDomainCrawlState> getAllStates() {
        List<MetrikaCrawlStateData> statesData = new ArrayList<>();
        metrikaCrawlStateYDao.forEach(statesData::add);

        return collectDomainsStates(statesData);
    }

    public void updateCrawlStateWithEnabled(String domain, @NotNull Set<Long> enabledCounters) {
        MetrikaDomainCrawlState currentState = getDomainCrawlState(domain);
        Set<Long> disabledCounters = Sets.difference(currentState.getEnabledCounters(), enabledCounters);
        DateTime updateDate = DateTime.now();

        enabledCounters.stream()
            .map(counterId -> new MetrikaCrawlStateData(domain, counterId, MetrikaCounterCrawlStateEnum.ENABLED, updateDate))
            .forEach(metrikaCrawlStateYDao::saveState);

        disabledCounters.stream()
                .map(counterId -> new MetrikaCrawlStateData(domain, counterId, MetrikaCounterCrawlStateEnum.DISABLED, updateDate))
                .forEach(metrikaCrawlStateYDao::saveState);
    }

    public void updateCounterCrawlState(String domain, long counterId, MetrikaCounterCrawlStateEnum state) {
        var data = new MetrikaCrawlStateData(domain, counterId, state, DateTime.now());
        metrikaCrawlStateYDao.saveState(data);
    }

    public boolean maybeSuspendCounterCrawl(String domain, long counterId, DateTime suspendDate) {
        MetrikaCrawlStateData crawlStateData = metrikaCrawlStateYDao.getState(domain, counterId);
        boolean shouldSuspend = true;
        if (crawlStateData != null) {
            shouldSuspend = crawlStateData.getState() == MetrikaCounterCrawlStateEnum.ENABLED &&
                    suspendDate.isAfter(crawlStateData.getUpdateDate());
        }

        if (shouldSuspend) {
            updateCounterCrawlState(domain, counterId, MetrikaCounterCrawlStateEnum.SUSPENDED);
        }

        return shouldSuspend;
    }

    @NotNull
    private Map<String, MetrikaDomainCrawlState> collectDomainsStates(List<MetrikaCrawlStateData> statesData) {
        Map<String, List<MetrikaCrawlStateData>> statesDataMap = new HashMap<>();
        statesData.forEach(s -> statesDataMap.computeIfAbsent(s.getDomain(), k -> new ArrayList<>()).add(s));

        Map<String, MetrikaDomainCrawlState> res = new HashMap<>();
        statesDataMap.forEach((domain, states) -> res.put(domain, collectDomainStates(domain, states)));
        return res;
    }

    @NotNull
    private MetrikaDomainCrawlState collectDomainStates(String domain, List<MetrikaCrawlStateData> states) {
        Map<MetrikaCounterCrawlStateEnum, Set<Long>> groupByStateMap = states.stream()
                .collect(Collectors.groupingBy(
                        MetrikaCrawlStateData::getState,
                        mapping(MetrikaCrawlStateData::getCounterId, Collectors.toSet())));

        return new MetrikaDomainCrawlState(
                domain,
                groupByStateMap.getOrDefault(MetrikaCounterCrawlStateEnum.ENABLED, Collections.emptySet()),
                groupByStateMap.getOrDefault(MetrikaCounterCrawlStateEnum.SUSPENDED, Collections.emptySet()),
                groupByStateMap.getOrDefault(MetrikaCounterCrawlStateEnum.DISABLED, Collections.emptySet()));
    }
}
