from fastapi import APIRouter, Depends, HTTPException, Body from sqlalchemy.orm import Session from datetime import datetime, timedelta import json from database import get_db from schemas import GroceryListRead from models import GroceryList, MenuPlan, Recipe, Ingredient from services import pantry_service, ai_service router = APIRouter(prefix="/api/grocery", tags=["grocery"]) ai_router = APIRouter(prefix="/api/ai", tags=["ai"]) @router.get("") async def list_grocery_lists(db: Session = Depends(get_db)): """List all grocery lists.""" lists = db.query(GroceryList).order_by(GroceryList.generated_at.desc()).all() return [GroceryListRead.from_orm(l) for l in lists] @router.get("/current") async def get_current_grocery_list(db: Session = Depends(get_db)): """Get grocery list for current week.""" today = datetime.utcnow().date() monday = today - timedelta(days=today.weekday()) grocery_list = db.query(GroceryList).filter(GroceryList.generated_for == monday).first() if not grocery_list: raise HTTPException(status_code=404, detail="No grocery list for current week") return GroceryListRead.from_orm(grocery_list) @router.get("/{id}") async def get_grocery_list(id: int, db: Session = Depends(get_db)): """Get a grocery list by ID.""" grocery_list = db.query(GroceryList).filter(GroceryList.id == id).first() if not grocery_list: raise HTTPException(status_code=404, detail="Grocery list not found") return GroceryListRead.from_orm(grocery_list) @router.post("/generate") async def generate_grocery_list(db: Session = Depends(get_db)): """Generate a grocery list for the current week.""" # Get current week's Monday today = datetime.utcnow().date() monday = today - timedelta(days=today.weekday()) # Fetch current MenuPlan menu_plan = db.query(MenuPlan).filter(MenuPlan.week_start == monday).first() if not menu_plan: raise HTTPException(status_code=404, detail="No menu plan for current week. Generate a menu first.") # Parse the plan JSON try: plan_dict = json.loads(menu_plan.plan) except json.JSONDecodeError: raise HTTPException(status_code=500, detail="Invalid menu plan data") # Collect all recipe IDs and fetch recipes recipe_ids = set() for day_meals in plan_dict.values(): if isinstance(day_meals, dict): for recipe_id in day_meals.values(): if isinstance(recipe_id, int): recipe_ids.add(recipe_id) recipes = db.query(Recipe).filter(Recipe.id.in_(recipe_ids)).all() if recipe_ids else [] recipe_map = {r.id: r for r in recipes} # Build menu_plan_for_ai menu_plan_for_ai = {} for day, day_meals in plan_dict.items(): menu_plan_for_ai[day] = {} for meal_type, recipe_id in day_meals.items(): recipe = recipe_map.get(recipe_id) if recipe: try: ingredients = json.loads(recipe.ingredients) except (json.JSONDecodeError, TypeError): ingredients = [] menu_plan_for_ai[day][meal_type] = { "name": recipe.name, "ingredients": ingredients, } # Build pantry context and generate grocery list try: pantry_context = pantry_service.build_pantry_context(db) ai_result = await ai_service.generate_grocery_list(menu_plan_for_ai, pantry_context) except ValueError as e: raise HTTPException(status_code=503, detail=str(e)) except (ConnectionError, Exception) as e: raise HTTPException(status_code=503, detail=f"Ollama service error: {str(e)}") # Upsert GroceryList existing_list = db.query(GroceryList).filter(GroceryList.generated_for == monday).first() if existing_list: existing_list.items = json.dumps(ai_result.get("items", [])) existing_list.total_estimate = ai_result.get("total_estimate", 0.0) existing_list.notes = ai_result.get("shopping_notes", "") existing_list.generated_at = datetime.utcnow() grocery_list = existing_list else: grocery_list = GroceryList( generated_for=monday, items=json.dumps(ai_result.get("items", [])), total_estimate=ai_result.get("total_estimate", 0.0), notes=ai_result.get("shopping_notes", ""), generated_at=datetime.utcnow(), ) db.add(grocery_list) db.commit() db.refresh(grocery_list) return { "grocery_list": GroceryListRead.from_orm(grocery_list), "items": ai_result.get("items", []), "shopping_notes": ai_result.get("shopping_notes", ""), } @router.put("/{id}/purchased") async def mark_purchased(id: int, db: Session = Depends(get_db)): """Mark a grocery list as purchased and update pantry.""" grocery_list = db.query(GroceryList).filter(GroceryList.id == id).first() if not grocery_list: raise HTTPException(status_code=404, detail="Grocery list not found") # Mark as purchased grocery_list.is_purchased = True # Parse items try: items = json.loads(grocery_list.items) except json.JSONDecodeError: items = [] # Update pantry for each item for item in items: if isinstance(item, dict): name = item.get("name") quantity = item.get("quantity", 0) unit = item.get("unit", "") if name: # Check if ingredient exists existing_ingredient = db.query(Ingredient).filter(Ingredient.name == name).first() if existing_ingredient: existing_ingredient.quantity += quantity existing_ingredient.updated_at = datetime.utcnow() else: # Create new ingredient new_ingredient = Ingredient( name=name, quantity=quantity, unit=unit, category=item.get("store_section"), ) db.add(new_ingredient) db.commit() db.refresh(grocery_list) return GroceryListRead.from_orm(grocery_list) @ai_router.post("/suggest-recipe") async def suggest_recipe_endpoint(db: Session = Depends(get_db)): """Suggest a recipe based on current pantry.""" try: context = pantry_service.build_pantry_context(db) result = await ai_service.suggest_recipe(context) return result except ValueError as e: raise HTTPException(status_code=503, detail=str(e)) except (ConnectionError, Exception) as e: raise HTTPException(status_code=503, detail=f"Ollama service error: {str(e)}") @ai_router.get("/models") async def list_models(): """List available Ollama models.""" models = await ai_service.get_available_models() return {"models": models}