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

import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

import com.google.common.collect.Iterables;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.stereotype.Repository;

import ru.yandex.webmaster3.core.data.WebmasterHostId;
import ru.yandex.webmaster3.core.searchquery.QueryGroupId;
import ru.yandex.webmaster3.core.searchquery.QueryId;
import ru.yandex.webmaster3.core.util.functional.Bijection;
import ru.yandex.webmaster3.storage.util.ydb.AbstractYDao;
import ru.yandex.webmaster3.storage.util.ydb.query.Clause;
import ru.yandex.webmaster3.storage.util.ydb.query.Statement;
import ru.yandex.webmaster3.storage.util.ydb.querybuilder.QueryBuilder;
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.FieldMapper;
import ru.yandex.webmaster3.storage.util.ydb.querybuilder.typesafe.Fields;
import ru.yandex.webmaster3.storage.util.ydb.querybuilder.typesafe.ValueDataMapper;

/**
 * @author tsyplyaev
 */
@Repository
public class QueriesGroupsRelationYDao extends AbstractYDao {

    private static final String GROUP_ID_INDEX = "group_id_index";
    private static final ValueDataMapper<Pair<QueryGroupId, QueryId>> INSERT_VALUE_MAPPER = ValueDataMapper.create2(
            Pair.of(F.HOST_ID, pair -> pair.getLeft().getHostId()),
            Pair.of(F.QUERY_ID, Pair::getRight),
            Pair.of(F.GROUP_ID, pair -> pair.getLeft().getGroupId())
    );
    private static DataMapper<Pair<QueryGroupId, QueryId>> MAPPER = DataMapper.create(F.HOST_ID, F.GROUP_ID, F.QUERY_ID,
            (hostId, groupId, queryId) -> Pair.of(new QueryGroupId(hostId, groupId), queryId));

    public QueriesGroupsRelationYDao() {
        super(PREFIX_QUERIES, "queries_groups_relation");
    }

    public Set<QueryId> getQueries(QueryGroupId groupId) {
        return new HashSet<>(select(F.QUERY_ID).secondaryIndex(GROUP_ID_INDEX)
                .where(F.HOST_ID.eq(groupId.getHostId()))
                .and(F.GROUP_ID.eq(groupId.getGroupId()))
                .queryForList(FieldMapper.create(F.QUERY_ID, Function.identity())));
    }

    public Set<QueryId> getNonExistentQueries(WebmasterHostId hostId, Set<QueryId> queries) {
        Set<QueryId> result = new HashSet<>(queries);
        select(F.QUERY_ID).where(F.HOST_ID.eq(hostId)).and(F.QUERY_ID.in(queries)).queryForList().forEach(result::remove);
        return result;
    }

    public void batchInsert(Collection<Pair<QueryGroupId, QueryId>> batch) {
        batchInsert(INSERT_VALUE_MAPPER, batch).execute();
    }

    public void addQueriesToGroup(QueryGroupId groupId, Set<QueryId> queryIds) {
        batchInsert(INSERT_VALUE_MAPPER, queryIds.stream().map(qid -> Pair.of(groupId, qid)).collect(Collectors.toList())).execute();
    }

    public void removeQueryFromGroup(QueryGroupId groupId, QueryId queryId) {
        removeQueriesFromGroup(groupId, Collections.singleton(queryId));
    }

    public void removeQueriesFromGroup(QueryGroupId groupId, Set<QueryId> queryIds) {
        delete()
                .where(F.HOST_ID.eq(groupId.getHostId()))
                .and(F.QUERY_ID.in(queryIds))
                .and(F.GROUP_ID.eq(groupId.getGroupId()))
                .execute();
    }

    public void removeQuery(WebmasterHostId hostId, QueryId queryId) {
        removeQueries(hostId, Collections.singleton(queryId));
    }

    public void removeQueries(WebmasterHostId hostId, Set<QueryId> queryIds) {
        delete()
                .where(F.HOST_ID.eq(hostId))
                .and(F.QUERY_ID.in(queryIds))
                .execute();
    }

    public void removeGroup(QueryGroupId groupId) {
        // TODO прикрутить secondaryIndex к delete ?
        Set<QueryId> queries = getQueries(groupId);
        removeQueries(groupId.getHostId(), queries);
    }

    public int getGroupSize(QueryGroupId groupId) {
        return countAll().secondaryIndex(GROUP_ID_INDEX)
                .where(F.HOST_ID.eq(groupId.getHostId()))
                .and(F.GROUP_ID.eq(groupId.getGroupId()))
                .queryOne().intValue();
    }

    public boolean hasQueries(QueryGroupId groupId) {
        return getGroupSize(groupId) > 0;
    }

    public List<Pair<QueryGroupId, QueryId>> getRelations(List<WebmasterHostId> hostIds) {
        var st = select(MAPPER).where(F.HOST_ID.in(hostIds))
                .order(F.HOST_ID.asc())
                .order(F.QUERY_ID.asc())
                .order(F.GROUP_ID.asc());

        return queryForList(st, MAPPER, list -> {
            var last = Iterables.getLast(list);
            WebmasterHostId lastHostId = last.getLeft().getHostId();
            QueryId lastQueryId = last.getRight();
            UUID lastGroupId = last.getLeft().getGroupId();

            List<WebmasterHostId> contHostIds = hostIds.stream().filter(h -> h.compareTo(lastHostId) >= 0).toList();
            var newSt = select(MAPPER).where(F.HOST_ID.in(contHostIds))
                    .order(F.HOST_ID.asc())
                    .order(F.QUERY_ID.asc())
                    .order(F.GROUP_ID.asc());

            var or1 = F.HOST_ID.gt(lastHostId);

            var or2 = QueryBuilder.and(List.of(
                    F.HOST_ID.eq(lastHostId),
                    F.QUERY_ID.gt(lastQueryId)
            ));

            var or3 = QueryBuilder.and(List.of(
                    F.HOST_ID.eq(lastHostId),
                    F.QUERY_ID.eq(lastQueryId),
                    F.GROUP_ID.gt(lastGroupId)
            ));

            return newSt.cont(QueryBuilder.or(List.of(or1, or2, or3))).getStatement();
        });
    }

    private interface F {
        Field<WebmasterHostId> HOST_ID = Fields.hostIdField("host_id");
        Field<QueryId> QUERY_ID = Fields.longField("query_id").map(Bijection.create(QueryId::new, QueryId::getQueryId));
        Field<UUID> GROUP_ID = Fields.uuidField("group_id");
    }
}
