using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Scripting;
using Microsoft.Extensions.Logging;
using ZB.MOM.WW.ScadaBridge.Commons.Types;

namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;

/// <summary>
/// WP-19: Script Trust Model — compiles C# scripts using Roslyn with restricted API access.
/// Forbidden APIs: System.IO, Process, Threading (except async/await), Reflection,
/// System.Net.Sockets, System.Net.Http.
/// </summary>
public class ScriptCompilationService
{
    private readonly ILogger<ScriptCompilationService> _logger;

    /// <summary>
    /// Forbidden API roots. Each entry is matched as a prefix against both the resolved
    /// symbol's containing namespace and its fully-qualified containing type name, so an
    /// entry may name a whole namespace ("System.IO") or a single type
    /// ("System.Diagnostics.Process").
    /// </summary>
    private static readonly string[] ForbiddenNamespaces =
    [
        "System.IO",
        "System.Diagnostics.Process",
        "System.Threading",
        "System.Reflection",
        "System.Net.Sockets",
        "System.Net.Http"
    ];

    /// <summary>
    /// Specific namespaces/types allowed even though they sit under a forbidden root.
    /// async/await and cancellation tokens are OK despite System.Threading being blocked.
    /// </summary>
    private static readonly string[] AllowedExceptions =
    [
        "System.Threading.Tasks",
        "System.Threading.CancellationToken",
        "System.Threading.CancellationTokenSource"
    ];

    /// <summary>Initializes a new instance of the ScriptCompilationService class.</summary>
    /// <param name="logger">Logger instance.</param>
    public ScriptCompilationService(ILogger<ScriptCompilationService> logger)
    {
        _logger = logger;
    }

    /// <summary>
    /// SiteRuntime-011: validates that the script does not reference forbidden APIs.
    ///
    /// Validation is performed with Roslyn semantic analysis rather than a raw substring
    /// scan of the source text. The script is parsed and a semantic model is built; every
    /// identifier, type reference, member access, and object creation is resolved to its
    /// symbol and the symbol's containing namespace is checked against the forbidden list.
    ///
    /// This is reliable in both directions a textual scan was not:
    /// - it catches forbidden types regardless of how they are written (<c>global::</c>
    ///   prefixes, aliases, transitively-imported namespaces) because it inspects the
    ///   resolved symbol, not the spelling;
    /// - it does not raise false positives for the namespace string appearing in a
    ///   comment, a string literal, or an unrelated identifier.
    ///
    /// Returns a list of violation messages, empty if clean.
    /// </summary>
    /// <param name="code">The script code to validate.</param>
    /// <returns>A list of trust-model violation messages; empty if the script is clean.</returns>
    public IReadOnlyList<string> ValidateTrustModel(string code)
    {
        var tree = CSharpSyntaxTree.ParseText(
            code, new CSharpParseOptions(kind: SourceCodeKind.Script));

        var compilation = CSharpCompilation.CreateScriptCompilation(
            "TrustValidation",
            tree,
            ScriptReferences,
            new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

        var model = compilation.GetSemanticModel(tree);
        var root = tree.GetRoot();

        // Deduplicate so a forbidden symbol used many times is reported once but
        // distinct forbidden symbols are all reported.
        var violations = new SortedSet<string>(StringComparer.Ordinal);

        foreach (var node in root.DescendantNodes())
        {
            // Only inspect nodes that name a type or member; skip declarations,
            // string literals and comments entirely. Member-access and qualified-name
            // parents are evaluated as a whole, so their nested name parts are skipped.
            if (node is not (SimpleNameSyntax or MemberAccessExpressionSyntax
                or QualifiedNameSyntax or ObjectCreationExpressionSyntax))
            {
                continue;
            }

            var info = model.GetSymbolInfo(node);
            var symbol = info.Symbol ?? info.CandidateSymbols.FirstOrDefault();

            // The set of fully-qualified scopes this reference touches: the resolved
            // symbol's containing namespace and type, or — when the symbol could not
            // be resolved (a type from an unreferenced assembly) — the syntactic
            // fully-qualified name written in source as a safe fallback.
            var scopes = symbol != null
                ? GetSymbolScopes(symbol)
                : GetSyntacticScopes(node);
            if (scopes.Count == 0)
                continue;

            var forbidden = ForbiddenNamespaces.FirstOrDefault(
                f => scopes.Any(s => IsUnderScope(s, f)));
            if (forbidden == null)
                continue;

            // Allow specific exception namespaces/types (async/await, cancellation).
            if (scopes.Any(s => AllowedExceptions.Any(a => IsUnderScope(s, a))))
                continue;

            var name = symbol?.Name ?? node.ToString();
            violations.Add($"Forbidden API reference: '{forbidden}' ({scopes[0]}.{name})");
        }

        return violations.ToList();
    }

    /// <summary>
    /// Returns the fully-qualified scopes a resolved symbol belongs to — its containing
    /// namespace and, for a type or member, the fully-qualified containing type. A bare
    /// namespace symbol is intentionally ignored: a namespace name on its own performs
    /// no action; harm requires referencing a type or a member.
    /// </summary>
    private static List<string> GetSymbolScopes(ISymbol symbol)
    {
        var scopes = new List<string>();

        switch (symbol)
        {
            case INamespaceSymbol:
                // A namespace reference alone is harmless — skip it. (This avoids a
                // false positive on the "System.Threading" qualifier of the allowed
                // "System.Threading.Tasks.Task".)
                break;
            case ITypeSymbol typeSymbol:
                scopes.Add(typeSymbol.ToDisplayString());
                if (typeSymbol.ContainingNamespace is { IsGlobalNamespace: false } typeNs)
                    scopes.Add(typeNs.ToDisplayString());
                break;
            default:
                if (symbol.ContainingType != null)
                {
                    scopes.Add(symbol.ContainingType.ToDisplayString());
                    if (symbol.ContainingType.ContainingNamespace is { IsGlobalNamespace: false } memberNs)
                        scopes.Add(memberNs.ToDisplayString());
                }
                else if (symbol.ContainingNamespace is { IsGlobalNamespace: false } ns)
                {
                    scopes.Add(ns.ToDisplayString());
                }
                break;
        }

        return scopes;
    }

    /// <summary>
    /// Fallback used when a name could not be resolved to a symbol (e.g. a type from an
    /// assembly the script is not allowed to reference). The fully-qualified name as
    /// written in source is used directly — a script that names
    /// <c>System.Net.Http.HttpClient</c> is still rejected even though that assembly is
    /// deliberately absent from the script's metadata references.
    /// </summary>
    private static List<string> GetSyntacticScopes(SyntaxNode node)
    {
        // A dotted name written in source is itself the fully-qualified scope. Only
        // consider names that actually contain a dot — bare local identifiers cannot
        // reach a forbidden namespace.
        var text = node switch
        {
            QualifiedNameSyntax q => q.ToString(),
            MemberAccessExpressionSyntax m => m.ToString(),
            _ => string.Empty
        };

        // Strip whitespace/newlines that a multi-line member-access chain may contain.
        text = new string(text.Where(c => !char.IsWhiteSpace(c)).ToArray());

        return string.IsNullOrEmpty(text) || !text.Contains('.')
            ? []
            : [text];
    }

    /// <summary>
    /// True if <paramref name="actual"/> is exactly, or nested within,
    /// <paramref name="root"/> (e.g. "System.IO.Compression" is under "System.IO",
    /// "System.Diagnostics.Process" is under "System.Diagnostics.Process").
    /// </summary>
    private static bool IsUnderScope(string actual, string root)
        => actual.Equals(root, StringComparison.Ordinal)
           || actual.StartsWith(root + ".", StringComparison.Ordinal);

    /// <summary>
    /// Assemblies referenced by compiled scripts. Shared between the Roslyn scripting
    /// options and the semantic-analysis compilation built for trust validation
    /// (SiteRuntime-011), so the validator resolves symbols against exactly the same
    /// metadata the script is compiled against.
    /// </summary>
    private static readonly System.Reflection.Assembly[] ScriptAssemblies =
    [
        typeof(object).Assembly,
        typeof(Enumerable).Assembly,
        typeof(Math).Assembly,
        typeof(Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo).Assembly,
        typeof(Commons.Types.DynamicJsonElement).Assembly
    ];

    /// <summary>
    /// Metadata references for the trust-validation semantic compilation.
    /// </summary>
    private static readonly MetadataReference[] ScriptReferences =
        ScriptAssemblies
            .Select(a => (MetadataReference)MetadataReference.CreateFromFile(a.Location))
            .ToArray();

    /// <summary>
    /// Shared Roslyn scripting options (references + imports) used by both full
    /// script compilation and trigger-expression compilation.
    /// </summary>
    private static ScriptOptions BuildScriptOptions() => ScriptOptions.Default
        .WithReferences(ScriptAssemblies)
        .WithImports(
            "System",
            "System.Collections.Generic",
            "System.Linq",
            "System.Threading.Tasks");

    /// <summary>
    /// Compiles a script into a reusable delegate that takes a ScriptRuntimeContext
    /// and parameters dictionary, and returns an object? result.
    /// </summary>
    /// <param name="scriptName">The name of the script.</param>
    /// <param name="code">The script code to compile.</param>
    /// <returns>A <see cref="ScriptCompilationResult"/> containing the compiled script on success, or error messages on failure.</returns>
    public ScriptCompilationResult Compile(string scriptName, string code)
        => CompileCore(scriptName, code, typeof(ScriptGlobals));

    /// <summary>
    /// Compiles a bare C# boolean trigger expression against the restricted
    /// read-only <see cref="TriggerExpressionGlobals"/>. The expression is a
    /// trailing expression (no <c>return</c>); Roslyn scripting yields its
    /// value, which the caller coerces to <c>bool</c>. Reuses the same script
    /// options and forbidden-API trust validation as <see cref="Compile"/>.
    /// </summary>
    /// <param name="name">The name of the trigger expression.</param>
    /// <param name="expression">The trigger expression to compile.</param>
    /// <returns>A <see cref="ScriptCompilationResult"/> containing the compiled expression on success, or error messages on failure.</returns>
    public ScriptCompilationResult CompileTriggerExpression(string name, string expression)
        => CompileCore(name, expression, typeof(TriggerExpressionGlobals));

    /// <summary>
    /// Shared compilation path: validates the trust model, builds the script
    /// against the given globals type, and returns the compiled result.
    /// </summary>
    private ScriptCompilationResult CompileCore(string name, string code, Type globalsType)
    {
        // Validate trust model
        var violations = ValidateTrustModel(code);
        if (violations.Count > 0)
        {
            _logger.LogWarning(
                "Script {Script} failed trust validation: {Violations}",
                name, string.Join("; ", violations));
            return ScriptCompilationResult.Failed(violations);
        }

        try
        {
            var script = CSharpScript.Create<object?>(
                code,
                BuildScriptOptions(),
                globalsType: globalsType);

            var diagnostics = script.Compile();
            var errors = diagnostics
                .Where(d => d.Severity == DiagnosticSeverity.Error)
                .Select(d => d.GetMessage())
                .ToList();

            if (errors.Count > 0)
            {
                _logger.LogWarning(
                    "Script {Script} compilation failed: {Errors}",
                    name, string.Join("; ", errors));
                return ScriptCompilationResult.Failed(errors);
            }

            _logger.LogDebug("Script {Script} compiled successfully", name);
            return ScriptCompilationResult.Succeeded(script);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Unexpected error compiling script {Script}", name);
            return ScriptCompilationResult.Failed([$"Compilation exception: {ex.Message}"]);
        }
    }
}

/// <summary>
/// Result of script compilation, containing either the compiled script or error messages.
/// </summary>
public class ScriptCompilationResult
{
    /// <summary>Indicates whether compilation succeeded.</summary>
    public bool IsSuccess { get; }
    /// <summary>The compiled script, or null if compilation failed.</summary>
    public Script<object?>? CompiledScript { get; }
    /// <summary>List of error messages, empty if compilation succeeded.</summary>
    public IReadOnlyList<string> Errors { get; }

    private ScriptCompilationResult(bool success, Script<object?>? script, IReadOnlyList<string> errors)
    {
        IsSuccess = success;
        CompiledScript = script;
        Errors = errors;
    }

    /// <summary>Creates a successful compilation result.</summary>
    /// <param name="script">The compiled script.</param>
    /// <returns>A <see cref="ScriptCompilationResult"/> with <see cref="IsSuccess"/> set to <c>true</c> and the compiled script attached.</returns>
    public static ScriptCompilationResult Succeeded(Script<object?> script) =>
        new(true, script, []);

    /// <summary>Creates a failed compilation result.</summary>
    /// <param name="errors">List of error messages.</param>
    /// <returns>A <see cref="ScriptCompilationResult"/> with <see cref="IsSuccess"/> set to <c>false</c> and the provided error messages.</returns>
    public static ScriptCompilationResult Failed(IReadOnlyList<string> errors) =>
        new(false, null, errors);
}

/// <summary>
/// Global variables available to compiled scripts. The ScriptRuntimeContext is injected
/// as the "Instance" global, and parameters are available via "Parameters".
/// </summary>
public class ScriptGlobals
{
    /// <summary>The script runtime context providing access to instance state.</summary>
    public ScriptRuntimeContext Instance { get; set; } = null!;
    /// <summary>Script parameters passed by the caller.</summary>
    public ScriptParameters Parameters { get; set; } = new ScriptParameters();
    /// <summary>Cancellation token for script execution.</summary>
    public CancellationToken CancellationToken { get; set; }

    /// <summary>
    /// Alarm context when this script is invoked as an on-trigger handler.
    /// Null for instance scripts, shared scripts, and inbound-API-routed
    /// scripts. Lets on-trigger scripts read the firing alarm's Name, Level
    /// (HiLo only), Priority, and per-band Message to branch routing logic.
    /// </summary>
    public Commons.Types.Scripts.AlarmContext? Alarm { get; set; }

    /// <summary>
    /// Where this script sits in the composition tree. Defaults to root for
    /// scripts on top-level templates; a flattened composed script gets
    /// SelfPath = "TempSensor" (etc.) and a ParentPath set to one level up.
    /// </summary>
    public Commons.Types.Scripts.ScriptScope Scope { get; set; } =
        Commons.Types.Scripts.ScriptScope.Root;

    /// <summary>
    /// Top-level ExternalSystem access for scripts (delegates to Instance.ExternalSystem).
    /// Usage: ExternalSystem.Call("systemName", "methodName", params)
    /// </summary>
    public ScriptRuntimeContext.ExternalSystemHelper ExternalSystem => Instance.ExternalSystem;

    /// <summary>
    /// Top-level Database access for scripts (delegates to Instance.Database).
    /// Usage: Database.Connection("name") or Database.CachedWrite("name", "sql", params)
    /// </summary>
    public ScriptRuntimeContext.DatabaseHelper Database => Instance.Database;

    /// <summary>
    /// Top-level Notify access for scripts (delegates to Instance.Notify).
    /// Usage: Notify.To("listName").Send("subject", "message")
    /// </summary>
    public ScriptRuntimeContext.NotifyHelper Notify => Instance.Notify;

    /// <summary>
    /// Top-level Scripts access for shared script calls (delegates to Instance.Scripts).
    /// Usage: Scripts.CallShared("scriptName", params)
    /// </summary>
    public ScriptRuntimeContext.ScriptCallHelper Scripts => Instance.Scripts;

    /// <summary>
    /// Read/write the current template's attributes by name. Resolves to the
    /// canonical name for the script's scope, so a script on a composed
    /// TempSensor reads its own Temperature via <c>Attributes["Temperature"]</c>.
    /// </summary>
    public AttributeAccessor Attributes => new(Instance, Scope.SelfPath);

    /// <summary>
    /// Indexed access to child compositions.
    /// <c>Children["TempSensor"].Attributes["Temperature"]</c> reads the
    /// composed child's attribute. <c>Children["TempSensor"].CallScript("Sample")</c>
    /// invokes a script on the child.
    /// </summary>
    public ChildrenAccessor Children => new(Instance, Scope.SelfPath);

    /// <summary>
    /// Parent composition (null when this script is on a root-level template).
    /// <c>Parent.Attributes["SpeedRPM"]</c> reaches the parent's attribute;
    /// <c>Parent.CallScript("Trip")</c> invokes a parent script.
    /// </summary>
    public CompositionAccessor? Parent =>
        Scope.ParentPath == null ? null : new CompositionAccessor(Instance, Scope.ParentPath);
}
