using System.Linq; using System.Text.Json; using GB5Shared.DaprCache; using GB5Shared.DTO.Framework.AutoNumber; using GB5Shared.DTO.Framework.Criteria; using GB5Shared.DTO.Framework.Login; using GB5Shared.GenerateAutoNumber; using GB5Shared.Resource.Response; using GB5Shared.Validation; using WiDAL.CustomCode.WiAssignment; using WiDAL.DTOs; namespace WiBLL.WiAssignment; public class WiAssignmentBLL : IWiAssignmentBLL { private readonly IWiAssignmentDAL _dal; private readonly AutoNumber _autoNumber; private readonly IValidation _validation; private readonly KeyInvalidate _keyInvalidate; private static readonly JsonSerializerOptions s_caseInsensitiveJson = new() { PropertyNameCaseInsensitive = true }; public WiAssignmentBLL(IWiAssignmentDAL dal, AutoNumber autoNumber, IValidation validation, KeyInvalidate keyInvalidate) { _dal = dal; _autoNumber = autoNumber; _validation = validation; _keyInvalidate = keyInvalidate; } // ResolveForWorkstation/ResolveForEntity (WiSL/EndPoints/WiExecution) cache their result // under a literal key keyed on the REQUEST's concrete dimensions — a saved/deleted // assignment row can only ever change what a future resolve returns if that row's own // dimension is either the exact same concrete value (safe to invalidate one key) or a // -1 wildcard (matches every concrete value for that dimension — not enumerable, so the // whole namespace for this tenant/EntityType has to be scope-invalidated instead of left // stale for the endpoint's cache TTL). See GetCacheKey() on both endpoints for the exact // literal format being reconstructed here. private async Task InvalidateResolveCachesAsync(WiAssignmentDTO dto, LoginDTO login) { if (dto.Schedules is not { Count: > 0 }) return; var triggerEvents = dto.Schedules.Select(s => (byte)s.TriggerEvent).Distinct().ToList(); if (dto.WorkStationId == -1) { await _keyInvalidate.InvalidateByPattern($"Wi:ResolveWorkstation:{login.ClientId}:").ConfigureAwait(false); } else { foreach (var triggerEvent in triggerEvents) await _keyInvalidate.InvalidateCache($"Wi:ResolveWorkstation:{login.ClientId}:{dto.WorkStationId}:{triggerEvent}").ConfigureAwait(false); } if (dto.EntityId == -1) { await _keyInvalidate.InvalidateByPattern($"Wi:ResolveEntity:{login.ClientId}:{dto.EntityType}:").ConfigureAwait(false); } else { foreach (var triggerEvent in triggerEvents) await _keyInvalidate.InvalidateCache($"Wi:ResolveEntity:{login.ClientId}:{dto.EntityType}:{dto.EntityId}:{triggerEvent}").ConfigureAwait(false); } } public async Task GetAssignmentsById(int WiAssignmentId, LoginDTO login, CancellationToken ct) => await _dal.GetAssignmentsById(WiAssignmentId, login, ct).ConfigureAwait(false); public async Task GetWorkstationConfig(int workStationId, LoginDTO login, CancellationToken ct) { var config = await _dal.GetWorkstationConfig(workStationId, login, ct).ConfigureAwait(false); return JsonSerializer.Serialize(config); } //public async Task SaveAssignment(WiAssignmentDTO dto, LoginDTO login, CancellationToken ct) //{ // dto.SpecificityScore = WiMaster.WiMasterBLL.ComputeSpecificityScore(dto); // dto.TenantId = login.ClientId; // dto.ModifiedById = login.UserId; // bool isNew = dto.WiAssignmentId == 0; // GB5Shared.DTO.Framework.AutoNumber.AutoNumberDTO? an = null; // try // { // if (isNew) // { // int scheduleCount = dto.Schedules?.Count ?? 0; // int totalIds = 1 + scheduleCount; // an = await _autoNumber.GetNumberAsync(totalIds, "WIVERSION", login).ConfigureAwait(false); // int nextId = an.StartNumber; // dto.WiAssignmentId = nextId++; // dto.CreatedById = login.UserId; // foreach (var schedule in dto.Schedules ?? []) // { // schedule.WiAssignmentId = dto.WiAssignmentId; // schedule.WiActivityScheduleId = nextId++; // } // } // await _dal.SaveAssignment(dto, login, ct).ConfigureAwait(false); // return isNew // ? $"{SuccessResponse.SaveSuccessMessage} {dto.WiAssignmentId}" // : $"{SuccessResponse.UpdateSuccessMessage} {dto.WiAssignmentId}"; // } // catch // { // if (an != null && isNew) // await _autoNumber.RollbackAutoNumber("WIVERSION", an.StartNumber, login).ConfigureAwait(false); // throw; // } //} public async Task SaveAssignment(WiAssignmentDTO dto, LoginDTO login, CancellationToken ct) { if (dto == null) throw new ArgumentNullException(nameof(dto)); AutoNumberDTO? autoNumberDTO = null; try { dto.TenantId = login.ClientId; dto.ModifiedById = login.UserId; bool isNew = dto.WiAssignmentId == 0; if (isNew) { autoNumberDTO = await _autoNumber.GetNumberAsync(1, "WIASSIGNMENT", login); dto.WiAssignmentId = autoNumberDTO.StartNumber; // ID changes here dto.CreatedById = login.UserId; } // Assign real WiActivityScheduleIds before the DAL persists them — ResolveForWorkstation/ // ResolveForEntity INNER JOIN MWIACTIVITYSCHEDULE, so an assignment with no schedule rows // can never actually be resolved. Was previously silently dropped entirely (dto.Schedules // was never wired to any DAL write) — every assignment saved via this endpoint was // unreachable through ResolveForWorkstation/ResolveForEntity regardless of caller intent. if (dto.Schedules is { Count: > 0 }) { var scheduleNumbers = await _autoNumber.GetNumberAsync(dto.Schedules.Count, "WIACTIVITYSCHEDULE", login); var nextId = scheduleNumbers.StartNumber; foreach (var schedule in dto.Schedules) { schedule.WiAssignmentId = dto.WiAssignmentId; schedule.WiActivityScheduleId = nextId++; } } // ✅ pass isNew to DAL await _dal.SaveAssignment(dto, isNew, login, ct); await InvalidateResolveCachesAsync(dto, login).ConfigureAwait(false); return isNew ? string.Format(SuccessResponse.SaveSuccessWithName, dto.AssignmentName) : string.Format(SuccessResponse.UpdateSuccessName, dto.AssignmentName); } catch { if (autoNumberDTO != null && dto.WiAssignmentId > 0) { await _autoNumber.RollbackAutoNumber("WIASSIGNMENT", dto.WiAssignmentId, login); } throw; } } public async Task SaveWorkstationConfig(WorkstationConfigDTO dto, LoginDTO login, CancellationToken ct) { dto.TenantId = login.ClientId; dto.ModifiedById = login.UserId; bool isNew = dto.WorkStationConfigId == 0; GB5Shared.DTO.Framework.AutoNumber.AutoNumberDTO? an = null; try { if (isNew) { an = await _autoNumber.GetNumberAsync(1, "WORKSTATIONCONFIG", login).ConfigureAwait(false); dto.WorkStationConfigId = an.StartNumber; dto.CreatedById = login.UserId; } await _dal.SaveWorkstationConfig(dto, login, ct).ConfigureAwait(false); return isNew ? $"{SuccessResponse.SaveSuccess}" : $"{SuccessResponse.UpdateSuccess}"; } catch { if (an != null && isNew) await _autoNumber.RollbackAutoNumber("WORKSTATIONCONFIG", an.StartNumber, login).ConfigureAwait(false); throw; } } public async Task GetSelectListAssignment(CriteriaDTO criteriaDTO, LoginDTO login, CancellationToken ct) => await _dal.GetSelectListAssignment(criteriaDTO, login, ct); public async Task DeleteAssignment(int AssignmentId, LoginDTO loginDTO, CancellationToken ct) { try { // Fetch before delete — invalidation needs the assignment's own dimensions // (WorkStationId/EntityType/EntityId/Schedules), which are gone once the row is. var existingJson = await _dal.GetAssignmentsById(AssignmentId, loginDTO, ct).ConfigureAwait(false); var existing = JsonSerializer.Deserialize>(existingJson, s_caseInsensitiveJson)?.FirstOrDefault(); var result = await _dal.DeleteAssignment(AssignmentId, loginDTO, ct); if (existing != null) await InvalidateResolveCachesAsync(existing, loginDTO).ConfigureAwait(false); return result; } catch (Exception) { throw; } } public async Task GetAssignmentList(CriteriaDTO criteriaDTO, LoginDTO login, CancellationToken ct) => await _dal.GetAssignmentList(criteriaDTO, login, ct); }