from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, status, Request from sqlalchemy.orm import Session from datetime import date from app.api.deps import get_db, get_current_read_user, get_current_operator_user from app.crud.crud_ppr import ppr as crud_ppr from app.crud.crud_journal import journal as crud_journal from app.crud.crud_arrival import arrival as crud_arrival from app.crud.crud_departure import departure as crud_departure from app.schemas.ppr import PPR, PPRCreate, PPRUpdate, PPRStatus, PPRStatusUpdate, Journal from app.schemas.arrival import ArrivalCreate from app.schemas.departure import DepartureCreate from app.models.ppr import User from app.core.utils import get_client_ip from app.core.email import email_service from app.core.config import settings router = APIRouter() @router.get("/", response_model=List[PPR]) async def get_pprs( request: Request, skip: int = 0, limit: int = 100, status: Optional[PPRStatus] = None, date_from: Optional[date] = None, date_to: Optional[date] = None, db: Session = Depends(get_db), current_user: User = Depends(get_current_read_user) ): """Get PPR records with optional filtering""" pprs = crud_ppr.get_multi( db, skip=skip, limit=limit, status=status, date_from=date_from, date_to=date_to ) return pprs @router.post("/", response_model=PPR) async def create_ppr( request: Request, ppr_in: PPRCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_operator_user) ): """Create a new PPR record""" client_ip = get_client_ip(request) ppr = crud_ppr.create(db, obj_in=ppr_in, created_by=current_user.username, user_ip=client_ip) # Send real-time update via WebSocket if hasattr(request.app.state, 'connection_manager'): await request.app.state.connection_manager.broadcast({ "type": "ppr_created", "data": { "id": ppr.id, "ac_reg": ppr.ac_reg, "status": ppr.status.value } }) return ppr @router.post("/public", response_model=PPR) async def create_public_ppr( request: Request, ppr_in: PPRCreate, db: Session = Depends(get_db) ): """Create a new PPR record (public endpoint, no authentication required)""" client_ip = get_client_ip(request) # For public submissions, use a default created_by or None ppr = crud_ppr.create(db, obj_in=ppr_in, created_by="public", user_ip=client_ip) # Send real-time update via WebSocket if hasattr(request.app.state, 'connection_manager'): await request.app.state.connection_manager.broadcast({ "type": "ppr_created", "data": { "id": ppr.id, "ac_reg": ppr.ac_reg, "status": ppr.status.value } }) # Send email if email provided if ppr_in.email: await email_service.send_email( to_email=ppr_in.email, subject="PPR Submitted Successfully", template_name="ppr_submitted.html", template_vars={ "name": ppr_in.captain, "aircraft": ppr_in.ac_reg, "arrival_time": ppr_in.eta.strftime("%Y-%m-%d %H:%M"), "departure_time": ppr_in.etd.strftime("%Y-%m-%d %H:%M") if ppr_in.etd else "N/A", "purpose": ppr_in.notes or "N/A", "public_token": ppr.public_token, "base_url": settings.base_url } ) return ppr @router.get("/{ppr_id}", response_model=PPR) async def get_ppr( ppr_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_read_user) ): """Get a specific PPR record""" ppr = crud_ppr.get(db, ppr_id=ppr_id) if not ppr: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="PPR record not found" ) return ppr @router.put("/{ppr_id}", response_model=PPR) async def update_ppr( request: Request, ppr_id: int, ppr_in: PPRUpdate, db: Session = Depends(get_db), current_user: User = Depends(get_current_operator_user) ): """Update a PPR record""" db_ppr = crud_ppr.get(db, ppr_id=ppr_id) if not db_ppr: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="PPR record not found" ) client_ip = get_client_ip(request) ppr = crud_ppr.update(db, db_obj=db_ppr, obj_in=ppr_in, user=current_user.username, user_ip=client_ip) # Send real-time update if hasattr(request.app.state, 'connection_manager'): await request.app.state.connection_manager.broadcast({ "type": "ppr_updated", "data": { "id": ppr.id, "ac_reg": ppr.ac_reg, "status": ppr.status.value } }) return ppr @router.patch("/{ppr_id}", response_model=PPR) async def patch_ppr( request: Request, ppr_id: int, ppr_in: PPRUpdate, db: Session = Depends(get_db), current_user: User = Depends(get_current_operator_user) ): """Partially update a PPR record (only provided fields will be updated)""" db_ppr = crud_ppr.get(db, ppr_id=ppr_id) if not db_ppr: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="PPR record not found" ) client_ip = get_client_ip(request) ppr = crud_ppr.update(db, db_obj=db_ppr, obj_in=ppr_in, user=current_user.username, user_ip=client_ip) # Send real-time update if hasattr(request.app.state, 'connection_manager'): await request.app.state.connection_manager.broadcast({ "type": "ppr_updated", "data": { "id": ppr.id, "ac_reg": ppr.ac_reg, "status": ppr.status.value } }) return ppr @router.patch("/{ppr_id}/status", response_model=PPR) async def update_ppr_status( request: Request, ppr_id: int, status_update: PPRStatusUpdate, db: Session = Depends(get_db), current_user: User = Depends(get_current_operator_user) ): """Update PPR status (LANDED, DEPARTED, etc.)""" client_ip = get_client_ip(request) ppr = crud_ppr.update_status( db, ppr_id=ppr_id, status=status_update.status, timestamp=status_update.timestamp, user=current_user.username, user_ip=client_ip ) if not ppr: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="PPR record not found" ) # Send real-time update if hasattr(request.app.state, 'connection_manager'): await request.app.state.connection_manager.broadcast({ "type": "status_update", "data": { "id": ppr.id, "ac_reg": ppr.ac_reg, "status": ppr.status.value, "timestamp": ppr.landed_dt.isoformat() if ppr.landed_dt else (ppr.departed_dt.isoformat() if ppr.departed_dt else None) } }) # Send email if cancelled and email provided if status_update.status == PPRStatus.CANCELED and ppr.email: await email_service.send_email( to_email=ppr.email, subject="PPR Cancelled", template_name="ppr_cancelled.html", template_vars={ "name": ppr.captain, "aircraft": ppr.ac_reg, "arrival_time": ppr.eta.strftime("%Y-%m-%d %H:%M"), "departure_time": ppr.etd.strftime("%Y-%m-%d %H:%M") if ppr.etd else "N/A" } ) return ppr @router.post("/{ppr_id}/acknowledge", response_model=PPR) async def acknowledge_ppr_strip( request: Request, ppr_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_operator_user) ): """Acknowledge that the operator has created the paper strip for a PPR.""" client_ip = get_client_ip(request) ppr = crud_ppr.acknowledge_strip( db, ppr_id=ppr_id, user=current_user.username, user_ip=client_ip ) if not ppr: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="PPR record not found" ) if hasattr(request.app.state, 'connection_manager'): await request.app.state.connection_manager.broadcast({ "type": "ppr_acknowledged", "data": { "id": ppr.id, "ac_reg": ppr.ac_reg, "acknowledged_dt": ppr.acknowledged_dt.isoformat() if ppr.acknowledged_dt else None, "acknowledged_by": ppr.acknowledged_by } }) return ppr @router.delete("/{ppr_id}", response_model=PPR) async def delete_ppr( request: Request, ppr_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_operator_user) ): """Delete (soft delete) a PPR record""" client_ip = get_client_ip(request) ppr = crud_ppr.delete(db, ppr_id=ppr_id, user=current_user.username, user_ip=client_ip) if not ppr: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="PPR record not found" ) # Send real-time update if hasattr(request.app.state, 'connection_manager'): await request.app.state.connection_manager.broadcast({ "type": "ppr_deleted", "data": { "id": ppr.id, "ac_reg": ppr.ac_reg } }) return ppr @router.get("/public/edit/{token}", response_model=PPR) async def get_ppr_for_edit( token: str, db: Session = Depends(get_db) ): """Get PPR details for public editing using token""" ppr = crud_ppr.get_by_public_token(db, token) if not ppr: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Invalid or expired token" ) # Only allow editing if not already processed if ppr.status in [PPRStatus.CANCELED, PPRStatus.DELETED, PPRStatus.LANDED, PPRStatus.DEPARTED]: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="PPR cannot be edited at this stage" ) return ppr @router.patch("/public/edit/{token}", response_model=PPR) async def update_ppr_public( token: str, ppr_in: PPRUpdate, request: Request, db: Session = Depends(get_db) ): """Update PPR publicly using token""" ppr = crud_ppr.get_by_public_token(db, token) if not ppr: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Invalid or expired token" ) # Only allow editing if not already processed if ppr.status in [PPRStatus.CANCELED, PPRStatus.DELETED, PPRStatus.LANDED, PPRStatus.DEPARTED]: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="PPR cannot be edited at this stage" ) client_ip = get_client_ip(request) updated_ppr = crud_ppr.update(db, db_obj=ppr, obj_in=ppr_in, user="public", user_ip=client_ip) return updated_ppr @router.delete("/public/cancel/{token}", response_model=PPR) async def cancel_ppr_public( token: str, request: Request, db: Session = Depends(get_db) ): """Cancel PPR publicly using token""" ppr = crud_ppr.get_by_public_token(db, token) if not ppr: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Invalid or expired token" ) # Only allow canceling if not already processed if ppr.status in [PPRStatus.CANCELED, PPRStatus.DELETED, PPRStatus.LANDED, PPRStatus.DEPARTED]: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="PPR cannot be cancelled at this stage" ) client_ip = get_client_ip(request) # Cancel by setting status to CANCELED cancelled_ppr = crud_ppr.update_status( db, ppr_id=ppr.id, status=PPRStatus.CANCELED, user="public", user_ip=client_ip ) # Send cancellation email if email provided if cancelled_ppr.email: await email_service.send_email( to_email=cancelled_ppr.email, subject="PPR Cancelled", template_name="ppr_cancelled.html", template_vars={ "name": cancelled_ppr.captain, "aircraft": cancelled_ppr.ac_reg, "arrival_time": cancelled_ppr.eta.strftime("%Y-%m-%d %H:%M"), "departure_time": cancelled_ppr.etd.strftime("%Y-%m-%d %H:%M") if cancelled_ppr.etd else "N/A" } ) return cancelled_ppr @router.get("/{ppr_id}/journal", response_model=List[Journal]) async def get_ppr_journal( ppr_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_read_user) ): """Get journal entries for a specific PPR""" # Verify PPR exists ppr = crud_ppr.get(db, ppr_id=ppr_id) if not ppr: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="PPR record not found" ) return crud_journal.get_by_ppr_id(db, ppr_id=ppr_id) @router.post("/{ppr_id}/activate") async def activate_ppr( request: Request, ppr_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_operator_user) ): """Activate a PPR by creating BOOKED_IN arrival and (if out_to set) BOOKED_OUT departure records.""" db_ppr = crud_ppr.get(db, ppr_id=ppr_id) if not db_ppr: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="PPR record not found") if db_ppr.status not in (PPRStatus.NEW, PPRStatus.CONFIRMED): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"PPR cannot be activated in its current state ({db_ppr.status.value})" ) client_ip = get_client_ip(request) username = current_user.username # Create INBOUND arrival (ADMIN submitted_via sets status to INBOUND) in_from = (db_ppr.in_from or "ZZZZ")[:4].upper() arrival_in = ArrivalCreate( registration=db_ppr.ac_reg, type=db_ppr.ac_type, callsign=db_ppr.ac_call, pob=db_ppr.pob_in, in_from=in_from, eta=db_ppr.eta, notes=db_ppr.notes, submitted_via="ADMIN" ) new_arrival = crud_arrival.create(db, obj_in=arrival_in, created_by=username, submitted_via="ADMIN", user_ip=client_ip) # Create PENDING departure linked to this arrival (only visible once arrival lands) new_departure = None if db_ppr.out_to: departure_in = DepartureCreate( registration=db_ppr.ac_reg, type=db_ppr.ac_type, callsign=db_ppr.ac_call, pob=db_ppr.pob_out if db_ppr.pob_out else db_ppr.pob_in, out_to=db_ppr.out_to, etd=db_ppr.etd, notes=db_ppr.notes, arrival_id=new_arrival.id, ) new_departure = crud_departure.create(db, obj_in=departure_in, created_by=username, submitted_via="ADMIN", user_ip=client_ip) # Mark PPR as ACTIVATED — removes it from Today's PPR and pending arrivals displays crud_ppr.update_status(db, ppr_id=ppr_id, status=PPRStatus.ACTIVATED, user=username, user_ip=client_ip) # Broadcast WebSocket update if hasattr(request.app.state, 'connection_manager'): await request.app.state.connection_manager.broadcast({ "type": "ppr_activated", "data": { "ppr_id": ppr_id, "arrival_id": new_arrival.id, "departure_id": new_departure.id if new_departure else None } }) return { "arrival_id": new_arrival.id, "departure_id": new_departure.id if new_departure else None, "message": ( f"PPR activated: arrival #{new_arrival.id} created" + (f", departure #{new_departure.id} queued (will appear when aircraft lands)" if new_departure else "") ) }