package ru.yandex.webmaster3.storage.metrika;

import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.tuple.Pair;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import ru.yandex.webmaster3.core.WebmasterException;
import ru.yandex.webmaster3.core.blackbox.UserWithLogin;
import ru.yandex.webmaster3.core.blackbox.service.BlackboxUsersService;
import ru.yandex.webmaster3.core.data.WebmasterHostId;
import ru.yandex.webmaster3.core.data.WebmasterUser;
import ru.yandex.webmaster3.core.http.WebmasterErrorResponse;
import ru.yandex.webmaster3.core.metrika.counters.*;
import ru.yandex.webmaster3.core.util.RetryUtils;
import ru.yandex.webmaster3.storage.metrika.dao.MetrikaCounterBindingStateYDao;
import ru.yandex.webmaster3.storage.metrika.data.MetrikaCounterCrawlStateEnum;
import ru.yandex.webmaster3.storage.spam.MetrikaCountersSpamStatsYDao;
import ru.yandex.webmaster3.storage.user.UserTakeoutDataProvider;
import ru.yandex.webmaster3.storage.user.UserTakeoutTableData;
import ru.yandex.webmaster3.storage.util.ydb.exception.WebmasterYdbException;

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

/**
 * Created by ifilippov5 on 24.10.17.
 */
@Service("metrikaCounterBindingService")
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class MetrikaCounterBindingService  implements UserTakeoutDataProvider {
    private static final Logger log = LoggerFactory.getLogger(MetrikaCounterBindingService.class);
    private static final String USER_TAKEOUT_METRIKA_COUNTERS_LABEL = "metrikaCounters";

    private final MetrikaCounterBindingStateYDao metrikaCounterBindingStateYDao;
    private final MetrikaCountersSpamStatsYDao metrikaCountersSpamStatsYDao;
    private final MetrikaCountersApiService metrikaCountersApiService;
    private final BlackboxUsersService blackboxExternalYandexUsersService;
    private final MetrikaCrawlStateService metrikaCrawlStateService;

    @NotNull
    public CounterBindingStateEnum getState(String domain, long counterId) {
        try {
            CounterBindingStateEnum state =  RetryUtils.query(RetryUtils.instantRetry(5),
                    () -> metrikaCounterBindingStateYDao.getState(domain, counterId));
            if (state == null) {
                log.info("No DB entry for counter {}:{}", domain, counterId);
                state = CounterBindingStateEnum.NONE;
            }

            return state;
        } catch (WebmasterYdbException | InterruptedException e) {
            throw new WebmasterException("Unable to get metrika counter state",
                    new WebmasterErrorResponse.YDBErrorResponse(this.getClass(), e), e);
        }
    }

    public List<CounterBinding> getBindingsForDomain(String domain, boolean approvedOnly) {
        List<CounterBinding> bindings = metrikaCounterBindingStateYDao.getAllForDomain(domain);
        if (approvedOnly) {
            bindings = bindings.stream().
                    filter(cb -> cb.getCounterBindingState().isApproved())
                    .collect(Collectors.toList());
        }

        return bindings;
    }

    public Map<String, List<CounterBinding>> getBindingsForDomains(Collection<String> domains, boolean approvedOnly) {
        List<CounterBinding> bindings = metrikaCounterBindingStateYDao.getAllForDomains(domains);
        Map<String, List<CounterBinding>> res = new HashMap<>();
        bindings.forEach(b -> {
            if (!approvedOnly || b.getCounterBindingState().isApproved()) {
                res.computeIfAbsent(b.getDomain(), k -> new ArrayList<>()).add(b);
            }
        });

        return res;
    }

    public Pair<Long, List<MetrikaCounterBindingInfo>> getBindingsWithCounterInfo(
            WebmasterHostId hostId,
            EnumSet<CounterBindingStateEnum> acceptableStates,
            long limitFrom, long limitSize) {

        Comparator<Map.Entry<Long, CounterBinding>> linksComparator =
                Map.Entry.comparingByValue(Comparator.comparing(CounterBinding::getUpdateDate,
                Comparator.nullsFirst(Comparator.naturalOrder())).reversed());
        try {
            Map<Long, CounterBinding> bindings = metrikaCounterBindingStateYDao.getAllForDomainAsMap(MetrikaCountersUtil.hostToPunycodeDomain(hostId))
                    .entrySet()
                    .stream()
                    .filter(entry -> acceptableStates.contains(entry.getValue().getCounterBindingState()))
                    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
            long bindingsCount = bindings.size();

            Map<Long, CounterBinding> bindingsPage = bindings.entrySet()
                    .stream()
                    .sorted(linksComparator)
                    .skip(limitFrom)
                    .limit(limitSize)
                    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

            return Pair.of(bindingsCount, bindingsPage.entrySet()
                    .stream()
                    .sorted(linksComparator)
                    .map(binding -> {
                        long counterId = binding.getKey();
                        CounterBinding cb = binding.getValue();
                        CounterInfo counterInfo = cb.isFake()?
                                CounterInfo.newFakeCounterInfo(counterId) :
                                getCounterInfo(counterId, hostId.getReadableHostname());
                        //TODO если counterSite пустой
                        return new MetrikaCounterBindingInfo(counterId, counterInfo.getCounterName(),
                                counterInfo.getCounterSite(), binding.getValue().getLastUserLogin(),
                                Optional.ofNullable(binding.getValue().getUpdateDate()).map(DateTime::toLocalDate).orElse(null),
                                binding.getValue().getCounterBindingState());
                    })
                    .collect(Collectors.toList()));

        } catch (WebmasterYdbException e) {
            throw new WebmasterException("Unable to get all metrika counters binding info",
                    new WebmasterErrorResponse.YDBErrorResponse(this.getClass(), e), e);
        }
    }

    @NotNull
    public CounterInfo getCounterInfo(long counterId, String defaultName) {
        // пойдем в API Метрики
        CounterInfo counterInfo = metrikaCountersApiService.getCounterInfo(counterId);

        String counterName = counterInfo.getCounterName();
        if (counterName.isEmpty()) {
            counterInfo = counterInfo.withCounterName(defaultName);
        }

        return counterInfo;
    }

    public void updateStateWithMetrikaUser(String domain, CounterBindingStateEnum state, long counterId, String userLogin,
                                      DateTime updateDate, long metrikaUserId, @Nullable String origin)  {
        updateState(domain, state, counterId, userLogin, updateDate, metrikaUserId, null, origin);
    }

    public void updateStateWithWebmasterUser(String domain, CounterBindingStateEnum state, long counterId, String userLogin,
                                        DateTime updateDate, long webmasterUserId, @Nullable String origin) {
        updateState(domain, state, counterId, userLogin, updateDate, null, webmasterUserId, origin);
    }

    public void updateStateWithMetrikaAndWebmasterUser(String domain, CounterBindingStateEnum state, long counterId, String userLogin,
                                                  DateTime updateDate, long userId, @Nullable String origin) {
        updateState(domain, state, counterId, userLogin, updateDate, userId, userId, origin);
    }

    private void updateState(String domain, CounterBindingStateEnum state, long counterId,
                             String userLogin, DateTime updateDate,
                             @Nullable Long metrikaUserId, @Nullable Long webmasterUserId,
                             @Nullable String origin) {
        try {
            RetryUtils.execute(RetryUtils.instantRetry(5), () -> {
                if (CounterBindingStateEnum.NONE == state) {
                    // принудительно выключим обход по счетчику
                    metrikaCrawlStateService.updateCounterCrawlState(domain, counterId, MetrikaCounterCrawlStateEnum.DISABLED);
                }

                metrikaCounterBindingStateYDao.saveRecord(
                        domain, state, counterId, userLogin, updateDate, metrikaUserId, webmasterUserId, origin);
            });
        } catch (WebmasterYdbException | InterruptedException e) {
            throw new WebmasterException("Unable to update metrika counter state",
                    new WebmasterErrorResponse.YDBErrorResponse(null, e), e);
        }
    }

    public String getUserLogin(long userId) {
        UserWithLogin userWithLogin = blackboxExternalYandexUsersService.getUserById(userId);
        String userLogin = String.valueOf(userId);
        if (userWithLogin != null) {
            userLogin = userWithLogin.getLogin();
        }
        return userLogin;
    }

    @Override
    @NotNull
    public List<UserTakeoutTableData> getUserTakeoutData(WebmasterUser user) {
        List<UserTakeoutTableData> takeoutData = new ArrayList<>();

        List<AnonymousCounterBinding> counterBindings = new ArrayList<>();
        metrikaCounterBindingStateYDao.forEachLink(counterBinding -> {
            Long wmUserId = counterBinding.getWebmasterUserId();
            Long metrikaUserId = counterBinding.getMetrikaUserId();

            if ((wmUserId != null && wmUserId == user.getUserId()) ||
                (metrikaUserId != null && metrikaUserId == user.getUserId())) {

                counterBindings.add(counterBinding);
            }
        }, false);

        takeoutData.add(new UserTakeoutTableData(USER_TAKEOUT_METRIKA_COUNTERS_LABEL, counterBindings));

        return takeoutData;
    }

    @Override
    public void deleteUserData(WebmasterUser user) {
        long userId = user.getUserId();
        metrikaCounterBindingStateYDao.deleteForMetrikaUser(userId);
        metrikaCounterBindingStateYDao.deleteForWebmasterUser(userId);
        metrikaCountersSpamStatsYDao.deleteForUser(userId);
    }

    @Override
    public @NotNull List<String> getTakeoutTables() {
        return List.of(
                metrikaCounterBindingStateYDao.getTablePath(),
                metrikaCountersSpamStatsYDao.getTablePath()
        );
    }
}
