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

namespace ScadaLink.InboundAPI;

/// <summary>
/// InboundAPI-005: Enforces the ScadaLink 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.
/// </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>
    /// Analyses the script source and returns the list of trust-model violations.
    /// An empty list means the script is acceptable.
    /// </summary>
    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();

        public IReadOnlyList<string> Violations => _violations;

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

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

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

            base.VisitMemberAccessExpression(node);
        }
    }
}
