using GB5Shared.DaprCache; using GB5Shared.DTO.Framework.Login; using GB5Shared.EntityHandler; using GB5Shared.GenerateAutoNumber; using GB5Shared.QueryExecutor; using GB5Shared.Resource.Response; using PIEDAL.CustomCode.Module; using PIEDAL.DTO.Module; using static GB5Shared.GB5Constant.Constant; namespace PIEBLL.Module; public class PIEModuleBLL : IPIEModuleBLL { private readonly IPIEModuleDAL _PIEModuleDAL; private readonly IQueryExecutor _QueryExecutor; private readonly KeyInvalidate _KeyInvalidate; private readonly BaseEntityAppService _BaseEntityAppService; public PIEModuleBLL( IPIEModuleDAL pieModuleDAL, IQueryExecutor queryExecutor, KeyInvalidate keyInvalidate, BaseEntityAppService baseEntityAppService) { _PIEModuleDAL = pieModuleDAL; _QueryExecutor = queryExecutor; _KeyInvalidate = keyInvalidate; _BaseEntityAppService = baseEntityAppService; } // ── Reads ───────────────────────────────────────────────────────────────── public async Task GetPIEModuleAsync( Guid moduleId, LoginDTO login, CancellationToken ct) { var module = await _PIEModuleDAL.GetPIEModuleAsync(moduleId, login, ct).ConfigureAwait(false); if (module is null) return null; var deps = await _PIEModuleDAL.GetPIEModuleDependenciesAsync(moduleId, login, ct).ConfigureAwait(false); var kpis = await _PIEModuleDAL.GetKpisByModuleAsync(moduleId, login, ct).ConfigureAwait(false); module.Dependencies = deps.ToList(); module.KpiItems = kpis.ToList(); return module; } public async Task> GetPIEModuleSelectListAsync( LoginDTO login, CancellationToken ct) => await _PIEModuleDAL.GetPIEModuleSelectListAsync(login, ct).ConfigureAwait(false); public async Task GetPIEKpiAsync( Guid kpiId, LoginDTO login, CancellationToken ct) => await _PIEModuleDAL.GetPIEKpiAsync(kpiId, login, ct).ConfigureAwait(false); // ── Saves ───────────────────────────────────────────────────────────────── public async Task SavePIEModuleAsync( PIEModuleDTO dto, LoginDTO login, CancellationToken ct) { if (dto is null) throw new ArgumentNullException(nameof(dto)); // Validate dependency graph before touching the DB await ValidateDependencyCycleAsync(dto, login, ct).ConfigureAwait(false); bool isNew = dto.ModuleId == Guid.Empty; if (isNew) dto.ModuleId = Guid.NewGuid(); var Trans = await _QueryExecutor.BeginTransactionAsync(login).ConfigureAwait(false); try { await _BaseEntityAppService.ExecuteSaveAsync( EntityConstant.OBJECTPIEMODULE, isNew ? EventTypeConstant.SAVEPIEMODULEEVENTTYPEID : EventTypeConstant.UPDATEPIEMODULEEVENTTYPEID, dto, login, async tx => { if (isNew) await _PIEModuleDAL.SavePIEModuleAsync(dto, login, tx, ct); else await _PIEModuleDAL.UpdatePIEModuleAsync(dto, login, tx, ct); // Replace dependencies await _PIEModuleDAL.DeleteModuleDependenciesAsync(dto.ModuleId, login, tx, ct); foreach (var dep in dto.Dependencies) { dep.DependencyId = dep.DependencyId == Guid.Empty ? Guid.NewGuid() : dep.DependencyId; dep.ModuleId = dto.ModuleId; dep.TenantId = dto.TenantId; await _PIEModuleDAL.InsertModuleDependencyAsync(dep, login, tx, ct); } return 0; // PIEModule uses a Guid PK — no int object ID to report back }, null, BIZTRANSACTIONCLASSCONSTANT.PIEMODULE, BizTransactionConstant.PIEMODULE, Trans).ConfigureAwait(false); await _QueryExecutor.CommitAsync(Trans).ConfigureAwait(false); var cacheKey = new CacheKeyGeneration().KeyGeneration( dto.ModuleId, EntityConstant.OBJECTPIEMODULE, CacheKeyLevel.CLIENT_LEVEL, login); await _KeyInvalidate.AllInvalidateCache(cacheKey).ConfigureAwait(false); return isNew ? SuccessResponse.SaveSuccess : SuccessResponse.UpdateSuccess; } catch (Exception ex) { GB5Shared.Telemetry.GB5Trace.MarkFailed("save-pie-module-failed", ex); await _QueryExecutor.RollbackAsync(Trans).ConfigureAwait(false); throw; } } public async Task DeletePIEModuleAsync( Guid moduleId, LoginDTO login, CancellationToken ct) { var Trans = await _QueryExecutor.BeginTransactionAsync(login).ConfigureAwait(false); try { await _BaseEntityAppService.ExecuteSaveAsync( EntityConstant.OBJECTPIEMODULE, EventTypeConstant.DELETEPIEMODULEEVENTTYPEID, new PIEModuleDTO { ModuleId = moduleId }, login, async tx => { await _PIEModuleDAL.SoftDeletePIEModuleAsync(moduleId, login, tx, ct); return 0; }, null, BIZTRANSACTIONCLASSCONSTANT.PIEMODULE, BizTransactionConstant.PIEMODULE, Trans).ConfigureAwait(false); await _QueryExecutor.CommitAsync(Trans).ConfigureAwait(false); var cacheKey = new CacheKeyGeneration().KeyGeneration( moduleId, EntityConstant.OBJECTPIEMODULE, CacheKeyLevel.CLIENT_LEVEL, login); await _KeyInvalidate.AllInvalidateCache(cacheKey).ConfigureAwait(false); return SuccessResponse.DeleteSuccessMessage; } catch (Exception ex) { GB5Shared.Telemetry.GB5Trace.MarkFailed("delete-pie-module-failed", ex); await _QueryExecutor.RollbackAsync(Trans).ConfigureAwait(false); throw; } } public async Task SavePIEKpiAsync( PIEKpiDTO dto, LoginDTO login, CancellationToken ct) { if (dto is null) throw new ArgumentNullException(nameof(dto)); bool isNew = dto.KpiId == Guid.Empty; if (isNew) dto.KpiId = Guid.NewGuid(); var Trans = await _QueryExecutor.BeginTransactionAsync(login).ConfigureAwait(false); try { // KPIs are module children — use module entity constant for audit await _BaseEntityAppService.ExecuteSaveAsync( EntityConstant.OBJECTPIEMODULE, isNew ? EventTypeConstant.SAVEPIEMODULEEVENTTYPEID : EventTypeConstant.UPDATEPIEMODULEEVENTTYPEID, new PIEModuleDTO { ModuleId = dto.ModuleId }, login, async tx => { if (isNew) await _PIEModuleDAL.SavePIEKpiAsync(dto, login, tx, ct); else await _PIEModuleDAL.UpdatePIEKpiAsync(dto, login, tx, ct); return 0; // PIEKpi uses a Guid PK — no int object ID to report back }, null, BIZTRANSACTIONCLASSCONSTANT.PIEMODULE, BizTransactionConstant.PIEMODULE, Trans).ConfigureAwait(false); await _QueryExecutor.CommitAsync(Trans).ConfigureAwait(false); var cacheKey = new CacheKeyGeneration().KeyGeneration( dto.ModuleId, EntityConstant.OBJECTPIEMODULE, CacheKeyLevel.CLIENT_LEVEL, login); await _KeyInvalidate.AllInvalidateCache(cacheKey).ConfigureAwait(false); return isNew ? SuccessResponse.SaveSuccess : SuccessResponse.UpdateSuccess; } catch (Exception ex) { GB5Shared.Telemetry.GB5Trace.MarkFailed("save-pie-kpi-failed", ex); await _QueryExecutor.RollbackAsync(Trans).ConfigureAwait(false); throw; } } public async Task DeletePIEKpiAsync( Guid kpiId, LoginDTO login, CancellationToken ct) { var Trans = await _QueryExecutor.BeginTransactionAsync(login).ConfigureAwait(false); try { await _BaseEntityAppService.ExecuteSaveAsync( EntityConstant.OBJECTPIEMODULE, EventTypeConstant.DELETEPIEMODULEEVENTTYPEID, new PIEModuleDTO { ModuleId = Guid.Empty }, login, async tx => { await _PIEModuleDAL.SoftDeletePIEKpiAsync(kpiId, login, tx, ct); return 0; }, null, BIZTRANSACTIONCLASSCONSTANT.PIEMODULE, BizTransactionConstant.PIEMODULE, Trans).ConfigureAwait(false); await _QueryExecutor.CommitAsync(Trans).ConfigureAwait(false); return SuccessResponse.DeleteSuccessMessage; } catch (Exception ex) { GB5Shared.Telemetry.GB5Trace.MarkFailed("delete-pie-kpi-failed", ex); await _QueryExecutor.RollbackAsync(Trans).ConfigureAwait(false); throw; } } // ── Cycle detection (DFS on dependency graph) ───────────────────────────── private async Task ValidateDependencyCycleAsync( PIEModuleDTO dto, LoginDTO login, CancellationToken ct) { if (dto.Dependencies.Count == 0) return; var existingEdges = await _PIEModuleDAL.GetAllDependenciesForCycleCheckAsync(login, ct).ConfigureAwait(false); // Exclude current module's existing edges (they'll be replaced) var otherEdges = existingEdges .Where(e => e.ModuleId != dto.ModuleId) .ToList(); // Build proposed graph: existing (other modules) + new edges from this save var proposedEdges = otherEdges .Concat(dto.Dependencies.Select(d => new PIEModuleDependencyDTO { ModuleId = dto.ModuleId, DependsOnModuleId = d.DependsOnModuleId })) .ToList(); if (GraphHasCycle(proposedEdges)) throw new InvalidOperationException( "Circular module dependency detected. Save cancelled."); } private static bool GraphHasCycle(List edges) { var graph = edges .GroupBy(e => e.ModuleId) .ToDictionary( g => g.Key, g => g.Select(e => e.DependsOnModuleId).ToHashSet()); var visited = new HashSet(); var stack = new HashSet(); foreach (var node in graph.Keys) { if (DfsHasCycle(graph, node, visited, stack)) return true; } return false; } private static bool DfsHasCycle( Dictionary> graph, Guid node, HashSet visited, HashSet stack) { if (stack.Contains(node)) return true; if (visited.Contains(node)) return false; visited.Add(node); stack.Add(node); if (graph.TryGetValue(node, out var neighbors)) { foreach (var neighbor in neighbors) { if (DfsHasCycle(graph, neighbor, visited, stack)) return true; } } stack.Remove(node); return false; } }