using System.Text.Json; using FLSBLL.Dispatch; using FLSDAL.CustomCode.Instance; using FLSDAL.CustomCode.Registration; using FLSDAL.DTO.Instance; using GB5Shared.DaprCache; using GB5Shared.DTO.Framework.Criteria; using GB5Shared.DTO.Framework.Login; using GB5Shared.EventLogPublish; using GB5Shared.GenerateAutoNumber; using GB5Shared.Resource.Response; using GB5Shared.Telemetry; using GB5Shared.Validation; using Microsoft.Extensions.Logging; using static GB5Shared.GB5Constant.Constant; namespace FLSBLL.Instance; public class FlsInstanceBLL : IFlsInstanceBLL { private readonly IFlsInstanceDAL _dal; private readonly IFlsRegistrationDAL _regDal; private readonly IFlsDispatchBLL _dispatchBll; private readonly IValidation _validation; private readonly AutoNumber _autoNumber; private readonly KeyInvalidate _keyInvalidate; private readonly EventLogPublish _eventLog; private readonly ILogger _logger; public FlsInstanceBLL( IFlsInstanceDAL dal, IFlsRegistrationDAL regDal, IFlsDispatchBLL dispatchBll, IValidation validation, AutoNumber autoNumber, KeyInvalidate keyInvalidate, EventLogPublish eventLog, ILogger logger) { _dal = dal; _regDal = regDal; _dispatchBll = dispatchBll; _validation = validation; _autoNumber = autoNumber; _keyInvalidate = keyInvalidate; _eventLog = eventLog; _logger = logger; } public async Task CreateInstanceAsync(CreateFlsInstanceRequest req, LoginDTO login, CancellationToken ct) { try { await _validation.NotEmpty(req.InstanceName, nameof(req.InstanceName)).ConfigureAwait(false); var instCacheKey = $"FlsInstance:{login.ClientId}"; // PK convention: FlsInstanceId == 0 → insert (new AutoNumber); non-zero → update existing row. if (req.FlsInstanceId == 0) { GB5Trace.Step("validate-fls-instance", new { req.InstanceCode }); await _validation.NotEmpty(req.InstanceCode, nameof(req.InstanceCode)).ConfigureAwait(false); if (!req.Groups.Any()) return "At least one group is required."; var reg = await _regDal.GetByIdAsync(req.FlsRegistrationId, login, ct).ConfigureAwait(false); if (reg is null || reg.Status != 1) return "Registration not found or inactive."; bool duplicate = await _dal.CodeExistsAsync(req.InstanceCode, 0, login, ct).ConfigureAwait(false); if (duplicate) return $"Instance code '{req.InstanceCode}' already exists."; var instanceAutoNum = await _autoNumber.GetNumberAsync(1, "FLSINSTANCE", login).ConfigureAwait(false); int flsInstanceId = instanceAutoNum.StartNumber; GB5Trace.Step("save-fls-instance", new { req.InstanceCode }); await _dal.InsertInstanceAsync(flsInstanceId, req, login, ct).ConfigureAwait(false); var groupAutoNum = await _autoNumber.GetNumberAsync(req.Groups.Count, "FLSINSTANCEGROUP", login).ConfigureAwait(false); var groupIds = Enumerable.Range(groupAutoNum.StartNumber, req.Groups.Count).ToList(); var groupCodeToId = await _dal.InsertGroupsAsync(flsInstanceId, req.Groups, groupIds, login, ct).ConfigureAwait(false); if (req.Incharges.Any()) { var inchargeAutoNum = await _autoNumber.GetNumberAsync(req.Incharges.Count, "FLSINSTANCEINCHARGE", login).ConfigureAwait(false); var inchargeIds = Enumerable.Range(inchargeAutoNum.StartNumber, req.Incharges.Count).ToList(); await _dal.InsertInchargesAsync(flsInstanceId, req.Incharges, inchargeIds, groupCodeToId, login, ct).ConfigureAwait(false); } if (req.OpensFrom.HasValue && req.ClosesOn.HasValue) { var scheduleAutoNum = await _autoNumber.GetNumberAsync(1, "FLSINSTANCESCHEDULE", login).ConfigureAwait(false); await _dal.UpsertScheduleAsync( flsInstanceId, scheduleAutoNum.StartNumber, req.OpensFrom.Value, req.ClosesOn.Value, req.GlobalDeadlineDt, login, ct).ConfigureAwait(false); } GB5Trace.Step("event-publish", new { EventTypeId = EventTypeConstant.SAVEDOCUMENTSET }); await _eventLog.PublishEventLogAsync( "FLS Instance Created", new { req.InstanceCode, flsInstanceId }, EventTypeConstant.SAVEDOCUMENTSET, flsInstanceId, login, ct: ct).ConfigureAwait(false); await _keyInvalidate.InvalidateCache(instCacheKey).ConfigureAwait(false); return $"{SuccessResponse.SaveSuccessMessage} {flsInstanceId}"; } else { var existing = await _dal.GetByIdAsync(req.FlsInstanceId, login, ct).ConfigureAwait(false); if (existing is null) return $"Instance {req.FlsInstanceId} not found."; GB5Trace.Step("update-fls-instance", new { req.FlsInstanceId }); bool updated = await _dal.UpdateInstanceAsync(req.FlsInstanceId, req, login, ct).ConfigureAwait(false); if (!updated) return $"Instance {req.FlsInstanceId} not found."; if (req.OpensFrom.HasValue && req.ClosesOn.HasValue) { var scheduleAutoNum = await _autoNumber.GetNumberAsync(1, "FLSINSTANCESCHEDULE", login).ConfigureAwait(false); await _dal.UpsertScheduleAsync( req.FlsInstanceId, scheduleAutoNum.StartNumber, req.OpensFrom.Value, req.ClosesOn.Value, req.GlobalDeadlineDt, login, ct).ConfigureAwait(false); } await _keyInvalidate.InvalidateCache(instCacheKey).ConfigureAwait(false); return $"{SuccessResponse.UpdateSuccess} {req.FlsInstanceId}"; } } catch (Exception ex) { GB5Trace.MarkFailed("save-fls-instance-failed", ex); _logger.LogError(ex, "CreateInstance failed for code {Code}", req.InstanceCode); throw; } } public async Task ScheduleInstanceAsync(ScheduleInstanceRequest req, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("validate-fls-schedule", new { req.FlsInstanceId }); var instance = await _dal.GetByIdAsync(req.FlsInstanceId, login, ct).ConfigureAwait(false); if (instance is null) return "Instance not found."; if (instance.InstanceStatus != 0) return "Only Draft instances can be scheduled."; if (req.OpensFrom >= req.ClosesOn) return "OpensFrom must be before ClosesOn."; var scheduleAutoNum = await _autoNumber.GetNumberAsync(1, "FLSINSTANCESCHEDULE", login).ConfigureAwait(false); GB5Trace.Step("save-fls-schedule", new { req.FlsInstanceId }); await _dal.UpsertScheduleAsync( req.FlsInstanceId, scheduleAutoNum.StartNumber, req.OpensFrom, req.ClosesOn, req.GlobalDeadlineDt, login, ct).ConfigureAwait(false); await _keyInvalidate.InvalidateCache($"FlsInstance:{login.ClientId}").ConfigureAwait(false); return SuccessResponse.UpdateSuccess; } catch (Exception ex) { GB5Trace.MarkFailed("save-fls-schedule-failed", ex); _logger.LogError(ex, "ScheduleInstance failed for id {Id}", req.FlsInstanceId); throw; } } public async Task OpenInstanceAsync(int flsInstanceId, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("open-fls-instance", new { flsInstanceId }); var instance = await _dal.GetByIdAsync(flsInstanceId, login, ct).ConfigureAwait(false); if (instance is null) return "Instance not found."; if (instance.InstanceStatus is not (1 or 3)) // must be Scheduled(1) or Paused(3) return "Instance must be Scheduled or Paused to open."; await _dal.OpenAsync(flsInstanceId, login, ct).ConfigureAwait(false); // Dispatch EIP to all NOT_STARTED respondents in each group var groups = await _dal.GetGroupsAsync(flsInstanceId, login, ct).ConfigureAwait(false); foreach (var g in groups) await _dispatchBll.DispatchInitialAsync(flsInstanceId, g.GroupId, login, ct).ConfigureAwait(false); await _keyInvalidate.InvalidateCache($"FlsInstance:{login.ClientId}").ConfigureAwait(false); return SuccessResponse.UpdateSuccess; } catch (Exception ex) { GB5Trace.MarkFailed("open-fls-instance-failed", ex); _logger.LogError(ex, "OpenInstance failed for id {Id}", flsInstanceId); throw; } } public async Task ControlInstanceAsync(ControlInstanceRequest req, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("control-fls-instance", new { req.FlsInstanceId, req.Action }); var instance = await _dal.GetByIdAsync(req.FlsInstanceId, login, ct).ConfigureAwait(false); if (instance is null) return "Instance not found."; // Validate transitions bool valid = req.Action switch { 0 => instance.InstanceStatus == 2, // Pause: must be Open 1 => instance.InstanceStatus == 3, // Resume: must be Paused 2 => instance.InstanceStatus == 2, // Close: must be Open 3 => instance.InstanceStatus == 4, // Archive: must be Closed _ => false }; if (!valid) return $"Action {req.Action} is not valid for instance status {instance.InstanceStatus}."; GB5Trace.Step("save-fls-control", new { req.FlsInstanceId, req.Action }); await _dal.ControlAsync(req.FlsInstanceId, req.Action, req.ActionNote, login, ct).ConfigureAwait(false); await _keyInvalidate.InvalidateCache($"FlsInstance:{login.ClientId}").ConfigureAwait(false); return SuccessResponse.UpdateSuccess; } catch (Exception ex) { GB5Trace.MarkFailed("control-fls-instance-failed", ex); _logger.LogError(ex, "ControlInstance failed for id {Id}", req.FlsInstanceId); throw; } } public async Task GetInstanceAsync(int flsInstanceId, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("get-fls-instance", new { flsInstanceId }); var instance = await _dal.GetByIdAsync(flsInstanceId, login, ct).ConfigureAwait(false); var groups = await _dal.GetGroupsAsync(flsInstanceId, login, ct).ConfigureAwait(false); var incharges = await _dal.GetInchargesAsync(flsInstanceId, login, ct).ConfigureAwait(false); return JsonSerializer.Serialize(new { Instance = instance, Groups = groups, Incharges = incharges }); } catch (Exception ex) { GB5Trace.MarkFailed("get-fls-instance-failed", ex); _logger.LogError(ex, "GetInstance failed for id {Id}", flsInstanceId); throw; } } public async Task GetSummaryAsync(int flsInstanceId, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("get-fls-summary", new { flsInstanceId }); var summaryRows = await _dal.GetSummaryAsync(flsInstanceId, login, ct).ConfigureAwait(false); var rows = summaryRows.ToList(); var groupDtos = rows.Select(r => new FlsGroupSummaryDto( r.GroupId, r.GroupCode, r.GroupName, r.RespondentCount, r.SubmittedCount, r.DeadlineDt, r.BlockingFlag)).ToList(); int total = rows.Sum(r => r.RespondentCount); int submitted = rows.Sum(r => r.SubmittedCount); decimal pct = total > 0 ? Math.Round((decimal)submitted / total * 100, 1) : 0; var now = DateTime.UtcNow; int overdue = rows.Where(r => r.DeadlineDt.HasValue && r.DeadlineDt < now) .Sum(r => r.RespondentCount - r.SubmittedCount); var dto = new FlsInstanceSummaryDto( FlsInstanceId : flsInstanceId, InstanceName : rows.FirstOrDefault()?.InstanceName ?? string.Empty, InstanceStatus : rows.FirstOrDefault()?.InstanceStatus ?? 0, OpensFrom : rows.FirstOrDefault()?.OpensFrom, ClosesOn : rows.FirstOrDefault()?.ClosesOn, Groups : groupDtos, TotalRespondents : total, Submitted : submitted, InProgress : 0, NotStarted : total - submitted, Expired : 0, Overdue : overdue, CompletionPct : pct); return JsonSerializer.Serialize(dto); } catch (Exception ex) { GB5Trace.MarkFailed("get-fls-summary-failed", ex); _logger.LogError(ex, "GetSummary failed for id {Id}", flsInstanceId); throw; } } public async Task> GetSelectListFlsInstance( int firstNumber, int maxResult, CriteriaDTO criteriaDTO, LoginDTO login) { try { if (firstNumber <= 0 && maxResult <= 0) { firstNumber = -1; maxResult = -1; } return await _dal.GetSelectListFlsInstance(firstNumber, maxResult, criteriaDTO, login).ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("get-selectlist-flsinstance-failed", ex); _logger.LogError(ex, "GetSelectListFlsInstance failed"); throw; } } public async Task> GetSelectListFlsInstanceGroup( int flsInstanceId, int firstNumber, int maxResult, CriteriaDTO criteriaDTO, LoginDTO login) { try { if (firstNumber <= 0 && maxResult <= 0) { firstNumber = -1; maxResult = -1; } return await _dal.GetSelectListFlsInstanceGroup(flsInstanceId, firstNumber, maxResult, criteriaDTO, login).ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("get-selectlist-flsinstancegroup-failed", ex); _logger.LogError(ex, "GetSelectListFlsInstanceGroup failed for instance {Id}", flsInstanceId); throw; } } public async Task SaveInstanceGroupAsync(SaveFlsInstanceGroupRequest req, LoginDTO login, CancellationToken ct) { try { // PK convention: GroupId == 0 → insert (new AutoNumber); non-zero → update existing row. if (req.GroupId == 0) { GB5Trace.Step("save-instance-group", new { req.FlsInstanceId, req.GroupCode }); var autoNum = await _autoNumber.GetNumberAsync(1, "FLSINSTANCEGROUP", login).ConfigureAwait(false); int groupId = autoNum.StartNumber; await _dal.InsertGroupOnlyAsync(groupId, req, login, ct).ConfigureAwait(false); return $"{SuccessResponse.SaveSuccessMessage} {groupId}"; } else { GB5Trace.Step("update-instance-group", new { req.GroupId }); bool updated = await _dal.UpdateGroupOnlyAsync(req, login, ct).ConfigureAwait(false); if (!updated) return $"Group {req.GroupId} not found."; return $"{SuccessResponse.UpdateSuccess} {req.GroupId}"; } } catch (Exception ex) { GB5Trace.MarkFailed("save-instance-group-failed", ex); _logger.LogError(ex, "SaveInstanceGroup failed for groupId {Id}", req.GroupId); throw; } } public async Task DeleteInstanceGroupAsync(int groupId, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("delete-instance-group", new { groupId }); bool deleted = await _dal.DeleteGroupAsync(groupId, login, ct).ConfigureAwait(false); return deleted ? SuccessResponse.DeleteSuccessMessage : $"Group {groupId} not found."; } catch (Exception ex) { GB5Trace.MarkFailed("delete-instance-group-failed", ex); _logger.LogError(ex, "DeleteInstanceGroup failed for id {Id}", groupId); throw; } } public async Task GetInstanceGroupAsync(int groupId, LoginDTO login, CancellationToken ct) { try { return await _dal.GetGroupByIdAsync(groupId, login, ct).ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("get-instance-group-failed", ex); _logger.LogError(ex, "GetInstanceGroup failed for id {Id}", groupId); throw; } } public async Task> GetInstanceGroupListAsync(int flsInstanceId, LoginDTO login, CancellationToken ct) { try { return await _dal.GetGroupsAsync(flsInstanceId, login, ct).ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("get-instance-group-list-failed", ex); _logger.LogError(ex, "GetInstanceGroupList failed for instance {Id}", flsInstanceId); throw; } } public async Task> GetInstancesAsync(int flsRegistrationId, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("get-fls-instances", new { flsRegistrationId }); return await _dal.GetInstancesByRegistrationAsync(flsRegistrationId, login, ct).ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("get-fls-instances-failed", ex); _logger.LogError(ex, "GetInstances failed for registration {Id}", flsRegistrationId); throw; } } public async Task> GetInchargesAsync(int flsInstanceId, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("get-fls-incharges", new { flsInstanceId }); return await _dal.GetInchargesAsync(flsInstanceId, login, ct).ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("get-fls-incharges-failed", ex); _logger.LogError(ex, "GetIncharges failed for instance {Id}", flsInstanceId); throw; } } }