from __future__ import annotations from typing import List, Optional from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Query, Response, status from sqlalchemy.orm import Session from app.database import get_db from app.models import Ingredient, Recipe from app.schemas.recipe import RecipeCreate, RecipeRead, RecipeUpdate from app.security import require_admin public_router = APIRouter(prefix="/api/recipes", tags=["recipes"]) admin_router = APIRouter( prefix="/api/admin/recipes", tags=["recipes-admin"], dependencies=[Depends(require_admin)], ) def _validate_ingredient_ids(db: Session, ingredient_refs) -> None: ids = [ref.ingredient_id for ref in ingredient_refs] found = ( db.query(Ingredient.id).filter(Ingredient.id.in_(ids)).all() ) found_ids = {row[0] for row in found} missing = [str(i) for i in ids if i not in found_ids] if missing: raise HTTPException( status_code=422, detail={"error": "unknown ingredient_ids", "missing": missing}, ) def _serialize(row: Recipe) -> dict: return { "id": row.id, "name": row.name, "description": row.description, "image_url": row.image_url, "prep_time_minutes": row.prep_time_minutes or 0, "cook_time_minutes": row.cook_time_minutes or 0, "servings": row.servings, "cuisine_tags": list(row.cuisine_tags or []), "dietary_tags": list(row.dietary_tags or []), "protein_type": row.protein_type, "spice_level": row.spice_level, "calories_per_serving": row.calories_per_serving, "ingredients": row.ingredients or [], "instructions": list(row.instructions or []), "source_url": row.source_url, } @public_router.get("", response_model=List[RecipeRead]) def list_recipes( q: Optional[str] = Query(default=None), limit: int = Query(default=100, le=500), db: Session = Depends(get_db), ): query = db.query(Recipe) if q: like = f"%{q.lower()}%" query = query.filter(Recipe.name.ilike(like)) rows = query.order_by(Recipe.name).limit(limit).all() return [_serialize(r) for r in rows] @public_router.get("/{recipe_id}", response_model=RecipeRead) def get_recipe(recipe_id: UUID, db: Session = Depends(get_db)): row = db.query(Recipe).filter(Recipe.id == recipe_id).first() if row is None: raise HTTPException(status_code=404, detail="recipe not found") return _serialize(row) @admin_router.post("", response_model=RecipeRead, status_code=status.HTTP_201_CREATED) def create_recipe(payload: RecipeCreate, db: Session = Depends(get_db)): _validate_ingredient_ids(db, payload.ingredients) row = Recipe( name=payload.name, description=payload.description, image_url=payload.image_url, prep_time_minutes=payload.prep_time_minutes, cook_time_minutes=payload.cook_time_minutes, servings=payload.servings, cuisine_tags=payload.cuisine_tags, dietary_tags=payload.dietary_tags, protein_type=payload.protein_type, spice_level=payload.spice_level, calories_per_serving=payload.calories_per_serving, ingredients=[ref.model_dump(mode="json") for ref in payload.ingredients], instructions=payload.instructions, source_url=payload.source_url, is_manually_added=True, ) db.add(row) db.commit() db.refresh(row) return _serialize(row) @admin_router.patch("/{recipe_id}", response_model=RecipeRead) def update_recipe(recipe_id: UUID, payload: RecipeUpdate, db: Session = Depends(get_db)): row = db.query(Recipe).filter(Recipe.id == recipe_id).first() if row is None: raise HTTPException(status_code=404, detail="recipe not found") data = payload.model_dump(exclude_unset=True) if "ingredients" in data and data["ingredients"] is not None: _validate_ingredient_ids(db, payload.ingredients) row.ingredients = [ref.model_dump(mode="json") for ref in payload.ingredients] data.pop("ingredients") for field, value in data.items(): setattr(row, field, value) db.commit() db.refresh(row) return _serialize(row) @admin_router.delete("/{recipe_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response) def delete_recipe(recipe_id: UUID, db: Session = Depends(get_db)): row = db.query(Recipe).filter(Recipe.id == recipe_id).first() if row is None: raise HTTPException(status_code=404, detail="recipe not found") db.delete(row) db.commit() return Response(status_code=status.HTTP_204_NO_CONTENT)