from __future__ import annotations from datetime import date, datetime from decimal import Decimal from enum import Enum from typing import Any, Dict, List, Optional from uuid import UUID from pydantic import BaseModel, Field, model_validator # Re-export from submodules so forward references resolve from .recipe import ( RecipeBase, RecipeCreate, RecipeIngredientRef, RecipeRead, RecipeUpdate, ResolveIngredientCandidate, ResolveIngredientRequest, ResolveIngredientResponse, ) # These may be referenced by other models; re-export as aliases if needed. RecipeResponse = RecipeRead class FamilyMemberRole(str, Enum): adult = "adult" child = "child" class MealType(str, Enum): breakfast = "breakfast" lunch = "lunch" dinner = "dinner" class MealPlanStatus(str, Enum): draft = "draft" pending_approval = "pending_approval" approved = "approved" locked = "locked" class MealPlanItemStatus(str, Enum): pending = "pending" approved = "approved" denied = "denied" swapped = "swapped" class DenialReason(str, Enum): too_expensive = "too_expensive" boring = "boring" disliked_ingredient = "disliked_ingredient" cultural = "cultural" other = "other" class NeverSuggestReason(str, Enum): allergy = "allergy" dislike = "dislike" tried_too_much = "tried_too_much" other = "other" class IngredientBase(BaseModel): name: str name_lower: str plural_name: Optional[str] = None aisle: Optional[str] = None typical_price: Optional[float] = None unit: Optional[str] = None season_months: Optional[List[int]] = None class IngredientResponse(IngredientBase): id: UUID created_at: Optional[datetime] = None class Config: from_attributes = True class IngredientCreate(IngredientBase): pass class FamilyMemberBase(BaseModel): name: str email: Optional[str] = None role: FamilyMemberRole likes_mushrooms: bool = False class FamilyMemberResponse(FamilyMemberBase): id: UUID family_profile_id: UUID created_at: Optional[datetime] = None updated_at: Optional[datetime] = None class Config: from_attributes = True class FamilyMemberCreate(FamilyMemberBase): pass class FamilyProfileBase(BaseModel): name: str household_size: int adult_count: int child_count: int dietary_notes: Optional[str] = None budget_per_meal: float = 50.00 class FamilyProfileResponse(BaseModel): id: UUID name: str household_size: int adult_count: int child_count: int dietary_notes: Optional[str] = None budget_per_meal: float = 50.00 created_at: Optional[datetime] = None updated_at: Optional[datetime] = None members: List[FamilyMemberResponse] = [] planner_config: Optional[dict] = None class Config: from_attributes = True class PlannerConfigOverride(BaseModel): recency_weeks: Optional[int] = Field(default=None, ge=0) calorie_tolerance_pct: Optional[int] = Field(default=None, ge=0, le=100) max_total_minutes: Optional[int] = Field(default=None, ge=0) max_meal_cost: Optional[float] = Field(default=None, ge=0) w_savings: Optional[float] = Field(default=None, ge=0, le=1) w_coverage: Optional[float] = Field(default=None, ge=0, le=1) w_pantry: Optional[float] = Field(default=None, ge=0, le=1) w_time: Optional[float] = Field(default=None, ge=0, le=1) w_recency: Optional[float] = Field(default=None, ge=0, le=1) time_ideal_minutes: Optional[int] = Field(default=None, ge=0) time_full_minutes: Optional[int] = Field(default=None, ge=0) recency_full_weeks: Optional[int] = Field(default=None, ge=0) top_k: Optional[int] = Field(default=None, ge=1) set_size: Optional[int] = Field(default=None, ge=1) p_protein: Optional[float] = Field(default=None, ge=0) p_cuisine: Optional[float] = Field(default=None, ge=0) class PlannerConfigResponse(BaseModel): recency_weeks: int calorie_tolerance_pct: int max_total_minutes: int max_meal_cost: float w_savings: float w_coverage: float w_pantry: float w_time: float w_recency: float time_ideal_minutes: int time_full_minutes: int recency_full_weeks: int top_k: int set_size: int p_protein: float p_cuisine: float source: str = "default" class PlannerConfigUpdateRequest(BaseModel): planner_config: PlannerConfigOverride class FamilyProfileCreate(FamilyProfileBase): pass class FamilyProfileUpdate(BaseModel): name: Optional[str] = None household_size: Optional[int] = None adult_count: Optional[int] = None child_count: Optional[int] = None dietary_notes: Optional[str] = None budget_per_meal: Optional[float] = None planner_config: Optional[dict] = None class RecipeCreate(RecipeBase): pass class MealPlanItemBase(BaseModel): recipe_id: UUID day_of_week: int = Field(..., ge=1, le=7) meal_type: MealType estimated_cost: Optional[float] = None class MealPlanItemResponse(MealPlanItemBase): id: UUID meal_plan_id: UUID approval_status: MealPlanItemStatus = MealPlanItemStatus.pending denial_reason: Optional[DenialReason] = None denial_details: Optional[str] = None used_pantry_items: Optional[List[UUID]] = [] score: Optional[float] = None components: Optional[Dict[str, float]] = None created_at: Optional[datetime] = None updated_at: Optional[datetime] = None recipe: Optional[RecipeResponse] = None class Config: from_attributes = True class MealPlanItemCreate(MealPlanItemBase): pass class MealPlanBase(BaseModel): week_start_date: date status: MealPlanStatus = MealPlanStatus.draft approval_deadline: Optional[datetime] = None notes: Optional[str] = None class MealPlanResponse(MealPlanBase): id: UUID family_profile_id: UUID total_estimated_cost: Optional[float] = None created_at: Optional[datetime] = None updated_at: Optional[datetime] = None items: List[MealPlanItemResponse] = [] class Config: from_attributes = True class MealPlanCreate(MealPlanBase): items: List[MealPlanItemCreate] = [] class VoteRequest(BaseModel): vote: bool denial_reason: Optional[DenialReason] = None denial_details: Optional[str] = None class VoteResponse(BaseModel): id: UUID meal_plan_item_id: UUID family_member_id: UUID vote: bool voted_at: Optional[datetime] = None class Config: from_attributes = True class HomePantryBase(BaseModel): ingredient_id: UUID quantity: Optional[float] = None unit: Optional[str] = None expires_at: Optional[date] = None class HomePantryResponse(HomePantryBase): id: UUID family_profile_id: UUID added_at: Optional[datetime] = None created_at: Optional[datetime] = None ingredient: Optional[IngredientResponse] = None class Config: from_attributes = True class HomePantryCreate(HomePantryBase): pass class HomePantryBulkCreate(BaseModel): """Request body for POST /api/pantry/bulk. Accepts a list of items to add in one call; each item follows the same upsert semantics as HomePantryCreate (insert or overwrite qty/unit/expires).""" items: List[HomePantryCreate] class HomePantryBulkResultItem(BaseModel): ingredient_id: UUID status: str # "added" | "updated" | "skipped" id: Optional[UUID] = None reason: Optional[str] = None class HomePantryBulkResult(BaseModel): """Response body for POST /api/pantry/bulk. Reports per-item outcomes so the UI can show a precise toast ("Added 8 items, 1 skipped — no ingredient link"). Total counts are derived for convenience.""" added: int updated: int skipped: int results: List[HomePantryBulkResultItem] class FillEmptySlotsRequest(BaseModel): """Request body for POST /api/meals/{id}/fill-empty-slots. The caller selects which meal types to fill (dinner only, or all three). Days 1-7 are filled automatically; the backend iterates in day order then meal_type order.""" meal_types: List[str] = Field( default_factory=lambda: ["breakfast", "lunch", "dinner"], description="Subset of {breakfast, lunch, dinner} to fill.", ) class FilledSlot(BaseModel): day_of_week: int meal_type: str item: MealPlanItemResponse class FailedSlot(BaseModel): day_of_week: int meal_type: str reason: str class FillEmptySlotsResult(BaseModel): """Response body for POST /api/meals/{id}/fill-empty-slots. Reports each slot as either 'filled' (with the new MealPlanItem) or 'failed' (with a human-readable reason). Partial success is the model: the caller decides whether to retry the failed slots.""" filled: List[FilledSlot] failed: List[FailedSlot] class FeedbackBase(BaseModel): rating: Optional[int] = Field(None, ge=1, le=5) never_suggest: bool = False denial_reason: Optional[DenialReason] = None feedback_text: Optional[str] = None class FeedbackResponse(FeedbackBase): id: UUID family_profile_id: UUID family_member_id: Optional[UUID] = None meal_plan_item_id: UUID created_at: Optional[datetime] = None class Config: from_attributes = True class FeedbackCreate(FeedbackBase): meal_plan_item_id: UUID class ShoppingListItem(BaseModel): ingredient_id: Optional[UUID] = None name: str quantity: Optional[float] = None unit: Optional[str] = None aisle: Optional[str] = None estimated_price: Optional[float] = None is_on_sale: bool = False sale_price: Optional[float] = None in_season: bool = False in_pantry: bool = False class ShoppingListResponse(BaseModel): week_start_date: date items: List[ShoppingListItem] total_estimated_cost: float sale_items_count: int by_aisle: dict[str, List[ShoppingListItem]]