using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;

namespace ZB.MOM.WW.OtOpcUa.Core.Scripting;

/// <summary>
///     Compiles + runs user scripts against a <see cref="ScriptContext"/> subclass. Core
///     evaluator — no caching, no timeout, no logging side-effects yet (those land in
///     Stream A.3, A.4, A.5 respectively). Stream B + C wrap this with the dependency
///     scheduler + alarm state machine.
/// </summary>
/// <remarks>
///     <para>
///         Scripts are compiled against <see cref="ScriptGlobals{TContext}"/> so the
///         context member is named <c>ctx</c> in the script, matching the
///         <see cref="DependencyExtractor"/>'s walker and the Admin UI type stub.
///     </para>
///     <para>
///         Compile pipeline is a three-step gate: (1) Roslyn compile — catches syntax
///         errors + type-resolution failures, throws <see cref="CompilationErrorException"/>;
///         (2) <see cref="ForbiddenTypeAnalyzer"/> runs against the semantic model —
///         catches sandbox escapes that slipped past reference restrictions due to .NET's
///         type forwarding, throws <see cref="ScriptSandboxViolationException"/>; (3)
///         delegate creation — throws at this layer only for internal Roslyn bugs, not
///         user error.
///     </para>
///     <para>
///         Runtime exceptions thrown from user code propagate unwrapped. The virtual-tag
///         engine (Stream B) catches them per-tag + maps to <c>BadInternalError</c>
///         quality per Phase 7 decision #11 — this layer doesn't swallow anything so
///         tests can assert on the original exception type.
///     </para>
/// </remarks>
public sealed class ScriptEvaluator<TContext, TResult>
    where TContext : ScriptContext
{
    private readonly ScriptRunner<TResult> _runner;

    private ScriptEvaluator(ScriptRunner<TResult> runner)
    {
        _runner = runner;
    }

    public static ScriptEvaluator<TContext, TResult> Compile(string scriptSource)
    {
        if (scriptSource is null) throw new ArgumentNullException(nameof(scriptSource));

        var options = ScriptSandbox.Build(typeof(TContext));
        var script = CSharpScript.Create<TResult>(
            code: scriptSource,
            options: options,
            globalsType: typeof(ScriptGlobals<TContext>));

        // Step 1 — Roslyn compile. Throws CompilationErrorException on syntax / type errors.
        var diagnostics = script.Compile();

        // Step 2 — forbidden-type semantic analysis. Defense-in-depth against reference-list
        // leaks due to type forwarding.
        var rejections = ForbiddenTypeAnalyzer.Analyze(script.GetCompilation());
        if (rejections.Count > 0)
            throw new ScriptSandboxViolationException(rejections);

        // Step 3 — materialize the callable delegate.
        var runner = script.CreateDelegate();
        return new ScriptEvaluator<TContext, TResult>(runner);
    }

    /// <summary>Run against an already-constructed context.</summary>
    public Task<TResult> RunAsync(TContext context, CancellationToken ct = default)
    {
        if (context is null) throw new ArgumentNullException(nameof(context));
        var globals = new ScriptGlobals<TContext> { ctx = context };
        return _runner(globals, ct);
    }
}
