from __future__ import annotations from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy import func, or_, select from sqlalchemy.orm import Session from app.api.deps import get_current_user, get_db from app.models.booking import Booking from app.models.customer import Customer from app.models.data_quality import DataQualityIssue from app.models.inspection import Inspection from app.models.maintenance import MaintenanceRecord from app.models.vehicle import Vehicle from app.schemas import ( BookingSummaryOut, CurrentUser, DataQualityIssueOut, InspectionOut, MaintenanceOut, VehicleDetailOut, VehicleOut, VehiclePageOut, ) router = APIRouter(prefix="/api/v1/vehicles", tags=["vehicles"]) def _attention_vehicle_ids(db: Session) -> set: rows = db.scalars( select(DataQualityIssue.entity_id).where( DataQualityIssue.entity_type == "vehicle", DataQualityIssue.status == "open", ) ).all() return set(rows) @router.get("", response_model=list[VehicleOut] | VehiclePageOut) def list_vehicles( status: str | None = Query(default=None), attention_only: bool = Query(default=False), query: str | None = Query(default=None, min_length=1, max_length=100), page: int | None = Query(default=None, ge=1), page_size: int = Query(default=25, ge=1, le=25), db: Session = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ) -> list[VehicleOut] | VehiclePageOut: stmt = select(Vehicle).order_by(Vehicle.public_ref) if status: stmt = stmt.where(Vehicle.operational_status == status) if query: term = f"%{query.strip()}%" stmt = stmt.where( or_( Vehicle.public_ref.ilike(term), Vehicle.make.ilike(term), Vehicle.model.ilike(term), Vehicle.location.ilike(term), Vehicle.registration_number.ilike(term), ) ) attention_ids = _attention_vehicle_ids(db) if attention_only: stmt = stmt.where( or_(Vehicle.id.in_(attention_ids), Vehicle.operational_status == "blocked") ) total = db.scalar(select(func.count()).select_from(stmt.subquery())) or 0 page_number = page or 1 vehicles = db.scalars( stmt if page is None else stmt.offset((page_number - 1) * page_size).limit(page_size) ).all() items = [ VehicleOut( public_ref=v.public_ref, make=v.make, model=v.model, model_year=v.model_year, registration_number=v.registration_number, location=v.location, operational_status=v.operational_status, odometer_km=v.odometer_km, next_service_km=v.next_service_km, active=v.active, attention=v.id in attention_ids or v.operational_status == "blocked", ) for v in vehicles ] if page is None: return items total_pages = max(1, (total + page_size - 1) // page_size) return VehiclePageOut( items=items, page=min(page_number, total_pages), page_size=page_size, total=total, total_pages=total_pages, ) @router.get("/{public_ref}", response_model=VehicleDetailOut) def get_vehicle( public_ref: str, db: Session = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ) -> VehicleDetailOut: vehicle = db.scalar(select(Vehicle).where(Vehicle.public_ref == public_ref)) if vehicle is None: raise HTTPException(status_code=404, detail="Vehicle not found") bookings = db.scalars( select(Booking).where(Booking.vehicle_id == vehicle.id).order_by(Booking.starts_at.desc()) ).all() customer_ref_by_id = {c.id: c.public_ref for c in db.scalars(select(Customer)).all()} inspections = db.scalars( select(Inspection) .where(Inspection.vehicle_id == vehicle.id) .order_by(Inspection.completed_at.desc()) ).all() maintenance = db.scalars( select(MaintenanceRecord) .where(MaintenanceRecord.vehicle_id == vehicle.id) .order_by(MaintenanceRecord.occurred_at.desc()) ).all() issues = db.scalars( select(DataQualityIssue) .where(DataQualityIssue.entity_type == "vehicle", DataQualityIssue.entity_id == vehicle.id) .order_by(DataQualityIssue.detected_at.desc()) ).all() booking_by_id = {b.id: b.public_ref for b in bookings} attention_ids = _attention_vehicle_ids(db) return VehicleDetailOut( public_ref=vehicle.public_ref, make=vehicle.make, model=vehicle.model, model_year=vehicle.model_year, registration_number=vehicle.registration_number, location=vehicle.location, operational_status=vehicle.operational_status, odometer_km=vehicle.odometer_km, next_service_km=vehicle.next_service_km, active=vehicle.active, attention=vehicle.id in attention_ids or vehicle.operational_status == "blocked", bookings=[ BookingSummaryOut( public_ref=b.public_ref, customer_ref=customer_ref_by_id.get(b.customer_id, ""), vehicle_ref=vehicle.public_ref, starts_at=b.starts_at, ends_at=b.ends_at, status=b.status, ) for b in bookings ], inspections=[ InspectionOut( public_ref=i.public_ref, booking_ref=booking_by_id.get(i.booking_id, ""), type=i.type, fuel_level_percent=i.fuel_level_percent, cleanliness_ok=i.cleanliness_ok, damage_reported=i.damage_reported, technical_warning=i.technical_warning, odometer_km=i.odometer_km, completed_at=i.completed_at, ) for i in inspections ], maintenance=[ MaintenanceOut( public_ref=m.public_ref, occurred_at=m.occurred_at, odometer_km=m.odometer_km, category=m.category, summary=m.summary, ) for m in maintenance ], quality_issues=[ DataQualityIssueOut( public_ref=q.public_ref, rule_type=q.rule_type, entity_type=q.entity_type, entity_ref=vehicle.public_ref, severity=q.severity, status=q.status, evidence=q.evidence_json, detected_at=q.detected_at, resolved_at=q.resolved_at, ) for q in issues ], )