package ru.yandex.webmaster3.storage.sanctions;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.NavigableSet;
import java.util.Optional;
import java.util.TreeSet;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import com.google.common.primitives.UnsignedLong;
import org.joda.time.Instant;
import org.joda.time.LocalDate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ru.yandex.webmaster3.core.data.WebmasterUser;
import ru.yandex.webmaster3.core.sanctions.SanctionsRecheckRequested;
import ru.yandex.webmaster3.core.util.CityHash102;
import ru.yandex.webmaster3.core.util.IdUtils;
import ru.yandex.webmaster3.core.util.PageUtils;
import ru.yandex.webmaster3.storage.user.UserTakeoutDataProvider;
import ru.yandex.webmaster3.storage.util.clickhouse2.AbstractClickhouseDao;
import ru.yandex.webmaster3.storage.util.clickhouse2.AbstractSupportMergeDataFromTempTableCHDao;
import ru.yandex.webmaster3.storage.util.clickhouse2.ClickhouseException;
import ru.yandex.webmaster3.storage.util.clickhouse2.ClickhouseHost;
import ru.yandex.webmaster3.storage.util.clickhouse2.ClickhouseQueryContext;
import ru.yandex.webmaster3.storage.util.clickhouse2.ClickhouseServer;
import ru.yandex.webmaster3.storage.util.clickhouse2.SimpleByteArrayOutputStream;
import ru.yandex.webmaster3.storage.util.clickhouse2.TempDataChunksStoreUtil;
import ru.yandex.webmaster3.storage.util.clickhouse2.query.Format;
import ru.yandex.webmaster3.storage.util.clickhouse2.query.From;
import ru.yandex.webmaster3.storage.util.clickhouse2.query.GroupableLimitableOrderable;
import ru.yandex.webmaster3.storage.util.clickhouse2.query.OrderBy;
import ru.yandex.webmaster3.storage.util.clickhouse2.query.QueryBuilder;
import ru.yandex.webmaster3.storage.util.clickhouse2.query.Statement;
import ru.yandex.webmaster3.storage.util.clickhouse2.query.Where;
import ru.yandex.webmaster3.storage.util.clickhouse2.query.cases.Case;

/**
 * Created by ifilippov5 on 22.08.17.
 */
public class SanctionsCHDao extends AbstractClickhouseDao
        implements AbstractSupportMergeDataFromTempTableCHDao, UserTakeoutDataProvider {
    private static final Logger log = LoggerFactory.getLogger(SanctionsCHDao.class);

    private static final String DIST_TABLE = "sanctions";
    private static final String SHARD_TABLE = "sanctions_shard";
    private static final String TEMP_TABLE_PREFIX = "sanctions_temp_";
    private static final int TIME_ALIVE_OF_TEMP_TABLE_IN_MINUTES = 3;

    @Override
    public String getDbName() {
        return DB_WEBMASTER3;
    }

    @Override
    public String getTempTablePrefix() {
        return TEMP_TABLE_PREFIX;
    }

    @Override
    public int getMinutesIntervalSize() {
        return TIME_ALIVE_OF_TEMP_TABLE_IN_MINUTES;
    }

    @Override
    public void insertFromTempTable(String sourceTableName) {
        Statement st = QueryBuilder.insertInto(getDbName(), SHARD_TABLE)
                .fields(INSERT_FIELDS);

        From select = QueryBuilder.select(INSERT_FIELDS)
                .from(getDbName(), sourceTableName);

        insert(st.toString() + " " + select.toString());
    }

    private String getTempTableName() {
        return TEMP_TABLE_PREFIX + TempDataChunksStoreUtil.getCurrentMinutesInterval(TIME_ALIVE_OF_TEMP_TABLE_IN_MINUTES);
    }

    public void insertSanctions(List<SanctionsRecheckRequested> sanctions) throws ClickhouseException {
        Map<Integer, List<SanctionsRecheckRequested>> shard2Sanctions = sanctions
                .stream()
                .collect(Collectors.groupingBy(this::getShard));

        for (Map.Entry<Integer, List<SanctionsRecheckRequested>> entry : shard2Sanctions.entrySet()) {
            ClickhouseHost host = TempDataChunksStoreUtil.selectHostForShard(entry.getKey(), getClickhouseServer(), TIME_ALIVE_OF_TEMP_TABLE_IN_MINUTES);
            getClickhouseServer().executeWithFixedHost(host, () -> {
                        insertSanctions(entry.getValue(), SHARD_TABLE);
                        return null;
                    }
            );
        }
    }

    private void insertSanctions(List<SanctionsRecheckRequested> sanctions, String tableName) throws ClickhouseException {
        Statement st = QueryBuilder.insertInto(DB_WEBMASTER3, tableName)
                .fields(INSERT_FIELDS)
                .format(Format.Type.TAB_SEPARATED);

        SimpleByteArrayOutputStream bs = new SimpleByteArrayOutputStream();

        for (SanctionsRecheckRequested sanction : sanctions) {
            bs = packRowValues(bs,
                    toClickhouseDate(sanction.getDate()),
                    sanction.getDate().getMillis(),
                    sanction.getHostId().toString(),
                    zeroOnNull(sanction.getUserId()),
                    emptyOnNullAndRemoveNullItems(sanction.getSanctions())
            );
        }

        insert(st, bs.toInputStream());
    }

    private int getShard(SanctionsRecheckRequested sanction) {
        byte[] hostIdBytes = sanction.getHostId().toString().getBytes();
        return UnsignedLong.fromLongBits(CityHash102.cityHash64(hostIdBytes, 0, hostIdBytes.length)).mod(
                UnsignedLong.valueOf(getClickhouseServer().getShardsCount())).intValue();
    }

    public void insertSanctionsIntoTempTable(SanctionsRecheckRequested sanction) throws ClickhouseException {
        String createTableQuery = "CREATE TABLE IF NOT EXISTS " + getDbName() + "." + getTempTableName() +
                " ( " +
                "date Date, " +
                "timestamp UInt64, " +
                "host_id String, " +
                "user_id UInt64, " +
                "sanctions Array(String)" +
                " ) ENGINE = Log;";
        int shard = getShard(sanction);
        //100% не пуст
        List<ClickhouseHost> hosts = TempDataChunksStoreUtil.selectKHostsForShard(shard, getClickhouseServer(), getMinutesIntervalSize(), 2);
        if (hosts == null) {
            log.error("Failed to select host");
            return;
        }
        log.debug("Selected hosts: {}", hosts);
        hosts.forEach(host -> {
            ClickhouseQueryContext.Builder chContext = ClickhouseQueryContext.useDefaults().setHost(host);
            getClickhouseServer().execute(chContext, ClickhouseServer.QueryType.INSERT, createTableQuery,
                    Optional.empty(), Optional.empty());
            getClickhouseServer().executeWithFixedHost(host, () -> {
                        insertSanctions(Collections.singletonList(sanction), getTempTableName());
                        return null;
                    }
            );
        });
    }

    public List<SanctionsRecheckRequested> listSanctions(SanctionsFilter filter, PageUtils.Pager pager) throws ClickhouseException {
        List<SanctionsRecheckRequested> result = new ArrayList<>();
        forEach(filter, pager, result::add);
        return result;
    }

    private long getHitsAfterDate(LocalDate localDate, SanctionsFilter filter) throws ClickhouseException {
        From from = QueryBuilder.select("count() AS count")
                .from(DB_WEBMASTER3, DIST_TABLE);
        Where where = buildWhere(filter, from);
        where = where.and(QueryBuilder.gt(F.DATE, localDate));
        return queryOne(where, row -> row.getLongUnsafe("count")).orElse(0L);
    }

    private NavigableSet<LocalDate> getDatesForFilter(SanctionsFilter filter, PageUtils.Pager pager) throws ClickhouseException {
        From from = QueryBuilder.select(F.DATE).from(DB_WEBMASTER3, DIST_TABLE);
        Where where = buildWhere(filter, from);

        GroupableLimitableOrderable st = where.orderBy(F.DATE, OrderBy.Direction.DESC);
        if (pager != null) {
            st = st.limit(pager.toRangeStart(), pager.getPageSize());
        }

        return collectAll(st, Collectors.mapping(row -> getLocalDate(row, F.DATE), Collectors.toCollection(TreeSet::new)));
    }

    public void forEach(SanctionsFilter filter, PageUtils.Pager pager, Consumer<SanctionsRecheckRequested> consumer) throws ClickhouseException {
        From from = QueryBuilder.select(F.TIMESTAMP, F.HOST_ID, F.USER_ID, F.SANCTIONS)
                .from(DB_WEBMASTER3, DIST_TABLE);
        Where where = buildWhere(filter, from);
        int offsetCorrection = 0;
        if (filter.getHost() == null) {
            // При указании хоста все работает быстро.
            // Если хоста нет, применяем оптимизацию:
            // 1. Запрос только date вместо * работает быстрее в 10-50 раз
            // 2. Если в исходный запрос явно добавить фильтр по date - он ускорится на порядок
            // 3. При этом пострадает pager - поэтому посмотрим, сколько результатов отбросилось из-за
            //    фильтра по дате, и скорректируем offset
            NavigableSet<LocalDate> dates = getDatesForFilter(filter, pager);
            if (dates.isEmpty()) {
                return;
            }
            where = where.and(QueryBuilder.in(F.DATE, dates));
            // Должно влезать в int, пока в исходный pager не передадут long, а это пока невозможно
            offsetCorrection = Math.toIntExact(getHitsAfterDate(dates.last(), filter));
        }

        GroupableLimitableOrderable st = where.orderBy(F.TIMESTAMP, OrderBy.Direction.DESC);
        if (pager != null) {
            st = st.limit(pager.toRangeStart() - offsetCorrection, pager.getPageSize());
        }

        forEach(st, row -> {
            long ts = row.getLongUnsafe(F.TIMESTAMP);
            Instant timestamp = new Instant(ts);
            SanctionsRecheckRequested sanction = new SanctionsRecheckRequested(
                    timestamp,
                    getNullableLong(row, F.USER_ID),
                    IdUtils.stringToHostId(row.getString(F.HOST_ID)),
                    new HashSet<>(row.getStringList(F.SANCTIONS, StandardCharsets.UTF_8))
            );
            consumer.accept(sanction);
        });
    }

    public long countSanctions(SanctionsFilter filter) throws ClickhouseException {
        From from = QueryBuilder.select("count() AS count").from(DB_WEBMASTER3, DIST_TABLE);
        Statement st = buildWhere(filter, from);
        return queryOne(st, row -> row.getLongUnsafe("count")).orElse(0L);
    }

    private Where buildWhere(SanctionsFilter filter, From from) {
        Where where = from.where(new Case() {
            @Override
            public String toString() {
                return "1";
            }
        });
        if (filter.getHost() != null) {
            where = where.and(QueryBuilder.eq(F.HOST_ID, filter.getHost().toString()));
        }
        if (filter.getUserId() != null) {
            where = where.and(QueryBuilder.eq(F.USER_ID, filter.getUserId()));
        }
        if (filter.getDateFrom() != null) {
            where = where.and(QueryBuilder.gte(F.TIMESTAMP, filter.getDateFrom().getMillis()));
        }
        if (filter.getDateTo() != null) {
            where = where.and(QueryBuilder.lte(F.TIMESTAMP, filter.getDateTo().getMillis()));
        }
        if (filter.getSanctions() != null && !filter.getSanctions().isEmpty()) {
            where = where.and(new Case() {
                @Override
                public String toString() {
                    return "arrayCount(elem -> has(" +
                            AbstractClickhouseDao.toClickhouseStringArray(filter.getSanctions()) +
                            ",elem)," +
                            F.SANCTIONS +
                            ") > 0";
                }
            });
        }
        return where;
    }

    @Override
    public void deleteUserData(WebmasterUser user) {
        long userId = user.getUserId();
        String query = String.format("ALTER TABLE %s ON CLUSTER %s DELETE WHERE user_id=%s",
                getDbName() + "." + SHARD_TABLE, getClickhouseServer().getClusterId(), userId);
        getClickhouseServer().execute(ClickhouseQueryContext.useDefaults(), query);
    }

    private static final String[] INSERT_FIELDS = {
            F.DATE, F.TIMESTAMP, F.HOST_ID, F.USER_ID, F.SANCTIONS
    };

    private static class F {
        static final String DATE = "date";
        static final String TIMESTAMP = "timestamp";
        static final String HOST_ID = "host_id";
        static final String USER_ID = "user_id";
        static final String SANCTIONS = "sanctions";
    }
}
