using System; using System.Collections.Generic; using System.Data.Common; using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using GB5Shared.DTO.Draft; using GB5Shared.DTO.Framework.Login; using GB5Shared.Enums; using GB5Shared.GenerateAutoNumber; using GB5Shared.Resource.Response; using Microsoft.Extensions.Logging; using Newtonsoft.Json; namespace GB5Shared.Draft { public class DraftRowBLL : IDraftRowBLL { private readonly IDraftRowDAL _dal; private readonly AutoNumber _autoNumber; private readonly GB5Shared.EventLogPublish.EventLogPublish _eventLog; private readonly ILogger _logger; public DraftRowBLL( IDraftRowDAL dal, AutoNumber autoNumber, GB5Shared.EventLogPublish.EventLogPublish eventLog, ILogger logger) { _dal = dal; _autoNumber = autoNumber; _eventLog = eventLog; _logger = logger; } // ── Header ──────────────────────────────────────────────────────── public async Task UpsertHeaderAsync( DraftSessionDTO dto, LoginDTO login, CancellationToken ct) { try { int draftId; bool isNew; if (dto.DraftId != 0) { // FE already holds a DraftId (update path) — use it directly draftId = dto.DraftId; isNew = false; } else { // DraftId = 0 (new session) — check DB by SessionGuid first to // guard against duplicate allocations on retry/reconnect int existingId = await _dal.GetDraftIdBySessionAsync( dto.SessionGuid, login, ct).ConfigureAwait(false); if (existingId != 0) { draftId = existingId; isNew = false; } else { var autoNum = await _autoNumber.GetAutoNumber( 1, "DRAFT", login).ConfigureAwait(false); draftId = autoNum.StartNumber; isNew = true; } } await _dal.UpsertHeaderAsync(dto, draftId, login, ct).ConfigureAwait(false); return isNew ? $"Detail Saved Successfully With DraftId : {draftId}" : $"Detail Updated Successfully With DraftId : {draftId}"; } catch (Exception ex) { _logger.LogError( ex, "UpsertHeaderAsync failed for session {SessionGuid}", dto.SessionGuid); throw; } } // ── Rows ────────────────────────────────────────────────────────── public async Task UpsertRowAsync( DraftRowDTO dto, LoginDTO login, CancellationToken ct) { if (dto.RowState == RowState.Unchanged) { _logger.LogWarning( "UpsertRowAsync: RowState=Unchanged blocked. RowGuid={RowGuid} Session={Session}", dto.RowGuid, dto.SessionGuid); return $"Unchanged rows RowGuid={dto.RowGuid} Session={dto.SessionGuid}"; } // RowState=Deleted — hard-delete the row from TDRAFT_ROWS if (dto.RowState == RowState.Deleted) { try { await _dal.DeleteRowAsync(dto.SessionGuid, dto.RowGuid, login, ct).ConfigureAwait(false); _logger.LogInformation( "UpsertRowAsync: RowState=Deleted, row removed. RowGuid={RowGuid} Session={Session}", dto.RowGuid, dto.SessionGuid); return SuccessResponse.DeleteSuccessMessage; } catch (Exception ex) { _logger.LogError(ex, "UpsertRowAsync: DeleteRowAsync failed. RowGuid={RowGuid} Session={Session}", dto.RowGuid, dto.SessionGuid); throw; } } try { await _dal.UpsertRowAsync(dto, login, ct).ConfigureAwait(false); return $"Detail Saved Successfully {dto.SessionGuid}"; } catch (Exception ex) { _logger.LogError(ex, "UpsertRowAsync failed. RowGuid={RowGuid} Session={Session}", dto.RowGuid, dto.SessionGuid); throw; } } public async Task GetDraftHeaderAsync( string sessionGuid, LoginDTO login, CancellationToken ct) { try { return await _dal.GetDraftHeaderAsync(sessionGuid, login, ct).ConfigureAwait(false); } catch (Exception ex) { _logger.LogError(ex, "GetDraftHeaderAsync failed for session {SessionGuid}", sessionGuid); throw; } } public async Task GetDraftRowsAsync( string sessionGuid, LoginDTO login, CancellationToken ct) { try { var rows = await _dal.GetDraftRowsAsync(sessionGuid, login, ct) .ConfigureAwait(false); return JsonConvert.SerializeObject(rows); } catch (Exception ex) { _logger.LogError(ex, "GetDraftRowsAsync failed for session {SessionGuid}", sessionGuid); throw; } } public async Task> GetDraftRowsForPromotionAsync( string sessionGuid, LoginDTO login, CancellationToken ct) { try { return await _dal.GetDraftRowsAsync(sessionGuid, login, ct).ConfigureAwait(false); } catch (Exception ex) { _logger.LogError(ex, "GetDraftRowsForPromotionAsync failed for session {SessionGuid}", sessionGuid); throw; } } public async Task GetDraftPoolAsync( int menuId, LoginDTO login, CancellationToken ct) { try { var items = await _dal.GetDraftPoolAsync(menuId, login, ct) .ConfigureAwait(false); return JsonConvert.SerializeObject(items); } catch (Exception ex) { _logger.LogError(ex, "GetDraftPoolAsync failed for menu {MenuId}", menuId); throw; } } // ── Lifecycle ───────────────────────────────────────────────────── public async Task DiscardDraftAsync( string sessionGuid, LoginDTO login, CancellationToken ct) { try { await _dal.CleanupAsync(sessionGuid, 8, login, null, ct) .ConfigureAwait(false); await _eventLog.PublishEventLogAsync( "Draft Discarded", new { SessionGuid = sessionGuid }, EventTypeId: 3, DataId: 0, loginDTO: login, ct: ct).ConfigureAwait(false); return SuccessResponse.DeleteSuccessMessage; } catch (Exception ex) { _logger.LogError(ex, "DiscardDraftAsync failed for session {SessionGuid}", sessionGuid); throw; } } public async Task ShareDraftAsync( DraftShareDTO dto, LoginDTO login, CancellationToken ct) { // Application-layer ownership check (defence in depth — SP also checks) int ownerCount = await _dal.CheckOwnershipAsync(dto.SessionGuid, login, ct) .ConfigureAwait(false); if (ownerCount == 0) throw new UnauthorizedAccessException( "Draft not found, not owned by the current user, or not in Active state."); try { await _dal.ShareAsync(dto, login, ct).ConfigureAwait(false); await _eventLog.PublishEventLogAsync( "Draft Shared", dto, EventTypeId: 2, DataId: 0, loginDTO: login, ct: ct).ConfigureAwait(false); return SuccessResponse.UpdateSuccess; } catch (UnauthorizedAccessException) { throw; } catch (Exception ex) { _logger.LogError(ex, "ShareDraftAsync failed for session {SessionGuid} target {TargetUserId}", dto.SessionGuid, dto.TargetUserId); throw; } } /// /// Called by entity save BLLs inside their promotion transaction. /// ActionType=7=Submitted. tx participates in the entity's UoW. /// public async Task CleanupAfterSaveAsync( string sessionGuid, LoginDTO login, DbTransaction tx, CancellationToken ct) { await _dal.CleanupAsync(sessionGuid, 7, login, tx, ct).ConfigureAwait(false); } // ── Static helpers for entity BLL promotion ─────────────────────── /// /// Partitions and deserialises dirty rows by object type and state. /// Entity BLLs call this — the framework never knows the entity DTO shape. /// private static readonly System.Text.Json.JsonSerializerOptions RowDataOptions = GB5Shared.DateConverter.GB5JsonOptions.CreateDefaultOptions(); public static IEnumerable DeserializeRows( IEnumerable rows, int objectTypeId, RowState state) => rows .Where(r => r.ObjectTypeId == objectTypeId && r.RowState == state) .Select(r => System.Text.Json.JsonSerializer.Deserialize(r.RowData, RowDataOptions)!); /// Extracts ServerRowIds for DELETE batch queries. public static IEnumerable GetServerIds( IEnumerable rows, int objectTypeId, RowState state) => rows .Where(r => r.ObjectTypeId == objectTypeId && r.RowState == state) .Select(r => r.ServerRowId); } }