"""Meals router (spec §3.2). Thin handlers — business logic in services/meals.py.""" from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from database import get_db from schemas import ( LogEntryRead, MealComponentsUpdateRequest, MealFromLogRequest, MealFromLogResponse, MealRead, MealUnpackRequest, MealUnpackResponse, ) from services import meals as svc router = APIRouter(prefix="/api/meals", tags=["meals"]) @router.post("/from-log", response_model=MealFromLogResponse, status_code=201) def create_meal_from_log(data: MealFromLogRequest, db: Session = Depends(get_db)): """Create a meal food from selected daily log entries (§4.3). Runs as one transaction: creates the meal + components, deletes source entries, inserts one replacement entry. Rolls back entirely on failure. """ try: meal, entry = svc.create_meal_from_log( db, data.date, data.entry_ids, data.name, ) except svc.EntryNotFoundError as e: raise HTTPException(status_code=404, detail=str(e)) except svc.EntryDateMismatchError as e: raise HTTPException(status_code=400, detail=str(e)) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) return MealFromLogResponse(meal=meal, entry=entry) @router.post("/{meal_id}/unpack", response_model=MealUnpackResponse) def unpack_meal( meal_id: int, data: MealUnpackRequest, db: Session = Depends(get_db), ): """Replace a logged meal entry with its leaf component entries (§4.4). Components are resolved recursively — nested meals are flattened to leaf foods with their scaling factors multiplied down. Runs as one transaction. If multiple entries match the meal on this date, specify ``entry_id`` to disambiguate. """ try: entries = svc.unpack_meal(db, meal_id, data.date, data.entry_id) except svc.EntryNotFoundError as e: raise HTTPException(status_code=404, detail=str(e)) except svc.AmbiguousMealEntryError as e: raise HTTPException(status_code=400, detail=str(e)) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) return MealUnpackResponse(entries=entries) @router.put("/{meal_id}/components", response_model=MealRead) def update_meal_components( meal_id: int, data: MealComponentsUpdateRequest, db: Session = Depends(get_db), ): """Replace a meal's component list (§3.2 PUT). Cycle-checked. Components are validated for cycles (direct or transitive self-reference). A 422 is returned when the update would create a cycle. This is a full replacement — all existing components are removed and replaced with the provided list. """ comps = [{"food_id": c.food_id, "quantity": c.quantity} for c in data.components] try: return svc.update_meal_components(db, meal_id, comps) except svc.MealNotFoundError as e: raise HTTPException(status_code=404, detail=str(e)) except svc.ComponentFoodNotFoundError as e: raise HTTPException(status_code=404, detail=str(e)) except svc.MealCycleError as e: raise HTTPException(status_code=422, detail=str(e))