using ZB.MOM.WW.GalaxyRepository; using ZB.MOM.WW.MxGateway.Contracts.Proto; using ZB.MOM.WW.MxGateway.Server.Configuration; 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. /// public sealed class AlarmWatchListResolver : IAlarmWatchListResolver { private const string ProviderLiteral = "Galaxy"; private const string DefaultActiveSubtag = "InAlarm"; 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; // GWC-37: the caller cannot grade an empty watch-list without knowing whether discovery // was asked to contribute and whether it failed while doing so, because the failure is // deliberately swallowed below. string? discoveryFailureMessage = null; // Config fallback area used only for explicit IncludeAttributes entries (which // carry no discovered area): discovery area, else the default area (may be empty). string configFallbackArea = string.IsNullOrEmpty(discovery.Area) ? options.DefaultArea : discovery.Area; // 1. Build the ordered, de-duplicated attribute reference set. // Each entry carries the reference, the source-object reference, and the // per-entry area used to compose the canonical reference. GR rows contribute // the object's real Galaxy area; config includes contribute the config // fallback area (Discovery.Area else DefaultArea). List<(string Reference, string SourceObject, string Area)> ordered = []; HashSet seen = new(StringComparer.OrdinalIgnoreCase); if (discovery.UseGalaxyRepository) { List rows; try { rows = await _repository.GetAlarmAttributesAsync(cancellationToken).ConfigureAwait(false); } catch (Exception ex) when (ex is not OperationCanceledException) { // 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. Cancellation is the one // exception — an OperationCanceledException propagates per the // IAlarmWatchListResolver contract so the caller unwinds promptly. _logger.LogWarning( ex, "Galaxy Repository alarm-attribute discovery failed; continuing with configuration-only watch-list."); discoveryFailureMessage = ex.Message; rows = []; } foreach (GalaxyAlarmAttributeRow row in rows) { if (string.IsNullOrEmpty(row.FullTagReference) || !seen.Add(row.FullTagReference)) { continue; } ordered.Add((row.FullTagReference, row.SourceObjectReference, row.Area)); } } foreach (string include in discovery.IncludeAttributes) { if (string.IsNullOrEmpty(include) || !seen.Add(include)) { continue; } ordered.Add((include, DeriveSourceObject(include), configFallbackArea)); } // 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. Compose one target per reference, using the PER-ENTRY area: the GR row's // real Galaxy area (matching the alarmmgr group), or the config fallback for // explicit includes. List targets = new(ordered.Count); foreach ((string reference, string sourceObject, string area) 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}", }); } // 4. 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 new AlarmWatchListResolution( targets, DiscoveryAttempted: discovery.UseGalaxyRepository, DiscoveryFailed: discoveryFailureMessage is not null, discoveryFailureMessage); } /// /// 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}"; }