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; public class RegionService : IRegionService { private readonly IRegionRepository _regionRepository; private readonly IRegionRequestQueue _queue; private readonly ITileService _tileService; private readonly StorageConfig _storageConfig; private readonly ILogger _logger; public RegionService( IRegionRepository regionRepository, IRegionRequestQueue queue, ITileService tileService, IOptions storageConfig, ILogger logger) { _regionRepository = regionRepository; _queue = queue; _tileService = tileService; _storageConfig = storageConfig.Value; _logger = logger; } public async Task RequestRegionAsync(Guid id, double latitude, double longitude, double sizeMeters, int zoomLevel) { var now = DateTime.UtcNow; var region = new RegionEntity { Id = id, Latitude = latitude, Longitude = longitude, SizeMeters = sizeMeters, ZoomLevel = zoomLevel, Status = "queued", TilesDownloaded = 0, TilesReused = 0, CreatedAt = now, UpdatedAt = now }; await _regionRepository.InsertAsync(region); var request = new RegionRequest { Id = id, Latitude = latitude, Longitude = longitude, SizeMeters = sizeMeters, ZoomLevel = zoomLevel }; await _queue.EnqueueAsync(request); _logger.LogInformation("Region {RegionId} queued for processing", id); return MapToStatus(region); } public async Task GetRegionStatusAsync(Guid id) { var region = await _regionRepository.GetByIdAsync(id); return region != null ? MapToStatus(region) : null; } public async Task ProcessRegionAsync(Guid id, CancellationToken cancellationToken = default) { _logger.LogInformation("Processing region {RegionId}", id); var startTime = DateTime.UtcNow; var region = await _regionRepository.GetByIdAsync(id); if (region == null) { _logger.LogWarning("Region {RegionId} not found", id); return; } region.Status = "processing"; region.UpdatedAt = DateTime.UtcNow; await _regionRepository.UpdateAsync(region); try { _logger.LogInformation("Downloading tiles for region {RegionId} at ({Lat}, {Lon}) size {Size}m zoom {Zoom}", id, region.Latitude, region.Longitude, region.SizeMeters, region.ZoomLevel); var processingStartTime = DateTime.UtcNow; _logger.LogInformation("Checking for existing tiles in region {RegionId}", id); var tilesBeforeDownload = await _tileService.GetTilesByRegionAsync( region.Latitude, region.Longitude, region.SizeMeters, region.ZoomLevel); var existingTileIds = new HashSet(tilesBeforeDownload.Select(t => t.Id)); _logger.LogInformation("Found {Count} existing tiles for region {RegionId}", existingTileIds.Count, id); _logger.LogInformation("Starting tile download for region {RegionId}", id); var tiles = await _tileService.DownloadAndStoreTilesAsync( region.Latitude, region.Longitude, region.SizeMeters, region.ZoomLevel, cancellationToken); var tilesDownloaded = tiles.Count(t => !existingTileIds.Contains(t.Id)); var tilesReused = tiles.Count(t => existingTileIds.Contains(t.Id)); _logger.LogInformation("Region {RegionId}: Downloaded {Downloaded} tiles, Reused {Reused} tiles", id, tilesDownloaded, tilesReused); var readyDir = _storageConfig.ReadyDirectory; Directory.CreateDirectory(readyDir); var csvPath = Path.Combine(readyDir, $"region_{id}_ready.csv"); var summaryPath = Path.Combine(readyDir, $"region_{id}_summary.txt"); await GenerateCsvFileAsync(csvPath, tiles, cancellationToken); await GenerateSummaryFileAsync(summaryPath, id, region, tiles, tilesDownloaded, tilesReused, processingStartTime, cancellationToken); region.Status = "completed"; region.CsvFilePath = csvPath; region.SummaryFilePath = summaryPath; region.TilesDownloaded = tilesDownloaded; region.TilesReused = tilesReused; region.UpdatedAt = DateTime.UtcNow; await _regionRepository.UpdateAsync(region); var duration = (DateTime.UtcNow - startTime).TotalSeconds; _logger.LogInformation("Region {RegionId} processing completed in {Duration:F2}s", id, duration); } catch (Exception ex) { _logger.LogError(ex, "Failed to process region {RegionId}: {Message}", id, ex.Message); region.Status = "failed"; region.UpdatedAt = DateTime.UtcNow; await _regionRepository.UpdateAsync(region); } } private async Task GenerateCsvFileAsync(string filePath, List tiles, CancellationToken cancellationToken) { var orderedTiles = tiles.OrderByDescending(t => t.Latitude).ThenBy(t => t.Longitude).ToList(); using var writer = new StreamWriter(filePath); await writer.WriteLineAsync("latitude,longitude,file_path"); foreach (var tile in orderedTiles) { await writer.WriteLineAsync($"{tile.Latitude:F6},{tile.Longitude:F6},{tile.FilePath}"); } } private async Task GenerateSummaryFileAsync( string filePath, Guid regionId, RegionEntity region, List tiles, int tilesDownloaded, int tilesReused, DateTime startTime, CancellationToken cancellationToken) { var endTime = DateTime.UtcNow; var processingTime = (endTime - startTime).TotalSeconds; var summary = new System.Text.StringBuilder(); summary.AppendLine("Region Processing Summary"); summary.AppendLine("========================"); summary.AppendLine($"Region ID: {regionId}"); summary.AppendLine($"Center: {region.Latitude:F6}, {region.Longitude:F6}"); summary.AppendLine($"Size: {region.SizeMeters:F0} meters"); summary.AppendLine($"Zoom Level: {region.ZoomLevel}"); summary.AppendLine(); summary.AppendLine("Processing Statistics:"); summary.AppendLine($"- Tiles Downloaded: {tilesDownloaded}"); summary.AppendLine($"- Tiles Reused from Cache: {tilesReused}"); summary.AppendLine($"- Total Tiles: {tiles.Count}"); summary.AppendLine($"- Processing Time: {processingTime:F2} seconds"); summary.AppendLine($"- Started: {startTime:yyyy-MM-dd HH:mm:ss} UTC"); summary.AppendLine($"- Completed: {endTime:yyyy-MM-dd HH:mm:ss} UTC"); summary.AppendLine(); summary.AppendLine("Files Created:"); summary.AppendLine($"- CSV: {Path.GetFileName(filePath).Replace("_summary.txt", "_ready.csv")}"); summary.AppendLine($"- Summary: {Path.GetFileName(filePath)}"); await File.WriteAllTextAsync(filePath, summary.ToString(), cancellationToken); } private static RegionStatus MapToStatus(RegionEntity region) { return new RegionStatus { Id = region.Id, Status = region.Status, CsvFilePath = region.CsvFilePath, SummaryFilePath = region.SummaryFilePath, TilesDownloaded = region.TilesDownloaded, TilesReused = region.TilesReused, CreatedAt = region.CreatedAt, UpdatedAt = region.UpdatedAt }; } }