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

namespace ZB.MOM.WW.ScadaBridge.InboundAPI;

/// <summary>
/// InboundAPI-005: Enforces the ScadaBridge script trust model on inbound API method
/// scripts before they are compiled into executable handlers.
///
/// The trust model (CLAUDE.md, Akka.NET conventions) forbids scripts from reaching
/// <c>System.IO</c>, <c>System.Diagnostics.Process</c>, <c>System.Threading</c>,
/// <c>System.Reflection</c>, and raw network APIs. Roslyn scripting performs no
/// API allow/deny-listing — restricting default imports is a convenience, not a
/// sandbox — so a script can fully-qualify any referenced type. This static check
/// walks the script syntax tree and rejects any reference to a forbidden namespace,
/// whether reached through a <c>using</c> directive or a fully-qualified name.
///
/// <para>
/// InboundAPI-015: a purely namespace-textual deny-list is bypassable because
/// reflection is reachable through members of <em>permitted</em> types that never
/// spell a forbidden namespace, e.g.
/// <c>typeof(string).Assembly.GetType("System.IO.File")</c>. The walker therefore
/// also rejects a curated set of reflection-gateway member names (<c>GetType</c>,
/// <c>Assembly</c>, <c>GetMethod</c>, <c>InvokeMember</c>, <c>CreateInstance</c>, …)
/// and the <c>dynamic</c> keyword. This is hardening of a best-effort static check,
/// <strong>not</strong> a true sandbox — a determined script author may still find
/// a vector the syntax walker cannot see (see the security notes in
/// <c>code-reviews/InboundAPI/findings.md</c>, InboundAPI-015). The check is
/// defence-in-depth; genuine containment needs a runtime boundary (restricted
/// <c>AssemblyLoadContext</c> / curated reference set / out-of-process sandbox).
/// </para>
/// </summary>
public static class ForbiddenApiChecker
{
    /// <summary>
    /// Namespace prefixes the trust model forbids. A script segment matches if it is
    /// equal to one of these or is a child namespace of it.
    /// </summary>
    private static readonly string[] ForbiddenNamespaces =
    {
        "System.IO",
        "System.Diagnostics",   // covers Process
        "System.Threading",     // Task/Tasks is explicitly re-allowed below
        "System.Reflection",
        "System.Net",           // raw network (Sockets, HttpClient, etc.)
        "System.Runtime.InteropServices",
        "Microsoft.Win32",
    };

    /// <summary>
    /// Namespaces that would otherwise be caught by a forbidden prefix but are
    /// required for normal async script authoring and carry no host-access risk.
    /// </summary>
    private static readonly string[] AllowedExceptions =
    {
        "System.Threading.Tasks",
    };

    /// <summary>
    /// InboundAPI-015: member names that are reflection gateways. Reaching any of
    /// these — even off a permitted type such as <c>typeof(string)</c> or a plain
    /// <c>string</c> — lets a script escape the namespace deny-list (obtain an
    /// arbitrary <c>Type</c>, load an assembly, late-bind a method). They are
    /// rejected regardless of the receiver expression. <c>Invoke</c> is deliberately
    /// excluded because <c>Action</c>/<c>Func</c> delegate invocation is legitimate;
    /// the reflection <c>MethodInfo.Invoke</c> path is already cut off by rejecting
    /// the <c>GetMethod</c>/<c>GetConstructor</c> that produces the <c>MethodInfo</c>.
    /// </summary>
    private static readonly HashSet<string> ForbiddenMemberNames = new(StringComparer.Ordinal)
    {
        "GetType",          // object.GetType() / Type.GetType(string) — yields a System.Type
        "GetTypeInfo",      // -> TypeInfo (reflection)
        "Assembly",         // Type.Assembly — yields a System.Reflection.Assembly
        "Module",           // Type.Module / MethodBase.Module
        "CreateInstance",   // Activator.CreateInstance / Assembly.CreateInstance
        "InvokeMember",     // Type.InvokeMember — late-bound dispatch
        "GetMethod",
        "GetMethods",
        "GetConstructor",
        "GetConstructors",
        "GetField",
        "GetFields",
        "GetProperty",
        "GetProperties",
        "GetMember",
        "GetMembers",
        "GetRuntimeMethod",
        "GetRuntimeMethods",
        "MethodHandle",     // RuntimeMethodHandle escape
        "TypeHandle",
    };

    /// <summary>
    /// Analyses the script source and returns the list of trust-model violations.
    /// An empty list means the script is acceptable.
    /// </summary>
    /// <param name="scriptCode">The C# script source to analyse.</param>
    /// <returns>A list of trust-model violation messages; empty if the script is clean.</returns>
    public static IReadOnlyList<string> FindViolations(string scriptCode)
    {
        if (string.IsNullOrWhiteSpace(scriptCode))
            return Array.Empty<string>();

        var tree = CSharpSyntaxTree.ParseText(
            scriptCode,
            new CSharpParseOptions(kind: SourceCodeKind.Script));

        var walker = new ForbiddenApiWalker();
        walker.Visit(tree.GetRoot());
        return walker.Violations;
    }

    private static bool IsForbidden(string dottedName)
    {
        foreach (var allowed in AllowedExceptions)
        {
            if (dottedName == allowed || dottedName.StartsWith(allowed + ".", StringComparison.Ordinal))
                return false;
        }

        foreach (var forbidden in ForbiddenNamespaces)
        {
            if (dottedName == forbidden || dottedName.StartsWith(forbidden + ".", StringComparison.Ordinal))
                return true;
        }

        return false;
    }

    private sealed class ForbiddenApiWalker : CSharpSyntaxWalker
    {
        private readonly List<string> _violations = new();

        /// <summary>Gets the accumulated list of trust-model violation descriptions.</summary>
        public IReadOnlyList<string> Violations => _violations;

        /// <inheritdoc />
        public override void VisitUsingDirective(UsingDirectiveSyntax node)
        {
            if (node.Name is not null && IsForbidden(node.Name.ToString()))
                _violations.Add($"forbidden namespace import '{node.Name}'");

            base.VisitUsingDirective(node);
        }

        /// <inheritdoc />
        public override void VisitQualifiedName(QualifiedNameSyntax node)
        {
            // Check the longest qualified name; do not descend so a single
            // System.IO.File reference is reported once, not three times.
            var text = node.ToString();
            if (IsForbidden(text))
            {
                _violations.Add($"forbidden type reference '{text}'");
                return;
            }

            base.VisitQualifiedName(node);
        }

        /// <inheritdoc />
        public override void VisitMemberAccessExpression(MemberAccessExpressionSyntax node)
        {
            // Catches fully-qualified expressions such as System.IO.File.Delete(...).
            var text = node.ToString();
            if (IsForbidden(text))
            {
                _violations.Add($"forbidden API access '{text}'");
                return;
            }

            // InboundAPI-015: reject reflection-gateway members regardless of the
            // receiver. typeof(string).Assembly.GetType("System.IO.File") never
            // spells a forbidden namespace, but '.Assembly' and '.GetType' do
            // appear here as the accessed member name.
            var memberName = node.Name.Identifier.ValueText;
            if (ForbiddenMemberNames.Contains(memberName))
            {
                _violations.Add($"forbidden reflection member access '.{memberName}'");
                // Still descend: the receiver may contain a further violation.
            }

            base.VisitMemberAccessExpression(node);
        }

        /// <inheritdoc />
        public override void VisitIdentifierName(IdentifierNameSyntax node)
        {
            // InboundAPI-015: 'dynamic' widens late-bound member access that the
            // static walker cannot see through — reject its use outright. The
            // 'dynamic' contextual keyword surfaces as an identifier name.
            if (node.Identifier.ValueText == "dynamic")
            {
                _violations.Add("forbidden use of the 'dynamic' keyword");
                return;
            }

            // InboundAPI-015: a bare reference to the reflection entry-point types
            // (e.g. 'Activator', 'Type') as an identifier. 'Activator' has no
            // non-reflection use; flag it. ('Type' as an identifier is too broad
            // to flag here — the gateway members above already cut off its use.)
            if (node.Identifier.ValueText == "Activator")
            {
                _violations.Add("forbidden reflection type reference 'Activator'");
                return;
            }

            base.VisitIdentifierName(node);
        }
    }
}
