using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using SatelliteProvider.Common.Configs; using SatelliteProvider.Common.DTO; using SatelliteProvider.Common.Interfaces; using SatelliteProvider.DataAccess.Models; using SatelliteProvider.DataAccess.Repositories; namespace SatelliteProvider.Services.RouteManagement; public class RouteService : IRouteService { private readonly IRouteRepository _routeRepository; private readonly IRegionService _regionService; private readonly ILogger _logger; private readonly RouteValidator _validator; private readonly RoutePointGraphBuilder _pointGraphBuilder; private readonly GeofenceGridCalculator _geofenceGridCalculator; private readonly RouteResponseMapper _responseMapper; public RouteService( IRouteRepository routeRepository, IRegionService regionService, IOptions processingConfig, ILogger logger) : this(routeRepository, regionService, logger, new RouteValidator(processingConfig), new RoutePointGraphBuilder(processingConfig), new GeofenceGridCalculator(), new RouteResponseMapper()) { } public RouteService( IRouteRepository routeRepository, IRegionService regionService, ILogger logger, RouteValidator validator, RoutePointGraphBuilder pointGraphBuilder, GeofenceGridCalculator geofenceGridCalculator, RouteResponseMapper responseMapper) { _routeRepository = routeRepository; _regionService = regionService; _logger = logger; _validator = validator; _pointGraphBuilder = pointGraphBuilder; _geofenceGridCalculator = geofenceGridCalculator; _responseMapper = responseMapper; } public async Task CreateRouteAsync(CreateRouteRequest request) { ArgumentNullException.ThrowIfNull(request); // AZ-362: idempotent POST contract. A retried POST with the same caller-supplied // Id returns the existing route instead of re-running point generation and // re-queueing geofence regions. var existing = await GetRouteAsync(request.Id); if (existing != null) { _logger.LogInformation( "Idempotent route POST: id {RouteId} already exists; returning existing resource", request.Id); return existing; } _validator.Validate(request); var graph = _pointGraphBuilder.Build(request.Points); var now = DateTime.UtcNow; var routeEntity = new RouteEntity { Id = request.Id, Name = request.Name, Description = request.Description, RegionSizeMeters = request.RegionSizeMeters, ZoomLevel = request.ZoomLevel, TotalDistanceMeters = graph.TotalDistanceMeters, TotalPoints = graph.Points.Count, RequestMaps = request.RequestMaps, CreateTilesZip = request.CreateTilesZip, MapsReady = false, CreatedAt = now, UpdatedAt = now, }; await _routeRepository.InsertRouteAsync(routeEntity); var pointEntities = graph.Points.Select(p => new RoutePointEntity { Id = Guid.NewGuid(), RouteId = request.Id, SequenceNumber = p.SequenceNumber, Latitude = p.Latitude, Longitude = p.Longitude, PointType = p.PointType, SegmentIndex = p.SegmentIndex, DistanceFromPrevious = p.DistanceFromPrevious, CreatedAt = now, }).ToList(); await _routeRepository.InsertRoutePointsAsync(pointEntities); await ProcessGeofencePolygonsAsync(request); return _responseMapper.Map(routeEntity, graph.Points); } public async Task GetRouteAsync(Guid id) { var route = await _routeRepository.GetByIdAsync(id); if (route == null) { return null; } var points = await _routeRepository.GetRoutePointsAsync(id); return _responseMapper.Map(route, points); } private async Task ProcessGeofencePolygonsAsync(CreateRouteRequest request) { var polygons = request.Geofences?.Polygons; if (polygons is null || polygons.Count == 0) { return; } for (int polygonIndex = 0; polygonIndex < polygons.Count; polygonIndex++) { var polygon = polygons[polygonIndex]; // Validator (above) guarantees NorthWest/SouthEast are non-null and well-formed. var regions = _geofenceGridCalculator.GenerateRegions( polygon.NorthWest!, polygon.SouthEast!, request.RegionSizeMeters); foreach (var center in regions) { var geofenceRegionId = Guid.NewGuid(); await _regionService.RequestRegionAsync( geofenceRegionId, center.Lat, center.Lon, request.RegionSizeMeters, request.ZoomLevel, stitchTiles: false); await _routeRepository.LinkRouteToRegionAsync( request.Id, geofenceRegionId, isGeofence: true, geofencePolygonIndex: polygonIndex); } } } }