package ru.yandex.webmaster3.storage.checklist.dao;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.collect.Lists;
import org.apache.commons.lang3.tuple.Pair;
import org.jetbrains.annotations.Nullable;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.springframework.stereotype.Repository;

import ru.yandex.webmaster3.core.WebmasterException;
import ru.yandex.webmaster3.core.checklist.data.SiteProblemContent;
import ru.yandex.webmaster3.core.checklist.data.SiteProblemState;
import ru.yandex.webmaster3.core.checklist.data.SiteProblemTypeEnum;
import ru.yandex.webmaster3.core.data.WebmasterHostId;
import ru.yandex.webmaster3.storage.checklist.data.RealTimeSiteProblemInfo;
import ru.yandex.webmaster3.storage.checklist.data.SiteProblemContentSerializer;
import ru.yandex.webmaster3.storage.util.ydb.AbstractYDao;
import ru.yandex.webmaster3.storage.util.ydb.query.Statement;
import ru.yandex.webmaster3.storage.util.ydb.querybuilder.BatchUpdate;
import ru.yandex.webmaster3.storage.util.ydb.querybuilder.typesafe.DataMapper;
import ru.yandex.webmaster3.storage.util.ydb.querybuilder.typesafe.Field;
import ru.yandex.webmaster3.storage.util.ydb.querybuilder.typesafe.Fields;
import ru.yandex.webmaster3.storage.util.ydb.querybuilder.typesafe.ValueDataMapper;


/**
 * @author avhaliullin
 */
@Repository
public class RealTimeSiteProblemsYDao extends AbstractYDao {
    private static final Logger log = org.slf4j.LoggerFactory.getLogger(RealTimeSiteProblemsYDao.class);
    private final int batchSize;

    public RealTimeSiteProblemsYDao() {
        super(PREFIX_WEBMASTER3, "real_time_site_problems");
        //Максимальное кол-во ответов которое может вернуть запрос это 1001, ограничиваем что бы не было проблем
        batchSize = 1000 / SiteProblemTypeEnum.values().length;
    }

    public List<RealTimeSiteProblemInfo> listSiteProblems(WebmasterHostId hostId) {
        return select(MAPPER).where(F.HOST_ID.eq(hostId)).queryForList()
                .stream().filter(Objects::nonNull).collect(Collectors.toList());
    }

    private Statement hostProblemsStatement(List<WebmasterHostId> hostIds, @Nullable SiteProblemTypeEnum problemType) {
        var result = select(MAPPER).where(F.HOST_ID.in(hostIds));
        if (problemType != null) {
            result = result.and(F.PROBLEM_TYPE.eq(problemType));
        }
        return result.getStatement();
    }

    public Map<WebmasterHostId, RealTimeSiteProblemInfo> listSitesProblems(
            Collection<WebmasterHostId> hostIds, SiteProblemTypeEnum problemType) {
        List<Statement> statements = Lists.partition(new ArrayList<>(hostIds), batchSize).stream()
                .map(hostId -> hostProblemsStatement(hostId, problemType))
                .collect(Collectors.toList());
        return asyncExecute(statements, MAPPER).stream()
                .collect(Collectors.toMap(RealTimeSiteProblemInfo::getHostId, Function.identity()));
    }

    public Map<WebmasterHostId, List<RealTimeSiteProblemInfo>> listSitesProblems(
            Collection<WebmasterHostId> hostIds) {
        List<Statement> statements = Lists.partition(new ArrayList<>(hostIds), batchSize).stream()
                .distinct()
                .map(hostId -> hostProblemsStatement(hostId, null))
                .collect(Collectors.toList());

        return asyncExecute(statements, MAPPER)
                .stream()
                .filter(Objects::nonNull)
                .collect(Collectors.groupingBy(RealTimeSiteProblemInfo::getHostId));
    }

    public RealTimeSiteProblemInfo getProblemInfo(WebmasterHostId hostId, SiteProblemTypeEnum problemType) {
        return select(MAPPER)
                .where(F.HOST_ID.eq(hostId))
                .and(F.PROBLEM_TYPE.eq(problemType))
                .queryOne();
    }

    public void addProblem(RealTimeSiteProblemInfo problemInfo) {

        upsert(
                F.HOST_ID.value(problemInfo.getHostId()),
                F.PROBLEM_TYPE.value(problemInfo.getProblemType()),
                F.ACTUAL_SINCE.value(problemInfo.getActualSince()),
                F.LAST_UPDATE.value(problemInfo.getLastUpdate()),
                F.LAST_FLUSHED.value(problemInfo.getLastFlushed()),
                F.PROBLEM_INFO.value(SiteProblemContentSerializer.serialize(problemInfo.getContent())),
                F.WEIGHT.value(problemInfo.getWeight()),
                F.STATE.value(problemInfo.getState())
        ).execute(); // данные периодически обновляются из Yt, поэтому можно писать с ConsistencyLevel.ONE


    }

    public void addProblems(List<RealTimeSiteProblemInfo> list) {
        final List<BatchUpdate<RealTimeSiteProblemInfo>> collect = Lists.partition(list, 200).stream()
                .map(e -> batchUpdate(UPDATE_DATA_MAPPER, e))
                .collect(Collectors.toList());
        asyncExecute(collect);
    }

    public void deleteProblems(WebmasterHostId hostId, List<SiteProblemTypeEnum> problems) {
        List<Statement> statements = problems.stream().map(problemType ->
                delete()
                        .where(F.HOST_ID.eq(hostId))
                        .and(F.PROBLEM_TYPE.eq(problemType))
                        .getStatement()
        ).collect(Collectors.toList());

        try {
            asyncExecute(statements);
        } catch (Exception e) {
            for (Statement st : statements) {
                execute(st);
            }
        }
    }

    public void deleteProblems(Collection<WebmasterHostId> hostIds, Collection<SiteProblemTypeEnum> problems) {
        if (hostIds.isEmpty() || problems.isEmpty()) {
            return;
        }

        List<Pair<WebmasterHostId, SiteProblemTypeEnum>> items = new ArrayList<>();

        for (var hostId : hostIds) {
            for (var problem : problems) {
                items.add(Pair.of(hostId, problem));
            }
        }

        batchDelete(DELETE_DATA_MAPPER, items).execute();
    }

    public void deleteProblems(Collection<Pair<WebmasterHostId, SiteProblemTypeEnum>> hostIdsWithProblemType) {
        if (hostIdsWithProblemType.isEmpty()) {
            return;
        }
        batchDelete(DELETE_DATA_MAPPER, hostIdsWithProblemType).execute();
    }

    public void forEach(Consumer<RealTimeSiteProblemInfo> consumer) {
        streamReader(MAPPER, consumer);
    }

    private static final DataMapper<RealTimeSiteProblemInfo> MAPPER = DataMapper.create(
            F.HOST_ID, F.PROBLEM_TYPE, F.STATE, F.PROBLEM_INFO, F.LAST_UPDATE, F.LAST_FLUSHED, F.ACTUAL_SINCE, F.WEIGHT,
            (hostId, problemType, state, problemInfo, lastUpdate, lastFlushed, actualSince, weight) -> {
                if (problemType == null || state == null) {
                    return null;
                }
                try {
                    if (problemType == SiteProblemTypeEnum.NOT_MOBILE_FRIENDLY && state == SiteProblemState.PRESENT && problemInfo == null) {
                        log.warn("Empty mobile friendly problem for host {}", hostId);
                        return null;
                    }
                    SiteProblemContent problemContent = state == SiteProblemState.PRESENT || problemInfo != null ?
                            SiteProblemContentSerializer.deserialize(problemType, problemInfo) :
                            null;
                    if (lastFlushed == null) {
                        lastFlushed = actualSince;
                    }
                    return new RealTimeSiteProblemInfo(hostId, lastUpdate, actualSince, lastFlushed, state, problemType, problemContent, weight);
                } catch (WebmasterException e) {
                    if (problemType == SiteProblemTypeEnum.NOT_MOBILE_FRIENDLY && e.getCause() != null &&
                            e.getCause() instanceof JsonProcessingException) {
                        log.warn("Bad mobile friendly problem for host {}", hostId);
                        return null;
                    } else {
                        throw e;
                    }
                }
            }
    );

    private static final ValueDataMapper<RealTimeSiteProblemInfo> UPDATE_DATA_MAPPER = ValueDataMapper.create(
            Pair.of(F.HOST_ID, data -> F.HOST_ID.get(data.getHostId())),
            Pair.of(F.PROBLEM_TYPE, data -> F.PROBLEM_TYPE.get(data.getProblemType())),
            Pair.of(F.ACTUAL_SINCE, data -> F.ACTUAL_SINCE.get(data.getActualSince())),
            Pair.of(F.LAST_UPDATE, data -> F.LAST_UPDATE.get(data.getLastUpdate())),
            Pair.of(F.LAST_FLUSHED, data -> F.LAST_FLUSHED.get(data.getLastFlushed())),
            Pair.of(F.PROBLEM_INFO, data -> F.PROBLEM_INFO.get(SiteProblemContentSerializer.serialize(data.getContent()))),
            Pair.of(F.WEIGHT, data -> F.WEIGHT.get(data.getWeight())),
            Pair.of(F.STATE, data -> F.STATE.get(data.getState()))
    );

    private static final ValueDataMapper<Pair<WebmasterHostId, SiteProblemTypeEnum>> DELETE_DATA_MAPPER = ValueDataMapper.create2(
            Pair.of(F.HOST_ID, Pair::getKey),
            Pair.of(F.PROBLEM_TYPE, Pair::getValue)
    );

    protected interface F {
        Field<WebmasterHostId> HOST_ID = Fields.hostIdField("host_id");
        Field<SiteProblemTypeEnum> PROBLEM_TYPE = Fields.intEnumField("problem_type", SiteProblemTypeEnum.R);
        Field<DateTime> ACTUAL_SINCE = Fields.jodaDateTimeField("actual_since").makeOptional();
        Field<DateTime> LAST_UPDATE = Fields.jodaDateTimeField("last_update").makeOptional();
        Field<DateTime> LAST_FLUSHED = Fields.jodaDateTimeField("last_flushed").makeOptional();
        Field<String> PROBLEM_INFO = Fields.stringRawField("problem_info").makeOptional();
        Field<Integer> WEIGHT = Fields.intField("weight").withDefault(0);
        Field<SiteProblemState> STATE = Fields.intEnumField("state", SiteProblemState.R).withDefault(SiteProblemState.UNDEFINED);
    }
}
