/**
 * Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
 * <p>
 * Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.gnu.org/licenses/lgpl-3.0.txt
 * <p>
 * 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.easyagents.flow.core.code.impl;

import com.alibaba.fastjson.JSONObject;
import com.easyagents.flow.core.chain.Chain;
import com.easyagents.flow.core.chain.ChainState;
import com.easyagents.flow.core.chain.NodeState;
import com.easyagents.flow.core.chain.Parameter;
import com.easyagents.flow.core.code.CodeRuntimeEngine;
import com.easyagents.flow.core.node.CodeNode;
import com.easyagents.flow.core.util.graalvm.JsInteropUtils;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Engine;
import org.graalvm.polyglot.HostAccess;
import org.graalvm.polyglot.PolyglotException;
import org.graalvm.polyglot.Source;
import org.graalvm.polyglot.SourceSection;
import org.graalvm.polyglot.Value;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 基于 GraalVM 的 JavaScript 代码节点执行器。
 *
 * <p>优先保留历史 {@code _result} 输出约定；当脚本未写入
 * {@code _result} 且声明了 {@code main} 函数时，自动传入节点参数并使用
 * {@code main} 返回的对象作为节点输出。</p>
 */
public class JavascriptRuntimeEngine implements CodeRuntimeEngine {

    private static final int SOURCE_CACHE_LIMIT = 512;
    private static final int SOURCE_CACHE_SEGMENT_COUNT = 16;
    private static final int SOURCE_CACHE_SEGMENT_LIMIT =
            SOURCE_CACHE_LIMIT / SOURCE_CACHE_SEGMENT_COUNT;
    private static final int MAX_CACHEABLE_SOURCE_CHARS = 16 * 1024;
    private static final int TIMEOUT_EXECUTOR_THREADS =
            Math.max(2, Math.min(
                    Runtime.getRuntime().availableProcessors(), 8));
    private static final String SOURCE_NAME =
            "workflow-code-node.js";
    private static final AtomicInteger TIMEOUT_THREAD_SEQUENCE =
            new AtomicInteger();
    /**
     * Engine 跨隔离 Context 共享编译缓存。
     */
    private static final Engine ENGINE = Engine.newBuilder()
            .option("engine.WarnInterpreterOnly", "false")
            .build();
    private static final Source RESULT_INIT_SOURCE =
            Source.create("js", "var _result = {};");
    private static final Source STRICT_EQUAL_SOURCE =
            Source.create(
                    "js",
                    "(left, right) => left === right;");
    private static final List<Map<String, Source>> SOURCE_CACHE_SEGMENTS =
            createSourceCacheSegments();
    private static final ScheduledThreadPoolExecutor
            TIMEOUT_EXECUTOR = createTimeoutExecutor();

    /**
     * 单次 JavaScript 执行超时，0 表示关闭。
     */
    private final long timeoutMs;

    /**
     * 创建不启用超时的 JavaScript 执行器。
     */
    public JavascriptRuntimeEngine() {
        this(0L);
    }

    /**
     * 创建 JavaScript 执行器。
     *
     * @param timeoutMs 单次执行超时，0 表示关闭
     * @throws IllegalArgumentException 超时时间为负数时抛出
     */
    public JavascriptRuntimeEngine(long timeoutMs) {
        if (timeoutMs < 0L) {
            throw new IllegalArgumentException(
                    "JavaScript 执行超时时间不能为负数");
        }
        this.timeoutMs = timeoutMs;
    }

    /**
     * 执行 JavaScript 代码并返回节点输出，兼容直接调用旧接口的场景。
     *
     * @param code 用户代码
     * @param node 当前代码节点
     * @param chain 当前工作流
     * @return 代码节点输出
     * @throws JavascriptExecutionException JavaScript 执行失败时抛出
     */
    @Override
    public Map<String, Object> execute(String code, CodeNode node, Chain chain) {
        Map<String, Object> parameterValues =
                chain.getExecutionState().resolveParameters(node);
        return execute(code, node, chain, parameterValues);
    }

    /**
     * 使用已经解析的节点参数执行 JavaScript 代码。
     *
     * @param code 用户代码
     * @param node 当前代码节点
     * @param chain 当前工作流
     * @param parameterValues 已解析的节点参数
     * @return 代码节点输出
     * @throws JavascriptExecutionException JavaScript 执行失败时抛出
     */
    @Override
    public Map<String, Object> execute(String code,
                                       CodeNode node,
                                       Chain chain,
                                       Map<String, Object> parameterValues) {
        Context context = createContext();
        AtomicBoolean timedOut = new AtomicBoolean(false);
        ScheduledFuture<?> timeoutFuture =
                scheduleTimeout(context, timedOut);
        try (Context ignored = context) {
            Value bindings = context.getBindings("js");
            ChainState chainState =
                    chain.getExecutionState();
            NodeState nodeState =
                    chain.getNodeState(node.getId());

            Map<String, Object> all =
                    chainState.getMemory();
            all.forEach((key, value) -> {
                if (!key.contains(".")) {
                    bindings.putMember(key, JsInteropUtils.wrapJavaValueForJS(context, value));
                }
            });

            if (parameterValues != null) {
                for (Map.Entry<String, Object> entry : parameterValues.entrySet()) {
                    bindings.putMember(entry.getKey(), JsInteropUtils.wrapJavaValueForJS(context, entry.getValue()));
                }
            }

            // 在 JS 中创建 _result 对象
            context.eval(RESULT_INIT_SOURCE);
            Value initialResult = bindings.getMember("_result");

            // 注入 _chain 和 _context
            bindings.putMember("_chain", chain);
            bindings.putMember("_state", nodeState);

            // 执行用户脚本
            context.eval(source(code, node));

            boolean legacyResultAssigned =
                    !context.eval(STRICT_EQUAL_SOURCE)
                            .execute(
                                    initialResult,
                                    bindings.getMember("_result"))
                            .asBoolean();
            return resolveResult(
                    context,
                    bindings,
                    node,
                    parameterValues,
                    legacyResultAssigned);
        } catch (PolyglotException e) {
            throw executionException(e, timedOut.get());
        } catch (Exception e) {
            if (timedOut.get()) {
                throw timeoutException(e);
            }
            throw new JavascriptExecutionException(
                    "JavaScript 执行失败：" + safeMessage(e),
                    e);
        } finally {
            if (timeoutFuture != null) {
                timeoutFuture.cancel(false);
            }
        }
    }

    /**
     * 创建一次隔离的 JavaScript Context。
     *
     * @return 新的 JavaScript Context
     */
    private Context createContext() {
        return Context.newBuilder("js")
                .engine(ENGINE)
                .allowHostAccess(HostAccess.ALL)
                .allowHostClassLookup(className -> false)
                .option("js.ecmascript-version", "2021")
                .build();
    }

    /**
     * 获取可复用的用户脚本 Source。
     *
     * <p>模板化脚本可能随输入生成大量不同源码，因此不进入共享缓存。
     * 超长脚本同样跳过缓存，限制源码与编译元数据占用。</p>
     *
     * @param code 解析模板后的代码
     * @param node 当前代码节点
     * @return GraalVM Source
     */
    private Source source(String code, CodeNode node) {
        String originalCode =
                node == null ? null : node.getCode();
        boolean dynamicTemplate = originalCode != null
                && originalCode.contains("{{");
        if (dynamicTemplate
                || code.length() > MAX_CACHEABLE_SOURCE_CHARS) {
            return Source.newBuilder("js", code, SOURCE_NAME)
                    .cached(false)
                    .buildLiteral();
        }
        Map<String, Source> sourceCache =
                SOURCE_CACHE_SEGMENTS.get(
                        Math.floorMod(
                                code.hashCode(),
                                SOURCE_CACHE_SEGMENT_COUNT));
        synchronized (sourceCache) {
            return sourceCache.computeIfAbsent(
                    code,
                    script -> Source.newBuilder(
                                    "js",
                                    script,
                                    SOURCE_NAME)
                            .cached(true)
                            .buildLiteral());
        }
    }

    /**
     * 创建分段有界的 Source 缓存。
     *
     * <p>每个分段独立维护 LRU 淘汰，避免所有工作流竞争同一把锁，
     * 同时保证总缓存条目不超过全局上限。</p>
     *
     * @return Source 缓存分段
     */
    private static List<Map<String, Source>>
            createSourceCacheSegments() {
        List<Map<String, Source>> segments =
                new ArrayList<>(SOURCE_CACHE_SEGMENT_COUNT);
        for (int index = 0;
             index < SOURCE_CACHE_SEGMENT_COUNT;
             index++) {
            segments.add(new LinkedHashMap<>(
                    SOURCE_CACHE_SEGMENT_LIMIT + 1,
                    0.75F,
                    true) {
                @Override
                protected boolean removeEldestEntry(
                        Map.Entry<String, Source> eldest) {
                    return size()
                            > SOURCE_CACHE_SEGMENT_LIMIT;
                }
            });
        }
        return Collections.unmodifiableList(segments);
    }

    /**
     * 安排单次执行超时。
     *
     * @param context 待取消的 Context
     * @param timedOut 超时状态
     * @return 超时任务；关闭超时时返回 {@code null}
     */
    private ScheduledFuture<?> scheduleTimeout(
            Context context,
            AtomicBoolean timedOut) {
        if (timeoutMs <= 0L) {
            return null;
        }
        return TIMEOUT_EXECUTOR.schedule(() -> {
            timedOut.set(true);
            context.close(true);
        }, timeoutMs, TimeUnit.MILLISECONDS);
    }

    /**
     * 将 GraalVM 异常转换为用户可定位的脚本异常。
     *
     * @param error GraalVM 异常
     * @param timedOut 是否由超时任务取消
     * @return JavaScript 执行异常
     */
    private JavascriptExecutionException executionException(
            PolyglotException error,
            boolean timedOut) {
        if (timedOut) {
            return timeoutException(error);
        }
        String errorType = error.isSyntaxError()
                ? "JavaScript 语法错误"
                : "JavaScript 执行失败";
        SourceSection location = sourceLocation(error);
        String locationText = location == null
                ? ""
                : "（第 " + location.getStartLine()
                + " 行，第 " + location.getStartColumn() + " 列）";
        return new JavascriptExecutionException(
                errorType + locationText + "：" + safeMessage(error),
                error);
    }

    /**
     * 获取异常对应的用户脚本位置。
     *
     * <p>部分 GraalVM 版本不会在运行时异常上直接设置位置，
     * 此时从首个 guest stack frame 补取。</p>
     *
     * @param error GraalVM 异常
     * @return 用户脚本位置；无法定位时返回 {@code null}
     */
    private SourceSection sourceLocation(PolyglotException error) {
        SourceSection location = error.getSourceLocation();
        if (location != null) {
            return location;
        }
        for (PolyglotException.StackFrame frame
                : error.getPolyglotStackTrace()) {
            if (frame.isGuestFrame()
                    && frame.getSourceLocation() != null) {
                return frame.getSourceLocation();
            }
        }
        return null;
    }

    /**
     * 创建 JavaScript 执行超时异常。
     *
     * @param cause 超时取消产生的异常
     * @return JavaScript 执行异常
     */
    private JavascriptExecutionException timeoutException(
            Throwable cause) {
        return new JavascriptExecutionException(
                "JavaScript 执行超时（" + timeoutMs + "ms）",
                cause);
    }

    /**
     * 获取稳定的异常消息。
     *
     * @param error 原始异常
     * @return 非空异常消息
     */
    private String safeMessage(Throwable error) {
        String message = error.getMessage();
        return message == null || message.isBlank()
                ? error.getClass().getSimpleName()
                : message;
    }

    /**
     * 创建超时调度器。
     *
     * @return 有界多线程守护调度器
     */
    private static ScheduledThreadPoolExecutor
            createTimeoutExecutor() {
        ScheduledThreadPoolExecutor executor =
                new ScheduledThreadPoolExecutor(
                        TIMEOUT_EXECUTOR_THREADS,
                        task -> {
                            Thread thread = new Thread(
                                    task,
                                    "javascript-runtime-timeout-"
                                            + TIMEOUT_THREAD_SEQUENCE
                                            .incrementAndGet());
                            thread.setDaemon(true);
                            return thread;
                        });
        executor.setRemoveOnCancelPolicy(true);
        return executor;
    }

    /**
     * 解析脚本输出，兼容历史 _result 写法并自动调用 main 函数。
     *
     * @param context JavaScript 执行上下文
     * @param bindings JavaScript 全局绑定
     * @param node 当前代码节点
     * @param parameterValues 当前节点已解析的输入参数
     * @param legacyResultAssigned 用户代码是否重新赋值 _result
     * @return 代码节点输出
     * @throws IllegalArgumentException main 返回值不是对象时抛出
     */
    private Map<String, Object> resolveResult(Context context,
                                              Value bindings,
                                              CodeNode node,
                                              Map<String, Object> parameterValues,
                                              boolean legacyResultAssigned) {
        JSONObject legacyResult = GraalvmToFastJSONUtils.toJSONObject(
                bindings.getMember("_result"));
        if (legacyResultAssigned || !legacyResult.isEmpty()) {
            return legacyResult;
        }

        Value mainFunction = bindings.getMember("main");
        if (mainFunction == null || !mainFunction.canExecute()) {
            return legacyResult;
        }

        Value mainResult = mainFunction.execute(
                resolveMainArguments(context, node, parameterValues));
        Object convertedResult =
                GraalvmToFastJSONUtils.toFastJsonValue(mainResult);
        if (!(convertedResult instanceof JSONObject)) {
            throw new IllegalArgumentException(
                    "JavaScript main 函数必须返回对象，例如：return { result: value }");
        }
        return (JSONObject) convertedResult;
    }

    /**
     * 根据节点契约构建 main 自动调用参数。
     *
     * @param context JavaScript 执行上下文
     * @param node 当前代码节点
     * @param parameterValues 当前节点已解析的输入参数
     * @return GraalVM main 调用参数
     */
    private Object[] resolveMainArguments(
            Context context,
            CodeNode node,
            Map<String, Object> parameterValues) {
        if (node != null
                && CodeNode.MAIN_ARGS_MODE_NAMED.equals(
                        node.getMainArgsMode())) {
            List<Parameter> parameters = node.getParameters();
            if (parameters == null || parameters.isEmpty()) {
                return new Object[0];
            }
            Object[] arguments = new Object[parameters.size()];
            for (int index = 0; index < parameters.size(); index++) {
                String name = parameters.get(index).getName();
                Object value = parameterValues == null
                        ? null
                        : parameterValues.get(name);
                arguments[index] =
                        JsInteropUtils.wrapJavaValueForJS(
                                context, value);
            }
            return arguments;
        }

        Map<String, Object> mainInput = parameterValues == null
                ? Collections.emptyMap()
                : parameterValues;
        return new Object[]{
                JsInteropUtils.wrapJavaValueForJS(
                        context, mainInput)
        };
    }

}
