using ZB.MOM.WW.MxGateway.Contracts.Proto; using ZB.MOM.WW.MxGateway.Server.Configuration; using ZB.MOM.WW.MxGateway.Server.Galaxy; namespace ZB.MOM.WW.MxGateway.Server.Alarms; /// /// Default . Merges Galaxy Repository /// alarm-attribute discovery with the configured include/exclude overrides /// and composes the per-attribute subtag item addresses from the configured /// subtag names. /// // NOTE: The exact subtag names and the canonical AlarmFullReference shape // ("Galaxy!{area}.{reference}") are validated against a live Galaxy in the // Task 17 live smoke test. The config Subtags block exists precisely so these // names are not hard-coded here. public sealed class AlarmWatchListResolver : IAlarmWatchListResolver { private const string ProviderLiteral = "Galaxy"; private const string DefaultActiveSubtag = "active"; private const string DefaultAckedSubtag = "acked"; private readonly IGalaxyRepository _repository; private readonly ILogger _logger; /// Initializes the watch-list resolver. /// Galaxy Repository used for alarm-attribute discovery. /// Diagnostic logger. public AlarmWatchListResolver( IGalaxyRepository repository, ILogger logger) { _repository = repository ?? throw new ArgumentNullException(nameof(repository)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } /// public async Task> ResolveAsync( AlarmsOptions options, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(options); AlarmDiscoveryOptions discovery = options.Fallback.Discovery; // 1. Build the ordered, de-duplicated attribute reference set. // Each entry carries the reference plus the source-object reference. List<(string Reference, string SourceObject)> ordered = []; HashSet seen = new(StringComparer.OrdinalIgnoreCase); if (discovery.UseGalaxyRepository) { List rows; try { rows = await _repository.GetAlarmAttributesAsync(cancellationToken).ConfigureAwait(false); } catch (Exception ex) { // Discovery being unavailable must not crash the resolver: log and // continue with an empty discovery set. The caller decides what to // do with the (possibly config-only) result. _logger.LogWarning( ex, "Galaxy Repository alarm-attribute discovery failed; continuing with configuration-only watch-list."); rows = []; } foreach (GalaxyAlarmAttributeRow row in rows) { if (string.IsNullOrEmpty(row.FullTagReference) || !seen.Add(row.FullTagReference)) { continue; } ordered.Add((row.FullTagReference, row.SourceObjectReference)); } } foreach (string include in discovery.IncludeAttributes) { if (string.IsNullOrEmpty(include) || !seen.Add(include)) { continue; } ordered.Add((include, DeriveSourceObject(include))); } // Remove excluded references (case-insensitive), but only when GR discovery // is active. ExcludeAttributes is documented as "Ignored when // UseGalaxyRepository is false" (AlarmDiscoveryOptions.ExcludeAttributes). // Whitespace-only entries are skipped, consistent with the include guard above. if (discovery.UseGalaxyRepository) { HashSet excluded = new( discovery.ExcludeAttributes.Where(e => !string.IsNullOrWhiteSpace(e)), StringComparer.OrdinalIgnoreCase); if (excluded.Count > 0) { ordered.RemoveAll(e => excluded.Contains(e.Reference)); } } // 2. Resolve subtag names with safe fallbacks. string active = string.IsNullOrEmpty(options.Fallback.Subtags.Active) ? DefaultActiveSubtag : options.Fallback.Subtags.Active; string acked = string.IsNullOrEmpty(options.Fallback.Subtags.Acked) ? DefaultAckedSubtag : options.Fallback.Subtags.Acked; string priority = options.Fallback.Subtags.Priority; string ackComment = options.Fallback.Subtags.AckComment; // 3. Resolve the area: discovery area, else the default area (may be empty). string area = string.IsNullOrEmpty(discovery.Area) ? options.DefaultArea : discovery.Area; // 4. Compose one target per reference. List targets = new(ordered.Count); foreach ((string reference, string sourceObject) in ordered) { targets.Add(new AlarmSubtagTarget { AlarmFullReference = ComposeFullReference(area, reference), SourceObjectReference = sourceObject, ActiveSubtag = $"{reference}.{active}", AckedSubtag = $"{reference}.{acked}", PrioritySubtag = string.IsNullOrEmpty(priority) ? string.Empty : $"{reference}.{priority}", AckCommentSubtag = string.IsNullOrEmpty(ackComment) ? string.Empty : $"{reference}.{ackComment}", }); } // 5. Report the resolved count; warn when subtag mode was expected to cover // something (GR enabled, or explicit includes were configured) but resolved // to nothing. Only emit the Debug line when there is at least one target, // to avoid a confusing "0 target(s)" noise line. if (targets.Count == 0 && (discovery.UseGalaxyRepository || discovery.IncludeAttributes.Length > 0)) { _logger.LogWarning( "Alarm subtag watch-list resolved to zero targets; subtag-polling fallback will cover no alarms."); } else if (targets.Count > 0) { _logger.LogDebug("Resolved alarm subtag watch-list with {TargetCount} target(s).", targets.Count); } return targets; } /// /// Derives the source-object reference for a configuration entry: the /// substring before the first '.', or the whole string when there is no dot. /// private static string DeriveSourceObject(string reference) { int dot = reference.IndexOf('.', StringComparison.Ordinal); return dot < 0 ? reference : reference[..dot]; } /// /// Composes the canonical alarm full reference: Galaxy!{area}.{reference} /// when an area is set, otherwise Galaxy!{reference}. /// private static string ComposeFullReference(string area, string reference) => string.IsNullOrEmpty(area) ? $"{ProviderLiteral}!{reference}" : $"{ProviderLiteral}!{area}.{reference}"; }