using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

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

/// <summary>
///     Factory for the <see cref="ScriptOptions"/> every user script is compiled against.
///     Implements Phase 7 plan decision #6 (read-only sandbox) by whitelisting only the
///     assemblies + namespaces the script API needs; no <c>System.IO</c>, no
///     <c>System.Net</c>, no <c>System.Diagnostics.Process</c>, no
///     <c>System.Reflection</c>. Attempts to reference those types in a script fail at
///     compile with a compiler error that points at the exact span — the operator sees
///     the rejection before publish, not at evaluation.
/// </summary>
/// <remarks>
///     <para>
///         Roslyn's default <see cref="ScriptOptions"/> references <c>mscorlib</c> /
///         <c>System.Runtime</c> transitively which pulls in every type in the BCL — this
///         class overrides that with an explicit minimal allow-list.
///     </para>
///     <para>
///         Namespaces pre-imported so scripts don't have to write <c>using</c> clauses:
///         <c>System</c>, <c>System.Math</c>-style statics are reachable via
///         <see cref="Math"/>, and <c>ZB.MOM.WW.OtOpcUa.Core.Abstractions</c> so scripts
///         can name <see cref="DataValueSnapshot"/> directly.
///     </para>
///     <para>
///         The sandbox cannot prevent a script from allocating unbounded memory or
///         spinning in a tight loop — those are budget concerns, handled by the
///         per-evaluation timeout (Stream A.4) + the test-harness (Stream F.4) that lets
///         operators preview output before publishing.
///     </para>
/// </remarks>
public static class ScriptSandbox
{
    /// <summary>
    ///     Build the <see cref="ScriptOptions"/> used for every virtual-tag / alarm
    ///     script. <paramref name="contextType"/> is the concrete
    ///     <see cref="ScriptContext"/> subclass the globals will be of — the compiler
    ///     uses its type to resolve <c>ctx.GetTag(...)</c> calls.
    /// </summary>
    public static ScriptOptions Build(Type contextType)
    {
        if (contextType is null) throw new ArgumentNullException(nameof(contextType));
        if (!typeof(ScriptContext).IsAssignableFrom(contextType))
            throw new ArgumentException(
                $"Script context type must derive from {nameof(ScriptContext)}", nameof(contextType));

        // Allow-listed assemblies — each explicitly chosen. Adding here is a
        // plan-level decision; do not expand casually. HashSet so adding the
        // contextType's assembly is idempotent when it happens to be Core.Scripting
        // already.
        var allowedAssemblies = new HashSet<System.Reflection.Assembly>
        {
            // System.Private.CoreLib — primitives (int, double, bool, string, DateTime,
            // TimeSpan, Math, Convert, nullable<T>). Can't practically script without it.
            typeof(object).Assembly,
            // System.Linq — IEnumerable extensions (Where / Select / Sum / Average / etc.).
            typeof(System.Linq.Enumerable).Assembly,
            // Core.Abstractions — DataValueSnapshot + DriverDataType so scripts can name
            // the types they receive from ctx.GetTag.
            typeof(DataValueSnapshot).Assembly,
            // Core.Scripting itself — ScriptContext base class + Deadband static.
            typeof(ScriptContext).Assembly,
            // Serilog.ILogger — script-side logger type.
            typeof(Serilog.ILogger).Assembly,
            // Concrete context type's assembly — production contexts subclass
            // ScriptContext in Core.VirtualTags / Core.ScriptedAlarms; tests use their
            // own subclass. The globals wrapper is generic on this type so Roslyn must
            // be able to resolve it during compilation.
            contextType.Assembly,
        };

        var allowedImports = new[]
        {
            "System",
            "System.Linq",
            "ZB.MOM.WW.OtOpcUa.Core.Abstractions",
            "ZB.MOM.WW.OtOpcUa.Core.Scripting",
        };

        return ScriptOptions.Default
            .WithReferences(allowedAssemblies)
            .WithImports(allowedImports);
    }
}
