using StellaOps.Concelier.Storage.Aliases; using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace StellaOps.Concelier.Merge.Services; public sealed class AliasGraphResolver { private readonly IAliasStore _aliasStore; public AliasGraphResolver(IAliasStore aliasStore) { _aliasStore = aliasStore ?? throw new ArgumentNullException(nameof(aliasStore)); } public async Task ResolveAsync(string advisoryKey, CancellationToken cancellationToken) { ArgumentException.ThrowIfNullOrEmpty(advisoryKey); var aliases = await _aliasStore.GetByAdvisoryAsync(advisoryKey, cancellationToken).ConfigureAwait(false); var collisions = new List(); foreach (var alias in aliases) { var candidates = await _aliasStore.GetByAliasAsync(alias.Scheme, alias.Value, cancellationToken).ConfigureAwait(false); var advisoryKeys = candidates .Select(static candidate => candidate.AdvisoryKey) .Where(static key => !string.IsNullOrWhiteSpace(key)) .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); if (advisoryKeys.Length <= 1) { continue; } collisions.Add(new AliasCollision(alias.Scheme, alias.Value, advisoryKeys)); } var unique = new Dictionary(StringComparer.Ordinal); foreach (var collision in collisions) { var key = $"{collision.Scheme}\u0001{collision.Value}"; if (!unique.ContainsKey(key)) { unique[key] = collision; } } var distinctCollisions = unique.Values.ToArray(); return new AliasIdentityResult(advisoryKey, aliases, distinctCollisions); } public async Task BuildComponentAsync(string advisoryKey, CancellationToken cancellationToken) { ArgumentException.ThrowIfNullOrEmpty(advisoryKey); var visited = new HashSet(StringComparer.OrdinalIgnoreCase); var queue = new Queue(); var collisionMap = new Dictionary(StringComparer.Ordinal); var aliasCache = new Dictionary>(StringComparer.OrdinalIgnoreCase); queue.Enqueue(advisoryKey); while (queue.Count > 0) { cancellationToken.ThrowIfCancellationRequested(); var current = queue.Dequeue(); if (!visited.Add(current)) { continue; } var aliases = await GetAliasesAsync(current, cancellationToken, aliasCache).ConfigureAwait(false); aliasCache[current] = aliases; foreach (var alias in aliases) { var aliasRecords = await GetAdvisoriesForAliasAsync(alias.Scheme, alias.Value, cancellationToken).ConfigureAwait(false); var advisoryKeys = aliasRecords .Select(static record => record.AdvisoryKey) .Where(static key => !string.IsNullOrWhiteSpace(key)) .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); if (advisoryKeys.Length <= 1) { continue; } foreach (var candidate in advisoryKeys) { if (!visited.Contains(candidate)) { queue.Enqueue(candidate); } } var collision = new AliasCollision(alias.Scheme, alias.Value, advisoryKeys); var key = $"{collision.Scheme}\u0001{collision.Value}"; collisionMap.TryAdd(key, collision); } } var aliasMap = new Dictionary>(aliasCache, StringComparer.OrdinalIgnoreCase); return new AliasComponent(advisoryKey, visited.ToArray(), collisionMap.Values.ToArray(), aliasMap); } private async Task> GetAliasesAsync( string advisoryKey, CancellationToken cancellationToken, IDictionary> cache) { if (cache.TryGetValue(advisoryKey, out var cached)) { return cached; } var aliases = await _aliasStore.GetByAdvisoryAsync(advisoryKey, cancellationToken).ConfigureAwait(false); cache[advisoryKey] = aliases; return aliases; } private Task> GetAdvisoriesForAliasAsync( string scheme, string value, CancellationToken cancellationToken) => _aliasStore.GetByAliasAsync(scheme, value, cancellationToken); } public sealed record AliasIdentityResult(string AdvisoryKey, IReadOnlyList Aliases, IReadOnlyList Collisions); public sealed record AliasComponent( string SeedAdvisoryKey, IReadOnlyList AdvisoryKeys, IReadOnlyList Collisions, IReadOnlyDictionary> AliasMap);