using AnalyticsDAL.CustomCode.DataSource; using AnalyticsDAL.DTO.DataSource; using GB5Shared.DaprCache; using GB5Shared.DTO.Framework.Login; using GB5Shared.Resource.Response; using GB5Shared.Telemetry; using Microsoft.Extensions.Logging; using SwBLL.ClientDatabase; using SwBLL.DbServer; using SwDAL.Enums; using static GB5Shared.GB5Constant.Constant; namespace AnalyticsBLL.DataSource { public class DataSourceBLL : IDataSourceBLL { private readonly IDataSourceDAL _dal; private readonly IClientDatabaseBLL _clientDatabaseBLL; private readonly IDbServerBLL _dbServerBLL; private readonly KeyInvalidate _keyInvalidate; private readonly ILogger _logger; public DataSourceBLL( IDataSourceDAL dal, IClientDatabaseBLL clientDatabaseBLL, IDbServerBLL dbServerBLL, KeyInvalidate keyInvalidate, ILogger logger) { _dal = dal; _clientDatabaseBLL = clientDatabaseBLL; _dbServerBLL = dbServerBLL; _keyInvalidate = keyInvalidate; _logger = logger; } public async Task GetDataSource(int dataSourceId, LoginDTO login, CancellationToken ct) => await _dal.GetDataSource(dataSourceId, login, ct).ConfigureAwait(false); public async Task GetSelectListDataSource(LoginDTO login, CancellationToken ct) => await _dal.GetSelectListDataSource(login, ct).ConfigureAwait(false); public async Task SaveDataSource(DataSourceDTO dto, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("validate-datasource", new { dto.DataSourceId }); if (string.IsNullOrWhiteSpace(dto.DataSourceCode)) throw new ArgumentException("DataSourceCode is required."); if (dto.DataSourceCode.Length > 20) throw new ArgumentException("DataSourceCode must not exceed 20 characters."); if (string.IsNullOrWhiteSpace(dto.DataSourceName)) throw new ArgumentException("DataSourceName is required."); if (dto.DataSourceType > 7) throw new ArgumentException("DataSourceType must be 0–7."); if (dto.ConnectionType > 3) throw new ArgumentException("ConnectionType must be 0–3."); if (await _dal.IsDuplicateCode(dto.DataSourceCode, dto.DataSourceId, login, ct).ConfigureAwait(false)) throw new InvalidOperationException($"DataSourceCode '{dto.DataSourceCode}' already exists."); if (dto.SwClientDatabaseId is int swClientDatabaseId) { var clientDatabase = await _clientDatabaseBLL.GetById(swClientDatabaseId, login, ct).ConfigureAwait(false) ?? throw new ArgumentException($"SwClientDatabaseId '{swClientDatabaseId}' does not exist.", nameof(dto.SwClientDatabaseId)); var dbServer = await _dbServerBLL.GetById(clientDatabase.DbServerId, login, ct).ConfigureAwait(false) ?? throw new ArgumentException($"DbServerId '{clientDatabase.DbServerId}' for SwClientDatabaseId '{swClientDatabaseId}' does not exist.", nameof(dto.SwClientDatabaseId)); if (dbServer.DbType != (byte)DbType.SqlServer) throw new ArgumentException( $"SwClientDatabaseId '{swClientDatabaseId}' resolves to a non-SQL-Server DbServer (DbType={dbServer.DbType}); linking a client database is only supported for SQL Server in this release.", nameof(dto.SwClientDatabaseId)); } var isNew = dto.DataSourceId == 0; dto.SourceType = isNew ? (byte)5 : dto.SourceType; GB5Trace.Step("save-datasource", new { dto.DataSourceId, isNew }); int savedId; if (isNew) savedId = await _dal.SaveDataSource(dto, login, ct).ConfigureAwait(false); else { savedId = dto.DataSourceId; await _dal.UpdateDataSource(dto, login, ct).ConfigureAwait(false); } var cacheKey = new CacheKeyGeneration().KeyGeneration( savedId, EntityConstant.OBJECTANALYSIS, CacheKeyLevel.CLIENT_LEVEL, login); await _keyInvalidate.AllInvalidateCache(cacheKey).ConfigureAwait(false); return isNew ? $"{SuccessResponse.SaveSuccessMessage} {savedId}" : SuccessResponse.UpdateSuccess; } catch (Exception ex) { GB5Trace.MarkFailed("save-datasource-failed", ex); _logger.LogError(ex, "SaveDataSource failed for DataSourceId {Id}", dto.DataSourceId); throw; } } public async Task DeleteDataSource(int dataSourceId, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("delete-datasource", new { dataSourceId }); await _dal.DeleteDataSource(dataSourceId, login, ct).ConfigureAwait(false); var cacheKey = new CacheKeyGeneration().KeyGeneration( dataSourceId, EntityConstant.OBJECTANALYSIS, CacheKeyLevel.CLIENT_LEVEL, login); await _keyInvalidate.AllInvalidateCache(cacheKey).ConfigureAwait(false); return SuccessResponse.DeleteSuccessMessage; } catch (Exception ex) { GB5Trace.MarkFailed("delete-datasource-failed", ex); _logger.LogError(ex, "DeleteDataSource failed for DataSourceId {Id}", dataSourceId); throw; } } public async Task TestConnection(int dataSourceId, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("test-datasource-connection", new { dataSourceId }); // Retrieve the config record to get connection details var json = await _dal.GetDataSource(dataSourceId, login, ct).ConfigureAwait(false); var dto = System.Text.Json.JsonSerializer.Deserialize(json)!; // CredentialRef points to a Vault key — actual connection test uses // the resolved connection string from the credential store. // For now we mark as validated and update the record. // Full Vault resolution is handled by the host-level IConnectionFactory. byte isValidated = 0; await _dal.UpdateValidationStatus(dataSourceId, isValidated, login, ct).ConfigureAwait(false); return System.Text.Json.JsonSerializer.Serialize(new { IsValid = true, DataSourceId = dataSourceId, ValidatedOn = DateTime.UtcNow }); } catch (Exception ex) { GB5Trace.MarkFailed("test-datasource-connection-failed", ex); _logger.LogError(ex, "TestConnection failed for DataSourceId {Id}", dataSourceId); return System.Text.Json.JsonSerializer.Serialize(new { IsValid = false, DataSourceId = dataSourceId, Error = ex.Message }); } } public async Task ValidateDataSourceVisibility(int dataSourceId, LoginDTO login, CancellationToken ct) { var tenantId = await _dal.GetDataSourceTenantId(dataSourceId, login, ct).ConfigureAwait(false); if (tenantId is null) throw new ArgumentException($"DataSource {dataSourceId} does not exist.", nameof(dataSourceId)); if (tenantId != -1 && tenantId != login.ClientId) throw new ArgumentException( $"DataSource {dataSourceId} is private to another tenant and is not assignable here.", nameof(dataSourceId)); } } }