package ru.yandex.webmaster3.worker.http.clickhouse;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.google.common.base.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;

import ru.yandex.autodoc.common.doc.annotation.Description;
import ru.yandex.webmaster3.core.WebmasterException;
import ru.yandex.webmaster3.core.http.Action;
import ru.yandex.webmaster3.core.http.ActionRequest;
import ru.yandex.webmaster3.core.http.ActionResponse;
import ru.yandex.webmaster3.core.http.ReadAction;
import ru.yandex.webmaster3.core.http.RequestQueryProperty;
import ru.yandex.webmaster3.storage.clickhouse.system.dao.ClickhouseSystemReplicasCHDao;
import ru.yandex.webmaster3.storage.clickhouse.system.dao.ClickhouseSystemTablesCHDao;
import ru.yandex.webmaster3.storage.clickhouse.system.data.ClickhouseSystemReplicaInfo;
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.worker.http.clickhouse.GetClickhouseReplicasAction.Request;
import ru.yandex.webmaster3.worker.http.clickhouse.GetClickhouseReplicasAction.Response;

import static ru.yandex.webmaster3.worker.http.clickhouse.GetClickhouseReplicasAction.ClickhouseReplicaState.NOT_EXISTS;
import static ru.yandex.webmaster3.worker.http.clickhouse.GetClickhouseReplicasAction.ClickhouseReplicaState.REPLICATED;
import static ru.yandex.webmaster3.worker.http.clickhouse.GetClickhouseReplicasAction.ClickhouseReplicaState.REPLICATING;

/**
 * Created by Oleg Bazdyrev on 31/05/2017.
 */
@ReadAction
@Description("Получение информации о таблицах на всех шардах Clickhouse")
public class GetClickhouseReplicasAction extends Action<Request, Response> {

    private static final Logger log = LoggerFactory.getLogger(GetClickhouseReplicasAction.class);

    private static final String ZK_WEBMASTER_ROOT = "/webmaster3";
    private static final Pattern SHARDED_PATTERN = Pattern.compile("/(shard-)?\\d+/");

    private ClickhouseServer clickhouseServer;
    private ClickhouseSystemTablesCHDao clickhouseSystemTablesCHDao;
    private ClickhouseSystemReplicasCHDao clickhouseSystemReplicasCHDao;

    @Override
    public Response process(Request request) throws WebmasterException {
        // собираем данные по хостам
        List<ClickhouseHost> hosts = clickhouseServer.getHosts().stream()
                .sorted(Comparator.comparing(ClickhouseHost::getShard).thenComparing(
                        host -> host.getDcName().equals(request.referenceDc) ? "0" : "" + host.getDcName()))
                .collect(Collectors.toList());
        Map<String, ClickhouseTableInfo> tableInfosByName = new TreeMap<>();
        Map<String, List<String>> zkChildrenCache = new HashMap<>();
        String refDc = Strings.isNullOrEmpty(request.referenceDc) ? hosts.get(0).getDcName() : request.referenceDc;
        int refDcShardIndex = 0;

        for (int i = 0; i < hosts.size(); i++) {
            ClickhouseHost clickhouseHost = hosts.get(i);
            if (clickhouseHost.getDcName().equals(refDc)) {
                refDcShardIndex = i;
            }
            try {
                List<ClickhouseSystemReplicaInfo> replicas = clickhouseSystemReplicasCHDao.getReplicas(
                        clickhouseHost, request.database, false);
                // раскладываем по именам таблиц
                for (ClickhouseSystemReplicaInfo replica : replicas) {
                    String tableName = replica.getDatabase() + "." + replica.getTable();
                    ClickhouseTableInfo tableInfo = tableInfosByName.computeIfAbsent(tableName, key -> {
                        // несколько кривой способ определения, является ли таблица шардируемой
                        boolean sharded = SHARDED_PATTERN.matcher(replica.getReplicaPath()).find();
                        //getCreateTableStatement(clickhouseHost, replica).contains("{shard}");
                        return new ClickhouseTableInfo(replica.getDatabase(), replica.getTable(), hosts.size(), sharded);
                    });

                    // пометим, что эта реплика живая
                    tableInfo.enabledZkReplicas.computeIfPresent(replica.getReplicaPath(), (k, v) -> true);
                    // сравним columnsVersion и zookeperPath с эталонным ДЦ
                    boolean differFromReference = false;
                    int refIndex = tableInfo.sharded ? refDcShardIndex : 0;
                    if (i != refIndex) {
                        ClickhouseReplicaInfo refTableInfo = tableInfo.getReplicas().get(refIndex);
                        differFromReference = refTableInfo != null && refTableInfo.state != NOT_EXISTS && (
                                refTableInfo.columnsVersion != replica.getColumnsVersion() ||
                                        !refTableInfo.zookeeperPath.equals(replica.getZookeeperPath()));
                    }
                    ClickhouseReplicaState state = replica.getInsertsInQueue() == 0 ? REPLICATED : REPLICATING;
                    ClickhouseReplicaInfo replicaInfo = new ClickhouseReplicaInfo(replica.getZookeeperPath(),
                            replica.getColumnsVersion(), state, differFromReference);
                    tableInfo.getReplicas().set(i, replicaInfo);
                }
            } catch (ClickhouseException e) {
                log.error("Error getting replicas info from Clickhouse");
            }
        }

        return new Response(hosts, new ArrayList<>(tableInfosByName.values()));
    }

    @Required
    public void setClickhouseServer(ClickhouseServer clickhouseServer) {
        this.clickhouseServer = clickhouseServer;
    }

    @Required
    public void setClickhouseSystemTablesCHDao(ClickhouseSystemTablesCHDao clickhouseSystemTablesCHDao) {
        this.clickhouseSystemTablesCHDao = clickhouseSystemTablesCHDao;
    }

    @Required
    public void setClickhouseSystemReplicasCHDao(ClickhouseSystemReplicasCHDao clickhouseSystemReplicasCHDao) {
        this.clickhouseSystemReplicasCHDao = clickhouseSystemReplicasCHDao;
    }

   public enum ClickhouseReplicaState {
        NOT_EXISTS, // таблица отсутствует
        REPLICATING, // таблица реплицируется
        REPLICATED, // таблица отреплицирована - все ОК
        ERROR, // не удалось получить информацию о таблицы
        ;
    }

    public static class Request implements ActionRequest {

        private String database;
        private String referenceDc;

        @RequestQueryProperty(required = true)
        @Description("БД, таблицы которой интересуют")
        public void setDatabase(String database) {
            this.database = database;
        }

        @RequestQueryProperty
        @Description("Эталонный ДЦ")
        public void setReferenceDc(String referenceDc) {
            this.referenceDc = referenceDc;
        }
    }

    public static class Response implements ActionResponse.NormalResponse {

        private final List<ClickhouseHost> hosts;
        private final List<ClickhouseTableInfo> tableInfos;

        public Response(List<ClickhouseHost> hosts, List<ClickhouseTableInfo> tableInfos) {
            this.hosts = hosts;
            this.tableInfos = tableInfos;
        }

        @Description("Список серверов Clickhouse с инфорацией о них")
        public List<ClickhouseHost> getHosts() {
            return hosts;
        }

        @Description("Информация о таблицах")
        public List<ClickhouseTableInfo> getTableInfos() {
            return tableInfos;
        }
    }

    public static final class ClickhouseTableInfo {

        private final String database;
        private final String table;
        private final List<ClickhouseReplicaInfo> replicas;
        private final boolean sharded;
        private final Map<String, Boolean> enabledZkReplicas = new HashMap<>();

        public ClickhouseTableInfo(String database, String table, int hostCount, boolean sharded) {
            this.database = database;
            this.table = table;
            this.replicas = Stream.generate(() -> new ClickhouseReplicaInfo()).limit(hostCount).collect(Collectors.toList());
            this.sharded = sharded;
        }

        public String getDatabase() {
            return database;
        }

        public String getTable() {
            return table;
        }

        public List<ClickhouseReplicaInfo> getReplicas() {
            return replicas;
        }

        public boolean isSharded() {
            return sharded;
        }
    }

    public static final class ClickhouseReplicaInfo {

        private final String zookeeperPath;
        private final int columnsVersion;
        private final ClickhouseReplicaState state;
        private final boolean differFromReference;

        public ClickhouseReplicaInfo() {
            this(null, -1, NOT_EXISTS, false);
        }

        public ClickhouseReplicaInfo(String zookeeperPath, int columnsVersion, ClickhouseReplicaState state,
                                     boolean differFromReference) {
            this.zookeeperPath = zookeeperPath;
            this.columnsVersion = columnsVersion;
            this.state = state;
            this.differFromReference = differFromReference;
        }

        public String getZookeeperPath() {
            return zookeeperPath;
        }

        public int getColumnsVersion() {
            return columnsVersion;
        }

        public ClickhouseReplicaState getState() {
            return state;
        }

        public boolean isDifferFromReference() {
            return differFromReference;
        }
    }
}
