using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;

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

/// <summary>
///     Post-compile sandbox guard. <c>ScriptOptions</c> alone can't reliably
///     constrain the type surface a script can reach because .NET 10's type-forwarding
///     system resolves many BCL types through multiple assemblies — restricting the
///     reference list doesn't stop <c>System.Net.Http.HttpClient</c> from being found if
///     any transitive reference forwards to <c>System.Net.Http</c>. This analyzer walks
///     the script's syntax tree after compile, uses the <see cref="SemanticModel"/> to
///     resolve every type / member reference, and rejects any whose containing namespace
///     matches a deny-list pattern.
/// </summary>
/// <remarks>
///     <para>
///         Deny-list is the authoritative Phase 7 plan set:
///         <c>System.IO</c>, <c>System.Net</c>, <c>System.Diagnostics</c>,
///         <c>System.Reflection</c>, <c>System.Threading.Thread</c>,
///         <c>System.Threading.Tasks</c> (scripts are synchronous predicates — no
///         legitimate need to start background tasks; a <c>Task.Run</c> fan-out outlives
///         the evaluation timeout entirely), <c>System.Runtime.InteropServices</c>,
///         <c>Microsoft.Win32</c>.
///     </para>
///     <para>
///         Deny-list prefix match. <c>System.Net</c> catches <c>System.Net.Http</c>,
///         <c>System.Net.Sockets</c>, <c>System.Net.NetworkInformation</c>, etc. — every
///         subnamespace. If a script needs something under a denied prefix, Phase 7's
///         operator audience authors it through a helper the plan team adds as part of
///         the <see cref="ScriptContext"/> surface, not by unlocking the namespace.
///     </para>
///     <para>
///         A namespace-prefix deny-list is necessary but not sufficient: dangerous types
///         such as <c>System.Environment</c>, <c>System.AppDomain</c>, <c>System.GC</c>,
///         and <c>System.Activator</c> live <em>directly</em> in the <c>System</c>
///         namespace inside <c>System.Private.CoreLib</c> — the same allow-listed assembly
///         that supplies primitives (<c>int</c>, <c>string</c>, <c>Math</c>). They cannot
///         be blocked by namespace because <c>System</c> itself must stay allowed. They
///         are therefore denied <em>type-granularly</em> via
///         <see cref="ForbiddenFullTypeNames"/>. <c>Environment.Exit</c> /
///         <c>Environment.FailFast</c> kill the in-process OPC UA server outright;
///         <c>Activator.CreateInstance</c> is a reflection-equivalent escape; <c>GC</c>
///         and <c>AppDomain</c> expose process-wide control. Legitimate <c>System</c>
///         types (<c>Math</c>, <c>String</c>, <c>Convert</c>, <c>DateTime</c>, …) are not
///         on the list and stay usable.
///     </para>
/// </remarks>
public static class ForbiddenTypeAnalyzer
{
    /// <summary>
    ///     Namespace prefixes scripts are NOT allowed to reference. Each string is
    ///     matched as a prefix against the resolved symbol's namespace name (dot-
    ///     delimited), so <c>System.IO</c> catches <c>System.IO.File</c>,
    ///     <c>System.IO.Pipes</c>, and any future subnamespace without needing explicit
    ///     enumeration.
    /// </summary>
    public static readonly IReadOnlyList<string> ForbiddenNamespacePrefixes =
    [
        "System.IO",
        "System.Net",
        "System.Diagnostics",           // catches Process, ProcessStartInfo, EventLog, Trace/Debug file sinks
        "System.Reflection",
        // System.Threading.Thread is NOT in this list: Thread's containing namespace is
        // "System.Threading" (not "System.Threading.Thread"), so a prefix check on
        // "System.Threading.Thread" never matches. Thread is denied type-granularly via
        // ForbiddenFullTypeNames instead so the check actually fires.
        "System.Threading.Tasks",       // Task.Run / Parallel — scripts are synchronous predicates
                                        // and have no legitimate need to start background work;
                                        // a Task fan-out outlives the evaluation timeout entirely.
        "System.Runtime.InteropServices",
        "System.Runtime.Loader",        // AssemblyLoadContext + AssemblyDependencyResolver —
                                        // arbitrary DLL load into the host process.
                                        // Namespace-prefix rather than type-granular so
                                        // future BCL additions to this namespace are
                                        // denied by default.
        "Microsoft.Win32",              // registry
    ];

    /// <summary>
    ///     Fully-qualified type names scripts are NOT allowed to reference, regardless of
    ///     namespace. Used when a dangerous type shares its namespace with legitimate types
    ///     (e.g. <c>System.Threading</c> hosts both <c>CancellationToken</c> and
    ///     <c>Thread</c>) so a namespace-prefix rule would over-block. Matched by exact
    ///     fully-qualified name against the resolved <em>type</em> symbol — every member of
    ///     the type (including read-only ones) is therefore rejected.
    /// </summary>
    /// <remarks>
    ///     <list type="bullet">
    ///         <item><c>System.Environment</c> — <c>Exit</c> / <c>FailFast</c> terminate
    ///         the host process; the whole type is denied (the read members have no
    ///         legitimate SCADA-predicate use either).</item>
    ///         <item><c>System.AppDomain</c> — process-wide assembly-load /
    ///         unhandled-exception control.</item>
    ///         <item><c>System.GC</c> — <c>Collect</c> / <c>AddMemoryPressure</c> perturb
    ///         the process memory subsystem.</item>
    ///         <item><c>System.Activator</c> — <c>CreateInstance</c> is a
    ///         reflection-equivalent escape that constructs a forbidden type by name
    ///         without ever naming it syntactically.</item>
    ///         <item><c>System.Threading.Thread</c> — raw thread creation bypasses the
    ///         per-evaluation timeout; denied type-granularly because its containing
    ///         namespace is <c>System.Threading</c> (shared with allowed types like
    ///         <c>CancellationToken</c>), so a namespace-prefix rule cannot reach it
    ///         without blocking unrelated types.</item>
    ///         <item><c>System.Threading.ThreadPool</c> / <c>System.Threading.Timer</c> —
    ///         background-work vectors that outlive the per-evaluation timeout; same
    ///         threat as <c>Task.Run</c>.</item>
    ///         <item><c>System.Runtime.CompilerServices.Unsafe</c> — exposes raw type-
    ///         reinterpretation (<c>As&lt;TFrom,TTo&gt;</c>, <c>As&lt;T&gt;(object)</c>)
    ///         that bypasses the CLR type system without requiring an <c>unsafe</c> context at
    ///         the call site; enables type-confusion and managed heap corruption.</item>
    ///     </list>
    /// </remarks>
    public static readonly IReadOnlyList<string> ForbiddenFullTypeNames =
    [
        "System.Environment",
        "System.AppDomain",
        "System.GC",
        "System.Activator",
        // System.Threading.Thread lives in the System.Threading namespace (shared with
        // CancellationToken, SemaphoreSlim, etc.), so a namespace-prefix deny-list cannot
        // target it without blocking those legitimate types. Denied type-granularly here.
        "System.Threading.Thread",
        // Broadening the references list to the BCL trusted-platform-assemblies set
        // re-exposed two background-work vectors the original deny-list missed. Both live
        // in System.Threading (shared with allowed sync primitives like CancellationToken /
        // SemaphoreSlim), so they must be denied type-granularly:
        //
        //   System.Threading.ThreadPool — QueueUserWorkItem / UnsafeQueueUserWorkItem
        //                                 re-introduce the background-fanout threat
        //                                 closed against System.Threading.Tasks.
        //   System.Threading.Timer      — Timer(callback, …) schedules unbounded work
        //                                 that outlives the per-evaluation timeout.
        //
        // System.Runtime.Loader.AssemblyLoadContext is also covered, but at the namespace-
        // prefix level above (System.Runtime.Loader) so future BCL additions to that
        // namespace are denied by default.
        "System.Threading.ThreadPool",
        "System.Threading.Timer",
        // System.Runtime.CompilerServices.Unsafe exposes raw type-reinterpretation
        // (Unsafe.As<TFrom, TTo>, Unsafe.As<T>(object)) that bypasses the CLR type system
        // without requiring an 'unsafe' compilation context at the call site. In a script,
        // calling Unsafe.As<string>(someObject) can corrupt managed heap references and
        // cause type-confusion in downstream virtual-tag consumers. Type-granular (not
        // namespace-prefix) because System.Runtime.CompilerServices also contains harmless
        // compile-time-only types (CallerMemberNameAttribute, MethodImplAttribute,
        // MethodImplOptions) that scripts may legitimately reference.
        "System.Runtime.CompilerServices.Unsafe",
    ];

    /// <summary>
    ///     Scan the <paramref name="compilation"/> for references to forbidden types.
    ///     Returns empty list when the script is clean; non-empty list means the script
    ///     must be rejected at publish with the rejections surfaced to the operator.
    /// </summary>
    /// <param name="compilation">The compilation to analyze.</param>
    /// <returns>A list of forbidden type rejections, or empty if the script is clean.</returns>
    /// <remarks>
    ///     <para>
    ///         The walker has two passes per node. Pass (1) is the member / call surface:
    ///         <c>ObjectCreationExpressionSyntax</c>, <c>InvocationExpressionSyntax</c> with
    ///         a member-access target, <c>MemberAccessExpressionSyntax</c>, and bare
    ///         <c>IdentifierNameSyntax</c> are resolved via
    ///         <see cref="SemanticModel"/>.<c>GetSymbolInfo</c>. This catches static calls
    ///         (<c>System.IO.File.ReadAllText</c>) and constructors, and is deliberately
    ///         narrow: resolving <c>GetSymbolInfo</c> on <em>every</em> node would flag
    ///         harmless inherited members (e.g. <c>typeof(int).Name</c> resolves
    ///         <c>Name</c> to <c>System.Reflection.MemberInfo</c>, the base type that
    ///         declares it, even though the receiver type <c>System.Type</c> is allowed).
    ///     </para>
    ///     <para>
    ///         Pass (2) resolves the <em>type</em> of every
    ///         <c>TypeSyntax</c> node via <c>GetTypeInfo</c>. The old walker only inspected
    ///         the four node kinds above, so a forbidden type named through
    ///         <c>typeof(System.IO.File)</c>, a generic argument
    ///         (<c>List&lt;System.IO.FileInfo&gt;</c>), a cast
    ///         (<c>(System.IO.Stream)null</c>), an <c>is</c> / <c>as</c> type pattern,
    ///         <c>default(System.Reflection.Assembly)</c>, an array-creation element type,
    ///         or an explicitly-typed local declaration produced no examined node and so
    ///         slipped through. Every <c>TypeSyntax</c> resolves to a concrete
    ///         <see cref="ITypeSymbol"/>; generic type arguments and array element types
    ///         are unwrapped recursively so a forbidden type nested at any depth is caught.
    ///     </para>
    /// </remarks>
    public static IReadOnlyList<ForbiddenTypeRejection> Analyze(Compilation compilation)
    {
        if (compilation is null) throw new ArgumentNullException(nameof(compilation));

        var rejections = new List<ForbiddenTypeRejection>();
        foreach (var tree in compilation.SyntaxTrees)
        {
            var semantic = compilation.GetSemanticModel(tree);
            var root = tree.GetRoot();
            foreach (var node in root.DescendantNodes())
            {
                // Pass (1) — member / call surface. Narrowly targeted at the node kinds
                // that name a callable member or constructor, so inherited-member
                // resolution does not produce false positives.
                switch (node)
                {
                    case ObjectCreationExpressionSyntax obj:
                        CheckSymbol(semantic.GetSymbolInfo(obj.Type).Symbol, obj.Type.Span, rejections);
                        break;
                    case InvocationExpressionSyntax inv when inv.Expression is MemberAccessExpressionSyntax memberAcc:
                        CheckSymbol(semantic.GetSymbolInfo(memberAcc.Expression).Symbol, memberAcc.Expression.Span, rejections);
                        CheckSymbol(semantic.GetSymbolInfo(inv).Symbol, inv.Span, rejections);
                        break;
                    case MemberAccessExpressionSyntax mem:
                        // Catches static calls like System.IO.File.ReadAllText(...) — the
                        // MemberAccess "System.IO.File" resolves to the File type symbol
                        // whose containing namespace is System.IO, triggering a rejection.
                        CheckSymbol(semantic.GetSymbolInfo(mem.Expression).Symbol, mem.Expression.Span, rejections);
                        break;
                    case IdentifierNameSyntax id when node.Parent is not MemberAccessExpressionSyntax:
                        CheckSymbol(semantic.GetSymbolInfo(id).Symbol, id.Span, rejections);
                        break;
                }

                // Pass (2) — type-reference surface. Every TypeSyntax
                // resolves to the type it names, regardless of the syntactic form that
                // introduced it (typeof operand, cast type, generic argument, default(T)
                // operand, array element type, is/as pattern type, declared local type).
                // Type arguments and array element types are walked recursively.
                if (node is TypeSyntax)
                    CheckTypeSymbol(semantic.GetTypeInfo(node).Type, node.Span, rejections);
            }
        }
        return rejections;
    }

    /// <summary>
    ///     Reject <paramref name="type"/> if it (or, recursively, any of its generic type
    ///     arguments / array element types) is forbidden. Walks the full type tree so a
    ///     forbidden type nested inside an allowed generic — e.g.
    ///     <c>List&lt;System.IO.FileInfo&gt;</c> — is still caught.
    /// </summary>
    private static void CheckTypeSymbol(ITypeSymbol? type, TextSpan span, List<ForbiddenTypeRejection> rejections)
    {
        if (type is null) return;

        CheckSymbol(type, span, rejections);

        switch (type)
        {
            case IArrayTypeSymbol array:
                CheckTypeSymbol(array.ElementType, span, rejections);
                break;
            case INamedTypeSymbol named:
                foreach (var arg in named.TypeArguments)
                    CheckTypeSymbol(arg, span, rejections);
                break;
        }
    }

    private static void CheckSymbol(ISymbol? symbol, TextSpan span, List<ForbiddenTypeRejection> rejections)
    {
        if (symbol is null) return;

        var typeSymbol = symbol switch
        {
            ITypeSymbol t => t,
            IMethodSymbol m => m.ContainingType,
            IPropertySymbol p => p.ContainingType,
            IFieldSymbol f => f.ContainingType,
            _ => null,
        };
        if (typeSymbol is null) return;

        var typeName = typeSymbol.ToDisplayString();

        // The broadened walk resolves both GetSymbolInfo and GetTypeInfo on every node,
        // so the same forbidden reference can be hit several times. Dedupe on span + type
        // so the operator sees one rejection per offending reference, not a noisy pile of
        // identical messages.
        if (rejections.Any(r => r.Span == span && r.TypeName == typeName))
            return;

        var ns = typeSymbol.ContainingNamespace?.ToDisplayString() ?? string.Empty;
        foreach (var forbidden in ForbiddenNamespacePrefixes)
        {
            if (ns == forbidden || ns.StartsWith(forbidden + ".", StringComparison.Ordinal))
            {
                rejections.Add(new ForbiddenTypeRejection(
                    Span: span,
                    TypeName: typeName,
                    Namespace: ns,
                    Message: $"Type '{typeName}' is in the forbidden namespace '{ns}'. " +
                             $"Scripts cannot reach {forbidden}* per Phase 7 sandbox rules."));
                return;
            }
        }

        // Type-granular deny-list — dangerous types that live in the allow-listed
        // System namespace and so cannot be caught by ForbiddenNamespacePrefixes.
        // Matched on the full type name; OriginalDefinition unwraps any generic
        // construction before naming.
        var fullTypeName = typeSymbol.OriginalDefinition.ToDisplayString(
            SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle(
                SymbolDisplayGlobalNamespaceStyle.Omitted));
        foreach (var forbiddenType in ForbiddenFullTypeNames)
        {
            if (fullTypeName == forbiddenType)
            {
                rejections.Add(new ForbiddenTypeRejection(
                    Span: span,
                    TypeName: typeSymbol.ToDisplayString(),
                    Namespace: ns,
                    Message: $"Type '{forbiddenType}' is on the Phase 7 sandbox forbidden-type " +
                             $"deny-list. Scripts cannot reach this type per sandbox rules " +
                             $"(see ForbiddenTypeAnalyzer.ForbiddenFullTypeNames for the authoritative list)."));
                return;
            }
        }
    }
}

/// <summary>A single forbidden-type reference in a user script.</summary>
public sealed record ForbiddenTypeRejection(
    TextSpan Span,
    string TypeName,
    string Namespace,
    string Message);

/// <summary>Thrown from <see cref="ScriptEvaluator{TContext, TResult}.Compile"/> when the
/// post-compile forbidden-type analyzer finds references to denied namespaces.</summary>
public sealed class ScriptSandboxViolationException : Exception
{
    /// <summary>Gets the list of forbidden type rejections that triggered this exception.</summary>
    public IReadOnlyList<ForbiddenTypeRejection> Rejections { get; }

    /// <summary>Initializes a new instance of the <see cref="ScriptSandboxViolationException"/> class.</summary>
    /// <param name="rejections">The list of forbidden type rejections found in the script.</param>
    public ScriptSandboxViolationException(IReadOnlyList<ForbiddenTypeRejection> rejections)
        : base(BuildMessage(rejections))
    {
        Rejections = rejections;
    }

    private static string BuildMessage(IReadOnlyList<ForbiddenTypeRejection> rejections)
    {
        var lines = rejections.Select(r => $"  - {r.Message}");
        return "Script references types outside the Phase 7 sandbox allow-list:\n"
               + string.Join("\n", lines);
    }
}
