""" Vehicle API — Query vehicle events and identities. See where a plate has been seen across all cameras and when. """ from fastapi import APIRouter, Depends, Query from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, desc from pydantic import BaseModel from typing import Optional from uuid import UUID from datetime import datetime from app.core.database import get_db from app.models.vehicle import VehicleEvent, VehicleIdentity router = APIRouter(prefix="/vehicles", tags=["vehicles"]) class VehicleIdentityResponse(BaseModel): id: UUID plate_number: Optional[str] vehicle_type: str color: Optional[str] label: Optional[str] is_whitelisted: int is_blacklisted: int first_seen_at: Optional[datetime] last_seen_at: Optional[datetime] total_sightings: int class Config: from_attributes = True class VehicleEventResponse(BaseModel): id: UUID camera_id: UUID plate_number: Optional[str] vehicle_type: str color: Optional[str] person_count: Optional[int] detection_confidence: Optional[float] snapshot_path: Optional[str] plate_snapshot_path: Optional[str] captured_at: datetime class Config: from_attributes = True @router.get("/identities", response_model=list[VehicleIdentityResponse]) async def list_vehicle_identities( plate: Optional[str] = Query(None), vehicle_type: Optional[str] = Query(None), limit: int = Query(50, le=200), db: AsyncSession = Depends(get_db), ): q = select(VehicleIdentity).order_by(desc(VehicleIdentity.last_seen_at)).limit(limit) if plate: q = q.where(VehicleIdentity.plate_number.ilike(f"%{plate}%")) if vehicle_type: q = q.where(VehicleIdentity.vehicle_type == vehicle_type) result = await db.execute(q) return result.scalars().all() @router.get("/identities/{identity_id}/trail") async def vehicle_trail(identity_id: UUID, db: AsyncSession = Depends(get_db)): """ Get full sighting trail for a vehicle — every camera it was seen on and when. """ result = await db.execute( select(VehicleEvent) .where(VehicleEvent.identity_id == identity_id) .order_by(desc(VehicleEvent.captured_at)) .limit(500) ) events = result.scalars().all() return { "identity_id": str(identity_id), "total_sightings": len(events), "trail": [ { "camera_id": str(e.camera_id), "captured_at": e.captured_at, "plate_number": e.plate_number, "color": e.color, "vehicle_type": e.vehicle_type, "person_count": e.person_count, "snapshot_path": e.snapshot_path, } for e in events ] } @router.get("/events", response_model=list[VehicleEventResponse]) async def list_vehicle_events( camera_id: Optional[UUID] = Query(None), plate: Optional[str] = Query(None), limit: int = Query(100, le=500), db: AsyncSession = Depends(get_db), ): q = select(VehicleEvent).order_by(desc(VehicleEvent.captured_at)).limit(limit) if camera_id: q = q.where(VehicleEvent.camera_id == camera_id) if plate: q = q.where(VehicleEvent.plate_number.ilike(f"%{plate}%")) result = await db.execute(q) return result.scalars().all() @router.patch("/identities/{identity_id}") async def update_vehicle_identity( identity_id: UUID, label: Optional[str] = None, is_whitelisted: Optional[int] = None, is_blacklisted: Optional[int] = None, notes: Optional[str] = None, db: AsyncSession = Depends(get_db), ): """Label a vehicle — mark as resident, delivery, blacklisted, etc.""" result = await db.execute(select(VehicleIdentity).where(VehicleIdentity.id == identity_id)) identity = result.scalar_one_or_none() if not identity: from fastapi import HTTPException raise HTTPException(status_code=404, detail="Vehicle identity not found") if label is not None: identity.label = label if is_whitelisted is not None: identity.is_whitelisted = is_whitelisted if is_blacklisted is not None: identity.is_blacklisted = is_blacklisted if notes is not None: identity.notes = notes await db.commit() return {"message": "Updated"}