package ru.yandex.webmaster3.worker.metrika;

import com.google.common.collect.Ordering;
import lombok.extern.slf4j.Slf4j;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import ru.yandex.webmaster3.core.checklist.data.SiteProblemContent;
import ru.yandex.webmaster3.core.checklist.data.SiteProblemTypeEnum;
import ru.yandex.webmaster3.core.data.WebmasterHostId;
import ru.yandex.webmaster3.core.metrika.counters.MetrikaCountersUtil;
import ru.yandex.webmaster3.core.worker.task.PeriodicTaskState;
import ru.yandex.webmaster3.core.worker.task.PeriodicTaskType;
import ru.yandex.webmaster3.storage.checklist.data.ProblemSignal;
import ru.yandex.webmaster3.storage.checklist.service.SiteProblemsService;
import ru.yandex.webmaster3.storage.metrika.MetrikaCrawlStateService;
import ru.yandex.webmaster3.storage.metrika.dao.MetrikaCounterBindingStateYDao;
import ru.yandex.webmaster3.storage.metrika.data.MetrikaDomainCrawlState;
import ru.yandex.webmaster3.worker.PeriodicTask;
import ru.yandex.webmaster3.worker.TaskSchedule;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
 * @author leonidrom
 */
@Component
@Slf4j
public class UpdateMetrikaCrawlProblemsPeriodicTask extends PeriodicTask<UpdateMetrikaCrawlProblemsPeriodicTask.TaskState> {
    private final static Duration NO_METRIKA_COUNTER_CRAWL_ENABLED_DELAY = Duration.standardHours(2);
    private final static int BATCH_SIZE = 2000;

    private final MetrikaCounterBindingStateYDao metrikaCounterBindingStateYDao;
    private final MetrikaCrawlStateService metrikaCrawlStateService;
    private final SiteProblemsService siteProblemsService;

    @Autowired
    public UpdateMetrikaCrawlProblemsPeriodicTask(
            MetrikaCounterBindingStateYDao metrikaCounterBindingStateYDao,
            MetrikaCrawlStateService metrikaCrawlStateService,
            SiteProblemsService siteProblemsService) {
        this.metrikaCounterBindingStateYDao = metrikaCounterBindingStateYDao;
        this.metrikaCrawlStateService = metrikaCrawlStateService;
        this.siteProblemsService = siteProblemsService;
    }

    @Override
    public Result run(UUID runId) throws Exception {
        var updateStartTime = DateTime.now();
        setState(new TaskState());

        Map<String, MetrikaDomainCrawlState> allCrawlStates =  metrikaCrawlStateService.getAllStates();

        Map<String, DateTime> domainsWithApprovedBindings = new HashMap<>();
        collectDomainsWithApprovedBindings(domainsWithApprovedBindings);
        state.domainsWithApprovedBindings = domainsWithApprovedBindings.size();

        Map<WebmasterHostId, ProblemSignal> problemsBatch = new HashMap<>(BATCH_SIZE);
        var alertThresholdDate = DateTime.now().minus(NO_METRIKA_COUNTER_CRAWL_ENABLED_DELAY);
        for (String domain : domainsWithApprovedBindings.keySet()) {
            var lastUpdateDate = domainsWithApprovedBindings.get(domain);
            if (lastUpdateDate.isAfter(alertThresholdDate)) {
                state.domainsNoCrawlEnabledSkipped++;
                continue;
            }

            updateProblemForDomain(domain, problemsBatch, allCrawlStates);

            if (problemsBatch.size() > BATCH_SIZE) {
                siteProblemsService.updateCleanableProblems(problemsBatch, SiteProblemTypeEnum.NO_METRIKA_COUNTER_CRAWL_ENABLED);
                problemsBatch.clear();
            }
        }

        if (!problemsBatch.isEmpty()) {
            siteProblemsService.updateCleanableProblems(problemsBatch, SiteProblemTypeEnum.NO_METRIKA_COUNTER_CRAWL_ENABLED);
        }

        siteProblemsService.notifyCleanableProblemUpdateFinished(SiteProblemTypeEnum.NO_METRIKA_COUNTER_CRAWL_ENABLED, updateStartTime);

        return Result.SUCCESS;
    }

    private void collectDomainsWithApprovedBindings(Map<String, DateTime> domainsWithApprovedBindings) {
        metrikaCounterBindingStateYDao.forEachLink(b -> {
            if (b.getUpdateDate() == null) {
                // такого быть не должно, но на всякий
                log.error("Invalid counter binding: {}", b);
                state.invalidBindings++;
                return;
            }

            if (b.getCounterBindingState().isApproved()) {
                domainsWithApprovedBindings.merge(b.getDomain(), b.getUpdateDate(), (d1, d2) -> Ordering.natural().max(d1, d2));
            }
        }, false);
    }

    private void updateProblemForDomain(String domain, Map<WebmasterHostId, ProblemSignal> problemsBatch,
                                        Map<String, MetrikaDomainCrawlState> allCrawlStates) {
        var crawlState = allCrawlStates.getOrDefault(domain, new MetrikaDomainCrawlState(domain));
        boolean hasProblem = crawlState.getEnabledCounters().isEmpty();
        if (hasProblem) {
            problemsBatch.put(MetrikaCountersUtil.domainToHostId(domain), new ProblemSignal(
                    new SiteProblemContent.NoMetrikaCounterCrawlEnabled(), DateTime.now()
            ));
            state.domainsNoCrawlEnabled++;
        } else {
            state.domainsCrawlEnabled++;
        }
    }

    @Override
    public PeriodicTaskType getType() {
        return PeriodicTaskType.UPDATE_METRIKA_CRAWL_PROBLEMS;
    }

    @Override
    public TaskSchedule getSchedule() {
        return TaskSchedule.startByCron("0 5 * * * *");
    }

    public static class TaskState implements PeriodicTaskState {
        public int domainsWithApprovedBindings;
        public int domainsNoCrawlEnabled;
        public int domainsCrawlEnabled;
        public int domainsNoCrawlEnabledSkipped;
        public int invalidBindings;
    }
}
