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

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.jetbrains.annotations.NotNull;
import org.joda.time.LocalDate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import ru.yandex.webmaster3.core.WebmasterException;
import ru.yandex.webmaster3.core.data.WebmasterUser;
import ru.yandex.webmaster3.core.http.WebmasterErrorResponse;
import ru.yandex.webmaster3.core.notification.LanguageEnum;
import ru.yandex.webmaster3.storage.clickhouse.system.dao.ClickhouseSystemTablesCHDao;
import ru.yandex.webmaster3.storage.clickhouse.system.data.ClickhouseSystemTableInfo;
import ru.yandex.webmaster3.storage.notifications.NotificationChannel;
import ru.yandex.webmaster3.storage.notifications.NotificationRecListId;
import ru.yandex.webmaster3.storage.user.MessageContentConverter;
import ru.yandex.webmaster3.storage.user.UserTakeoutDataProvider;
import ru.yandex.webmaster3.storage.user.message.MessageTypeEnum;
import ru.yandex.webmaster3.storage.util.clickhouse2.ClickhouseException;
import ru.yandex.webmaster3.storage.util.clickhouse2.ClickhouseHost;
import ru.yandex.webmaster3.storage.util.clickhouse2.ClickhouseServer;
import ru.yandex.webmaster3.storage.util.clickhouse2.SimpleByteArrayOutputStream;
import ru.yandex.webmaster3.storage.util.clickhouse2.query.*;
import ru.yandex.webmaster3.storage.util.clickhouse2.query.cases.Case;

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

/**
 * @author avhaliullin
 */
public class SearchBaseNotificationListCHDao extends AbstractNotificationDao
        implements UserTakeoutDataProvider {
    private static final Logger log = LoggerFactory.getLogger(SearchBaseNotificationListCHDao.class);
    private static LocalDate TMPDATE_VALUE = LocalDate.now();
    public static final String TABLE_NAME_PREFIX = "search_base_notification_list_";

    @Autowired
    private ClickhouseSystemTablesCHDao clickhouseSystemTablesCHDao;

    private static String nullAsEmpty(String s) {
        return s == null ? "" : s;
    }

    private static String emptyAsNull(String s) {
        return StringUtils.isEmpty(s) ? null : s;
    }

    public void addRecords(NotificationRecListId listId, List<PreparedGlobalMessageInfo> messages, ClickhouseHost host) throws ClickhouseException {
        Statement st = QueryBuilder.insertInto(DB_NAME, getTableName(listId))
                .fields("tmpdate", F.USER_ID, F.HOST_ID, F.MESSAGE_TYPE, F.CHANNELS, F.EMAIL, F.MESSAGE, F.LANG, F.LOGIN, F.FIO)
                .format(Format.Type.TAB_SEPARATED);

        SimpleByteArrayOutputStream bs = new SimpleByteArrayOutputStream();
        for (PreparedGlobalMessageInfo messageInfo : messages) {
            bs = packRowValues(bs,
                    TMPDATE_VALUE,
                    messageInfo.getUserId(),
                    messageInfo.getHostIdStr(),
                    messageInfo.getMessage().getType().value(),
                    encodeChannels(messageInfo.getChannels()),
                    nullAsEmpty(messageInfo.getEmail()),
                    MessageContentConverter.serialize(messageInfo.getMessage()),
                    messageInfo.getLang().getName(),
                    messageInfo.getLogin(),
                    nullAsEmpty(messageInfo.getFio())
            );
        }
        if (bs.size() == 0L) {
            return;
        }

        SimpleByteArrayOutputStream finalBs = bs;
        getClickhouseServer().executeWithFixedHost(host, () -> {
                    insert(st, finalBs.toInputStream());
                    return null;
                }
        );
    }

    public List<PreparedGlobalMessageInfo> getMessages(NotificationRecListId listId, int fromOffset, int size) throws ClickhouseException {
        // прямой запрос сжирает слишком много памяти, поэтому делим запрос на два
        // во внутреннем собираем только ключи
        Statement innerQuery = QueryBuilder.select(F.USER_ID, F.HOST_ID, F.MESSAGE_TYPE)
                .from(getFullTableName(listId))
                .orderBy(Arrays.asList(Pair.of(F.USER_ID, OrderBy.Direction.ASC), Pair.of(F.HOST_ID, OrderBy.Direction.ASC), Pair.of(F.MESSAGE_TYPE, OrderBy.Direction.ASC)))
                .limit(fromOffset, size);

        Statement st = QueryBuilder.select(F.USER_ID, F.EMAIL, F.CHANNELS, F.MESSAGE, F.LANG, F.LOGIN, F.FIO, F.MESSAGE_TYPE, F.PATH)
                .from(getFullTableName(listId))
                .where(new Case() {
                    @Override
                    public String toString() {
                        return "(" + F.USER_ID + "," + F.HOST_ID + "," + F.MESSAGE_TYPE + ") IN (" + innerQuery.toString() + ")";
                    }
                });

        ClickhouseHost targetHost = pickHost(getTableName(listId));
        return getClickhouseServer().executeWithFixedHost(targetHost, () ->
                queryAll(st, row -> new PreparedGlobalMessageInfo(
                        row.getLong(F.USER_ID),
                        emptyAsNull(row.getString(F.EMAIL)),
                        row.getString(F.PATH),
                        MessageContentConverter.deserialize(
                                MessageTypeEnum.R.fromValue(row.getInt(F.MESSAGE_TYPE)),
                                row.getString(F.MESSAGE)
                        ),
                        decodeChannels(row.getInt(F.CHANNELS)),
                        LanguageEnum.fromString(row.getString(F.LANG)),
                        row.getString(F.LOGIN),
                        emptyAsNull(row.getString(F.FIO))
            )));
    }

    @NotNull
    private ClickhouseHost pickHost(String tableName) {
        Where countTablesQuery = QueryBuilder.select()
                .countAll()
                .from("system.tables")
                .where(QueryBuilder.eq("database", DB_NAME))
                .and(QueryBuilder.eq("name", tableName));

        ClickhouseHost targetHost = null;
        List<ClickhouseHost> hosts = new ArrayList<>(getClickhouseServer().getHosts());
        // чтобы все сервера мучались равномерно
        Collections.shuffle(hosts);
        for (ClickhouseHost host : ClickhouseServer.alive().apply(hosts)) {
            try {
                long count = getClickhouseServer().executeWithFixedHost(host, () -> queryOne(countTablesQuery,
                        r -> r.getLongUnsafe(0)).orElse(0L));
                if (count > 0) {
                    targetHost = host;
                    break;
                }
            } catch (ClickhouseException e) {
                log.error("Failed to show clickhouse tables on {}", host, e);
            }
        }

        if (targetHost == null) {
            throw new WebmasterException("Get messages failed",
                    new WebmasterErrorResponse.ClickhouseErrorResponse(getClass(), null),
                        new ClickhouseException("No clickhouse hosts avaliable with table " + tableName, countTablesQuery.toString(), null));
        }

        return targetHost;
    }

    @Override
    public void deleteUserData(WebmasterUser user) {
        Set<String> tableNames = getClickhouseServer().getHosts().stream()
                .filter(ClickhouseHost::isUp)
                .flatMap(host -> clickhouseSystemTablesCHDao.getTablesForPrefix(host, DB_NAME, TABLE_NAME_PREFIX).stream()
                .map(ClickhouseSystemTableInfo::getName))
                .filter(tableName -> !tableName.contains("_distrib"))
                .collect(Collectors.toSet());

        log.info("Shard tables: {}", Arrays.toString(tableNames.toArray()));
        for (var tableName : tableNames) {
            String query = String.format("ALTER TABLE %s DELETE WHERE user_id=%s",
                    DB_NAME + "." + tableName, user.getUserId());
            getClickhouseServer().execute(pickHost(tableName), query);
        }
    }

    public String buildCreateTableQuery(NotificationRecListId listId) throws ClickhouseException {
        String tName = getFullTableName(listId);
        String createQuery =
                String.format(" ("
                                + "  tmpdate Date"
                                + ", " + F.USER_ID + " Int64"
                                + ", " + F.HOST_ID + " String"
                                + ", " + F.PATH + " String"
                                + ", " + F.MESSAGE_TYPE + " Int32"
                                + ", " + F.CHANNELS + " Int32"
                                + ", " + F.EMAIL + " String"
                                + ", " + F.MESSAGE + " String"
                                + ", " + F.LANG + " String"
                                + ", " + F.LOGIN + " String"
                                + ", " + F.FIO + " String"
                                + ") ENGINE=ReplicatedMergeTree('/webmaster3/clickhouse/tables/%s', '{replica}', tmpdate, " +
                                "(" + F.USER_ID + ", " + F.HOST_ID + ", " + F.MESSAGE_TYPE + "), 8192)", tName.replace('.', '/'));
        return createQuery;
    }

    public void createTable(NotificationRecListId listId, ClickhouseHost host) throws ClickhouseException {
        getClickhouseServer().executeWithFixedHost(host, () -> {
            insert("CREATE TABLE IF NOT EXISTS " + getFullTableName(listId) + buildCreateTableQuery(listId));
            return null;
        });
    }

    public int countUniqueTargets(NotificationRecListId listId) throws ClickhouseException {
        String st = "SELECT count() as count FROM " + getFullTableName(listId);
        return queryOne(st, row -> row.getLongUnsafe("count")).orElse(0L).intValue();
    }

    public String getDbName() {
        return DB_NAME;
    }

    public static String getTableName(NotificationRecListId listId) {
        return TABLE_NAME_PREFIX + inTableName(listId.getNotificationId()) + "_" + inTableName(listId.getListId());
    }

    static String getFullTableName(NotificationRecListId listId) {
        return DB_NAME + "." + getTableName(listId);
    }

    static class F {
        static final String USER_ID = "user_id";
        static final String HOST_ID = "host_id";
        static final String CHANNELS = "channels";
        static final String EMAIL = "email";
        static final String PATH = "path";
        static final String MESSAGE = "message";
        static final String MESSAGE_TYPE = "message_type";
        static final String LANG = "lang";
        static final String LOGIN = "login";
        static final String FIO = "fio";
    }

    public static class ChannelInfo {
        private final long userId;
        private final NotificationChannel channel;
        private final String email;

        public ChannelInfo(long userId, NotificationChannel channel, String email) {
            this.userId = userId;
            this.channel = channel;
            this.email = email;
        }

        public long getUserId() {
            return userId;
        }

        public NotificationChannel getChannel() {
            return channel;
        }

        public String getEmail() {
            return email;
        }
    }
}

