package lc.esp.script.engine;

import com.oracle.truffle.js.scriptengine.GraalJSScriptEngine;
import lc.mecha.log.MechaLogger;
import lc.mecha.log.MechaLoggerFactory;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Source;
import org.graalvm.polyglot.io.IOAccess;

import javax.script.ScriptEngine;
import java.io.IOException;
import java.time.Duration;
import java.util.concurrent.*;

/**
 * A context for the ESP/SCRIPT system. This provides Javascript execution support within the ESP environment and
 * provides the necessary features for binding the ESP language features into the script environment.
 *
 * The script engine is backed with an {@link EOFileSystem} object which provides access to EOFS. This allows
 * the scripts themselves to be stored in EOFS and executed directly. During execution, other scripts may be
 * imported from EOFS.
 *
 * Security: The script engine provides support for partitioning the script from the host; for example, removing any
 * access to unsanctioned Java objects within the JVM as well as unsanctioned access to host resources list the
 * filesystem or networking. Basic controls are provided to limit overall script execution time.
 *
 * It is not thought that this implementation provides enough security control to allow the execution of mutually
 * un-trusted scripts. Only mutually trusted scripts should be executed within a single JVM task. This is due to a wide
 * variety of factors, not least of which are threats posed by spectre/meltdown type attacks. Protecting against that
 * class of attack is beyond the scope of this implementation, and, possibly, Java itself.
 *
 * @author Alex Leigh
 * @since mk20 (TOKYO PROTOCOL)
 */
public class ESPScriptEngine {
    private final ScriptEngine engine;
    private static final MechaLogger logger = MechaLoggerFactory.getLogger(ESPScriptEngine.class);
    private int timeoutMs = 10 * 1000;

    public ESPScriptEngine() {
        engine = GraalJSScriptEngine.create(null,
                Context.newBuilder("js")
                        .option("js.ecmascript-version", "2022")
                        .option("js.script-engine-global-scope-import", "false")
        );
    }

    public Context getContext() {
        return Context.newBuilder("js")
                .allowIO(IOAccess.newBuilder()
                        .fileSystem(new EOFileSystem()).build()).build();
    }

    /**
     * Evaluate the given script.
     * <p>
     * The script will be allowed to run for up to the timeout quota. If the timeout quota is exceeded, then the
     * script will be aborted and {@link TimeoutException} will be thrown.
     */
    public void eval(String script) throws TimeoutException, ExecutionException, InterruptedException, IOException {
        final ExecutorService executor = Executors.newSingleThreadExecutor();
        final Context context = getContext();
        final Source source1 = Source.newBuilder("js", script, "unnamed.mjs").build();
        final Future<Object> futureResult = executor.submit(() -> context.eval(source1));
        try {
            final Object result = futureResult.get(timeoutMs, TimeUnit.MILLISECONDS);
            logger.info("Script evaluated within 10 seconds, result: " + result);
        } catch (TimeoutException e) {
            context.interrupt(Duration.ZERO);
            logger.warn("Script execution time limit exceeded. Aborting.");
            throw new TimeoutException("Execution limit exceeded.");
        }
    }

    public int getTimeoutMs() {
        return timeoutMs;
    }

    /**
     * Set the script execution quota in milliseconds. The timeout is realtime, not execution time. If a script
     * execution exceeds this amount it will be terminated.
     */
    public void setTimeoutMs(int timeoutMs) {
        this.timeoutMs = timeoutMs;
    }

    public Object get(String key) {
        return engine.get(key);
    }

    public ScriptEngine getEngine() {
        return engine;
    }
}
