using System.Text.Json; using EnablementDAL.DTO; using EnablementBLL.ShowRule; using EnablementDAL.CustomCode.ShowResolver; using EnablementDAL.CustomCode.ShowRequestLog; using GB5Shared.DTO.Framework.Login; using Microsoft.Extensions.Logging; using System.Diagnostics; namespace EnablementBLL.ShowResolver; public class ShowResolverBLL : IShowResolverBLL { private readonly IShowResolverDAL _dal; private readonly IShowRequestLogDAL _logDal; private readonly IShowRuleBLL _ruleEvaluator; private readonly ILogger _logger; public ShowResolverBLL( IShowResolverDAL dal, IShowRequestLogDAL logDal, IShowRuleBLL ruleEvaluator, ILogger logger) { _dal = dal; _logDal = logDal; _ruleEvaluator = ruleEvaluator; _logger = logger; } public async Task ResolveAsync( ResolverRequestDTO request, LoginDTO login, CancellationToken ct) { if (login is null) throw new ArgumentException("A valid Login context (header) is required to resolve shows — the tenant/database cannot be determined without it."); ValidateRequest(request); if (request.Context.User != null) { request.Context.User.UserId = login.UserId; request.Context.User.OuId = login.WorkOUId; } int userId = login.UserId; string triggerType = request.Context.Trigger?.Type ?? string.Empty; if (string.IsNullOrWhiteSpace(triggerType)) triggerType = "ModuleNavigation"; var stopwatch = Stopwatch.StartNew(); _logger.LogInformation("Resolver started RequestId={RequestId} UserId={UserId} TriggerType={TriggerType}", request.RequestId, userId, triggerType); int startType = ResolveStartType(triggerType); var showsTask = _dal.GetCandidateShowsAsync(startType, login, ct); var rulesTask = _dal.GetShowRulesAsync(login, ct); var schedulesTask = _dal.GetShowSchedulesAsync(login, ct); var completedTask = _dal.GetUserCompletedRunShowIdsAsync(userId, login, ct); await Task.WhenAll(showsTask, rulesTask, schedulesTask, completedTask) .ConfigureAwait(false); var shows = (await showsTask).ToList(); var allRules = (await rulesTask).ToList(); var allSchedules = (await schedulesTask).ToList(); var completedIds = (await completedTask).ToHashSet(); var showIds = shows.Select(s => s.ShowId).ToList(); var stepContents = (await _dal.GetStepContentAsync(showIds, login, ct)) .ToDictionary(sc => sc.ShowId, sc => sc.Content); _logger.LogInformation("Loaded Shows={ShowCount} Rules={RuleCount} Schedules={ScheduleCount} Completed={CompletedCount}", shows.Count, allRules.Count, allSchedules.Count, completedIds.Count); bool isFirstLogin = false; if (allSchedules.Any(s => s.ScheduleType == 5)) isFirstLogin = await _dal.IsFirstLoginAsync(userId, login, ct).ConfigureAwait(false); var rulesLookup = allRules.ToLookup(r => r.ShowId); var scheduleLookup = allSchedules.ToLookup(s => s.ShowId); // ========================================================================= // Build user roles from context // ========================================================================= var userRoles = new List(); if (request.Context.User != null) { if (request.Context.User.RoleId > 0) { userRoles.Add(request.Context.User.RoleId.ToString()); _logger.LogInformation("Using RoleId from context: {RoleId}", request.Context.User.RoleId); } else if (!string.IsNullOrEmpty(request.Context.User.Role)) { userRoles.Add(request.Context.User.Role); _logger.LogInformation("Using Role from context: {Role}", request.Context.User.Role); } } if (!userRoles.Any() && login.RoleId > 0) { userRoles.Add(login.RoleId.ToString()); _logger.LogInformation("Using RoleId from login: {RoleId}", login.RoleId); } if (!userRoles.Any()) { userRoles.Add("Admin"); _logger.LogWarning("No role found, using default: Admin"); } _logger.LogInformation("UserRoles: {Roles}", string.Join(", ", userRoles)); var utcNow = DateTime.UtcNow; var actions = new List(); foreach (var show in shows) { var showRules = rulesLookup[show.ShowId]; var showSchedules = scheduleLookup[show.ShowId].ToList(); // ========================================================================= // Track if rules evaluation passed // ========================================================================= bool rulesPassed = true; bool hasRules = showRules.Any(); // ========================================================================= // Evaluate rules if they exist // ========================================================================= if (hasRules) { // --- SCOPE EVALUATION --- bool scopePassed = _ruleEvaluator.Evaluate(showRules, ShowRuleBLL.SCOPE, request.Context, userRoles); _logger.LogDebug("ShowId={ShowId} SCOPE: {Passed}", show.ShowId, scopePassed); if (!scopePassed) { _logger.LogWarning("ShowId={ShowId} FAILED SCOPE", show.ShowId); continue; } // --- AUDIENCE EVALUATION --- bool audiencePassed = _ruleEvaluator.Evaluate(showRules, ShowRuleBLL.AUDIENCE, request.Context, userRoles); _logger.LogDebug("ShowId={ShowId} AUDIENCE: {Passed}", show.ShowId, audiencePassed); if (!audiencePassed) { _logger.LogWarning("ShowId={ShowId} FAILED AUDIENCE", show.ShowId); continue; } // --- TRIGGER EVALUATION --- bool triggerPassed = _ruleEvaluator.Evaluate(showRules, ShowRuleBLL.TRIGGER, request.Context, userRoles); _logger.LogDebug("ShowId={ShowId} TRIGGER: {Passed}", show.ShowId, triggerPassed); if (!triggerPassed) { _logger.LogWarning("ShowId={ShowId} FAILED TRIGGER", show.ShowId); continue; } rulesPassed = true; _logger.LogInformation("✅ ShowId={ShowId} PASSED ALL RULES", show.ShowId); } else { rulesPassed = true; _logger.LogInformation("ShowId={ShowId} ({Name}) - No rules, treating as PASS", show.ShowId, show.ShowName); } // ========================================================================= // SCHEDULE CHECK - DYNAMIC & FUTURE-PROOF // ========================================================================= if (showSchedules.Any()) { bool schedulePassed = false; foreach (var schedule in showSchedules) { if (IsWithinWindow(schedule, utcNow, triggerType, isFirstLogin)) { schedulePassed = true; break; } } if (!schedulePassed) { _logger.LogWarning("ShowId={ShowId} FAILED SCHEDULE", show.ShowId); continue; } _logger.LogInformation("ShowId={ShowId} - Schedule passed", show.ShowId); } else { // ============================================================= // DYNAMIC LOGIC: Works for ANY ShowConditions value // ============================================================= bool isAllowedWithoutSchedule = false; string reason = string.Empty; // Case 1: All Pages - Always allowed if (show.ConditionType == 0) { isAllowedWithoutSchedule = true; reason = "ConditionType=0 (All Pages)"; } // Case 2: Selected Pages else if (show.ConditionType == 1) { // Check if show has any targeting mechanism bool hasTargeting = false; List targetingReasons = new List(); // 2a: Check if rules exist and passed if (hasRules && rulesPassed) { hasTargeting = true; targetingReasons.Add($"Rules passed"); } // 2b: Check if show has MODULE condition if (show.ModuleId > 0 && show.ModuleId != -1) { hasTargeting = true; targetingReasons.Add($"ModuleId: {show.ModuleId}"); } // 2c: Check if show has CONTENTCATEGORYID condition if (show.ContentCategoryId > 0 && show.ContentCategoryId != -1) { hasTargeting = true; targetingReasons.Add($"ContentCategoryId: {show.ContentCategoryId}"); } // 2d: Check SHOWCONDITIONS value (any non-NONE value) if (!string.IsNullOrEmpty(show.ShowConditions) && show.ShowConditions.Trim().ToUpper() != "NONE") { hasTargeting = true; targetingReasons.Add($"ShowConditions: {show.ShowConditions}"); } if (hasTargeting) { isAllowedWithoutSchedule = true; reason = $"ConditionType=1 with targeting: {string.Join(", ", targetingReasons)}"; } else { _logger.LogWarning("ShowId={ShowId} - ConditionType=1, NO targeting, NO SCHEDULE, skipping", show.ShowId); continue; } } else { _logger.LogWarning("ShowId={ShowId} - Unknown ConditionType={ConditionType}, skipping", show.ShowId, show.ConditionType); continue; } if (isAllowedWithoutSchedule) { _logger.LogInformation("ShowId={ShowId} - No schedule, {Reason}, ALLOWING", show.ShowId, reason); } } // ========================================================================= // OneTime check // ========================================================================= if (show.ShowType == 0 && completedIds.Contains(show.ShowId)) { _logger.LogWarning("ShowId={ShowId} FAILED ONETIME - already completed", show.ShowId); continue; } // ========================================================================= // ✅ SHOW PASSED ALL CHECKS // ========================================================================= _logger.LogInformation("✅ ShowId={ShowId} ({Name}) PASSED ALL CHECKS!", show.ShowId, show.ShowName); actions.Add(new ResolverActionDTO { ShowId = show.ShowId, ShowName = show.ShowName, DisplayName = show.DisplayName, Description = show.Description, StepContent = stepContents.GetValueOrDefault(show.ShowId) ?? string.Empty, ShowNature = show.ShowNature, Priority = show.Priority, Blocking = show.ShowType == 0, StartType = show.StartType, Theme = show.Theme, DisplayHint = new DisplayHintDTO { Position = show.DisplayPosition, Duration = show.DisplayDuration, Intent = MapIntent(show.ShowNature) } }); } actions = actions.OrderByDescending(a => a.Priority).ToList(); stopwatch.Stop(); _logger.LogInformation("Resolver completed RequestId={RequestId} Matched={Count} ElapsedMs={Ms}", request.RequestId, actions.Count, stopwatch.ElapsedMilliseconds); // ========================================================================= // Audit log // ========================================================================= var logDto = new ShowRequestLogDTO { RequestId = request.RequestId, UserId = userId, TenantId = login.ClientId, TriggerType = triggerType, TriggerSource = request.Context.Trigger?.Source ?? string.Empty, ContextSnapshot = JsonSerializer.Serialize(request.Context), MatchedCount = (short)actions.Count, ResolvedMs = (int)stopwatch.ElapsedMilliseconds, IsFrontendResolved = 0, CreatedById = userId }; _ = _logDal.InsertAsync(logDto, login, CancellationToken.None) .ContinueWith(t => { if (t.Exception != null) _logger.LogWarning("Failed to insert audit log: {Message}", t.Exception.GetBaseException().Message); }, TaskScheduler.Default); return new ResolverResponseDTO { RequestId = request.RequestId, ServerTimestamp = DateTime.UtcNow, ResolvedMs = stopwatch.ElapsedMilliseconds, Actions = actions }; } // ========================================================================= // "System A" — pull/context resolver // ========================================================================= public async Task ResolveMatchesAsync( EnablementContextDTO context, LoginDTO login, CancellationToken ct) { if (login is null) throw new ArgumentException("A valid Login context (header) is required to resolve show matches — the tenant/database cannot be determined without it."); if (context is null) throw new ArgumentException("Context must not be null."); string requestId = Guid.NewGuid().ToString(); var stopwatch = Stopwatch.StartNew(); _logger.LogInformation("Match resolver started RequestId={RequestId} UserId={UserId} Module={Module}", requestId, login.UserId, context.Ui?.Module); var showsTask = _dal.GetCandidateShowsForMatchAsync(login, ct); var rulesTask = _dal.GetShowRulesAsync(login, ct); await Task.WhenAll(showsTask, rulesTask).ConfigureAwait(false); var shows = (await showsTask).ToList(); var allRules = (await rulesTask).ToList(); _logger.LogInformation("Loaded Shows={ShowCount} Rules={RuleCount}", shows.Count, allRules.Count); var rulesLookup = allRules.ToLookup(r => r.ShowId); var resolverContext = MapToResolverContext(context, login); var userRoles = new List(); if (login.RoleId > 0) userRoles.Add(login.RoleId.ToString()); if (!userRoles.Any()) userRoles.Add("Admin"); var matches = new List(); foreach (var show in shows) { var showRules = rulesLookup[show.ShowId].ToList(); if (showRules.Any()) { bool scopePassed = _ruleEvaluator.Evaluate(showRules, ShowRuleBLL.SCOPE, resolverContext, userRoles); if (!scopePassed) continue; bool audiencePassed = _ruleEvaluator.Evaluate(showRules, ShowRuleBLL.AUDIENCE, resolverContext, userRoles); if (!audiencePassed) continue; } var (matchedRules, totalRules) = _ruleEvaluator.EvaluateDetailed(showRules, resolverContext, userRoles); double score = totalRules == 0 ? 1.0 : (double)matchedRules / totalRules; matches.Add(new ResolverMatchResultDTO { ShowId = show.ShowId, ShowName = show.ShowName, ShowNature = show.ShowNature, MatchedRules = matchedRules, TotalRules = totalRules, Score = score, DeploymentMode = show.DeploymentMode, ExternalRef = show.ExternalRef }); } matches = matches.OrderByDescending(m => m.Score).ThenByDescending(m => m.MatchedRules).ToList(); stopwatch.Stop(); _logger.LogInformation("Match resolver completed RequestId={RequestId} Matched={Count} ElapsedMs={Ms}", requestId, matches.Count, stopwatch.ElapsedMilliseconds); var logDto = new ShowRequestLogDTO { RequestId = requestId, UserId = login.UserId, TenantId = login.ClientId, TriggerType = context.Trigger?.Type ?? string.Empty, TriggerSource = context.Trigger?.Source ?? string.Empty, ContextSnapshot = JsonSerializer.Serialize(context), MatchedCount = (short)matches.Count, ResolvedMs = (int)stopwatch.ElapsedMilliseconds, IsFrontendResolved = 0, CreatedById = login.UserId }; _ = _logDal.InsertAsync(logDto, login, CancellationToken.None) .ContinueWith(t => { if (t.Exception != null) _logger.LogWarning("Failed to insert match-resolver audit log: {Message}", t.Exception.GetBaseException().Message); }, TaskScheduler.Default); return new ResolverMatchResponseDTO { RequestId = requestId, ServerTimestamp = DateTime.UtcNow, ResolvedMs = stopwatch.ElapsedMilliseconds, Matches = matches }; } private static ResolverContextDTO MapToResolverContext(EnablementContextDTO context, LoginDTO login) { return new ResolverContextDTO { Database = new ResolverDatabaseContextDTO { DbName = context.Database?.DbName ?? string.Empty }, User = new ResolverUserContextDTO { UserId = login.UserId, UserCode = context.User?.UserCode ?? string.Empty, UserName = context.User?.UserName ?? string.Empty, Role = string.Empty, RoleId = login.RoleId, OuId = login.WorkOUId, OuName = context.User?.OuName ?? string.Empty, WorkRegion = context.User?.WorkRegion ?? string.Empty }, Ui = new ResolverUiContextDTO { Product = context.Ui?.Product ?? string.Empty, Module = context.Ui?.Module ?? string.Empty, Menu = context.Ui?.Menu ?? string.Empty, MenuId = context.Ui?.MenuId ?? -1, Page = context.Ui?.Page ?? string.Empty, Screen = context.Ui?.Screen ?? string.Empty, Action = string.Empty }, Entity = new ResolverEntityContextDTO { EntityType = context.Entity?.EntityType ?? string.Empty, EntityId = context.Entity?.EntityId ?? -1, BizTransactionClassId = context.Entity?.BizTransactionClassId ?? -1, SelectedId = context.Entity?.SelectedId ?? -1 }, Temporal = new ResolverTemporalContextDTO { BusinessDate = context.Temporal?.BusinessDate ?? string.Empty, WorkPeriodId = context.Temporal?.WorkPeriodId ?? -1, WorkPeriodFromDate = context.Temporal?.WorkPeriodFromDate ?? default, WorkPeriodToDate = context.Temporal?.WorkPeriodToDate ?? default }, Trigger = new ResolverTriggerContextDTO { Type = context.Trigger?.Type ?? string.Empty, Source = context.Trigger?.Source ?? string.Empty } }; } private static bool IsWithinWindow(ShowScheduleDTO schedule, DateTime utcNow, string triggerType, bool isFirstLogin) { try { bool isLogin = !string.IsNullOrEmpty(triggerType) && triggerType.Trim().Equals("Login", StringComparison.OrdinalIgnoreCase); if (schedule.ScheduleType == 4) return isLogin; if (schedule.ScheduleType == 5) return isLogin && isFirstLogin; if (schedule.StartDate.HasValue && utcNow.Date < schedule.StartDate.Value.Date) return false; if (schedule.EndDate.HasValue && utcNow.Date > schedule.EndDate.Value.Date) return false; if (schedule.ScheduleType == 0) return schedule.StartDate.HasValue && utcNow.Date == schedule.StartDate.Value.Date; if (schedule.ScheduleType == 1) return true; if (schedule.ScheduleType == 2) { if (string.IsNullOrWhiteSpace(schedule.RecurrenceJson)) return false; try { using var doc = JsonDocument.Parse(schedule.RecurrenceJson); if (doc.RootElement.TryGetProperty("daysOfWeek", out var daysElem)) { var allowedDays = daysElem.EnumerateArray().Select(d => d.GetInt32()).ToHashSet(); return allowedDays.Contains((int)utcNow.DayOfWeek); } } catch { return false; } } if (schedule.ScheduleType == 3) { if (string.IsNullOrWhiteSpace(schedule.RecurrenceJson)) return false; try { using var doc = JsonDocument.Parse(schedule.RecurrenceJson); if (doc.RootElement.TryGetProperty("dayOfMonth", out var dayElem)) { return utcNow.Day == dayElem.GetInt32(); } } catch { return false; } } return false; } catch { return false; } } private static int ResolveStartType(string triggerType) { if (string.IsNullOrEmpty(triggerType)) return 0; var normalized = triggerType.Trim().ToLowerInvariant(); return (normalized == "manual" || normalized == "useraction") ? 1 : 0; } private static void ValidateRequest(ResolverRequestDTO request) { if (string.IsNullOrWhiteSpace(request.RequestId)) throw new ArgumentException("RequestId must not be empty."); if (request.Context is null) throw new ArgumentException("Context must not be null."); } private static string MapIntent(byte showNature) => showNature switch { 0 => "Guide", 1 => "Guide", 2 => "Inform", 3 => "Track", 4 => "Announce", 5 => "Teach", 13 => "Guide", _ => "Inform" }; }