package ru.yandex.webmaster3.storage.searchurl.samples.dao;

import lombok.Builder;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Pair;
import org.jetbrains.annotations.Nullable;
import org.joda.time.DateTime;
import org.joda.time.Instant;
import org.joda.time.LocalDateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Repository;
import ru.yandex.webmaster3.core.data.WebmasterHostId;
import ru.yandex.webmaster3.core.util.TimeUtils;
import ru.yandex.webmaster3.storage.abt.AbtService;
import ru.yandex.webmaster3.storage.abt.model.Experiment;
import ru.yandex.webmaster3.storage.searchbase.SearchBaseUpdatesService;
import ru.yandex.webmaster3.storage.searchurl.samples.data.RawSearchUrlEventSample;
import ru.yandex.webmaster3.storage.searchurl.samples.data.SearchUrlEventType;
import ru.yandex.webmaster3.storage.util.clickhouse2.*;
import ru.yandex.webmaster3.storage.util.clickhouse2.condition.Condition;
import ru.yandex.webmaster3.storage.util.clickhouse2.query.*;
import ru.yandex.webmaster3.storage.util.clickhouse2.query.cases.Case;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;

/**
 * ishalaru
 * 16.03.2021
 **/
@Repository
@Slf4j
@RequiredArgsConstructor(onConstructor_ = {@Autowired})
public class SearchUrlFreshUrlSampleCHDao extends AbstractClickhouseDao {
    private static final DateTimeFormatter DATE_TIME_FORMAT = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss");
    public static final String DATABASE = DB_WEBMASTER3_SEARCHURLS;
    public static final String TABLE_NAME = "fresh_url_event_distributed";
    private static final String DATABASE_TABLE_NAME = DATABASE + "." + TABLE_NAME;
    @Qualifier("legacyMdbClickhouseServer")
    private final MdbClickhouseServer clickhouseServer;
    private final SearchBaseUpdatesService searchBaseUpdatesService;
    private final AbtService abtService;


    public long getExcludedSamplesCount(WebmasterHostId hostId, Condition filters) {
        return getSamplesCount(hostId, filters, QueryBuilder.eq(F.EVENT_TYPE, SearchUrlEventType.GONE.value()));
    }

    public long getNewSamplesCount(WebmasterHostId hostId, Condition filters) {
        return getSamplesCount(hostId, filters, QueryBuilder.eq(F.EVENT_TYPE, SearchUrlEventType.NEW.value()));
    }

    public long getSamplesCount(WebmasterHostId hostId, Condition filters) {
        return getSamplesCount(hostId, filters, null);
    }


    private String tableSubQuery(WebmasterHostId hostId) {
        if (abtService.isInExperiment(hostId, Experiment.SEARCH_URL_FRESH_GROUPING)) {
            List<String> selectFields = List.of(F.HOST_ID,
                    F.PATH,
                    "any(" + F.EVENT_TYPE + ") as " + F.EVENT_TYPE,
                    "max(" + F.ADD_TIME + ") as " + F.ADD_TIME,
                    "max(" + F.LAST_ACCESS + ") as " + F.LAST_ACCESS,
                    "any(" + F.TITLE + ") as " + F.TITLE,
                    "any(" + F.HTTP_CODE + ") as " + F.HTTP_CODE,
                    "any(" + F.EX_REL_CANONICAL_TARGET + ") as " + F.EX_REL_CANONICAL_TARGET,
                    "any(" + F.IS_FROM_SITEMAP + ") as " + F.IS_FROM_SITEMAP,
                    "any(" + F.IS_TURBO + ") as " + F.IS_TURBO,
                    "max(" + F.BASE_DATE + ") as " + F.BASE_DATE);
            String[] groupFields = {F.HOST_ID,
                    F.PATH};
            return "( " + QueryBuilder.select(selectFields).from(DATABASE_TABLE_NAME).where(QueryBuilder.eq(F.HOST_ID, hostId)).groupBy(groupFields).toString() + " ) ";

        }
        return DATABASE_TABLE_NAME;

    }

    private long getSamplesCount(WebmasterHostId hostId, Condition filters, Case extraFilters) {

        Where st = QueryBuilder.select()
                .countAll()
                .from(tableSubQuery(hostId))
                .where(QueryBuilder.eq(F.HOST_ID, hostId));
        if (extraFilters != null) {
            st = st.and(extraFilters);
        }
        final Instant baseCollectionDate = searchBaseUpdatesService.getSearchBaseUpdates().getCurrentBase().getBaseCollectionDate();
        st = st.and(QueryBuilder.gt(F.BASE_DATE, baseCollectionDate.toDateTime().getMillis() / 1000));
        st = filter(st, filters);
        ClickhouseQueryContext.Builder context = ClickhouseQueryContext.useDefaults();
        return clickhouseServer.queryOne(context, st.toString(),
                r -> r.getLongUnsafe(0)).orElse(0L);
    }

    private static final Function<CHRow, Pair<String, Long>> COUNT_MAPPER = chRow ->
            Pair.of(chRow.getString("la"), chRow.getLongUnsafe("cnt"));

    public List<Pair<String, Long>> countGropingByLastAccess(WebmasterHostId hostId, Condition filters) {
        String groupField = "toDate(FROM_UNIXTIME(last_access))";
        String selectField = "toDate(FROM_UNIXTIME(last_access)) as la,count() as cnt";
        Where st = QueryBuilder.select(selectField)
                .from(tableSubQuery(hostId))
                .where(QueryBuilder.eq(F.HOST_ID, hostId));

        final Instant baseCollectionDate = searchBaseUpdatesService.getSearchBaseUpdates().getCurrentBase().getBaseCollectionDate();
        st = st.and(QueryBuilder.gt(F.BASE_DATE, baseCollectionDate.toDateTime().getMillis() / 1000));
        st = filter(st, filters);
        ClickhouseQueryContext.Builder context = ClickhouseQueryContext.useDefaults();
        return clickhouseServer.queryAll(context, st.groupBy(groupField).toString(), COUNT_MAPPER);
    }

    public List<Pair<String, Long>> countGroupingByBaseDate(WebmasterHostId hostId, Condition filters) {
        String groupField = "toDate(FROM_UNIXTIME(base_date))";
        String selectField = "toDate(FROM_UNIXTIME(base_date)) as la,count() as cnt";
        Where st = QueryBuilder.select(selectField)
                .from(tableSubQuery(hostId))
                .where(QueryBuilder.eq(F.HOST_ID, hostId));

        final Instant baseCollectionDate = searchBaseUpdatesService.getSearchBaseUpdates().getCurrentBase().getBaseCollectionDate();
        st = st.and(QueryBuilder.gt(F.BASE_DATE, baseCollectionDate.toDateTime().getMillis() / 1000));
        st = filter(st, filters);
        ClickhouseQueryContext.Builder context = ClickhouseQueryContext.useDefaults();
        return clickhouseServer.queryAll(context, st.groupBy(groupField).toString(), COUNT_MAPPER);
    }


    public List<Row> getExcludedSamples(WebmasterHostId hostId, Condition filters, long limitFrom, long limitSize) {
        return getSamples(hostId, filters, limitFrom, limitSize, QueryBuilder.eq(F.EVENT_TYPE, SearchUrlEventType.GONE.value()));
    }

    public List<Row> getNewSamples(WebmasterHostId hostId, Condition filters, long limitFrom, long limitSize) {
        return getSamples(hostId, filters, limitFrom, limitSize, QueryBuilder.eq(F.EVENT_TYPE, SearchUrlEventType.NEW.value()));
    }

    public List<Row> getSamples(WebmasterHostId hostId, Condition filters, long limitFrom, long limitSize) {
        return getSamples(hostId, filters, limitFrom, limitSize, null);
    }

    public List<Row> getSamplesOrderBySearchBaseDate(WebmasterHostId hostId, Condition filters, long limitFrom, long limitSize) {
        Where st = getSampleWhereStatement(hostId);
        st = st.and(QueryBuilder.eq(F.EVENT_TYPE, SearchUrlEventType.NEW.value()));
        final Instant baseCollectionDate = searchBaseUpdatesService.getSearchBaseUpdates().getCurrentBase().getBaseCollectionDate();
        st = st.and(QueryBuilder.gt(F.BASE_DATE, baseCollectionDate.toDateTime().getMillis() / 1000));

        st = filter(st, filters);

        GroupableLimitableOrderable fst = st
                .orderBy(Arrays.asList(
                        Pair.of(F.BASE_DATE, OrderBy.Direction.DESC),
                        Pair.of(F.PATH, OrderBy.Direction.DESC)
                ))
                .limit((int) limitFrom, (int) limitSize);
        ClickhouseQueryContext.Builder context = ClickhouseQueryContext.useDefaults();
        return clickhouseServer.queryAll(context, fst.toString(), MAPPER);
    }


    private List<Row> getSamples(WebmasterHostId hostId, Condition filters, long limitFrom, long limitSize, Case extraCondition) {
        Where st = getSampleWhereStatement(hostId);
        if (extraCondition != null) {
            st.and(extraCondition);
        }
        final Instant baseCollectionDate = searchBaseUpdatesService.getSearchBaseUpdates().getCurrentBase().getBaseCollectionDate();
        st = st.and(QueryBuilder.gt(F.BASE_DATE, baseCollectionDate.toDateTime().getMillis() / 1000));

        st = filter(st, filters);

        GroupableLimitableOrderable fst = st
                .orderBy(Arrays.asList(
                        Pair.of(F.LAST_ACCESS, OrderBy.Direction.DESC),
                        Pair.of(F.PATH, OrderBy.Direction.DESC)
                ))
                .limit((int) limitFrom, (int) limitSize);
        ClickhouseQueryContext.Builder context = ClickhouseQueryContext.useDefaults();
        return clickhouseServer.queryAll(context, fst.toString(), MAPPER);
    }

    public Optional<Row> getExcludeSample(WebmasterHostId hostId, String path) {
        return getSample(hostId, path, QueryBuilder.eq(F.EVENT_TYPE, SearchUrlEventType.GONE.value()));
    }

    public Optional<Row> getNewSample(WebmasterHostId hostId, String path) {
        return getSample(hostId, path, QueryBuilder.eq(F.EVENT_TYPE, SearchUrlEventType.NEW.value()));
    }

    private Optional<Row> getSample(WebmasterHostId hostId, String path, Case externalCase) {

        Where st = getSampleWhereStatement(hostId);
        st = st.and(QueryBuilder.eq(F.PATH, path));
        if (externalCase != null) {
            st = st.and(externalCase);
        }
        final Instant baseCollectionDate = searchBaseUpdatesService.getSearchBaseUpdates().getCurrentBase().getBaseCollectionDate();
        st = st.and(QueryBuilder.gt(F.BASE_DATE, baseCollectionDate.toDateTime().getMillis() / 1000));

        ClickhouseQueryContext.Builder context = ClickhouseQueryContext.useDefaults();
        return clickhouseServer.queryOne(context, st.toString(), MAPPER);
    }


    public List<RawSearchUrlEventSample> getSamplesLight(WebmasterHostId hostId, Condition filters, long limitFrom, long limitSize) throws ClickhouseException {
        Where st = QueryBuilder.select(
                F.LAST_ACCESS,
                F.PATH,
                F.EVENT_TYPE,
                F.BASE_DATE
        ).from(tableSubQuery(hostId))
                .where(QueryBuilder.eq(F.HOST_ID, hostId));

        final Instant baseCollectionDate = searchBaseUpdatesService.getSearchBaseUpdates().getCurrentBase().getBaseCollectionDate();
        st = st.and(QueryBuilder.gt(F.BASE_DATE, baseCollectionDate.toDateTime().getMillis() / 1000));

        st = filter(st, filters);
        GroupableLimitableOrderable fst = st
                .orderBy(Arrays.asList(
                        Pair.of(F.BASE_DATE, OrderBy.Direction.DESC),
                        Pair.of(F.PATH, OrderBy.Direction.ASC)
                ))
                .limit((int) limitFrom, (int) limitSize);
        ClickhouseQueryContext.Builder context = ClickhouseQueryContext.useDefaults();
        return clickhouseServer.queryAll(context,
                fst.toString(), chRow -> new RawSearchUrlEventSample(
                        chRow.getString(F.PATH),
                        "",
                        new DateTime(chRow.getLong(F.LAST_ACCESS) * 1000L),
                        new DateTime(chRow.getLong(F.BASE_DATE) * 1000L),
                        SearchUrlEventType.R.fromValue(chRow.getInt(F.EVENT_TYPE)),
                        null
                ));
    }

    @Nullable
    public DateTime getMaxBaseDate(WebmasterHostId hostId) {
        var st = QueryBuilder.select(QueryBuilder.as(QueryBuilder.max(F.BASE_DATE), F.BASE_DATE).toString())
                .from(DATABASE_TABLE_NAME).where(QueryBuilder.eq(F.HOST_ID, hostId));

        ClickhouseQueryContext.Builder context = ClickhouseQueryContext.useDefaults();
        Optional<DateTime> maxBaseDateOpt = clickhouseServer.queryOne(context, st.toString(),
                chRow -> new DateTime(chRow.getLong(F.BASE_DATE) * 1000L));

        return maxBaseDateOpt.orElse(null);
    }

    private Where getSampleWhereStatement(WebmasterHostId hostId) {
        return QueryBuilder.select(
                F.HOST_ID,
                F.PATH,
                F.EVENT_TYPE,
                F.ADD_TIME,
                F.LAST_ACCESS,
                F.TITLE,
                F.HTTP_CODE,
                F.EX_REL_CANONICAL_TARGET,
                F.BASE_DATE

        ).from(tableSubQuery(hostId))
                .where(QueryBuilder.eq(F.HOST_ID, hostId));

    }

    private Where filter(Where st, Condition filters) {
        if (filters == null) {
            return st;
        }
        String filterStr = filters.toQuery();
        return st.and(new Case() {
            @Override
            public String toString() {
                return filterStr;
            }
        });
    }


    public void addRecords(List<Row> messages) throws ClickhouseException {
        Statement st = QueryBuilder.insertInto(DATABASE, TABLE_NAME)
                .fields(
                        F.HOST_ID,
                        F.PATH,
                        F.EVENT_TYPE,
                        F.ADD_TIME,
                        F.LAST_ACCESS,
                        F.TITLE,
                        F.HTTP_CODE,
                        F.EX_REL_CANONICAL_TARGET,
                        F.IS_FROM_SITEMAP,
                        F.IS_TURBO,
                        F.BASE_DATE,
                        F.EX_VALID_FROM_METRIKA_LAST_ACCESS,
                        F.EX_VALID_FROM_INDEXNOW_LAST_ACCESS
                ).format(Format.Type.TAB_SEPARATED);

        SimpleByteArrayOutputStream bs = new SimpleByteArrayOutputStream();
        for (Row row : messages) {
            bs = packRowValues(bs,
                    row.hostId,
                    row.path,
                    row.actionType,
                    DATE_TIME_FORMAT.print(row.addTime),
                    row.lastAccess.toDateTime(TimeUtils.EUROPE_MOSCOW_ZONE).getMillis() / 1000,
                    row.title,
                    row.httpCode,
                    row.relCanonicalTarget,
                    0,
                    0,
                    row.insetTime.toDateTime(TimeUtils.EUROPE_MOSCOW_ZONE).getMillis() / 1000,
                    row.validFromMetrikaLastAccess,
                    row.validFromIndexNowLastAccess
            );
        }
        if (bs.size() == 0L) {
            return;
        }

        ClickhouseQueryContext.Builder context = ClickhouseQueryContext.useDefaults();
        clickhouseServer.insert(context, st.toString(), bs.toInputStream());

    }

    private static final Function<CHRow, Row> MAPPER = chRow ->
            new Row(chRow.getHostId(F.HOST_ID),
                    chRow.getString(F.PATH),
                    chRow.getInt(F.EVENT_TYPE),
                    LocalDateTime.parse(chRow.getString(F.ADD_TIME), DATE_TIME_FORMAT),
                    new LocalDateTime(chRow.getLong(F.LAST_ACCESS) * 1000, TimeUtils.EUROPE_MOSCOW_ZONE),
                    chRow.getString(F.TITLE),
                    (int) chRow.getLong(F.HTTP_CODE),
                    chRow.getString(F.EX_REL_CANONICAL_TARGET),
                    new LocalDateTime(chRow.getLong(F.BASE_DATE) * 1000, TimeUtils.EUROPE_MOSCOW_ZONE),
                    0L, // getNullableDate(chRow, F.EX_VALID_FROM_METRIKA_LAST_ACCESS),
                    0L //getNullableDate(chRow, F.EX_VALID_FROM_INDEXNOW_LAST_ACCESS)
            );

    @Value
    @Builder
    @ToString
    public static class Row {
        WebmasterHostId hostId;
        String path;
        int actionType;
        LocalDateTime addTime;
        LocalDateTime lastAccess;
        String title;
        int httpCode;
        String relCanonicalTarget;
        LocalDateTime insetTime;
        long validFromMetrikaLastAccess;
        long validFromIndexNowLastAccess;
    }

    public static class F {
        static final String HOST_ID = "host_id";
        static final String PATH = "path";
        static final String EVENT_TYPE = "event_type";
        static final String ADD_TIME = "add_time";
        static final String LAST_ACCESS = "last_access";
        static final String TITLE = "title";
        static final String HTTP_CODE = "http_code";
        static final String EX_REL_CANONICAL_TARGET = "ex_rel_canonical_target";
        static final String BASE_DATE = "base_date";
        static final String IS_TURBO = "ex_is_turbo";
        static final String IS_FROM_SITEMAP = "ex_is_from_sitemap";
        static final String EX_VALID_FROM_METRIKA_LAST_ACCESS = "ex_valid_from_metrika_last_access";
        static final String EX_VALID_FROM_INDEXNOW_LAST_ACCESS = "ex_valid_from_indexnow_last_access";
    }
}
