"""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