package org.bcos.proxy.tool;

import org.bcos.channel.client.Service;
import org.bcos.channel.client.TransactionSucCallback;
import org.bcos.channel.dto.EthereumResponse;
import org.bcos.proxy.config.Config;
import org.bcos.proxy.contract.Node;
import org.bcos.proxy.contract.RouteManager;
import org.bcos.proxy.contract.Set;
import org.bcos.proxy.protocol.UserInfo;
import org.bcos.proxy.server.HttpServer;
import org.bcos.proxy.util.ToolUtil;
import org.bcos.web3j.abi.datatypes.Address;
import org.bcos.web3j.abi.datatypes.Bool;
import org.bcos.web3j.abi.datatypes.Type;
import org.bcos.web3j.abi.datatypes.Utf8String;
import org.bcos.web3j.abi.datatypes.generated.Bytes32;
import org.bcos.web3j.abi.datatypes.generated.Uint256;
import org.bcos.web3j.crypto.Credentials;
import org.bcos.web3j.protocol.ObjectMapperFactory;
import org.bcos.web3j.protocol.Web3j;
import org.bcos.web3j.protocol.channel.ChannelEthereumService;
import org.bcos.web3j.protocol.core.DefaultBlockParameterName;
import org.bcos.web3j.protocol.core.methods.request.Transaction;
import org.bcos.web3j.protocol.core.methods.response.EthCall;
import org.bcos.web3j.protocol.core.methods.response.TransactionReceipt;
import org.bcos.web3j.utils.Numeric;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;

/**
 * Created by fisco-dev on 17/8/25.
 */
public class DeployContract {
    public static java.math.BigInteger gasPrice = new BigInteger("1000000");
    public static java.math.BigInteger gasLimit = new BigInteger("1000000");
    public static java.math.BigInteger initialWeiValue = new BigInteger("0");
    public static HttpServer.WCS routeWCS;

    public static String contractVersion = "";

    public static ConcurrentHashMap<String, HttpServer.WCS> nameSetServiceMap = new ConcurrentHashMap();

    static {

        try {
            Config config = Config.getConfig();
            ApplicationContext context = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");

            //先初始化route的
            Credentials credentialsRoute = Credentials.create(config.getPrivateKey());
            Service serviceRoute = (Service) context.getBean(HttpServer.ROUTE_SERVICE_NAME);
            ChannelEthereumService channelEthereumServiceRoute = new ChannelEthereumService();
            channelEthereumServiceRoute.setChannelService(serviceRoute);
            serviceRoute.run();

            Web3j web3jRoute = Web3j.build(channelEthereumServiceRoute);
            routeWCS = new HttpServer.WCS(web3jRoute, credentialsRoute, serviceRoute, HttpServer.ROUTE_SERVICE_NAME);

            //init for all set service
            for (String setName : context.getBeanNamesForType(Service.class)) {
                Service serviceSet = (Service) context.getBean(setName);
                ChannelEthereumService setChannelEthereumService = new ChannelEthereumService();
                setChannelEthereumService.setChannelService(serviceSet);
                serviceSet.run();
                Web3j web3jSet = Web3j.build(setChannelEthereumService);
                HttpServer.WCS wcs = new HttpServer.WCS(web3jSet, Credentials.create(config.getPrivateKey()), serviceSet, setName);
                nameSetServiceMap.put(setName, wcs);
            }

            Thread.sleep(5000L);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * @desc read json file,format like:[{"set_name":"", "set_warn_num":8,"set_max_num":10,"set_node_list":[{"ip":"","p2p_port":12,"rpc_port":34,"node_id":"","type":1}]}]
     * @param fileName
     * @return JSONObject
     * @throws IOException
     */
    public static JSONArray readJSONFile(String fileName) throws IOException {
        BufferedReader reader = new BufferedReader(new FileReader(fileName));
        try {
            StringBuilder sb = new StringBuilder();
            String tmp = null;
            while((tmp = reader.readLine()) != null) {
                sb.append(tmp);
            }
            JSONArray jsonArray = JSON.parseArray(sb.toString());
            return jsonArray;

        } catch (IOException e) {
            throw e;
        } finally {
            reader.close();
        }
    }

    /**
     * @desc deploy route contract
     * @param jsonArray
     * @throws Exception
     */
    public static void deployContract(JSONArray jsonArray) throws Exception {
        if (jsonArray == null) {
            throw new Exception("addSetToRoute jsonArray is null");
        }

        Future<RouteManager> routeManagerFuture = RouteManager.deploy(routeWCS.getWeb3j(), routeWCS.getCredentials(), gasPrice, gasLimit, initialWeiValue, new Uint256(0), new Utf8String(""));
        RouteManager routeManager = routeManagerFuture.get();
        for (int i = 0; i < jsonArray.size(); i++) {
            JSONObject jsonObject = jsonArray.getJSONObject(i);
            String setName = jsonObject.getString("set_name");
            int setWarnNum = jsonObject.getInteger("set_warn_num");
            int setMaxNum = jsonObject.getInteger("set_max_num");
            JSONArray nodeList = jsonObject.getJSONArray("set_node_list");
            Future<Set> setFuture = Set.deploy(routeWCS.getWeb3j(), routeWCS.getCredentials(), gasPrice, gasLimit, initialWeiValue, new Uint256(setMaxNum), new Uint256(setWarnNum));
            Set set = setFuture.get();
            for(int j = 0; j < nodeList.size(); j++) {
                JSONObject nodeJson = nodeList.getJSONObject(j);
                Utf8String nodeId = new Utf8String(nodeJson.getString("node_id"));
                Utf8String ip = new Utf8String(nodeJson.getString("ip"));
                Uint256 p2p = new Uint256(nodeJson.getIntValue("p2p_port"));
                Uint256 rpc = new Uint256(nodeJson.getIntValue("rpc_port"));
                Uint256 type = new Uint256(nodeJson.getInteger("type"));
                Utf8String desc = new Utf8String("");
                Utf8String caHash = new Utf8String("");
                Utf8String agencyInfo = new Utf8String("");
                Future<Node> nodeFuture = Node.deploy(routeWCS.getWeb3j(), routeWCS.getCredentials(), gasPrice, gasLimit, initialWeiValue, nodeId, ip, p2p, rpc, type, desc, caHash, agencyInfo);
                Node node = nodeFuture.get();
                String address = node.getContractAddress();
                Future<TransactionReceipt> receiptFuture = set.addNode(new Address(address));
                TransactionReceipt receipt = receiptFuture.get();
                if (receipt == null || receipt.getTransactionHash() == null) {
                    System.out.println("get receipt is null or transaction hash is null.exec again");
                    return;
                }
            }

            Future<TransactionReceipt> transactionReceiptFuture = routeManager.registerSet(new Address(set.getContractAddress()), new Utf8String(setName));
            TransactionReceipt transactionReceipt = transactionReceiptFuture.get();
            if (transactionReceipt == null || transactionReceipt.getTransactionHash() == null) {
                System.out.println("get receipt is null or transaction hash is null.exec again");
                return;
            }

        }

        System.out.println("register route contract success.address:" + routeManager.getContractAddress());
    }


    /**
     * @desc get user info
     * @param uid
     */
    public static UserInfo queryUserInfo(String uid) throws Exception {

        byte[] uidBytes = Arrays.copyOf(uid.getBytes(), 32);
        Bytes32 uidByte32 = new Bytes32(uidBytes);

        RouteManager routeManager = RouteManager.load(Config.getConfig().getRouteAddress(), routeWCS.getWeb3j(), routeWCS.getCredentials(), gasPrice, gasLimit);

        List<Type> typeList = routeManager.getRoute(uidByte32).get();
        if (typeList.size() != 2) {
            return null;
        }

        Boolean existed = (Boolean)typeList.get(0).getValue();
        BigInteger setId = (BigInteger)typeList.get(1).getValue();

        if (!existed) {
            return null;
        }

        Future<Utf8String> addressFuture = routeManager.m_setNames(new Uint256(setId));
        String setName = addressFuture.get().toString();


        JSONObject jsonObject = JSON.parseObject("{}");
        jsonObject.put("contract", "Meshchain");
        jsonObject.put("func", "getUserInfo");
        jsonObject.put("version", "");

        List<Object> params = new ArrayList<>();
        params.add(uid);
        jsonObject.put("params", params);

        Web3j web3j = nameSetServiceMap.get(setName).getWeb3j();

        if (web3j == null) {
            throw new Exception("bad chain name");
        }

        String data = Numeric.toHexString(jsonObject.toJSONString().getBytes());

        EthCall ethCall = web3j.ethCall(Transaction.createEthCallTransaction(null, null, data, BigInteger.ZERO, false), DefaultBlockParameterName.LATEST).sendAsync().get();
        String value = ethCall.getResult();
        JSONArray array = JSON.parseArray(value);
        if (array.size() == 0) {
            throw new Exception("array size = 0");
        }

        int availAssets = array.getInteger(0);
        int unAvailAssets = array.getInteger(1);
        int identity = array.getInteger(2);
        String name = array.getString(3);

        UserInfo userInfo = new UserInfo(uid, availAssets, unAvailAssets, identity, name);
        System.out.printf("uid:%s,set id:%d\n", uid, setId.intValue());
        return userInfo;

    }

    public static void getSetUsers(int setId) throws Exception {
        if (setId < 0) {
            System.out.println("set id error: set id:" + setId);
            return;
        }

        RouteManager routeManager = RouteManager.load(Config.getConfig().getRouteAddress(), routeWCS.getWeb3j(), routeWCS.getCredentials(), gasPrice, gasLimit);
        List<Type> typeList = routeManager.getSetAddress(new Uint256(setId)).get();
        if (typeList.size() != 2) {
            System.out.println("get setId:" + setId + " failed");
            return;
        }

        Bool ok = (Bool)typeList.get(0);
        Address setAddress = (Address)typeList.get(1);

        if (!ok.getValue()) {
            System.out.println("setId:" + setId + " not existed");
            return;
        }

        Set set = Set.load(setAddress.toString(), routeWCS.getWeb3j(), routeWCS.getCredentials(), gasPrice, gasLimit);
        Uint256 userNum = set.getUsersNum().get();

        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < userNum.getValue().intValue(); i++) {
            Bytes32 user = set.m_users(new Uint256(i)).get();
            sb.append(ToolUtil.byte32ToString(user.getValue())).append(",");
        }

        String users = sb.toString();
        if (users.length() > 0) {
            users = users.substring(0, users.length() - 1);
        }

        System.out.println("set id :" + setId + ", users:[" + users + "]");
    }

    /**
     * @desc get set capacity
     * @param setId
     * @throws Exception
     */
    public static void getSetCapacity(int setId) throws Exception {
        if (setId < 0) {
            System.out.println("set id error: set id:" + setId);
            return;
        }

        RouteManager routeManager = RouteManager.load(Config.getConfig().getRouteAddress(), routeWCS.getWeb3j(), routeWCS.getCredentials(), gasPrice, gasLimit);
        List<Type> typeList = routeManager.getSetAddress(new Uint256(setId)).get();
        if (typeList.size() != 2) {
            System.out.println("get setId:" + setId + " failed");
            return;
        }

        Bool ok = (Bool)typeList.get(0);
        Address setAddress = (Address)typeList.get(1);

        if (!ok.getValue()) {
            System.out.println("setId:" + setId + " not existed");
            return;
        }

        Set set = Set.load(setAddress.toString(), routeWCS.getWeb3j(), routeWCS.getCredentials(), gasPrice, gasLimit);
        List<Type> retListType = set.getSetCapacity().get();

        if (retListType.size() != 2) {
            System.out.println("get set capacity failed");
            return;
        }

        Uint256 warn = (Uint256)retListType.get(0);
        Uint256 max = (Uint256)retListType.get(1);

        System.out.println("warn num:" + warn.getValue().intValue() + ", max num:" + max.getValue().intValue());
    }

    /**
     * @desc  expand set capacity
     * @param setId
     * @param warn
     * @param max
     * @throws Exception
     */
    public static void expandSet(int setId, int warn, int max) throws Exception {
        if (setId < 0) {
            System.out.println("set id error: set id:" + setId);
            return;
        }

        RouteManager routeManager = RouteManager.load(Config.getConfig().getRouteAddress(), routeWCS.getWeb3j(), routeWCS.getCredentials(), gasPrice, gasLimit);
        List<Type> typeList = routeManager.getSetAddress(new Uint256(setId)).get();
        if (typeList.size() != 2) {
            System.out.println("get setId:" + setId + " failed");
            return;
        }

        Bool ok = (Bool)typeList.get(0);
        Address setAddress = (Address)typeList.get(1);

        if (!ok.getValue()) {
            System.out.println("setId:" + setId + " not existed");
            return;
        }


        Set set = Set.load(setAddress.toString(), routeWCS.getWeb3j(), routeWCS.getCredentials(), gasPrice, gasLimit);
        set.expandSet(new Uint256(max), new Uint256(warn), new TransactionSucCallback() {
            @Override
            public void onResponse(EthereumResponse ethereumResponse) {
                try {
                    TransactionReceipt transactionReceipt = ObjectMapperFactory.getObjectMapper().readValue(ethereumResponse.getContent(), TransactionReceipt.class);
                    List<Set.RetEventResponse> retEventResponseList = Set.getRetEvents(transactionReceipt);
                    if (retEventResponseList.size() != 1) {
                        System.out.println("get expandSet event log failed.");
                        return;
                    }

                    Set.RetEventResponse retEventResponse = retEventResponseList.get(0);
                    if (retEventResponse.code.getValue().intValue() == 0) {
                        System.out.println("expandSet success.");
                    } else {
                        System.out.println("expandSet failed.code:" + retEventResponse.code.getValue().intValue());
                    }

                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }


    public static void main(String[] args) throws Exception {

        if (args.length < 1 || args[0] == "-h") {
            System.out.println("usage:[deploy route.json");
            System.out.println("      [queryUserInfo $uid");
            System.out.println("      [expandSet $setid $warnNum $maxNum");
            System.out.println("      [getSetCapacity $setid");
            System.exit(0);
        }

        switch (args[0]) {
            case "deploy":
                if (args.length < 2) {
                    System.out.println("miss the route.json file");
                    return;
                }

                JSONArray jsonArray = readJSONFile(args[1]);
                deployContract(jsonArray);
                break;
            case "queryUserInfo":
                if (args.length < 2) {
                    System.out.println("miss the uid");
                    return;
                }
                UserInfo userInfo = queryUserInfo(args[1]);
                if (userInfo == null) {
                    System.out.println("uid:" + args[1] + " not exist");
                    System.exit(0);
                }

                System.out.printf("uid:%s, queryUserInfo get availAssets:%d unAvailAssets: %d, identity:%d, name:%s\n", userInfo.getUid(),
                        userInfo.getAvailAssets(), userInfo.getUnAvailAssets(), userInfo.getIdentity(), userInfo.getName());
                break;
            case "expandSet":
                if (args.length < 4) {
                    System.out.println("miss the args:$setid $warnNum $maxNum");
                    return;
                }

                expandSet(Integer.parseInt(args[1]), Integer.parseInt(args[2]), Integer.parseInt(args[3]));
                break;
            case "getSetCapacity":
                if (args.length < 2) {
                    System.out.println("miss the $setid");
                    return;
                }

                getSetCapacity(Integer.parseInt(args[1]));
                break;
            case "getSetUsers":
                if (args.length < 2) {
                    System.out.println("miss the $setid");
                    return;
                }

                getSetUsers(Integer.parseInt(args[1]));
                break;
            default:
                System.out.println("not support command");
                return;
        }

        Thread.sleep(3000L);
        System.exit(0);
    }
}