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.EAIBotAdmin { public class EAIBotAdminBLL : IEAIBotAdminBLL { private static readonly Regex BotCodePattern = new(@"^[A-Za-z0-9_]{1,50}$", RegexOptions.Compiled); private readonly IEAIBotAdminDAL _dal; private readonly IEAISyncClient _syncClient; private readonly ILogger _logger; public EAIBotAdminBLL( IEAIBotAdminDAL 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> GetBotListAsync( string? search, LoginDTO login, CancellationToken ct = default) { try { return await _dal.GetBotListAsync(search, login, ct).ConfigureAwait(false); } catch (Exception ex) { _logger.LogError(ex, "BLL | GetBotList | Error | Tenant={TenantId}", login.ClientId); throw; } } public async Task GetBotAsync( int botId, LoginDTO login, CancellationToken ct = default) { try { return await _dal.GetBotAsync(botId, login, ct).ConfigureAwait(false); } catch (Exception ex) { _logger.LogError(ex, "BLL | GetBot | Error | BotId={Id}", botId); throw; } } public async Task SaveBotAsync( EAIBotAdminDTO dto, LoginDTO login, CancellationToken ct = default) { try { ValidateBotCode(dto.BotCode); if (string.IsNullOrWhiteSpace(dto.Name)) throw new ArgumentException("Name is required."); if (string.IsNullOrWhiteSpace(dto.InitiativeCode)) throw new ArgumentException("InitiativeCode is required."); // BOTCODE is globally unique — matches gbEAI's unique=True on aibotcode, not // per-tenant. bool codeExists = await _dal.BotCodeExistsAsync( dto.BotCode, dto.BotId, login, ct).ConfigureAwait(false); if (codeExists) throw new InvalidOperationException( $"BotCode '{dto.BotCode}' already exists."); dto.TenantId = login.ClientId; var isNew = dto.BotId == 0; if (isNew) { var newId = await _dal.InsertBotAsync(dto, login, ct).ConfigureAwait(false); dto.BotId = newId; } else { await _dal.UpdateBotAsync(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.PushBotAsync(dto, login, ct).ConfigureAwait(false); if (syncResult.Success && syncResult.EAIRemoteId is { } remoteId) { await _dal.UpdateEAIRemoteIdAsync(dto.BotId, remoteId, login, ct).ConfigureAwait(false); } var baseMessage = isNew ? $"{SuccessResponse.SaveSuccessMessage} {dto.BotId}" : SuccessResponse.UpdateSuccess; return syncResult.Success ? baseMessage : $"{baseMessage} Sync to AI-Enterprise-v1.0 failed: {syncResult.Message} — retry from this screen."; } catch (Exception ex) { _logger.LogError(ex, "BLL | SaveBot | Error | Tenant={TenantId} | BotCode={Code}", login.ClientId, dto.BotCode); throw; } } public async Task DeleteBotAsync( int botId, LoginDTO login, CancellationToken ct = default) { try { if (botId == 0) throw new ArgumentException("BotId must be a positive integer."); var existing = await _dal.GetBotAsync(botId, login, ct).ConfigureAwait(false) ?? throw new InvalidOperationException($"Bot {botId} not found."); await _dal.DeleteBotAsync(botId, login, ct).ConfigureAwait(false); var syncResult = await _syncClient.DeleteBotAsync(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 | DeleteBot | Error | BotId={Id}", botId); throw; } } private static void ValidateBotCode(string botCode) { if (string.IsNullOrWhiteSpace(botCode)) throw new ArgumentException("BotCode is required."); if (!BotCodePattern.IsMatch(botCode)) throw new ArgumentException( "BotCode must contain only alphanumeric characters and underscores (max 50 chars)."); } } }