using ZB.MOM.WW.ScadaBridge.Commons.Types;
using ZB.MOM.WW.ScadaBridge.ScriptAnalysis;

namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Validation;

/// <summary>
/// The authoritative design-time / deploy-blocking script gate. Delegates to the
/// shared <see cref="ZB.MOM.WW.ScadaBridge.ScriptAnalysis"/> analyzer: a
/// real Roslyn semantic forbidden-API verdict
/// (<see cref="ScriptTrustValidator.FindViolations(string, System.Collections.Generic.IEnumerable{Microsoft.CodeAnalysis.MetadataReference})"/>)
/// followed by a real CSharpScript compile against the
/// <see cref="ScriptCompileSurface"/> globals
/// (<see cref="RoslynScriptCompiler.Compile(string, System.Type, System.Collections.Generic.IEnumerable{Microsoft.CodeAnalysis.MetadataReference}, System.Collections.Generic.IEnumerable{string})"/>).
///
/// <para>
/// This is a real trust-model boundary, not the previous advisory substring +
/// brace-balance scan: the forbidden-API check resolves symbols (so namespace
/// aliases, <c>using static</c>, <c>global::</c>-qualified references, and the
/// reflection gateway are all caught), and the compile catches undefined symbols
/// and signature mismatches that a structural scan could never see.
/// </para>
/// </summary>
public class ScriptCompiler
{
    /// <summary>
    /// Attempts to compile a script and returns success or a compilation error.
    /// The security check runs first: a script that uses a forbidden API is
    /// rejected before any compile diagnostics are considered.
    /// </summary>
    /// <param name="code">The C# script code.</param>
    /// <param name="scriptName">The canonical name of the script (for error messages).</param>
    /// <returns>Success if the script is clean and compiles, or Failure with the error message.</returns>
    public Result<bool> TryCompile(string code, string scriptName)
    {
        if (string.IsNullOrWhiteSpace(code))
            return Result<bool>.Failure($"Script '{scriptName}' has empty code.");

        // The verdict is a pure function of the code (the forbidden-API policy and
        // globals surface are compile-time static), so memoise it process-wide: an
        // unchanged script compiles exactly once per process. The cached error is
        // name-free — the script name is formatted in below at read time.
        var (ok, error) = ScriptCompileVerdictCache.GetOrAdd(nameof(ScriptCompileSurface), code, () =>
        {
            // Authoritative forbidden-API verdict first — a security violation must
            // gate the script regardless of whether it otherwise compiles. Report
            // ALL violations, not just the first, so an operator fixing
            // one forbidden API isn't surprised by a second on the next attempt.
            var violations = ScriptTrustValidator.FindViolations(code);
            if (violations.Count > 0)
                return (false, $"uses forbidden API: {string.Join("; ", violations)}");

            // Real CSharpScript compile against the runtime-mirroring globals surface.
            var errors = RoslynScriptCompiler.Compile(code, typeof(ScriptCompileSurface));
            if (errors.Count > 0)
                return (false, $"failed to compile: {string.Join("; ", errors)}");

            return (true, (string?)null);
        });

        return ok
            ? Result<bool>.Success(true)
            : Result<bool>.Failure($"Script '{scriptName}' {error}");
    }
}
