# Module: Services/TileService

## Purpose
Orchestrates tile downloading and persistence. Bridges the downloader (Google Maps) with the tile repository (PostgreSQL), handling in-memory caching, entity creation, and metadata mapping. Single ownership point for all tile read/write business logic — both region-batch and single-tile API endpoints route through this service.

**csproj**: `SatelliteProvider.Services.TileDownloader/TileService.cs`

## Public Interface

### TileService (implements ITileService)
- `DownloadAndStoreTilesAsync(double lat, double lon, double sizeMeters, int zoomLevel, CancellationToken) → Task<List<TileMetadata>>`:
  1. Queries existing tiles in the region from the repository — most-recent across sources per `(latitude, longitude, tile_zoom, tile_size_meters)` (AZ-484 selection rule applied by `TileRepository.GetTilesByRegionAsync` via `DISTINCT ON`)
  2. Calls `ISatelliteDownloader.GetTilesWithMetadataAsync` with existing tiles to skip
  3. Creates `TileEntity` for each newly downloaded tile and inserts via repository (per-source UPSERT keyed on `(latitude, longitude, tile_zoom, tile_size_meters, source)`)
  4. Returns combined list of existing + new tile metadata
- `GetTileAsync(Guid id) → Task<TileMetadata?>`: single tile lookup
- `GetTilesByRegionAsync(double lat, double lon, double sizeMeters, int zoomLevel) → Task<IEnumerable<TileMetadata>>`: query tiles in a region
- `GetOrDownloadTileAsync(int z, int x, int y, CancellationToken) → Task<TileBytes>` (AZ-310): cache → repository → downloader fallback for single Z/X/Y serving
- `DownloadAndStoreSingleTileAsync(double latitude, double longitude, int zoomLevel, CancellationToken) → Task<TileMetadata>` (AZ-311): download one tile by lat/lon, persist, return metadata

## Internal Logic
- New rows write `Version = null` and `MapsVersion = null` (post-AZ-357 / AZ-373); the `version` and `maps_version` columns are retained for backward compatibility with pre-existing rows
- AZ-484: `BuildTileEntity` stamps every newly downloaded row with `Source = TileSourceConverter.ToWireValue(TileSource.GoogleMaps)` (wire value `"google_maps"`) and `CapturedAt = DateTime.UtcNow`. The Google Maps download path is the only producer of `'google_maps'` rows; UAV ingestion (separate task) is the only producer of `'uav'` rows.
- `MapToMetadata(TileEntity) → TileMetadata`: entity-to-DTO mapping (static helper); `MapsVersion` is no longer projected onto `TileMetadata` / `DownloadTileResponse`. `Source` and `CapturedAt` are not currently projected to the public DTO (no API contract change observable for AZ-484).
- `TileSizePixels` sourced from `MapConfig.TileSizePixels` (default 256, post-AZ-371); image type fixed at `"jpg"`
- `IMemoryCache` keyed by `(z, x, y)` with 1h absolute / 30min sliding expiration; populated on first hit and on downloader fallback

## Dependencies
- `ISatelliteDownloader` (resolved via DI; concrete is `GoogleMapsDownloaderV2`)
- `ITileRepository`
- `IMemoryCache` (registered by `AddTileDownloader()`)
- `SatelliteProvider.Common.DTO` — GeoPoint, TileMetadata, TileBytes
- `SatelliteProvider.Common.Enums` — `TileSource`, `TileSourceConverter` (AZ-484)
- `SatelliteProvider.DataAccess.Models` — TileEntity

## Consumers
- `RegionService.ProcessRegionAsync` — downloads and retrieves tiles for a region

## Data Models
Transforms between `TileEntity` (persistence) and `TileMetadata` (DTO).

## Configuration
None directly; relies on `GoogleMapsDownloaderV2`'s configuration.

## External Integrations
Indirect: Google Maps (via downloader), PostgreSQL (via repository).

## Security
None.

## Tests
No dedicated tests.
