using EAIAdminDAL.CustomCode; using EAIAdminDAL.DTO; using GB5Shared.DTO.Framework.Login; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace EAIAdminBLL.EAIContextResolver { public class EAIContextResolver : IEAIContextResolver { private const byte COMPOSITION_MODE_OVERRIDE = 0; private const byte COMPOSITION_MODE_ADDITIVE = 1; private const byte SCOPE_GLOBAL = 0; private const byte SCOPE_CLIENT_ATTRIBUTE = 1; private const byte SCOPE_CLIENT = 2; private const byte SCOPE_ROLE = 3; private const byte SCOPE_OU = 4; private const byte SCOPE_USER = 5; private readonly IEAIContextSettingDAL _dal; private readonly ILogger _logger; public EAIContextResolver(IEAIContextSettingDAL dal, ILogger logger) { _dal = dal ?? throw new ArgumentNullException(nameof(dal)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public async Task> ResolveContextAsync( int tenantId, int userId, LoginDTO login, CancellationToken ct = default) { try { // Called from POST /EAIAdmin/ResolveContext, which the M2M API-key middleware // fronts with a placeholder LoginDTO{ClientId=-1} (no browser session to derive a // real tenant from). Rebuild a real, DB-routable login from the request's own // TenantId — same idiom as KmDiscoveryBLL.EffectiveLogin. var effectiveLogin = login.ClientId > 0 ? login : new LoginDTO { ClientId = tenantId, UserId = 0 }; var roleId = -1; var workOUId = -1; var userRoleOu = await _dal.GetUserRoleAndOuAsync(userId, tenantId, effectiveLogin, ct) .ConfigureAwait(false); if (userRoleOu is not null) { roleId = userRoleOu.RoleId; workOUId = userRoleOu.WorkOUId; } else { _logger.LogWarning( "BLL | ResolveContext | User not found, resolving without Role/OU scope | " + "TenantId={TenantId} UserId={UserId}", tenantId, userId); } var classificationEntries = await _dal.GetClassificationValuesForTenantAsync( tenantId, effectiveLogin, ct).ConfigureAwait(false); var classificationValues = classificationEntries .Select(e => $"{e.DimensionCode}:{e.ValueCode}") .ToList(); var candidates = await _dal.GetCandidateSettingsForResolutionAsync( tenantId, roleId, workOUId, userId, classificationValues, effectiveLogin, ct) .ConfigureAwait(false); var result = new Dictionary(); foreach (var group in candidates.GroupBy(c => c.ContextKey)) { var matches = group.ToList(); var compositionMode = matches[0].CompositionMode; result[group.Key] = compositionMode == COMPOSITION_MODE_ADDITIVE ? ComposeAdditive(matches) : ComposeOverride(matches); } return result; } catch (Exception ex) { _logger.LogError(ex, "BLL | ResolveContext | Error | TenantId={TenantId} UserId={UserId}", tenantId, userId); throw; } } private static string ComposeAdditive(List matches) { var ordered = matches .OrderBy(m => TierWeight(m.ScopeLevel)) .ThenBy(m => m.Priority) .Select(m => m.ContextValue); return string.Join("\n\n", ordered); } private static string ComposeOverride(List matches) { var best = matches .OrderByDescending(m => TierWeight(m.ScopeLevel)) .ThenByDescending(m => m.Priority) .ThenByDescending(m => m.ModifiedOn ?? DateTime.MinValue) .First(); return best.ContextValue; } // Role and OU are deliberately co-equal (tier 3) — Priority breaks ties between them, // not scope level. See IEAIContextResolver's doc comment. private static int TierWeight(byte scopeLevel) => scopeLevel switch { SCOPE_GLOBAL => 0, SCOPE_CLIENT_ATTRIBUTE => 1, SCOPE_CLIENT => 2, SCOPE_ROLE => 3, SCOPE_OU => 3, SCOPE_USER => 4, _ => -1 }; } }