package ru.yandex.webmaster3.worker.metrika;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.collect.Range;
import org.apache.commons.lang3.mutable.MutableBoolean;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import ru.yandex.webmaster3.core.WebmasterException;
import ru.yandex.webmaster3.core.data.WebmasterHostId;
import ru.yandex.webmaster3.core.http.WebmasterErrorResponse;
import ru.yandex.webmaster3.core.metrika.counters.MetrikaCountersUtil;
import ru.yandex.webmaster3.core.util.RetryUtils;
import ru.yandex.webmaster3.core.worker.task.PeriodicTaskState;
import ru.yandex.webmaster3.core.worker.task.PeriodicTaskType;
import ru.yandex.webmaster3.core.worker.task.TaskResult;
import ru.yandex.webmaster3.storage.events.data.WMCEvent;
import ru.yandex.webmaster3.storage.events.data.events.RetranslateToUsersEvent;
import ru.yandex.webmaster3.storage.events.data.events.UserHostMessageEvent;
import ru.yandex.webmaster3.storage.events.service.WMCEventsService;
import ru.yandex.webmaster3.storage.host.AllHostsCacheService;
import ru.yandex.webmaster3.storage.host.CommonDataState;
import ru.yandex.webmaster3.storage.metrika.MetrikaCrawlStateService;
import ru.yandex.webmaster3.storage.metrika.dao.MetrikaCrawlSamplesInfoYDao;
import ru.yandex.webmaster3.storage.metrika.data.MetrikaCrawlSampleInfo;
import ru.yandex.webmaster3.storage.settings.dao.CommonDataStateYDao;
import ru.yandex.webmaster3.storage.user.message.content.MessageContent;
import ru.yandex.webmaster3.storage.user.notification.NotificationType;
import ru.yandex.webmaster3.storage.util.yt.*;
import ru.yandex.webmaster3.worker.PeriodicTask;
import ru.yandex.webmaster3.worker.TaskSchedule;

import java.util.ArrayList;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.*;
import java.util.stream.Collectors;

import static ru.yandex.webmaster3.storage.host.CommonDataType.METRIKA_CRAWL_SAMPLES_INFO_LAST_UPDATE;
import static ru.yandex.webmaster3.storage.host.CommonDataType.METRIKA_CRAWL_SAMPLES_LAST_UPDATE;
import static ru.yandex.webmaster3.worker.metrika.ImportMetrikaCrawlSamplesTask.ATTR_UPDATE_TIMESTAMP;

/**
 * @author leonidrom
 */
@Component("importMetrikaCrawlSamplesInfoTask")
public class ImportMetrikaCrawlSamplesInfoTask extends PeriodicTask<ImportMetrikaCrawlSamplesInfoTask.TaskState> {
    private static final Logger log = LoggerFactory.getLogger(ImportMetrikaCrawlSamplesInfoTask.class);
    private static final RetryUtils.RetryPolicy RETRY_POLICY =
            RetryUtils.linearBackoff(3, Duration.standardSeconds(30));
    public static final int TOTAL_THREADS = 16;

    private final MetrikaCrawlSamplesInfoYDao metrikaCrawlSamplesInfoYDao;
    private final CommonDataStateYDao commonDataStateYDao;
    private final YtService ytService;
    private final MetrikaCrawlStateService metrikaCrawlStateService;
    private final AllHostsCacheService allHostsCacheService;
    private final WMCEventsService wmcEventsService;
    private final YtPath tablePath;

    @Autowired
    public ImportMetrikaCrawlSamplesInfoTask(
            MetrikaCrawlSamplesInfoYDao metrikaCrawlSamplesInfoYDao, CommonDataStateYDao commonDataStateYDao,
            YtService ytService,
            MetrikaCrawlStateService metrikaCrawlStateService,
            AllHostsCacheService allHostsCacheService,
            WMCEventsService wmcEventsService,
            @Value("${webmaster3.worker.metrika.crawSamples.info.path}") YtPath tablePath) {
        this.metrikaCrawlSamplesInfoYDao = metrikaCrawlSamplesInfoYDao;
        this.commonDataStateYDao = commonDataStateYDao;
        this.ytService = ytService;
        this.metrikaCrawlStateService = metrikaCrawlStateService;
        this.allHostsCacheService = allHostsCacheService;
        this.wmcEventsService = wmcEventsService;
        this.tablePath = tablePath;
    }

    @Override
    public Result run(UUID runId) throws Exception {
        setState(new TaskState());

        log.info("Started importing Metrika crawl samples info");

        CommonDataState lastInfoUpdateState = commonDataStateYDao.getValue(METRIKA_CRAWL_SAMPLES_INFO_LAST_UPDATE);
        DateTime lastInfoUpdateTime = lastInfoUpdateState == null?
                new DateTime(0) : new DateTime(Long.parseLong(lastInfoUpdateState.getValue()));

        CommonDataState lastSamplesUpdateState = commonDataStateYDao.getValue(METRIKA_CRAWL_SAMPLES_LAST_UPDATE);
        DateTime lastSamplesUpdateTime = lastSamplesUpdateState == null?
                new DateTime(0) : new DateTime(Long.parseLong(lastSamplesUpdateState.getValue()));

        ytService.inTransaction(tablePath).execute(cypressService -> {
            YtNode node = cypressService.getNode(tablePath);
            if (node != null) {
                String updateTimestampAsText = node.getNodeMeta().get(ATTR_UPDATE_TIMESTAMP).asText();
                DateTime tableUpdateTime = updateTimestampAsText == null?
                        new DateTime(0) : new DateTime(Long.parseLong(updateTimestampAsText) * 1000);
                if (lastInfoUpdateTime.isBefore(tableUpdateTime)) {
                    if (lastSamplesUpdateTime.isBefore(tableUpdateTime)) {
                        log.info("Need to import samples for {} first", tableUpdateTime);
                    } else {
                        log.info("Got new data to import for {}", tableUpdateTime);
                        importMetrikaCrawlSamples(cypressService, tableUpdateTime);
                        getState().tableUpdateTime = tableUpdateTime;
                    }
                } else {
                    log.info("Nothing to import.");
                }
            } else {
                log.error("Table {} is missing", tablePath);
            }

            return true;
        });

        log.info("Finished importing Metrika crawl samples info");

        return new Result(TaskResult.SUCCESS);
    }

    private void importMetrikaCrawlSamples(YtCypressService cypressService, DateTime tableUpdateTime) {
        try {
            var reader = new AsyncTableReader<>(cypressService, tablePath, Range.all(),
                    YtTableReadDriver.createYSONDriver(YtRow.class)).withRetry(3);

            ExecutorService executorService = ru.yandex.common.util.concurrent.Executors.newBlockingFixedThreadPool(
                    TOTAL_THREADS, TOTAL_THREADS,
                    0, TimeUnit.MILLISECONDS,
                    new ArrayBlockingQueue<>(TOTAL_THREADS),
                    Executors.defaultThreadFactory());
            ArrayList<Future<?>> futures = new ArrayList<>();
            try (var iterator = reader.read()) {
                while (iterator.hasNext()) {
                    var row = iterator.next();
                    futures.add(executorService.submit(() -> processSampleInfo(row.toMetrikaCrawlSampleInfo())));
                }
            }

            for (var f : futures) {
                f.get();
            }

            commonDataStateYDao.update(new CommonDataState(METRIKA_CRAWL_SAMPLES_INFO_LAST_UPDATE,
                    String.valueOf(tableUpdateTime.getMillis()), DateTime.now()));

            executorService.shutdownNow();
            getState().totalRows = futures.size();
        } catch (Exception e) {
            throw new WebmasterException("Failed to import samples info",
                    new WebmasterErrorResponse.InternalUnknownErrorResponse(getClass(), "Failed to import samples info"), e);
        }
    }

    private void processSampleInfo(MetrikaCrawlSampleInfo sampleInfo) {
        log.info("Sample: {}", sampleInfo);

        try {
            RetryUtils.execute(RETRY_POLICY, () -> {
                metrikaCrawlSamplesInfoYDao.save(sampleInfo);
            });

            if (sampleInfo.isCrawlSuspended()) {
                var wasSuspended = new MutableBoolean(false);
                RetryUtils.execute(RETRY_POLICY, () -> {
                    boolean rv = metrikaCrawlStateService.maybeSuspendCounterCrawl(
                            sampleInfo.getDomain(), sampleInfo.getCounterId(), sampleInfo.getLastUpdated());
                    wasSuspended.setValue(rv);
                });

                if (wasSuspended.booleanValue()) {
                    sendCrawlSuspendedNotifications(sampleInfo.getDomain(), sampleInfo.getCounterId());
                }
            }
        } catch (Exception e) {
            log.error("Error updating YDB", e);
            throw new WebmasterException("Error updating YDB",
                    new WebmasterErrorResponse.YDBErrorResponse(getClass(), e), e);
        }
    }

    private void sendCrawlSuspendedNotifications(String domain, long counterId) {
        var events = MetrikaCountersUtil.generateHostIds(domain)
                .filter(allHostsCacheService::contains)
                .map(hostId -> createCrawlSuspendedEvent(hostId, counterId))
                .collect(Collectors.toList());
        wmcEventsService.addEvents(events);
    }

    private WMCEvent createCrawlSuspendedEvent(WebmasterHostId hostId, long counterId) {
        return WMCEvent.create(
                new RetranslateToUsersEvent<>(
                        new UserHostMessageEvent<>(
                                hostId,
                                null,
                                new MessageContent.HostMetrikaCounterCrawlSuspended(hostId, counterId),
                                NotificationType.METRIKA_COUNTER_CRAWL, false
                        ),
                        Collections.emptyList()
                )
        );
    }

    @JsonIgnoreProperties(ignoreUnknown=true)
    public static final class YtRow {
        private final String domain;
        private final long counterId;
        private final boolean dropCrawlPermission;
        private final long updateTimestamp;

        @JsonCreator
        public YtRow(
                @JsonProperty("Domain") String domain,
                @JsonProperty("CounterId") long counterId,
                @JsonProperty("DropCrawlPermission") boolean dropCrawlPermission,
                @JsonProperty("UpdateTimestamp") long updateTimestamp) {
            this.domain = domain;
            this.counterId = counterId;
            this.dropCrawlPermission = dropCrawlPermission;
            this.updateTimestamp = updateTimestamp;
        }

        private MetrikaCrawlSampleInfo toMetrikaCrawlSampleInfo() {
            return new MetrikaCrawlSampleInfo(
                    MetrikaCountersUtil.domainToCanonicalAscii(domain),
                    counterId,
                    dropCrawlPermission,
                    new DateTime(updateTimestamp * 1000));
        }
    }

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

    @Override
    public TaskSchedule getSchedule() {
        // Запускаем через 50 мин после таски ImportMetrikaCrawlSamplesTask,
        // чтобы дать ей возможность завершиться
        return TaskSchedule.startByCron("0 55 * * * *");
    }

    public static class TaskState implements PeriodicTaskState {
        public int totalRows;
        public DateTime tableUpdateTime;
    }
}
