"""Desires & Bounties router.""" from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, text from app.database import get_db from app.models import User, Desire, Shader from app.schemas import DesireCreate, DesirePublic from app.middleware.auth import get_current_user, require_tier router = APIRouter() @router.get("", response_model=list[DesirePublic]) async def list_desires( status_filter: str | None = Query(None, alias="status"), min_heat: float = Query(0, ge=0), limit: int = Query(20, ge=1, le=50), offset: int = Query(0, ge=0), db: AsyncSession = Depends(get_db), ): query = select(Desire).where(Desire.heat_score >= min_heat) if status_filter: query = query.where(Desire.status == status_filter) else: query = query.where(Desire.status == "open") query = query.order_by(Desire.heat_score.desc()).limit(limit).offset(offset) result = await db.execute(query) desires = list(result.scalars().all()) # Batch-annotate cluster_count to avoid N+1 queries desire_ids = [d.id for d in desires] if desire_ids: cluster_query = text(""" SELECT dc1.desire_id, COUNT(dc2.desire_id) as cluster_count FROM desire_clusters dc1 JOIN desire_clusters dc2 ON dc1.cluster_id = dc2.cluster_id WHERE dc1.desire_id = ANY(:desire_ids) GROUP BY dc1.desire_id """) cluster_result = await db.execute(cluster_query, {"desire_ids": desire_ids}) cluster_counts = {row[0]: row[1] for row in cluster_result} for d in desires: d.cluster_count = cluster_counts.get(d.id, 0) return desires @router.get("/{desire_id}", response_model=DesirePublic) async def get_desire(desire_id: UUID, db: AsyncSession = Depends(get_db)): result = await db.execute(select(Desire).where(Desire.id == desire_id)) desire = result.scalar_one_or_none() if not desire: raise HTTPException(status_code=404, detail="Desire not found") # Annotate cluster_count for single desire cluster_query = text(""" SELECT COUNT(dc2.desire_id) as cluster_count FROM desire_clusters dc1 JOIN desire_clusters dc2 ON dc1.cluster_id = dc2.cluster_id WHERE dc1.desire_id = :desire_id """) cluster_result = await db.execute(cluster_query, {"desire_id": desire_id}) row = cluster_result.first() desire.cluster_count = row[0] if row else 0 return desire @router.post("", response_model=DesirePublic, status_code=status.HTTP_201_CREATED) async def create_desire( body: DesireCreate, db: AsyncSession = Depends(get_db), user: User = Depends(require_tier("pro", "studio")), ): desire = Desire( author_id=user.id, prompt_text=body.prompt_text, style_hints=body.style_hints, ) db.add(desire) await db.flush() # Fire-and-forget: enqueue embedding + clustering worker task from app.worker import process_desire process_desire.delay(str(desire.id)) return desire @router.post("/{desire_id}/fulfill", status_code=status.HTTP_200_OK) async def fulfill_desire( desire_id: UUID, shader_id: UUID = Query(..., description="Shader that fulfills this desire"), db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user), ): """Mark a desire as fulfilled by a shader. (Track G)""" desire = (await db.execute(select(Desire).where(Desire.id == desire_id))).scalar_one_or_none() if not desire: raise HTTPException(status_code=404, detail="Desire not found") if desire.status != "open": raise HTTPException(status_code=400, detail="Desire is not open") # Validate shader exists and is published shader = (await db.execute(select(Shader).where(Shader.id == shader_id))).scalar_one_or_none() if not shader: raise HTTPException(status_code=404, detail="Shader not found") if shader.status != "published": raise HTTPException(status_code=400, detail="Shader must be published to fulfill a desire") from datetime import datetime, timezone desire.status = "fulfilled" desire.fulfilled_by_shader = shader_id desire.fulfilled_at = datetime.now(timezone.utc) return {"status": "fulfilled", "desire_id": desire_id, "shader_id": shader_id} @router.post("/{desire_id}/tip") async def tip_desire( desire_id: UUID, db: AsyncSession = Depends(get_db), user: User = Depends(require_tier("pro", "studio")), ): """Add a tip to a bounty. (Track H — stub)""" raise HTTPException( status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Bounty tipping coming in M4" )