"""Image retouch router: process images, get results, price comparison.""" import uuid from fastapi import ( APIRouter, BackgroundTasks, Depends, File, Form, HTTPException, UploadFile, status, ) from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings from app.database import get_db from app.dependencies import get_current_user from app.models.user import User from app.models.retouch import RetouchStatus from app.schemas.retouch import ( PriceCompareRequest, PriceCompareResponse, RetouchProcessResponse, RetouchResultResponse, ) from app.services import retouch_service, price_compare_service from app.services.vehicle_service import get_vehicle_by_id from app.tasks.retouch_processing import run_retouch_processing router = APIRouter(prefix="/retouch", tags=["retouch"]) @router.post( "/process", response_model=RetouchProcessResponse, status_code=status.HTTP_202_ACCEPTED, ) async def process_image( background_tasks: BackgroundTasks, file: UploadFile = File(..., description="Image file to retouch"), vehicle_id: str | None = Form(None, description="Optional vehicle ID to link"), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """Upload an image file for retouching via Flux.1-Pro. Returns 202 with retouch_id. Processing happens asynchronously. """ if not file or not file.filename: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail={"error": {"code": "NO_FILE", "message": "No file provided"}}, ) mime_type = file.content_type or "" if not retouch_service.validate_mime_type(mime_type): raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail={ "error": { "code": "INVALID_MIME_TYPE", "message": f"Invalid MIME type: {mime_type}. Only image/* types are allowed.", } }, ) file_bytes = await file.read() if not retouch_service.validate_file_size(len(file_bytes)): raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail={ "error": { "code": "FILE_TOO_LARGE", "message": f"File size exceeds limit of {settings.MAX_FILE_SIZE_MB} MB", } }, ) # Parse optional vehicle_id parsed_vehicle_id: uuid.UUID | None = None vehicle_info = None if vehicle_id: try: parsed_vehicle_id = uuid.UUID(vehicle_id) except (ValueError, TypeError): raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail={ "error": { "code": "INVALID_VEHICLE_ID", "message": "Invalid vehicle UUID", } }, ) # Fetch vehicle info for better retouch prompt vehicle = await get_vehicle_by_id(db, parsed_vehicle_id) if vehicle: vehicle_info = { "make": vehicle.make, "model": vehicle.model, "year": vehicle.year, "color": vehicle.color, } try: result = await retouch_service.upload_retouch_file( db=db, file_bytes=file_bytes, file_name=file.filename or "upload.png", mime_type=mime_type, vehicle_id=parsed_vehicle_id, ) except ValueError as exc: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail={"error": {"code": "UPLOAD_FAILED", "message": str(exc)}}, ) # Queue background processing background_tasks.add_task(run_retouch_processing, result.id, vehicle_info) return RetouchProcessResponse( message="Retouch processing queued", retouch_id=result.id, status=result.status.value if isinstance(result.status, RetouchStatus) else str(result.status), ) @router.get( "/results/{result_id}", response_model=RetouchResultResponse, status_code=status.HTTP_200_OK, ) async def get_retouch_result( result_id: uuid.UUID, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """Get a single retouch result by ID. Returns status, original and retouched file paths. If processing is still ongoing, status will be 'processing'. If processing failed, status will be 'failed' with error_message. """ result = await retouch_service.get_result(db, result_id) if result is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail={ "error": { "code": "RETOUCH_NOT_FOUND", "message": "Retouch result not found", } }, ) return RetouchResultResponse.model_validate(result) @router.post( "/price-compare", response_model=PriceCompareResponse, status_code=status.HTTP_200_OK, ) async def price_compare( body: PriceCompareRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """Compare prices for a vehicle by searching comparable listings. Returns comparable listings and average price. If no comparable listings found, returns empty list with null average. """ try: result = await price_compare_service.compare_prices(db, body.vehicle_id) except ValueError as exc: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail={"error": {"code": "VEHICLE_NOT_FOUND", "message": str(exc)}}, ) return result