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

import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import ru.yandex.webmaster3.core.WebmasterException;
import ru.yandex.webmaster3.core.data.WebmasterHostId;
import ru.yandex.webmaster3.core.util.PageUtils;
import ru.yandex.webmaster3.storage.user.MessageContentConverter;
import ru.yandex.webmaster3.storage.user.message.MessageId;
import ru.yandex.webmaster3.storage.user.message.MessageTypeEnum;
import ru.yandex.webmaster3.storage.user.message.MessagesFilter;
import ru.yandex.webmaster3.storage.user.message.UserMessageInfo;
import ru.yandex.webmaster3.storage.user.message.content.MessageContent;
import ru.yandex.webmaster3.storage.util.clickhouse2.*;
import ru.yandex.webmaster3.storage.util.clickhouse2.query.*;

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


/**
 * Created by kravchenko99 on 16.10.19.
 */
@Slf4j
public class UserMessages4CHDao extends AbstractClickhouseDao {
    private static final String UPDATES_ALIAS = "upd_alias";
    private static final String OBJECTS_ALIAS = "obj_alias";

    @Getter
    @Setter
    private UserMessagesObjectsCHDao userMessagesObjectsCHDao;
    @Getter
    @Setter
    private UserMessagesUpdatesCHDao userMessagesUpdatesCHDao;

    public Map<Integer, List<String>> getShards() {
        Map<Integer, List<String>> res = new HashMap<>();
        for (var h : getClickhouseServer().getHosts()) {
            final int shard = h.getShard();
            if (!res.containsKey(shard)) {
                res.put(shard, new ArrayList<>());
            }

            res.get(shard).add(h.getHostURI().getHost());
        }

        return res;
    }

    public UserMessageInfo getMessage(UUID userUUID, MessageId messageId) {
        Statement updates = userMessagesUpdatesCHDao.createLastMessageUpdateStatement(userUUID, messageId);
        Statement objects = userMessagesObjectsCHDao.createRowByMessageIdStatement(userUUID, messageId);

        Join lastRecordQuery = QueryBuilder.select(LAST_RECORD)
                .from(updates)
                .join(Join.Strictness.ANY, Join.Type.INNER, objects,
                        String.join(", ", PRIMARY_KEY), UPDATES_ALIAS, OBJECTS_ALIAS);

        return getClickhouseServer().queryOne(getCHContext(userUUID), lastRecordQuery, MAPPER).flatMap(o -> o).orElse(null);
    }


    public long count(UUID userUUID, Set<MessageId> excludeIds, MessagesFilter filter) {

        Statement updates = userMessagesUpdatesCHDao.createCountStatement(userUUID, excludeIds, filter);
        Statement objects = userMessagesObjectsCHDao.createCountStatement(userUUID);

        Join join = QueryBuilder.select("0").from(updates).join(Join.Strictness.ANY, Join.Type.INNER, objects,
                String.join(", ", PRIMARY_KEY_WITHOUT_USER_UUID), OBJECTS_ALIAS, UPDATES_ALIAS);

        From count = QueryBuilder.select().countAll().from(join);

        return getClickhouseServer().queryOne(
                getCHContext(userUUID),
                count,
                r -> r.getLongUnsafe("count()")
        ).orElse(0L);
    }

    public List<UserMessageInfo> listMessages(UUID userUUID, Set<MessageId> deletedIds, MessagesFilter filter) {
        return listMessages(userUUID, deletedIds, filter, null);
    }

    public List<UserMessageInfo> listMessages(UUID userUUID,
                                              Set<MessageId> deletedIds,
                                              MessagesFilter filter,
                                              @Nullable PageUtils.Pager pager) {
        Statement updates = userMessagesUpdatesCHDao.createListMessagesStatement(userUUID, deletedIds, filter, pager);
        Statement objects = userMessagesObjectsCHDao.createRowsByUserIdStatement(userUUID);

        Join request = QueryBuilder.select(LAST_RECORD)
                .from(objects)
                .join(Join.Strictness.ANY, Join.Type.INNER, updates,
                        String.join(", ", PRIMARY_KEY), OBJECTS_ALIAS, UPDATES_ALIAS);

        OrderBy orderBy = request.orderBy(Arrays.asList(
                Pair.of(F.LAST_DATE_TIME, OrderBy.Direction.DESC),
                Pair.of(F.EVENT_UUID, OrderBy.Direction.DESC),
                Pair.of(F.HASH, OrderBy.Direction.DESC)
        ));

        return getClickhouseServer().queryAll(getCHContext(userUUID), orderBy, MAPPER).stream()
                .flatMap(Optional::stream)
                .collect(Collectors.toList());
    }


    public void sendMessage(UserMessageInfo... message) {
        sendMessages(Arrays.asList(message));
    }

    private void sendMessages(List<UserMessageInfo> userMessageInfos) {
        insertMessagesIntoTempTable(userMessageInfos, null);
    }

    public void insertMessagesIntoTempTable(List<UserMessageInfo> messages, Long version) {
        if (userMessagesObjectsCHDao.insertMessagesIntoTempTable(messages, version)) {
            if (!userMessagesUpdatesCHDao.insertMessagesIntoTempTable(messages, version)) {
                log.error("Insert message updates to temp table ended with error");
            }
        } else {
            log.error("Insert message objects to temp table ended with error");
        }
    }

    public void updateMessages(List<UserMessageInfo> messages, Long version) {
        userMessagesUpdatesCHDao.insertMessagesIntoTempTable(messages, version);
    }

    public void deleteAllMessagesForUser(UUID userUUID, long version) {
        userMessagesUpdatesCHDao.deleteAllForUser(userUUID, version);
    }

    public void markReadAllMessagesForUser(UUID userUUID, boolean isRead, long version) {
        userMessagesUpdatesCHDao.markReadAllForUser(userUUID, isRead, version);
    }

    private static final Function<CHRow, Optional<UserMessageInfo>> MAPPER = (CHRow r) -> {
        try {
            MessageTypeEnum messageType = MessageTypeEnum.R.fromValueOrNull(r.getInt(F.TYPE));
            if (messageType == null) {
                return Optional.empty(); // неизвестный тип сообщения (для тестинга)
            }
            return Optional.of(UserMessageInfo.createWithMessageId(
                    new MessageId(r.getStringUUID(F.EVENT_UUID), r.getLong(F.HASH)),
                    r.getStringUUID(F.USER_UUID),
                    getNullableHostId(r, F.LAST_HOST_ID),
                    getBoolean(r, F.LAST_IS_READ),
                    getBoolean(r, F.LAST_IS_CRITICAL),
                    r.getDateTime(F.LAST_DATE_TIME),
                    MessageContentConverter.deserialize(messageType, r.getString(F.MESSAGE))
            ));
        } catch (WebmasterException e) {
            // Выстреливает для сообщения с неизвестным типом (такое бывает в тестинге)
            return Optional.empty();
        }
    };

    private static boolean getBoolean(CHRow row, String field) {
        return row.getInt(field) > 0;
    }

    private ClickhouseQueryContext.Builder getCHContext(UUID userUUID) {
        return ClickhouseQueryContext.useDefaults().setHost(getClickhouseServer().pickAliveHostOrFail(getShard(userUUID)));
    }

    private int getShard(UUID userUUID) {
        return Math.abs(userUUID.hashCode() % getClickhouseServer().getShardsCount());
    }


    /////////////////IMMUTABLE TABLE/////////////////
    private static class UserMessagesObjectsCHDao extends AbstractUserMessage4Dao implements AbstractSupportMergeDataFromTempTableCHDao {
        private static final String DB_NAME = DB_WEBMASTER3;
        private static final String SHARD_TABLE = "user_messages_objects2";
        private static final String TEMP_TABLE_PREFIX = "user_messages_objects2_temp_";
        private static final int TIME_ALIVE_OF_TEMP_TABLE_IN_MINUTES = 5;

        public Statement createRowsByUserIdStatement(UUID userUUID) {
            return createRowsByUserIdWithFieldsStatement(userUUID, UNIQUE_RECORD)
                    .groupBy(PRIMARY_KEY);
        }

        public Statement createRowByMessageIdStatement(UUID userUUID, MessageId messageId) {
            return createRowsByUserIdWithFieldsStatement(userUUID, RECORD)
                    .and(QueryBuilder.eq(F.EVENT_UUID, messageId.getEventUuid()))
                    .and(QueryBuilder.eq(F.HASH, messageId.getHash()));
        }

        public Where createRowsByUserIdWithFieldsStatement(UUID userUUID, String[] fields) {
            return QueryBuilder.select(fields).from(DB_NAME, SHARD_TABLE)
                    .prewhere(QueryBuilder.eq(F.USER_UUID, userUUID));
        }

        public Statement createCountStatement(UUID userUUID) {
            return QueryBuilder.select(PRIMARY_KEY_WITHOUT_USER_UUID)
                    .from(DB_NAME, SHARD_TABLE)
                    .prewhere(QueryBuilder.eq(F.USER_UUID, userUUID))
                    .groupBy(PRIMARY_KEY_WITHOUT_USER_UUID);
        }

        @Override
        protected SimpleByteArrayOutputStream packRowValuesWithVersion(SimpleByteArrayOutputStream bs,
                                                                       UserMessageInfo message,
                                                                       Long version) {
            return packRowValues(bs,
                    toClickhouseDate(message.getTime()),
                    message.getUserUUID().toString(),
                    Optional.ofNullable(message.getId()).map(MessageId::getEventUuid).map(UUID::toString).orElse(""),
                    Optional.ofNullable(message.getId()).map(MessageId::getHash).orElse(0L),
                    serialize(message.getContent()),
                    Optional.ofNullable(message.getContent()).map(MessageContent::getType).map(MessageTypeEnum::value).orElse(0)
            );
        }

        private static String serialize(MessageContent messageInfo) {
            return MessageContentConverter.serialize(messageInfo);
        }

        @Override
        protected String getTempTableName() {
            return TEMP_TABLE_PREFIX + TempDataChunksStoreUtil.getCurrentMinutesInterval(TIME_ALIVE_OF_TEMP_TABLE_IN_MINUTES);
        }

        @Override
        protected String getCreateTableQuery(String tableName) {
            return "CREATE TABLE IF NOT EXISTS " + DB_NAME + "." + tableName +
                    " ( " +
                    "date Date, " +
                    "user_uuid String, " +
                    "event_uuid String, " +
                    "hash Int64, " +
                    "message String, " +
                    "type Int16 " +
                    " ) ENGINE = Log;";
        }

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

        @Override
        protected String getShardTable() {
            return SHARD_TABLE;
        }

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

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

        @Override
        protected String[] getInsertFields() {
            return INSERT_FIELDS;
        }

        private static final String[] INSERT_FIELDS = {
                F.DATE,
                F.USER_UUID,
                F.EVENT_UUID,
                F.HASH,
                F.MESSAGE,
                F.TYPE,
        };

        private static final String[] RECORD = {
                F.USER_UUID,
                F.EVENT_UUID,
                F.HASH,
                F.TYPE,
                F.MESSAGE,
        };

        private static final String[] UNIQUE_RECORD = {
                F.USER_UUID,
                F.EVENT_UUID,
                F.HASH,
                QueryBuilder.as(QueryBuilder.any(F.TYPE), F.TYPE).toString(),
                QueryBuilder.as(QueryBuilder.any(F.MESSAGE), F.MESSAGE).toString(),
        };
    }


    /////////////////MUTABLE TABLE/////////////////
    private static class UserMessagesUpdatesCHDao extends AbstractUserMessage4Dao implements AbstractSupportMergeDataFromTempTableCHDao {

        private static final String DB_NAME = DB_WEBMASTER3;
        private static final String SHARD_TABLE = "user_messages_updates2";
        private static final String TEMP_TABLE_PREFIX = "user_messages_updates2_temp_";
        private static final int TIME_ALIVE_OF_TEMP_TABLE_IN_MINUTES = 5;

        public Statement createListMessagesStatement(UUID userUUID,
                                                     Set<MessageId> deletedIds,
                                                     MessagesFilter filter,
                                                     @Nullable PageUtils.Pager pager) {
            Clause having = QueryBuilder.select(LAST_RECORD)
                    .from(DB_NAME, SHARD_TABLE)
                    .prewhere(QueryBuilder.eq(F.USER_UUID, userUUID))
                    .groupBy(PRIMARY_KEY)
                    .having(QueryBuilder.eq(F.LAST_DELETED, booleanToInt(false)));
            having = buildWhereForFilter(having, filter).and(messageIdNotIn(deletedIds));

            OrderBy orderBy = having.orderBy(Arrays.asList(
                    Pair.of(F.LAST_DATE_TIME, OrderBy.Direction.DESC),
                    Pair.of(F.EVENT_UUID, OrderBy.Direction.DESC),
                    Pair.of(F.HASH, OrderBy.Direction.DESC)
            ));

            return pager != null ? orderBy.limit(pager.toRangeStart(), pager.getPageSize()) : orderBy;
        }


        public Statement createCountStatement(UUID userUUID, Set<MessageId> excludeIds, MessagesFilter filter) {
            Having having = QueryBuilder.select(PRIMARY_KEY_WITHOUT_USER_UUID)
                    .from(DB_NAME, SHARD_TABLE)
                    .prewhere(QueryBuilder.eq(F.USER_UUID, userUUID))
                    .groupBy(PRIMARY_KEY_WITHOUT_USER_UUID)
                    .having(QueryBuilder.eq(QueryBuilder.argMax(F.DELETED, F.VERSION).toString(), booleanToInt(false)));
            return buildWhereForFilter2(having, filter).and(messageIdNotIn(excludeIds));
        }

        public Statement createLastMessageUpdateStatement(UUID userUUID, MessageId id) {
            return QueryBuilder.select(LAST_RECORD)
                    .from(DB_NAME, SHARD_TABLE)
                    .prewhere(QueryBuilder.eq(F.USER_UUID, userUUID))
                    .and(QueryBuilder.eq(F.EVENT_UUID, id.getEventUuid()))
                    .and(QueryBuilder.eq(F.HASH, id.getHash()))
                    .groupBy(PRIMARY_KEY)
                    .having(QueryBuilder.eq(F.LAST_DELETED, booleanToInt(false)));
        }

        public void deleteAllForUser(UUID userUUID, long version) {
            String tableName = DB_NAME + "." + SHARD_TABLE;
            int deletedVal = booleanToInt(true);

            String query = String.format(
                    "INSERT INTO %s(date, user_uuid, event_uuid, hash, version, deleted)" +
                            " SELECT argMax(date, version) AS last_date, user_uuid, event_uuid, hash, %s, %s FROM %s " +
                            "PREWHERE user_uuid='%s' GROUP BY user_uuid, event_uuid, hash HAVING argMax(deleted, version)=0",
                    tableName, version, deletedVal, tableName, userUUID);

            getClickhouseServer().insert(getCHContext(userUUID), query);
        }

        public void markReadAllForUser(UUID userUUID, boolean isRead, long version) {
            String tableName = DB_NAME + "." + SHARD_TABLE;
            int readNewVal = booleanToInt(isRead);
            int readOldVal = booleanToInt(!isRead);

            String query = String.format(
                    "INSERT INTO %s(date, user_uuid, event_uuid, hash, date_time, deleted, is_critical, host_id, " +
                            "version, is_read) " +
                            "SELECT argMax(date, version) AS last_date, user_uuid, event_uuid, hash" +
                            ", argMax(date_time, version) AS last_date_time, argMax(deleted, version) AS last_deleted" +
                            ", argMax(is_critical, version) AS last_is_critical, argMax(host_id, version) AS " +
                            "last_host_id, %s, %s " +
                            "FROM %s WHERE user_uuid='%s' GROUP BY user_uuid, event_uuid, hash HAVING last_deleted=0 AND " +
                            "argMax(is_read, version)=%s",
                    tableName, version, readNewVal, tableName, userUUID, readOldVal);

            getClickhouseServer().insert(getCHContext(userUUID), query);
        }

        @NotNull
        private Statement messageIdNotIn(Set<MessageId> ids) {
            if (CollectionUtils.isEmpty(ids)) {
                return QueryBuilder.trueCondition();
            }
            return QueryBuilder.not(
                    QueryBuilder.tupleIn(F.EVENT_UUID, F.HASH,
                            ids.stream().map(mId -> Pair.of(mId.getEventUuid(), mId.getHash())).collect(Collectors.toList())
                    )
            );
        }

        private static Clause buildWhereForFilter(Clause clause, MessagesFilter filter) {
            if (filter.isOnlyCritical()) {
                clause = clause.and(QueryBuilder.eq(F.LAST_IS_CRITICAL, booleanToInt(true)));
            }
            if (filter.isOnlyUnread()) {
                clause = clause.and(QueryBuilder.eq(F.LAST_IS_READ, booleanToInt(false)));
            }
            if (filter.getHostId() != null) {
                clause = clause.and(QueryBuilder.eq(F.LAST_HOST_ID, filter.getHostId().toString()));
            }
            return clause;
        }

        private static Clause buildWhereForFilter2(Clause clause, MessagesFilter filter) {
            if (filter.isOnlyCritical()) {
                clause = clause.and(QueryBuilder.eq(QueryBuilder.argMax(F.IS_CRITICAL, F.VERSION).toString(),
                        booleanToInt(true)));
            }
            if (filter.isOnlyUnread()) {
                clause = clause.and(QueryBuilder.eq(QueryBuilder.argMax(F.IS_READ, F.VERSION).toString(),
                        booleanToInt(false)));
            }
            if (filter.getHostId() != null) {
                clause = clause.and(QueryBuilder.eq(QueryBuilder.argMax(F.HOST_ID, F.VERSION).toString(),
                        filter.getHostId().toString()));
            }
            return clause;
        }

        @Override
        protected SimpleByteArrayOutputStream packRowValuesWithVersion(SimpleByteArrayOutputStream bs,
                                                                       UserMessageInfo message,
                                                                       Long version) {
            return packRowValues(bs,
                    toClickhouseDate(message.getTime()),
                    message.getUserUUID().toString(),
                    Optional.ofNullable(message.getId()).map(MessageId::getEventUuid).map(UUID::toString).orElse(""),
                    Optional.ofNullable(message.getId()).map(MessageId::getHash).orElse(0L),
                    toDateTimeAsTimestamp(message.getTime()),
                    booleanToInt(message.isCritical()),
                    Optional.ofNullable(message.getHostId()).map(WebmasterHostId::toStringId).orElse(""),
                    booleanToInt(message.isRead()),
                    booleanToInt(message.isDeleted()),
                    version == null ? message.getTime().getMillis() : version
            );
        }

        @Override
        protected String getTempTableName() {
            return TEMP_TABLE_PREFIX + TempDataChunksStoreUtil.getCurrentMinutesInterval(TIME_ALIVE_OF_TEMP_TABLE_IN_MINUTES);
        }

        @Override
        protected String getCreateTableQuery(String tableName) {
            return "CREATE TABLE IF NOT EXISTS " + DB_NAME + "." + tableName +
                    " ( " +
                    "date Date, " +
                    "user_uuid String, " +
                    "event_uuid String, " +
                    "hash Int64, " +
                    "date_time UInt64, " +
                    "is_critical Int8, " +
                    "host_id String, " +
                    "is_read Int8, " +
                    "deleted Int8, " +
                    "version UInt64" +
                    " ) ENGINE = Log;";
        }

        @Override
        public String getShardTable() {
            return SHARD_TABLE;
        }

        // interface methods for merge with template table
        @Override
        public String getDbName() {
            return DB_NAME;
        }

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

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

        public String[] getInsertFields() {
            return INSERT_FIELDS;
        }

        private static final String[] INSERT_FIELDS = {
                F.DATE,
                F.USER_UUID,
                F.EVENT_UUID,
                F.HASH,
                F.DATE_TIME,
                F.IS_CRITICAL,
                F.HOST_ID,
                F.IS_READ,
                F.DELETED,
                F.VERSION
        };

        private static final String[] LAST_RECORD = {
                F.USER_UUID,
                F.EVENT_UUID,
                F.HASH,
                QueryBuilder.argMax(F.DATE_TIME, F.VERSION, F.LAST_DATE_TIME).toString(),
                QueryBuilder.argMax(F.IS_CRITICAL, F.VERSION, F.LAST_IS_CRITICAL).toString(),
                QueryBuilder.argMax(F.HOST_ID, F.VERSION, F.LAST_HOST_ID).toString(),
                QueryBuilder.argMax(F.IS_READ, F.VERSION, F.LAST_IS_READ).toString(),
                QueryBuilder.argMax(F.DELETED, F.VERSION, F.LAST_DELETED).toString(),
        };


        private static final String[] LAST_RECORD_FOR_GET_MESSAGE_ID = {
                F.EVENT_UUID,
                F.HASH,
                QueryBuilder.argMax(F.DATE_TIME, F.VERSION, F.LAST_DATE_TIME).toString(),
        };

    }

    private static final String[] PRIMARY_KEY_WITHOUT_USER_UUID = {
            F.EVENT_UUID,
            F.HASH
    };

    private static final String[] PRIMARY_KEY = {
            F.USER_UUID,
            F.EVENT_UUID,
            F.HASH
    };

    private static final String[] LAST_RECORD = {
            F.USER_UUID,
            F.EVENT_UUID,
            F.HASH,
            UPDATES_ALIAS + "." + F.LAST_IS_CRITICAL,
            UPDATES_ALIAS + "." + F.LAST_HOST_ID,
            OBJECTS_ALIAS + "." + F.MESSAGE,
            OBJECTS_ALIAS + "." + F.TYPE,
            UPDATES_ALIAS + "." + F.LAST_IS_READ,
            UPDATES_ALIAS + "." + F.LAST_DATE_TIME,
            UPDATES_ALIAS + "." + F.LAST_DELETED,
    };


    private static class F {
        static final String DATE = "date";

        //key
        static final String USER_UUID = "user_uuid";
        static final String EVENT_UUID = "event_uuid";
        static final String HASH = "hash";

        //immutable fields
        static final String TYPE = "type";
        static final String MESSAGE = "message";

        //mutable fields
        static final String DATE_TIME = "date_time";
        static final String LAST_DATE_TIME = "last_date_time";
        static final String HOST_ID = "host_id";
        static final String LAST_HOST_ID = "last_host_id";
        static final String IS_CRITICAL = "is_critical";
        static final String LAST_IS_CRITICAL = "last_is_critical";

        static final String IS_READ = "is_read";
        static final String LAST_IS_READ = "last_is_read";

        static final String DELETED = "deleted";
        static final String LAST_DELETED = "last_deleted";

        static final String VERSION = "version";
    }
}
