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();
}
}