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"]) def _current_monday(): today = datetime.utcnow().date() return today - timedelta(days=today.weekday()) @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.""" monday = _current_monday() 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(request: dict = Body(default={}), db: Session = Depends(get_db)): """Generate a grocery list for the current week.""" user_notes = request.get("user_notes") if request else None # Get current week's Monday monday = _current_monday() # Fetch current MenuPlan menu_plan = db.query(MenuPlan).filter(MenuPlan.week_start == monday).first() pantry_context = pantry_service.build_pantry_context(db) try: if menu_plan: # Parse the plan JSON (now a flat array of recipe IDs) try: recipe_ids = json.loads(menu_plan.plan) except json.JSONDecodeError: raise HTTPException(status_code=500, detail="Invalid menu plan data") recipes = db.query(Recipe).filter(Recipe.id.in_(recipe_ids)).all() if recipe_ids else [] recipe_map = {r.id: r for r in recipes} recipes_for_ai = [] for recipe_id in recipe_ids: recipe = recipe_map.get(recipe_id) if recipe: try: ingredients = json.loads(recipe.ingredients) except (json.JSONDecodeError, TypeError): ingredients = [] recipes_for_ai.append({ "name": recipe.name, "meal_type": recipe.meal_type, "ingredients": ingredients, }) ai_result = await ai_service.generate_grocery_list(recipes_for_ai, pantry_context, user_notes) else: ai_result = await ai_service.generate_grocery_list_from_pantry(pantry_context, user_notes) except ValueError as e: raise HTTPException(status_code=503, detail=str(e)) except 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.delete("/current") async def delete_current_grocery_list(db: Session = Depends(get_db)): """Delete the grocery list for the current week.""" monday = _current_monday() 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") db.delete(grocery_list) db.commit() return {"status": "deleted"} @router.patch("/{id}/check-item") async def check_grocery_item(id: int, request: dict = Body(...), db: Session = Depends(get_db)): """Mark a grocery item as checked/unchecked.""" grocery_list = db.query(GroceryList).filter(GroceryList.id == id).first() if not grocery_list: raise HTTPException(status_code=404, detail="Grocery list not found") item_name = request.get("name") checked = request.get("checked", False) if not item_name: raise HTTPException(status_code=400, detail="Item name is required") # Parse items try: items = json.loads(grocery_list.items) except json.JSONDecodeError: items = [] # Find and update the item item_found = False for item in items: if isinstance(item, dict) and item.get("name") == item_name: item["checked"] = checked item_found = True break if not item_found: raise HTTPException(status_code=404, detail="Item not found in grocery list") # Save back to database grocery_list.items = json.dumps(items) db.commit() return {"status": "ok"} @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 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} @ai_router.post("/chat") async def chat_endpoint(request: dict = Body(default={}), db: Session = Depends(get_db)): """Chat with Commis using current kitchen context.""" message = request.get("message", "").strip() if not message: raise HTTPException(status_code=400, detail="Message is required") history = request.get("history", []) # Build pantry context pantry_context = pantry_service.build_pantry_context(db) # Get current week's menu recipes monday = _current_monday() menu_plan = db.query(MenuPlan).filter(MenuPlan.week_start == monday).first() menu_context = [] if menu_plan: try: plan_ids = json.loads(menu_plan.plan) recipes = db.query(Recipe).filter(Recipe.id.in_(plan_ids)).all() if plan_ids else [] menu_context = [{"name": r.name, "meal_type": r.meal_type} for r in recipes] except Exception: menu_context = [] # Get current grocery list items grocery_list = db.query(GroceryList).filter(GroceryList.generated_for == monday).first() grocery_context = [] if grocery_list: try: grocery_context = json.loads(grocery_list.items) except Exception: grocery_context = [] try: response = await ai_service.chat_with_commis(message, history, pantry_context, menu_context, grocery_context) return {"response": response} except ValueError as e: raise HTTPException(status_code=503, detail=str(e)) except Exception as e: raise HTTPException(status_code=503, detail=f"Ollama service error: {str(e)}")