using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Npgsql; using StellaOps.AdvisoryAI.KnowledgeSearch; namespace StellaOps.AdvisoryAI.UnifiedSearch; internal sealed class EntityAliasService : IEntityAliasService { private readonly KnowledgeSearchOptions _options; private readonly ILogger _logger; private readonly Lazy _dataSource; public EntityAliasService( IOptions options, ILogger logger) { ArgumentNullException.ThrowIfNull(options); _options = options.Value ?? new KnowledgeSearchOptions(); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _dataSource = new Lazy(() => { if (!_options.Enabled || string.IsNullOrWhiteSpace(_options.ConnectionString)) { return null; } return new NpgsqlDataSourceBuilder(_options.ConnectionString).Build(); }, isThreadSafe: true); } public async Task> ResolveAliasesAsync( string alias, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(alias) || _dataSource.Value is null) { return []; } const string sql = """ SELECT entity_key, entity_type FROM advisoryai.entity_alias WHERE lower(alias) = lower(@alias) ORDER BY entity_key, entity_type; """; await using var command = _dataSource.Value.CreateCommand(sql); command.CommandTimeout = 10; command.Parameters.AddWithValue("alias", alias.Trim()); var results = new List<(string, string)>(); await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) { results.Add((reader.GetString(0), reader.GetString(1))); } return results; } public async Task RegisterAliasAsync( string entityKey, string entityType, string alias, string source, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(entityKey) || string.IsNullOrWhiteSpace(entityType) || string.IsNullOrWhiteSpace(alias) || _dataSource.Value is null) { return; } const string sql = """ INSERT INTO advisoryai.entity_alias (alias, entity_key, entity_type, source, created_at) VALUES (@alias, @entity_key, @entity_type, @source, NOW()) ON CONFLICT (alias, entity_key) DO UPDATE SET entity_type = EXCLUDED.entity_type, source = EXCLUDED.source; """; await using var command = _dataSource.Value.CreateCommand(sql); command.CommandTimeout = 10; command.Parameters.AddWithValue("alias", alias.Trim()); command.Parameters.AddWithValue("entity_key", entityKey.Trim()); command.Parameters.AddWithValue("entity_type", entityType.Trim()); command.Parameters.AddWithValue("source", source.Trim()); await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); } }