from __future__ import annotations import uuid from typing import Optional from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession from db.models import Topic, User from deps.auth import get_current_user from deps.db import get_db from services import classifier, storage router = APIRouter(prefix="/api/topics", tags=["topics"]) class TopicCreate(BaseModel): name: str description: str = "" color: str = "#6366f1" class TopicUpdate(BaseModel): name: Optional[str] = None description: Optional[str] = None color: Optional[str] = None class SuggestRequest(BaseModel): document_id: str @router.get("") async def list_topics( session: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """List topics visible to the current user: system topics + their own topics. D-08 + D-17: namespace-scoped query — returns system topics (user_id IS NULL) + the authenticated user's own topics. Never returns other users' topics. """ topics = await storage.load_topics_for_user(session, user_id=current_user.id) counts = await storage.topic_doc_counts(session, user_id=current_user.id) for t in topics: t["doc_count"] = counts.get(t["name"], 0) return {"topics": topics} @router.post("") async def create_topic( body: TopicCreate, session: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """Create a per-user topic (user_id = current_user.id). D-09: regular users can only create topics in their own namespace. For system topics (user_id = NULL), use POST /api/admin/topics. Deduplication is scoped to the user's namespace. """ topic = await storage.create_topic( session, body.name, body.description, body.color, user_id=current_user.id ) topic["doc_count"] = 0 return topic @router.patch("/{topic_id}") async def update_topic( topic_id: str, body: TopicUpdate, session: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """Update a user-owned topic. D-09: users can only edit their own topics. System topics (user_id IS NULL) and other users' topics return 404 (not 403) to avoid information leakage. """ try: uid = uuid.UUID(topic_id) except ValueError: raise HTTPException(404, "Topic not found") t = await session.get(Topic, uid) if t is None or t.user_id != current_user.id: raise HTTPException(404, "Topic not found") topic = await storage.update_topic( session, topic_id, name=body.name, description=body.description, color=body.color, ) if topic is None: raise HTTPException(404, "Topic not found") counts = await storage.topic_doc_counts(session, user_id=current_user.id) topic["doc_count"] = counts.get(topic["name"], 0) return topic @router.delete("/{topic_id}") async def delete_topic( topic_id: str, session: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """Delete a user-owned topic. D-09: users can only delete their own topics. System topics (user_id IS NULL) and other users' topics return 404 (not 403) to avoid information leakage. """ try: uid = uuid.UUID(topic_id) except ValueError: raise HTTPException(404, "Topic not found") t = await session.get(Topic, uid) if t is None or t.user_id != current_user.id: raise HTTPException(404, "Topic not found") name = await storage.delete_topic(session, topic_id) if name is None: raise HTTPException(404, "Topic not found") return {"success": True, "removed_from_documents": True} @router.post("/suggest") async def suggest_topics( body: SuggestRequest, session: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """Suggest topics for a document using AI. D-11: classifier uses the user's namespace (system + user topics) for suggestions. """ meta = await storage.get_metadata(session, body.document_id) if meta is None: raise HTTPException(404, "Document not found") try: suggestions = await classifier.suggest_topics_for_document(session, body.document_id) except Exception as e: raise HTTPException(500, f"Suggestion failed: {e}") return {"suggested": suggestions}