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