using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using GB5Shared.GOP.GBQueryExecutor; using GB5Shared.GOP.Qualifier; using GB5Shared.GOP.Qualifier.DTOs; using GB5Shared.DTO.Framework.Login; using GB5Shared.DTO.Qualifier; using Microsoft.Extensions.Logging; namespace GB5Shared.GOP.Qualifier { // ============================================================ // QualifierPlanCompiler — Builds and validates a compiled plan // for a QualifierVersion before activation. // // Responsibilities: // 1. Load all qualifier elements (methods, rule groups, // conditions, outputs, dependencies) from the DAL. // 2. Build the dependency DAG using Kahn's topological sort. // 3. Detect cycles → throw QualifierActivationException. // 4. Validate all DataSource methods via IGBQueryExecutor. // 5. Serialize the compiled plan snapshot to JSON (stored in // MQUALIFIERVERSION.SNAPSHOTJSON at activation time). // // The compiled snapshot is intentionally simple JSON — it is // deserialized at runtime by QualifierFacade to avoid re-loading // all DB tables on every execution. // ============================================================ public class QualifierPlanCompiler : IQualifierPlanCompiler { private readonly IQualifierDAL _Dal; private readonly IGBQueryExecutor _QueryExecutor; private readonly ILogger _Logger; public QualifierPlanCompiler( IQualifierDAL dal, IGBQueryExecutor queryExecutor, ILogger logger) { _Dal = dal; _QueryExecutor = queryExecutor; _Logger = logger; } // ═══════════════════════════════════════════════════════ // PUBLIC API // ═══════════════════════════════════════════════════════ /// /// Compiles the plan for the given qualifier and returns the JSON snapshot /// ready to be stored in MQUALIFIERVERSION.SNAPSHOTJSON. /// Throws if any rule is violated. /// public async Task CompileAsync( int qualifierId, LoginDTO loginDTO, CancellationToken ct = default) { _Logger.LogInformation( "QualifierPlanCompiler: compiling qualifier {QualifierId}", qualifierId); // ── Load all elements in parallel ───────────────────────────────────── var (methods, ruleGroups, conditions, outputs, dependencies) = await LoadElementsAsync(qualifierId, loginDTO, ct); // ── Rule 1: Validate DataSource methods via GBQueryExecutor ─────────── await ValidateDataSourceMethodsAsync(methods, loginDTO, ct); // ── Rule 2: DAG — topological sort + cycle detection ────────────────── var sortedDependencies = TopologicalSort(qualifierId, dependencies); // ── Build snapshot ──────────────────────────────────────────────────── var snapshot = new QualifierPlanSnapshot { QualifierId = qualifierId, CompiledAt = DateTime.UtcNow, Methods = methods.Select(m => new SnapshotMethod { QualifierMethodId = m.QualifierMethodId, ExecutionMode = m.ExecutionMode, Expression = m.Expression, QueryCode = m.QueryCode, ServiceCode = m.ServiceCode, ReturnType = m.ReturnType, SortOrder = m.SortOrder }).ToList(), RuleGroups = ruleGroups.Select(rg => new SnapshotRuleGroup { RuleGroupId = rg.RuleGroupId, ParentRuleGroupId = rg.ParentRuleGroupId, GroupOperator = rg.GroupOperator, SortOrder = rg.SortOrder }).ToList(), Conditions = conditions.Select(c => new SnapshotCondition { ConditionId = c.ConditionId, RuleGroupId = c.RuleGroupId, FieldPath = c.FieldPath, ConditionOperator = c.ConditionOperator, ExpectedValue = c.ExpectedValue, Severity = c.Severity, ErrorCode = c.ErrorCode, ErrorMessage = c.ErrorMessage, SortOrder = c.SortOrder }).ToList(), Outputs = outputs.Select(o => new SnapshotOutput { OutputId = o.OutputId, OutputKey = o.OutputKey, OutputScope = o.OutputScope, SourceExpression = o.SourceExpression, SortOrder = o.SortOrder }).ToList(), DependencyOrder = sortedDependencies }; var json = JsonSerializer.Serialize(snapshot, new JsonSerializerOptions { WriteIndented = false, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); _Logger.LogInformation( "QualifierPlanCompiler: qualifier {QualifierId} compiled successfully " + "({MethodCount} methods, {GroupCount} rule groups, {ConditionCount} conditions)", qualifierId, methods.Count, ruleGroups.Count, conditions.Count); return json; } // ═══════════════════════════════════════════════════════ // PRIVATE HELPERS // ═══════════════════════════════════════════════════════ private async Task<( IReadOnlyList methods, IReadOnlyList ruleGroups, IReadOnlyList conditions, IReadOnlyList outputs, IReadOnlyList dependencies)> LoadElementsAsync(int qualifierId, LoginDTO loginDTO, CancellationToken ct) { var methodsTask = _Dal.GetQualifierMethods(qualifierId, loginDTO, ct); var ruleGroupsTask = _Dal.GetRuleGroups(qualifierId, loginDTO, ct); var conditionsTask = _Dal.GetRuleConditions(qualifierId, loginDTO, ct); var outputsTask = _Dal.GetQualifierOutputs(qualifierId, loginDTO, ct); var dependenciesTask = _Dal.GetQualifierDependencies(qualifierId, loginDTO, ct); await Task.WhenAll(methodsTask, ruleGroupsTask, conditionsTask, outputsTask, dependenciesTask); // Tasks are already complete after WhenAll — await is free (no blocking) return (await methodsTask, await ruleGroupsTask, await conditionsTask, await outputsTask, await dependenciesTask); } private async Task ValidateDataSourceMethodsAsync( IReadOnlyList methods, LoginDTO loginDTO, CancellationToken ct) { foreach (var method in methods.Where(m => m.ExecutionMode == (byte)ExecutionMode.DataSource)) { if (string.IsNullOrWhiteSpace(method.QueryCode)) throw new QualifierActivationException( $"DataSource method {method.QualifierMethodId} has no QueryCode."); var validationResult = await _QueryExecutor.ValidateAsync(method.QueryCode, loginDTO, ct); if (!validationResult.IsValid) { throw new QualifierActivationException( $"DataSource method {method.QualifierMethodId} references query '{method.QueryCode}' " + $"which failed GBQueryExecutor validation: " + string.Join("; ", validationResult.Errors)); } } } /// /// Kahn's algorithm topological sort on the qualifier dependency DAG. /// Returns qualifiers in execution order (dependencies before dependents). /// Throws if a cycle is detected. /// private static List TopologicalSort( int qualifierId, IReadOnlyList dependencies) { if (dependencies.Count == 0) return [qualifierId]; // Build adjacency: prerequisite → list of dependents // and in-degree count per qualifier var allIds = new HashSet(); allIds.Add(qualifierId); foreach (var d in dependencies) { allIds.Add(d.QualifierId); allIds.Add(d.DependsOnQualifierId); } var inDegree = allIds.ToDictionary(id => id, _ => 0); var adjacency = allIds.ToDictionary(id => id, _ => new List()); foreach (var dep in dependencies) { // dep.QualifierId depends on dep.DependsOnQualifierId // So DependsOnQualifierId → QualifierId (prerequisite → dependent) adjacency[dep.DependsOnQualifierId].Add(dep.QualifierId); inDegree[dep.QualifierId]++; } // Initialize queue with nodes that have no prerequisites var queue = new Queue(inDegree.Where(kv => kv.Value == 0).Select(kv => kv.Key)); var sorted = new List(allIds.Count); while (queue.Count > 0) { var node = queue.Dequeue(); sorted.Add(node); foreach (var neighbor in adjacency[node]) { inDegree[neighbor]--; if (inDegree[neighbor] == 0) queue.Enqueue(neighbor); } } if (sorted.Count != allIds.Count) { // Some nodes were never dequeued — they are part of a cycle. var cycleNodes = allIds.Except(sorted); throw new QualifierActivationException( $"Qualifier {qualifierId} dependency graph contains a cycle involving " + $"qualifier IDs: [{string.Join(", ", cycleNodes)}]. " + "Fix the dependency configuration before activating."); } return sorted; } } // ════════════════════════════════════════════════════════════════ // IQualifierPlanCompiler — interface for DI registration // ════════════════════════════════════════════════════════════════ public interface IQualifierPlanCompiler { Task CompileAsync(int qualifierId, LoginDTO loginDTO, CancellationToken ct = default); } // ════════════════════════════════════════════════════════════════ // QualifierActivationException // ════════════════════════════════════════════════════════════════ public sealed class QualifierActivationException : Exception { public QualifierActivationException(string message) : base(message) { } } // ════════════════════════════════════════════════════════════════ // Plan snapshot model — serialized to MQUALIFIERVERSION.SNAPSHOTJSON // ════════════════════════════════════════════════════════════════ public sealed class QualifierPlanSnapshot { public int QualifierId { get; set; } public DateTime CompiledAt { get; set; } public List DependencyOrder { get; set; } = []; public List Methods { get; set; } = []; public List RuleGroups { get; set; } = []; public List Conditions { get; set; } = []; public List Outputs { get; set; } = []; } public sealed class SnapshotMethod { public int QualifierMethodId { get; set; } public byte ExecutionMode { get; set; } public string? Expression { get; set; } public string? QueryCode { get; set; } public string? ServiceCode { get; set; } public string? ReturnType { get; set; } public int SortOrder { get; set; } } public sealed class SnapshotRuleGroup { public int RuleGroupId { get; set; } public int? ParentRuleGroupId { get; set; } public string GroupOperator { get; set; } = "AND"; public int SortOrder { get; set; } } public sealed class SnapshotCondition { public int ConditionId { get; set; } public int RuleGroupId { get; set; } public string FieldPath { get; set; } = string.Empty; public string ConditionOperator { get; set; } = string.Empty; public string? ExpectedValue { get; set; } public byte Severity { get; set; } public string? ErrorCode { get; set; } public string? ErrorMessage { get; set; } public int SortOrder { get; set; } } public sealed class SnapshotOutput { public int OutputId { get; set; } public string OutputKey { get; set; } = string.Empty; public byte OutputScope { get; set; } public string? SourceExpression { get; set; } public int SortOrder { get; set; } } }