"""FDDB recipe lists: upsert, link to diary items, catalog macros via ingredients.""" from __future__ import annotations import uuid from typing import Any from data_layer.food_mapping import get_food_mapping_with_cursor, normalize_food_name from data_layer.nutrition_items import catalog_macros_for_item, _f def upsert_recipes(cur, profile_id: str, recipes: list[dict[str, Any]]) -> dict[str, int]: inserted = updated = ingredients = 0 for rec in recipes: norm = rec.get("name_normalized") or normalize_food_name(rec.get("name_raw")) if not norm: continue cur.execute( "SELECT id FROM food_recipes WHERE profile_id = %s AND name_normalized = %s", (profile_id, norm), ) row = cur.fetchone() if row: rid = row["id"] cur.execute( """ UPDATE food_recipes SET name_raw=%s, portions=%s, description=%s, updated_at=NOW() WHERE id=%s """, (rec["name_raw"], rec.get("portions") or 1, rec.get("description"), rid), ) cur.execute("DELETE FROM food_recipe_ingredients WHERE recipe_id = %s", (rid,)) updated += 1 else: rid = str(uuid.uuid4()) cur.execute( """ INSERT INTO food_recipes (id, profile_id, name_raw, name_normalized, portions, description, source) VALUES (%s,%s,%s,%s,%s,%s,'fddb_list') """, (rid, profile_id, rec["name_raw"], norm, rec.get("portions") or 1, rec.get("description")), ) inserted += 1 for ing in rec.get("ingredients") or []: inorm = ing.get("source_name_normalized") or normalize_food_name(ing.get("source_name_raw")) if not inorm: continue cur.execute( """ INSERT INTO food_recipe_ingredients (id, recipe_id, source_name_raw, source_name_normalized, quantity_raw, quantity_g, sort_order) VALUES (%s,%s,%s,%s,%s,%s,%s) """, ( str(uuid.uuid4()), rid, ing["source_name_raw"], inorm, ing.get("quantity_raw"), ing.get("quantity_g"), ing.get("sort_order") or 0, ), ) ingredients += 1 linked, dates = link_recipes_to_items(cur, profile_id) return { "inserted": inserted, "updated": updated, "ingredients": ingredients, "items_linked": linked, "dates_linked": dates, } def link_recipes_to_items(cur, profile_id: str) -> tuple[int, list[str]]: cur.execute( """ SELECT DISTINCT i.date::text AS date FROM nutrition_items i JOIN food_recipes r ON r.profile_id = i.profile_id AND i.source_name_normalized = r.name_normalized WHERE i.profile_id = %s AND i.food_id IS NULL """, (profile_id,), ) dates = [r["date"] for r in cur.fetchall()] cur.execute( """ UPDATE nutrition_items i SET recipe_id = r.id, updated_at = NOW() FROM food_recipes r WHERE i.profile_id = %s AND r.profile_id = %s AND i.source_name_normalized = r.name_normalized AND i.food_id IS NULL """, (profile_id, profile_id), ) return cur.rowcount or 0, dates def list_recipes(cur, profile_id: str) -> list[dict[str, Any]]: cur.execute( """ SELECT id, name_raw, name_normalized, portions, description, source FROM food_recipes WHERE profile_id = %s ORDER BY name_normalized """, (profile_id,), ) recipes = [dict(r) for r in cur.fetchall()] if not recipes: return [] ids = [r["id"] for r in recipes] cur.execute( """ SELECT recipe_id, source_name_raw, source_name_normalized, quantity_raw, quantity_g, sort_order FROM food_recipe_ingredients WHERE recipe_id = ANY(%s) ORDER BY sort_order, source_name_raw """, (ids,), ) by_r: dict[str, list] = {str(i): [] for i in ids} for row in cur.fetchall(): by_r.setdefault(str(row["recipe_id"]), []).append(dict(row)) for rec in recipes: rec["ingredients"] = by_r.get(str(rec["id"]), []) return recipes def apply_recipe_to_items(cur, profile_id: str, source_name_normalized: str, recipe_id: str) -> int: cur.execute( """ UPDATE nutrition_items SET recipe_id = %s, food_id = NULL, mapping_id = NULL, value_origin = 'fddb', updated_at = NOW() WHERE profile_id = %s AND source_name_normalized = %s """, (recipe_id, profile_id, source_name_normalized), ) return cur.rowcount or 0 def mapped_ingredient_quantities( cur, profile_id: str, recipe_id: str, eaten_qty_g: float | None ) -> list[dict[str, Any]] | None: cur.execute( "SELECT portions FROM food_recipes WHERE id = %s AND profile_id = %s", (recipe_id, profile_id), ) rec = cur.fetchone() if not rec: return None cur.execute( """ SELECT source_name_raw, quantity_g FROM food_recipe_ingredients WHERE recipe_id = %s ORDER BY sort_order """, (recipe_id,), ) ings = cur.fetchall() if not ings: return None total_g = sum(_f(i.get("quantity_g")) for i in ings) if eaten_qty_g and eaten_qty_g > 0 and total_g > 0: scale = float(eaten_qty_g) / total_g else: portions = float(rec.get("portions") or 1) or 1.0 scale = 1.0 / portions out = [] for ing in ings: mapping = get_food_mapping_with_cursor(cur, ing["source_name_raw"], profile_id) if not mapping: return None out.append({ "food_id": mapping["food_id"], "quantity_g": _f(ing.get("quantity_g")) * scale, }) return out def catalog_macros_for_recipe(cur, profile_id: str, recipe_id: str, eaten_qty_g: float | None) -> dict[str, float] | None: parts = mapped_ingredient_quantities(cur, profile_id, recipe_id, eaten_qty_g) if not parts: return None acc = {"kcal": 0.0, "protein_g": 0.0, "fat_g": 0.0, "carbs_g": 0.0} for part in parts: cat = catalog_macros_for_item(cur, part["food_id"], part["quantity_g"]) if not cat: return None for k in acc: acc[k] += cat[k] return acc