package org.bdware.sc.boundry;

import com.google.gson.JsonObject;
import groovy.util.ScriptException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bdware.sc.ContractProcess;
import org.bdware.sc.bean.ContractExecType;
import org.bdware.sc.bean.ContractRequest;
import org.bdware.sc.conn.ResultCallback;
import org.bdware.sc.conn.SocketGet;
import org.bdware.sc.engine.DesktopEngine;
import org.bdware.sc.engine.JSONTool;
import org.bdware.sc.engine.SyncMechUtil;
import org.bdware.sc.event.REvent;
import org.bdware.sc.http.ApiGate;
import org.bdware.sc.syncMech.SyncType;
import org.bdware.sc.util.HashUtil;
import org.bdware.sc.util.JsonUtil;
import org.zz.gmhelper.SM2KeyPair;
import wrp.jdk.nashorn.api.scripting.NashornScriptEngine;
import wrp.jdk.nashorn.api.scripting.ScriptObjectMirror;
import wrp.jdk.nashorn.internal.objects.Global;
import wrp.jdk.nashorn.internal.runtime.PropertyMap;
import wrp.jdk.nashorn.internal.runtime.ScriptFunction;
import wrp.jdk.nashorn.internal.runtime.ScriptObject;
import wrp.jdk.nashorn.internal.scripts.JO;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.math.BigInteger;
import java.net.URL;
import java.net.URLConnection;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import static org.bdware.sc.event.REvent.REventType.*;

public class JavaScriptEntry {
    //    private static final HostnameVerifier DO_NOT_VERIFY = (hostname, session) -> true;
    public static final Map<String, ScriptFunction> topic_handlers = new HashMap<>();
    private static final Logger LOGGER = LogManager.getLogger(JavaScriptEntry.class);
    public static NashornScriptEngine currentEngine;
    public static SyncMechUtil currentSyncUtil;
    //    public static int contractManagerPort;
    public static Random random;
    public static long invokeID;
    public static String doi;
    public static String authInfoPersistDOI;
    public static SocketGet get; // public static CloseableHttpClient httpClient = getHttpClient();
    public static int numOfCopies;
    public static boolean isDebug;
    public static List<REvent> msgList;
    public static int shardingID;
    public static List<String> members;
    // private static SM2KeyPair keyPair = new SM2().generateKeyPair(); // TODO ？？ 本地服务器的,39上运行39的
    // public static String privKey;
    // public static String pubKey;
    private static SM2KeyPair keyPair;

    public static boolean resetContractName(String name) {
        StackTraceElement[] stacktrace = Thread.currentThread().getStackTrace();
        String method = null;
        if (stacktrace.length > 2) {
            method = stacktrace[2].getMethodName();
        }
        if (method != null && method.equals("onCreate")) {
            ContractProcess.instance.resetContractName(name);
            return true;
        }
        return false;
    }

    public static void setSM2KeyPair(String pubKey, String privKey) {
        keyPair =
                new SM2KeyPair(
                        SM2KeyPair.publicKeyStr2ECPoint(pubKey), new BigInteger(privKey, 16));
    }

    public static SM2KeyPair getKeyPair() {
        return keyPair;
    }

    public static Global getEngineGlobal() {
        return currentEngine.getNashornGlobal();
    }

    public static String byteArrayHash(byte[] hash) {
        return HashUtil.hashByteArray(hash);
    }

    public static String bytes2Str(byte[] bytes) {
        return new String(bytes);
    }

    public static Object connectNeo4j(String url, String usrName, String pwd) {
        try {
            if (url.startsWith("jdbc:neo4j")) {
                Connection con;
                con = DriverManager.getConnection(url, usrName, pwd);
                return con;
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return null;
    }

    public static long currentTimeMillis() {
        return System.currentTimeMillis();
    }

    public static Lock createLock() {
        return new ReentrantLock();
    }

    public static String asyncTest(String str, ScriptFunction fun) {
        LOGGER.debug(str);
        DesktopEngine.applyWithGlobal(fun, currentEngine.getNashornGlobal(), str);
        return "success";
    }

    //	public static String http(String baseUrl, String method, Map<String, String> header,
    // Map<String, String> argMap,
    //			List<String> reservedList) {
    //		return HttpUtil.request(baseUrl, method, header, argMap, reservedList);
    //	}

    public static byte[] inputStreamToBytes(InputStream in) {
        ByteArrayOutputStream bo = new ByteArrayOutputStream();
        byte[] buff = new byte[4 * 1024 * 1024];
        try {
            for (int count; (count = in.read(buff)) > 0; ) {
                bo.write(buff, 0, count);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bo.toByteArray();
    }

    public static InputStream httpAsInputStream(String url) {
        try {
            URL realUrl = new URL(url);
            URLConnection conn = realUrl.openConnection();
            return conn.getInputStream();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    public static Object executeFunction(ScriptFunction callback, Object arg) {
        return DesktopEngine.applyWithGlobal(callback, currentEngine.getNashornGlobal(), arg);
    }

    public static ApiGate createAPIGate(String ip) {
        return new ApiGate(ip);
    }

    public static ApiGate createAPIGate(String ip, String port) {
        return new ApiGate(ip, Integer.parseInt(port));
    }

    public static String executeContractWithSig(
            String contractID, String action, String arg, String pubkey, String sig) {
        try {
            ContractRequest app = new ContractRequest();
            app.setContractID(contractID).setAction(action).setArg(arg);
            app.setPublicKey(pubkey);
            app.setSignature(sig);
            app.fromContract = keyPair.getPublicKeyStr();
            if (!app.verifySignature()) {
                return "{\"status\":\"Exception\",\"data\":\"invalid signature\"}";
            }
            app.setRequesterDOI(doi);
            app.setFromDebug(isDebug);
            if (numOfCopies > 1) {
                // The caller is special.
                app.setRequestID(
                        app.getPublicKey().hashCode()
                                + "_"
                                + numOfCopies
                                + "_"
                                + (invokeID++)
                                + "_"
                                + random.nextInt()
                                + "_mul");
            } else {
                app.setRequestID(
                        app.getPublicKey().hashCode()
                                + "_"
                                + (invokeID++)
                                + "_"
                                + random.nextInt());
            }
            return get.syncGet("dd", "executeContract", JsonUtil.toJson(app));

        } catch (Exception e) {
            ByteArrayOutputStream bo = new ByteArrayOutputStream();
            e.printStackTrace(new PrintStream(bo));
            return bo.toString();
        }
    }

    public static Object executeContract(String contractID, String action, Object arg) {
        if (currentSyncUtil.engine.recovering) {
            String str =
                    currentSyncUtil.transRecoverUtil.curRecoverRecord.getExecuteResult(
                            invokeID + "");
            String[] strs = str.split("<seperate>");
            String flag1 = strs[0];
            String flag = strs[1];
            String res = strs[2];
            if (flag1.equals("1")) {
                invokeID++;
            }
            if (flag.equals("1")) {
                random.nextInt();
            }
            JsonObject jo = JsonUtil.parseStringAsJsonObject(res);
            return JSONTool.convertJsonElementToMirror(jo);
        }

        long formerInvokeID = invokeID;
        int flag1 = 0; // 标志invokeID++操作是否进行过
        int flag = 0; // 标志random是否取下一个

        try {
            ContractRequest app = new ContractRequest();
            app.setContractID(contractID).setAction(action).setArg(JSONTool.convertMirrorToJson(arg));
            app.doSignature(keyPair);
            app.setRequesterDOI(doi);
            app.setFromDebug(isDebug);
            ContractExecType type = ContractProcess.instance.getContract().getType();
            if (type.needSeq()) {
                app.setRequestID(
                        String.format(
                                "%d_%d_%d_%d_mul",
                                keyPair.getPublicKeyStr().hashCode(),
                                numOfCopies,
                                (invokeID++),
                                random.nextInt()));
                // The caller is special.
                flag = 1;
                flag1 = 1;
                LOGGER.warn("invoke contractExecution! " + JsonUtil.toJson(app));
            } else {
                app.setRequestID(
                        String.format(
                                "%d_%d_%d",
                                keyPair.getPublicKeyStr().hashCode(),
                                (invokeID++),
                                random.nextInt()));
                flag = 1;
                flag1 = 1;
            }
            return executeContract(formerInvokeID, flag1, flag, app);
        } catch (Exception e) {
            ByteArrayOutputStream bo = new ByteArrayOutputStream();
            e.printStackTrace(new PrintStream(bo));
            String result = bo.toString();
            if (currentSyncUtil.startFlag
                    && currentSyncUtil.currType == SyncType.Trans
                    && !currentSyncUtil.engine.recovering) {
                currentSyncUtil.transRecordUtil.recordExecutes(
                        formerInvokeID + "", flag1 + "<seperate>" + flag + "<seperate>" + result);
            }
            return result;
        }
    }

    private static Object executeContract(long formerInvokeID, int flag1, int flag, ContractRequest app) {
        String result = get.syncGet("dd", "executeContract", JsonUtil.toJson(app));
        if (currentSyncUtil.startFlag
                && currentSyncUtil.currType == SyncType.Trans
                && !currentSyncUtil.engine.recovering) {
            currentSyncUtil.transRecordUtil.recordExecutes(
                    formerInvokeID + "",
                    flag1 + "<seperate>" + flag + "<seperate>" + result);
        }
        JsonObject jo = JsonUtil.parseStringAsJsonObject(result);
        return JSONTool.convertJsonElementToMirror(jo);
    }

    public static void executeContractAsyncWithoutSig(
            String contractID, String action, String arg, final ScriptFunction cb) {
        try {
            ContractRequest app = new ContractRequest();
            app.setContractID(contractID).setAction(action).setArg(arg);
            app.setRequestID((invokeID++) + "_" + random.nextInt());
            get.asyncGet(
                    "dd",
                    "executeContract",
                    JsonUtil.toJson(app),
                    new ResultCallback() {
                        @Override
                        public void onResult(String str) {
                            if (null != cb) {
                                DesktopEngine.applyWithGlobal(
                                        cb, currentEngine.getNashornGlobal(), str);
                            }
                        }
                    });
        } catch (Exception e) {
            ByteArrayOutputStream bo = new ByteArrayOutputStream();
            e.printStackTrace(new PrintStream(bo));
        }
    }

    public static String executeContractAsync(
            String contractID, String action, String arg, final ScriptFunction cb) {
        try {

            ContractRequest app = new ContractRequest();
            app.setContractID(contractID).setAction(action).setArg(arg);
            app.doSignature(keyPair);
            app.setRequestID((invokeID++) + "_" + random());
            app.setRequesterDOI(doi);
            get.asyncGet(
                    "dd",
                    "executeContract",
                    JsonUtil.toJson(app),
                    new ResultCallback() {
                        @Override
                        public void onResult(String str) {
                            if (cb != null) {
                                DesktopEngine.applyWithGlobal(
                                        cb, currentEngine.getNashornGlobal(), str, arg);
                            }
                        }
                    });
            return "success";
        } catch (Exception e) {
            ByteArrayOutputStream bo = new ByteArrayOutputStream();
            e.printStackTrace(new PrintStream(bo));
            return bo.toString();
        }
    }

    //YancloudUtil.exceptionReturn({"msg":"missing arguments repoId ","code":1});
    public static void exceptionReturn(Object obj) throws ScriptReturnException {
        throw new ScriptReturnException(JSONTool.convertMirrorToJson(obj));
    }

    /**
     * publish an event with semantic AT_LEAST_ONCE
     *
     * @param topic   the topic
     * @param content the content
     * @author Kaidong Wu
     */
    public static void pubEvent(String topic, String content) {
        pubEventConstraint(topic, content, null);
    }

    /**
     * publish an event with some semantic
     *
     * @param topic      the topic
     * @param content    the content
     * @param constraint the constraint, AT_LEAST_ONCE, AT_MOST_ONCE, and ONLY_ONCE
     * @author Kaidong Wu
     */
    public static void pubEventConstraint(String topic, String content, String constraint) {
        String reqID =
                String.format(
                        "%d_%d_%d_%s_pe",
                        keyPair.getPublicKeyStr().hashCode(), numOfCopies, invokeID, random());
        REvent msg = new REvent(topic, PUBLISH, content, reqID);
        if (null != constraint) {
            msg.setSemantics(REvent.REventSemantics.valueOf(constraint));
        }
        msgList.add(msg);
    }

    /**
     * subscribe a topic
     *
     * @param topic event topic
     * @param fun   related handler function
     * @author Kaidong Wu
     */
    public static String subscribe(String topic, ScriptFunction fun) {
        subscribe(topic, fun, false);
        if (topic_handlers.containsKey(topic)) {
            ContractProcess.instance.unSubscribe(topic_handlers.get(topic).getName());
        }
        topic_handlers.put(topic, fun);
        return topic;
    }

    /**
     * subscribe a local event
     *
     * @param contractID contractID
     * @param event      local event topic
     * @param fun        related handler function
     * @author Kaidong Wu
     */
    public static String subscribe(String contractID, String event, ScriptFunction fun) {
        String topic = HashUtil.sha3(contractID, event);
        subscribe(topic, fun, false);
        if (topic_handlers.containsKey(topic)) {
            ContractProcess.instance.unSubscribe(topic_handlers.get(topic).getName());
        }
        topic_handlers.put(topic, fun);
        return topic;
    }

    private static void subscribe(String topic, ScriptFunction fun, boolean fromPreSub) {
        String reqID =
                String.format(
                        "%d_%d_%d_%s_se",
                        keyPair.getPublicKeyStr().hashCode(), numOfCopies, invokeID, random());

        REvent msg =
                new REvent(
                        topic,
                        SUBSCRIBE,
                        String.format(
                                "{\"subscriber\":\"%s\",\"handler\":\"%s\"}",
                                ContractProcess.instance.getContractName(), fun.getName()),
                        reqID);
        if (fromPreSub) {
            msg.setSemantics(REvent.REventSemantics.ONLY_ONCE);
        }
        msgList.add(msg);

        ContractProcess.instance.subscribe(fun.getName());
    }

    public static void unsubscribe(String topic) {
        String reqID =
                String.format(
                        "%d_%d_%d_%s_us",
                        keyPair.getPublicKeyStr().hashCode(), numOfCopies, invokeID, random());
        String content;
        if (null == topic) {
            content = "{\"subscriber\":\"" + ContractProcess.instance.getContractName() + "\"}";
            topic_handlers.forEach((k, c) -> {
                topic_handlers.remove(k);
                ContractProcess.instance.unSubscribe(c.getName());
            });
        } else {
            String handler = topic_handlers.get(topic).getName();
            content =
                    String.format(
                            "{\"subscriber\":\"%s\",\"handler\":\"%s\"}",
                            ContractProcess.instance.getContractName(), handler);
            topic_handlers.remove(topic);
            ContractProcess.instance.unSubscribe(handler);
        }
        REvent msg = new REvent(topic, UNSUBSCRIBE, content, reqID);
        msgList.add(msg);
    }

    /**
     * pre-sub in ONLY_ONCE
     *
     * @param topic   the topic
     * @param content the content
     * @author Kaidong Wu
     */
    public static void preSub(String topic, String content) {
        String newTopic = topic + "|" + content + "|" + ContractProcess.instance.getContractName();
        subscribe(newTopic, topic_handlers.get(topic), true);
        String reqID =
                String.format(
                        "%d_%d_%d_%s_pse",
                        keyPair.getPublicKeyStr().hashCode(), numOfCopies, (invokeID++), random());
        REvent msg = new REvent(topic, REvent.REventType.PRESUB, newTopic, reqID);
        msg.setSemantics(REvent.REventSemantics.ONLY_ONCE);
        msgList.add(msg);
    }

    /**
     * @return a random value with string format
     * @author Kaidong Wu
     */
    public static String random() {
        String seed = String.valueOf(null == random ? System.currentTimeMillis() : random.nextInt());
        return HashUtil.sha3(seed);
    }

    public static String getContractInfo(String topic) {
        return null;
        // TODO
    }


    public static ScriptObject getCaller(int i) {
        JO ret = new JO(PropertyMap.newMap());
        StackTraceElement[] stacktrace = Thread.currentThread().getStackTrace();
        if (stacktrace.length > i + 2) {
            ret.put("name", stacktrace[i + 2].getMethodName(), false);
            ret.put("file", stacktrace[i + 2].getFileName(), false);
        }
        return ret;
    }

    public static class Result {
        public int responseCode;
        public String response;
    }

}
