using System.Text.Json; using FLSBLL.Event; using FLSDAL.CustomCode.Respondent; using FLSDAL.CustomCode.Session; using FLSDAL.DTO.Session; using GB5Shared.DTO.Framework.Login; using GB5Shared.GenerateAutoNumber; using GB5Shared.Resource.Response; using GB5Shared.Telemetry; using Microsoft.Extensions.Logging; namespace FLSBLL.Session; public class FlsSessionBLL : IFlsSessionBLL { private readonly IFlsSessionDAL _sessionDal; private readonly IFlsRespondentDAL _respondentDal; private readonly IFlsEventBLL _eventBll; private readonly AutoNumber _autoNumber; private readonly ILogger _logger; public FlsSessionBLL( IFlsSessionDAL sessionDal, IFlsRespondentDAL respondentDal, IFlsEventBLL eventBll, AutoNumber autoNumber, ILogger logger) { _sessionDal = sessionDal; _respondentDal = respondentDal; _eventBll = eventBll; _autoNumber = autoNumber; _logger = logger; } public async Task OpenSessionAsync( Guid accessToken, string? ipAddress, string? userAgent, CancellationToken ct) { try { GB5Trace.Step("open-fls-session", new { tokenPartial = accessToken.ToString()[..8] }); // Token lookup uses ClientId=0 (cross-tenant system DB lookup) var sysLogin = new LoginDTO { ClientId = 0, UserId = -1 }; var respondent = await _respondentDal.GetByTokenAsync(accessToken, sysLogin, ct).ConfigureAwait(false); if (respondent is null) return ErrorContext("TOKEN_NOT_FOUND"); if (respondent.TokenExpiry.HasValue && respondent.TokenExpiry < DateTime.UtcNow) return ErrorContext("TOKEN_EXPIRED"); if (respondent.RespondentSts == 3) return new FlsSessionContextDto( FlsSessionId: 0, FlsInstanceId: respondent.FlsInstanceId, GroupId: respondent.GroupId, InstrumentConfigId: -1, FlsRegistrationId: -1, SourceObjectTypeId: respondent.SourceObjectTypeId, StepNo: 0, AllowDraftResponse: 0, IsAnonymous: false, AllowMultiAttempt: 0, ErrorCode: "ALREADY_SUBMITTED"); // Build tenant-specific login from respondent row var tenantLogin = new LoginDTO { ClientId = respondent.TenantId, UserId = -1 }; var context = await _sessionDal.GetOpenContextAsync(respondent.FlsRespondentId, tenantLogin, ct).ConfigureAwait(false); if (context is null) return ErrorContext("SESSION_OPEN_FAILED"); // Reuse an in-progress session for this step if one exists; otherwise start a new attempt. int? flsSessionId = await _sessionDal.GetActiveSessionIdAsync( respondent.FlsRespondentId, context.StepNo, tenantLogin, ct).ConfigureAwait(false); if (flsSessionId is null) { int maxAttemptNo = await _sessionDal.GetMaxAttemptNoAsync( respondent.FlsRespondentId, context.StepNo, tenantLogin, ct).ConfigureAwait(false); var autoNum = await _autoNumber.GetNumberAsync(1, "FLSSESSION", tenantLogin).ConfigureAwait(false); flsSessionId = autoNum.StartNumber; await _sessionDal.InsertSessionAsync( flsSessionId.Value, respondent.FlsRespondentId, context.FlsInstanceId, context.GroupId, context.StepNo, maxAttemptNo + 1, tenantLogin, ct).ConfigureAwait(false); } await _sessionDal.MarkRespondentOpenedAsync(respondent.FlsRespondentId, tenantLogin, ct).ConfigureAwait(false); await LogAuditSafeAsync( flsSessionId.Value, respondent.FlsRespondentId, context.FlsInstanceId, AuditAction.Opened, ipAddress, userAgent, respondent.FlsRespondentId, tenantLogin, ct).ConfigureAwait(false); await _eventBll.PublishAsync( context.FlsInstanceId, respondent.FlsRespondentId, context.GroupId, context.StepNo, FlsEventType.RespondentOpened, templateDataId: null, sourceReference: null, tenantLogin, ct).ConfigureAwait(false); return new FlsSessionContextDto( FlsSessionId : flsSessionId.Value, FlsInstanceId : context.FlsInstanceId, GroupId : context.GroupId, InstrumentConfigId : context.InstrumentConfigId, FlsRegistrationId : context.FlsRegistrationId, SourceObjectTypeId : respondent.SourceObjectTypeId, StepNo : context.StepNo, AllowDraftResponse : context.AllowDraftResponse, IsAnonymous : context.IsAnonymous == 1, AllowMultiAttempt : context.AllowMultiAttempt, ErrorCode : null); } catch (Exception ex) { GB5Trace.MarkFailed("open-fls-session-failed", ex); _logger.LogError(ex, "OpenSession failed for token {Token}", accessToken.ToString()[..8]); throw; } } public async Task SaveDraftAsync(int flsSessionId, string draftDataJson, CancellationToken ct) { try { GB5Trace.Step("save-fls-draft", new { flsSessionId }); var session = await GetSessionRowCrosstenantAsync(flsSessionId, ct).ConfigureAwait(false); if (session is null) return "Session not found."; var login = new LoginDTO { ClientId = session.TenantId, UserId = -1 }; await _sessionDal.SaveDraftAsync(flsSessionId, draftDataJson, login, ct).ConfigureAwait(false); await LogAuditSafeAsync( flsSessionId, session.FlsRespondentId, session.FlsInstanceId, AuditAction.DraftSaved, ipAddress: null, userAgent: null, session.FlsRespondentId, login, ct).ConfigureAwait(false); return SuccessResponse.UpdateSuccess; } catch (Exception ex) { GB5Trace.MarkFailed("save-fls-draft-failed", ex); _logger.LogError(ex, "SaveDraft failed for session {Id}", flsSessionId); throw; } } public async Task SubmitAsync(int flsSessionId, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("submit-fls-session", new { flsSessionId }); var sessionRow = await _sessionDal.GetByIdAsync(flsSessionId, login, ct).ConfigureAwait(false); if (sessionRow is null) return "Session not found."; if (sessionRow.SessionStatus == 1) // 1 = Submitted (CK_TFLSRESPONSESESSION_SESS: 0=InProgress,1=Submitted,2=Abandoned) return "Already submitted."; await _sessionDal.SubmitAsync(flsSessionId, sessionRow.FlsRespondentId, sessionRow.GroupId, login, ct).ConfigureAwait(false); await LogAuditSafeAsync( flsSessionId, sessionRow.FlsRespondentId, sessionRow.FlsInstanceId, AuditAction.Submitted, ipAddress: null, userAgent: null, sessionRow.FlsRespondentId, login, ct).ConfigureAwait(false); await _eventBll.PublishAsync( sessionRow.FlsInstanceId, sessionRow.FlsRespondentId, sessionRow.GroupId, sessionRow.StepNo, FlsEventType.FormSubmitted, templateDataId: null, sourceReference: null, login, ct).ConfigureAwait(false); return SuccessResponse.UpdateSuccess; } catch (Exception ex) { GB5Trace.MarkFailed("submit-fls-session-failed", ex); _logger.LogError(ex, "Submit failed for session {Id}", flsSessionId); throw; } } public async Task MarkExternalSubmitAsync( int flsRespondentId, string sourceReference, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("mark-fls-external", new { flsRespondentId, sourceReference }); var respondent = await _respondentDal.GetByIdAsync(flsRespondentId, login, ct).ConfigureAwait(false); if (respondent is null) return "Respondent not found."; await _sessionDal.MarkExternalAsync(flsRespondentId, sourceReference, login, ct).ConfigureAwait(false); await LogAuditSafeAsync( flsSessionId: 0, flsRespondentId, respondent.FlsInstanceId, AuditAction.ManualMark, ipAddress: null, userAgent: null, flsRespondentId, login, ct).ConfigureAwait(false); await _eventBll.PublishAsync( respondent.FlsInstanceId, flsRespondentId, respondent.GroupId, respondent.StepNo, FlsEventType.FormSubmitted, templateDataId: null, sourceReference, login, ct).ConfigureAwait(false); return SuccessResponse.UpdateSuccess; } catch (Exception ex) { GB5Trace.MarkFailed("mark-fls-external-failed", ex); _logger.LogError(ex, "MarkExternalSubmit failed for respondent {Id}", flsRespondentId); throw; } } public async Task GetSessionAsync(int flsSessionId, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("get-fls-session", new { flsSessionId }); var sessionRow = await _sessionDal.GetByIdAsync(flsSessionId, login, ct).ConfigureAwait(false); if (sessionRow is null) return "Session not found."; return JsonSerializer.Serialize(sessionRow); } catch (Exception ex) { GB5Trace.MarkFailed("get-fls-session-failed", ex); _logger.LogError(ex, "GetSession failed for session {Id}", flsSessionId); throw; } } public async Task> GetResponseAuditTrailAsync( int flsRespondentId, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("get-response-audit-trail", new { flsRespondentId }); return await _sessionDal.GetAuditTrailByRespondentAsync(flsRespondentId, login, ct).ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("get-response-audit-trail-failed", ex); _logger.LogError(ex, "GetResponseAuditTrail failed for respondent {Id}", flsRespondentId); throw; } } // Cross-tenant session lookup via system login (token-based flows where tenant is unknown) private async Task GetSessionRowCrosstenantAsync(int flsSessionId, CancellationToken ct) { var sysLogin = new LoginDTO { ClientId = 0, UserId = -1 }; return await _sessionDal.GetByIdAsync(flsSessionId, sysLogin, ct).ConfigureAwait(false); } private static FlsSessionContextDto ErrorContext(string errorCode) => new(0, 0, 0, -1, -1, -1, 0, 0, false, 0, errorCode); // AUDITACTION: 0=Opened, 1=DraftSaved, 2=Submitted, 3=Expired, 4=ManualMark, 5=OptedOut private static class AuditAction { public const int Opened = 0; public const int DraftSaved = 1; public const int Submitted = 2; public const int Expired = 3; public const int ManualMark = 4; public const int OptedOut = 5; } // Audit logging must never break the primary flow — swallow and log any failure. private async Task LogAuditSafeAsync( int flsSessionId, int flsRespondentId, int flsInstanceId, int auditAction, string? ipAddress, string? userAgent, int performedBy, LoginDTO login, CancellationToken ct) { try { await _sessionDal.LogResponseAuditAsync( flsSessionId, flsRespondentId, flsInstanceId, auditAction, ipAddress, userAgent, performedBy, login, ct).ConfigureAwait(false); } catch (Exception ex) { _logger.LogError(ex, "LogResponseAudit failed for session {SessionId}, action {Action}", flsSessionId, auditAction); } } }