package ru.yandex.webmaster3.storage.links;

import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.Duration;
import org.joda.time.Instant;
import org.joda.time.LocalDate;
import org.springframework.beans.factory.annotation.Autowired;

import ru.yandex.webmaster3.core.codes.ErrorGroupEnum;
import ru.yandex.webmaster3.core.codes.LinkType;
import ru.yandex.webmaster3.core.data.HttpCodeInfo;
import ru.yandex.webmaster3.core.data.WebmasterHostId;
import ru.yandex.webmaster3.core.link.HostLinkStatistics;
import ru.yandex.webmaster3.core.link.LinkHistoryIndicatorType;
import ru.yandex.webmaster3.core.link.LinksHistoryIndicator;
import ru.yandex.webmaster3.storage.abt.AbtService;
import ru.yandex.webmaster3.storage.abt.model.Experiment;
import ru.yandex.webmaster3.storage.links.dao.LinkStatisticsCHDao;

/**
 * @author aherman
 */
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class LinksService {
    public final static Predicate<HttpCodeInfo> INTERNAL_LINKS_BROKEN_HISTORY_PREDICATE = codeInfo -> {
        return LinkType.get(codeInfo.getCode()) == LinkType.BROKEN && ErrorGroupEnum.get(codeInfo.getCode()).isPresent();
    };

    private final LinkStatisticsCHDao mdbLinkStatisticsCHDao;

    private Duration historyStoreDuration = Duration.standardDays(365);


    public List<HostLinkStatistics> getLatestLinkStatisticsGeneration(WebmasterHostId hostId, int limit) {
        return mdbLinkStatisticsCHDao.list(hostId, limit);
    }

    public List<HostLinkStatistics> getLatestLinkStatisticsGeneration(WebmasterHostId hostId, DateTime after, int limit) {
        return mdbLinkStatisticsCHDao.listAfter(hostId, after, limit);
    }

    public List<LinksHistoryIndicator> getActualIndicators(WebmasterHostId hostId) {
        Instant tooOldIndicator = Instant.now().minus(historyStoreDuration);
        return getAllAvailableIndicators(hostId).stream()
                .filter(i -> i.getLastUpdateDate().isAfter(tooOldIndicator))
                .collect(Collectors.toList());
    }

    public List<LinksHistoryIndicator> getAllAvailableIndicators(WebmasterHostId hostId) {
        return mdbLinkStatisticsCHDao.getIndicators(hostId);
    }

    public Map<LinkHistoryIndicatorType, NavigableMap<LocalDate, Long>> getSimpleHistory(WebmasterHostId hostId) {
        Instant toDate = Instant.now();
        Instant fromDate = toDate.minus(historyStoreDuration);
        return getSimpleHistory(hostId, fromDate, toDate);
    }

    public Map<LinkHistoryIndicatorType, NavigableMap<LocalDate, Long>> getSimpleHistory(WebmasterHostId hostId, DateTime fromDate, DateTime toDate) {
        return getSimpleHistory(hostId, fromDate != null ? fromDate.toInstant() : null, toDate != null ? toDate.toInstant() : null);
    }

    public Map<LinkHistoryIndicatorType, NavigableMap<LocalDate, Long>> getSimpleHistory(WebmasterHostId hostId, Instant fromDate, Instant toDate) {
        if (toDate == null) {
            toDate = Instant.now();
        }

        if (fromDate == null) {
            fromDate = toDate.minus(historyStoreDuration);
        }

        RawSimpleLinkHistory rawHistory = mdbLinkStatisticsCHDao.getSimpleHistory(hostId, fromDate, toDate);

        return convert(rawHistory);
    }

    public Map<HttpCodeInfo, NavigableMap<LocalDate, Long>> getInternalHttpHistory(
            WebmasterHostId hostId, DateTime dateFrom, DateTime dateTo) {
        return getHttpHistory(hostId, dateFrom, dateTo, false, LinkHistoryIndicatorType.INTERNAL_LINKS_HTTP_CODES);
    }

    public Map<HttpCodeInfo, NavigableMap<LocalDate, Long>> getInternalHttpHistory(
            WebmasterHostId hostId, @Nullable DateTime dateFrom, @Nullable DateTime dateTo,
            Predicate<HttpCodeInfo> predicate) {
        return getHttpHistory(hostId, dateFrom, dateTo, false, LinkHistoryIndicatorType.INTERNAL_LINKS_HTTP_CODES, predicate);
    }

    public Map<HttpCodeInfo, NavigableMap<LocalDate, Long>> getInternalHttpHistory(
            WebmasterHostId hostId, @Nullable DateTime dateFrom, @Nullable DateTime dateTo, boolean includeFromDate,
            Predicate<HttpCodeInfo> predicate) {
        return getHttpHistory(hostId, dateFrom, dateTo, includeFromDate, LinkHistoryIndicatorType.INTERNAL_LINKS_HTTP_CODES, predicate);
    }

    public Map<HttpCodeInfo, NavigableMap<LocalDate, Long>> getExternalHttpHistory(WebmasterHostId hostId,
        @Nullable DateTime dateFrom, @Nullable DateTime dateTo, Predicate<HttpCodeInfo> predicate) {
        return getHttpHistory(hostId, dateFrom, dateTo, false, LinkHistoryIndicatorType.EXTERNAL_LINKS_HTTP_CODES, predicate);
    }

    private Map<HttpCodeInfo, NavigableMap<LocalDate, Long>> getHttpHistory(
            WebmasterHostId hostId, @Nullable DateTime dateFrom, @Nullable DateTime dateTo, boolean includeFromDate,
            LinkHistoryIndicatorType indicator, Predicate<HttpCodeInfo> predicate) {
        Map<HttpCodeInfo, NavigableMap<LocalDate, Long>> res = getHttpHistory(hostId, dateFrom, dateTo, includeFromDate, indicator);

        //filter
        res = res.entrySet().stream().filter(e -> predicate.test(e.getKey())).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

        //collect dates
        Set<LocalDate> dates = new TreeSet<>();
        res.values().forEach(m -> {
            dates.addAll(m.keySet());
        });

        //additional
        Map<HttpCodeInfo, NavigableMap<LocalDate, Long>> result = new HashMap<>();
        res.forEach((k,v) -> {
            result.put(k, new TreeMap<>(dates.stream().collect(Collectors.toMap(dt -> dt, dt -> v.getOrDefault(dt, 0L)))));
        });

        return result;
    }

    public Map<HttpCodeInfo, NavigableMap<LocalDate, Long>> getHttpHistory(
            WebmasterHostId hostId, @Nullable DateTime dateFrom, @Nullable DateTime dateTo, boolean includeFromDate,
            LinkHistoryIndicatorType indicator) {
        Instant toDate = dateTo == null ? Instant.now() : dateTo.toInstant();
        Instant fromDate = dateFrom == null ? toDate.minus(historyStoreDuration) : dateFrom.toInstant();

        RawHttpCodeHistory rawHistory = mdbLinkStatisticsCHDao.getHttpCodeHistory(hostId, fromDate, toDate, includeFromDate, indicator);

        return convert(rawHistory, null);
    }

    @SuppressWarnings("unused")
    public Map<String, NavigableMap<LocalDate, Long>> getTldHistory(WebmasterHostId hostId) {
        Instant toDate = Instant.now();
        RawLinkTldHistory rawHistory = mdbLinkStatisticsCHDao.getTldHistory(hostId, toDate.minus(historyStoreDuration), toDate);

        return convert(rawHistory);
    }

    @NotNull
    protected static Map<LinkHistoryIndicatorType, NavigableMap<LocalDate, Long>> convert(
            RawSimpleLinkHistory rawHistory) {
        Map<LinkHistoryIndicatorType, NavigableMap<LocalDate, Long>> simpleIndicators = new HashMap<>();
        for (Map.Entry<LinkHistoryIndicatorType, Map<Instant, Long>> e : rawHistory
                .getSimpleIndicators().entrySet())
        {
            Map<LocalDate, Long> indicatorData = simpleIndicators.computeIfAbsent(e.getKey(), i -> new TreeMap<>());
            for (Map.Entry<Instant, Long> rawData : e.getValue().entrySet()) {
                indicatorData.put(new LocalDate(rawData.getKey(), DateTimeZone.UTC), rawData.getValue());
            }
        }
        return simpleIndicators;
    }

    protected static Map<HttpCodeInfo, NavigableMap<LocalDate, Long>> convert(RawHttpCodeHistory rawHistory,
            HostLinkStatistics statistics) {
        Map<HttpCodeInfo, NavigableMap<LocalDate, Long>> httpCodes = new HashMap<>();
        for (Map.Entry<Instant, Map<Integer, Long>> e : rawHistory.getHttpCodes().entrySet()) {
            LocalDate d = new LocalDate(e.getKey(), DateTimeZone.UTC);
            for (Map.Entry<Integer, Long> codes : e.getValue().entrySet()) {
                HttpCodeInfo codeInfo = new HttpCodeInfo(codes.getKey());
                httpCodes.computeIfAbsent(codeInfo, c -> new TreeMap<>()).put(d, codes.getValue());
            }
        }
        if (statistics != null && !statistics.getInternalLinkHttpCodes().isEmpty()) {
            Map<Integer, Long> internalLinkHttpCodes = statistics.getInternalLinkHttpCodes();
            LocalDate d = LocalDate.now();
            for (Map.Entry<Integer, Long> entry : internalLinkHttpCodes.entrySet()) {
                HttpCodeInfo codeInfo = new HttpCodeInfo(entry.getKey());
                httpCodes.computeIfAbsent(codeInfo, c -> new TreeMap<>()).put(d, entry.getValue());
            }
        }

        Set<LocalDate> httpDates = new HashSet<>();
        for (Instant instant : rawHistory.getHttpCodes().keySet()) {
            httpDates.add(new LocalDate(instant, DateTimeZone.UTC));
        }
        for (Map<LocalDate, Long> data : httpCodes.values()) {
            for (LocalDate knownDate : httpDates) {
                if (!data.containsKey(knownDate)) {
                    data.put(knownDate, 0L);
                }
            }
        }
        return httpCodes;
    }

    protected static Map<String, NavigableMap<LocalDate, Long>> convert(RawLinkTldHistory rawHistory) {
        Map<String, NavigableMap<LocalDate, Long>> tciGroups = new HashMap<>();
        for (Map.Entry<Instant, Map<String, Long>> e : rawHistory.getTldCount().entrySet()) {
            LocalDate d = new LocalDate(e.getKey(), DateTimeZone.UTC);
            for (Map.Entry<String, Long> groups : e.getValue().entrySet()) {
                tciGroups.computeIfAbsent(groups.getKey(), c -> new TreeMap<>()).put(d, groups.getValue());
            }
        }

        Set<LocalDate> tldDates = new HashSet<>();
        for (Instant instant : rawHistory.getTldCount().keySet()) {
            tldDates.add(new LocalDate(instant, DateTimeZone.UTC));
        }
        for (Map<LocalDate, Long> data : tciGroups.values()) {
            for (LocalDate knownDate : tldDates) {
                if (!data.containsKey(knownDate)) {
                    data.put(knownDate, 0L);
                }
            }
        }
        return tciGroups;
    }

}
