using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using DynamicExpresso; using GB5Shared.DTO.Framework.Login; using GB5Shared.DTO.Qualifier; using Microsoft.Extensions.Logging; namespace GB5Shared.GOP.Qualifier.Executors { // ============================================================ // NCalcEvaluator — ExecutionMode 1 (NCalc expression) // // Evaluates a mathematical / logical expression stored in // SnapshotMethod.Expression using DynamicExpresso. // // Parameter binding: // - Document-level FactBag facts are bound as parameters. // - The source JsonDocument root properties are also bound. // - FactBag facts take precedence (overwrite document values). // // The expression must return a bool (true = finding suppressed, // false = finding raised). Any non-bool truthy/falsy value is // coerced to bool. // // If the expression cannot be parsed or evaluation throws, // a single Error finding is emitted and execution continues // (fail-open per finding, fail-safe per run). // ============================================================ public class NCalcEvaluator : IQualifierMethodExecutor { private readonly ILogger _Logger; public NCalcEvaluator(ILogger logger) => _Logger = logger; public string ExecutionModeKey => "NCalc"; public Task> ExecuteAsync( SnapshotMethod method, QualifierExecutionContext ctx, LoginDTO loginDTO, CancellationToken ct) { if (string.IsNullOrWhiteSpace(method.Expression)) return Task.FromResult>([]); var findings = new List(); try { var interpreter = new Interpreter(); // Bind document root properties as parameters. if (ctx.Document is not null) { try { foreach (var prop in ctx.Document.RootElement.EnumerateObject()) { var value = ExtractJsonValue(prop.Value); interpreter.SetVariable(prop.Name, value); } } catch { // Non-object root — no document params to bind. } } // Bind document-level FactBag facts (override document values). var exported = ctx.Facts.ExportAll(); if (exported.TryGetValue("Document", out var docSection) && docSection is IReadOnlyDictionary docFacts) { foreach (var (k, v) in docFacts) interpreter.SetVariable(k, v); } var result = interpreter.Eval(method.Expression); // Treat null or non-bool falsy as expression passed. bool passed = result switch { bool b => b, int i => i != 0, double d => d != 0, _ => result is not null }; if (!passed) { findings.Add(new QualifierFinding { Severity = FindingSeverity.Error, Code = $"NCALC_{method.QualifierMethodId}", Message = $"Expression evaluated to false: {method.Expression}", QualifierId = 0, // filled by facade from context QualifierCode = string.Empty }); } } catch (Exception ex) { _Logger.LogWarning(ex, "NCalcEvaluator: expression evaluation failed for method {MethodId}", method.QualifierMethodId); findings.Add(new QualifierFinding { Severity = FindingSeverity.Error, Code = $"NCALC_EVAL_ERROR_{method.QualifierMethodId}", Message = $"Expression error: {ex.Message}", QualifierId = 0, QualifierCode = string.Empty }); } return Task.FromResult>(findings); } private static object? ExtractJsonValue(JsonElement element) => element.ValueKind switch { JsonValueKind.String => element.GetString(), JsonValueKind.Number => element.TryGetInt64(out var l) ? (object)l : element.GetDouble(), JsonValueKind.True => true, JsonValueKind.False => false, JsonValueKind.Null => null, _ => element.GetRawText() }; } }