"""REST API Endpoints for Flight Management.""" from __future__ import annotations import asyncio import json from typing import Annotated from fastapi import APIRouter, File, Form, HTTPException, Path, UploadFile from sse_starlette.sse import EventSourceResponse from gps_denied.api.deps import ProcessorDep, SessionDep from gps_denied.schemas.flight import ( BatchMetadata, BatchResponse, BatchUpdateResponse, DeleteResponse, FlightCreateRequest, FlightDetailResponse, FlightResponse, FlightStatusResponse, ObjectGPSResponse, ObjectToGPSRequest, UpdateResponse, UserFixRequest, UserFixResponse, Waypoint, ) router = APIRouter(prefix="/flights", tags=["flights"]) @router.post("", response_model=FlightResponse, status_code=201) async def create_flight( req: FlightCreateRequest, processor: ProcessorDep, session: SessionDep, ) -> FlightResponse: """Create a new flight and trigger prefetching.""" res = await processor.create_flight(req) await session.commit() return res @router.get("/{flight_id}", response_model=FlightDetailResponse) async def get_flight( flight_id: Annotated[str, Path(..., title="The ID of the flight")], processor: ProcessorDep, ) -> FlightDetailResponse: """Get complete flight information.""" res = await processor.get_flight(flight_id) if not res: raise HTTPException(status_code=404, detail="Flight not found") return res @router.delete("/{flight_id}", response_model=DeleteResponse) async def delete_flight( flight_id: Annotated[str, Path(...)], processor: ProcessorDep, session: SessionDep, ) -> DeleteResponse: """Delete a flight and all associated data.""" res = await processor.delete_flight(flight_id) if not res.deleted: raise HTTPException(status_code=404, detail="Flight not found") await session.commit() return res @router.put("/{flight_id}/waypoints/{waypoint_id}", response_model=UpdateResponse) async def update_waypoint( flight_id: Annotated[str, Path(...)], waypoint_id: Annotated[str, Path(...)], waypoint: Waypoint, processor: ProcessorDep, session: SessionDep, ) -> UpdateResponse: """Update a specific waypoint.""" res = await processor.update_waypoint(flight_id, waypoint_id, waypoint) if not res.updated: raise HTTPException(status_code=404, detail="Waypoint or Flight not found") await session.commit() return res @router.put("/{flight_id}/waypoints/batch", response_model=BatchUpdateResponse) async def batch_update_waypoints( flight_id: Annotated[str, Path(...)], waypoints: list[Waypoint], processor: ProcessorDep, session: SessionDep, ) -> BatchUpdateResponse: """Batch update multiple waypoints.""" res = await processor.batch_update_waypoints(flight_id, waypoints) await session.commit() return res @router.post("/{flight_id}/images/batch", response_model=BatchResponse, status_code=202) async def upload_image_batch( flight_id: Annotated[str, Path(...)], metadata: Annotated[str, Form(...)], images: list[UploadFile] = File(...), processor: ProcessorDep = None, # type: ignore session: SessionDep = None, # type: ignore ) -> BatchResponse: """Upload a batch of UAV images.""" try: meta_dict = json.loads(metadata) meta_obj = BatchMetadata(**meta_dict) except Exception as e: raise HTTPException(status_code=400, detail=f"Invalid metadata JSON: {e}") f_info = await processor.get_flight(flight_id) if not f_info: raise HTTPException(status_code=404, detail="Flight not found") # P1#6: Batch size validation (allow 1-50 for dev, spec says 10-50) if len(images) < 1 or len(images) > 50: raise HTTPException( status_code=422, detail=f"Batch must contain 1-50 images, got {len(images)}", ) res = await processor.queue_images(flight_id, meta_obj, len(images)) # P1#5: Spawn background task to process each frame import cv2 import numpy as np async def _process_batch(): for idx, upload in enumerate(images): raw = await upload.read() arr = np.frombuffer(raw, dtype=np.uint8) img = cv2.imdecode(arr, cv2.IMREAD_COLOR) if img is not None: frame_id = meta_obj.start_frame_id + idx await processor.process_frame(flight_id, frame_id, img) asyncio.create_task(_process_batch()) await session.commit() return res @router.post("/{flight_id}/user-fix", response_model=UserFixResponse) async def submit_user_fix( flight_id: Annotated[str, Path(...)], fix_data: UserFixRequest, processor: ProcessorDep, session: SessionDep, ) -> UserFixResponse: """Submit a verified GPS anchor to unblock processing.""" res = await processor.handle_user_fix(flight_id, fix_data) await session.commit() return res @router.post("/{flight_id}/frames/{frame_id}/object-to-gps", response_model=ObjectGPSResponse) async def convert_object_to_gps( flight_id: Annotated[str, Path(...)], frame_id: Annotated[int, Path(...)], req: ObjectToGPSRequest, processor: ProcessorDep, ) -> ObjectGPSResponse: """Convert a pixel coordinate to GPS coordinate for an object.""" return await processor.convert_object_to_gps(flight_id, frame_id, (req.pixel_x, req.pixel_y)) @router.get("/{flight_id}/status", response_model=FlightStatusResponse) async def get_flight_status( flight_id: Annotated[str, Path(...)], processor: ProcessorDep, ) -> FlightStatusResponse: """Get processing status of a flight.""" res = await processor.get_flight_status(flight_id) if not res: raise HTTPException(status_code=404, detail="Flight not found") return res @router.get("/{flight_id}/stream") async def create_sse_stream( flight_id: Annotated[str, Path(...)], processor: ProcessorDep, ) -> EventSourceResponse: """SSE endpoint for real-time processing events.""" f_info = await processor.get_flight(flight_id) if not f_info: raise HTTPException(status_code=404, detail="Flight not found") return EventSourceResponse(processor.stream_events(flight_id, client_id="default"))