using Microsoft.EntityFrameworkCore; using RobotNet10.FleetManager.Data; using RobotNet10.FleetManager.Events; using RobotNet10.FleetManager.Events.Events; using RobotNet10.FleetManager.Shared.DTOs.RobotModel; namespace RobotNet10.FleetManager.Services; /// /// Service implementation for managing robot models. /// Handles business logic for CRUD operations on robot models. /// /// /// This service validates business rules such as duplicate model names, /// checks for associated robots before deletion, and provides search functionality. /// public class RobotModelService( ApplicationDbContext context, Logger logger, IRobotEventBus? eventBus = null) : IRobotModelService { private readonly ApplicationDbContext _context = context; private readonly Logger _logger = logger; private readonly IRobotEventBus? _eventBus = eventBus; public async Task CreateAsync(CreateRobotModelRequest request) { // Check if model name already exists if (await ExistsAsync(request.ModelName)) { throw new InvalidOperationException($"Robot model with name '{request.ModelName}' already exists."); } var robotModel = new RobotModel { Id = Guid.NewGuid(), ModelName = request.ModelName, Length = request.Length, Width = request.Width, ImageWidth = request.ImageWidth, ImageHeight = request.ImageHeight, NavigationPointX = request.NavigationPointX, NavigationPointY = request.NavigationPointY, NavigationType = request.NavigationType, VehicleTypeId = request.VehicleTypeId, CreatedDate = DateTime.UtcNow }; _context.RobotModels.Add(robotModel); await _context.SaveChangesAsync(); _logger.Info($"Created robot model {robotModel.ModelName} with ID {robotModel.Id}"); return robotModel; } public async Task> GetAllAsync() { return await _context.RobotModels .AsNoTracking() .OrderBy(rm => rm.ModelName) .ToListAsync(); } public async Task GetByIdAsync(Guid id) { return await _context.RobotModels .AsNoTracking() .Include(rm => rm.Robots) .FirstOrDefaultAsync(rm => rm.Id == id); } public async Task UpdateAsync(Guid id, UpdateRobotModelRequest request) { var robotModel = await _context.RobotModels.FindAsync(id) ?? throw new KeyNotFoundException($"Robot model with ID {id} not found."); // Check if model name is being changed and if new name already exists if (request.ModelName != null && request.ModelName != robotModel.ModelName) { if (await ExistsAsync(request.ModelName)) { throw new InvalidOperationException($"Robot model with name '{request.ModelName}' already exists."); } robotModel.ModelName = request.ModelName; } if (request.Length.HasValue) robotModel.Length = request.Length.Value; if (request.Width.HasValue) robotModel.Width = request.Width.Value; if (request.ImageWidth.HasValue) robotModel.ImageWidth = request.ImageWidth.Value; if (request.ImageHeight.HasValue) robotModel.ImageHeight = request.ImageHeight.Value; if (request.NavigationPointX.HasValue) robotModel.NavigationPointX = request.NavigationPointX.Value; if (request.NavigationPointY.HasValue) robotModel.NavigationPointY = request.NavigationPointY.Value; if (request.NavigationType.HasValue) robotModel.NavigationType = request.NavigationType.Value; // VehicleTypeId: nullable, can be set or cleared // Note: In ASP.NET Core, if VehicleTypeId is in the JSON request, it will be bound // If not in JSON, it remains the default (null for nullable Guid?) // For simplicity, we always update VehicleTypeId if the request contains it // This allows setting to null by sending "VehicleTypeId": null in JSON // To avoid updating when not provided, we'd need a different approach (e.g., use a wrapper DTO) // For now, we'll always update if the property exists in the request object robotModel.VehicleTypeId = request.VehicleTypeId; robotModel.UpdatedDate = DateTime.UtcNow; await _context.SaveChangesAsync(); _logger.Info($"Updated robot model {robotModel.ModelName} with ID {robotModel.Id}"); // Publish event for cache invalidation if (_eventBus != null) { try { // Get all robots using this model var affectedRobots = await _context.Robots .Where(r => r.ModelId == robotModel.Id) .Select(r => r.RobotId) .ToListAsync(); var eventData = new RobotModelUpdatedEvent { ModelId = robotModel.Id, AffectedRobotIds = affectedRobots }; _eventBus.PublishRobotModelUpdated(eventData); _logger.Debug($"Published RobotModelUpdated event for ModelId {robotModel.Id} affecting {affectedRobots.Count} robot(s)"); } catch (Exception ex) { _logger.Error($"Error publishing RobotModelUpdated event: {ex.Message}"); // Don't fail the update if event publishing fails } } return robotModel; } public async Task DeleteAsync(Guid id) { var robotModel = await _context.RobotModels .Include(rm => rm.Robots) .FirstOrDefaultAsync(rm => rm.Id == id); if (robotModel == null) { return false; } // Check if there are any robots using this model if (robotModel.Robots.Count != 0) { throw new InvalidOperationException($"Cannot delete robot model '{robotModel.ModelName}' because it is being used by {robotModel.Robots.Count} robot(s)."); } _context.RobotModels.Remove(robotModel); await _context.SaveChangesAsync(); _logger.Info($"Deleted robot model {robotModel.ModelName} with ID {robotModel.Id}"); return true; } public async Task ExistsAsync(string modelName) { return await _context.RobotModels .AnyAsync(rm => rm.ModelName == modelName); } public async Task> SearchAsync(string query) { if (string.IsNullOrWhiteSpace(query)) { return await GetAllAsync(); } var lowerQuery = query.ToLowerInvariant(); return await _context.RobotModels .AsNoTracking() .Where(rm => rm.ModelName.ToLower().Contains(lowerQuery)) .OrderBy(rm => rm.ModelName) .ToListAsync(); } public async Task GetUsageInfoAsync(Guid id) { var robotModel = await _context.RobotModels .AsNoTracking() .Include(rm => rm.Robots) .FirstOrDefaultAsync(rm => rm.Id == id); return robotModel == null ? throw new KeyNotFoundException($"Robot model with ID {id} not found.") : new RobotModelUsageInfoDto { Id = robotModel.Id, ModelName = robotModel.ModelName, RobotCount = robotModel.Robots.Count }; } }