package com.brilliance.isc.common.validator.es.support.impl;


import com.brilliance.isc.common.validator.es.factory.CompiledScriptFactory;
import com.brilliance.isc.common.validator.es.factory.ScriptTemplateFactory;
import com.brilliance.isc.common.validator.es.support.IRuleValidator;
import com.brilliance.isc.common.validator.es.support.IScriptValidator;
import com.brilliance.isc.common.validator.es.utils.EsConstant;
import com.brilliance.isc.common.validator.es.utils.EsSpringUtils;
import com.brilliance.isc.common.validator.es.vo.EnvDespVo;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jdk.nashorn.api.scripting.ScriptObjectMirror;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.script.*;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * @author fukai
 * Es脚本校验器入口
 */
@Component
public class EsScriptValidator implements IScriptValidator<CompiledScript> {

    private static Logger logger = LoggerFactory.getLogger(EsScriptValidator.class);

    private static ObjectMapper objectMapper = new ObjectMapper();

    @Resource
    private CompiledScriptFactory compiledScriptFactory;

    @Resource
    private ScriptTemplateFactory scriptTemplateFactory;

    @Resource
    private RedissonClient redissonClient;

    @Override
    public String outputScript(EnvDespVo envDespVo,Set<String> loadSet) {
        String jsfunc =  scriptTemplateFactory.getEsScript(envDespVo,loadSet);
        if(EsConstant.VUE_TERMINAL.equals(envDespVo.getTerminal())) {
            jsfunc = jsfunc.replaceAll("function\\s*(\\([\\s,\\w]+\\))","$1=>");
        }
        return jsfunc;
    }

    public String outputEsModuleScriptWithCache(String moduleName,List<String> scriptNames,String loader){
        if(!scriptTemplateFactory.isDebugMode()) {
            String key = "ES:MODULE:" + moduleName;
            RBucket bucket = redissonClient.getBucket(key);
            String script = (String) bucket.get();
            if (script != null && !script.isEmpty()) {
                logger.info("{}脚本快速返回", moduleName);
                return script;
            }
            if (scriptTemplateFactory.isDebugMode()) {
                logger.info("EsValidator模块调试模式，{}重新加载", moduleName);
            } else {
                logger.info("{}缓存不存在，重新加载", moduleName);
            }
            script = outputEsModuleScript(moduleName, scriptNames, loader);
            boolean isSucc = bucket.trySet(script, 10, TimeUnit.HOURS);
            logger.info("{}缓存设置结果:{}", moduleName, isSucc);
            return script;
        }
        return outputEsModuleScript(moduleName, scriptNames, loader);
    }

    /**
     * 输出标准的ES6模块的Script，支持webpack external导出
     * @param moduleName 要输出的模块名称
     * @return
     */
    public String outputEsModuleScript(String moduleName,List<String> scriptNames,String loader){
        StringBuilder sb = new StringBuilder("(function(global){\n//#FIRST_LINE#\n");
        Set<String> loadSet = new LinkedHashSet<>();
        String template = "function checkRuleOf%s(){\n%s\n return checkRule\n}";
        if(loader == null || loader.isEmpty()){
            loader =  scriptTemplateFactory.getLoader();
        }
        EnvDespVo envDespVo = EnvDespVo.builder()
                .scriptName(null)
                .terminal(EsConstant.VUE_TERMINAL)
                .loader(loader)
                .subtype(EsConstant.JS_ES6_MODULE).build();
        String initObj = String.format("if(!global.%s)\n\t global.%s = {};\n var destObj = global.%s;\n",moduleName,moduleName,moduleName);
        StringBuilder exports = new StringBuilder(initObj);
        for (int i = 0; i < scriptNames.size(); i++) {
            String transName = scriptNames.get(i);
            envDespVo.setScriptName(transName);
            String esString = outputScript(envDespVo,loadSet);

            esString = String.format(template,transName,esString);
            sb.append(esString);
            sb.append('\n');

            String funcName = String.format("checkRuleOf%s",transName);
            exports.append(String.format("destObj.%s = %s;\n",funcName,funcName));

        }
        //加载依赖的模块
        for(String load:loadSet){
            envDespVo.setScriptName(load);
            String loadStr = outputScript(envDespVo,null);
            sb.append(loadStr);
            sb.append('\n');
        }
        sb.append(exports);
        sb.append('\n');
        sb.append("})(this)\n");
        return sb.toString();
    }

    @Override
    public CompiledScript compileForJava(EnvDespVo envDespVo) {
        return compiledScriptFactory.getCompileScriptByScriptName(envDespVo);
    }

    @Override
    public Map<String, String> validateBo(Object bo, String scriptName){
        return  validateBo(bo,scriptName,null);
    }

    /**
     * 实现模型校验函数，执行局部校验
     * @param bo 业务模型
     * @param scriptName 脚本模型，大小写严格
     * @param path 要设定起头路径
     * @return
     */
    public Map<String, String> validateBo(Object bo, String scriptName,String path) {
        EnvDespVo envDespVo = EnvDespVo.builder().
                terminal(EsConstant.JAVA_TERMINAL).
                scriptName(scriptName).
                subtype(EsConstant.JS_TYPE).build();
        CompiledScript compiledScript = compileForJava(envDespVo);
        //用于存放错误信息
        Map<String, String> messageMap = new HashMap<>();
        Map<String, Object> boMap = exchangeBoToMap(bo);
        try {
            compiledScript.eval();
            ScriptEngine scriptEngine = compiledScript.getEngine();
            ScriptObjectMirror checkFunction = (ScriptObjectMirror) scriptEngine.get(EsConstant.CHECK_RULE_FUNC_NAME);
            //构建this环境
            Map<String, Object> thiz = createThisMap(bo,scriptEngine);

            ScriptObjectMirror schema = (ScriptObjectMirror)checkFunction.call(thiz);
            logger.info("js返回的对象:{}", schema);
            validateBoWithSchema(boMap, scriptEngine, schema, messageMap,path);
        } catch (Exception e) {
            logger.error("validate bo error:{}", e);
            throw new RuntimeException("validate bo error:", e);
        }
        return messageMap;
    }

    private Map<String,Object> createThisMap(Object bo,ScriptEngine engine){
        Map<String, Object> thiz = new HashMap<>();
        thiz.put("model", bo);
        //设置Java开启标签传递给Js，可通过判断this.isJava有值为Java模式执行
        thiz.put("isJava", true);
        thiz.put("curPath", scriptTemplateFactory.getCurPath());
        Invocable inv = (Invocable) engine;
        try {
            ScriptObjectMirror invokeCallback = (ScriptObjectMirror) inv.invokeFunction(EsConstant.GET_CALLBACK, new InvokeBeanCallback());
            thiz.put("invoke", invokeCallback);
            ScriptObjectMirror notifyCallback = (ScriptObjectMirror) inv.invokeFunction(EsConstant.GET_CALLBACK, new NotifyCallback());
            thiz.put("$notify",notifyCallback);
            thiz.put("$message",notifyCallback);
            thiz.put("$msgbox",notifyCallback);
            thiz.put("$smsgbox",notifyCallback);
            thiz.put("$alert",notifyCallback);
        }catch (Exception e) {
            logger.error("get callback error:{}", e);
            throw new RuntimeException("get callback error:", e);
        }
        return thiz;
    }

    public Map<String, String> validateBoWithSchema(Map<String, Object> boMap,
                                                    ScriptEngine scriptEngine,
                                                    ScriptObjectMirror schema,
                                                    Map<String, String> messageMap,
                                                    String startPath
                                                    ) {

        try {
            schema.entrySet().forEach(stringObjectEntry -> {
                String path = stringObjectEntry.getKey();
                //不是子路径，不执行校验，跳出
                if((startPath != null || !startPath.isEmpty()) && !path.startsWith(startPath) ){
                    return;
                }
                logger.info("rule path:{}", path);

                Object value = getValueByPath(path, boMap);
                ScriptObjectMirror rules = (ScriptObjectMirror) stringObjectEntry.getValue();
                List ruleList = new ArrayList<>();
                if (rules.isArray()) {
                    ruleList.addAll(rules.values());
                } else {
                    //支持单条直接写
                    ruleList.add(rules);
                }
                ruleList.forEach(item -> {
                    if (messageMap.containsKey(stringObjectEntry.getKey())) {
                        //已经产生错误信息，不再继续当前路径下的规则
                        return;
                    }
                    ScriptObjectMirror oneRule = (ScriptObjectMirror) item;
                    if (oneRule.containsKey(EsConstant.VALIDATOR_PROP_NAME)) {
                        validateSingleRuleFunction(boMap, value, scriptEngine, path, oneRule, messageMap);
                    } else {
                        validateSingleRule(boMap, value, scriptEngine, path, oneRule, messageMap);
                    }
                });
            });
        } catch (Exception e) {
            logger.error("validate bo error:{}", e);
            throw new RuntimeException("validate bo error:", e);
        }
        return messageMap;
    }

    /**
     * 执行自定义函数类型的校验
     * @param bo
     * @param value
     * @param scriptEngine
     * @param path
     * @param oneRule
     * @param messageMap
     */
    public void validateSingleRuleFunction(Map<String, Object> bo,
                                           Object value,
                                           ScriptEngine scriptEngine,
                                           String path,
                                           ScriptObjectMirror oneRule,
                                           Map<String, String> messageMap) {
        ScriptObjectMirror validatorFunc = (ScriptObjectMirror) oneRule.get(EsConstant.VALIDATOR_PROP_NAME);
        Map<String, Object> thiz = createThisMap(bo,scriptEngine);
        Map<String, Object> ruleMap = new HashMap<>();
        ruleMap.put("path", path);
        ScriptObjectMirror callback = null;
        //创建回调
        try {
            Invocable inv = (Invocable) scriptEngine;
            //message Callback
            callback = (ScriptObjectMirror) inv.invokeFunction(EsConstant.GET_CALLBACK,
                    new MessageCallback(messageMap,path));
            //I18N Callback
            ScriptObjectMirror i18NCallback = (ScriptObjectMirror) inv.invokeFunction(EsConstant.GET_CALLBACK,
                    new I18NCallback(Locale.SIMPLIFIED_CHINESE));
            thiz.put("$t", i18NCallback);

        } catch (Exception e) {
            logger.error("get callback error:{}", e);
            throw new RuntimeException("get callback error:", e);
        }
        //执行函数
        validatorFunc.call(thiz, ruleMap, value, callback, bo);
    }

    /**
     * 执行单条规则的校验
     * @param bo
     * @param value
     * @param scriptEngine
     * @param path
     * @param oneRule
     * @param messageMap
     */
    public void validateSingleRule(Map<String, Object> bo,
                                   Object value,
                                   ScriptEngine scriptEngine,
                                   String path,
                                   ScriptObjectMirror oneRule,
                                   Map<String, String> messageMap) {
        String type = "string";
        if (oneRule.containsKey("type")) {
            type = (String) oneRule.get("type");
        }

        List<IRuleValidator> validatorList = getItemValidator(oneRule);
        for (int i = 0; i < validatorList.size(); i++) {
            IRuleValidator itemValidator = validatorList.get(i);
            boolean isSuccess = itemValidator.validate(value, bo, type, oneRule, scriptEngine);
            //单条没成功，马上返回错误
            if (!isSuccess) {
                //应该要支持回调函数
                Object message = oneRule.get("message");
                if (message instanceof String) {
                    messageMap.put(path, message.toString());
                } else if (message instanceof ScriptObjectMirror && ((ScriptObjectMirror) message).isFunction()) {
                    Map<String, Object> thiz = createThisMap(bo,scriptEngine);
                    //I18N Callback
                    Invocable inv = (Invocable)scriptEngine;
                    try {
                        ScriptObjectMirror i18NCallback = (ScriptObjectMirror) inv.invokeFunction(EsConstant.GET_CALLBACK, new I18NCallback(Locale.SIMPLIFIED_CHINESE));
                        thiz.put("$t", i18NCallback);
                    } catch (Exception e) {
                        logger.error("get i18n callback error:{}", e);
                        throw new RuntimeException("get i18n callback error:", e);
                    }
                    Object rtnMsg = ((ScriptObjectMirror)message).call(thiz);
                    //接受返回的字符串
                    if(rtnMsg instanceof String){
                        messageMap.put(path, rtnMsg.toString());
                    }
                }
                return;
            }
        }
        //支持数组嵌套校验
        if ("array".equals(type) && oneRule.containsKey("fields")) {

            ScriptObjectMirror subSchema = (ScriptObjectMirror) oneRule.get("fields");
            //循环取出每一行数据，然后校验
            if (value instanceof List) {
                List valueList = (List) value;
                for (int i = 0; i < valueList.size(); i++) {
                    Map<String, String> subMessageMap = new HashMap<>();
                    validateBoWithSchema((Map<String, Object>) valueList.get(i), scriptEngine, subSchema, subMessageMap,null);
                    int finalJ = i;
                    //汇总单行数据的结果集
                    subMessageMap.entrySet().forEach(stringStringEntry -> {
                        messageMap.put(path + "." + finalJ + "." + stringStringEntry.getKey(), stringStringEntry.getValue());
                    });
                }
            }
        }
    }

    public List<IRuleValidator> getItemValidator(ScriptObjectMirror oneRule) {
        List<IRuleValidator> validatorList = new ArrayList<>();
        if (oneRule.containsKey(EsConstant.VALIDATOR_TYPE_REQUIRED)) {
            String key = EsConstant.VALIDATOR_TYPE_REQUIRED + "." + EsConstant.VALIDATOR_SUFFIX_NAME;
            validatorList.add(EsSpringUtils.getBean(key));
        }
        if (oneRule.containsKey("max") || oneRule.containsKey("min") || oneRule.containsKey("len")) {
            String key = EsConstant.VALIDATOR_TYPE_RANGE + "." + EsConstant.VALIDATOR_SUFFIX_NAME;
            validatorList.add(EsSpringUtils.getBean(key));
        }
        if (oneRule.containsKey("pattern")) {
            String key = EsConstant.VALIDATOR_TYPE_PATTERN + "." + EsConstant.VALIDATOR_SUFFIX_NAME;
            validatorList.add(EsSpringUtils.getBean(key));
        }
        return validatorList;
    }

    public Map<String, Object> exchangeBoToMap(Object bo) {
        try {
            String json = objectMapper.writeValueAsString(bo);
            Map<String, Object> boMap = objectMapper.readValue(json, Map.class);
            return boMap;
        } catch (JsonProcessingException e) {
            logger.error("转换bo至map异常");
            throw new RuntimeException("转换bo至map异常");
        }

    }

    public Object getValueByPath(String path, Map<String, Object> boMap) {
        String[] pathArr = path.split("\\.");
        Map<String, Object> target = boMap;
        for (int i = 0; i < pathArr.length; i++) {
            String key = pathArr[i];
            //找不到值
            if (!target.containsKey(key)) {
                logger.warn("can not find key: {},{}", path, key);
                return null;
            }
            Object value = target.get(key);
            if (i == pathArr.length - 1) {
                return value;
            } else if (i != pathArr.length - 1 && value instanceof Map) {
                target = (Map<String, Object>) value;
            } else {
                logger.warn("{} value not fit: {},{}", value, path, key);
                return null;
            }

        }
        return null;
    }
}
