using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using RobotNet10.MapEditor.Shared.DTOs.Edge; using RobotNet10.MapEditor.Shared.DTOs.Node; using RobotNet10.MapEditor.Shared.DTOs.Requests; using RobotNet10.MapEditor.Shared.DTOs.Responses; using RobotNet10.MapManager.Services; using System.Text.Json; namespace RobotNet10.MapManager.Controllers; /// /// Controller for managing edges with complex node detection and cascade delete logic /// [ApiController] [Route("api/edges")] [Authorize] public class EdgesController( IEdgeService edgeService, ILogger logger) : ControllerBase { private readonly IEdgeService _edgeService = edgeService; private readonly ILogger _logger = logger; /// /// Get all edges for a layout level /// /// Layout level ID /// List of edges with nodes and vehicle properties [HttpGet("level/{layoutLevelId}")] [ProducesResponseType(typeof(List), 200)] public async Task>> GetEdgesByLevel(Guid layoutLevelId) { var edges = await _edgeService.GetEdgesByLevelAsync(layoutLevelId, includeNodes: true, includeVehicleProperties: true); var dtos = edges.Select(MapToDto).ToList(); return Ok(dtos); } /// /// Get edge by ID /// /// Edge database ID /// Edge details with nodes and vehicle properties [HttpGet("{edgeId}")] [ProducesResponseType(typeof(EdgeDto), 200)] [ProducesResponseType(404)] public async Task> GetEdge(Guid edgeId) { var edge = await _edgeService.GetByIdAsync(edgeId, includeNodes: true, includeVehicleProperties: true); if (edge == null) { return NotFound(CreateErrorResponse($"Edge with ID '{edgeId}' not found", "EDGE_NOT_FOUND")); } return Ok(MapToDto(edge)); } /// /// Create edge with automatic node detection/creation /// If start/end point is within NodeProximityRadius (default 0.35m) of existing node, connect to that node /// Otherwise, create new node at exact coordinates /// /// Edge creation request with coordinates in METERS /// Created edge with connected nodes [HttpPost] [ProducesResponseType(typeof(EdgeDto), 201)] [ProducesResponseType(400)] public async Task> CreateEdge([FromBody] CreateEdgeRequest request) { try { var edge = await _edgeService.CreateAsync(request); var dto = MapToDto(edge); return CreatedAtAction( nameof(GetEdge), new { edgeId = edge.Id }, dto); } catch (InvalidOperationException ex) { _logger.LogWarning(ex, "Failed to create edge"); return BadRequest(CreateErrorResponse(ex.Message, "VALIDATION_ERROR")); } } /// /// Update edge properties /// /// Edge database ID /// Update request /// Updated edge [HttpPut("{edgeId}")] [ProducesResponseType(typeof(EdgeDto), 200)] [ProducesResponseType(400)] [ProducesResponseType(404)] public async Task> UpdateEdge( Guid edgeId, [FromBody] UpdateEdgeRequest request) { try { var edge = await _edgeService.UpdateAsync(edgeId, request); return Ok(MapToDto(edge)); } catch (InvalidOperationException ex) { if(_logger.IsEnabled(LogLevel.Warning))_logger.LogWarning(ex, "Failed to update edge: {edgeId}", edgeId); if (ex.Message.Contains("not found")) return NotFound(CreateErrorResponse(ex.Message, "EDGE_NOT_FOUND")); return BadRequest(CreateErrorResponse(ex.Message, "VALIDATION_ERROR")); } } /// /// Delete edge /// Cascade deletes orphan nodes (nodes not connected to any other edge) /// Also deletes StationInteractionNodes referencing orphan nodes /// /// Edge database ID /// No content on success [HttpDelete("{edgeId}")] [ProducesResponseType(204)] [ProducesResponseType(404)] public async Task DeleteEdge(Guid edgeId) { var deleted = await _edgeService.DeleteAsync(edgeId); if (!deleted) { return NotFound(CreateErrorResponse($"Edge with ID '{edgeId}' not found", "EDGE_NOT_FOUND")); } return NoContent(); } /// /// Delete multiple edges in a transaction /// All edges are deleted or none (transaction) /// Cascade deletes orphan nodes and StationInteractionNodes /// /// Batch delete request with edge IDs /// No content on success [HttpDelete("batch")] [ProducesResponseType(204)] [ProducesResponseType(400)] public async Task DeleteEdgesBatch([FromBody] DeleteEdgesRequest request) { if (request.EdgeIds == null || request.EdgeIds.Count == 0) { return BadRequest(CreateErrorResponse("No edge IDs provided", "VALIDATION_ERROR")); } try { await _edgeService.DeleteBatchAsync(request.EdgeIds); return NoContent(); } catch (InvalidOperationException ex) { _logger.LogWarning(ex, "Failed to batch delete edges"); return BadRequest(CreateErrorResponse(ex.Message, "VALIDATION_ERROR")); } catch (Exception ex) { _logger.LogError(ex, "Failed to batch delete edges"); return StatusCode(500, CreateErrorResponse("Internal server error while deleting edges", "INTERNAL_ERROR")); } } // Helper method to map entity to DTO private static EdgeDto MapToDto(Data.Edge edge) { return new EdgeDto { Id = edge.Id, LevelId = edge.LevelId, EdgeId = edge.EdgeId, EdgeName = edge.EdgeName, EdgeDescription = edge.EdgeDescription, StartNodeId = edge.StartNodeId, EndNodeId = edge.EndNodeId, StartNode = edge.StartNode != null ? new NodeDto { Id = edge.StartNode.Id, NodeId = edge.StartNode.NodeId, NodeName = edge.StartNode.NodeName, X = edge.StartNode.X, Y = edge.StartNode.Y } : null, EndNode = edge.EndNode != null ? new NodeDto { Id = edge.EndNode.Id, NodeId = edge.EndNode.NodeId, NodeName = edge.EndNode.NodeName, X = edge.EndNode.X, Y = edge.EndNode.Y } : null, VehicleProperties = edge.VehicleProperties?.Select(vp => new EdgeVehiclePropertyDto { Id = vp.Id, EdgeId = vp.EdgeId, VehicleTypeId = vp.VehicleTypeId, VehicleTypeIdString = vp.VehicleType?.VehicleTypeId, VehicleOrientation = vp.VehicleOrientation, OrientationType = vp.OrientationType, RotationAllowed = vp.RotationAllowed, RotationAtStartNodeAllowed = vp.RotationAtStartNodeAllowed, RotationAtEndNodeAllowed = vp.RotationAtEndNodeAllowed, MaxSpeed = vp.MaxSpeed, MaxRotationSpeed = vp.MaxRotationSpeed, MinHeight = vp.MinHeight, MaxHeight = vp.MaxHeight, LoadRestriction = (vp.LoadRestriction_Unloaded.HasValue || vp.LoadRestriction_Loaded.HasValue || !string.IsNullOrWhiteSpace(vp.LoadRestriction_LoadSetNames)) ? new LoadRestrictionDto { Unloaded = vp.LoadRestriction_Unloaded, Loaded = vp.LoadRestriction_Loaded, LoadSetNames = SafeDeserializeLoadSetNames(vp.LoadRestriction_LoadSetNames) } : null, TrajectoryDegree = vp.TrajectoryDegree, TrajectoryControlPoint1X = vp.TrajectoryControlPoint1X, TrajectoryControlPoint1Y = vp.TrajectoryControlPoint1Y, TrajectoryControlPoint2X = vp.TrajectoryControlPoint2X, TrajectoryControlPoint2Y = vp.TrajectoryControlPoint2Y, CorridorLeftWidth = vp.CorridorLeftWidth, CorridorRightWidth = vp.CorridorRightWidth, CorridorRefPoint = vp.CorridorRefPoint }).ToList() }; } private static List? SafeDeserializeLoadSetNames(string? json) { if (string.IsNullOrWhiteSpace(json)) return null; try { return System.Text.Json.JsonSerializer.Deserialize>(json); } catch (System.Text.Json.JsonException) { return null; } } private static ErrorResponseDto CreateErrorResponse( string error, string? errorCode = null, Dictionary? details = null) { return new ErrorResponseDto { Error = error, ErrorCode = errorCode, Details = details }; } }