"""Core Flight Processor (Dummy / Stub for Stage 3).""" from __future__ import annotations import asyncio from datetime import datetime, timezone from gps_denied.core.pipeline import ImageInputPipeline from gps_denied.core.results import ResultManager from gps_denied.core.sse import SSEEventStreamer from gps_denied.db.repository import FlightRepository from gps_denied.schemas import GPSPoint from gps_denied.schemas.flight import ( BatchMetadata, BatchResponse, BatchUpdateResponse, DeleteResponse, FlightCreateRequest, FlightDetailResponse, FlightResponse, FlightStatusResponse, ObjectGPSResponse, UpdateResponse, UserFixRequest, UserFixResponse, Waypoint, ) from gps_denied.schemas.image import ImageBatch class FlightProcessor: """Manages business logic and background processing for flights.""" def __init__(self, repository: FlightRepository, streamer: SSEEventStreamer) -> None: self.repository = repository self.streamer = streamer self.result_manager = ResultManager(repository, streamer) self.pipeline = ImageInputPipeline(storage_dir=".image_storage", max_queue_size=50) async def create_flight(self, req: FlightCreateRequest) -> FlightResponse: flight = await self.repository.insert_flight( name=req.name, description=req.description, start_lat=req.start_gps.lat, start_lon=req.start_gps.lon, altitude=req.altitude, camera_params=req.camera_params.model_dump(), ) for poly in req.geofences.polygons: await self.repository.insert_geofence( flight.id, nw_lat=poly.north_west.lat, nw_lon=poly.north_west.lon, se_lat=poly.south_east.lat, se_lon=poly.south_east.lon, ) for w in req.rough_waypoints: await self.repository.insert_waypoint(flight.id, lat=w.lat, lon=w.lon) return FlightResponse( flight_id=flight.id, status="prefetching", message="Flight created and prefetching started.", created_at=flight.created_at, ) async def get_flight(self, flight_id: str) -> FlightDetailResponse | None: flight = await self.repository.get_flight(flight_id) if not flight: return None wps = await self.repository.get_waypoints(flight_id) state = await self.repository.load_flight_state(flight_id) waypoints = [ Waypoint( id=w.id, lat=w.lat, lon=w.lon, altitude=w.altitude, confidence=w.confidence, timestamp=w.timestamp, refined=w.refined, ) for w in wps ] status = state.status if state else "unknown" frames_processed = state.frames_processed if state else 0 frames_total = state.frames_total if state else 0 # Assuming empty geofences for now unless loaded (omitted for brevity) from gps_denied.schemas import Geofences return FlightDetailResponse( flight_id=flight.id, name=flight.name, description=flight.description, start_gps=GPSPoint(lat=flight.start_lat, lon=flight.start_lon), waypoints=waypoints, geofences=Geofences(polygons=[]), camera_params=flight.camera_params, altitude=flight.altitude, status=status, frames_processed=frames_processed, frames_total=frames_total, created_at=flight.created_at, updated_at=flight.updated_at, ) async def delete_flight(self, flight_id: str) -> DeleteResponse: deleted = await self.repository.delete_flight(flight_id) return DeleteResponse(deleted=deleted, flight_id=flight_id) async def update_waypoint( self, flight_id: str, waypoint_id: str, waypoint: Waypoint ) -> UpdateResponse: ok = await self.repository.update_waypoint( flight_id, waypoint_id, lat=waypoint.lat, lon=waypoint.lon, altitude=waypoint.altitude, confidence=waypoint.confidence, refined=waypoint.refined, ) return UpdateResponse(updated=ok, waypoint_id=waypoint_id) async def batch_update_waypoints( self, flight_id: str, waypoints: list[Waypoint] ) -> BatchUpdateResponse: failed = [] updated = 0 for wp in waypoints: ok = await self.repository.update_waypoint( flight_id, wp.id, lat=wp.lat, lon=wp.lon, altitude=wp.altitude, confidence=wp.confidence, refined=wp.refined, ) if ok: updated += 1 else: failed.append(wp.id) return BatchUpdateResponse(success=(len(failed) == 0), updated_count=updated, failed_ids=failed) async def queue_images( self, flight_id: str, metadata: BatchMetadata, file_count: int ) -> BatchResponse: state = await self.repository.load_flight_state(flight_id) if state: total = state.frames_total + file_count await self.repository.save_flight_state(flight_id, frames_total=total, status="processing") next_seq = metadata.end_sequence + 1 seqs = list(range(metadata.start_sequence, metadata.end_sequence + 1)) return BatchResponse( accepted=True, sequences=seqs, next_expected=next_seq, message=f"Queued {file_count} images.", ) async def handle_user_fix(self, flight_id: str, req: UserFixRequest) -> UserFixResponse: await self.repository.save_flight_state(flight_id, blocked=False, status="processing") return UserFixResponse( accepted=True, processing_resumed=True, message="Fix applied." ) async def get_flight_status(self, flight_id: str) -> FlightStatusResponse | None: state = await self.repository.load_flight_state(flight_id) if not state: return None return FlightStatusResponse( status=state.status, frames_processed=state.frames_processed, frames_total=state.frames_total, current_frame=state.current_frame, current_heading=None, # would load from latest blocked=state.blocked, search_grid_size=state.search_grid_size, created_at=state.created_at, updated_at=state.updated_at, ) async def convert_object_to_gps( self, flight_id: str, frame_id: int, pixel: tuple[float, float] ) -> ObjectGPSResponse: # Dummy math return ObjectGPSResponse( gps=GPSPoint(lat=48.0, lon=37.0), accuracy_meters=5.0, frame_id=frame_id, pixel=pixel, ) async def stream_events(self, flight_id: str, client_id: str): """Async generator for SSE stream.""" # Yield from the real SSE streamer generator async for event in self.streamer.stream_generator(flight_id, client_id): yield event