from datetime import date from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, Request, status from sqlalchemy.orm import Session from app.api.deps import get_current_operator_user, get_current_read_user, get_db from app.core.email import email_service from app.core.config import settings from app.core.frz import swansea_frz_geojson from app.core.utils import get_client_ip from app.crud.crud_drone_request import drone_request as crud_drone_request from app.crud.crud_journal import journal as crud_journal from app.models.journal import EntityType from app.models.ppr import User from app.schemas.drone_request import ( DroneRequest, DroneRequestComment, DroneRequestCreate, DroneRequestPublicSubmission, DroneRequestStatus, DroneRequestStatusUpdate, DroneRequestUpdate, ) from app.schemas.journal import JournalEntryResponse router = APIRouter() async def _broadcast(request: Request, event_type: str, drone_request: DroneRequest): if hasattr(request.app.state, "connection_manager"): await request.app.state.connection_manager.broadcast({ "type": event_type, "data": { "id": drone_request.id, "reference_number": drone_request.reference_number, "status": drone_request.status.value, }, }) async def _send_drone_email(drone_request, subject: str, message: str): await email_service.send_email( to_email=drone_request.email, subject=subject, template_name="drone_request_update.html", template_vars={ "name": drone_request.operator_name, "reference_number": drone_request.reference_number, "status": drone_request.status.value, "message": message, "takeoff_time": drone_request.estimated_takeoff_at.strftime("%Y-%m-%d %H:%M"), "completion_time": drone_request.estimated_completion_at.strftime("%Y-%m-%d %H:%M"), "location": drone_request.location_description or f"{drone_request.location_latitude}, {drone_request.location_longitude}", "maximum_elevation_ft_agl": drone_request.maximum_elevation_ft_agl, "edit_url": f"{settings.base_url}/drone-request.html?token={drone_request.public_token}" if drone_request.public_token else None, }, ) async def _send_drone_submitted_email(drone_request): await email_service.send_email( to_email=drone_request.email, subject=f"Drone flight request received {drone_request.reference_number}", template_name="drone_request_submitted.html", template_vars={ "name": drone_request.operator_name, "reference_number": drone_request.reference_number, "status": drone_request.status.value, "takeoff_time": drone_request.estimated_takeoff_at.strftime("%Y-%m-%d %H:%M"), "completion_time": drone_request.estimated_completion_at.strftime("%Y-%m-%d %H:%M"), "location": drone_request.location_description or f"{drone_request.location_latitude}, {drone_request.location_longitude}", "maximum_elevation_ft_agl": drone_request.maximum_elevation_ft_agl, "edit_url": f"{settings.base_url}/drone-request.html?token={drone_request.public_token}" if drone_request.public_token else None, }, ) async def _send_drone_tower_notification(drone_request): tower_email = settings.drone_request_tower_email or settings.mail_from await email_service.send_email( to_email=tower_email, subject=f"Drone flight request awaiting review {drone_request.reference_number}", template_name="drone_request_tower_notification.html", template_vars={ "reference_number": drone_request.reference_number, "operator_name": drone_request.operator_name, "operator_id": drone_request.operator_id, "flyer_name": drone_request.flyer_name, "flyer_id": drone_request.flyer_id, "email": drone_request.email, "phone": drone_request.phone, "takeoff_time": drone_request.estimated_takeoff_at.strftime("%Y-%m-%d %H:%M"), "completion_time": drone_request.estimated_completion_at.strftime("%Y-%m-%d %H:%M"), "location": drone_request.location_description or f"{drone_request.location_latitude}, {drone_request.location_longitude}", "maximum_elevation_ft_agl": drone_request.maximum_elevation_ft_agl, "inside_frz": "Yes" if drone_request.location_inside_frz else "No", "notes": drone_request.applicant_notes, "requests_url": f"{settings.base_url}/drone-requests", }, reply_to=f"{drone_request.operator_name} <{drone_request.email}>", ) async def _send_drone_approved_email(drone_request, message: Optional[str] = None): await email_service.send_email( to_email=drone_request.email, subject=f"Drone request {drone_request.reference_number} APPROVED", template_name="drone_request_approved.html", template_vars={ "name": drone_request.operator_name, "reference_number": drone_request.reference_number, "status": drone_request.status.value, "message": message, "takeoff_time": drone_request.estimated_takeoff_at.strftime("%Y-%m-%d %H:%M"), "completion_time": drone_request.estimated_completion_at.strftime("%Y-%m-%d %H:%M"), "location": drone_request.location_description or f"{drone_request.location_latitude}, {drone_request.location_longitude}", "maximum_elevation_ft_agl": drone_request.maximum_elevation_ft_agl, "edit_url": f"{settings.base_url}/drone-request.html?token={drone_request.public_token}" if drone_request.public_token else None, }, ) def _public_submission_response(drone_request): payload = DroneRequest.model_validate(drone_request, from_attributes=True).model_dump(mode="json") payload["request_id"] = drone_request.reference_number payload["secure_link"] = f"{settings.base_url}/drone-request.html?token={drone_request.public_token}" return payload @router.get("/", response_model=List[DroneRequest]) async def get_drone_requests( skip: int = 0, limit: int = 100, status: Optional[DroneRequestStatus] = None, date_from: Optional[date] = None, date_to: Optional[date] = None, db: Session = Depends(get_db), current_user: User = Depends(get_current_read_user), ): return crud_drone_request.get_multi( db, skip=skip, limit=limit, status=status, date_from=date_from, date_to=date_to, ) @router.post("/public", response_model=DroneRequestPublicSubmission) async def create_public_drone_request( request: Request, drone_request_in: DroneRequestCreate, db: Session = Depends(get_db), ): client_ip = get_client_ip(request) drone_request = crud_drone_request.create( db, obj_in=drone_request_in, created_by="public", user_ip=client_ip, submitted_via="PUBLIC", ) await _broadcast(request, "drone_request_created", drone_request) await _send_drone_submitted_email(drone_request) await _send_drone_tower_notification(drone_request) return _public_submission_response(drone_request) @router.get("/public/edit/{token}", response_model=DroneRequest) async def get_drone_request_for_edit( token: str, db: Session = Depends(get_db), ): drone_request = crud_drone_request.get_by_public_token(db, token) if not drone_request: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Invalid or expired token") return drone_request @router.patch("/public/edit/{token}", response_model=DroneRequest) async def update_drone_request_public( token: str, drone_request_in: DroneRequestUpdate, request: Request, db: Session = Depends(get_db), ): drone_request = crud_drone_request.get_by_public_token(db, token) if not drone_request: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Invalid or expired token") if drone_request.status not in [DroneRequestStatus.NEW, DroneRequestStatus.APPROVED]: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Drone request cannot be edited while {drone_request.status.value}", ) client_ip = get_client_ip(request) updated_request = crud_drone_request.update( db, db_obj=drone_request, obj_in=drone_request_in, user="public", user_ip=client_ip, ) await _broadcast(request, "drone_request_updated", updated_request) return updated_request @router.delete("/public/cancel/{token}", response_model=DroneRequest) async def cancel_drone_request_public( token: str, request: Request, db: Session = Depends(get_db), ): drone_request = crud_drone_request.get_by_public_token(db, token) if not drone_request: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Invalid or expired token") if drone_request.status not in [DroneRequestStatus.NEW, DroneRequestStatus.APPROVED]: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Drone request cannot be cancelled while {drone_request.status.value}", ) client_ip = get_client_ip(request) cancelled_request = crud_drone_request.update_status( db, request_id=drone_request.id, status=DroneRequestStatus.CANCELED, comment="Cancelled by operator using secure link", user="public", user_ip=client_ip, ) await _broadcast(request, "drone_request_status_update", cancelled_request) await _send_drone_email( cancelled_request, f"Drone request {cancelled_request.reference_number} CANCELED", "Your drone flight request has been cancelled.", ) return cancelled_request @router.get("/frz") async def get_swansea_drone_frz(): return swansea_frz_geojson() @router.get("/{request_id}", response_model=DroneRequest) async def get_drone_request( request_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_read_user), ): drone_request = crud_drone_request.get(db, request_id) if not drone_request: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Drone request not found") return drone_request @router.patch("/{request_id}", response_model=DroneRequest) async def update_drone_request( request: Request, request_id: int, drone_request_in: DroneRequestUpdate, db: Session = Depends(get_db), current_user: User = Depends(get_current_operator_user), ): db_request = crud_drone_request.get(db, request_id) if not db_request: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Drone request not found") client_ip = get_client_ip(request) drone_request = crud_drone_request.update( db, db_obj=db_request, obj_in=drone_request_in, user=current_user.username, user_ip=client_ip, ) await _broadcast(request, "drone_request_updated", drone_request) return drone_request @router.patch("/{request_id}/status", response_model=DroneRequest) async def update_drone_request_status( request: Request, request_id: int, status_update: DroneRequestStatusUpdate, db: Session = Depends(get_db), current_user: User = Depends(get_current_operator_user), ): client_ip = get_client_ip(request) drone_request = crud_drone_request.update_status( db, request_id=request_id, status=status_update.status, comment=status_update.comment, user=current_user.username, user_ip=client_ip, ) if not drone_request: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Drone request not found") await _broadcast(request, "drone_request_status_update", drone_request) message = status_update.comment or f"Your drone flight request status is now {drone_request.status.value}." if drone_request.status == DroneRequestStatus.APPROVED: await _send_drone_approved_email(drone_request, status_update.comment) else: await _send_drone_email( drone_request, f"Drone request {drone_request.reference_number} {drone_request.status.value}", message, ) return drone_request @router.post("/{request_id}/comments", response_model=DroneRequest) async def add_drone_request_comment( request: Request, request_id: int, comment_in: DroneRequestComment, db: Session = Depends(get_db), current_user: User = Depends(get_current_operator_user), ): client_ip = get_client_ip(request) drone_request = crud_drone_request.add_comment( db, request_id=request_id, comment=comment_in.comment, user=current_user.username, user_ip=client_ip, ) if not drone_request: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Drone request not found") await _broadcast(request, "drone_request_comment_added", drone_request) if comment_in.email_applicant: await _send_drone_email( drone_request, f"Drone request {drone_request.reference_number} update", comment_in.comment, ) return drone_request @router.get("/{request_id}/journal", response_model=List[JournalEntryResponse]) async def get_drone_request_journal( request_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_read_user), ): drone_request = crud_drone_request.get(db, request_id) if not drone_request: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Drone request not found") return crud_journal.get_entity_journal(db, EntityType.DRONE_REQUEST, request_id)