from __future__ import annotations from typing import List from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Query, Response, status from sqlalchemy.orm import Session from app.database import get_db from app.models import Ingredient, NeverSuggest, NeverSuggestReason, Recipe from app.schemas.never_suggest import NeverSuggestCreate, NeverSuggestRead from app.security import require_admin, require_session public_router = APIRouter(prefix="/api/never-suggest", tags=["never-suggest"]) admin_router = APIRouter( prefix="/api/admin/never-suggest", tags=["never-suggest-admin"], dependencies=[Depends(require_admin)], ) def _coerce_reason(raw: str | None) -> NeverSuggestReason | None: if raw is None: return None try: return NeverSuggestReason(raw) except ValueError: raise HTTPException(status_code=422, detail=f"unknown reason: {raw}") def _attach_names(rows: List[NeverSuggest], db: Session) -> List[dict]: """Hydrate recipe_name / ingredient_name for the response. One LEFT OUTER JOIN per kind, then merge into the response dicts. A second pass would be a `selectinload` if the list grows; for the family-scale (dozens of rows) this is simpler and fast enough. """ recipe_ids = {r.recipe_id for r in rows if r.recipe_id} ingredient_ids = {r.ingredient_id for r in rows if r.ingredient_id} recipe_map: dict[UUID, str] = {} if recipe_ids: for rid, name in db.query(Recipe.id, Recipe.name).filter(Recipe.id.in_(recipe_ids)).all(): recipe_map[rid] = name ingredient_map: dict[UUID, str] = {} if ingredient_ids: for iid, name in db.query(Ingredient.id, Ingredient.name).filter(Ingredient.id.in_(ingredient_ids)).all(): ingredient_map[iid] = name out = [] for r in rows: d = { "id": r.id, "family_profile_id": r.family_profile_id, "ingredient_id": r.ingredient_id, "recipe_id": r.recipe_id, "reason": r.reason.value if r.reason else None, "notes": r.notes, "recipe_name": recipe_map.get(r.recipe_id) if r.recipe_id else None, "ingredient_name": ingredient_map.get(r.ingredient_id) if r.ingredient_id else None, } out.append(d) return out @public_router.get("", response_model=List[NeverSuggestRead]) def list_for_family( family_profile_id: UUID = Query(...), db: Session = Depends(get_db), ): rows = ( db.query(NeverSuggest) .filter(NeverSuggest.family_profile_id == family_profile_id) .all() ) return _attach_names(rows, db) @public_router.post("", response_model=NeverSuggestRead, status_code=status.HTTP_201_CREATED) def add_block( payload: NeverSuggestCreate, db: Session = Depends(get_db), _session: str = Depends(require_session), ): """Family-facing: mark a recipe (or ingredient) as never-suggest. Idempotent on (family_profile_id, recipe_id, reason). Re-adding the same row returns the existing row instead of creating a duplicate. The on-conflict check is a single SELECT + INSERT; small enough that we don't need a unique index. """ existing = ( db.query(NeverSuggest) .filter( NeverSuggest.family_profile_id == payload.family_profile_id, NeverSuggest.recipe_id == payload.recipe_id, NeverSuggest.ingredient_id == payload.ingredient_id, NeverSuggest.reason == _coerce_reason(payload.reason), ) .first() ) if existing is not None: return _attach_names([existing], db)[0] row = NeverSuggest( family_profile_id=payload.family_profile_id, ingredient_id=payload.ingredient_id, recipe_id=payload.recipe_id, reason=_coerce_reason(payload.reason), notes=payload.notes, ) db.add(row) db.commit() db.refresh(row) return _attach_names([row], db)[0] @public_router.delete( "/{ns_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response, ) def remove_block( ns_id: UUID, db: Session = Depends(get_db), session: str = Depends(require_session), ) -> Response: """Family-facing: undo a never-suggest (used by the toast Undo button). require_session resolves to the auto-detected family id. The row's family_profile_id must match — otherwise a 403 prevents one family from removing another family's block. """ row = db.query(NeverSuggest).filter(NeverSuggest.id == ns_id).first() if row is None: raise HTTPException(status_code=404, detail="never-suggest entry not found") if str(row.family_profile_id) != session: raise HTTPException( status_code=403, detail="never-suggest entry belongs to a different family", ) db.delete(row) db.commit() return Response(status_code=204) @admin_router.post("", response_model=NeverSuggestRead, status_code=status.HTTP_201_CREATED) def block(payload: NeverSuggestCreate, db: Session = Depends(get_db)): row = NeverSuggest( family_profile_id=payload.family_profile_id, ingredient_id=payload.ingredient_id, recipe_id=payload.recipe_id, reason=_coerce_reason(payload.reason), notes=payload.notes, ) db.add(row) db.commit() db.refresh(row) return _attach_names([row], db)[0] @admin_router.delete( "/{ns_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response, ) def unblock(ns_id: UUID, db: Session = Depends(get_db)) -> Response: row = db.query(NeverSuggest).filter(NeverSuggest.id == ns_id).first() if row is None: raise HTTPException(status_code=404, detail="never-suggest entry not found") db.delete(row) db.commit() return Response(status_code=204)