using EAIAdminDAL.CustomCode; using EAIAdminDAL.DTO; using GB5Shared.DTO.Framework.Login; using GB5Shared.Resource.Response; using KmsDAL.CustomCode.KmDomain; using KmsDAL.CustomCode.KmSpace; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace EAIAdminBLL.EAIBotKnowledgeSource { /// /// Cross-module BLL — EAIAdmin and KMS both live in PlatformHost's process (see the approved /// plan's "Module placement — resolved" section), so this can make in-process DAL calls into /// KmsDAL's IKmDomainDAL/IKmSpaceDAL directly. Tenant-consistency validation is free: both /// GetKmDomain/GetKmSpace already filter by TENANTID in their own WHERE clause (see /// KmDomainQB.GET_KMDOMAIN/KmSpaceQB.GET_KMSPACE), so calling them with the requester's own /// login and getting nothing back means the target either doesn't exist or belongs to another /// tenant — same rejection either way, no separate TenantId-equality check needed. /// public class EAIBotKnowledgeSourceBLL : IEAIBotKnowledgeSourceBLL { private readonly IEAIBotKnowledgeSourceDAL _dal; private readonly IEAIBotAdminDAL _botDal; private readonly IEAICapabilityAdminDAL _capabilityDal; private readonly IKmDomainDAL _kmDomainDal; private readonly IKmSpaceDAL _kmSpaceDal; private readonly ILogger _logger; public EAIBotKnowledgeSourceBLL( IEAIBotKnowledgeSourceDAL dal, IEAIBotAdminDAL botDal, IEAICapabilityAdminDAL capabilityDal, IKmDomainDAL kmDomainDal, IKmSpaceDAL kmSpaceDal, ILogger logger) { _dal = dal ?? throw new ArgumentNullException(nameof(dal)); _botDal = botDal ?? throw new ArgumentNullException(nameof(botDal)); _capabilityDal = capabilityDal ?? throw new ArgumentNullException(nameof(capabilityDal)); _kmDomainDal = kmDomainDal ?? throw new ArgumentNullException(nameof(kmDomainDal)); _kmSpaceDal = kmSpaceDal ?? throw new ArgumentNullException(nameof(kmSpaceDal)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public async Task> GetListByBotAsync( int eaiBotId, LoginDTO login, CancellationToken ct = default) => await _dal.GetListByBotAsync(eaiBotId, login, ct).ConfigureAwait(false); public async Task> GetListByCapabilityAsync( int eaiCapabilityId, LoginDTO login, CancellationToken ct = default) => await _dal.GetListByCapabilityAsync(eaiCapabilityId, login, ct).ConfigureAwait(false); public async Task SaveLinkAsync( EAIBotKnowledgeSourceDTO dto, LoginDTO login, CancellationToken ct = default) { try { bool botSet = dto.EAIBotId > 0; bool capSet = dto.EAICapabilityId > 0; if (botSet == capSet) // both set or neither set throw new ArgumentException("Exactly one of EAIBotId/EAICapabilityId must be set."); bool spaceSet = dto.KmSpaceId > 0; bool domainSet = dto.KmDomainId > 0; if (spaceSet == domainSet) throw new ArgumentException("Exactly one of KmSpaceId/KmDomainId must be set."); // Never allow cross-tenant linkage — enforced exactly once, at save time, in the // one BLL method that can create this row. if (botSet && await _botDal.GetBotAsync(dto.EAIBotId, login, ct).ConfigureAwait(false) is null) throw new InvalidOperationException($"Bot {dto.EAIBotId} not found for this tenant."); if (capSet && await _capabilityDal.GetCapabilityAsync(dto.EAICapabilityId, login, ct).ConfigureAwait(false) is null) throw new InvalidOperationException($"Capability {dto.EAICapabilityId} not found for this tenant."); if (spaceSet && string.IsNullOrWhiteSpace(await _kmSpaceDal.GetKmSpace(dto.KmSpaceId, login, ct).ConfigureAwait(false))) throw new InvalidOperationException($"KmSpace {dto.KmSpaceId} not found for this tenant."); if (domainSet && string.IsNullOrWhiteSpace(await _kmDomainDal.GetKmDomain(dto.KmDomainId, login, ct).ConfigureAwait(false))) throw new InvalidOperationException($"KmDomain {dto.KmDomainId} not found for this tenant."); var newId = await _dal.InsertLinkAsync(dto, login, ct).ConfigureAwait(false); _logger.LogInformation("BLL | SaveLink | Completed | NewId={NewId}", newId); return $"{SuccessResponse.SaveSuccessMessage} {newId}"; } catch (Exception ex) { _logger.LogError(ex, "BLL | SaveLink | Error | BotId={BotId} CapabilityId={CapabilityId}", dto.EAIBotId, dto.EAICapabilityId); throw; } } public async Task DeleteLinkAsync( int eaiBotKnowledgeSourceId, LoginDTO login, CancellationToken ct = default) { if (eaiBotKnowledgeSourceId == 0) throw new ArgumentException("EAIBotKnowledgeSourceId must be a positive integer."); await _dal.DeleteLinkAsync(eaiBotKnowledgeSourceId, login, ct).ConfigureAwait(false); return SuccessResponse.DeleteSuccessMessage; } } }