from __future__ import annotations 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 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]]