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.EAIInitiativeAdmin { public class EAIInitiativeAdminBLL : IEAIInitiativeAdminBLL { private static readonly Regex InitiativeCodePattern = new(@"^[A-Za-z0-9_]{1,50}$", RegexOptions.Compiled); private readonly IEAIInitiativeAdminDAL _dal; private readonly IEAISyncClient _syncClient; private readonly ILogger _logger; public EAIInitiativeAdminBLL( IEAIInitiativeAdminDAL 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> GetInitiativeListAsync( string? search, LoginDTO login, CancellationToken ct = default) { try { return await _dal.GetInitiativeListAsync(search, login, ct).ConfigureAwait(false); } catch (Exception ex) { _logger.LogError(ex, "BLL | GetInitiativeList | Error | Tenant={TenantId}", login.ClientId); throw; } } public async Task GetInitiativeAsync( int initiativeId, LoginDTO login, CancellationToken ct = default) { try { return await _dal.GetInitiativeAsync(initiativeId, login, ct).ConfigureAwait(false); } catch (Exception ex) { _logger.LogError(ex, "BLL | GetInitiative | Error | InitiativeId={Id}", initiativeId); throw; } } public async Task SaveInitiativeAsync( EAIInitiativeAdminDTO dto, LoginDTO login, CancellationToken ct = default) { try { ValidateInitiativeCode(dto.InitiativeCode); if (string.IsNullOrWhiteSpace(dto.InitiativeName)) throw new ArgumentException("InitiativeName is required."); // INITIATIVECODE is globally unique — matches gbEAI's unique=True on // aiinitiativecode, not per-tenant. bool codeExists = await _dal.InitiativeCodeExistsAsync( dto.InitiativeCode, dto.InitiativeId, login, ct).ConfigureAwait(false); if (codeExists) throw new InvalidOperationException( $"InitiativeCode '{dto.InitiativeCode}' already exists."); dto.TenantId = login.ClientId; var isNew = dto.InitiativeId == 0; if (isNew) { var newId = await _dal.InsertInitiativeAsync(dto, login, ct).ConfigureAwait(false); dto.InitiativeId = newId; } else { await _dal.UpdateInitiativeAsync(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.PushInitiativeAsync(dto, login, ct).ConfigureAwait(false); if (syncResult.Success && syncResult.EAIRemoteId is { } remoteId) { await _dal.UpdateEAIRemoteIdAsync(dto.InitiativeId, remoteId, login, ct).ConfigureAwait(false); } var baseMessage = isNew ? $"{SuccessResponse.SaveSuccessMessage} {dto.InitiativeId}" : 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 | SaveInitiative | Error | Tenant={TenantId} | InitiativeCode={Code}", login.ClientId, dto.InitiativeCode); throw; } } public async Task DeleteInitiativeAsync( int initiativeId, LoginDTO login, CancellationToken ct = default) { try { if (initiativeId == 0) throw new ArgumentException("InitiativeId must be a positive integer."); var existing = await _dal.GetInitiativeAsync(initiativeId, login, ct).ConfigureAwait(false) ?? throw new InvalidOperationException($"Initiative {initiativeId} not found."); await _dal.DeleteInitiativeAsync(initiativeId, login, ct).ConfigureAwait(false); var syncResult = await _syncClient.DeleteInitiativeAsync(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 | DeleteInitiative | Error | InitiativeId={Id}", initiativeId); throw; } } private static void ValidateInitiativeCode(string initiativeCode) { if (string.IsNullOrWhiteSpace(initiativeCode)) throw new ArgumentException("InitiativeCode is required."); if (!InitiativeCodePattern.IsMatch(initiativeCode)) throw new ArgumentException( "InitiativeCode must contain only alphanumeric characters and underscores (max 50 chars)."); } } }