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 decision #6 set:
///         <c>System.IO</c>, <c>System.Net</c>, <c>System.Diagnostics.Process</c>,
///         <c>System.Reflection</c>, <c>System.Threading.Thread</c>,
///         <c>System.Runtime.InteropServices</c>. <c>System.Environment</c> (for process
///         env-var read) is explicitly left allowed — it's read-only process state, doesn't
///         persist outside, and the test file pins this compromise so tightening later is
///         a deliberate plan decision.
///     </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>
/// </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",  // raw Thread — Tasks stay allowed (different namespace)
        "System.Runtime.InteropServices",
        "Microsoft.Win32",          // registry
    ];

    /// <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>
    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())
            {
                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;
                }
            }
        }
        return rejections;
    }

    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 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: typeSymbol.ToDisplayString(),
                    Namespace: ns,
                    Message: $"Type '{typeSymbol.ToDisplayString()}' is in the forbidden namespace '{ns}'. " +
                             $"Scripts cannot reach {forbidden}* per Phase 7 sandbox rules."));
                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
{
    public IReadOnlyList<ForbiddenTypeRejection> Rejections { get; }

    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);
    }
}
