/*
 * Copyright (C) 2016-2023 ActionTech.
 * based on code by MyCATCopyrightHolder Copyright (c) 2013, OpenCloudDB/MyCAT.
 * License: http://www.gnu.org/licenses/gpl.html GPL version 2 or higher.
 */
package com.actiontech.dble.services.manager.response;

import com.actiontech.dble.DbleServer;
import com.actiontech.dble.backend.datasource.ApNode;
import com.actiontech.dble.backend.datasource.PhysicalDbGroup;
import com.actiontech.dble.backend.datasource.PhysicalDbInstance;
import com.actiontech.dble.backend.datasource.ShardingNode;
import com.actiontech.dble.btrace.provider.ClusterDelayProvider;
import com.actiontech.dble.cluster.ClusterHelper;
import com.actiontech.dble.cluster.DistributeLock;
import com.actiontech.dble.cluster.logic.ClusterLogic;
import com.actiontech.dble.cluster.logic.ClusterOperation;
import com.actiontech.dble.cluster.path.ClusterMetaUtil;
import com.actiontech.dble.cluster.path.ClusterPathUtil;
import com.actiontech.dble.cluster.values.ConfStatus;
import com.actiontech.dble.cluster.values.FeedBackType;
import com.actiontech.dble.cluster.values.RawJson;
import com.actiontech.dble.config.ConfigInitializer;
import com.actiontech.dble.config.DbleTempConfig;
import com.actiontech.dble.config.ErrorCode;
import com.actiontech.dble.config.ServerConfig;
import com.actiontech.dble.config.model.ClusterConfig;
import com.actiontech.dble.config.model.SystemConfig;
import com.actiontech.dble.config.model.sharding.SchemaConfig;
import com.actiontech.dble.config.model.sharding.table.ERTable;
import com.actiontech.dble.config.model.user.RwSplitUserConfig;
import com.actiontech.dble.config.model.user.UserConfig;
import com.actiontech.dble.config.model.user.UserName;
import com.actiontech.dble.config.util.ConfigException;
import com.actiontech.dble.config.util.ConfigUtil;
import com.actiontech.dble.meta.ReloadException;
import com.actiontech.dble.meta.ReloadLogHelper;
import com.actiontech.dble.meta.ReloadManager;
import com.actiontech.dble.net.IOProcessor;
import com.actiontech.dble.net.connection.BackendConnection;
import com.actiontech.dble.net.connection.FrontendConnection;
import com.actiontech.dble.net.mysql.OkPacket;
import com.actiontech.dble.route.function.AbstractPartitionAlgorithm;
import com.actiontech.dble.route.parser.ManagerParseConfig;
import com.actiontech.dble.server.variables.SystemVariables;
import com.actiontech.dble.server.variables.VarsExtractorHandler;
import com.actiontech.dble.services.manager.ManagerService;
import com.actiontech.dble.singleton.CronScheduler;
import com.actiontech.dble.singleton.FrontendUserManager;
import com.actiontech.dble.singleton.TraceManager;
import com.google.common.collect.Lists;
import com.google.common.collect.MapDifference;
import com.google.common.collect.Maps;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import static com.actiontech.dble.cluster.path.ClusterPathUtil.SEPARATOR;
import static com.actiontech.dble.meta.ReloadStatus.TRIGGER_TYPE_COMMAND;
import static com.actiontech.dble.services.manager.response.ChangeItemType.PHYSICAL_DB_INSTANCE;

public final class ReloadConfig {
    private static final Logger LOGGER = LoggerFactory.getLogger(ReloadConfig.class);

    private ReloadConfig() {
    }

    public static void execute(ManagerService service, String stmt, int offset) {
        try {
            ManagerParseConfig parser = new ManagerParseConfig();
            int rs = parser.parse(stmt, offset);
            switch (rs) {
                case ManagerParseConfig.CONFIG:
                case ManagerParseConfig.CONFIG_ALL:
                    ReloadConfig.execute(service, parser.getMode(), true, new ConfStatus(ConfStatus.Status.RELOAD_ALL));
                    writePacket(true, service, null, 0);
                    break;
                default:
                    service.writeErrMessage(ErrorCode.ER_YES, "Unsupported statement");
                    break;
            }
        } catch (ReloadException e) {
            writePacket(false, service, e.getMessage() == null ? e.toString() : e.getMessage(), e.getErrorCode());
        } catch (Exception e) {
            writePacket(false, service, e.getMessage() == null ? e.toString() : e.getMessage(),
                    (e.getCause() instanceof ReloadException) ? ((ReloadException) e.getCause()).getErrorCode() : ErrorCode.ER_YES);
        }
    }

    public static void execute(ManagerService service, final int loadAllMode, boolean returnFlag, ConfStatus confStatus) throws Exception {
        try {
            if (ClusterConfig.getInstance().isClusterEnable()) {
                reloadWithCluster(service, loadAllMode, confStatus);
            } else {
                reloadWithoutCluster(service, loadAllMode, returnFlag, confStatus);
            }
        } catch (ReloadException e) {
            ReloadLogHelper.warn2("Reload config failure. The reason is {}", e.getMessage());
            throw e;
        } catch (ConfigException e) {
            ReloadLogHelper.warn2("Reload config failure. The reason is {}", e.getMessage());
            throw e;
        } catch (Exception e) {
            if (e.getCause() instanceof ReloadException || e.getCause() instanceof ConfigException) {
                ReloadLogHelper.warn2("Reload config failure. The reason is {}", e.getMessage());
            } else {
                ReloadLogHelper.warn2("Reload config failure. catch exception is ", e);
            }
            throw e;
        } finally {
            ReloadManager.reloadFinish();
        }
    }

    private static void reloadWithCluster(ManagerService service, int loadAllMode, ConfStatus confStatus) throws Exception {
        TraceManager.TraceObject traceObject = TraceManager.serviceTrace(service, "reload-with-cluster");
        try {

            ClusterHelper clusterHelper = ClusterHelper.getInstance(ClusterOperation.CONFIG);
            DistributeLock distributeLock = null;
            if (!confStatus.getStatus().equals(ConfStatus.Status.MANAGER_INSERT) && !confStatus.getStatus().equals(ConfStatus.Status.MANAGER_UPDATE) &&
                    !confStatus.getStatus().equals(ConfStatus.Status.MANAGER_DELETE)) {
                distributeLock = clusterHelper.createDistributeLock(ClusterMetaUtil.getConfChangeLockPath());
                if (!distributeLock.acquire()) {
                    throw new ReloadException(ErrorCode.ER_YES, "Other instance is reloading, please try again later.");
                }
                ReloadLogHelper.graceInfo("added distributeLock, path" + ClusterMetaUtil.getConfChangeLockPath());
            }
            try {
                ClusterDelayProvider.delayAfterReloadLock();
                if (!ReloadManager.startReload(TRIGGER_TYPE_COMMAND, confStatus)) {
                    throw new ReloadException(ErrorCode.ER_YES, "reload status error, other client or cluster may in reload");
                }
                //step 1 lock the local meta ,than all the query depends on meta will be hanging
                final ReentrantReadWriteLock lock = DbleServer.getInstance().getConfig().getLock();
                lock.writeLock().lock();
                try {
                    ReloadLogHelper.briefInfo("added configLock");
                    //step 2 reload the local config file
                    ReloadResult reloadResult;
                    if (confStatus.getStatus().equals(ConfStatus.Status.MANAGER_INSERT) || confStatus.getStatus().equals(ConfStatus.Status.MANAGER_UPDATE) ||
                            confStatus.getStatus().equals(ConfStatus.Status.MANAGER_DELETE)) {
                        reloadResult = reloadByConfig(loadAllMode, true);
                    } else {
                        reloadResult = reloadByLocalXml(loadAllMode);
                    }
                    if (!reloadResult.isSuccess()) {
                        throw new ReloadException(ErrorCode.ER_RELOAD_INTERRUPUTED, "reload interruputed by others, config should be reload");
                    }
                    ReloadLogHelper.briefInfo("single instance(self) finished");
                    ClusterDelayProvider.delayAfterMasterLoad();

                    //step 3 if the reload with no error ,than write the config file into cluster center remote
                    ClusterHelper.writeConfToCluster(reloadResult);
                    ReloadLogHelper.briefInfo("sent config file to cluster center");

                    //step 4 write the reload flag and self reload result into cluster center,notify the other dble to reload
                    ConfStatus status = new ConfStatus(SystemConfig.getInstance().getInstanceName(),
                            ConfStatus.Status.RELOAD_ALL, String.valueOf(loadAllMode));
                    clusterHelper.setKV(ClusterMetaUtil.getConfStatusOperatorPath(), status);
                    ReloadLogHelper.briefInfo("sent config status to cluster center");
                    //step 5 start a loop to check if all the dble in cluster is reload finished
                    ReloadManager.waitingOthers();
                    clusterHelper.createSelfTempNode(ClusterPathUtil.getConfStatusOperatorPath(), FeedBackType.SUCCESS);
                    final String errorMsg = ClusterLogic.forConfig().waitingForAllTheNode(ClusterPathUtil.getConfStatusOperatorPath());
                    ReloadLogHelper.briefInfo("all instances finished");
                    ClusterDelayProvider.delayBeforeDeleteReloadLock();

                    if (errorMsg != null) {
                        throw new ReloadException(errorMsg.contains("interrupt by command") ? ErrorCode.ER_RELOAD_INTERRUPUTED : ErrorCode.ER_CLUSTER_RELOAD,
                                "partial instance reload failed, failed because of:[" + errorMsg + "]");
                    }
                } finally {
                    lock.writeLock().unlock();
                    ClusterHelper.cleanPath(ClusterPathUtil.getConfStatusOperatorPath() + SEPARATOR);
                    ReloadLogHelper.briefInfo("released configLock");
                }
            } finally {
                if (distributeLock != null) {
                    distributeLock.release();
                    ReloadLogHelper.briefInfo("released distributeLock");
                }
            }
        } finally {
            TraceManager.finishSpan(service, traceObject);
        }
    }

    private static void reloadWithoutCluster(ManagerService service, final int loadAllMode, boolean returnFlag, ConfStatus confStatus) throws ReloadException, Exception {
        TraceManager.TraceObject traceObject = TraceManager.serviceTrace(service, "reload-in-local");
        final ReentrantReadWriteLock lock = DbleServer.getInstance().getConfig().getLock();
        lock.writeLock().lock();
        try {
            ReloadLogHelper.graceInfo("added configLock");
            if (!ReloadManager.startReload(TRIGGER_TYPE_COMMAND, confStatus)) {
                throw new ReloadException(ErrorCode.ER_YES, "reload status error ,other client or cluster may in reload");
            }
            ReloadResult reloadResult;
            if (confStatus.getStatus().equals(ConfStatus.Status.MANAGER_INSERT) || confStatus.getStatus().equals(ConfStatus.Status.MANAGER_UPDATE) ||
                    confStatus.getStatus().equals(ConfStatus.Status.MANAGER_DELETE)) {
                reloadResult = reloadByConfig(loadAllMode, true);
            } else {
                reloadResult = reloadByLocalXml(loadAllMode);
            }
            if (reloadResult.isSuccess() && returnFlag) {
                // ok package
                return;
            } else if (!reloadResult.isSuccess()) {
                throw new ReloadException(ErrorCode.ER_RELOAD_INTERRUPUTED, "reload interruputed by others,metadata should be reload");
            }
        } finally {
            lock.writeLock().unlock();
            ReloadLogHelper.briefInfo("released configLock");
            TraceManager.finishSpan(service, traceObject);
        }
    }


    private static void writeErrorResult(ManagerService c, String errorMsg) {
        String sb = "Reload config failure.The reason is " + errorMsg;
        LOGGER.warn(sb);
        c.writeErrMessage(ErrorCode.ER_YES, sb);
    }

    public static ReloadResult reloadByLocalXml(final int loadAllMode) throws Exception {
        return reload(loadAllMode, null, null, null, null);
    }

    public static ReloadResult reloadByConfig(final int loadAllMode, boolean isWriteToLocal) throws Exception {
        RawJson userConfig = DbleTempConfig.getInstance().getUserConfig();
        userConfig = userConfig == null ? DbleServer.getInstance().getConfig().getUserConfig() : userConfig;
        RawJson dbConfig = DbleTempConfig.getInstance().getDbConfig();
        dbConfig = dbConfig == null ? DbleServer.getInstance().getConfig().getDbConfig() : dbConfig;
        RawJson shardingConfig = DbleTempConfig.getInstance().getShardingConfig();
        shardingConfig = shardingConfig == null ? DbleServer.getInstance().getConfig().getShardingConfig() : shardingConfig;
        RawJson sequenceConfig = DbleTempConfig.getInstance().getSequenceConfig();
        sequenceConfig = sequenceConfig == null ? DbleServer.getInstance().getConfig().getSequenceConfig() : sequenceConfig;
        final ReloadResult reloadResult = reload(loadAllMode, userConfig, dbConfig, shardingConfig, sequenceConfig);

        ReloadLogHelper.briefInfo("clean temp config ...");
        DbleTempConfig.getInstance().clean();
        //sync json to local
        if (isWriteToLocal)
            ReloadLogHelper.briefInfo("sync json to local ...");
        DbleServer.getInstance().getConfig().syncJsonToLocal(isWriteToLocal);
        return reloadResult;
    }

    private static ReloadResult reload(final int loadAllMode, RawJson userConfig, RawJson dbConfig, RawJson shardingConfig, RawJson sequenceConfig) throws Exception {
        TraceManager.TraceObject traceObject = TraceManager.threadTrace("self-reload");
        try {
            // load configuration
            ConfigInitializer loader = loadConfig(userConfig, dbConfig, shardingConfig, sequenceConfig);

            // compare changes
            List<ChangeItem> changeItemList = compareChange(loader);

            boolean forceAllReload = false;
            if ((loadAllMode & ManagerParseConfig.OPTR_MODE) != 0) {
                //-r
                forceAllReload = true;
            }

            // test connection
            testConnection(loader, changeItemList, forceAllReload, loadAllMode);

            ServerConfig newConfig = new ServerConfig(loader);
            Map<String, PhysicalDbGroup> newDbGroups = newConfig.getDbGroups();

            // check version/packetSize/lowerCase && get system variables
            SystemVariables newSystemVariables = checkVersionAGetSystemVariables(loader, newDbGroups, changeItemList, forceAllReload);

            // recycle old active conn
            recycleOldBackendConnections(forceAllReload, (loadAllMode & ManagerParseConfig.OPTF_MODE) != 0);


            // lowerCase
            if (loader.isFullyConfigured()) {
                if (newSystemVariables.isLowerCaseTableNames()) {
                    ReloadLogHelper.briefInfo("dbGroup's lowerCaseTableNames=1, lower the config properties ...");
                    newConfig.reviseLowerCase();
                }
                ReloadLogHelper.briefInfo("selfChecking0 ...");
                newConfig.selfChecking0();
            }
            // load sequence
            newConfig.reloadSequence(loader.getSequenceConfig());

            Map<UserName, UserConfig> newUsers = newConfig.getUsers();
            Map<String, SchemaConfig> newSchemas = newConfig.getSchemas();
            Map<String, ShardingNode> newShardingNodes = newConfig.getShardingNodes();
            Map<String, ApNode> newApNodes = newConfig.getApNodes();
            Map<ERTable, Set<ERTable>> newErRelations = newConfig.getErRelations();
            Map<String, Set<ERTable>> newFuncNodeERMap = newConfig.getFuncNodeERMap();
            Map<String, Properties> newBlacklistConfig = newConfig.getBlacklistConfig();
            Map<String, AbstractPartitionAlgorithm> newFunctions = newConfig.getFunctions();


            // start/stop connection pool && heartbeat
            // replace config
            ReloadLogHelper.briefInfo("apply new config start");
            ServerConfig oldConfig = DbleServer.getInstance().getConfig();
            boolean result;
            try {
                result = oldConfig.reload(newUsers, newSchemas, newShardingNodes, newApNodes, newDbGroups, oldConfig.getDbGroups(), newErRelations, newFuncNodeERMap,
                        newSystemVariables, loader.isFullyConfigured(), loadAllMode, newBlacklistConfig, newFunctions,
                        loader.getUserConfig(), loader.getSequenceConfig(), loader.getShardingConfig(), loader.getDbConfig(), changeItemList);
                CronScheduler.getInstance().init(oldConfig.getSchemas());
                if (!result) {
                    initFailed(newDbGroups);
                }
                FrontendUserManager.getInstance().changeUser(changeItemList, SystemConfig.getInstance().getMaxCon());
                ReloadLogHelper.briefInfo("apply new config end");
                // recycle old active conn
                recycleOldBackendConnections(!forceAllReload, (loadAllMode & ManagerParseConfig.OPTF_MODE) != 0);
                if (!loader.isFullyConfigured()) {
                    recycleServerConnections();
                }
                return packReloadResult(result, changeItemList, forceAllReload, newDbGroups, oldConfig.getDbGroups());
            } catch (Exception e) {
                initFailed(newDbGroups);
                throw e;
            }
        } finally {
            TraceManager.finishSpan(traceObject);
        }
    }

    private static ReloadResult packReloadResult(boolean result, List<ChangeItem> changeItemList,
                                                 boolean forceAllReload,
                                                 Map<String, PhysicalDbGroup> newDbGroups,
                                                 Map<String, PhysicalDbGroup> oldDbGroups) {
        if (forceAllReload) {
            return new ReloadResult(result, newDbGroups, oldDbGroups);
        } else {
            Map<String, PhysicalDbGroup> addOrChangeMap0 = new HashMap<>();
            Map<String, PhysicalDbGroup> recycleMap0 = new HashMap<>();
            for (ChangeItem changeItem : changeItemList) {
                if (changeItem.getItemType() == ChangeItemType.PHYSICAL_DB_GROUP) {
                    PhysicalDbGroup dbGroup = ((PhysicalDbGroup) changeItem.getItem());
                    switch (changeItem.getType()) {
                        case ADD:
                        case UPDATE:
                            addOrChangeMap0.put(dbGroup.getGroupName(), dbGroup);
                            break;
                        case DELETE:
                            recycleMap0.put(dbGroup.getGroupName(), dbGroup);
                            break;
                        default:
                            break;
                    }
                }
            }
            return new ReloadResult(result, addOrChangeMap0, recycleMap0);
        }
    }

    /**
     * check version/packetSize/lowerCase
     * get system variables
     */
    private static SystemVariables checkVersionAGetSystemVariables(ConfigInitializer loader, Map<String, PhysicalDbGroup> newDbGroups, List<ChangeItem> changeItemList, boolean forceAllReload) throws Exception {
        ReloadLogHelper.briefInfo("check and get system variables from random node start");
        SystemVariables newSystemVariables;
        if (forceAllReload || ConfigUtil.isAllDbInstancesChange(changeItemList, newDbGroups)) {
            //check version
            ConfigUtil.checkDbleAndMysqlVersion(newDbGroups);
            //check packetSize/lowerCase
            ConfigUtil.getAndSyncKeyVariables(newDbGroups, true);
            //get system variables
            newSystemVariables = getSystemVariablesFromdbGroup(loader, newDbGroups);
        } else {
            //check version
            ConfigUtil.checkDbleAndMysqlVersion(changeItemList, loader);
            //check packetSize/lowerCase
            ConfigUtil.getAndSyncKeyVariables(changeItemList, true);
            //keep the original system variables
            SystemVariables memorySystemVariables = DbleServer.getInstance().getSystemVariables();
            SystemVariables currentSystemVariables = getSystemVariablesFromdbGroup(loader, loader.getDbGroups());
            int memoryLowerCase = memorySystemVariables.isLowerCaseTableNames() ? 1 : 0;
            if (memorySystemVariables.isLowerCaseTableNames() != currentSystemVariables.isLowerCaseTableNames()) { // check if the lowerCase of the backend has changed
                throw new Exception("Dble memory's lowercase value is " + memoryLowerCase + ", " +
                        "But it was found that the lower_case_table_names value of the dbInstance is not " + memoryLowerCase + ". " +
                        "Please unify dbInstances's lower_case_table_names or use 'reload @@config_all -r;'");
            } else {
                newSystemVariables = memorySystemVariables;
            }
        }
        ReloadLogHelper.briefInfo("check and get system variables from random node end");
        return newSystemVariables;
    }


    /**
     * test connection
     */
    private static void testConnection(ConfigInitializer loader, List<ChangeItem> changeItemList, boolean forceAllReload, int loadAllMode) throws Exception {
        ReloadLogHelper.briefInfo("test connection start");
        try {
            //test connection
            if (forceAllReload && loader.isFullyConfigured()) {
                loader.testConnection();
            } else {
                syncShardingAndApNode(loader);
                loader.testConnection(changeItemList);
            }
        } catch (Exception e) {
            if ((loadAllMode & ManagerParseConfig.OPTS_MODE) == 0 && loader.isFullyConfigured()) {
                //default/-f/-r
                throw new Exception(e);
            } else {
                //-s
                ReloadLogHelper.debug("just test, not stop reload, catch exception", e);
            }
        }
        ReloadLogHelper.briefInfo("test connection end");
    }

    /**
     * compare change
     */
    private static List<ChangeItem> compareChange(ConfigInitializer loader) {
        ReloadLogHelper.briefInfo("compare changes start");
        List<ChangeItem> changeItemList = differentiateChanges(loader);
        ReloadLogHelper.debug("change items :{}", changeItemList);
        ReloadLogHelper.briefInfo("compare changes end");
        return changeItemList;
    }

    /**
     * load configuration
     * xml:db.xml/user.xml/sharding.xml/sequence
     * memory:dble_information/cluster synchronization
     */
    private static ConfigInitializer loadConfig(RawJson userConfig, RawJson dbConfig, RawJson shardingConfig, RawJson sequenceConfig) throws Exception {
        ConfigInitializer loader;
        try {
            if (null == userConfig && null == dbConfig && null == shardingConfig && null == sequenceConfig) {
                ReloadLogHelper.briefInfo("load config start [local xml]");
                loader = new ConfigInitializer();
            } else {
                ReloadLogHelper.briefInfo("load config start [memory]");
                ReloadLogHelper.debug("memory to Users is :{}\r\n" +
                        "memory to DbGroups is :{}\r\n" +
                        "memory to Shardings is :{}\r\n" +
                        "memory to sequence is :{}", userConfig, dbConfig, shardingConfig, sequenceConfig);
                loader = new ConfigInitializer(userConfig, dbConfig, shardingConfig, sequenceConfig);
            }
            ReloadLogHelper.briefInfo("load config end");
            return loader;
        } catch (Exception e) {
            throw new Exception(e.getMessage() == null ? e.toString() : e.getMessage(), e);
        }
    }

    private static void syncShardingAndApNode(ConfigInitializer loader) {
        // sync schema_exists(only testConn can update schema_exists)
        Map<String, ShardingNode> oldShardingNodeMap = DbleServer.getInstance().getConfig().getShardingNodes();
        Map<String, ShardingNode> newShardingNodeMap = loader.getShardingNodes();
        for (Map.Entry<String, ShardingNode> shardingNodeEntry : newShardingNodeMap.entrySet()) {
            if (oldShardingNodeMap.containsKey(shardingNodeEntry.getKey())) {
                shardingNodeEntry.getValue().setSchemaExists(oldShardingNodeMap.get(shardingNodeEntry.getKey()).isSchemaExists());
            }
        }

        Map<String, ApNode> oldApNodeMap = DbleServer.getInstance().getConfig().getApNodes();
        Map<String, ApNode> newApNodeMap = loader.getApNodes();
        for (Map.Entry<String, ApNode> apNodeEntry : newApNodeMap.entrySet()) {
            if (oldApNodeMap.containsKey(apNodeEntry.getKey())) {
                apNodeEntry.getValue().setSchemaExists(oldApNodeMap.get(apNodeEntry.getKey()).isSchemaExists());
            }
        }

    }

    private static List<ChangeItem> differentiateChanges(ConfigInitializer newLoader) {
        List<ChangeItem> changeItemList = Lists.newArrayList();
        //user
        //old
        ServerConfig oldServerConfig = DbleServer.getInstance().getConfig();
        Map<UserName, UserConfig> oldUserMap = oldServerConfig.getUsers();
        //new
        Map<UserName, UserConfig> newUserMap = newLoader.getUsers();
        MapDifference<UserName, UserConfig> userMapDifference = Maps.difference(newUserMap, oldUserMap);
        //delete
        userMapDifference.entriesOnlyOnRight().keySet().stream().map(username -> new ChangeItem(ChangeType.DELETE, username, ChangeItemType.USERNAME)).forEach(changeItemList::add);
        //add
        userMapDifference.entriesOnlyOnLeft().keySet().stream().map(username -> new ChangeItem(ChangeType.ADD, username, ChangeItemType.USERNAME)).forEach(changeItemList::add);
        //update
        userMapDifference.entriesDiffering().entrySet().stream().map(differenceEntry -> {
            UserConfig newUserConfig = differenceEntry.getValue().leftValue();
            UserConfig oldUserConfig = differenceEntry.getValue().rightValue();
            ChangeItem changeItem = new ChangeItem(ChangeType.UPDATE, differenceEntry.getKey(), ChangeItemType.USERNAME);
            if (newUserConfig instanceof RwSplitUserConfig && oldUserConfig instanceof RwSplitUserConfig) {
                if (!((RwSplitUserConfig) newUserConfig).getDbGroup().equals(((RwSplitUserConfig) oldUserConfig).getDbGroup())) {
                    changeItem.setAffectEntryDbGroup(true);
                }
            }
            return changeItem;
        }).forEach(changeItemList::add);

        //shardingNode
        Map<String, ShardingNode> oldShardingNodeMap = oldServerConfig.getShardingNodes();
        Map<String, ShardingNode> newShardingNodeMap = newLoader.getShardingNodes();
        MapDifference<String, ShardingNode> shardingNodeMapDiff = Maps.difference(newShardingNodeMap, oldShardingNodeMap);
        //delete
        shardingNodeMapDiff.entriesOnlyOnRight().values().stream().map(sharingNode -> new ChangeItem(ChangeType.DELETE, sharingNode, ChangeItemType.SHARDING_NODE)).forEach(changeItemList::add);
        //add
        shardingNodeMapDiff.entriesOnlyOnLeft().values().stream().map(sharingNode -> new ChangeItem(ChangeType.ADD, sharingNode, ChangeItemType.SHARDING_NODE)).forEach(changeItemList::add);
        //update
        shardingNodeMapDiff.entriesDiffering().entrySet().stream().map(differenceEntry -> {
            ShardingNode newShardingNode = differenceEntry.getValue().leftValue();
            ChangeItem changeItem = new ChangeItem(ChangeType.UPDATE, newShardingNode, ChangeItemType.SHARDING_NODE);
            return changeItem;
        }).forEach(changeItemList::add);

        //apNode
        Map<String, ApNode> oldApNodeMap = oldServerConfig.getApNodes();
        Map<String, ApNode> newApNodeMap = newLoader.getApNodes();
        MapDifference<String, ApNode> apNodeMapDiff = Maps.difference(newApNodeMap, oldApNodeMap);
        //delete
        apNodeMapDiff.entriesOnlyOnRight().values().stream().map(apNode -> new ChangeItem(ChangeType.DELETE, apNode, ChangeItemType.AP_NODE)).forEach(changeItemList::add);
        //add
        apNodeMapDiff.entriesOnlyOnLeft().values().stream().map(apNode -> new ChangeItem(ChangeType.ADD, apNode, ChangeItemType.AP_NODE)).forEach(changeItemList::add);
        //update
        apNodeMapDiff.entriesDiffering().entrySet().stream().map(differenceEntry -> {
            ApNode newApNode = differenceEntry.getValue().leftValue();
            ChangeItem changeItem = new ChangeItem(ChangeType.UPDATE, newApNode, ChangeItemType.AP_NODE);
            return changeItem;
        }).forEach(changeItemList::add);

        //dbGroup
        Map<String, PhysicalDbGroup> oldDbGroupMap = oldServerConfig.getDbGroups();
        Map<String, PhysicalDbGroup> newDbGroupMap = newLoader.getDbGroups();
        Map<String, PhysicalDbGroup> removeDbGroup = new LinkedHashMap<>(oldDbGroupMap);
        for (Map.Entry<String, PhysicalDbGroup> newDbGroupEntry : newDbGroupMap.entrySet()) {
            PhysicalDbGroup oldDbGroup = oldDbGroupMap.get(newDbGroupEntry.getKey());
            PhysicalDbGroup newDbGroup = newDbGroupEntry.getValue();

            if (null == oldDbGroup) {
                //add dbGroup
                changeItemList.add(new ChangeItem(ChangeType.ADD, newDbGroup, ChangeItemType.PHYSICAL_DB_GROUP));
            } else {
                removeDbGroup.remove(newDbGroupEntry.getKey());
                //change dbGroup
                if (!newDbGroup.equalsBaseInfo(oldDbGroup)) {
                    ChangeItem changeItem = new ChangeItem(ChangeType.UPDATE, newDbGroup, ChangeItemType.PHYSICAL_DB_GROUP);
                    if (!newDbGroup.equalsForConnectionPool(oldDbGroup)) {
                        changeItem.setAffectConnectionPool(true);
                    }
                    if (!newDbGroup.equalsForHeartbeat(oldDbGroup)) {
                        changeItem.setAffectHeartbeat(true);
                    }
                    if (!newDbGroup.equalsForDelayDetection(oldDbGroup)) {
                        changeItem.setAffectDelayDetection(true);
                    }
                    changeItemList.add(changeItem);
                }

                //dbInstance
                Map<String, PhysicalDbInstance> newDbInstanceMap = newDbGroup.getAllDbInstanceMap();
                Map<String, PhysicalDbInstance> oldDbInstanceMap = oldDbGroup.getAllDbInstanceMap();

                MapDifference<String, PhysicalDbInstance> dbInstanceMapDifference = Maps.difference(newDbInstanceMap, oldDbInstanceMap);
                //delete
                dbInstanceMapDifference.entriesOnlyOnRight().values().stream().map(dbInstance -> new ChangeItem(ChangeType.DELETE, dbInstance, PHYSICAL_DB_INSTANCE)).forEach(changeItemList::add);
                //add
                dbInstanceMapDifference.entriesOnlyOnLeft().values().stream().map(dbInstance -> new ChangeItem(ChangeType.ADD, dbInstance, PHYSICAL_DB_INSTANCE)).forEach(changeItemList::add);
                //update
                dbInstanceMapDifference.entriesDiffering().values().stream().map(physicalDbInstanceValueDifference -> {
                    PhysicalDbInstance newDbInstance = physicalDbInstanceValueDifference.leftValue();
                    PhysicalDbInstance oldDbInstance = physicalDbInstanceValueDifference.rightValue();
                    ChangeItem changeItem = new ChangeItem(ChangeType.UPDATE, newDbInstance, PHYSICAL_DB_INSTANCE);
                    if (!newDbInstance.equalsForConnectionPool(oldDbInstance)) {
                        changeItem.setAffectConnectionPool(true);
                    }
                    if (!newDbInstance.equalsForPoolCapacity(oldDbInstance)) {
                        changeItem.setAffectPoolCapacity(true);
                    }
                    if (!newDbInstance.equalsForHeartbeat(oldDbInstance)) {
                        changeItem.setAffectHeartbeat(true);
                    }
                    if (!newDbInstance.equalsForDelayDetection(oldDbInstance)) {
                        changeItem.setAffectDelayDetection(true);
                    }
                    if (!newDbInstance.equalsForTestConn(oldDbInstance)) {
                        changeItem.setAffectTestConn(true);
                    } else {
                        newDbInstance.setTestConnSuccess(oldDbInstance.isTestConnSuccess());
                        newDbInstance.setDsVersion(oldDbInstance.getDsVersion());
                    }
                    return changeItem;
                }).forEach(changeItemList::add);
                //testConnSuccess with both
                for (Map.Entry<String, PhysicalDbInstance> dbInstanceEntry : dbInstanceMapDifference.entriesInCommon().entrySet()) {
                    dbInstanceEntry.getValue().setTestConnSuccess(oldDbInstanceMap.get(dbInstanceEntry.getKey()).isTestConnSuccess());
                    dbInstanceEntry.getValue().setDsVersion(oldDbInstanceMap.get(dbInstanceEntry.getKey()).getDsVersion());
                }

            }
        }
        for (Map.Entry<String, PhysicalDbGroup> entry : removeDbGroup.entrySet()) {
            PhysicalDbGroup value = entry.getValue();
            changeItemList.add(new ChangeItem(ChangeType.DELETE, value, ChangeItemType.PHYSICAL_DB_GROUP));
        }

        return changeItemList;
    }

    private static void recycleOldBackendConnections(boolean forceAllReload, boolean closeFrontCon) {
        if (forceAllReload && closeFrontCon) {
            TraceManager.TraceObject traceObject = TraceManager.threadTrace("recycle-activeBackend-connections");
            ReloadLogHelper.briefInfo("recycle old active backend connections start");
            try {
                for (IOProcessor processor : DbleServer.getInstance().getBackendProcessors()) {
                    for (BackendConnection con : processor.getBackends().values()) {
                        if (con.getPoolDestroyedTime() != 0) {
                            con.closeWithFront("old active backend conn will be forced closed by closing front conn");
                        }
                    }
                }
                ReloadLogHelper.briefInfo("recycle old active backend connections end");
            } finally {
                TraceManager.finishSpan(traceObject);
            }
        } else {
            ReloadLogHelper.briefInfo("skip recycle old active backend connections");
        }

    }


    private static void initFailed(Map<String, PhysicalDbGroup> newDbGroups) {
        // INIT FAILED
        ReloadLogHelper.briefInfo("reload failed, clear previously created dbInstances ");
        for (PhysicalDbGroup dbGroup : newDbGroups.values()) {
            dbGroup.stop("reload fail, stop");
        }
    }

    private static SystemVariables getSystemVariablesFromdbGroup(ConfigInitializer loader, Map<String, PhysicalDbGroup> newDbGroups) throws Exception {
        VarsExtractorHandler handler = new VarsExtractorHandler(newDbGroups);
        SystemVariables newSystemVariables;
        newSystemVariables = handler.execute();
        if (newSystemVariables == null) {
            if (loader.isFullyConfigured()) {
                throw new Exception("Can't get variables from any dbInstance, because all of dbGroup can't connect to MySQL correctly");
            } else {
                ReloadLogHelper.briefInfo("no valid dbGroup ,keep variables as old");
                newSystemVariables = DbleServer.getInstance().getSystemVariables();
            }
        }
        return newSystemVariables;
    }

    private static void recycleServerConnections() {
        TraceManager.TraceObject traceObject = TraceManager.threadTrace("recycle-sharding-connections");
        ReloadLogHelper.briefInfo("recycle front connection start");
        try {
            for (IOProcessor processor : DbleServer.getInstance().getFrontProcessors()) {
                for (FrontendConnection fcon : processor.getFrontends().values()) {
                    if (!fcon.isManager()) {
                        ReloadLogHelper.debug("recycle front connection:{}", fcon);
                        fcon.close("Reload causes the service to stop");
                    }
                }
            }
            ReloadLogHelper.briefInfo("recycle front connection end");
        } finally {
            TraceManager.finishSpan(traceObject);
        }
    }

    private static void writePacket(boolean isSuccess, ManagerService service, String errorMsg, int errorCode) {
        if (isSuccess) {
            OkPacket ok = new OkPacket();
            ok.setPacketId(1);
            ok.setAffectedRows(1);
            ok.setServerStatus(2);
            ok.setMessage("Reload config success".getBytes());
            ok.write(service.getConnection());
        } else {
            service.writeErrMessage(errorCode, "Reload Failure, The reason is " + errorMsg);
        }
    }

    public static class ReloadResult { // dbGroup
        private final boolean success;
        private final Map<String, PhysicalDbGroup> addOrChangeHostMap;
        private final Map<String, PhysicalDbGroup> recycleHostMap;

        public ReloadResult(boolean success, Map<String, PhysicalDbGroup> addOrChangeHostMap, Map<String, PhysicalDbGroup> recycleHostMap) {
            this.success = success;
            this.addOrChangeHostMap = addOrChangeHostMap;
            this.recycleHostMap = recycleHostMap;
        }

        public boolean isSuccess() {
            return success;
        }

        public Map<String, PhysicalDbGroup> getAddOrChangeHostMap() {
            return addOrChangeHostMap;
        }

        public Map<String, PhysicalDbGroup> getRecycleHostMap() {
            return recycleHostMap;
        }
    }
}
