using Application.Abstractions.Interfaces; using Application.DTOs.RobotDTOs; using Entities.Interfaces; using Entities.Models; using Microsoft.Extensions.Logging; namespace Application.Services { public class RobotService : IRobotService { private readonly IRobotRepository _robotRepository; private readonly IPasswordHasher _passwordHasher; private readonly IOrderRepository _orderRepository; private readonly INodeRepository _nodeRepository; private readonly IRoutingService _routingService; private readonly ILogger _logger; public RobotService( IRobotRepository robotRepository, IPasswordHasher passwordHasher, IOrderRepository orderRepository, INodeRepository nodeRepository, IRoutingService routingService, ILogger logger) { _robotRepository = robotRepository; _passwordHasher = passwordHasher; _orderRepository = orderRepository; _nodeRepository = nodeRepository; _routingService = routingService; _logger = logger; } public async Task CreateRobotAsync(CreateRobotDTO robotDto) { if (string.IsNullOrWhiteSpace(robotDto.Name)) throw new ArgumentException("Robot name is required"); if (string.IsNullOrWhiteSpace(robotDto.Model)) throw new ArgumentException("Robot model is required"); // Validate CurrentNodeId if provided if (robotDto.CurrentNodeId.HasValue) { var nodeExists = await _nodeRepository.ExistsAsync(robotDto.CurrentNodeId.Value); if (!nodeExists) throw new ArgumentException($"Node with ID {robotDto.CurrentNodeId.Value} does not exist"); } var robot = new Robot { Name = robotDto.Name, Model = robotDto.Model, Type = robotDto.Type, Status = RobotStatus.Charging, BatteryLevel = 100, CurrentNodeId = robotDto.CurrentNodeId }; await _robotRepository.AddAsync(robot); var created = await _robotRepository.GetByIdAsync(robot.Id); return MapToResponseDTO(created!); } public async Task GetRobotByIdAsync(int robotId) { var robot = await _robotRepository.GetByIdAsync(robotId); return robot == null ? null : MapToResponseDTO(robot); } public async Task> GetAllRobotsAsync() { var robots = await _robotRepository.GetAllAsync(); return robots.Select(MapToResponseDTO); } public async Task> GetByStatusAsync(RobotStatus status) { var robots = await _robotRepository.GetByStatusAsync(status); return robots.Select(MapToResponseDTO); } public async Task> GetByTypeAsync(RobotType type) { var robots = await _robotRepository.GetByTypeAsync(type); return robots.Select(MapToResponseDTO); } public async Task> GetAvailableRobotsAsync() { var robots = await _robotRepository.GetAvailableRobotsAsync(); return robots.Select(MapToResponseDTO); } public async Task UpdateRobotAsync(UpdateRobotDTO robotDto) { var robot = await _robotRepository.GetByIdAsync(robotDto.Id); if (robot == null) return false; if (string.IsNullOrWhiteSpace(robotDto.Name)) throw new ArgumentException("Robot name is required"); if (robotDto.BatteryLevel < 0 || robotDto.BatteryLevel > 100) throw new ArgumentException("Battery level must be between 0 and 100"); // Validate CurrentNodeId if provided if (robotDto.CurrentNodeId.HasValue) { var nodeExists = await _nodeRepository.ExistsAsync(robotDto.CurrentNodeId.Value); if (!nodeExists) throw new ArgumentException($"Node with ID {robotDto.CurrentNodeId.Value} does not exist"); } robot.Name = robotDto.Name; robot.Model = robotDto.Model; robot.Type = robotDto.Type; robot.Status = robotDto.Status; robot.BatteryLevel = robotDto.BatteryLevel; robot.CurrentNodeId = robotDto.CurrentNodeId; await _robotRepository.UpdateAsync(robot); return true; } public async Task DeleteRobotAsync(int robotId) { if (!await _robotRepository.ExistsAsync(robotId)) return false; await _robotRepository.DeleteAsync(robotId); return true; } public async Task<(bool Success, int? RobotId, string? ErrorMessage)> RegisterRobotAsync(RobotRegisterDTO registerDto) { // Validate and trim input registerDto.Name = registerDto.Name?.Trim(); registerDto.Model = registerDto.Model?.Trim(); registerDto.SerialNumber = registerDto.SerialNumber?.Trim(); registerDto.AccessKey = registerDto.AccessKey?.Trim(); if (string.IsNullOrWhiteSpace(registerDto.Name)) return (false, null, "Robot name is required"); if (string.IsNullOrWhiteSpace(registerDto.Model)) return (false, null, "Robot model is required"); if (string.IsNullOrWhiteSpace(registerDto.SerialNumber)) return (false, null, "Serial number is required"); if (string.IsNullOrWhiteSpace(registerDto.AccessKey)) return (false, null, "Access key is required"); // Validate robot type if (!Enum.TryParse(registerDto.Type, true, out var robotType)) return (false, null, "Invalid robot type. Must be 'GroundCourier' or 'Drone'"); // Check if serial number already exists var existingRobot = await _robotRepository.GetBySerialNumberAsync(registerDto.SerialNumber); if (existingRobot != null) { return (false, null, "Robot already registered"); } // Validate battery characteristics if (registerDto.BatteryCapacityJoules.HasValue && registerDto.BatteryCapacityJoules.Value <= 0) return (false, null, "Battery capacity must be greater than 0"); if (registerDto.EnergyConsumptionPerMeterJoules.HasValue && registerDto.EnergyConsumptionPerMeterJoules.Value <= 0) return (false, null, "Energy consumption must be greater than 0"); // Validate port range if (registerDto.Port.HasValue && (registerDto.Port.Value < 1 || registerDto.Port.Value > 65535)) return (false, null, "Port must be between 1 and 65535"); // Validate CurrentNodeId if provided if (registerDto.CurrentNodeId.HasValue) { var nodeExists = await _nodeRepository.ExistsAsync(registerDto.CurrentNodeId.Value); if (!nodeExists) return (false, null, $"Node with ID {registerDto.CurrentNodeId.Value} does not exist"); } // Hash the access key var accessKeyHash = _passwordHasher.Hash(registerDto.AccessKey); // Create new robot entity var robot = new Robot { Name = registerDto.Name, Model = registerDto.Model, Type = robotType, Status = RobotStatus.Charging, BatteryLevel = 100.0, SerialNumber = registerDto.SerialNumber, AccessKeyHash = accessKeyHash, // Battery characteristics (use provided values or defaults) BatteryCapacityJoules = registerDto.BatteryCapacityJoules ?? 360000, // Default 100Wh EnergyConsumptionPerMeterJoules = registerDto.EnergyConsumptionPerMeterJoules ?? 36, // Default 36J/m (10km range) HoverConsumptionPerSecondJoules = registerDto.HoverConsumptionPerSecondJoules ?? 100, // Default 100J/s // IoT Connection IpAddress = registerDto.IpAddress, Port = registerDto.Port ?? 80, // Default HTTP port // Initial location CurrentNodeId = registerDto.CurrentNodeId }; // Save to database await _robotRepository.AddAsync(robot); await _robotRepository.SaveChangesAsync(); return (true, robot.Id, null); } public async Task<(bool Success, int? RobotId, string? ErrorMessage)> AuthenticateRobotAsync(RobotLoginDTO loginDto) { // Validate and trim input var serialNumber = loginDto.SerialNumber?.Trim(); var accessKey = loginDto.AccessKey?.Trim(); if (string.IsNullOrWhiteSpace(serialNumber)) return (false, null, "Serial number is required"); if (string.IsNullOrWhiteSpace(accessKey)) return (false, null, "Access key is required"); // Get robot by SerialNumber var robot = await _robotRepository.GetBySerialNumberAsync(serialNumber); if (robot == null) { _logger.LogWarning("Authentication failed: Robot with SerialNumber '{SerialNumber}' not found", serialNumber); return (false, null, "Invalid serial number or access key"); } _logger.LogInformation("Found robot {RobotId} for SerialNumber {SerialNumber}", robot.Id, robot.SerialNumber); if (string.IsNullOrEmpty(robot.AccessKeyHash)) { _logger.LogWarning("Authentication failed: AccessKeyHash is empty for Robot {RobotId}", robot.Id); return (false, null, "Robot authentication not configured"); } // Verify access key var hashedAccessKey = _passwordHasher.Hash(accessKey); if (robot.AccessKeyHash != hashedAccessKey) { _logger.LogWarning("Authentication failed: Invalid AccessKey for Robot {RobotId}. Provided key (trimmed): '{Key}'", robot.Id, accessKey); return (false, null, "Invalid serial number or access key"); } _logger.LogInformation("Authentication successful for Robot {RobotId}", robot.Id); return (true, robot.Id, null); } public async Task UpdateRobotStatusAsync(int robotId, RobotStatusUpdateDTO statusUpdate) { var robot = await _robotRepository.GetByIdAsync(robotId); if (robot == null) { return null; } // Validate and parse status if (!Enum.TryParse(statusUpdate.Status, true, out var newStatus)) { throw new ArgumentException("Invalid status. Must be: Idle, Delivering, Charging, or Maintenance"); } // Validate CurrentNodeId if provided if (statusUpdate.CurrentNodeId.HasValue) { var nodeExists = await _nodeRepository.ExistsAsync(statusUpdate.CurrentNodeId.Value); if (!nodeExists) throw new ArgumentException($"Node with ID {statusUpdate.CurrentNodeId.Value} does not exist"); } // Validate TargetNodeId if provided if (statusUpdate.TargetNodeId.HasValue) { var nodeExists = await _nodeRepository.ExistsAsync(statusUpdate.TargetNodeId.Value); if (!nodeExists) throw new ArgumentException($"Target node with ID {statusUpdate.TargetNodeId.Value} does not exist"); } // Update robot status and location robot.Status = newStatus; robot.BatteryLevel = statusUpdate.BatteryLevel; robot.CurrentNodeId = statusUpdate.CurrentNodeId; robot.CurrentLatitude = statusUpdate.CurrentLatitude; robot.CurrentLongitude = statusUpdate.CurrentLongitude; robot.TargetNodeId = statusUpdate.TargetNodeId; await _robotRepository.UpdateAsync(robot); return MapToResponseDTO(robot); } // IoT Order Management public async Task> GetMyOrdersAsync(int robotId) { // Verify robot exists var robot = await _robotRepository.GetByIdAsync(robotId); if (robot == null) { throw new ArgumentException($"Robot with ID {robotId} not found"); } // Get all orders assigned to this robot that are not completed or cancelled var orders = robot.ActiveOrders?.Where(o => o.Status != OrderStatus.Delivered && o.Status != OrderStatus.Cancelled ).ToList() ?? new List(); var assignments = new List(); foreach (var order in orders) { // Get nodes var pickupNode = await _nodeRepository.GetByIdAsync(order.PickupNodeId); var dropoffNode = await _nodeRepository.GetByIdAsync(order.DropoffNodeId); if (pickupNode == null || dropoffNode == null) continue; // Determine robot's starting position Node startNode = robot.CurrentNode; if (startNode == null && robot.CurrentLatitude.HasValue && robot.CurrentLongitude.HasValue) { startNode = new Node { Name = $"Current position", Latitude = robot.CurrentLatitude.Value, Longitude = robot.CurrentLongitude.Value }; } if (startNode == null) continue; // Calculate route using the new unified routing service var (canComplete, routeSegments, totalDistance, batteryUsage, logs) = await _routingService.CalculateDroneRouteAsync( robot, startNode, pickupNode, dropoffNode, order.Weight ); if (!canComplete) continue; // Map RouteSegmentDTO to RouteWaypointDTO for backward compatibility with existing IoT clients var waypoints = routeSegments.Select(s => new RouteWaypointDTO { SequenceNumber = s.SegmentNumber, Latitude = s.ToLatitude, Longitude = s.ToLongitude, Action = s.Action.ToLower(), DistanceMeters = s.DistanceMeters }).ToList(); assignments.Add(new OrderAssignmentDTO { OrderId = order.Id, OrderName = order.Name, Description = order.Description ?? string.Empty, Weight = order.Weight, PickupNodeId = pickupNode.Id, PickupNodeName = pickupNode.Name, PickupLatitude = pickupNode.Latitude, PickupLongitude = pickupNode.Longitude, DropoffNodeId = dropoffNode.Id, DropoffNodeName = dropoffNode.Name, DropoffLatitude = dropoffNode.Latitude, DropoffLongitude = dropoffNode.Longitude, Route = waypoints, TotalDistanceMeters = totalDistance, EstimatedBatteryUsagePercent = batteryUsage, OrderStatus = order.Status.ToString(), AssignedAt = order.CreatedAt }); } return assignments; } public async Task AcceptOrderAsync(int robotId, int orderId) { // Verify robot exists var robot = await _robotRepository.GetByIdAsync(robotId); if (robot == null) { throw new ArgumentException($"Robot with ID {robotId} not found"); } // Get order var order = await _orderRepository.GetByIdAsync(orderId); if (order == null) { throw new ArgumentException($"Order with ID {orderId} not found"); } // Verify order is assigned to this robot if (order.RobotId != robotId) { throw new InvalidOperationException($"This order is not assigned to this robot. Order.RobotId={order.RobotId}, Expected={robotId}"); } // Verify order status if (order.Status != OrderStatus.Pending && order.Status != OrderStatus.Processing) { throw new InvalidOperationException($"Cannot accept order with status {order.Status}"); } // If order is already in Processing and robot is already Delivering, just return success // This handles the case where the order was already assigned via AssignRobotToOrderAsync bool alreadyAccepted = order.Status == OrderStatus.Processing && robot.Status == RobotStatus.Delivering; // Update order status to Processing only if it was Pending if (order.Status == OrderStatus.Pending) { order.Status = OrderStatus.Processing; await _orderRepository.UpdateAsync(order); } // Update robot status to Delivering only if it's not already if (robot.Status != RobotStatus.Delivering) { robot.Status = RobotStatus.Delivering; await _robotRepository.UpdateAsync(robot); } return new AcceptOrderResponseDTO { Message = alreadyAccepted ? "Order already accepted and in progress" : "Order accepted successfully", OrderId = orderId, OrderStatus = order.Status.ToString(), AcceptedAt = DateTime.UtcNow }; } public async Task UpdateOrderPhaseAsync(int robotId, int orderId, OrderPhaseUpdateDTO phaseUpdate) { // Verify robot exists var robot = await _robotRepository.GetByIdAsync(robotId); if (robot == null) { throw new ArgumentException($"Robot with ID {robotId} not found"); } // Get order var order = await _orderRepository.GetByIdAsync(orderId); if (order == null) { throw new ArgumentException($"Order with ID {orderId} not found"); } // Verify order is assigned to this robot if (order.RobotId != robotId) { throw new InvalidOperationException("This order is not assigned to this robot"); } // Update order status based on phase switch (phaseUpdate.Phase.ToUpper()) { case "FLIGHT_TO_PICKUP": case "AT_PICKUP": case "LOADING": order.Status = OrderStatus.Processing; break; case "FLIGHT_TO_DROPOFF": case "AT_DROPOFF": case "UNLOADING": order.Status = OrderStatus.EnRoute; break; case "PACKAGE_DELIVERED": order.Status = OrderStatus.Delivered; robot.Status = RobotStatus.Idle; await _robotRepository.UpdateAsync(robot); break; case "FLIGHT_TO_CHARGING": robot.Status = RobotStatus.Charging; await _robotRepository.UpdateAsync(robot); break; default: throw new ArgumentException($"Unknown phase: {phaseUpdate.Phase}"); } await _orderRepository.UpdateAsync(order); return true; } public async Task RoutingTestAsync(RoutingTestDTO testDto) { var robot = await _robotRepository.GetByIdAsync(testDto.RobotId); if (robot == null) throw new ArgumentException("Robot not found"); var pickupNode = await _nodeRepository.GetByIdAsync(testDto.PickupNodeId); var dropoffNode = await _nodeRepository.GetByIdAsync(testDto.DropoffNodeId); if (pickupNode == null || dropoffNode == null) throw new ArgumentException("Pickup or Dropoff node not found"); // Determine robot's starting position Node startNode = robot.CurrentNode; if (startNode == null && robot.CurrentLatitude.HasValue && robot.CurrentLongitude.HasValue) { startNode = new Node { Name = $"Current position", Latitude = robot.CurrentLatitude.Value, Longitude = robot.CurrentLongitude.Value }; } if (startNode == null) throw new ArgumentException("Robot has no current position/node"); var (canComplete, routeSegments, totalDistance, batteryUsage, logs) = await _routingService.CalculateDroneRouteAsync( robot, startNode, pickupNode, dropoffNode, testDto.PackageWeight ); return new RoutingTestResponseDTO { Success = canComplete, TotalDistanceMeters = totalDistance, EstimatedBatteryUsagePercent = batteryUsage, Logs = logs, Route = routeSegments }; } private static RobotResponseDTO MapToResponseDTO(Robot robot) { return new RobotResponseDTO { Id = robot.Id, Name = robot.Name, Model = robot.Model, SerialNumber = robot.SerialNumber, Type = robot.Type, TypeName = robot.Type.ToString(), Status = robot.Status, StatusName = robot.Status.ToString(), BatteryLevel = robot.BatteryLevel, BatteryCapacityJoules = robot.BatteryCapacityJoules, EnergyConsumptionPerMeterJoules = robot.EnergyConsumptionPerMeterJoules, HoverConsumptionPerSecondJoules = robot.HoverConsumptionPerSecondJoules, CurrentNodeId = robot.CurrentNodeId, CurrentNodeName = robot.CurrentNode?.Name, ActiveOrdersCount = robot.ActiveOrders?.Count ?? 0 }; } } }