using OptimAI.BRE.Shared.Domain; using System.Text.Json.Nodes; namespace OptimAI.BRE.RuleEngine.Domain; public interface IRuleExecutionService { Task ExecuteAsync(ExecutionContext context, CancellationToken ct = default); Task EvaluateRuleAsync(Rule rule, ExecutionContext context, CancellationToken ct = default); Task EvaluateConditionGroupAsync(ConditionGroup group, DynamicDataContext data, CancellationToken ct = default); } public interface IRuleLoader { Task> LoadRulesAsync(RuleLoadRequest request, CancellationToken ct = default); Task LoadRuleByCodeAsync(Guid tenantId, string ruleCode, CancellationToken ct = default); } public interface IDynamicFieldResolver { object? Resolve(string fieldPath, DynamicDataContext context); bool TryResolve(string fieldPath, DynamicDataContext context, out object? value); } public interface IConditionEvaluator { bool Evaluate(ConditionNode condition, DynamicDataContext context); } public interface IActionExecutor { Task ExecuteAsync(RuleAction action, ExecutionContext context, RuleExecutionState state); } public interface IRiskScoringEngine { Task CalculateAsync(ExecutionContext context, List results); } // ============================================================ // EXECUTION CONTEXT & STATE // ============================================================ public class ExecutionContext { public Guid TenantId { get; set; } public string? CorrelationId { get; set; } public string? ApplicationId { get; set; } public string? ProductCode { get; set; } public string? BranchCode { get; set; } public string? StageCode { get; set; } public string? SourceSystem { get; set; } public Guid? RuleSetId { get; set; } public DynamicDataContext Data { get; set; } = default!; public ExecutionOptions Options { get; set; } = new(); public Guid? RequestedBy { get; set; } } public class ExecutionOptions { public bool EnableAiAnalysis { get; set; } = true; public bool EnableRiskScoring { get; set; } = true; public bool StopOnFirstReject { get; set; } = false; public int TimeoutMs { get; set; } = 5000; public bool IncludeConditionDetails { get; set; } = true; public bool EnableCaching { get; set; } = true; } public class DynamicDataContext { private readonly JsonObject _root; public DynamicDataContext(Dictionary data) { _root = ConvertToJsonObject(data); } public DynamicDataContext(JsonObject root) { _root = root; } public object? GetValue(string path) { var parts = path.Split('.'); JsonNode? current = _root; foreach (var part in parts) { if (current is JsonObject obj) { if (!obj.TryGetPropertyValue(part, out current) || current == null) return null; } else if (current is JsonArray arr && int.TryParse(part, out var idx)) { current = idx < arr.Count ? arr[idx] : null; } else { return null; } } return ExtractValue(current); } public bool SetValue(string path, object value) { var parts = path.Split('.'); JsonObject? current = _root; for (int i = 0; i < parts.Length - 1; i++) { if (current == null) return false; if (!current.TryGetPropertyValue(parts[i], out var next) || next is not JsonObject nextObj) { nextObj = new JsonObject(); current[parts[i]] = nextObj; } current = nextObj as JsonObject; } if (current == null) return false; current[parts[^1]] = JsonValue.Create(value); return true; } public Dictionary ToDictionary() { var result = new Dictionary(); FlattenJsonObject(_root, "", result); return result; } private static object? ExtractValue(JsonNode? node) => node switch { JsonValue v when v.TryGetValue(out var d) => d, JsonValue v when v.TryGetValue(out var b) => b, JsonValue v when v.TryGetValue(out var s) => s, JsonValue v when v.TryGetValue(out var dt) => dt, JsonArray arr => arr.Select(ExtractValue).ToList(), JsonObject obj => obj.ToDictionary(kv => kv.Key, kv => ExtractValue(kv.Value)!), null => null, _ => node.ToString() }; private static JsonObject ConvertToJsonObject(Dictionary data) { var json = System.Text.Json.JsonSerializer.Serialize(data); return JsonNode.Parse(json) as JsonObject ?? new JsonObject(); } private static void FlattenJsonObject(JsonObject obj, string prefix, Dictionary result) { foreach (var kv in obj) { var key = string.IsNullOrEmpty(prefix) ? kv.Key : $"{prefix}.{kv.Key}"; if (kv.Value is JsonObject nested) FlattenJsonObject(nested, key, result); else if (kv.Value != null) result[key] = ExtractValue(kv.Value)!; } } } public class ExecutionState { public Decision CurrentDecision { get; set; } = Decision.Pending; public decimal RiskScore { get; set; } = 50m; public TrafficLight TrafficLight { get; set; } = TrafficLight.Amber; public List Deviations { get; set; } = new(); public List Tags { get; set; } = new(); public Dictionary OutputFields { get; set; } = new(); public bool ShouldStop { get; set; } } public class RuleExecutionState { public ExecutionState GlobalState { get; set; } = default!; public DynamicDataContext Data { get; set; } = default!; public Guid TenantId { get; set; } } public class RuleEvaluationResult { public Guid RuleId { get; set; } public string RuleCode { get; set; } = default!; public string RuleName { get; set; } = default!; public int VersionNumber { get; set; } public int ExecutionOrder { get; set; } public bool IsMatched { get; set; } public bool HasError { get; set; } public string? ErrorMessage { get; set; } public List ConditionResults { get; set; } = new(); public List ExecutedActions { get; set; } = new(); public int ExecutionMs { get; set; } } public class RuleLoadRequest { public Guid TenantId { get; set; } public string? ProductCode { get; set; } public string? BranchCode { get; set; } public string? StageCode { get; set; } public Guid? RuleSetId { get; set; } public List RuleTypes { get; set; } = new(); public bool PublishedOnly { get; set; } = true; } public class RiskScoreResult { public decimal Score { get; set; } public RiskCategory Category { get; set; } public TrafficLight TrafficLight { get; set; } public Dictionary ComponentScores { get; set; } = new(); }