package ru.yandex.webmaster3.worker.turbo;

import java.io.ByteArrayInputStream;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Strings;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.apache.http.HttpEntity;
import org.jetbrains.annotations.NotNull;
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.w3c.dom.Document;
import org.xml.sax.SAXParseException;

import ru.yandex.webmaster3.core.WebmasterException;
import ru.yandex.webmaster3.core.http.WebmasterErrorResponse;
import ru.yandex.webmaster3.core.metrics.Category;
import ru.yandex.webmaster3.core.turbo.TurboConstants;
import ru.yandex.webmaster3.core.turbo.ValidateTurboFeedTaskData;
import ru.yandex.webmaster3.core.turbo.model.TurboHostSettings;
import ru.yandex.webmaster3.core.turbo.model.TurboUrl;
import ru.yandex.webmaster3.core.turbo.model.error.TurboRawError;
import ru.yandex.webmaster3.core.turbo.model.error.TurboSeverity;
import ru.yandex.webmaster3.core.turbo.model.feed.TurboFeedSettings;
import ru.yandex.webmaster3.core.turbo.model.feed.TurboFeedState;
import ru.yandex.webmaster3.core.turbo.model.feed.TurboFeedStatistics;
import ru.yandex.webmaster3.core.turbo.model.feed.TurboFeedType;
import ru.yandex.webmaster3.core.turbo.xml.TurboXMLReader;
import ru.yandex.webmaster3.core.util.RetryUtils;
import ru.yandex.webmaster3.core.util.WwwUtil;
import ru.yandex.webmaster3.core.util.XmlUtil;
import ru.yandex.webmaster3.core.worker.task.TaskResult;
import ru.yandex.webmaster3.core.zora.ZoraForValidatorsService;
import ru.yandex.webmaster3.storage.turbo.dao.TurboFeedsSamplesYDao;
import ru.yandex.webmaster3.storage.turbo.dao.statistics.TurboFeedsStatistics2YDao;
import ru.yandex.webmaster3.storage.turbo.service.TurboFeedRawStatsData;
import ru.yandex.webmaster3.storage.turbo.service.TurboFeedsService;
import ru.yandex.webmaster3.storage.turbo.service.preview.TurboFeedPreviewService;
import ru.yandex.webmaster3.storage.turbo.service.settings.TurboSettingsService;
import ru.yandex.webmaster3.worker.Task;

/**
 * Задача валидации турбо-фида на соответствие схеме
 * Created by Oleg Bazdyrev on 02/08/2017.
 */
@Category("turbo")
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class ValidateTurboFeedTask extends Task<ValidateTurboFeedTaskData> {

    private static final Logger log = LoggerFactory.getLogger(ValidateTurboFeedTask.class);
    private static final ObjectMapper OM = new ObjectMapper();

    private final TurboFeedPreviewService turboFeedPreviewService;
    private final TurboFeedsSamplesYDao turboFeedsSamplesYDao;
    private final TurboFeedsService turboFeedsService;
    private final TurboFeedsStatistics2YDao turboFeedsStatistics2YDao;
    private final TurboSettingsService turboSettingsService;
    private final ZoraForValidatorsService zoraForValidatorsService;

    @Setter
    private Map<TurboFeedType, Long> maxFeedSize;

    private static TurboRawError createHttpError(Integer httpCode) {
        String code = "Fetcher.Http";
        if (httpCode != null) {
            if (httpCode / 100 == 3) {
                code = "Parser.Http3xx";
            } else if (httpCode / 100 == 4) {
                code = "Parser.Http4xx";
            } else if (httpCode / 100 == 5) {
                code = "Parser.Http5xx";
            }
        }
        return new TurboRawError(code, null, null, null, TurboSeverity.ERROR,
                OM.createObjectNode().put("http_code", httpCode));
    }

    private static TurboRawError createError(String code) {
        return new TurboRawError(code, null, null, null, TurboSeverity.ERROR, null);
    }

    private static TurboRawError createError(String code, Integer line, Integer column, String message) {
        return new TurboRawError(code, line, column, message, TurboSeverity.ERROR, null);
    }

    private static TurboFeedStatistics createErrorResult(ValidateTurboFeedTaskData data, TurboRawError error) {
        return new TurboFeedStatistics(data.getHostId(), data.getFeedType(), data.getFeedUrl(), false, null, null,
                DateTime.now(), null, Collections.singletonList(error), null, null);
    }

    @Override
    public Result run(ValidateTurboFeedTaskData data) throws Exception {
        String domain = WwwUtil.cutWWWAndM(data.getHostId());
        try {
            log.info("Downloading feed {}", data.getFeedUrl());
            TurboHostSettings hostSettings = turboSettingsService.getSettings(domain);
            String feedUrl = hostSettings.urlWithAuthentification(data.getFeedUrl());
            // login-password
            TurboFeedType feedType = data.getFeedType();
            TurboFeedStatistics stats;
            try {
                if (data.isRenewHost()) {
                    try {
                        zoraForValidatorsService.renewHost(data.getHostId());
                    } catch (Exception ignored) {
                        // игнорируем ошибки
                        log.error("Error when renewing host " + data.getHostId(), ignored);
                    }
                }

                stats = zoraForValidatorsService.processEntityGoZora(feedUrl, (HttpEntity httpEntity) -> {
                    try {
                        Document doc = TurboXMLReader.parseTurboFeed(httpEntity.getContent(),
                                TurboConstants.MAX_ITEMS_FOR_PREVIEW, null, maxFeedSize.get(feedType));
                        byte[] docBytes = XmlUtil.serializeDocument(doc);
                        // сразу прогрузим фид через http-загрузчик
                        log.info("Validating feed {}", data.getFeedUrl());
                        // log.debug("Feed: " + new String(docBytes));
                        TurboFeedRawStatsData rawStatsData = turboFeedPreviewService.uploadFeed(
                                data.getHostId(), data.getFeedUrl(), feedType, docBytes);
                        // сохраним фид в кэше (первый нормальный айтем)
                        List<TurboRawError> rawErrors = rawStatsData.collectTurboRawErrors();
                        List<TurboUrl> turboUrls = rawStatsData.collectTurboUrls("", false);

                        Optional<String> validUrl = turboUrls.stream().filter(turboUrl -> !turboUrl.isDeleted())
                                .filter(turboUrl -> !Strings.isNullOrEmpty(turboUrl.getTurboUrl()))
                                .map(TurboUrl::getUrl).findAny();
                        if (validUrl.isPresent()) {
                            log.info("Storing item {} for preview", validUrl.get());
                            // оставим только 1 элемент для превью
                            try {
                                Document shortDoc = TurboXMLReader.parseTurboFeed(
                                        new ByteArrayInputStream(docBytes), 0, validUrl.get(), 0L);
                                String content = new String(XmlUtil.serializeDocument(shortDoc));
                                turboFeedsSamplesYDao.putContent(domain, feedType, content);
                            } catch (Exception e) {
                                log.error("Error when saving preview for feed {}", feedUrl, e);
                            }
                        }

                        return new TurboFeedStatistics(data.getHostId(), feedType, data.getFeedUrl(), false,
                                turboUrls, null, DateTime.now(),
                                null, rawErrors, null, null);

                    } catch (TurboXMLReader.TurboFeedIsEmptyException e) {
                        return createErrorResult(data, createError("Parser.EmptyXML"));
                    } catch (TurboXMLReader.NoItemsFoundException e) {
                        return createErrorResult(data, createError("Parser.NoItems"));
                    } catch (TurboXMLReader.TurboFeedIsTooBigException e) {
                        return createErrorResult(data, createError("Parser.XMLTooBig"));
                    } catch (SAXParseException e) {
                        return createErrorResult(data, createError("Parser.XmlError",
                                e.getLineNumber(), e.getColumnNumber(), e.getMessage()));
                    } catch (Exception e) {
                        log.error("Unknown error when validating feed", e);
                        throw new WebmasterException("Unknown error",
                                new WebmasterErrorResponse.InternalUnknownErrorResponse(getDataClass(), "Unknown " +
                                        "error"), e);
                    }
                });

            } catch (WebmasterException e) {
                stats = processWebmasterException(data, e);
            }

            turboFeedsSamplesYDao.refreshUpdateDate(domain, feedType);

            // нежелательно тут терять данные
            TurboFeedStatistics finalStats = stats;
            RetryUtils.execute(RetryUtils.linearBackoff(4, Duration.standardSeconds(30L)), () ->
                    turboFeedsStatistics2YDao.updateStatistics(finalStats)
            );


        } catch (Exception e) {
            log.error("Error when validating turbo-feed", e);
        }
        RetryUtils.execute(RetryUtils.linearBackoff(4, Duration.standardSeconds(30L)), () -> updateFeed(domain, data.getFeedUrl()));

        return new Result(TaskResult.SUCCESS);
    }

    @NotNull
    private TurboFeedStatistics processWebmasterException(ValidateTurboFeedTaskData data, WebmasterException e) {
        if (e.getError() instanceof WebmasterErrorResponse.SitaErrorResponse) {
            // http error
            log.error("Zora error when downloading feed {}", data.getFeedUrl());
            WebmasterErrorResponse.SitaErrorResponse error = (WebmasterErrorResponse.SitaErrorResponse) e.getError();
            return createErrorResult(data, createHttpError(error.getHttpCode()));
        } else {
            return createErrorResult(data, createError("Internal"));
        }
    }

    private void updateFeed(String domain, String feedUrl) {
        // для начала проверим, что такой фид еще есть в списке
        TurboFeedSettings feedSettings = turboFeedsService.getFeed(domain, feedUrl);
        if (feedSettings == null || feedSettings.getState() != TurboFeedState.VALIDATING) {
            log.warn("Feed {} does not exists for host {}. Ignoring", feedUrl, domain);
            return;
        }
        turboFeedsService.insertFeed(feedSettings.withState(TurboFeedState.DOWNLOADING).withValidateDate());
    }

    @Override
    public Class<ValidateTurboFeedTaskData> getDataClass() {
        return ValidateTurboFeedTaskData.class;
    }
}
