using EAIAdminBLL.Sync; using EAIAdminDAL.CustomCode; using EAIAdminDAL.DTO; using GB5Shared.DTO.Framework.Login; using GB5Shared.Resource.Response; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; namespace EAIAdminBLL.EAIAgentRouteAdmin { public class EAIAgentRouteAdminBLL : IEAIAgentRouteAdminBLL { // Slug is a URL segment (POST /) — lowercase-only, matches gbEAI's own routing // convention for AGENT_ROUTES keys. private static readonly Regex SlugPattern = new(@"^[a-z0-9_]{1,100}$", RegexOptions.Compiled); private const string RestartRequiredSuffix = " Restart AI-Enterprise-v1.0 for routing changes to take effect."; private readonly IEAIAgentRouteAdminDAL _dal; private readonly IEAISyncClient _syncClient; private readonly ILogger _logger; public EAIAgentRouteAdminBLL( IEAIAgentRouteAdminDAL dal, IEAISyncClient syncClient, ILogger logger) { _dal = dal ?? throw new ArgumentNullException(nameof(dal)); _syncClient = syncClient ?? throw new ArgumentNullException(nameof(syncClient)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public async Task> GetAgentRouteListAsync( string? search, LoginDTO login, CancellationToken ct = default) { try { return await _dal.GetAgentRouteListAsync(search, login, ct).ConfigureAwait(false); } catch (Exception ex) { _logger.LogError(ex, "BLL | GetAgentRouteList | Error | Tenant={TenantId}", login.ClientId); throw; } } public async Task GetAgentRouteAsync( int agentRouteId, LoginDTO login, CancellationToken ct = default) { try { return await _dal.GetAgentRouteAsync(agentRouteId, login, ct).ConfigureAwait(false); } catch (Exception ex) { _logger.LogError(ex, "BLL | GetAgentRoute | Error | AgentRouteId={Id}", agentRouteId); throw; } } public async Task SaveAgentRouteAsync( EAIAgentRouteAdminDTO dto, LoginDTO login, CancellationToken ct = default) { try { ValidateSlug(dto.Slug); if (string.IsNullOrWhiteSpace(dto.InitiativeCode)) throw new ArgumentException("InitiativeCode is required."); if (string.IsNullOrWhiteSpace(dto.DefaultCapability)) throw new ArgumentException("DefaultCapability is required."); if (string.IsNullOrWhiteSpace(dto.Module)) throw new ArgumentException("Module is required."); // SLUG is globally unique — matches gbEAI's own process-wide AGENT_ROUTES tuple // keyed by slug, not per-tenant. MEAIAGENTROUTE is global config; every row is // visible/checked regardless of caller tenant. bool slugExists = await _dal.SlugExistsAsync( dto.Slug, dto.AgentRouteId, login, ct).ConfigureAwait(false); if (slugExists) throw new InvalidOperationException( $"Slug '{dto.Slug}' already exists."); // Audit/ownership only — not used as an access filter (see DTO/QB doc comments). dto.TenantId = login.ClientId; var isNew = dto.AgentRouteId == 0; if (isNew) { var newId = await _dal.InsertAgentRouteAsync(dto, login, ct).ConfigureAwait(false); dto.AgentRouteId = newId; } else { await _dal.UpdateAgentRouteAsync(dto, login, ct).ConfigureAwait(false); } // Push to gbEAI AFTER the local commit — never inside a transaction. A failed push // does not roll back the local save; GB5 stays system-of-record. var syncResult = await _syncClient.PushAgentRouteAsync(dto, login, ct).ConfigureAwait(false); if (syncResult.Success && syncResult.EAIRemoteId is { } remoteId) { await _dal.UpdateEAIRemoteIdAsync(dto.AgentRouteId, remoteId, login, ct).ConfigureAwait(false); } var baseMessage = isNew ? $"{SuccessResponse.SaveSuccessMessage} {dto.AgentRouteId}" : SuccessResponse.UpdateSuccess; // gbEAI registers agent routes once at process startup — an edit here needs a // gbEAI restart to actually serve traffic. This suffix is appended always, // success or failure, in addition to (after) the sync-failure suffix. return syncResult.Success ? $"{baseMessage}{RestartRequiredSuffix}" : $"{baseMessage} Sync to AI-Enterprise-v1.0 failed: {syncResult.Message} — retry from this screen.{RestartRequiredSuffix}"; } catch (Exception ex) { _logger.LogError(ex, "BLL | SaveAgentRoute | Error | Tenant={TenantId} | Slug={Slug}", login.ClientId, dto.Slug); throw; } } public async Task DeleteAgentRouteAsync( int agentRouteId, LoginDTO login, CancellationToken ct = default) { try { if (agentRouteId == 0) throw new ArgumentException("AgentRouteId must be a positive integer."); var existing = await _dal.GetAgentRouteAsync(agentRouteId, login, ct).ConfigureAwait(false) ?? throw new InvalidOperationException($"AgentRoute {agentRouteId} not found."); await _dal.DeleteAgentRouteAsync(agentRouteId, login, ct).ConfigureAwait(false); var syncResult = await _syncClient.DeleteAgentRouteAsync(existing, login, ct).ConfigureAwait(false); return syncResult.Success ? SuccessResponse.DeleteSuccessMessage : $"{SuccessResponse.DeleteSuccessMessage} Sync to AI-Enterprise-v1.0 failed: {syncResult.Message} — retry from this screen."; } catch (Exception ex) { _logger.LogError(ex, "BLL | DeleteAgentRoute | Error | AgentRouteId={Id}", agentRouteId); throw; } } private static void ValidateSlug(string slug) { if (string.IsNullOrWhiteSpace(slug)) throw new ArgumentException("Slug is required."); if (!SlugPattern.IsMatch(slug)) throw new ArgumentException( "Slug must be lowercase alphanumeric characters and underscores only (max 100 chars)."); } } }