using Marco.Pms.DataAccess.Data; using Marco.Pms.Helpers.Utility; using Marco.Pms.Model.MongoDBModels.Masters; using Marco.Pms.Model.MongoDBModels.Project; using MarcoBMS.Services.Service; using Microsoft.EntityFrameworkCore; namespace Marco.Pms.Services.Helpers { public class GeneralHelper { private readonly IDbContextFactory _dbContextFactory; private readonly ApplicationDbContext _context; // Keeping this for direct scoped context use where appropriate private readonly ILoggingService _logger; private readonly FeatureDetailsHelper _featureDetailsHelper; public GeneralHelper(IDbContextFactory dbContextFactory, ApplicationDbContext context, ILoggingService logger, FeatureDetailsHelper featureDetailsHelper) { _dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory)); _context = context ?? throw new ArgumentNullException(nameof(context)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _featureDetailsHelper = featureDetailsHelper ?? throw new ArgumentNullException(nameof(featureDetailsHelper)); } public async Task> GetProjectInfraFromDB(Guid projectId) { // Each task uses its own DbContext instance for thread safety. Projections are used for efficiency. // Task to fetch Buildings, Floors, and WorkAreas using projections var hierarchyTask = Task.Run(async () => { using var context = _dbContextFactory.CreateDbContext(); var buildings = await context.Buildings.AsNoTracking().Where(b => b.ProjectId == projectId).Select(b => new { b.Id, b.Name, b.Description }).ToListAsync(); var buildingIds = buildings.Select(b => b.Id).ToList(); var floors = await context.Floor.AsNoTracking().Where(f => buildingIds.Contains(f.BuildingId)).Select(f => new { f.Id, f.BuildingId, f.FloorName }).ToListAsync(); var floorIds = floors.Select(f => f.Id).ToList(); var workAreas = await context.WorkAreas.AsNoTracking().Where(wa => floorIds.Contains(wa.FloorId)).Select(wa => new { wa.Id, wa.FloorId, wa.AreaName }).ToListAsync(); return (buildings, floors, workAreas); }); // Task to get work summaries, AGGREGATED ON THE DATABASE SERVER var workSummaryTask = Task.Run(async () => { using var context = _dbContextFactory.CreateDbContext(); // This is the most powerful optimization. It avoids pulling all WorkItem rows. return await context.WorkItems.AsNoTracking() .Where(wi => wi.WorkArea != null && wi.WorkArea.Floor != null && wi.WorkArea.Floor.Building != null && wi.WorkArea.Floor.Building.ProjectId == projectId) .GroupBy(wi => wi.WorkAreaId) // Group by the parent WorkArea .Select(g => new { WorkAreaId = g.Key, PlannedWork = g.Sum(i => i.PlannedWork), CompletedWork = g.Sum(i => i.CompletedWork) }) .ToDictionaryAsync(x => x.WorkAreaId); // Return a ready-to-use dictionary for fast lookups }); await Task.WhenAll(hierarchyTask, workSummaryTask); var (buildings, floors, workAreas) = await hierarchyTask; var workSummariesByWorkAreaId = await workSummaryTask; // --- Step 4: Build the hierarchy efficiently using Lookups --- // Using lookups is much faster (O(1)) than repeated .Where() calls (O(n)). var floorsByBuildingId = floors.ToLookup(f => f.BuildingId); var workAreasByFloorId = workAreas.ToLookup(wa => wa.FloorId); var buildingMongoList = new List(); foreach (var building in buildings) { double buildingPlanned = 0, buildingCompleted = 0; var floorMongoList = new List(); foreach (var floor in floorsByBuildingId[building.Id]) // Fast lookup { double floorPlanned = 0, floorCompleted = 0; var workAreaMongoList = new List(); foreach (var workArea in workAreasByFloorId[floor.Id]) // Fast lookup { // Get the pre-calculated summary from the dictionary. O(1) operation. workSummariesByWorkAreaId.TryGetValue(workArea.Id, out var summary); var waPlanned = summary?.PlannedWork ?? 0; var waCompleted = summary?.CompletedWork ?? 0; workAreaMongoList.Add(new WorkAreaMongoDB { Id = workArea.Id.ToString(), AreaName = workArea.AreaName, PlannedWork = waPlanned, CompletedWork = waCompleted }); floorPlanned += waPlanned; floorCompleted += waCompleted; } floorMongoList.Add(new FloorMongoDB { Id = floor.Id.ToString(), FloorName = floor.FloorName, PlannedWork = floorPlanned, CompletedWork = floorCompleted, WorkAreas = workAreaMongoList }); buildingPlanned += floorPlanned; buildingCompleted += floorCompleted; } buildingMongoList.Add(new BuildingMongoDB { Id = building.Id.ToString(), BuildingName = building.Name, Description = building.Description, PlannedWork = buildingPlanned, CompletedWork = buildingCompleted, Floors = floorMongoList }); } return buildingMongoList; } /// /// Retrieves a list of work items for a specific work area, including a summary of tasks assigned for the current day. /// This method is highly optimized to run database operations in parallel and perform aggregations on the server. /// /// The ID of the work area. /// A list of WorkItemMongoDB objects with calculated daily assignments. public async Task> GetWorkItemsListFromDB(Guid workAreaId) { _logger.LogInfo("Fetching DB work items for WorkAreaId: {WorkAreaId}", workAreaId); try { // --- Step 1: Run independent database queries in PARALLEL --- // We can fetch the WorkItems and the aggregated TaskAllocations at the same time. // Task 1: Fetch the WorkItem entities and their related data. var workItemsTask = _context.WorkItems .Include(wi => wi.ActivityMaster) .Include(wi => wi.WorkCategoryMaster) .Where(wi => wi.WorkAreaId == workAreaId) .AsNoTracking() .ToListAsync(); // Task 2: Fetch and AGGREGATE today's task allocations ON THE DATABASE SERVER. var todaysAssignmentsTask = Task.Run(async () => { // Correctly define "today's" date range to avoid precision issues. var today = DateTime.UtcNow.Date; var tomorrow = today.AddDays(1); using var context = _dbContextFactory.CreateDbContext(); // Use a factory for thread safety // This is the most powerful optimization: // 1. It filters by WorkAreaId directly, making it independent of the first query. // 2. It filters by a correct date range. // 3. It groups and sums on the DB server, returning only a small summary. return await context.TaskAllocations .Where(t => t.WorkItem != null && t.WorkItem.WorkAreaId == workAreaId && t.AssignmentDate >= today && t.AssignmentDate < tomorrow) .GroupBy(t => t.WorkItemId) .Select(g => new { WorkItemId = g.Key, TodaysAssigned = g.Sum(x => x.PlannedTask) }) // Return a dictionary for instant O(1) lookups later. .ToDictionaryAsync(x => x.WorkItemId, x => x.TodaysAssigned); }); // Await both parallel database operations to complete. await Task.WhenAll(workItemsTask, todaysAssignmentsTask); // Retrieve the results from the completed tasks. var workItemsFromDb = await workItemsTask; var todaysAssignments = await todaysAssignmentsTask; // --- Step 2: Map to the ViewModel/MongoDB model efficiently --- var workItemVMs = workItemsFromDb.Select(wi => new WorkItemMongoDB { Id = wi.Id.ToString(), WorkAreaId = wi.WorkAreaId.ToString(), ParentTaskId = wi.ParentTaskId.ToString(), ActivityMaster = wi.ActivityMaster != null ? new ActivityMasterMongoDB { Id = wi.ActivityMaster.Id.ToString(), ActivityName = wi.ActivityMaster.ActivityName, UnitOfMeasurement = wi.ActivityMaster.UnitOfMeasurement } : null, WorkCategoryMaster = wi.WorkCategoryMaster != null ? new WorkCategoryMasterMongoDB { Id = wi.WorkCategoryMaster.Id.ToString(), Name = wi.WorkCategoryMaster.Name, Description = wi.WorkCategoryMaster.Description } : null, PlannedWork = wi.PlannedWork, CompletedWork = wi.CompletedWork, Description = wi.Description, TaskDate = wi.TaskDate, // Use the fast dictionary lookup instead of the slow in-memory Where/Sum. TodaysAssigned = todaysAssignments.GetValueOrDefault(wi.Id, 0) }).ToList(); _logger.LogInfo("Successfully processed {WorkItemCount} work items for WorkAreaId: {WorkAreaId}", workItemVMs.Count, workAreaId); return workItemVMs; } catch (Exception ex) { _logger.LogError(ex, "An error occurred while fetching DB work items for WorkAreaId: {WorkAreaId}", workAreaId); // Return an empty list or re-throw, depending on your application's error handling strategy. return new List(); } } /// /// Retrieves all enabled feature IDs for a given tenant based on their active subscription. /// /// The unique identifier of the tenant. /// A list of feature IDs available for the tenant. public async Task> GetFeatureIdsByTenentIdAsync(Guid tenantId) { try { _logger.LogInfo("Fetching feature IDs for tenant: {TenantId}", tenantId); // Step 1: Get active tenant subscription with plan var tenantSubscription = await _context.TenantSubscriptions .Include(ts => ts.Plan) .AsNoTracking() // Optimization: Read-only query, no need to track .FirstOrDefaultAsync(ts => ts.TenantId == tenantId && ts.Plan != null && !ts.IsCancelled && ts.EndDate.Date >= DateTime.UtcNow.Date); // FIX: Subscription should not be expired if (tenantSubscription == null) { _logger.LogWarning("No active subscription found for tenant: {TenantId}", tenantId); return new List(); } _logger.LogDebug("Active subscription found for tenant: {TenantId}, PlanId: {PlanId}", tenantId, tenantSubscription.Plan!.Id); var featureIds = new List { new Guid("2f3509b7-160d-410a-b9b6-daadd96c986d"), new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be") }; // Step 2: Get feature details from Plan var featureDetails = await _featureDetailsHelper.GetFeatureDetails(tenantSubscription.Plan!.FeaturesId); if (featureDetails == null) { _logger.LogWarning("No feature details found for tenant: {TenantId}, PlanId: {PlanId}", tenantId, tenantSubscription.Plan!.Id); return new List(); } // Step 3: Collect all enabled feature IDs from modules if (featureDetails.Modules?.Attendance?.Enabled == true) { featureIds.AddRange(featureDetails.Modules.Attendance.FeatureId); _logger.LogDebug("Added Attendance module features for tenant: {TenantId}", tenantId); } if (featureDetails.Modules?.ProjectManagement?.Enabled == true) { featureIds.AddRange(featureDetails.Modules.ProjectManagement.FeatureId); _logger.LogDebug("Added Project Management module features for tenant: {TenantId}", tenantId); } if (featureDetails.Modules?.Directory?.Enabled == true) { featureIds.AddRange(featureDetails.Modules.Directory.FeatureId); _logger.LogDebug("Added Directory module features for tenant: {TenantId}", tenantId); } if (featureDetails.Modules?.Expense?.Enabled == true) { featureIds.AddRange(featureDetails.Modules.Expense.FeatureId); _logger.LogDebug("Added Expense module features for tenant: {TenantId}", tenantId); } _logger.LogInfo("Returning {Count} feature IDs for tenant: {TenantId}", featureIds.Count, tenantId); return featureIds.Distinct().ToList(); } catch (Exception ex) { // Step 4: Handle unexpected errors _logger.LogError(ex, "Error retrieving feature IDs for tenant: {TenantId}", tenantId); return new List(); } } } }