# 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
- `GetInventoryAsync(TileInventoryRequest request, CancellationToken) → Task<TileInventoryResponse>` (AZ-505): bulk per-cell metadata read for `POST /api/satellite/tiles/inventory`. Steps:
  1. Project the request to an ordered `Guid[]` of `location_hash` values — either by computing `Uuidv5.LocationHashForTile(z, x, y)` per entry (Form A `request.Tiles`) or by echoing the caller-supplied hashes (Form B `request.LocationHashes`). The request-order vector is retained so step 3 can shape the response in input order.
  2. Call `ITileRepository.GetTilesByLocationHashesAsync(hashes, CancellationToken)` once. The repository returns the most-recent row per hash (`(captured_at DESC, updated_at DESC, id DESC) LIMIT 1` per `location_hash`); this is the AZ-484 / AZ-503-foundation selection rule preserved at the bulk layer.
  3. Build a `TileInventoryEntry[]` of the same length as the input vector. For each request slot: emit `Present=false` (only `LocationHash` populated) when no row was returned for that hash; otherwise emit `Present=true` with `Id` / `CapturedAt` / `Source` / `FlightId` / `ResolutionMPerPx` populated from the row. Order matches the request — duplicate hashes in the request produce duplicate entries pointing at the same row (`tile-inventory.md` v1.0.0 Inv-2 / Inv-3).
  4. XOR validation (both populated / neither populated) and the 5000-entry cap are NOT enforced here — they live in the API handler (`GetTilesInventory` in `Program.cs`) so the HTTP-layer error contract is single-sourced and `ITileService.GetInventoryAsync` can be called from non-HTTP contexts without re-implementing the gate.

## 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.
- AZ-503: `BuildTileEntity` computes deterministic identity fields — `Id = Uuidv5.Create(Uuidv5.TileNamespace, "{z}/{x}/{y}/google_maps/00000000-0000-0000-0000-000000000000")`, `LocationHash = Uuidv5.Create(Uuidv5.TileNamespace, "{z}/{x}/{y}")`, `ContentSha256 = SHA256.HashData(<jpeg bytes from disk>)`. `FlightId` is always `null` for Google Maps tiles. No `Guid.NewGuid()` remains on this path.
- `MapToMetadata(TileEntity) → TileMetadata`: entity-to-DTO mapping (static helper); `MapsVersion` is no longer projected onto `TileMetadata` / `DownloadTileResponse`. `Source`, `CapturedAt`, `FlightId`, `LocationHash`, `ContentSha256` are not currently projected to the public DTO (no API contract change observable for AZ-484 or AZ-503).
- `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.Common.Utils.Uuidv5` (AZ-503) — deterministic UUIDv5 generator + `TileNamespace` constant
- `System.Security.Cryptography.SHA256` (AZ-503) — content digest
- `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.
