from __future__ import annotations from collections import Counter from typing import Any from app.services.repository import repo def _overlap(left: list[str], right: list[str]) -> list[str]: return sorted(set(left) & set(right)) class PlannerService: def generate(self, *, week_start: str, anchors: list[str], target_meals: int) -> dict[str, Any]: suggestions = [ item for item in repo.list_suggestions() if item.get("status") in {"approved", "suggested", "saved"} ] chosen: list[dict[str, Any]] = [] used_ids: set[str] = set() for item in sorted(suggestions, key=lambda entry: entry.get("score", 0), reverse=True): haystack = " ".join([str(item.get("title") or ""), " ".join(item.get("ingredients") or []), " ".join(item.get("tags") or [])]).lower() if anchors and not any(anchor.strip().lower() in haystack for anchor in anchors if anchor.strip()): continue chosen.append(item) used_ids.add(str(item.get("id"))) if len(chosen) >= min(len(anchors), target_meals): break ingredient_counter: Counter[str] = Counter() for item in chosen: ingredient_counter.update(item.get("ingredients") or []) ranked = sorted( (item for item in suggestions if str(item.get("id")) not in used_ids), key=lambda entry: ( len(_overlap(list(ingredient_counter), entry.get("ingredients") or [])), entry.get("score", 0), ), reverse=True, ) for item in ranked: if len(chosen) >= target_meals: break chosen.append(item) used_ids.add(str(item.get("id"))) ingredient_counter.update(item.get("ingredients") or []) meals: list[dict[str, Any]] = [] shared = [name for name, count in ingredient_counter.items() if count >= 2] for item in chosen[:target_meals]: overlap = _overlap(shared, item.get("ingredients") or []) note = "Reuses: " + ", ".join(overlap[:4]) if overlap else "Adds variety without blowing up the ingredient list." meals.append( { "suggestion_id": item.get("id"), "title": item.get("title"), "url": item.get("url"), "score": item.get("score"), "ingredients": item.get("ingredients") or [], "note": note, } ) return { "week_start": week_start, "anchors": anchors, "target_meals": target_meals, "shared_ingredients": shared[:12], "meals": meals, "summary": f"Built {len(meals)} meals with {len(shared[:12])} shared ingredient lanes.", } planner = PlannerService()