using Fitness_core_BE.Models; using Fitness_core_BE.Helpers; using Fitness_core_BE.Infra.UoW; using Fitness_core_BE.Interfaces; using Fitness_core_BE.Interfaces.Services; using Fitness_core_BE.Exceptions; namespace Fitness_core_BE.Services.Workout { public class WorkoutService : IWorkoutService { private readonly IWorkoutRepository _repository; private readonly UserRoleHelper _userRoleHelper; private readonly IUnitOfWork _uow; private readonly IActivityLogService _activityLogService; private readonly IPushNotificationService _pushNotificationService; public WorkoutService(IWorkoutRepository repository, UserRoleHelper userRoleHelper, IUnitOfWork uow, IActivityLogService activityLogService, IPushNotificationService pushNotificationService) { _repository = repository; _userRoleHelper = userRoleHelper; _uow = uow; _activityLogService = activityLogService; _pushNotificationService = pushNotificationService; } private static string? GetOptionalJsonText(System.Text.Json.JsonElement source, params string[] propertyNames) { foreach (var propertyName in propertyNames) { if (!source.TryGetProperty(propertyName, out var value) || value.ValueKind is System.Text.Json.JsonValueKind.Null or System.Text.Json.JsonValueKind.Undefined) continue; return value.ValueKind == System.Text.Json.JsonValueKind.String ? value.GetString() : value.GetRawText(); } return null; } private static string? GetOptionalJsonText( Dictionary source, params string[] propertyNames) { foreach (var propertyName in propertyNames) { if (!source.TryGetValue(propertyName, out var value) || value.ValueKind is System.Text.Json.JsonValueKind.Null or System.Text.Json.JsonValueKind.Undefined) continue; return value.ValueKind == System.Text.Json.JsonValueKind.String ? value.GetString() : value.GetRawText(); } return null; } // Parses a day's "exercises" array (structured objects with name/muscleGroup/sets/reps/ // description/breathIn/breathOut/videoUrl) into real WorkoutExercise rows, mirroring // AdminSuggestionService's persistence for admin-authored suggestions. Legacy plain-string // exercise entries (no "name" property) are skipped here but still preserved in the // WorkoutItem.Exercises JSON blob for backward compatibility. private static List ParseWorkoutExercisesArray(System.Text.Json.JsonElement exercisesElement) { var result = new List(); if (exercisesElement.ValueKind != System.Text.Json.JsonValueKind.Array) return result; int order = 1; foreach (var ex in exercisesElement.EnumerateArray()) { if (ex.ValueKind != System.Text.Json.JsonValueKind.Object) continue; if (!ex.TryGetProperty("name", out var nameEl) || nameEl.ValueKind != System.Text.Json.JsonValueKind.String) continue; var name = nameEl.GetString(); if (string.IsNullOrWhiteSpace(name)) continue; int? sets = ex.TryGetProperty("sets", out var s) && s.ValueKind == System.Text.Json.JsonValueKind.Number ? s.GetInt32() : (int?)null; result.Add(new WorkoutExercise { WorkoutExerciseId = Guid.NewGuid(), Name = name.Trim(), MuscleGroup = GetOptionalJsonText(ex, "muscleGroup"), Sets = sets, Reps = GetOptionalJsonText(ex, "reps"), Description = GetOptionalJsonText(ex, "description"), BreathIn = GetOptionalJsonText(ex, "breathIn"), BreathOut = GetOptionalJsonText(ex, "breathOut"), VideoUrl = GetOptionalJsonText(ex, "videoUrl", "videoURL", "doURL"), SortOrder = order++, IsActive = true, CreateDate = DateTime.UtcNow }); } return result; } private static List ParseWorkoutExercises(System.Text.Json.JsonElement itemElement) => itemElement.TryGetProperty("exercises", out var exercisesElement) ? ParseWorkoutExercisesArray(exercisesElement) : new List(); private static List ParseWorkoutExercises(Dictionary source) => source.TryGetValue("exercises", out var exercisesElement) ? ParseWorkoutExercisesArray(exercisesElement) : new List(); // Workout Category operations public async Task GetWorkoutCategoryList(object payload, HttpContext httpContext) { try { var categories = await _repository.GetWorkoutCategoryListAsync(); return categories; } catch { throw; } } public async Task CreateWorkoutCategory(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); var workoutCategory = new WorkoutCategory { WorkoutCategoryName = data["name"].GetString()!, Description = data.ContainsKey("description") ? data["description"].GetString() : null, CreateDate = DateTime.UtcNow }; await _repository.CreateWorkoutCategoryAsync(workoutCategory); await _uow.CommitAsync(); // Log activity - non-blocking, failures won't crash the operation try { await _activityLogService.LogActivity(new { description = $"Workout category created with ID {workoutCategory.WorkoutCategoryId}" }, httpContext); } catch (Exception logEx) { Console.WriteLine($"Activity log failed (non-critical): {logEx.Message}"); } return workoutCategory; } catch { await _uow.RollbackAsync(); throw; } } // Workout Plan operations public async Task GetWorkoutPlanList(object payload, HttpContext httpContext) { Guid userId = DataExtractor.GetUserIdFromContext(httpContext); string userType = await _userRoleHelper.GetUserType(userId); Guid clientId; if (userType == "Customer") { Guid cusId = await _userRoleHelper.GetCusId(userId); try { clientId = await _userRoleHelper.GetClientIdByCusId(cusId); } catch (NotFoundException) { // Not every customer has joined a gym yet — they should still see the // platform-wide admin suggestions (FKClientId == null in the repo query). clientId = Guid.Empty; } } else { clientId = await _userRoleHelper.GetClientId(userId); } try { var workoutPlans = await _repository.GetWorkoutPlanListAsync(userType, clientId); return workoutPlans; } catch { throw; } } public async Task GetWorkoutPlanById(object payload, HttpContext httpContext) { try { var data = DeserializePayload.Deserialize(payload); Guid workoutPlanId = Guid.Parse(data["workoutPlanId"].GetString()!); var workoutPlan = await _repository.GetWorkoutPlanByIdAsync(workoutPlanId); if (workoutPlan == null) throw new NotFoundException($"Workout plan with ID {workoutPlanId} not found"); // Transform workout items to have exercises as objects instead of JSON strings var transformedWorkoutItems = workoutPlan.WorkoutItems?.Select(item => { object? exercisesObj = null; if (!string.IsNullOrEmpty(item.Exercises)) { try { exercisesObj = System.Text.Json.JsonSerializer.Deserialize(item.Exercises); } catch { exercisesObj = item.Exercises; // Keep as string if parsing fails } } return new { item.WorkoutItemId, item.ExerciseDay, item.Description, Exercises = exercisesObj, item.WorkoutExercises, item.TotalExercises, item.HowToDo, item.DoURL, item.Duration, item.WormUp, item.Cooldown, item.WorkoutPlanId, item.IsActive, item.CreateDate, item.UpdatedAt }; }).ToList(); return new { workoutPlan.WorkoutPlanId, workoutPlan.Name, workoutPlan.Description, workoutPlan.IsTemplate, workoutPlan.WorkoutCategoryId, workoutPlan.WorkoutCategoryName, workoutPlan.ClientId, workoutPlan.ClientName, workoutPlan.IsActive, workoutPlan.CreateDate, WorkoutItems = transformedWorkoutItems, AssignedCustomers = workoutPlan.AssignedCustomers }; } catch { throw; } } public async Task GetWorkoutPlanByCustomerId(object payload, HttpContext httpContext) { try { Guid customerId; var data = DeserializePayload.Deserialize(payload); if (data.ContainsKey("customerId") && !string.IsNullOrEmpty(data["customerId"].GetString())) { customerId = Guid.Parse(data["customerId"].GetString()!); } else { Guid userId = DataExtractor.GetUserIdFromContext(httpContext); customerId = await _userRoleHelper.GetCusId(userId); } var workoutPlans = await _repository.GetWorkoutPlansByCustomerIdAsync(customerId); var transformedPlans = workoutPlans.Select(plan => new { plan.WorkoutPlanId, plan.Name, plan.Description, plan.IsTemplate, plan.WorkoutCategoryId, plan.WorkoutCategoryName, plan.ClientId, plan.ClientName, plan.IsActive, plan.CreateDate, WorkoutItems = plan.WorkoutItems?.Select(item => { object? exercisesObj = null; if (!string.IsNullOrEmpty(item.Exercises)) { try { exercisesObj = System.Text.Json.JsonSerializer.Deserialize(item.Exercises); } catch { exercisesObj = item.Exercises; } } return new { item.WorkoutItemId, item.ExerciseDay, item.Description, Exercises = exercisesObj, item.HowToDo, item.DoURL, item.Duration, item.WormUp, item.Cooldown, item.WorkoutPlanId, item.IsActive, item.CreateDate, item.UpdatedAt }; }).ToList(), AssignedCustomers = plan.AssignedCustomers }).ToList(); return transformedPlans; } catch { throw; } } public async Task CreateWorkoutPlan(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); Guid userId = DataExtractor.GetUserIdFromContext(httpContext); Guid clientId = await _userRoleHelper.GetClientId(userId); var workoutCategoryId = Guid.Parse(data["workoutCategoryId"].GetString()!); if (!await _repository.WorkoutCategoryExistsAsync(workoutCategoryId)) throw new NotFoundException("Workout category not found."); var workoutPlan = new WorkoutPlan { WorkoutPlanName = data["name"].GetString()!, Description = data.ContainsKey("description") ? data["description"].GetString() : null, IsTemplate = data.ContainsKey("isTemplate") && data["isTemplate"].GetBoolean(), FKWorkoutCategoryId = workoutCategoryId, FKClientId = clientId, CreateDate = DateTime.UtcNow }; await _repository.CreateWorkoutPlanAsync(workoutPlan); if (data.ContainsKey("customerIds") && data["customerIds"].ValueKind == System.Text.Json.JsonValueKind.Array) { var customerIdsArray = data["customerIds"].EnumerateArray(); foreach (var customerIdElement in customerIdsArray) { if (Guid.TryParse(customerIdElement.GetString(), out var customerId)) { var customerWorkout = new CustomerWorkout { FKCustomerId = customerId, FKWorkoutPlanId = workoutPlan.WorkoutPlanId, AssignedDate = DateTime.UtcNow, IsActive = true }; await _repository.AssignWorkoutToCustomerAsync(customerWorkout); } } } else if (data.ContainsKey("customerId") && !string.IsNullOrEmpty(data["customerId"].GetString())) { var customerId = Guid.Parse(data["customerId"].GetString()!); var customerWorkout = new CustomerWorkout { FKCustomerId = customerId, FKWorkoutPlanId = workoutPlan.WorkoutPlanId, AssignedDate = DateTime.UtcNow, IsActive = true }; await _repository.AssignWorkoutToCustomerAsync(customerWorkout); } if (data.ContainsKey("workoutItems") && data["workoutItems"].ValueKind == System.Text.Json.JsonValueKind.Array) { var workoutItemsArray = data["workoutItems"].EnumerateArray(); foreach (var item in workoutItemsArray) { // Handle exercises JSON string? exercisesJson = null; if (item.TryGetProperty("exercises", out var exercisesElement)) { if (exercisesElement.ValueKind == System.Text.Json.JsonValueKind.Array || exercisesElement.ValueKind == System.Text.Json.JsonValueKind.Object) { exercisesJson = exercisesElement.GetRawText(); } else if (exercisesElement.ValueKind == System.Text.Json.JsonValueKind.String) { exercisesJson = exercisesElement.GetString(); } } var workoutItem = new WorkoutItem { ExerciseDay = item.GetProperty("exerciseDay").GetString()!, Description = item.TryGetProperty("description", out var descProp) ? descProp.GetString() : null, Exercises = exercisesJson, HowToDo = GetOptionalJsonText(item, "howToDo"), DoURL = GetOptionalJsonText(item, "doURL", "videoUrl", "videoURL"), FKWorkoutPlanId = workoutPlan.WorkoutPlanId, CreateDate = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow, Duration = GetOptionalJsonText(item, "duration"), WormUp = GetOptionalJsonText(item, "wormUp", "warmUp", "warmup"), Cooldown = GetOptionalJsonText(item, "cooldown", "coolDown"), }; workoutItem.WorkoutExercises = ParseWorkoutExercises(item); await _repository.CreateWorkoutItemAsync(workoutItem); } } await _uow.CommitAsync(); // Get the created plan with items var createdWorkoutPlan = await _repository.GetWorkoutPlanByIdAsync(workoutPlan.WorkoutPlanId); if (createdWorkoutPlan == null) throw new InvalidOperationException("Failed to retrieve created workout plan"); // Transform workout items to have exercises as objects instead of JSON strings var transformedWorkoutItems = createdWorkoutPlan.WorkoutItems?.Select(item => { object? exercisesObj = null; if (!string.IsNullOrEmpty(item.Exercises)) { try { exercisesObj = System.Text.Json.JsonSerializer.Deserialize(item.Exercises); } catch { exercisesObj = item.Exercises; // Keep as string if parsing fails } } return new { item.WorkoutItemId, item.ExerciseDay, item.Description, Exercises = exercisesObj, item.WorkoutExercises, item.TotalExercises, item.HowToDo, item.DoURL, item.Duration, item.WormUp, item.Cooldown, item.WorkoutPlanId, item.IsActive, item.CreateDate, item.UpdatedAt }; }).ToList(); try { await _activityLogService.LogActivity(new { description = $"Workout plan created with ID {workoutPlan.WorkoutPlanId}" }, httpContext); } catch (Exception logEx) { Console.WriteLine($"Activity log failed (non-critical): {logEx.Message}"); } return new { createdWorkoutPlan.WorkoutPlanId, createdWorkoutPlan.Name, createdWorkoutPlan.Description, createdWorkoutPlan.IsTemplate, createdWorkoutPlan.WorkoutCategoryId, createdWorkoutPlan.WorkoutCategoryName, createdWorkoutPlan.ClientId, createdWorkoutPlan.ClientName, createdWorkoutPlan.IsActive, createdWorkoutPlan.CreateDate, WorkoutItems = transformedWorkoutItems, AssignedCustomers = createdWorkoutPlan.AssignedCustomers }; } catch { await _uow.RollbackAsync(); throw; } } // Same rationale as MealService's equivalent: WorkoutCategory has no FKClientId (it's a // global taxonomy), so a single well-known "AI Generated" category is get-or-created // and reused for every gym's AI-generated workout plans, instead of the mobile app // guessing a category by fetching the list and taking the first entry. private const string AiGeneratedCategoryName = "AI Generated"; private async Task GetOrCreateAiWorkoutCategoryIdAsync() { var categories = await _repository.GetWorkoutCategoryListAsync(); var existing = categories.FirstOrDefault(c => string.Equals(c.Name, AiGeneratedCategoryName, StringComparison.OrdinalIgnoreCase)); if (existing != null) return existing.WorkoutCategoryId; var created = await _repository.CreateWorkoutCategoryAsync(new WorkoutCategory { WorkoutCategoryId = Guid.NewGuid(), WorkoutCategoryName = AiGeneratedCategoryName, Description = "Plans generated by the AI workout planner", IsActive = true, CreateDate = DateTime.UtcNow }); return created.WorkoutCategoryId; } public async Task CreateCustomerWorkout(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); Guid customerId; if (data.ContainsKey("customerId") && !string.IsNullOrEmpty(data["customerId"].GetString())) { customerId = Guid.Parse(data["customerId"].GetString()!); } else { Guid userId = DataExtractor.GetUserIdFromContext(httpContext); customerId = await _userRoleHelper.GetCusId(userId); } // The plan belongs to the customer's own gym, not a fixed placeholder client. Guid clientId = await _userRoleHelper.GetClientIdByCusId(customerId); Guid workoutCategoryId = data.ContainsKey("workoutCategoryId") && !string.IsNullOrEmpty(data["workoutCategoryId"].GetString()) ? Guid.Parse(data["workoutCategoryId"].GetString()!) : await GetOrCreateAiWorkoutCategoryIdAsync(); var workoutPlan = new WorkoutPlan { WorkoutPlanName = data["name"].GetString()!, Description = data.ContainsKey("description") ? data["description"].GetString() : null, IsTemplate = data.ContainsKey("isTemplate") && data["isTemplate"].GetBoolean(), FKWorkoutCategoryId = workoutCategoryId, FKClientId = clientId, CreateDate = DateTime.UtcNow }; await _repository.CreateWorkoutPlanAsync(workoutPlan); if (data.ContainsKey("workoutItems") && data["workoutItems"].ValueKind == System.Text.Json.JsonValueKind.Array) { var workoutItemsArray = data["workoutItems"].EnumerateArray(); foreach (var item in workoutItemsArray) { string? exercisesJson = null; if (item.TryGetProperty("exercises", out var exercisesElement)) { if (exercisesElement.ValueKind == System.Text.Json.JsonValueKind.Array || exercisesElement.ValueKind == System.Text.Json.JsonValueKind.Object) { exercisesJson = exercisesElement.GetRawText(); } else if (exercisesElement.ValueKind == System.Text.Json.JsonValueKind.String) { exercisesJson = exercisesElement.GetString(); } } var workoutItem = new WorkoutItem { ExerciseDay = item.GetProperty("exerciseDay").GetString()!, Description = item.TryGetProperty("description", out var descProp) ? descProp.GetString() : null, Exercises = exercisesJson, HowToDo = GetOptionalJsonText(item, "howToDo"), DoURL = GetOptionalJsonText(item, "doURL", "videoUrl", "videoURL"), FKWorkoutPlanId = workoutPlan.WorkoutPlanId, CreateDate = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow, Duration = GetOptionalJsonText(item, "duration"), WormUp = GetOptionalJsonText(item, "wormUp", "warmUp", "warmup"), Cooldown = GetOptionalJsonText(item, "cooldown", "coolDown"), }; await _repository.CreateWorkoutItemAsync(workoutItem); } } var customerWorkout = new CustomerWorkout { FKCustomerId = customerId, FKWorkoutPlanId = workoutPlan.WorkoutPlanId, AssignedDate = DateTime.UtcNow, IsActive = true, IsFollowing = true }; await _repository.AssignWorkoutToCustomerAsync(customerWorkout); await _uow.CommitAsync(); var createdWorkoutPlan = await _repository.GetWorkoutPlanByIdAsync(workoutPlan.WorkoutPlanId); if (createdWorkoutPlan == null) throw new InvalidOperationException("Failed to retrieve created workout plan"); var transformedWorkoutItems = createdWorkoutPlan.WorkoutItems?.Select(item => { object? exercisesObj = null; if (!string.IsNullOrEmpty(item.Exercises)) { try { exercisesObj = System.Text.Json.JsonSerializer.Deserialize(item.Exercises); } catch { exercisesObj = item.Exercises; } } return new { item.WorkoutItemId, item.ExerciseDay, item.Description, Exercises = exercisesObj, item.WorkoutExercises, item.TotalExercises, item.HowToDo, item.DoURL, item.Duration, item.WormUp, item.Cooldown, item.WorkoutPlanId, item.IsActive, item.CreateDate, item.UpdatedAt }; }).ToList(); try { await _activityLogService.LogActivity(new { description = $"Customer workout plan created with ID {workoutPlan.WorkoutPlanId}" }, httpContext); } catch (Exception logEx) { Console.WriteLine($"Activity log failed (non-critical): {logEx.Message}"); } return new { createdWorkoutPlan.WorkoutPlanId, createdWorkoutPlan.Name, createdWorkoutPlan.Description, createdWorkoutPlan.IsTemplate, createdWorkoutPlan.WorkoutCategoryId, createdWorkoutPlan.WorkoutCategoryName, createdWorkoutPlan.ClientId, createdWorkoutPlan.ClientName, createdWorkoutPlan.IsActive, createdWorkoutPlan.CreateDate, WorkoutItems = transformedWorkoutItems, AssignedCustomers = createdWorkoutPlan.AssignedCustomers }; } catch { await _uow.RollbackAsync(); throw; } } public async Task UpdateWorkoutPlan(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); Guid workoutPlanId = Guid.Parse(data["workoutPlanId"].GetString()!); var existingWorkoutPlan = await _repository.GetWorkoutPlanEntityByIdAsync(workoutPlanId); if (existingWorkoutPlan == null) throw new KeyNotFoundException($"Workout plan with ID {workoutPlanId} not found"); // Update workout plan properties if (data.ContainsKey("name") && !string.IsNullOrEmpty(data["name"].GetString())) existingWorkoutPlan.WorkoutPlanName = data["name"].GetString()!; if (data.ContainsKey("description")) existingWorkoutPlan.Description = data["description"].GetString(); if (data.ContainsKey("isTemplate")) existingWorkoutPlan.IsTemplate = data["isTemplate"].GetBoolean(); if (data.ContainsKey("workoutCategoryId")) existingWorkoutPlan.FKWorkoutCategoryId = Guid.Parse(data["workoutCategoryId"].GetString()!); await _repository.UpdateWorkoutPlanAsync(existingWorkoutPlan); // Handle workout items update if provided if (data.ContainsKey("workoutItems") && data["workoutItems"].ValueKind == System.Text.Json.JsonValueKind.Array) { // Get existing workout items for this plan var existingWorkoutItems = await _repository.GetWorkoutItemsByWorkoutPlanIdAsync(workoutPlanId); // Delete all existing workout items (clean slate approach) foreach (var existingItem in existingWorkoutItems) { await _repository.DeleteWorkoutItemAsync(existingItem.WorkoutItemId); } // Create new workout items from payload var workoutItemsArray = data["workoutItems"].EnumerateArray(); foreach (var workoutItemData in workoutItemsArray) { // Only create workout item if exerciseDay is provided if (workoutItemData.TryGetProperty("exerciseDay", out var nameElement) && !string.IsNullOrEmpty(nameElement.GetString())) { string? exercisesJson = null; if (workoutItemData.TryGetProperty("exercises", out var exercisesElement)) { if (exercisesElement.ValueKind == System.Text.Json.JsonValueKind.Array || exercisesElement.ValueKind == System.Text.Json.JsonValueKind.Object) { exercisesJson = exercisesElement.GetRawText(); } else if (exercisesElement.ValueKind == System.Text.Json.JsonValueKind.String) { exercisesJson = exercisesElement.GetString(); } } var workoutItem = new WorkoutItem { ExerciseDay = nameElement.GetString()!, Description = workoutItemData.TryGetProperty("description", out var desc) ? desc.GetString() : null, Exercises = exercisesJson, HowToDo = GetOptionalJsonText(workoutItemData, "howToDo"), DoURL = GetOptionalJsonText(workoutItemData, "doURL", "videoUrl", "videoURL"), Duration = GetOptionalJsonText(workoutItemData, "duration"), WormUp = GetOptionalJsonText(workoutItemData, "wormUp", "warmUp", "warmup"), Cooldown = GetOptionalJsonText(workoutItemData, "cooldown", "coolDown"), FKWorkoutPlanId = workoutPlanId, CreateDate = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }; workoutItem.WorkoutExercises = ParseWorkoutExercises(workoutItemData); await _repository.CreateWorkoutItemAsync(workoutItem); } } } await _uow.CommitAsync(); try { await _activityLogService.LogActivity( new { description = $"Workout plan updated with ID {workoutPlanId}" }, httpContext ); } catch (Exception logEx) { Console.WriteLine($"Activity log failed (non-critical): {logEx.Message}"); } var updatedWorkoutPlan = await _repository.GetWorkoutPlanByIdAsync(workoutPlanId); if (updatedWorkoutPlan == null) throw new KeyNotFoundException($"Workout plan with ID {workoutPlanId} not found after update"); // Transform workout items to have exercises as objects instead of JSON strings var transformedWorkoutItems = updatedWorkoutPlan.WorkoutItems?.Select(item => { object? exercisesObj = null; if (!string.IsNullOrEmpty(item.Exercises)) { try { exercisesObj = System.Text.Json.JsonSerializer.Deserialize(item.Exercises); } catch { exercisesObj = item.Exercises; // Keep as string if parsing fails } } return new { item.WorkoutItemId, item.ExerciseDay, item.Description, Exercises = exercisesObj, item.WorkoutExercises, item.TotalExercises, item.HowToDo, item.DoURL, item.Duration, item.WormUp, item.Cooldown, item.WorkoutPlanId, item.IsActive, item.CreateDate, item.UpdatedAt }; }).ToList(); return new { updatedWorkoutPlan.WorkoutPlanId, updatedWorkoutPlan.Name, updatedWorkoutPlan.Description, updatedWorkoutPlan.IsTemplate, updatedWorkoutPlan.WorkoutCategoryId, updatedWorkoutPlan.WorkoutCategoryName, updatedWorkoutPlan.ClientId, updatedWorkoutPlan.ClientName, updatedWorkoutPlan.IsActive, updatedWorkoutPlan.CreateDate, WorkoutItems = transformedWorkoutItems, AssignedCustomers = updatedWorkoutPlan.AssignedCustomers }; } catch { await _uow.RollbackAsync(); throw; } } public async Task DeleteWorkoutPlan(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); if (!data.ContainsKey("workoutPlanId")) { throw new KeyNotFoundException("workoutPlanId is required"); } Guid workoutPlanId = Guid.Parse(data["workoutPlanId"].GetString()!); var result = await _repository.DeleteWorkoutPlanAsync(workoutPlanId); if (!result) { throw new KeyNotFoundException("Workout plan not found"); } await _uow.CommitAsync(); try { await _activityLogService.LogActivity( new { description = $"Workout plan deleted with ID {workoutPlanId}" }, httpContext ); } catch (Exception logEx) { Console.WriteLine($"Activity log failed (non-critical): {logEx.Message}"); } return new { success = true, message = "Workout plan deleted successfully" }; } catch { await _uow.RollbackAsync(); throw; } } // Workout Item operations public async Task GetWorkoutItemsByWorkoutPlanId(object payload, HttpContext httpContext) { try { var data = DeserializePayload.Deserialize(payload); Guid workoutPlanId = Guid.Parse(data["workoutPlanId"].GetString()!); var workoutItems = await _repository.GetWorkoutItemsByWorkoutPlanIdAsync(workoutPlanId); // Transform workout items to have exercises as objects instead of JSON strings var transformedWorkoutItems = workoutItems.Select(item => { object? exercisesObj = null; if (!string.IsNullOrEmpty(item.Exercises)) { try { exercisesObj = System.Text.Json.JsonSerializer.Deserialize(item.Exercises); } catch { exercisesObj = item.Exercises; // Keep as string if parsing fails } } return new { item.WorkoutItemId, item.ExerciseDay, item.Description, Exercises = exercisesObj, item.WorkoutExercises, item.TotalExercises, item.HowToDo, item.DoURL, item.Duration, item.WormUp, item.Cooldown, item.WorkoutPlanId, item.IsActive, item.CreateDate, item.UpdatedAt }; }).ToList(); return transformedWorkoutItems; } catch { throw; } } public async Task CreateWorkoutItem(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); // Handle exercises JSON string? exercisesJson = null; if (data.ContainsKey("exercises")) { var exercisesElement = data["exercises"]; if (exercisesElement.ValueKind == System.Text.Json.JsonValueKind.Array || exercisesElement.ValueKind == System.Text.Json.JsonValueKind.Object) { exercisesJson = exercisesElement.GetRawText(); } else if (exercisesElement.ValueKind == System.Text.Json.JsonValueKind.String) { exercisesJson = exercisesElement.GetString(); } } var workoutItem = new WorkoutItem { ExerciseDay = data["exerciseDay"].GetString()!, Description = data.ContainsKey("description") ? data["description"].GetString() : null, Exercises = exercisesJson, HowToDo = GetOptionalJsonText(data, "howToDo"), DoURL = GetOptionalJsonText(data, "doURL", "videoUrl", "videoURL"), Duration = GetOptionalJsonText(data, "duration"), WormUp = GetOptionalJsonText(data, "wormUp", "warmUp", "warmup"), Cooldown = GetOptionalJsonText(data, "cooldown", "coolDown"), FKWorkoutPlanId = Guid.Parse(data["workoutPlanId"].GetString()!), CreateDate = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }; workoutItem.WorkoutExercises = ParseWorkoutExercises(data); await _repository.CreateWorkoutItemAsync(workoutItem); await _uow.CommitAsync(); var structuredExercises = workoutItem.WorkoutExercises .OrderBy(we => we.SortOrder) .Select(we => new { we.WorkoutExerciseId, we.Name, we.MuscleGroup, we.Sets, we.Reps, we.Description, we.BreathIn, we.BreathOut, we.VideoUrl, we.SortOrder }) .ToList(); // Parse legacy Exercises JSON if present (kept for backward compatibility) if (!string.IsNullOrEmpty(workoutItem.Exercises)) { try { var exercisesObj = System.Text.Json.JsonSerializer.Deserialize(workoutItem.Exercises); return new { workoutItem.WorkoutItemId, workoutItem.ExerciseDay, workoutItem.Description, Exercises = exercisesObj, WorkoutExercises = structuredExercises, workoutItem.HowToDo, workoutItem.DoURL, workoutItem.FKWorkoutPlanId, workoutItem.IsActive, workoutItem.CreateDate, workoutItem.UpdatedAt }; } catch { // If parsing fails, return as-is } } try { await _activityLogService.LogActivity(new { description = $"Workout item created with ID {workoutItem.WorkoutItemId}" }, httpContext); } catch (Exception logEx) { Console.WriteLine($"Activity log failed (non-critical): {logEx.Message}"); } return new { workoutItem.WorkoutItemId, workoutItem.ExerciseDay, workoutItem.Description, workoutItem.Exercises, WorkoutExercises = structuredExercises, workoutItem.HowToDo, workoutItem.DoURL, workoutItem.Duration, workoutItem.WormUp, workoutItem.Cooldown, workoutItem.FKWorkoutPlanId, workoutItem.IsActive, workoutItem.CreateDate, workoutItem.UpdatedAt }; } catch { await _uow.RollbackAsync(); throw; } } public async Task AssignWorkoutToCustomer(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); Guid workoutPlanId = Guid.Parse(data["workoutPlanId"].GetString()!); Guid customerId = Guid.Parse(data["customerId"].GetString()!); var customerWorkout = new CustomerWorkout { FKCustomerId = customerId, FKWorkoutPlanId = workoutPlanId, AssignedDate = DateTime.UtcNow, Notes = data.ContainsKey("notes") ? data["notes"].GetString() : null, IsActive = true }; await _repository.AssignWorkoutToCustomerAsync(customerWorkout); await _uow.CommitAsync(); try { await _activityLogService.LogActivity( new { description = $"Workout plan {workoutPlanId} assigned to customer {customerId}" }, httpContext ); } catch (Exception logEx) { Console.WriteLine($"Activity log failed (non-critical): {logEx.Message}"); } try { var plan = await _repository.GetWorkoutPlanEntityByIdAsync(workoutPlanId); var planName = plan?.WorkoutPlanName ?? "a workout plan"; await _pushNotificationService.NotifyCustomerAsync( customerId, "New workout plan assigned", $"Your coach assigned you \"{planName}\". Tap to start.", "WorkoutPlanAssigned", "WorkoutFollowedDetail", new { workoutPlanId } ); } catch (Exception notifyEx) { Console.WriteLine($"Push notification failed (non-critical): {notifyEx.Message}"); } return new { customerWorkout.CustomerWorkoutId, customerWorkout.FKCustomerId, customerWorkout.FKWorkoutPlanId, customerWorkout.AssignedDate, customerWorkout.Notes, customerWorkout.IsActive }; } catch { await _uow.RollbackAsync(); throw; } } public async Task UnassignWorkoutFromCustomer(object payload, HttpContext httpContext) { await _uow.BeginAsync(); try { var data = DeserializePayload.Deserialize(payload); Guid customerWorkoutId = Guid.Parse(data["customerWorkoutId"].GetString()!); var result = await _repository.UnassignWorkoutFromCustomerAsync(customerWorkoutId); if (!result) { throw new NotFoundException($"Customer workout assignment with ID {customerWorkoutId} not found"); } await _uow.CommitAsync(); try { await _activityLogService.LogActivity( new { description = $"Workout assignment {customerWorkoutId} removed" }, httpContext ); } catch (Exception logEx) { Console.WriteLine($"Activity log failed (non-critical): {logEx.Message}"); } return new { success = true, message = "Workout unassigned from customer successfully" }; } catch { await _uow.RollbackAsync(); throw; } } } }