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

import org.apache.commons.lang3.tuple.Pair;
import org.jetbrains.annotations.Nullable;
import org.joda.time.DateTime;
import org.springframework.stereotype.Repository;
import ru.yandex.webmaster3.core.metrika.counters.CounterBinding;
import ru.yandex.webmaster3.core.metrika.counters.CounterBindingStateEnum;
import ru.yandex.webmaster3.storage.util.ydb.AbstractYDao;
import ru.yandex.webmaster3.storage.util.ydb.querybuilder.AbstractInsert;
import ru.yandex.webmaster3.storage.util.ydb.querybuilder.QueryBuilder;
import ru.yandex.webmaster3.storage.util.ydb.querybuilder.Select;
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 java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;

/**
 * @author leonidrom
 */
@Repository
public class MetrikaCounterBindingStateYDao extends AbstractYDao {
    private static final String TABLE_NAME = "metrika_counter_state";

    public MetrikaCounterBindingStateYDao() {
        super(PREFIX_METRIKA, TABLE_NAME);
    }

    public void saveRecord(String domain, CounterBindingStateEnum state, long counterId,
                           String userLogin, DateTime updateDate,
                           @Nullable Long metrikaUserId, @Nullable Long webmasterUserId,
                           @Nullable String origin)  {
        AbstractInsert st = upsert(
                F.DOMAIN.value(domain),
                F.STATE.value(state),
                F.COUNTER_ID.value(counterId),
                F.LAST_USER_LOGIN.value(userLogin),
                F.LAST_UPDATE.value(updateDate)
        );

        if (metrikaUserId != null) {
            st = st.withValue(F.METRIKA_USER_ID.value(metrikaUserId));
        }

        if (webmasterUserId != null) {
            st = st.withValue(F.WEBMASTER_USER_ID.value(webmasterUserId));
        }

        if (origin != null) {
            st = st.withValue(F.ORIGIN.value(origin));
        }

        execute(st);
    }

    public void delete(String domain) {
        delete()
                .where(F.DOMAIN.eq(domain))
                .execute();
    }

    public CounterBindingStateEnum getState(String domain, long counterId) {
        return select(F.STATE)
                .where(F.DOMAIN.eq(domain))
                .and(F.COUNTER_ID.eq(counterId))
                .queryOne();
    }

    public Long getWebmasterUserId(String domain, long counterId) {
        return select(F.WEBMASTER_USER_ID)
                .where(F.DOMAIN.eq(domain))
                .and(F.COUNTER_ID.eq(counterId))
                .queryOne();
    }

    @Nullable
    public CounterBinding get(String domain, long counterId) {
        return select(MAPPER)
                .where(F.DOMAIN.eq(domain))
                .and(F.COUNTER_ID.eq(counterId))
                .queryOne();
    }

    public List<CounterBinding> getAllForDomain(String domain) {
        return select(MAPPER)
                .where(F.DOMAIN.eq(domain))
                .queryForList()
                .stream().sorted(Comparator.comparingLong(CounterBinding::getCounterId))
                .collect(Collectors.toList());
    }

    public List<CounterBinding> getAllForDomains(Collection<String> domains) {
        if (domains.isEmpty()) {
            return Collections.emptyList();
        }

        return select(MAPPER)
                .where(F.DOMAIN.in(domains))
                .queryForList();
    }

    public Map<Long, CounterBinding> getAllForDomainAsMap(String domain) {
        return getAllForDomain(domain).stream().collect(Collectors.toMap(CounterBinding::getCounterId, r -> r));
    }

    public void forEachLink(Consumer<CounterBinding> consumer, boolean includeFake) {
        streamReader(MAPPER, b -> {
            if (includeFake || !b.isFake()) {
                consumer.accept(b);
            }
        });
    }

    public void forEachDomain(Consumer<String> consumer) {
        streamReader(DOMAIN_MAPPER, consumer);
    }

    public void deleteForMetrikaUser(long userId) {
        var sel = select(PK_MAPPER)
                .secondaryIndex("metrika_user_id_index")
                .where(F.METRIKA_USER_ID.eq(userId))
                .getStatement();
        delete().on(sel).execute();
    }

    public void deleteForWebmasterUser(long userId) {
        var sel = select(PK_MAPPER).secondaryIndex(USER_ID_INDEX).where(F.WEBMASTER_USER_ID.eq(userId)).getStatement();
        delete().on(sel).execute();
    }

    private static final DataMapper<CounterBinding> MAPPER = DataMapper.create(
            F.DOMAIN, F.COUNTER_ID, F.STATE, F.WEBMASTER_USER_ID, F.LAST_USER_LOGIN, F.LAST_UPDATE, F.METRIKA_USER_ID, F.ORIGIN, CounterBinding::new
    );

    private static final DataMapper<String> DOMAIN_MAPPER = DataMapper.create(F.DOMAIN, d -> d);

    private static final DataMapper<Pair<String, Long>> PK_MAPPER = DataMapper.create(F.DOMAIN, F.COUNTER_ID, Pair::of);

    private static class F {
        static final Field<String> DOMAIN = Fields.stringField("domain");
        static final Field<CounterBindingStateEnum> STATE = Fields.intEnumField("state", CounterBindingStateEnum.R);
        static final Field<Long> COUNTER_ID = Fields.longField("counter_id");
        static final Field<Long> WEBMASTER_USER_ID = Fields.longField("user_id").makeOptional();
        static final Field<String> LAST_USER_LOGIN = Fields.stringField("user_login");
        static final Field<DateTime> LAST_UPDATE = Fields.jodaDateTimeField("last_update");
        static final Field<Long> METRIKA_USER_ID = Fields.longField("metrika_user_id").makeOptional();
        static final Field<String> ORIGIN = Fields.stringField("origin").makeOptional();
    }
}
