package ru.yandex.webmaster3.storage.addurl;

import java.util.Collection;
import java.util.List;
import java.util.UUID;
import java.util.function.Consumer;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Pair;
import org.jetbrains.annotations.Nullable;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import ru.yandex.webmaster3.core.addurl.RecrawlState;
import ru.yandex.webmaster3.core.addurl.UrlForRecrawl;
import ru.yandex.webmaster3.core.data.WebmasterHostId;
import ru.yandex.webmaster3.storage.util.ydb.AbstractYDao;
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 leonidrom
 */
@Repository
@Slf4j
public class AddUrlRequestsYDao extends AbstractYDao {
    private static final String TABLE_NAME = "recrawl_requests";
    private static final String ADD_DATE_INDEX = "add_date_index";
    private static final String ADD_DATE_STATE_INDEX = "add_date_state_index";

    @Autowired
    public AddUrlRequestsYDao() {
        super("/webmaster3/addurl", TABLE_NAME);
    }

    public void add(UrlForRecrawl url) {
        var st = insert(
                F.HOST_ID.value(url.getHostId()),
                F.URL_ID.value(url.getUrlId()),
                F.RELATIVE_URL.value(url.getRelativeUrl()),
                F.ADD_DATE.value(url.getAddDate()),
                F.PROCESS_DATE.value(url.getProcessedDate()),
                F.STATE.value(url.getState())
        );

        execute(st);
    }

    public void add(UrlForRecrawl url, String requestId) {
        var st = insert(
                F.HOST_ID.value(url.getHostId()),
                F.URL_ID.value(url.getUrlId()),
                F.RELATIVE_URL.value(url.getRelativeUrl()),
                F.ADD_DATE.value(url.getAddDate()),
                F.PROCESS_DATE.value(url.getProcessedDate()),
                F.STATE.value(url.getState()),
                F.REQUEST_ID.value(requestId)
        );

        execute(st);
    }


    public void addBatch(List<UrlForRecrawl> batch, String requestId) {
        if (batch.isEmpty()) {
            return;
        }

        execute(batchUpdate(createInsertValueMapper(requestId), batch));
    }

    public void updateBatch(List<UrlForRecrawl> batch) {
        if (batch.isEmpty()) {
            return;
        }

        execute(batchUpdate(UPDATE_VALUE_MAPPER, batch));
    }

    public void update(UrlForRecrawl url) {
        var st = upsert(
                F.HOST_ID.value(url.getHostId()),
                F.URL_ID.value(url.getUrlId()),
                F.RELATIVE_URL.value(url.getRelativeUrl()),
                F.ADD_DATE.value(url.getAddDate()),
                F.PROCESS_DATE.value(url.getProcessedDate()),
                F.STATE.value(url.getState())
        );

        execute(st);
    }

    @Nullable
    public UrlForRecrawl get(WebmasterHostId hostId, UUID urlId) {
        var sel = select(MAPPER).where(F.HOST_ID.eq(hostId)).and(F.URL_ID.eq(urlId));
        return queryOne(sel.getStatement(), MAPPER);
    }

    public List<UrlForRecrawl> get(Collection<Pair<WebmasterHostId, UUID>> keys) {
        // такой select работает, потому что url_id уникальны
        return select(MAPPER)
                .where(F.HOST_ID.in(keys.stream().map(Pair::getLeft).distinct().toList()))
                .and(F.URL_ID.in(keys.stream().map(Pair::getRight).toList()))
                .queryForList(Pair.of(
                        F.HOST_ID, UrlForRecrawl::getHostId),
                        Pair.of(F.URL_ID, UrlForRecrawl::getUrlId)
                );
    }

    public int count(WebmasterHostId hostId, DateTime fromDate, DateTime toDate) {
        var st = select().countAll().where(F.HOST_ID.eq(hostId));
        if (fromDate != null) {
            st.and(F.ADD_DATE.gte(fromDate));
        }

        if (toDate != null) {
            st.and(F.ADD_DATE.lte(toDate));
        }

        return queryOne(st.getStatement(), DataMapper.SINGLE_COLUMN_INT_MAPPER);
    }

    public List<UrlForRecrawl> list(WebmasterHostId hostId, DateTime fromDate, DateTime toDate, int skip, int limit) {
        var st = select(MAPPER).where(F.HOST_ID.eq(hostId));
        if (fromDate != null) {
            st.and(F.ADD_DATE.gte(fromDate));
        }

        if (toDate != null) {
            st.and(F.ADD_DATE.lte(toDate));
        }

        st.order(F.ADD_DATE.desc());
        st.limit(skip, limit);

        // используется для пагинации с маленьким размером страницы, так что в лимит YDB не упираемся
        return queryForList(st.getStatement(), MAPPER);
    }

    public List<Pair<WebmasterHostId, UUID>> listUnprocessed(DateTime fromDate, DateTime toDate) {
        return select(PK_MAPPER).secondaryIndex(ADD_DATE_STATE_INDEX)
                .where(F.ADD_DATE.gte(fromDate))
                .and(F.ADD_DATE.lte(toDate))
                .and(F.STATE.ne(RecrawlState.PROCESSED))
                .queryForList(
                        Pair.of(F.HOST_ID, Pair::getLeft),
                        Pair.of(F.URL_ID, Pair::getRight)
                );
    }

    public List<UrlForRecrawl> list(WebmasterHostId hostId, DateTime fromDate) {
        var st = select(MAPPER).where(F.HOST_ID.eq(hostId));
        if (fromDate != null) {
            st.and(F.ADD_DATE.gte(fromDate));
        }

        // сортируем по первичному ключу
        st.order(F.URL_ID.asc());

        return queryForList(st.getStatement(), MAPPER, list -> {
            UrlForRecrawl maxReq = list.get(list.size() - 1);
            return st.cont(F.URL_ID.gt(maxReq.getUrlId())).getStatement();
        });
    }

    public List<String> listRequestIds(WebmasterHostId hostId, DateTime fromDate) {
        var st = select(REQUEST_ID_MAPPER).where(F.HOST_ID.eq(hostId));
        if (fromDate != null) {
            st.and(F.ADD_DATE.gte(fromDate));
        }

        // используется для дедупликации возможных недавних ретраев балансера, поэтому лимит YDB нас здесь не сломает
        return queryForList(st.getStatement(), REQUEST_ID_MAPPER);
    }

    public void forEach(Consumer<Pair<WebmasterHostId, UUID>> consumer) {
        streamReader(PK_MAPPER, consumer);
    }

    public void batchDel(Collection<Pair<WebmasterHostId, UUID>> batch) {
        batchDelete(DELETE_VALUE_MAPPER, batch).execute();
    }

    private static final class F {
        public static final Field<UUID> URL_ID = Fields.uuidField("url_id");
        public static final Field<WebmasterHostId> HOST_ID = Fields.hostIdField("host_id");
        public static final Field<String> RELATIVE_URL = Fields.stringField("relative_url");
        public static final Field<DateTime> ADD_DATE = Fields.jodaDateTimeField("add_date");
        public static final Field<DateTime> PROCESS_DATE = Fields.jodaDateTimeField("process_date");
        public static final Field<RecrawlState> STATE = Fields.intEnumField("state", RecrawlState.R);
        public static final Field<String> REQUEST_ID = Fields.stringField("request_id");
    }

    private static final ValueDataMapper<UrlForRecrawl> UPDATE_VALUE_MAPPER = ValueDataMapper.create(
            Pair.of(F.HOST_ID, r -> F.HOST_ID.get(r.getHostId())),
            Pair.of(F.URL_ID, r -> F.URL_ID.get(r.getUrlId())),
            Pair.of(F.STATE, r -> F.STATE.get(r.getState())),
            Pair.of(F.PROCESS_DATE, r -> F.PROCESS_DATE.get(r.getProcessedDate()))
    );

    private static final ValueDataMapper<Pair<WebmasterHostId, UUID>> DELETE_VALUE_MAPPER = ValueDataMapper.create(
            Pair.of(F.HOST_ID, r -> F.HOST_ID.get(r.getLeft())),
            Pair.of(F.URL_ID, r -> F.URL_ID.get(r.getRight()))
    );


    private static final DataMapper<UrlForRecrawl> MAPPER = DataMapper.create(
            F.HOST_ID, F.URL_ID, F.RELATIVE_URL, F.ADD_DATE, F.PROCESS_DATE, F.STATE,
            ((hostId, urlId, relativeUrl, addDate, processDate, state) ->
                    new UrlForRecrawl(hostId, urlId, relativeUrl, addDate, processDate, state).withStaleCheck())
    );

    private static final DataMapper<Pair<WebmasterHostId, UUID>> PK_MAPPER = DataMapper.create(
            F.HOST_ID, F.URL_ID, Pair::of
    );

    private static ValueDataMapper<UrlForRecrawl> createInsertValueMapper(String requestId) {
        return ValueDataMapper.create(
                Pair.of(F.HOST_ID, r -> F.HOST_ID.get(r.getHostId())),
                Pair.of(F.URL_ID, r -> F.URL_ID.get(r.getUrlId())),
                Pair.of(F.RELATIVE_URL, r -> F.RELATIVE_URL.get(r.getRelativeUrl())),
                Pair.of(F.ADD_DATE, r -> F.ADD_DATE.get(r.getAddDate())),
                Pair.of(F.PROCESS_DATE, r -> F.PROCESS_DATE.get(r.getProcessedDate())),
                Pair.of(F.STATE, r -> F.STATE.get(r.getState())),
                Pair.of(F.REQUEST_ID, r -> F.REQUEST_ID.get(requestId)));
    }

    private static final DataMapper<String> REQUEST_ID_MAPPER = DataMapper.create(F.REQUEST_ID, r -> r);
}
