using Microsoft.EntityFrameworkCore; using RobotNet10.FleetManager.Data; using RobotNet10.FleetManager.Events; using RobotNet10.FleetManager.Events.Events; using RobotNet10.FleetManager.Shared.DTOs.Robot; namespace RobotNet10.FleetManager.Services; /// /// Service implementation for managing robots. /// Handles business logic for CRUD operations on robots. /// /// /// This service validates business rules such as duplicate robot IDs, /// ensures referenced robot models exist, and provides filtering and search functionality. /// public class RobotService( ApplicationDbContext context, Logger logger, IRobotEventBus? eventBus = null) : IRobotService { private readonly ApplicationDbContext _context = context; private readonly Logger _logger = logger; private readonly IRobotEventBus? _eventBus = eventBus; public async Task CreateAsync(CreateRobotRequest request) { // Check if robot ID already exists if (await ExistsAsync(request.RobotId)) { throw new InvalidOperationException($"Robot with ID '{request.RobotId}' already exists."); } // Verify that the model exists var modelExists = await _context.RobotModels.AnyAsync(rm => rm.Id == request.ModelId); if (!modelExists) { throw new KeyNotFoundException($"Robot model with ID {request.ModelId} not found."); } var robot = new Robot { Id = Guid.NewGuid(), RobotId = request.RobotId, Name = request.Name, ModelId = request.ModelId, MapId = request.MapId, CreatedDate = DateTime.UtcNow }; _context.Robots.Add(robot); await _context.SaveChangesAsync(); _logger.Info($"Created robot {robot.Name} with RobotId {robot.RobotId} and ID {robot.Id}"); return robot; } public async Task> GetAllAsync(Guid? modelId = null, Guid? mapId = null) { var query = _context.Robots.AsNoTracking().Include(r => r.Model).AsQueryable(); if (modelId.HasValue) { query = query.Where(r => r.ModelId == modelId.Value); } if (mapId.HasValue) { query = query.Where(r => r.MapId == mapId.Value); } return await query .OrderBy(r => r.Name) .ToListAsync(); } public async Task GetByIdAsync(Guid id) { return await _context.Robots .AsNoTracking() .Include(r => r.Model) .FirstOrDefaultAsync(r => r.Id == id); } public async Task GetByRobotIdAsync(string robotId) { return await _context.Robots .AsNoTracking() .Include(r => r.Model) .FirstOrDefaultAsync(r => r.RobotId == robotId); } public async Task UpdateAsync(Guid id, UpdateRobotRequest request) { var robot = await _context.Robots.FindAsync(id) ?? throw new KeyNotFoundException($"Robot with ID {id} not found."); // Check if robot ID is being changed and if new ID already exists if (request.RobotId != null && request.RobotId != robot.RobotId) { if (await ExistsAsync(request.RobotId)) { throw new InvalidOperationException($"Robot with ID '{request.RobotId}' already exists."); } robot.RobotId = request.RobotId; } if (request.Name != null) robot.Name = request.Name; if (request.ModelId.HasValue) { // Verify that the model exists var modelExists = await _context.RobotModels.AnyAsync(rm => rm.Id == request.ModelId.Value); if (!modelExists) { throw new KeyNotFoundException($"Robot model with ID {request.ModelId.Value} not found."); } // Check if ModelId is actually changing var previousModelId = robot.ModelId; if (previousModelId != request.ModelId.Value) { robot.ModelId = request.ModelId.Value; // Publish event for cache invalidation if (_eventBus != null) { try { var eventData = new RobotModelIdChangedEvent { RobotId = robot.RobotId, PreviousModelId = previousModelId, NewModelId = request.ModelId.Value }; _eventBus.PublishRobotModelIdChanged(eventData); _logger.Debug($"Published RobotModelIdChanged event for robot {robot.RobotId} (from {previousModelId} to {request.ModelId.Value})"); } catch (Exception ex) { _logger.Error($"Error publishing RobotModelIdChanged event: {ex.Message}"); // Don't fail the update if event publishing fails } } } } // MapId: if provided (including null), update it // Note: In C#, nullable Guid? means: HasValue = true means a value was provided (could be Guid.Empty or a valid Guid) // To distinguish between "not provided" and "explicitly set to null", we'd need a different approach // For now, we'll only update MapId if it has a value (non-null) if (request.MapId.HasValue) { robot.MapId = request.MapId.Value; } robot.UpdatedDate = DateTime.UtcNow; await _context.SaveChangesAsync(); _logger.Info($"Updated robot {robot.Name} with RobotId {robot.RobotId} and ID {robot.Id}"); return robot; } public async Task DeleteAsync(Guid id) { var robot = await _context.Robots.FindAsync(id); if (robot == null) { return false; } _context.Robots.Remove(robot); await _context.SaveChangesAsync(); _logger.Info($"Deleted robot {robot.Name} with RobotId {robot.RobotId} and ID {robot.Id}"); return true; } public async Task> GetByModelIdAsync(Guid modelId) { return await _context.Robots .AsNoTracking() .Include(r => r.Model) .Where(r => r.ModelId == modelId) .OrderBy(r => r.Name) .ToListAsync(); } public async Task> SearchAsync(string query) { if (string.IsNullOrWhiteSpace(query)) { return await GetAllAsync(); } var lowerQuery = query.ToLowerInvariant(); return await _context.Robots .AsNoTracking() .Include(r => r.Model) .Where(r => r.RobotId.ToLower().Contains(lowerQuery) || r.Name.ToLower().Contains(lowerQuery)) .OrderBy(r => r.Name) .ToListAsync(); } public async Task ExistsAsync(string robotId) { return await _context.Robots .AnyAsync(r => r.RobotId == robotId); } public async Task> GetByModelNameAsync(string modelName) { return await _context.Robots .AsNoTracking() .Include(r => r.Model) .Where(r => r.Name == modelName) .OrderBy(r => r.Name) .ToListAsync(); } }