"""Authenticated catalog search and user-owned foods / mappings.""" from __future__ import annotations import logging from typing import Optional from fastapi import APIRouter, Depends, Header, HTTPException from pydantic import BaseModel from auth import require_auth from data_layer.food_mapping import ( apply_mapping_to_items, apply_quantities_to_items, clear_mapping_from_items, normalize_food_name, suggest_catalog_foods, upsert_food_mapping, ) from data_layer.nutrition_items import dates_for_normalized_name, rebuild_daily_nutrients from db import get_cursor, get_db, r2d from routers.profiles import get_pid logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/bls", tags=["bls"]) class UserFoodCreate(BaseModel): name_de: str name_en: Optional[str] = None macros_per_100g: Optional[dict] = None class MappingUpsert(BaseModel): source_name: str food_id: str source_system: str = "fddb" grams_per_unit: Optional[float] = None source_unit: Optional[str] = None def _pid(session: dict, x_profile_id: Optional[str] = None) -> str: return x_profile_id or session["profile_id"] def _rebuild_days(cur, profile_id: str, dates: list[str]) -> None: for d in dates: rebuild_daily_nutrients(cur, profile_id, d) @router.get("/foods") def search_foods( q: str = "", limit: int = 20, session: dict = Depends(require_auth), ): pid = session["profile_id"] with get_db() as conn: cur = get_cursor(conn) return suggest_catalog_foods(cur, q, pid, limit=min(max(limit, 1), 50)) @router.post("/foods/manual") def create_user_food( body: UserFoodCreate, x_profile_id: Optional[str] = Header(default=None), session: dict = Depends(require_auth), ): from routers.admin_bls import _write_manual_macros import uuid pid = _pid(session, x_profile_id) name = (body.name_de or "").strip() if not name: raise HTTPException(400, "Name fehlt") with get_db() as conn: cur = get_cursor(conn) cur.execute( """ INSERT INTO food_catalog (name_de, name_en, catalog_kind, owner_profile_id, source, external_key) VALUES (%s, %s, 'manual_user', %s, 'manual', %s) RETURNING * """, (name, body.name_en, pid, f"man-user-{normalize_food_name(name)[:32]}-{uuid.uuid4().hex[:8]}"), ) food = r2d(cur.fetchone()) _write_manual_macros(cur, food["id"], body.macros_per_100g) return food @router.get("/mappings") def list_my_mappings( x_profile_id: Optional[str] = Header(default=None), session: dict = Depends(require_auth), ): pid = _pid(session, x_profile_id) with get_db() as conn: cur = get_cursor(conn) cur.execute( """ SELECT m.id, m.source_name_raw, m.source_name_normalized, m.food_id, m.source, m.grams_per_unit, m.source_unit, f.name_de AS food_name_de, f.bls_code, f.catalog_kind FROM food_name_mappings m JOIN food_catalog f ON f.id = m.food_id WHERE m.profile_id = %s ORDER BY m.source_name_normalized """, (pid,), ) return [r2d(r) for r in cur.fetchall()] @router.post("/mappings") def upsert_my_mapping( body: MappingUpsert, x_profile_id: Optional[str] = Header(default=None), session: dict = Depends(require_auth), ): pid = _pid(session, x_profile_id) with get_db() as conn: cur = get_cursor(conn) cur.execute( """ SELECT id FROM food_catalog WHERE id = %s AND is_active = true AND (owner_profile_id IS NULL OR owner_profile_id = %s) """, (body.food_id, pid), ) if not cur.fetchone(): raise HTTPException(404, "Lebensmittel nicht gefunden") mid = upsert_food_mapping( cur, source_name_raw=body.source_name, food_id=body.food_id, profile_id=pid, source="bulk", source_system=body.source_system, grams_per_unit=body.grams_per_unit, source_unit=body.source_unit, ) norm = normalize_food_name(body.source_name) n = apply_mapping_to_items(cur, pid, norm, body.food_id, mid) apply_quantities_to_items(cur, pid, norm, body.grams_per_unit) dates = dates_for_normalized_name(cur, pid, norm) try: with get_db() as conn: _rebuild_days(get_cursor(conn), pid, dates) except Exception: logger.exception("Nährwert-Rebuild nach Mapping %s fehlgeschlagen", norm) return {"mapping_id": mid, "items_updated": n, "source_name_normalized": norm} @router.delete("/mappings/{mapping_id}") def delete_my_mapping( mapping_id: int, x_profile_id: Optional[str] = Header(default=None), session: dict = Depends(require_auth), ): pid = _pid(session, x_profile_id) with get_db() as conn: cur = get_cursor(conn) cur.execute( """ SELECT source_name_normalized FROM food_name_mappings WHERE id = %s AND profile_id = %s """, (mapping_id, pid), ) row = cur.fetchone() if not row: raise HTTPException(404, "Mapping nicht gefunden") norm = row["source_name_normalized"] dates = dates_for_normalized_name(cur, pid, norm) clear_mapping_from_items(cur, pid, norm) cur.execute("DELETE FROM food_name_mappings WHERE id = %s AND profile_id = %s", (mapping_id, pid)) try: with get_db() as conn: _rebuild_days(get_cursor(conn), pid, dates) except Exception: logger.exception("Nährwert-Rebuild nach Mapping-Löschen fehlgeschlagen") return {"ok": True} # keep get_pid imported for consistency with other routers _ = get_pid