package ru.yandex.webmaster3.worker.feeds;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.collect.Range;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.text.StrSubstitutor;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import ru.yandex.webmaster3.core.feeds.feed.NativeFeedInfo2;
import ru.yandex.webmaster3.core.feeds.feed.NativeFeedStatus;
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.feeds.FeedsNative2YDao;
import ru.yandex.webmaster3.storage.host.CommonDataState;
import ru.yandex.webmaster3.storage.host.CommonDataType;
import ru.yandex.webmaster3.storage.settings.SettingsService;
import ru.yandex.webmaster3.storage.util.yt.AsyncTableReader;
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.yql.YqlService;
import ru.yandex.webmaster3.worker.PeriodicTask;
import ru.yandex.webmaster3.worker.TaskSchedule;


/**
 * @author kravchenko99
 * @date 9/29/21
 */


@Slf4j
@RequiredArgsConstructor(onConstructor_ = @Autowired)
@Component
public class ImportFeedsParsingErrorTask extends PeriodicTask<ImportFeedsParsingErrorTask.TaskState> {
    private static final Set<String> OFFER_FEEDS_ERROR_CODES_SKIP_LIST = Set.of("Images.StillWaiting");
    private static final Integer MAX_SIZE = 5;


    private final SettingsService settingsService;
    private final YqlService yqlService;
    private final FeedsNative2YDao feedsNativeYDao;
    private final YtService ytService;
    @Value("${external.yt.service.hahn.root.default}/export/feeds/unisearch/offer_feeds")
    private final YtPath offerFeedsPath;
    @Value("${external.yt.service.hahn.root.default}/export/feeds/unisearch/offer_feeds_moderation")
    private final YtPath offerFeedsModerationPath;
    @Setter
    private YtPath turboLogsPath;
    @Setter
    private YtPath tmpPath;
    @Setter
    private String schedule;
    @Setter
    @Getter
    private PeriodicTaskType type;
    @Setter
    private CommonDataType commonDataType;

    @Override
    public Result run(UUID runId) throws Exception {
        CommonDataState settingUncached = settingsService.getSettingUncached(commonDataType);
        Long version = ytService.withoutTransactionQuery(cypressService -> {
            return cypressService.list(turboLogsPath).stream().map(YtPath::getName)
                    .map(Long::parseLong).max(Comparator.comparingLong(x -> x)).orElse(0L);
        });
        if (settingUncached != null && settingUncached.getValue().equals(version.toString())) {
            return new Result(TaskResult.SUCCESS);
        }
        StrSubstitutor substitutor = new StrSubstitutor(Map.of(
                "CLUSTER", offerFeedsPath.getCluster(),
                "LOGS", YtPath.path(turboLogsPath, version.toString()).toYtPath(),
                "OFFER_FEEDS", offerFeedsPath.toYtPath(),
                "OFFER_FEEDS_MODERATION", offerFeedsModerationPath.toYtPath(),
                "TMP_PATH", tmpPath.toYqlPath()
        ));
        yqlService.execute(
                substitutor.replace(
                        """
                                PRAGMA yt.InferSchema = '1';
                                PRAGMA yt.MaxRowWeight = '128M';
                                use ${CLUSTER};
                                insert into ${TMP_PATH} WITH TRUNCATE
                                SELECT
                                domain, feed_url, a.status as status, document, a.errors as errors
                                FROM `${LOGS}` as a
                                inner join  Concat(`${OFFER_FEEDS}`, `${OFFER_FEEDS_MODERATION}`) as b on
                                a.feed=b.feed_url
                                where document is null or document is not null and (a.status='error' or
                                a.status='warning')
                                order by domain, feed_url, document, status;
                                """
                )
        );
        Map<Pair<String, String>, Res> mp = new HashMap<>();
        ytService.inTransaction(tmpPath).execute(cypressService -> {

            var tableReadDriver = YtTableReadDriver.createYSONDriver(Row.class);
            var tableReader = new AsyncTableReader<>(cypressService, tmpPath, Range.all(), tableReadDriver)
                    .withRetry(5);
            try (AsyncTableReader.TableIterator<Row> read = tableReader.read()) {
                while (read.hasNext()) {
                    Row next = read.next();
                    Pair<String, String> key = Pair.of(next.domain, next.url);
                    Res res = mp.computeIfAbsent(key, x -> new Res(new ArrayList<>()));
                    if (next.isFeedStatus()) {
                        res.setStatus(next.status);
                    }
                    if (next.errors == null) {
                        continue;
                    }
                    for (var error : next.errors) {
                        if (res.getSize() >= MAX_SIZE) {
                            break;
                        }
                        if (OFFER_FEEDS_ERROR_CODES_SKIP_LIST.contains(error.codePublic)) {
                            continue;
                        }
                        if (error.details == null) {
                            error.details = new HashMap<>();
                        }
                        error.details.put("codePublic", error.codePublic);
                        if (next.document != null) {
                            error.details.put("document", next.document);
                        }
                        if (error.message != null) {
                            error.details.put("message", error.message);
                        }
                        if (error.severity != null) {
                            error.details.put("severity", error.severity);
                        }

                        res.errorsOfferBase.add(error.details);
                    }
                }
            } catch (IOException e) {
                log.error("Error reading table", e);
                throw new RuntimeException(e);
            }

            return true;
        });
        List<NativeFeedInfo2> collect = new ArrayList<>();


        feedsNativeYDao.forEach(c -> {
            Pair<String, String> key = Pair.of(c.getDomain(), c.getUrl());
            Res res = mp.get(key);
            if (res == null) {
                return;
            }

            if (res.getStatus() != null &&
                    (!c.getStatus().getStatus().equals(res.getStatus()) || !c.getErrorsOfferBase().equals(res.getErrorsOfferBase()))) {
                collect.add(toNativeFeedInfo(c.getDomain(), c.getUrl(), res));
            }

        });

        feedsNativeYDao.updateError(collect);
        settingsService.update(commonDataType, version.toString());

        return new Result(TaskResult.SUCCESS);
    }

    private NativeFeedInfo2 toNativeFeedInfo(String domain, String url, Res res) {
        NativeFeedStatus status = res.getSize() == 0 ? NativeFeedStatus.SUCCESS :
                NativeFeedStatus.S.get(res.getStatus());
        return NativeFeedInfo2.builder()
                .domain(domain)
                .url(url)
                .errorsOfferBase(res.getErrorsOfferBase())
                .status(status)
                .build();
    }


    @lombok.Value
    @AllArgsConstructor(onConstructor_ = @JsonCreator)
    public static class Row {
        @JsonProperty("domain")
        String domain;
        @JsonProperty("feed_url")
        String url;
        @JsonProperty("status")
        String status;
        @JsonProperty("document")
        String document;
        @JsonProperty(value = "errors")
        List<RowError> errors;

        public boolean isFeedStatus() {
            return document == null;
        }

    }

    public static class RowError {

        @JsonProperty("code_public")
        String codePublic;
        @JsonProperty("details")
        Map<String, Object> details;
        @JsonProperty("message")
        String message;
        @JsonProperty("severity")
        String severity;
    }

    @lombok.Data
    public static class Res {

        @NonNull
        private List<Map<String, Object>> errorsOfferBase;
        String status;

        public int getSize() {
            return errorsOfferBase.size();
        }
    }


    @Getter
    public static class TaskState implements PeriodicTaskState {

    }

    @Override
    public TaskSchedule getSchedule() {
        return schedule == null ? TaskSchedule.never() : TaskSchedule.startByCron(schedule);
    }
}
