package ru.yandex.webmaster3.worker.turbo.bans;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Range;
import com.google.common.collect.Sets;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
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.checklist.data.SiteProblemContent;
import ru.yandex.webmaster3.core.checklist.data.SiteProblemContent.TurboHostBan;
import ru.yandex.webmaster3.core.checklist.data.SiteProblemContent.TurboHostBan.TurboHostBanSeverity;
import ru.yandex.webmaster3.core.checklist.data.SiteProblemTypeEnum;
import ru.yandex.webmaster3.core.data.WebmasterHostId;
import ru.yandex.webmaster3.core.http.WebmasterErrorResponse;
import ru.yandex.webmaster3.core.util.IdUtils;
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.checklist.dao.ChecklistPageSamplesService;
import ru.yandex.webmaster3.storage.checklist.dao.ChecklistSamplesType;
import ru.yandex.webmaster3.storage.checklist.dao.RealTimeSiteProblemsYDao;
import ru.yandex.webmaster3.storage.checklist.dao.SiteProblemsRecheckYDao;
import ru.yandex.webmaster3.storage.checklist.data.ProblemSignal;
import ru.yandex.webmaster3.storage.checklist.data.RealTimeSiteProblemInfo;
import ru.yandex.webmaster3.storage.checklist.data.SiteProblemRecheckInfo;
import ru.yandex.webmaster3.storage.checklist.data.TurboHostBanSample;
import ru.yandex.webmaster3.storage.checklist.service.SiteProblemsService;
import ru.yandex.webmaster3.storage.host.CommonDataState;
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.worker.PeriodicTask;
import ru.yandex.webmaster3.worker.TaskSchedule;

import static ru.yandex.webmaster3.storage.host.CommonDataType.TURBO_HOST_BAN;

/**
 * Created by Oleg Bazdyrev on 06/02/2018.
 */
@Component("importTurboHostBansTask")
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class ImportTurboHostBansTask extends PeriodicTask<ImportTurboHostBansTask.State> {

    private static final Logger log = LoggerFactory.getLogger(ImportTurboHostBansTask.class);
    private static final DateTimeFormatter TABLE_DATE_FORMAT = DateTimeFormat.forPattern("y-M-d-H-m-s");
    private static final ObjectMapper OM = new ObjectMapper().configure(JsonGenerator.Feature.ESCAPE_NON_ASCII, true);

    private static final Set<SiteProblemTypeEnum> CONNECTED_TYPES = Set.of(SiteProblemTypeEnum.TURBO_HOST_BAN, SiteProblemTypeEnum.TURBO_HOST_BAN_INFO, SiteProblemTypeEnum.TURBO_HOST_BAN_OK);

    private final SiteProblemsService siteProblemsService;
    private final YtService ytService;
    private final SiteProblemsRecheckYDao siteProblemsRecheckYDao;
    private final SettingsService settingsService;
    private final RealTimeSiteProblemsYDao realTimeSiteProblemsYDao;
    private final ChecklistPageSamplesService checklistPageSamplesService;
    @Value("${webmaster3.worker.turbo.importHostBans.path}")
    private YtPath rootPath;

    @Override
    public Result run(UUID runId) throws Exception {
        log.info("Importing turbo host bans from {}", rootPath);
        DateTime updateStarted = DateTime.now();
        setState(new State());
        final CommonDataState value = settingsService.getSettingUncached(TURBO_HOST_BAN);
        ytService.withoutTransaction(cypressService -> {
            // ищем последнюю таблицу
            YtPath tablePath = cypressService.list(rootPath).stream().max(
                    Comparator.comparing(table -> TABLE_DATE_FORMAT.parseDateTime(table.getName()))).orElse(null);
            final long lastImportedTableTs = Long.parseLong(value.getValue());
            if (tablePath == null || TABLE_DATE_FORMAT.parseDateTime(tablePath.getName()).isEqual(lastImportedTableTs)) {
                return false;
            }
            state.tableProcessed = tablePath.toString();

            AsyncTableReader<TurboHostBanRow> reader = new AsyncTableReader<>(cypressService, tablePath, Range.all(),
                    YtTableReadDriver.createYSONDriver(TurboHostBanRow.class)).withRetry(3).splitInParts(10000L);

            try (AsyncTableReader.TableIterator<TurboHostBanRow> iterator = reader.read()) {
                while (iterator.hasNext()) {
                    TurboHostBanRow row = iterator.next();
                    WebmasterHostId hostId = IdUtils.urlToHostId(row.getHost());
                    TurboHostBanSeverity severity = TurboHostBanSeverity.R.fromValueOrNull(row.getSeverity());
                    Long deadlineTs = row.getDeadlineTs();

                    if (severity == null) {
                        log.warn("Unknown severity {}", row.getSeverity());
                        continue;
                    }
                    // Дальше пара костылей, так как турбо не хотят придерживаться нашей логики проблем
                    // Самые главные костыли:
                    //      1) Поле [Проверено] обновляем из присылаемого last_check_ts
                    //      2)

                    if (severity == TurboHostBanSeverity.OK) {
                        // статус OK своего рода костыль, выполнен через выключенную угрозу.
                        siteProblemsService.updateCleanableProblem(hostId, new ProblemSignal(new SiteProblemContent.TurboHostBanOk(), DateTime.now()));
                        cleanOtherTypes(hostId, Set.of(SiteProblemTypeEnum.TURBO_HOST_BAN_OK));
                        continue;
                    }
                    // генерируем проблему
                    // WARNING и BAN в TurboHostBan, INFO вынесено отдельно так как нельзя одну проблему сделать критичной в зависимости от контента
                    final SiteProblemContent content = severity == TurboHostBanSeverity.INFO
                            ? new SiteProblemContent.TurboHostBanInfo(severity, deadlineTs)
                            : new TurboHostBan(severity, deadlineTs);

                    // костыль для 1 случая
                    final DateTime lastCheck = row.getLastCheckTs();
                    final SiteProblemRecheckInfo recheckInfo = siteProblemsRecheckYDao.getRecheckInfo(hostId, content.getProblemType());

                    if (recheckInfo == null || recheckInfo.getRequestDate().isBefore(lastCheck)) {
                        siteProblemsRecheckYDao.updateRecheckState(hostId, content.getProblemType(), false, lastCheck);
                        state.countUpdateLastCheckTs++;
                    }

                    RealTimeSiteProblemInfo realTimeProblemInfo = siteProblemsService.getRealTimeProblemInfo(hostId,
                            content.getProblemType());

                    // чистим связанные проблемы, чтобы не показывать пользователю 2 проблемы
                    // кол-во проблем в районе 1500 поэтому удаляем без всяких батчевых оптимизаций
                    if (realTimeProblemInfo != null && lastCheck.isBefore(realTimeProblemInfo.getLastUpdate())) {
                        cleanOtherTypes(hostId, Set.of());
                        state.countWithSmallTs++;
                    } else {
                        cleanOtherTypes(hostId, Set.of(content.getProblemType()));
                    }
                    siteProblemsService.updateCleanableProblem(hostId, new ProblemSignal(content,  lastCheck), row.getTs());
                    saveSamples(row, hostId, content.getProblemType());
                    state.countUpdated++;
                }
            } catch (IOException e) {
                throw new WebmasterException("Yt IO error",
                        new WebmasterErrorResponse.YTServiceErrorResponse(getClass(), e), e);
            }
            settingsService.update(TURBO_HOST_BAN, String.valueOf(TABLE_DATE_FORMAT.parseDateTime(tablePath.getName()).getMillis()));

            siteProblemsService.notifyCleanableProblemUpdateFinished(SiteProblemTypeEnum.TURBO_HOST_BAN, updateStarted);
            siteProblemsService.notifyCleanableProblemUpdateFinished(SiteProblemTypeEnum.TURBO_HOST_BAN_INFO, updateStarted);
            siteProblemsService.notifyCleanableProblemUpdateFinished(SiteProblemTypeEnum.TURBO_HOST_BAN_OK, updateStarted);
            return true;
        });

        return new Result(TaskResult.SUCCESS);
    }

    private void cleanOtherTypes(WebmasterHostId hostId, Set<SiteProblemTypeEnum> problemTypes) {
        final ArrayList<SiteProblemTypeEnum> typesForClean = new ArrayList<>(Sets.difference(CONNECTED_TYPES, problemTypes));
        realTimeSiteProblemsYDao.deleteProblems(hostId, typesForClean);
    }

    private void saveSamples(TurboHostBanRow row, WebmasterHostId hostId, SiteProblemTypeEnum problemType) {
        List<String> samples = getTurboHostBanSamples(row);
        checklistPageSamplesService.saveSamples(hostId, ChecklistSamplesType.byProblemType(problemType), samples);
    }

    private List<String> getTurboHostBanSamples(TurboHostBanRow row) {
        List<String> result = new ArrayList<>();
        List<TurboHostBanSample> samples = row.getErrors().stream().map(TurboHostBanError::getSamples)
                .flatMap(Collection::stream).map(Sample::toTurboHostBanSample).collect(Collectors.toList());
        for (TurboHostBanSample sample : samples) {
            try {
                result.add(OM.writeValueAsString(sample));
            } catch (JsonProcessingException e) {
                throw new RuntimeException(e);
            }
        }
        return result;
    }

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

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

    @Getter
    @RequiredArgsConstructor(onConstructor_ = @JsonCreator)
    @ToString
    public static final class TurboHostBanRow {
        @JsonProperty("host")
        private final String host;
        @JsonProperty("severity")
        private final int severity;
        @JsonProperty("errors")
        private final List<TurboHostBanError> errors;
        @JsonProperty("ts")
        private final long ts;
        @JsonProperty("last_check_ts")
        private final long lastCheckTs;
        @JsonProperty("deadline_ts")
        private final Long deadlineTs;

        public DateTime getTs() {
            return new DateTime(ts * 1000);
        }

        public DateTime getLastCheckTs() {
            return new DateTime(lastCheckTs * 1000);
        }

        public Long getDeadlineTs() {
            return deadlineTs != null ? deadlineTs * 1000 : null;
        }
    }

    @Getter
    @RequiredArgsConstructor(onConstructor_ = @JsonCreator)
    public static final class TurboHostBanError {
        private final String code;
        private final List<String> urls;
        private final List<Sample> samples;
    }

    @Getter
    @RequiredArgsConstructor(onConstructor_ = @JsonCreator)
    public static final class Sample {
        @JsonProperty("frozen_turbo_url")
        private final String frozenTurboUrl;
        @JsonProperty("prod_turbo_url")
        private final String prodTurboUrl;
        @JsonProperty("original_url")
        private final String originalUrl;
        @JsonProperty("reasons")
        private final List<String> reasons;
        @JsonProperty("weight")
        private final int weight;

        public TurboHostBanSample toTurboHostBanSample() {
            return new TurboHostBanSample(frozenTurboUrl, prodTurboUrl, originalUrl, reasons, weight);
        }
    }


    @Getter
    public class State implements PeriodicTaskState {
        String tableProcessed;
        int countUpdated;
        int countUpdateLastCheckTs;
        int countWithSmallTs;
    }
}
