using System.Text.Json; using CMGMBLL.Interfaces; using CMGMDAL.DTOs; using CMGMDAL.Interfaces; using Dapr.Client; using GB5Shared.DaprCache; using GB5Shared.DTO.Framework.Login; using GB5Shared.EventLogPublish; using GB5Shared.Resource.Response; using GB5Shared.Telemetry; using static GB5Shared.GB5Constant.Constant; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using PERMDAL.Interfaces; using SkillManagementDAL.CustomCode.EmployeeSkillProfile; using SkillManagementDAL.DTO.EmployeeSkillProfile; namespace CMGMBLL.Implementations; public class CompetencyProfileBLL : ICompetencyProfileBLL { private readonly ICompetencyProfileDAL _profileDal; private readonly ICompetencyKRAMapDAL _kraMapDal; private readonly ICompetencyAnchorDAL _anchorDal; private readonly ICareerPathDAL _careerPathDal; private readonly ICompetencyFrameworkDAL _frameworkDal; private readonly IAppraisalEvalDAL _appraisalEvalDal; private readonly IEmployeeSkillProfileDAL _skillProfileDal; private readonly KeyInvalidate _keyInvalidate; private readonly EventLogPublish _eventLog; private readonly DaprClient _daprClient; private readonly ILogger _logger; public CompetencyProfileBLL( ICompetencyProfileDAL profileDal, ICompetencyKRAMapDAL kraMapDal, ICompetencyAnchorDAL anchorDal, ICareerPathDAL careerPathDal, ICompetencyFrameworkDAL frameworkDal, IAppraisalEvalDAL appraisalEvalDal, IEmployeeSkillProfileDAL skillProfileDal, KeyInvalidate keyInvalidate, EventLogPublish eventLog, DaprClient daprClient, ILogger logger) { _profileDal = profileDal; _kraMapDal = kraMapDal; _anchorDal = anchorDal; _careerPathDal = careerPathDal; _frameworkDal = frameworkDal; _appraisalEvalDal = appraisalEvalDal; _skillProfileDal = skillProfileDal; _keyInvalidate = keyInvalidate; _eventLog = eventLog; _daprClient = daprClient; _logger = logger; } public async Task GetByEmployeeAsync(int employeeId, LoginDTO login, CancellationToken ct) { try { var list = await _profileDal.GetByEmployeeAsync(employeeId, login, ct).ConfigureAwait(false); return System.Text.Json.JsonSerializer.Serialize(list); } catch (Exception ex) { GB5Trace.MarkFailed("get-profile-failed", ex); _logger.LogError(ex, "GetCompetencyProfile failed for EmployeeId {EmployeeId}", employeeId); throw; } } public async Task GenerateCompetencyProfileAsync(int employeeId, int frameworkId, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("validate-generate-profile", new { employeeId, frameworkId }); if (employeeId <= 0) return "Employee is required."; if (frameworkId <= 0) return "Framework is required."; // Load employee skill levels var skillJson = await _skillProfileDal.GetByEmployee(employeeId, -1, -1, login, ct).ConfigureAwait(false); var skillProfiles = JsonConvert.DeserializeObject>(skillJson) ?? Enumerable.Empty(); var currentSkillLevels = skillProfiles .Where(s => s.ProfileStatus == 1) .ToDictionary(s => s.SkillId, s => (int)s.CurrentLevNo); // Load KRA-skill mappings for the framework var mappings = (await _kraMapDal.GetByFrameworkAsync(frameworkId, login, ct).ConfigureAwait(false)).ToList(); if (mappings.Count == 0) return "No KRA-skill mappings found for this framework. Configure KRA mappings first."; // Load latest KRA scores from PERM var kraScores = (await _appraisalEvalDal.GetLatestKRAScoresByEmployeeAsync(employeeId, login, ct).ConfigureAwait(false)) .ToDictionary(k => k.KRAId, k => k.KRAScore); // Compute competency score and identify gaps GB5Trace.Step("compute-profile", new { employeeId, mappingCount = mappings.Count }); var gaps = new List(); int metCount = 0; foreach (var map in mappings) { currentSkillLevels.TryGetValue(map.SkillId, out int currentLevel); kraScores.TryGetValue(map.KRAId, out decimal kraScore); bool skillMet = currentLevel >= map.ExpectedProficiencyLevel || (kraScore >= 60 && currentLevel >= map.ExpectedProficiencyLevel - 1); if (skillMet) { metCount++; } else { gaps.Add(new SkillGapDTO { SkillId = map.SkillId, SkillName = map.SkillName, CurrentLevel = (byte)currentLevel, RequiredLevel = map.ExpectedProficiencyLevel, GapSize = map.ExpectedProficiencyLevel - currentLevel, IsMandatory = map.IsMandatory, KRAId = map.KRAId, KRAName = map.KRAName }); } } decimal overallScore = mappings.Count > 0 ? Math.Round((decimal)metCount / mappings.Count * 100, 2) : 0; // Upsert profile GB5Trace.Step("save-profile", new { employeeId, overallScore }); int? existingId = await _profileDal.GetExistingProfileIdAsync(employeeId, frameworkId, login, ct).ConfigureAwait(false); bool isNew = existingId is null; var profile = new CompetencyProfileDTO { CompetencyProfileId = existingId ?? 0, EmployeeId = employeeId, FrameworkId = frameworkId, CareerPathId = -1, TargetRoleId = -1, GeneratedOn = DateTime.UtcNow, OverallCompetencyScore = overallScore, SkillGapsJson = System.Text.Json.JsonSerializer.Serialize(gaps), Status = 1, TenantId = login.ClientId, CreatedBy = login.UserId }; int savedId = await _profileDal.UpsertAsync(profile, isNew, login, ct).ConfigureAwait(false); GB5Trace.Step("event-publish", new { EventTypeId = EventTypeConstant.GENERATECMGMPROFILEEVENTTYPEID }); await _eventLog.PublishEventLogAsync("Competency Profile Generated", new { EmployeeId = employeeId, FrameworkId = frameworkId, Score = overallScore }, EventTypeConstant.GENERATECMGMPROFILEEVENTTYPEID, savedId, login, ct: ct).ConfigureAwait(false); // Publish Dapr skill gap event if gaps exist if (gaps.Count > 0) { GB5Trace.Step("event-publish", new { Topic = "cmgm.skillgap.identified", GapCount = gaps.Count }); try { await _daprClient.PublishEventAsync("pubsub", "cmgm.skillgap.identified", new { EmployeeId = employeeId, FrameworkId = frameworkId, TenantId = login.ClientId, ClientId = login.ClientId, DatabaseName = login.DatabaseName, UserId = login.UserId, Gaps = gaps.Select(g => new { g.KRAId, g.SkillId, GapLevel = g.GapSize }) }, ct).ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("dapr-publish-skillgap-failed", ex); _logger.LogWarning(ex, "Dapr publish cmgm.skillgap.identified failed for Employee {EmployeeId} — non-fatal", employeeId); } } // Invalidate profile cache var cacheKey = new CacheKeyGeneration().KeyGeneration(employeeId, EntityConstant.OBJECTCOMPETENCYPROFILE, CacheKeyLevel.USER_LEVEL, login); await _keyInvalidate.AllInvalidateCache(cacheKey).ConfigureAwait(false); return isNew ? $"{SuccessResponse.SaveSuccessMessage} {savedId}" : SuccessResponse.UpdateSuccess; } catch (Exception ex) { GB5Trace.MarkFailed("generate-profile-failed", ex); _logger.LogError(ex, "GenerateCompetencyProfile failed for EmployeeId {EmployeeId}", employeeId); throw; } } public async Task GetSkillGapAnalysisAsync(int employeeId, int careerPathId, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("get-skillgap-analysis", new { employeeId, careerPathId }); // Load employee skill levels var skillJson = await _skillProfileDal.GetByEmployee(employeeId, -1, -1, login, ct).ConfigureAwait(false); var skillProfiles = JsonConvert.DeserializeObject>(skillJson) ?? Enumerable.Empty(); var currentSkillLevels = skillProfiles .Where(s => s.ProfileStatus == 1) .ToDictionary(s => s.SkillId, s => (int)s.CurrentLevNo); // Load career path requirements var requirements = (await _careerPathDal.GetRequirementsAsync(careerPathId, login, ct).ConfigureAwait(false)).ToList(); // Build gap analysis enriched with behavioral anchors var gaps = new List(); foreach (var req in requirements) { currentSkillLevels.TryGetValue(req.SkillId, out int currentLevel); int gap = req.MinProficiencyLevel - currentLevel; var anchor = gap > 0 ? await _anchorDal.GetBySkillLevelAsync(req.SkillId, (byte)req.MinProficiencyLevel, login, ct).ConfigureAwait(false) : null; gaps.Add(new SkillGapDTO { SkillId = req.SkillId, SkillName = req.SkillName, CurrentLevel = (byte)currentLevel, RequiredLevel = req.MinProficiencyLevel, GapSize = Math.Max(0, gap), IsMandatory = req.IsMandatory, AnchorText = anchor?.AnchorText }); } return System.Text.Json.JsonSerializer.Serialize(gaps.OrderByDescending(g => g.IsMandatory).ThenByDescending(g => g.GapSize)); } catch (Exception ex) { GB5Trace.MarkFailed("get-skillgap-analysis-failed", ex); _logger.LogError(ex, "GetSkillGapAnalysis failed for EmployeeId {EmployeeId} CareerPathId {CareerPathId}", employeeId, careerPathId); throw; } } public async Task GetCareerPathRecommendationsAsync(LoginDTO login, CancellationToken ct) { try { int employeeId = login.UserId; GB5Trace.Step("get-careerpath-recommendations", new { employeeId }); // Load employee skill levels var skillJson = await _skillProfileDal.GetByEmployee(employeeId, -1, -1, login, ct).ConfigureAwait(false); var skillProfiles = JsonConvert.DeserializeObject>(skillJson) ?? Enumerable.Empty(); var currentSkillLevels = skillProfiles .Where(s => s.ProfileStatus == 1) .ToDictionary(s => s.SkillId, s => (int)s.CurrentLevNo); // Load all active career paths (frameworkId = -1 = all frameworks) var allPaths = (await _careerPathDal.GetListAsync(-1, 1, login, ct).ConfigureAwait(false)).ToList(); // Compute readiness for each path var matches = new List(); foreach (var path in allPaths) { var requirements = (await _careerPathDal.GetRequirementsAsync(path.CareerPathId, login, ct).ConfigureAwait(false)).ToList(); if (requirements.Count == 0) continue; int metCount = 0; int mandatoryGaps = 0; foreach (var req in requirements) { currentSkillLevels.TryGetValue(req.SkillId, out int currentLevel); bool met = currentLevel >= req.MinProficiencyLevel; if (met) metCount++; else if (req.IsMandatory) mandatoryGaps++; } matches.Add(new CareerPathMatchDTO { CareerPathId = path.CareerPathId, PathName = path.PathName, TargetRoleId = path.TargetRoleId, TargetRoleName = path.TargetRoleName, ReadinessPercent = Math.Round((decimal)metCount / requirements.Count * 100, 1), MandatoryGapCount = mandatoryGaps, TotalRequirements = requirements.Count, MetRequirements = metCount, TypicalMonthsRequired = path.TypicalMonthsRequired }); } var sorted = matches.OrderByDescending(m => m.ReadinessPercent).ToList(); return System.Text.Json.JsonSerializer.Serialize(sorted); } catch (Exception ex) { GB5Trace.MarkFailed("get-careerpath-recommendations-failed", ex); _logger.LogError(ex, "GetCareerPathRecommendations failed for UserId {UserId}", login.UserId); throw; } } public async Task GenerateAllProfilesAsync(int employeeId, LoginDTO login, CancellationToken ct) { GB5Trace.Step("generate-all-profiles", new { employeeId }); var frameworks = (await _frameworkDal.GetListAsync(1, login, ct).ConfigureAwait(false)).ToList(); if (frameworks.Count == 0) { _logger.LogInformation("GenerateAllProfiles: no active frameworks for TenantId {TenantId}", login.ClientId); return; } foreach (var fw in frameworks) { try { await GenerateCompetencyProfileAsync(employeeId, fw.FrameworkId, login, ct).ConfigureAwait(false); } catch (Exception ex) { // Non-fatal per framework — continue with remaining frameworks GB5Trace.MarkFailed("generate-all-profiles-framework-failed", ex); _logger.LogWarning(ex, "GenerateAllProfiles: framework {FrameworkId} failed for employee {EmployeeId} — skipping", fw.FrameworkId, employeeId); } } } }