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

import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Pair;

import ru.yandex.webmaster3.core.WebmasterException;
import ru.yandex.webmaster3.core.http.WebmasterErrorResponse;
import ru.yandex.webmaster3.storage.user.message.UserMessageInfo;
import ru.yandex.webmaster3.storage.util.clickhouse2.AbstractClickhouseDao;
import ru.yandex.webmaster3.storage.util.clickhouse2.ClickhouseException;
import ru.yandex.webmaster3.storage.util.clickhouse2.ClickhouseHost;
import ru.yandex.webmaster3.storage.util.clickhouse2.ClickhouseHostLocation;
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.QueryBuilder;
import ru.yandex.webmaster3.storage.util.clickhouse2.query.Statement;

/**
 * Created by kravchenko99 on 16.10.19.
 */
@Slf4j
public abstract class AbstractUserMessage4Dao extends AbstractClickhouseDao {
    private volatile Pair<String, ClickhouseHostLocation> lastCreatedTable;

    public boolean insertMessagesIntoTempTable(List<UserMessageInfo> messages, Long version) {
        String tmpTable = getTempTableName();
        Map<Integer, List<UserMessageInfo>> shard2Messages = messages.stream()
                .collect(Collectors.groupingBy(this::getShard));
        for (Map.Entry<Integer, List<UserMessageInfo>> entry : shard2Messages.entrySet()) {
            //100% не пуст
            List<ClickhouseHost> hosts = TempDataChunksStoreUtil.selectKHostsForShard(entry.getKey(), getClickhouseServer(), getMinutesIntervalSize(), 2);
            if (hosts == null) {
                log.error("Failed to select host for table {}, shard {}", tmpTable, entry.getKey());
                return false;
            }

            log.info("Selected hosts: {}", hosts);
            hosts.forEach(host -> {
                createTableIfNotExists(host, tmpTable);
                getClickhouseServer().executeWithFixedHost(host, () -> {
                            insertMessages(entry.getValue(), tmpTable, version);
                            return null;
                        }
                );
                log.info("Messages inserted to temp table : {} on host : {}", tmpTable, host);
            });
        }
        return true;
    }

    private void createTableIfNotExists(ClickhouseHost host, String tableName) throws ClickhouseException {
        Pair<String, ClickhouseHostLocation> createdTable = lastCreatedTable;
        if (createdTable != null) {
            if (createdTable.getLeft().equals(tableName) &&
                    createdTable.getRight().getDcName().equals(host.getDcName()) &&
                    createdTable.getRight().getShard() == host.getShard()) {
                return; // таблица уже создана
            }
        }
        String createTableQuery = getCreateTableQuery(tableName);

        ClickhouseQueryContext.Builder chContext = ClickhouseQueryContext.useDefaults().setHost(host);
        getClickhouseServer().execute(chContext, ClickhouseServer.QueryType.INSERT, createTableQuery,
                Optional.empty(), Optional.empty());
        // да, оно недосинхронизрованно - но в худшем случае запустим create if not exists несколько раз - переживем
        lastCreatedTable = Pair.of(tableName, host);
    }

    private void insertMessages(List<UserMessageInfo> messages,
                                String tableName,
                                Long version) {
        Statement st = QueryBuilder.insertInto(getDbName(), tableName)
                .fields(getInsertFields())
                .format(Format.Type.TAB_SEPARATED);
        SimpleByteArrayOutputStream bs = new SimpleByteArrayOutputStream();

        for (UserMessageInfo message : messages) {
            bs = packRowValuesWithVersion(bs, message, version);
        }
        try {
            insert(st, bs.toInputStream());
        } catch (ClickhouseException e) {
            String message = "Failed to insert to table " + tableName;
            throw new WebmasterException(message,
                    new WebmasterErrorResponse.ClickhouseErrorResponse(getClass(), st.toString(), e), e);
        }
    }

    public void insertFromTempTable(String sourceTableName) {
        String[] insertFields = getInsertFields();
        String dbName = getDbName();
        String shardTable = getShardTable();

        Statement st = QueryBuilder.insertInto(dbName, shardTable)
                .fields(insertFields);

        From select = QueryBuilder.select(insertFields)
                .from(dbName, sourceTableName);

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

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

    protected int getShard(UserMessageInfo message) {
        return getShard(message.getUserUUID());
    }

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

    protected abstract SimpleByteArrayOutputStream packRowValuesWithVersion(SimpleByteArrayOutputStream bs,
                                                                            UserMessageInfo message,
                                                                            Long version);

    protected abstract String[] getInsertFields();

    protected abstract String getDbName();

    protected abstract String getShardTable();

    protected abstract String getTempTableName();

    protected abstract String getCreateTableQuery(String tableName);

    protected abstract int getMinutesIntervalSize();
}
