package ru.yandex.webmaster3.worker.metrika;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.joda.JodaModule;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
import com.google.common.collect.Range;
import lombok.Data;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.joda.time.Duration;
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.http.WebmasterJsonModule;
import ru.yandex.webmaster3.core.util.IdUtils;
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.storage.checklist.dao.ChecklistPageSamplesService;
import ru.yandex.webmaster3.storage.checklist.dao.ChecklistSamplesType;
import ru.yandex.webmaster3.storage.metrika.data.MetrikaCrawlLostHitsUrlSample;
import ru.yandex.webmaster3.storage.util.JsonDBMapping;
import ru.yandex.webmaster3.storage.util.yt.AsyncTableReader;
import ru.yandex.webmaster3.storage.util.yt.YtCypressService;
import ru.yandex.webmaster3.storage.util.yt.YtException;
import ru.yandex.webmaster3.storage.util.yt.YtPath;
import ru.yandex.webmaster3.storage.util.yt.YtService;
import ru.yandex.webmaster3.storage.util.yt.YtTableReadDriver;
import ru.yandex.webmaster3.storage.util.yt.YtUtils;
import ru.yandex.webmaster3.worker.PeriodicTask;
import ru.yandex.webmaster3.worker.TaskSchedule;


/**
 * @author leonidrom
 */
@Slf4j
@Component
public class ImportMetrikaCrawlLostHitsSamplesPeriodicTask extends PeriodicTask<ImportMetrikaCrawlLostHitsSamplesPeriodicTask.TaskState> {
    private static final Pattern TABLE_NAME_PATTERN = Pattern.compile("^.*/losthits-hosts-week\\.\\d\\d\\d\\d-\\d\\d-\\d\\d$");

    private static final RetryUtils.RetryPolicy RETRY_POLICY = RetryUtils.linearBackoff(5, Duration.standardSeconds(30));
    private static final ObjectMapper YT_OM = new ObjectMapper()
            .registerModule(new JodaModule())
            .registerModule(new ParameterNamesModule())
            .registerModule(new WebmasterJsonModule(false))
            .disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET)
            .disable(SerializationFeature.WRITE_NULL_MAP_VALUES)
            .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
            // для десериализации MetrikaCrawlLostHitsUrlSample
            .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES);

    private final YtService ytService;
    private final YtPath ytDirPath;
    private final ChecklistPageSamplesService checklistPageSamplesService;

    @Autowired
    public ImportMetrikaCrawlLostHitsSamplesPeriodicTask(
            YtService ytService,
            ChecklistPageSamplesService checklistPageSamplesService,
            @Value("arnold://home/robot-quality/losthits-stat") YtPath ytDirPath) {
        this.ytService = ytService;
        this.ytDirPath = ytDirPath;
        this.checklistPageSamplesService = checklistPageSamplesService;
    }

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

        YtPath tablePath = YtUtils.getLatestTablePath(ytService, ytDirPath, TABLE_NAME_PATTERN);
        if (tablePath == null) {
            log.info("Nothing to import");
            return Result.SUCCESS;
        }

        log.info("Table to import: {}", tablePath);
        importYtTable(tablePath);

        return Result.SUCCESS;
    }

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

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

    private void importYtTable(YtPath tablePath) {
        log.info("Started importing {}", tablePath);

        ytService.inTransaction(tablePath).execute(
                cypressService -> {
                    readYtTable(cypressService, tablePath, this::processYtRow);
                    return true;
                }
        );

        log.info("Finished importing {}", tablePath);
    }

    private void processYtRow(YtRow row) {
        getState().totalHosts++;

        WebmasterHostId hostId;
        try {
            hostId = IdUtils.urlToHostId(row.getHost());
        } catch (WebmasterException e) {
            log.error("Skipping invalid host {}", row.getHost());
            return;
        }

        log.info("Got samples for {}, size: {}", hostId, row.getSamples().size());
        List<String> serializedSamples = serializeSamples(row.getSamples());
        try {
            RetryUtils.execute(RETRY_POLICY, () -> {
                // идея здесь в том, чтобы независимо от того, какой алерт сейчас у пользователя,
                // мы всегда показывали бы ему семпля для его хоста
                checklistPageSamplesService.saveSamplesWithTTL(hostId, ChecklistSamplesType.METRIKA_CRAWL_LOST_HITS_SAMPLES_FOR_NO_METRIKA_COUNTER_BINDING, serializedSamples);
                checklistPageSamplesService.saveSamplesWithTTL(hostId, ChecklistSamplesType.METRIKA_CRAWL_LOST_HITS_SAMPLES_FOR_NO_METRIKA_COUNTER_CRAWL_ENABLED, serializedSamples);
            });
        } catch (InterruptedException e) {
            throw new WebmasterException("Failed to save samples",
                    new WebmasterErrorResponse.YDBErrorResponse(getClass(), e), e);
        }
    }

    private void readYtTable(YtCypressService cypressService, YtPath tablePath, Consumer<YtRow> consumer)
            throws YtException {
        var tableReader = new AsyncTableReader<>(cypressService, tablePath, Range.all(),
                YtTableReadDriver.createYSONDriver(YtRow.class, YT_OM));

        try (var it = tableReader.read()) {
            while (it.hasNext()) {
                YtRow row = it.next();
                if (row.isWebmasterHost()) {
                    consumer.accept(row);
                }
            }
        } catch (IOException | InterruptedException e) {
            throw new YtException("Unable to read table: " + tablePath, e);
        }
    }

    private List<String> serializeSamples(List<MetrikaCrawlLostHitsUrlSample> samples) {
        return samples.stream().map(sample -> {
            try {
                return JsonDBMapping.OM.writeValueAsString(sample);
            } catch (JsonProcessingException e) {
                throw new WebmasterException("Json error",
                        new WebmasterErrorResponse.InternalUnknownErrorResponse(getClass(), "Unknown json error"));
            }
        }).collect(Collectors.toList());
    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    @Data
    @ToString
    private static class YtRow {
        private final String host;
        private final boolean isWebmasterHost;
        private final ArrayList<MetrikaCrawlLostHitsUrlSample> samples;

        @JsonCreator
        public YtRow(
                @JsonProperty("Host") String host,
                @JsonProperty("IsWebmasterHost") boolean isWebmasterHost,
                @JsonProperty("TopUrlsWithShows") ArrayList<MetrikaCrawlLostHitsUrlSample> samples) {
            this.host = host;
            this.isWebmasterHost = isWebmasterHost;
            this.samples = samples;
        }
    }

    public static class TaskState implements PeriodicTaskState {
        public int totalHosts;
    }
}
