using Application.Abstractions.Interfaces; using Application.DTOs.OrderDTOs; using Application.DTOs.FileDTOs; using Entities.Interfaces; using Entities.Models; using Entities.Enums; using Microsoft.Extensions.Logging; using Application.Abstractions.Exceptions; namespace Application.Services { public class OrderService : IOrderService { private readonly IOrderRepository _orderRepository; private readonly IUserRepository _userRepository; private readonly IRobotRepository _robotRepository; private readonly INodeRepository _nodeRepository; private readonly IDroneConnectionService _droneConnectionService; private readonly ISettingsService _settingsService; private readonly IRoutingService _routingService; private readonly ILogger _logger; public OrderService( IOrderRepository orderRepository, IUserRepository userRepository, IRobotRepository robotRepository, INodeRepository nodeRepository, IDroneConnectionService droneConnectionService, ISettingsService settingsService, IRoutingService routingService, ILogger logger) { _orderRepository = orderRepository; _userRepository = userRepository; _robotRepository = robotRepository; _nodeRepository = nodeRepository; _droneConnectionService = droneConnectionService; _settingsService = settingsService; _routingService = routingService; _logger = logger; } public async Task CreateOrderAsync(int senderId, CreateOrderDTO orderDto) { // Validate sender exists var sender = await _userRepository.GetByIdAsync(senderId); if (sender == null) { throw new ArgumentException("Sender not found"); } // Validate recipient exists var recipient = await _userRepository.GetByIdAsync(orderDto.RecipientId); if (recipient == null) { throw new ArgumentException("Recipient not found"); } // Validate that sender and recipient are different if (senderId == orderDto.RecipientId) { throw new ArgumentException("Sender and recipient cannot be the same user"); } // Validate sender has personal node if (!sender.PersonalNodeId.HasValue) { throw new ArgumentException("Sender does not have a personal node configured"); } // Validate recipient has personal node if (!recipient.PersonalNodeId.HasValue) { throw new ArgumentException("Recipient does not have a personal node configured"); } // Validate weight and product price if (orderDto.Weight <= 0) { throw new ArgumentException("Weight must be greater than zero"); } if (orderDto.ProductPrice < 0) { throw new ArgumentException("Product price cannot be negative"); } // Calculate delivery price based on weight // Formula: Base price (50) + weight-based price (10 per kg) decimal deliveryPrice = await CalculateDeliveryPriceAsync(orderDto.Weight); // Create new order var order = new Order { Name = orderDto.Name, Description = orderDto.Description, Weight = orderDto.Weight, DeliveryPrice = deliveryPrice, ProductPrice = orderDto.ProductPrice, IsProductPaid = orderDto.IsProductPaid, DeliveryPayer = orderDto.DeliveryPayer, IsDeliveryPaid = false, Status = orderDto.DeliveryPayer == DeliveryPayer.Sender ? OrderStatus.AwaitingPayment : OrderStatus.AwaitingConfirmation, SenderId = senderId, RecipientId = orderDto.RecipientId, PickupNodeId = sender.PersonalNodeId.Value, DropoffNodeId = recipient.PersonalNodeId.Value, CreatedAt = DateTime.UtcNow }; await _orderRepository.AddAsync(order); // Retrieve the created order with all related data var createdOrder = await _orderRepository.GetByIdAsync(order.Id); return MapToResponseDTO(createdOrder!); } public async Task CalculateDeliveryPriceAsync(double weight) { var settings = await _settingsService.GetPricingSettingsAsync(); // Calculate total delivery price decimal totalPrice = settings.BasePrice + (decimal)weight * settings.PricePerKg; return Math.Round(totalPrice, 2); } public async Task GetOrderByIdAsync(int orderId) { var order = await _orderRepository.GetByIdAsync(orderId); return order == null ? null : MapToResponseDTO(order); } public async Task> GetAllOrdersAsync() { var orders = await _orderRepository.GetAllAsync(); return orders.Select(MapToResponseDTO); } public async Task> GetUserOrdersAsync(int userId) { var orders = await _orderRepository.GetByUserIdAsync(userId); return orders.Select(MapToResponseDTO); } public async Task> GetSentOrdersAsync(int senderId) { var orders = await _orderRepository.GetBySenderIdAsync(senderId); return orders.Select(MapToResponseDTO); } public async Task> GetReceivedOrdersAsync(int recipientId) { var orders = await _orderRepository.GetByRecipientIdAsync(recipientId); return orders.Select(MapToResponseDTO); } public async Task> GetOrdersByStatusAsync(OrderStatus status) { var orders = await _orderRepository.GetByStatusAsync(status); return orders.Select(MapToResponseDTO); } public async Task UpdateOrderStatusAsync(int orderId, OrderStatus newStatus) { var order = await _orderRepository.GetByIdAsync(orderId); if (order == null) { return false; } // Validate status transition if (!IsValidStatusTransition(order.Status, newStatus)) { throw new InvalidOperationException($"Invalid status transition from {order.Status} to {newStatus}"); } order.Status = newStatus; // Set completion time if order is delivered or cancelled if (newStatus == OrderStatus.Delivered || newStatus == OrderStatus.Cancelled) { order.CompletedAt = DateTime.UtcNow; } await _orderRepository.UpdateAsync(order); return true; } public async Task AssignRobotToOrderAsync(int orderId, int robotId) { var order = await _orderRepository.GetByIdAsync(orderId); if (order == null) { return false; } // Validate robot exists var robot = await _robotRepository.GetByIdAsync(robotId); if (robot == null) { throw new ArgumentException($"Robot with ID {robotId} does not exist"); } // Check if order can be assigned if (order.Status != OrderStatus.Pending && order.Status != OrderStatus.Processing) { throw new InvalidOperationException($"Cannot assign robot to order with status {order.Status}"); } // Assign robot to order order.RobotId = robotId; // Update order status to Processing if it was Pending if (order.Status == OrderStatus.Pending) { order.Status = OrderStatus.Processing; } // Update robot status to Delivering robot.Status = RobotStatus.Delivering; robot.CurrentNodeId = order.PickupNodeId; await _orderRepository.UpdateAsync(order); await _robotRepository.UpdateAsync(robot); return true; } public async Task CancelOrderAsync(int orderId, int userId) { var order = await _orderRepository.GetByIdAsync(orderId); if (order == null) { return false; } // Only sender can cancel the order and only if it's not already in progress if (order.SenderId != userId) { throw new UnauthorizedAccessException("Only the sender can cancel the order"); } if (order.Status != OrderStatus.Pending && order.Status != OrderStatus.Processing) { throw new InvalidOperationException("Cannot cancel order that is already en route or completed"); } // If robot was assigned, set it back to Idle if (order.RobotId.HasValue) { var robot = await _robotRepository.GetByIdAsync(order.RobotId.Value); if (robot != null) { robot.Status = RobotStatus.Idle; await _robotRepository.UpdateAsync(robot); } } order.Status = OrderStatus.Cancelled; order.CompletedAt = DateTime.UtcNow; await _orderRepository.UpdateAsync(order); return true; } public async Task DeleteOrderAsync(int orderId) { if (!await _orderRepository.ExistsAsync(orderId)) { return false; } await _orderRepository.DeleteAsync(orderId); return true; } private static OrderResponseDTO MapToResponseDTO(Order order) { return new OrderResponseDTO { Id = order.Id, Name = order.Name, Description = order.Description, Weight = order.Weight, DeliveryPrice = order.DeliveryPrice, ProductPrice = order.ProductPrice, IsProductPaid = order.IsProductPaid, DeliveryPayer = order.DeliveryPayer, DeliveryPayerName = order.DeliveryPayer.ToString(), IsDeliveryPaid = order.IsDeliveryPaid, Status = order.Status, CreatedAt = order.CreatedAt, CompletedAt = order.CompletedAt, SenderId = order.SenderId, SenderName = order.Sender?.UserName ?? "Unknown", RecipientId = order.RecipientId, RecipientName = order.Recipient?.UserName ?? "Unknown", RobotId = order.RobotId, RobotName = order.AssignedRobot?.Name, PickupNodeId = order.PickupNodeId, PickupNodeName = order.PickupNode?.Name ?? "Unknown", DropoffNodeId = order.DropoffNodeId, DropoffNodeName = order.DropoffNode?.Name ?? "Unknown", Images = order.Images?.Select(img => new FileResponseDTO { Id = img.Id, FileName = img.FileName, FilePath = img.FilePath, ContentType = img.ContentType, FileSize = img.FileSize, UploadedAt = img.UploadedAt }).ToList() }; } private static bool IsValidStatusTransition(OrderStatus currentStatus, OrderStatus newStatus) { // Define valid status transitions return currentStatus switch { OrderStatus.AwaitingPayment => newStatus == OrderStatus.AwaitingConfirmation || newStatus == OrderStatus.Cancelled, OrderStatus.AwaitingConfirmation => newStatus == OrderStatus.Pending || newStatus == OrderStatus.Cancelled, OrderStatus.Pending => newStatus == OrderStatus.Processing || newStatus == OrderStatus.Cancelled, OrderStatus.Processing => newStatus == OrderStatus.EnRoute || newStatus == OrderStatus.Cancelled, OrderStatus.EnRoute => newStatus == OrderStatus.Delivered || newStatus == OrderStatus.Cancelled, OrderStatus.Delivered => false, // Cannot change from delivered OrderStatus.Cancelled => false, // Cannot change from cancelled _ => false }; } public async Task ExecuteOrderAsync(int orderId, int userId) { var globalLogs = new List(); globalLogs.Add($"[SYSTEM] Execution started at {DateTime.UtcNow}"); // Get the order var order = await _orderRepository.GetByIdAsync(orderId); if (order == null) { globalLogs.Add($"[ERROR] Order {orderId} not found"); throw new ArgumentException($"Order with ID {orderId} not found"); } if (!(await _orderRepository.DoesItBelong(orderId, userId))) { globalLogs.Add($"[ERROR] Unauthorized access for user {userId}"); throw new InvalidOperationException($"Cannot execute order. Only sender's orders can be executed."); } // Validate order status if (order.Status != OrderStatus.Pending) { globalLogs.Add($"[ERROR] Order status is {order.Status}, must be Pending"); throw new InvalidOperationException($"Cannot execute order with status {order.Status}. Only Pending orders can be executed."); } if (!order.IsProductPaid || !order.IsDeliveryPaid) { globalLogs.Add($"[ERROR] Payment pending: ProductPaid={order.IsProductPaid}, DeliveryPaid={order.IsDeliveryPaid}"); throw new InvalidOperationException("Cannot execute order until both product and delivery are paid."); } // Get pickup and dropoff nodes var pickupNode = await _nodeRepository.GetByIdAsync(order.PickupNodeId); var dropoffNode = await _nodeRepository.GetByIdAsync(order.DropoffNodeId); if (pickupNode == null || dropoffNode == null) { globalLogs.Add($"[ERROR] Critical node missing. Pickup={order.PickupNodeId}, Dropoff={order.DropoffNodeId}"); throw new InvalidOperationException("Pickup or dropoff node not found"); } // Get all available drones (Idle and Charging) var availableDrones = await _robotRepository.GetAvailableRobotsAsync(); var dronesList = availableDrones.Where(r => r.Type == RobotType.Drone).ToList(); globalLogs.Add($"[SYSTEM] Found {dronesList.Count} available drones in the vicinity."); if (!dronesList.Any()) { globalLogs.Add("[ERROR] No drones are currently Idle or Charging. Try again when drones finish their missions."); var failedResponse = new ExecuteOrderResponseDTO { Message = "No drones available", DebugLogs = globalLogs }; // We'll throw but maybe the controller can handle this to return the logs throw new RoutingException("No suitable drones found (all are offline, busy, or low battery)", globalLogs); } // Find optimal drone and route Robot? optimalDrone = null; List optimalRoute = null; double minTotalDistance = double.MaxValue; double optimalBatteryUsage = 0; foreach (var drone in dronesList) { globalLogs.Add($"\n--- ANALYZING DRONE: {drone.Name} (ID:{drone.Id}) ---"); Node startNode = drone.CurrentNode; if (startNode == null && drone.CurrentLatitude.HasValue && drone.CurrentLongitude.HasValue) { startNode = new Node { Name = $"Drone Position", Latitude = drone.CurrentLatitude.Value, Longitude = drone.CurrentLongitude.Value, Type = NodeType.Other }; } if (startNode == null) { globalLogs.Add($"[SKIP] Drone has no reported location (GPS/Node)."); continue; } var (canComplete, route, totalDistance, batteryUsage, droneLogs) = await _routingService.CalculateDroneRouteAsync( drone, startNode, pickupNode, dropoffNode, order.Weight ); globalLogs.AddRange(droneLogs); if (canComplete && totalDistance < minTotalDistance) { optimalDrone = drone; optimalRoute = route; minTotalDistance = totalDistance; optimalBatteryUsage = batteryUsage; globalLogs.Add($"[MATCH] Drone {drone.Name} is currently the best candidate."); } } if (optimalDrone == null || optimalRoute == null) { globalLogs.Add("\n[CRITICAL] After evaluating all candidates, none could fulfill the route requirements."); throw new RoutingException("No suitable drone found that can complete this delivery", globalLogs); } // Assign the optimal drone to the order await AssignRobotToOrderAsync(orderId, optimalDrone.Id); globalLogs.Add($"\n[SUCCESS] Assigned to {optimalDrone.Name}. Total Distance: {Math.Round(minTotalDistance, 0)}m"); return new ExecuteOrderResponseDTO { OrderId = orderId, AssignedRobotId = optimalDrone.Id, AssignedRobotName = optimalDrone.Name, Message = "Order successfully assigned to optimal drone.", Route = optimalRoute, TotalDistanceMeters = minTotalDistance, EstimatedBatteryUsagePercent = optimalBatteryUsage, DebugLogs = globalLogs }; } public async Task AdminUpdateOrderAsync(AdminUpdateOrderDTO updateDto) { var order = await _orderRepository.GetByIdAsync(updateDto.Id); if (order == null) return false; if (!string.IsNullOrWhiteSpace(updateDto.Name)) order.Name = updateDto.Name; if (!string.IsNullOrWhiteSpace(updateDto.Description)) order.Description = updateDto.Description; if (updateDto.Weight.HasValue) order.Weight = updateDto.Weight.Value; if (updateDto.ProductPrice.HasValue) order.ProductPrice = updateDto.ProductPrice.Value; if (updateDto.IsProductPaid.HasValue) order.IsProductPaid = updateDto.IsProductPaid.Value; if (updateDto.Status.HasValue) { order.Status = updateDto.Status.Value; if (order.Status == OrderStatus.Delivered || order.Status == OrderStatus.Cancelled) { order.CompletedAt = DateTime.UtcNow; } } await _orderRepository.UpdateAsync(order); return true; } } }