from __future__ import annotations from dataclasses import replace from typing import List from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session from app.database import get_db from app.models import FamilyProfile, MealPlan, MealPlanItem from app.schemas.meal_plan_generation import ( GenerateRequest, GenerationDebug, GenerationItem, GenerationResponse, RegenerateRequest, ) from app.security import require_admin from app.services.planner.config import DEFAULT from app.services.planner.generate import generate_meal_plan from app.services.planner.types import GenerationResult admin_router = APIRouter( prefix="/api/admin/meal-plans", tags=["meal-plans-admin"], dependencies=[Depends(require_admin)], ) public_router = APIRouter(prefix="/api/meal-plans", tags=["meal-plans"]) def _components_payload(components: dict | None) -> dict: return dict(components or {}) def _to_response(week_start, plan_id, items: List[MealPlanItem], result: GenerationResult) -> GenerationResponse: item_payloads: List[GenerationItem] = [] score_by_recipe = {s.recipe_id: s for s in result.selected} for it in items: scored = score_by_recipe.get(it.recipe_id) item_payloads.append( GenerationItem( recipe_id=it.recipe_id, day_of_week=it.day_of_week, estimated_cost=it.estimated_cost or 0, score=scored.score if scored else 0.0, components=_components_payload(it.components or (scored.components if scored else None)), ) ) return GenerationResponse( meal_plan_id=plan_id, week_start_date=week_start, items=item_payloads, debug=GenerationDebug( feasible_count=result.feasible_count, rejected_summary=result.rejected_summary, set_score=result.set_score, ), ) @admin_router.post("/generate", response_model=GenerationResponse, status_code=status.HTTP_201_CREATED) def generate(payload: GenerateRequest, db: Session = Depends(get_db)) -> GenerationResponse: family = db.query(FamilyProfile).filter(FamilyProfile.id == payload.family_profile_id).first() if family is None: raise HTTPException(status_code=404, detail="family_profile not found") try: result = generate_meal_plan( db, family_id=payload.family_profile_id, week_start_date=payload.week_start_date, ) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) items = ( db.query(MealPlanItem) .filter(MealPlanItem.meal_plan_id == result.meal_plan_id) .order_by(MealPlanItem.day_of_week) .all() ) return _to_response(payload.week_start_date, result.meal_plan_id, items, result) @admin_router.post("/regenerate", response_model=GenerationResponse, status_code=status.HTTP_201_CREATED) def regenerate(payload: RegenerateRequest, db: Session = Depends(get_db)) -> GenerationResponse: family = db.query(FamilyProfile).filter(FamilyProfile.id == payload.family_profile_id).first() if family is None: raise HTTPException(status_code=404, detail="family_profile not found") config = DEFAULT if payload.relax_time_max_minutes is not None: config = replace(config, max_total_minutes=payload.relax_time_max_minutes) if payload.relax_calorie_pct is not None: config = replace(config, calorie_tolerance_pct=payload.relax_calorie_pct) if payload.relax_max_meal_cost is not None: config = replace(config, max_meal_cost=payload.relax_max_meal_cost) db.query(MealPlan).filter( MealPlan.family_profile_id == payload.family_profile_id, MealPlan.week_start_date == payload.week_start_date, ).delete(synchronize_session=False) db.commit() result = generate_meal_plan( db, family_id=payload.family_profile_id, week_start_date=payload.week_start_date, config=config, exclude_recipe_ids=set(payload.exclude_recipe_ids) if payload.exclude_recipe_ids else None, ) items = ( db.query(MealPlanItem) .filter(MealPlanItem.meal_plan_id == result.meal_plan_id) .order_by(MealPlanItem.day_of_week) .all() ) return _to_response(payload.week_start_date, result.meal_plan_id, items, result) @public_router.get("/{plan_id}", response_model=GenerationResponse) def get_plan(plan_id: UUID, db: Session = Depends(get_db)) -> GenerationResponse: plan = db.query(MealPlan).filter(MealPlan.id == plan_id).first() if plan is None: raise HTTPException(status_code=404, detail="meal_plan not found") items = ( db.query(MealPlanItem) .filter(MealPlanItem.meal_plan_id == plan.id) .order_by(MealPlanItem.day_of_week) .all() ) # Read-after-create: scores now stored on MealPlanItem return GenerationResponse( meal_plan_id=plan.id, week_start_date=plan.week_start_date, items=[ GenerationItem( recipe_id=it.recipe_id, day_of_week=it.day_of_week, estimated_cost=it.estimated_cost or 0, score=it.score or 0.0, components=_components_payload(it.components), ) for it in items ], debug=GenerationDebug(feasible_count=0, rejected_summary={}, set_score=0.0), )