using System.Collections.Generic; namespace GB5Shared.DTO.Qualifier { // ============================================================ // QualifierFactBag — Hierarchical Fact Store // // Three scopes, matching MQUALIFIEROUTPUT.OUTPUTSCOPE: // Document — one set of key-value facts for the whole document // Lines — per-line key-value facts (index matches source line index) // Groups — arbitrary named groups (e.g. tax buckets, approval tiers) // // ExportAll() produces: // { "Document": {...}, "Lines": [...], "Groups": {...} } // which is the shape expected by the mapping engine's [FactBag] path prefix. // // Thread safety: qualifiers at the same DAG execution level run concurrently // (QualifierFacadeImpl.ExecuteStageAsync uses Task.WhenAll) against the SAME // FactBag instance, so every read/write here is guarded by a single lock -- // the operations are simple dictionary/list mutations, so a coarse-grained // lock is cheap and avoids the finer-grained bugs a per-collection lock // scheme would risk. // ============================================================ public sealed class QualifierFactBag { private readonly object _gate = new(); private readonly Dictionary _document = new(); private readonly List> _lines = []; private readonly Dictionary> _groups = new(); // ── Document facts ──────────────────────────────────────────────────────── public void SetDocumentFact(string key, object? value) { lock (_gate) { _document[key] = value; } } public T? GetDocumentFact(string key) { lock (_gate) { return _document.TryGetValue(key, out var v) && v is T typed ? typed : default; } } public bool HasDocumentFact(string key) { lock (_gate) { return _document.ContainsKey(key); } } // ── Line facts ──────────────────────────────────────────────────────────── /// /// Ensures the Lines list has at least entries, /// padding with empty dictionaries as needed. /// public void EnsureLineCapacity(int minCount) { lock (_gate) { EnsureLineCapacityLocked(minCount); } } private void EnsureLineCapacityLocked(int minCount) { while (_lines.Count < minCount) _lines.Add(new Dictionary()); } public void SetLineFact(int lineIndex, string key, object? value) { lock (_gate) { EnsureLineCapacityLocked(lineIndex + 1); _lines[lineIndex][key] = value; } } public T? GetLineFact(int lineIndex, string key) { lock (_gate) { if (lineIndex < 0 || lineIndex >= _lines.Count) return default; return _lines[lineIndex].TryGetValue(key, out var v) && v is T typed ? typed : default; } } public bool HasLineFact(int lineIndex, string key) { lock (_gate) { return lineIndex >= 0 && lineIndex < _lines.Count && _lines[lineIndex].ContainsKey(key); } } public int LineCount { get { lock (_gate) { return _lines.Count; } } } // ── Group facts ─────────────────────────────────────────────────────────── public void SetGroup(string groupKey, string factKey, object? value) { lock (_gate) { if (!_groups.TryGetValue(groupKey, out var group)) { group = new Dictionary(); _groups[groupKey] = group; } group[factKey] = value; } } public T? GetGroupFact(string groupKey, string factKey) { lock (_gate) { if (!_groups.TryGetValue(groupKey, out var group)) return default; return group.TryGetValue(factKey, out var v) && v is T typed ? typed : default; } } public bool HasGroup(string groupKey) { lock (_gate) { return _groups.ContainsKey(groupKey); } } // ── Export ──────────────────────────────────────────────────────────────── /// /// Produces the canonical hierarchical export used by the mapping engine: /// { "Document": {...}, "Lines": [...], "Groups": {...} } /// public IReadOnlyDictionary ExportAll() { lock (_gate) { // Snapshot each section to avoid mutation while the caller reads. var docSnapshot = new Dictionary(_document); var linesSnapshot = new List>(_lines.Count); foreach (var line in _lines) linesSnapshot.Add(new Dictionary(line)); var groupsSnapshot = new Dictionary(); foreach (var (key, group) in _groups) groupsSnapshot[key] = new Dictionary(group); return new Dictionary { ["Document"] = docSnapshot, ["Lines"] = linesSnapshot, ["Groups"] = groupsSnapshot }; } } } }