using RobotNet.VDA5050; using RobotNet10.MapEditor.Shared.DTOs.Edge; using RobotNet10.MapEditor.Shared.DTOs.Layout; using RobotNet10.MapEditor.Shared.DTOs.LayoutData; using RobotNet10.MapEditor.Shared.DTOs.Node; using RobotNet10.MapEditor.Shared.DTOs.Requests; using RobotNet10.MapEditor.Shared.DTOs.Responses; using RobotNet10.MapEditor.Shared.DTOs.Station; using RobotNet10.MapEditor.Shared.DTOs.VehicleType; using System.Globalization; using System.Net.Http.Headers; using System.Net.Http.Json; namespace RobotNet10.MapEditor.Services.API; /// /// Service for communicating with MapManager REST API /// public class MapManagerApiService(HttpClient httpClient) { private readonly HttpClient _httpClient = httpClient; private readonly string? _baseUrl = httpClient.BaseAddress?.AbsoluteUri; // ========================================== // LAYOUTS // ========================================== /// /// Search layouts (returns nested Versions and Levels) /// public async Task> SearchLayoutsAsync(string? search = null) { var url = $"{_baseUrl}api/layouts"; if (!string.IsNullOrEmpty(search)) url += $"?search={Uri.EscapeDataString(search)}"; try { var response = await _httpClient.GetAsync(url); if (!response.IsSuccessStatusCode) { var errorMessage = await GetDetailedErrorMessageAsync(response, url); throw new HttpRequestException(errorMessage); } return await response.Content.ReadFromJsonAsync>() ?? new(); } catch (HttpRequestException) { throw; // Re-throw with detailed message } catch (Exception ex) { throw new HttpRequestException($"Failed to load layouts from {url}: {ex.Message}", ex); } } /// /// Get detailed error message from HTTP response /// private async Task GetDetailedErrorMessageAsync(HttpResponseMessage response, string url) { var statusCode = response.StatusCode; var statusText = response.ReasonPhrase ?? statusCode.ToString(); // Try to read error message from response body string? errorDetail = null; try { var content = await response.Content.ReadAsStringAsync(); if (!string.IsNullOrWhiteSpace(content) && content.Length < 500) { // Try to parse as JSON try { using var doc = System.Text.Json.JsonDocument.Parse(content); var root = doc.RootElement; if (root.TryGetProperty("error", out var errorProp)) errorDetail = errorProp.GetString(); else if (root.TryGetProperty("message", out var messageProp)) errorDetail = messageProp.GetString(); else if (root.ValueKind == System.Text.Json.JsonValueKind.String) errorDetail = root.GetString(); } catch { // If not JSON, use content as-is if reasonable length errorDetail = content; } } } catch { // Ignore errors reading response body } // Build detailed error message var message = $"HTTP {(int)statusCode} {statusText}"; if (!string.IsNullOrWhiteSpace(errorDetail)) { message += $": {errorDetail}"; } message += $" (URL: {url})"; return message; } /// /// Get layout by ID /// public async Task GetLayoutAsync(Guid layoutId) { return await _httpClient.GetFromJsonAsync( $"{_baseUrl}api/layouts/{layoutId}"); } /// /// Create new layout /// public async Task CreateLayoutAsync(CreateLayoutRequest request) { var response = await _httpClient.PostAsJsonAsync($"{_baseUrl}api/layouts", request); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync() ?? throw new Exception("Failed to create layout"); } /// /// Update layout /// public async Task UpdateLayoutAsync(Guid layoutId, UpdateLayoutRequest request) { var response = await _httpClient.PutAsJsonAsync($"{_baseUrl}api/layouts/{layoutId}", request); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync() ?? throw new Exception("Failed to update layout"); } /// /// Delete layout /// public async Task DeleteLayoutAsync(Guid layoutId) { var response = await _httpClient.DeleteAsync($"{_baseUrl}api/layouts/{layoutId}"); response.EnsureSuccessStatusCode(); } /// /// Activate layout /// public async Task ActivateLayoutAsync(Guid layoutId) { var response = await _httpClient.PostAsync($"{_baseUrl}api/layouts/{layoutId}/activate", null); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync() ?? throw new Exception("Failed to activate layout"); } /// /// Deactivate layout /// public async Task DeactivateLayoutAsync(Guid layoutId) { var response = await _httpClient.PostAsync($"{_baseUrl}api/layouts/{layoutId}/deactivate", null); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync() ?? throw new Exception("Failed to deactivate layout"); } // ========================================== // VERSIONS // ========================================== /// /// Create new version for a layout /// public async Task CreateVersionAsync(Guid layoutId, CreateLayoutVersionRequest request) { var response = await _httpClient.PostAsJsonAsync( $"{_baseUrl}api/layouts/{layoutId}/versions", request); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync() ?? throw new Exception("Failed to create version"); } /// /// Get all versions for a layout /// public async Task> GetVersionsAsync(Guid layoutId) { return await _httpClient.GetFromJsonAsync>( $"{_baseUrl}api/layouts/{layoutId}/versions") ?? new(); } /// /// Get version by ID /// public async Task GetVersionAsync(Guid versionId) { return await _httpClient.GetFromJsonAsync( $"{_baseUrl}api/layouts/versions/{versionId}"); } /// /// Delete version /// public async Task DeleteVersionAsync(Guid versionId) { var response = await _httpClient.DeleteAsync($"{_baseUrl}api/layouts/versions/{versionId}"); response.EnsureSuccessStatusCode(); } // ========================================== // LEVELS // ========================================== /// /// Create new level for a version /// public async Task CreateLevelAsync(Guid versionId, CreateLayoutLevelRequest request) { var response = await _httpClient.PostAsJsonAsync( $"{_baseUrl}api/layouts/versions/{versionId}/levels", request); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync() ?? throw new Exception("Failed to create level"); } /// /// Create a new level with background image in a single request /// /// Version ID /// Layout level identifier string /// Level order /// Resolution in meters per pixel /// Origin X coordinate in meters /// Origin Y coordinate in meters /// Image stream (PNG format) /// Image file name /// Created layout level DTO public async Task CreateLevelWithImageAsync( Guid versionId, string layoutLevelId, int levelOrder, double resolution, double originX, double originY, Stream imageStream, string fileName) { using var content = new MultipartFormDataContent(); // Add form fields content.Add(new StringContent(layoutLevelId), "layoutLevelId"); content.Add(new StringContent(levelOrder.ToString()), "levelOrder"); content.Add(new StringContent(resolution.ToString(CultureInfo.InvariantCulture)), "resolution"); content.Add(new StringContent(originX.ToString(CultureInfo.InvariantCulture)), "originX"); content.Add(new StringContent(originY.ToString(CultureInfo.InvariantCulture)), "originY"); // Add image file var streamContent = new StreamContent(imageStream); streamContent.Headers.ContentType = new MediaTypeHeaderValue("image/png"); content.Add(streamContent, "file", fileName); var response = await _httpClient.PostAsync( $"{_baseUrl}api/layouts/versions/{versionId}/levels/with-image", content); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync() ?? throw new Exception("Failed to create level with image"); } /// /// Get all levels for a version /// public async Task> GetLevelsAsync(Guid versionId) { return await _httpClient.GetFromJsonAsync>( $"{_baseUrl}api/layouts/versions/{versionId}/levels") ?? new(); } /// /// Get level by ID /// public async Task GetLevelAsync(Guid levelId) { return await _httpClient.GetFromJsonAsync( $"{_baseUrl}api/layouts/levels/{levelId}"); } /// /// Update level /// public async Task UpdateLevelAsync(Guid levelId, UpdateLayoutLevelRequest request) { var response = await _httpClient.PutAsJsonAsync($"{_baseUrl}api/layouts/levels/{levelId}", request); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync() ?? throw new Exception("Failed to update level"); } /// /// Delete level /// public async Task DeleteLevelAsync(Guid levelId) { var response = await _httpClient.DeleteAsync($"{_baseUrl}api/layouts/levels/{levelId}"); response.EnsureSuccessStatusCode(); } // ========================================== // LAYOUT DATA (For Preview) // ========================================== /// /// Get comprehensive layout data (nodes, edges, stations) /// public async Task GetLayoutDataAsync(Guid layoutLevelId) { return await _httpClient.GetFromJsonAsync( $"{_baseUrl}api/layout-data/{layoutLevelId}") ?? new(); } // ========================================== // IMAGES // ========================================== /// /// Get background image for a layout level /// public async Task GetLayoutImageAsync(Guid layoutLevelId) { try { return await _httpClient.GetByteArrayAsync($"{_baseUrl}api/images/layout/{layoutLevelId}"); } catch (HttpRequestException) { return null; // Image doesn't exist } } /// /// Upload background image for a layout level /// public async Task UploadLayoutImageAsync(Guid layoutLevelId, Stream imageStream, string fileName) { using var content = new MultipartFormDataContent(); var streamContent = new StreamContent(imageStream); streamContent.Headers.ContentType = new MediaTypeHeaderValue("image/png"); content.Add(streamContent, "file", fileName); var response = await _httpClient.PostAsync( $"{_baseUrl}api/images/layout/{layoutLevelId}", content); response.EnsureSuccessStatusCode(); } /// /// Delete background image for a layout level /// public async Task DeleteLayoutImageAsync(Guid layoutLevelId) { var response = await _httpClient.DeleteAsync($"{_baseUrl}api/images/layout/{layoutLevelId}"); response.EnsureSuccessStatusCode(); } // ========================================== // NODES // ========================================== /// /// Get node by ID /// public async Task GetNodeAsync(Guid nodeId) { return await _httpClient.GetFromJsonAsync($"{_baseUrl}api/nodes/{nodeId}"); } /// /// Update node properties /// public async Task UpdateNodeAsync(Guid nodeId, UpdateNodeRequest request) { var response = await _httpClient.PutAsJsonAsync($"{_baseUrl}api/nodes/{nodeId}", request); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync() ?? throw new Exception("Failed to update node"); } // ========================================== // EDGES // ========================================== /// /// Create a new edge with automatic node detection/creation /// public async Task CreateEdgeAsync(CreateEdgeRequest request) { var response = await _httpClient.PostAsJsonAsync($"{_baseUrl}api/edges", request); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync() ?? throw new Exception("Failed to create edge"); } /// /// Get edge by ID /// public async Task GetEdgeAsync(Guid edgeId) { return await _httpClient.GetFromJsonAsync($"{_baseUrl}api/edges/{edgeId}"); } /// /// Update edge properties /// public async Task UpdateEdgeAsync(Guid edgeId, UpdateEdgeRequest request) { var response = await _httpClient.PutAsJsonAsync($"{_baseUrl}api/edges/{edgeId}", request); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync() ?? throw new Exception("Failed to update edge"); } /// /// Delete edge /// public async Task DeleteEdgeAsync(Guid edgeId) { var response = await _httpClient.DeleteAsync($"{_baseUrl}api/edges/{edgeId}"); response.EnsureSuccessStatusCode(); } /// /// Delete multiple edges in batch /// public async Task DeleteEdgesBatchAsync(List edgeIds) { var request = new DeleteEdgesRequest { EdgeIds = edgeIds }; // Send DELETE request with body (non-standard but required by API) var content = new StringContent( System.Text.Json.JsonSerializer.Serialize(request, JsonOptionExtends.Write), System.Text.Encoding.UTF8, "application/json"); var deleteRequest = new HttpRequestMessage(HttpMethod.Delete, $"{_baseUrl}api/edges/batch") { Content = content }; var response = await _httpClient.SendAsync(deleteRequest); response.EnsureSuccessStatusCode(); } // ========================================== // MERGE/SPLIT OPERATIONS // ========================================== /// /// Merge multiple nodes into one node /// public async Task MergeNodesAsync(MergeNodesRequest request) { var response = await _httpClient.PostAsJsonAsync($"{_baseUrl}api/layout-data/merge-nodes", request); if (response.StatusCode == System.Net.HttpStatusCode.BadRequest) { var errorContent = await response.Content.ReadAsStringAsync(); throw new InvalidOperationException($"Failed to merge nodes: {errorContent}"); } response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync() ?? throw new Exception("Failed to merge nodes"); } /// /// Split a node into multiple nodes /// public async Task SplitNodeAsync(SplitNodeRequest request) { var response = await _httpClient.PostAsJsonAsync($"{_baseUrl}api/layout-data/split-node", request); if (response.StatusCode == System.Net.HttpStatusCode.BadRequest) { var errorContent = await response.Content.ReadAsStringAsync(); throw new InvalidOperationException($"Failed to split node: {errorContent}"); } response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync() ?? throw new Exception("Failed to split node"); } /// /// Save all layout changes (nodes and edges) in a batch operation /// public async Task SaveLayoutDataAsync(SaveLayoutDataRequest request) { var response = await _httpClient.PostAsJsonAsync($"{_baseUrl}api/layout-data/save", request); if (response.StatusCode == System.Net.HttpStatusCode.BadRequest) { var errorContent = await response.Content.ReadAsStringAsync(); throw new InvalidOperationException($"Failed to save layout data: {errorContent}"); } response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync() ?? throw new Exception("Failed to save layout data"); } /// /// Copy selected nodes and edges with an offset /// public async Task CopyNodesAsync(CopyNodesRequest request) { var response = await _httpClient.PostAsJsonAsync($"{_baseUrl}api/layout-data/copy-nodes", request); if (response.StatusCode == System.Net.HttpStatusCode.BadRequest) { var errorContent = await response.Content.ReadAsStringAsync(); throw new InvalidOperationException($"Failed to copy nodes: {errorContent}"); } response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync() ?? throw new Exception("Failed to copy nodes"); } // ========================================== // VEHICLE TYPES // ========================================== /// /// Get all vehicle types, optionally filtered by active status /// public async Task> GetVehicleTypesAsync(bool? isActive = null) { var url = $"{_baseUrl}api/vehicles"; if (isActive.HasValue) url += $"?isActive={isActive.Value}"; return await _httpClient.GetFromJsonAsync>(url) ?? new(); } /// /// Get vehicle type by database ID /// public async Task GetVehicleTypeAsync(Guid id) { return await _httpClient.GetFromJsonAsync($"{_baseUrl}api/vehicles/{id}"); } /// /// Get vehicle type by VehicleTypeId string /// public async Task GetVehicleTypeByStringIdAsync(string vehicleTypeId) { return await _httpClient.GetFromJsonAsync( $"{_baseUrl}api/vehicles/vehicleTypeId/{Uri.EscapeDataString(vehicleTypeId)}"); } /// /// Search vehicle types by query string /// public async Task> SearchVehicleTypesAsync(string query) { if (string.IsNullOrWhiteSpace(query)) return new List(); var url = $"{_baseUrl}api/vehicles/search?query={Uri.EscapeDataString(query)}"; return await _httpClient.GetFromJsonAsync>(url) ?? new(); } /// /// Create a new vehicle type /// public async Task CreateVehicleTypeAsync(CreateVehicleTypeRequest request) { var response = await _httpClient.PostAsJsonAsync($"{_baseUrl}api/vehicles", request); if (response.StatusCode == System.Net.HttpStatusCode.BadRequest) { var errorContent = await response.Content.ReadAsStringAsync(); throw new InvalidOperationException($"Failed to create vehicle type: {errorContent}"); } response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync() ?? throw new Exception("Failed to create vehicle type"); } /// /// Update an existing vehicle type /// public async Task UpdateVehicleTypeAsync(Guid id, UpdateVehicleTypeRequest request) { var response = await _httpClient.PutAsJsonAsync($"{_baseUrl}api/vehicles/{id}", request); if (response.StatusCode == System.Net.HttpStatusCode.NotFound) { var errorContent = await response.Content.ReadAsStringAsync(); throw new InvalidOperationException($"Vehicle type not found: {errorContent}"); } response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync() ?? throw new Exception("Failed to update vehicle type"); } /// /// Delete a vehicle type /// public async Task DeleteVehicleTypeAsync(Guid id) { var response = await _httpClient.DeleteAsync($"{_baseUrl}api/vehicles/{id}"); if (response.StatusCode == System.Net.HttpStatusCode.BadRequest) { var errorContent = await response.Content.ReadAsStringAsync(); throw new InvalidOperationException($"Cannot delete vehicle type: {errorContent}"); } response.EnsureSuccessStatusCode(); } /// /// Get usage information for a vehicle type /// public async Task GetVehicleTypeUsageAsync(Guid id) { return await _httpClient.GetFromJsonAsync( $"{_baseUrl}api/vehicles/{id}/usage") ?? throw new Exception("Failed to get vehicle type usage info"); } // ========================================== // STATIONS // ========================================== /// /// Get all stations for a layout level /// public async Task> GetStationsByLevelAsync(Guid layoutLevelId) { return await _httpClient.GetFromJsonAsync>( $"{_baseUrl}api/stations/level/{layoutLevelId}") ?? new(); } /// /// Get station by database ID /// public async Task GetStationAsync(Guid stationId) { return await _httpClient.GetFromJsonAsync( $"{_baseUrl}api/stations/{stationId}"); } /// /// Create a new station /// public async Task CreateStationAsync(CreateStationRequest request) { var response = await _httpClient.PostAsJsonAsync($"{_baseUrl}api/stations", request); if (response.StatusCode == System.Net.HttpStatusCode.BadRequest) { var errorContent = await response.Content.ReadAsStringAsync(); throw new InvalidOperationException($"Failed to create station: {errorContent}"); } response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync() ?? throw new Exception("Failed to create station"); } /// /// Update an existing station /// public async Task UpdateStationAsync(Guid stationId, UpdateStationRequest request) { var response = await _httpClient.PutAsJsonAsync($"{_baseUrl}api/stations/{stationId}", request); if (response.StatusCode == System.Net.HttpStatusCode.NotFound) { var errorContent = await response.Content.ReadAsStringAsync(); throw new InvalidOperationException($"Station not found: {errorContent}"); } if (response.StatusCode == System.Net.HttpStatusCode.BadRequest) { var errorContent = await response.Content.ReadAsStringAsync(); throw new InvalidOperationException($"Failed to update station: {errorContent}"); } response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync() ?? throw new Exception("Failed to update station"); } /// /// Delete a station /// public async Task DeleteStationAsync(Guid stationId) { var response = await _httpClient.DeleteAsync($"{_baseUrl}api/stations/{stationId}"); if (response.StatusCode == System.Net.HttpStatusCode.NotFound) { var errorContent = await response.Content.ReadAsStringAsync(); throw new InvalidOperationException($"Station not found: {errorContent}"); } response.EnsureSuccessStatusCode(); } }