/**
 * Copyright 2014-2020 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package com.webank.webase.front.web3api;

import com.webank.webase.front.base.code.ConstantCode;
import com.webank.webase.front.base.config.NodeConfig;
import com.webank.webase.front.base.config.Web3Config;
import com.webank.webase.front.base.enums.DataStatus;
import com.webank.webase.front.base.exception.FrontException;
import com.webank.webase.front.base.properties.Constants;
import com.webank.webase.front.base.response.BaseResponse;
import com.webank.webase.front.util.CommonUtils;
import com.webank.webase.front.util.JsonUtils;
import com.webank.webase.front.web3api.entity.GenerateGroupInfo;
import com.webank.webase.front.web3api.entity.GroupOperateStatus;
import com.webank.webase.front.web3api.entity.NodeStatusInfo;
import com.webank.webase.front.web3api.entity.PeerOfConsensusStatus;
import com.webank.webase.front.web3api.entity.PeerOfSyncStatus;
import com.webank.webase.front.web3api.entity.SyncStatus;
import java.io.IOException;
import java.math.BigInteger;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.fisco.bcos.channel.handler.ChannelConnections;
import org.fisco.bcos.channel.handler.GroupChannelConnectionsConfig;
import org.fisco.bcos.web3j.protocol.Web3j;
import org.fisco.bcos.web3j.protocol.channel.ChannelEthereumService;
import org.fisco.bcos.web3j.protocol.core.DefaultBlockParameter;
import org.fisco.bcos.web3j.protocol.core.methods.response.BcosBlock;
import org.fisco.bcos.web3j.protocol.core.methods.response.GroupPeers;
import org.fisco.bcos.web3j.protocol.core.methods.response.NodeVersion.Version;
import org.fisco.bcos.web3j.protocol.core.methods.response.Peers;
import org.fisco.bcos.web3j.protocol.core.methods.response.TotalTransactionCount;
import org.fisco.bcos.web3j.protocol.core.methods.response.Transaction;
import org.fisco.bcos.web3j.protocol.core.methods.response.TransactionReceipt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.DependsOn;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;

/**
 * Web3Api manage.
 */
@Slf4j
@Service
public class Web3ApiService {

    @Autowired
    Map<Integer, Web3j> web3jMap;
    @Autowired
    NodeConfig nodeConfig;
    @Autowired
    GroupChannelConnectionsConfig groupChannelConnectionsConfig;
    @Autowired
    ThreadPoolTaskExecutor threadPoolTaskExecutor;
    @Autowired
    Constants constants;
    @Autowired
    Web3Config web3Config;
    @Autowired
    Web3j independentWeb3j;

    private static Map<Integer, List<NodeStatusInfo>> nodeStatusMap = new HashMap<>();
    private static final Long CHECK_NODE_WAIT_MIN_MILLIS = 5000L;
    private static final int HASH_OF_TRANSACTION_LENGTH = 66;

    /**
     * getBlockNumber.
     */
    public BigInteger getBlockNumber(int groupId) {

        BigInteger blockNumber;
        try {
            blockNumber = getWeb3j(groupId).getBlockNumber().send().getBlockNumber();
        } catch (IOException e) {
            log.error("getBlockNumber fail.", e);
            throw new FrontException(ConstantCode.NODE_REQUEST_FAILED);
        }
        return blockNumber;
    }

    /**
     * getBlockByNumber.
     *
     * @param blockNumber blockNumber
     */
    public BcosBlock.Block getBlockByNumber(int groupId, BigInteger blockNumber) {
        if (blockNumberCheck(groupId, blockNumber)) {
            throw new FrontException(ConstantCode.BLOCK_NUMBER_ERROR);
        }
        BcosBlock.Block block;
        try {
            block = getWeb3j(groupId)
                    .getBlockByNumber(DefaultBlockParameter.valueOf(blockNumber), true)
                    .send()
                    .getBlock();
        } catch (IOException e) {
            log.info("get blocknumber failed" + e.getMessage());
            log.error("getBlAockByNumber fail. blockNumber:{} , groupID: {}", blockNumber, groupId);
            throw new FrontException(ConstantCode.NODE_REQUEST_FAILED);
        }
        return block;
    }

    /**
     * getBlockByHash.
     *
     * @param blockHash blockHash
     */
    public BcosBlock.Block getBlockByHash(int groupId, String blockHash) {
        BcosBlock.Block block;
        try {

            block = getWeb3j(groupId).getBlockByHash(blockHash, true)
                    .send()
                    .getBlock();
        } catch (IOException e) {
            log.error("getBlockByHash fail. blockHash:{} ", blockHash);
            throw new FrontException(ConstantCode.NODE_REQUEST_FAILED);
        }
        return block;
    }

    /**
     * getBlockTransCntByNumber.
     *
     * @param blockNumber blockNumber
     */
    public int getBlockTransCntByNumber(int groupId, BigInteger blockNumber) {
        int transCnt;
        try {
            if (blockNumberCheck(groupId, blockNumber)) {
                throw new FrontException("ConstantCode.NODE_REQUEST_FAILED");
            }
            BcosBlock.Block block = getWeb3j(groupId)
                    .getBlockByNumber(DefaultBlockParameter.valueOf(blockNumber), true)
                    .send()
                    .getBlock();
            transCnt = block.getTransactions().size();
        } catch (IOException e) {
            log.error("getBlockTransCntByNumber fail. blockNumber:{} ", blockNumber);
            throw new FrontException(ConstantCode.NODE_REQUEST_FAILED);
        }
        return transCnt;
    }

    /**
     * getPbftView.
     */
    public BigInteger getPbftView(int groupId) {

        BigInteger result;
        try {
            result = getWeb3j(groupId).getPbftView().send().getPbftView();
        } catch (IOException e) {
            log.error("getPbftView fail.");
            throw new FrontException(ConstantCode.NODE_REQUEST_FAILED);
        }
        return result;
    }

    /**
     * getTransactionReceipt.
     *
     * @param transHash transHash
     */
    public TransactionReceipt getTransactionReceipt(int groupId, String transHash) {

        TransactionReceipt transactionReceipt = null;
        try {
            Optional<TransactionReceipt> opt = getWeb3j(groupId)
                    .getTransactionReceipt(transHash).send().getTransactionReceipt();
            if (opt.isPresent()) {
                transactionReceipt = opt.get();
            }
        } catch (IOException e) {
            log.error("getTransactionReceipt fail. transHash:{} ", transHash);
            throw new FrontException(ConstantCode.NODE_REQUEST_FAILED);
        }
        return transactionReceipt;
    }

    /**
     * getTransactionByHash.
     *
     * @param transHash transHash
     */
    public Transaction getTransactionByHash(int groupId, String transHash) {

        Transaction transaction = null;
        try {
            Optional<Transaction> opt =
                    getWeb3j(groupId).getTransactionByHash(transHash).send().getTransaction();
            if (opt.isPresent()) {
                transaction = opt.get();
            }
        } catch (IOException e) {
            log.error("getTransactionByHash fail. transHash:{} ", transHash);
            throw new FrontException(ConstantCode.NODE_REQUEST_FAILED);
        }
        return transaction;
    }

    /**
     * getClientVersion.
     */
    public Version getClientVersion() {
        Version version;
        try {
            version = getWeb3j().getNodeVersion().send().getNodeVersion();
        } catch (IOException e) {
            log.error("getClientVersion fail.", e);
            throw new FrontException(ConstantCode.NODE_REQUEST_FAILED);
        }
        return version;
    }

    /**
     * getCode.
     *
     * @param address address
     * @param blockNumber blockNumber
     */
    public String getCode(int groupId, String address, BigInteger blockNumber) {
        String code;
        try {
            if (blockNumberCheck(groupId, blockNumber)) {
                throw new FrontException(ConstantCode.BLOCK_NUMBER_ERROR);
            }
            code = getWeb3j(groupId)
                    .getCode(address, DefaultBlockParameter.valueOf(blockNumber)).send().getCode();
        } catch (IOException e) {
            log.error("getCode fail.", e);
            throw new FrontException(ConstantCode.NODE_REQUEST_FAILED);
        }
        return code;
    }

    /**
     * get transaction counts.
     */
    public TotalTransactionCount.TransactionCount getTransCnt(int groupId) {
        TotalTransactionCount.TransactionCount transactionCount;
        try {
            transactionCount = getWeb3j(groupId)
                    .getTotalTransactionCount().send()
                    .getTotalTransactionCount();
        } catch (IOException e) {
            log.error("getTransCnt fail.", e);
            throw new FrontException(ConstantCode.NODE_REQUEST_FAILED);
        }
        return transactionCount;
    }

    /**
     * getTransByBlockHashAndIndex.
     *
     * @param blockHash blockHash
     * @param transactionIndex index
     */
    public Transaction getTransByBlockHashAndIndex(int groupId, String blockHash,
                                                   BigInteger transactionIndex) {

        Transaction transaction = null;
        try {
            Optional<Transaction> opt = getWeb3j(groupId)
                    .getTransactionByBlockHashAndIndex(blockHash, transactionIndex).send()
                    .getTransaction();
            if (opt.isPresent()) {
                transaction = opt.get();
            }
        } catch (IOException e) {
            log.error("getTransByBlockHashAndIndex fail.", e);
            throw new FrontException(ConstantCode.NODE_REQUEST_FAILED);
        }
        return transaction;
    }

    /**
     * getTransByBlockNumberAndIndex.
     *
     * @param blockNumber blockNumber
     * @param transactionIndex index
     */
    public Transaction getTransByBlockNumberAndIndex(int groupId, BigInteger blockNumber,
                                                     BigInteger transactionIndex) {
        Transaction transaction = null;
        try {
            if (blockNumberCheck(groupId, blockNumber)) {
                throw new FrontException("ConstantCode.NODE_REQUEST_FAILED");
            }
            Optional<Transaction> opt =
                    getWeb3j(groupId)
                            .getTransactionByBlockNumberAndIndex(
                                    DefaultBlockParameter.valueOf(blockNumber), transactionIndex)
                            .send().getTransaction();
            if (opt.isPresent()) {
                transaction = opt.get();
            }
        } catch (IOException e) {
            log.error("getTransByBlockNumberAndIndex fail.", e);
            throw new FrontException(ConstantCode.NODE_REQUEST_FAILED);
        }
        return transaction;
    }

    private boolean blockNumberCheck(int groupId, BigInteger blockNumber) {
        BigInteger currentNumber = null;
        try {
            currentNumber = getWeb3j(groupId).getBlockNumber().send().getBlockNumber();
        } catch (IOException e) {
            log.error("blockNumberCheck error:{}", e.getMessage());
        }
        log.info("**** currentNumber:{}", currentNumber);
        return (blockNumber.compareTo(currentNumber) > 0);

    }


    /**
     * nodeHeartBeat.
     */
    public List<NodeStatusInfo> getNodeStatusList(int groupId) {
        log.info("start getNodeStatusList. groupId:{}", groupId);
        try {
            List<NodeStatusInfo> statusList = new ArrayList<>();
            List<String> peerStrList = getGroupPeers(groupId);
            List<String> observerList = getObserverList(groupId);
            SyncStatus syncStatus = JsonUtils.toJavaObject(getSyncStatus(groupId), SyncStatus.class);
            List<PeerOfConsensusStatus> consensusList = getPeerOfConsensusStatus(groupId);
            if (Objects.isNull(peerStrList) || peerStrList.isEmpty() || consensusList == null) {
                log.info("end getNodeStatusList. peerStrList is empty");
                return Collections.emptyList();
            }
            for (String peer : peerStrList) {
                int nodeType = 0; // 0-consensus;1-observer
                if (observerList != null) {
                    nodeType = observerList.stream().filter(observer -> peer.equals(observer))
                            .map(c -> 1).findFirst().orElse(0);
                }
                BigInteger blockNumberOnChain = getBlockNumberOfNodeOnChain(syncStatus, peer);
                BigInteger latestView =
                        consensusList.stream().filter(cl -> peer.equals(cl.getNodeId()))
                                .map(c -> c.getView()).findFirst().orElse(BigInteger.ZERO);// pbftView
                // check node status
                statusList.add(
                        checkNodeStatus(groupId, peer, blockNumberOnChain, latestView, nodeType));
            }

            nodeStatusMap.put(groupId, statusList);
            log.info("end getNodeStatusList. groupId:{} statusList:{}", groupId,
                    JsonUtils.toJSONString(statusList));
            return statusList;
        } catch (Exception e) {
            log.error("nodeHeartBeat Exception.", e);
            throw new FrontException(ConstantCode.NODE_REQUEST_FAILED);
        }
    }

    /**
     * check node status.
     */
    private NodeStatusInfo checkNodeStatus(int groupId, String nodeId, BigInteger chainBlockNumber,
                                           BigInteger chainView, int nodeType) {
        log.info("start checkNodeStatus. groupId:{} nodeId:{} blockNumber:{} chainView:{}", groupId,
                nodeId, chainBlockNumber, chainView);

        if (Objects.isNull(nodeStatusMap.get(groupId))) {
            log.info("end checkNodeStatus. no cache group:{}", groupId);
            return new NodeStatusInfo(nodeId, chainBlockNumber, chainView,
                    DataStatus.NORMAL.getValue(), LocalDateTime.now());
        } else {
            List<NodeStatusInfo> statusList = nodeStatusMap.get(groupId);
            NodeStatusInfo localNodeStatus = statusList.stream()
                    .filter(s -> nodeId.equals(s.getNodeId())).findFirst().orElse(null);
            if (Objects.isNull(localNodeStatus)) {
                log.info("end checkNodeStatus. no cache node:{}", nodeId);
                return new NodeStatusInfo(nodeId, chainBlockNumber, chainView,
                        DataStatus.NORMAL.getValue(), LocalDateTime.now());
            }

            LocalDateTime latestUpdate = localNodeStatus.getLatestStatusUpdateTime();
            Long subTime = Duration.between(latestUpdate, LocalDateTime.now()).toMillis();
            if (subTime < CHECK_NODE_WAIT_MIN_MILLIS) {
                log.info("checkNodeStatus jump over. nodeId:{} subTime:{}", nodeId, subTime);
                return localNodeStatus;
            }

            BigInteger localBlockNumber = localNodeStatus.getBlockNumber();
            BigInteger localPbftView = localNodeStatus.getPbftView();
            // 0-consensus;1-observer
            if (nodeType == 0) {
                if (localBlockNumber.equals(chainBlockNumber) && localPbftView.equals(chainView)) {
                    log.warn(
                            "node[{}] is invalid. localNumber:{} chainNumber:{} localView:{} chainView:{}",
                            nodeId, localBlockNumber, chainBlockNumber, localPbftView, chainView);
                    localNodeStatus.setStatus(DataStatus.INVALID.getValue());
                } else {
                    localNodeStatus.setBlockNumber(chainBlockNumber);
                    localNodeStatus.setPbftView(chainView);
                    localNodeStatus.setStatus(DataStatus.NORMAL.getValue());
                }
            } else {
                if (!chainBlockNumber.equals(getBlockNumber(groupId))) {
                    log.warn(
                            "node[{}] is invalid. localNumber:{} chainNumber:{} localView:{} chainView:{}",
                            nodeId, localBlockNumber, chainBlockNumber, localPbftView, chainView);
                    localNodeStatus.setStatus(DataStatus.INVALID.getValue());
                } else {
                    localNodeStatus.setBlockNumber(chainBlockNumber);
                    localNodeStatus.setPbftView(chainView);
                    localNodeStatus.setStatus(DataStatus.NORMAL.getValue());
                }
            }
            localNodeStatus.setLatestStatusUpdateTime(LocalDateTime.now());
            return localNodeStatus;
        }
    }


    /**
     * get latest number of peer on chain.
     */
    private BigInteger getBlockNumberOfNodeOnChain(SyncStatus syncStatus, String nodeId) {
        if (Objects.isNull(syncStatus)) {
            log.warn("fail getBlockNumberOfNodeOnChain. SyncStatus is null");
            return BigInteger.ZERO;
        }
        if (StringUtils.isBlank(nodeId)) {
            log.warn("fail getBlockNumberOfNodeOnChain. nodeId is null");
            return BigInteger.ZERO;
        }
        if (nodeId.equals(syncStatus.getNodeId())) {
            return syncStatus.getBlockNumber();
        }
        List<PeerOfSyncStatus> peerList = syncStatus.getPeers();
        // blockNumber
        BigInteger latestNumber = peerList.stream().filter(peer -> nodeId.equals(peer.getNodeId()))
                .map(s -> s.getBlockNumber()).findFirst().orElse(BigInteger.ZERO);
        return latestNumber;
    }


    /**
     * get peer of consensusStatus
     */
    private List<PeerOfConsensusStatus> getPeerOfConsensusStatus(int groupId) {
        String consensusStatusJson = getConsensusStatus(groupId);
        if (StringUtils.isBlank(consensusStatusJson)) {
            return Collections.emptyList();
        }
        List jsonArr = JsonUtils.toJavaObject(consensusStatusJson, List.class);
        if (jsonArr == null) {
            log.error("getPeerOfConsensusStatus error");
            throw new FrontException(ConstantCode.FAIL_PARSE_JSON);
        }
        List<PeerOfConsensusStatus> dataIsList = new ArrayList<>();
        for (int i = 0; i < jsonArr.size(); i++ ) {
            if (jsonArr.get(i) instanceof List) {
                List<PeerOfConsensusStatus> tempList = JsonUtils.toJavaObjectList(
                    JsonUtils.toJSONString(jsonArr.get(i)), PeerOfConsensusStatus.class);
                if (tempList != null) {
                    dataIsList.addAll(tempList);
                } else {
                    throw new FrontException(ConstantCode.FAIL_PARSE_JSON);
                }
            }
        }
        return dataIsList;
    }


    public List<String> getGroupPeers(int groupId) {
        GroupPeers groupPeers = null;
        try {
            groupPeers = getWeb3j(groupId)
                    .getGroupPeers().send();
        } catch (IOException e) {
            log.error("getGroupPeers error:[]", e);
            throw new FrontException(e.getMessage());
        }
        return groupPeers.getGroupPeers();
    }

    /**
     * get group list and refresh web3j map
     * @return
     */
    public List<String> getGroupList() {
        log.debug("getGroupList. ");
        try {
            List<String> groupIdList = getWeb3j().getGroupList().send().getGroupList();
            // check web3jMap, if not match groupIdList, refresh web3jMap in front
            refreshWeb3jMap(groupIdList);
            return groupIdList;
        } catch (IOException e) {
            log.error("getGroupList error:[]", e);
            throw new FrontException(e.getMessage());
        }
    }

    public List<String> getNodeIdList() {
        try {
            return getWeb3j()
                    .getNodeIDList().send()
                    .getNodeIDList();
        } catch (IOException e) {
            log.error("getNodeIDList error:[]", e);
            throw new FrontException(e.getMessage());
        }
    }

    /**
     * add web3j from chain and remove web3j not in chain
     * @param groupIdList
     * @throws FrontException
     */
    @DependsOn("encryptType")
    public void refreshWeb3jMap(List<String> groupIdList) throws FrontException {
        log.debug("refreshWeb3jMap groupIdList:{}", groupIdList);
        // if localGroupIdList not contain group in groupList from chain, add it
        groupIdList.stream()
            .filter(groupId ->
                web3jMap.get(Integer.parseInt(groupId)) == null)
            .forEach(group2Init ->
                initWeb3j(Integer.parseInt(group2Init)));

        Set<Integer> localGroupIdList = web3jMap.keySet();
        log.debug("refreshWeb3jMap localGroupList:{}", localGroupIdList);
        // if local web3j map contains group that not in groupList from chain
        // remove it from local web3j map
        localGroupIdList.stream()
            // not contains in groupList from chain
            .filter(groupId ->
                !groupIdList.contains(String.valueOf(groupId)))
            .forEach(group2Remove ->
                web3jMap.remove(group2Remove));
    }

    /**
     * init a new web3j of group id, add in groupChannelConnectionsConfig's connections
     * @param groupId
     * @return
     */
    private synchronized Web3j initWeb3j(int groupId) {
        log.info("initWeb3j of groupId:{}", groupId);
        List<ChannelConnections> channelConnectionsList =
                groupChannelConnectionsConfig.getAllChannelConnections();
        ChannelConnections channelConnections = new ChannelConnections();
        channelConnections.setConnectionsStr(channelConnectionsList.get(0).getConnectionsStr());
        channelConnections.setGroupId(groupId);
        channelConnectionsList.add(channelConnections);
        org.fisco.bcos.channel.client.Service service = new org.fisco.bcos.channel.client.Service();
        service.setOrgID(Web3Config.orgName);
        service.setGroupId(groupId);
        service.setThreadPool(threadPoolTaskExecutor);
        service.setAllChannelConnections(groupChannelConnectionsConfig);
        try {
            service.run();
        } catch (Exception e) {
            log.error("initWeb3j fail. groupId:{} error:[]", groupId, e);
            throw new FrontException("refresh web3j failed");
        }
        ChannelEthereumService channelEthereumService = new ChannelEthereumService();
        channelEthereumService.setTimeout(web3Config.getTimeout());
        channelEthereumService.setChannelService(service);
        Web3j web3j = Web3j.build(channelEthereumService, service.getGroupId());
        web3jMap.put(groupId, web3j);
        return web3j;
    }

    // get all peers of chain
    public List<Peers.Peer> getPeers(int groupId) {
        try {
            return getWeb3j(groupId)
                    .getPeers().send()
                    .getPeers();
        } catch (IOException e) {
            log.error("getPeers error:[]", e);
            throw new FrontException(e.getMessage());
        }
    }

    public String getConsensusStatus(int groupId) {
        try {
            return getWeb3j(groupId)
                    .getConsensusStatus().sendForReturnString();
        } catch (IOException e) {
            log.error("getConsensusStatus error:[]", e);
            throw new FrontException(e.getMessage());
        }
    }

    public String getSyncStatus(int groupId) {
        try {
            return getWeb3j(groupId)
                    .getSyncStatus().sendForReturnString();
        } catch (IOException e) {
            log.error("getSyncStatus error:[]", e);
            throw new FrontException(e.getMessage());
        }
    }

    public String getSystemConfigByKey(int groupId, String key) {
        try {
            return getWeb3j(groupId)
                    .getSystemConfigByKey(key).send()
                    .getSystemConfigByKey();
        } catch (IOException e) {
            log.error("getSystemConfigByKey error:[]", e);
            throw new FrontException(e.getMessage());
        }
    }

    /**
     * getNodeInfo.
     */
    public Object getNodeInfo() {
        return JsonUtils.toJavaObject(nodeConfig.toString(), Object.class);
    }

    public int getPendingTransactions(int groupId) throws IOException {
        try {
            return getWeb3j(groupId)
                    .getPendingTransaction().send()
                    .getPendingTransactions().size();
        } catch (IOException e) {
            log.error("getPendingTransactions error:[]", e);
            throw new FrontException(e.getMessage());
        }
    }

    public BigInteger getPendingTransactionsSize(int groupId) throws IOException {
        try {
            return getWeb3j(groupId).getPendingTxSize().send().getPendingTxSize();
        } catch (IOException e) {
            log.error("getPendingTransactionsSize error:[]", e);
            throw new FrontException(e.getMessage());
        }
    }

    public List<String> getSealerList(int groupId) throws IOException {
        try {
            return getWeb3j(groupId).getSealerList().send().getSealerList();
        } catch (IOException e) {
            log.error("getSealerList error:[]", e);
            throw new FrontException(e.getMessage());
        }
    }

    public List<String> getObserverList(int groupId) throws IOException {
        try {
            return getWeb3j(groupId).getObserverList().send().getObserverList();
        } catch (IOException e) {
            log.error("getObserverList error:[]", e);
            throw new FrontException(e.getMessage());
        }
    }

    /**
     * search By Criteria
     */
    public Object searchByCriteria(int groupId, String input) {
        if (StringUtils.isBlank(input)) {
            log.warn("fail searchByCriteria. input is null");
            return null;
        }
        if (StringUtils.isNumeric(input)) {
            return getBlockByNumber(groupId, new BigInteger(input));
        } else if (input.length() == HASH_OF_TRANSACTION_LENGTH) {
            return getTransactionByHash(groupId, input);
        }

        return null;
    }

    /**
     * dynamic group management
     */

    public Object generateGroup(GenerateGroupInfo generateGroupInfo) {
        log.debug("start generateGroup. groupId:{}", generateGroupInfo.getGenerateGroupId());
        try {
            GroupOperateStatus status = CommonUtils.object2JavaBean(
                    getWeb3j().generateGroup(generateGroupInfo.getGenerateGroupId(),
                            generateGroupInfo.getTimestamp().longValue(), true,
                            generateGroupInfo.getNodeList()).send().getStatus(),
                    GroupOperateStatus.class);
            log.info("generateGroup. groupId:{} status:{}", generateGroupInfo.getGenerateGroupId(),
                    status);
            if (CommonUtils.parseHexStr2Int(status.getCode()) == 0) {
                return new BaseResponse(ConstantCode.RET_SUCCEED);
            } else {
                log.error("generateGroup failed:{}", status.getMessage());
                throw classifyGroupOperateException(status);
            }
        } catch (IOException e) {
            log.error("generateGroup fail:[]", e);
            throw new FrontException(ConstantCode.NODE_REQUEST_FAILED);
        }
    }

    public Object operateGroup(int groupId, String type) {
        log.debug("start operateGroup. groupId:{} type:{}", groupId, type);
        try {
            switch (type) {
                case Constants.OPERATE_GROUP_START:
                    return startGroup(groupId);
                case Constants.OPERATE_GROUP_STOP:
                    return stopGroup(groupId);
                case Constants.OPERATE_GROUP_REMOVE:
                    return removeGroup(groupId);
                case Constants.OPERATE_GROUP_RECOVER:
                    return recoverGroup(groupId);
                case Constants.OPERATE_GROUP_GET_STATUS:
                    return querySingleGroupStatus(groupId);
                default:
                    log.error("end operateGroup. invalid operate type");
                    throw new FrontException(ConstantCode.INVALID_GROUP_OPERATE_TYPE);
            }
        } catch (IOException e) {
            log.error("operateGroup fail:[]", e);
            throw new FrontException(ConstantCode.NODE_REQUEST_FAILED);
        }
    }

    private Object startGroup(int groupId) throws IOException {
        GroupOperateStatus status = CommonUtils.object2JavaBean(
                getWeb3j().startGroup(groupId).send().getStatus(), GroupOperateStatus.class);
        log.info("startGroup. groupId:{} status:{}", groupId, status);
        if (CommonUtils.parseHexStr2Int(status.getCode()) == 0) {
            initWeb3j(groupId);
            return new BaseResponse(ConstantCode.RET_SUCCEED);
        } else {
            log.error("startGroup fail:{}", status.getMessage());
            throw classifyGroupOperateException(status);
        }
    }

    private Object stopGroup(int groupId) throws IOException {
        GroupOperateStatus status = CommonUtils.object2JavaBean(
                getWeb3j().stopGroup(groupId).send().getStatus(), GroupOperateStatus.class);
        log.info("stopGroup. groupId:{} status:{}", groupId, status);
        if (CommonUtils.parseHexStr2Int(status.getCode()) == 0) {
            web3jMap.remove(groupId);
            return new BaseResponse(ConstantCode.RET_SUCCEED);
        } else {
            log.error("stopGroup fail:{}", status.getMessage());
            throw classifyGroupOperateException(status);
        }
    }

    private Object removeGroup(int groupId) throws IOException {
        GroupOperateStatus status = CommonUtils.object2JavaBean(
                getWeb3j().removeGroup(groupId).send().getStatus(), GroupOperateStatus.class);
        log.info("removeGroup. groupId:{} status:{}", groupId, status);
        if (CommonUtils.parseHexStr2Int(status.getCode()) == 0) {
            return new BaseResponse(ConstantCode.RET_SUCCEED);
        } else {
            log.error("removeGroup fail:{}", status.getMessage());
            throw classifyGroupOperateException(status);
        }
    }

    private Object recoverGroup(int groupId) throws IOException {
        GroupOperateStatus status = CommonUtils.object2JavaBean(
                getWeb3j().recoverGroup(groupId).send().getStatus(), GroupOperateStatus.class);
        log.info("recoverGroup. groupId:{} status:{}", groupId, status);
        if (CommonUtils.parseHexStr2Int(status.getCode()) == 0) {
            return new BaseResponse(ConstantCode.RET_SUCCEED);
        } else {
            log.error("recoverGroup fail:{}", status.getMessage());
            throw classifyGroupOperateException(status);
        }
    }

    /**
     * classify group operate exception code
     * @param status
     * @return FrontException
     */
    private FrontException classifyGroupOperateException(GroupOperateStatus status) {
        int groupOperateStatusCode = CommonUtils.parseHexStr2Int(status.getCode());
        switch (groupOperateStatusCode) {
            case 1:
                return new FrontException(ConstantCode.NODE_INTERNAL_ERROR);
            case 2:
                return new FrontException(ConstantCode.GROUP_ALREADY_EXISTS);
            case 3:
                return new FrontException(ConstantCode.GROUP_ALREADY_RUNNING);
            case 4:
                return new FrontException(ConstantCode.GROUP_ALREADY_STOPPED);
            case 5:
                return new FrontException(ConstantCode.GROUP_ALREADY_DELETED);
            case 6:
                return new FrontException(ConstantCode.GROUP_NOT_FOUND);
            case 7:
                return new FrontException(ConstantCode.GROUP_OPERATE_INVALID_PARAMS);
            case 8:
                return new FrontException(ConstantCode.PEERS_NOT_CONNECTED);
            case 9:
                return new FrontException(ConstantCode.GENESIS_CONF_ALREADY_EXISTS);
            case 10:
                return new FrontException(ConstantCode.GROUP_CONF_ALREADY_EXIST);
            case 11:
                return new FrontException(ConstantCode.GENESIS_CONF_NOT_FOUND);
            case 12:
                return new FrontException(ConstantCode.GROUP_CONF_NOT_FOUND);
            case 13:
                return new FrontException(ConstantCode.GROUP_IS_STOPPING);
            case 14:
                return new FrontException(ConstantCode.GROUP_NOT_DELETED);
            default:
                return new FrontException(ConstantCode.GROUP_OPERATE_FAIL);
        }
    }

    private BaseResponse querySingleGroupStatus(int groupId) throws IOException {
        GroupOperateStatus status = CommonUtils.object2JavaBean(
                getWeb3j().queryGroupStatus(groupId).send().getStatus(), GroupOperateStatus.class);
        log.info("queryGroupStatus. groupId:{} status:{}", groupId, status);
        if (CommonUtils.parseHexStr2Int(status.getCode()) == 0) {
            BaseResponse response = new BaseResponse(ConstantCode.RET_SUCCEED);
            response.setData(status.getStatus());
            return response;
        } else {
            log.error("queryGroupStatus fail:{}", status.getMessage());
            throw classifyGroupOperateException(status);
        }
    }

    /**
     * Map of <groupId, status>
     * @param groupIdList
     * @return status: "INEXISTENT"、"STOPPING"、"RUNNING"、"STOPPED"、"DELETED"
     * @throws IOException
     */
    public BaseResponse getGroupStatus(List<Integer> groupIdList) throws IOException {
        Map<Integer, String> groupIdStatusMap = new HashMap<>(groupIdList.size());
        for (Integer groupId: groupIdList) {
            BaseResponse res = querySingleGroupStatus(groupId);
            groupIdStatusMap.put(groupId, (String)res.getData());
        }
        return new BaseResponse(ConstantCode.RET_SUCCESS, groupIdStatusMap);
    }

    /**
     * get first web3j in web3jMap
     * 
     * @return
     */
    public Web3j getWeb3j() {
        Set<Integer> iSet = web3jMap.keySet();
        if (iSet.isEmpty()) {
            log.error("web3jMap is empty, groupList empty! please check your node status");
            // get default web3j of integer max value
            return independentWeb3j;
        }
        // get random index to get web3j
        Integer index = iSet.iterator().next();
        return web3jMap.get(index);
    }

    /**
     * get target group's web3j
     * @param groupId
     * @return
     */
    public Web3j getWeb3j(Integer groupId) {
        if (web3jMap.isEmpty()) {
            // refresh group list
            getGroupList();
            log.error("web3jMap is empty, groupList empty! please check your node status");
            throw new FrontException(ConstantCode.SYSTEM_ERROR_GROUP_LIST_EMPTY);
        }
        Web3j web3j = web3jMap.get(groupId);
        if (Objects.isNull(web3j)) {
            log.error("web3j of {} is null, please call /{}/web3/refresh to refresh", groupId, groupId);
            // refresh group list
            getGroupList();
            throw new FrontException(ConstantCode.SYSTEM_ERROR_WEB3J_NULL.getCode(),
                    "web3j of " + groupId + " is null");
        }
        return web3j;
    }
}