import logging from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks from sqlalchemy.orm import Session from typing import List, Dict import asyncio from datetime import datetime from api.auth import get_current_user from models import db_models from models.schemas import FlashcardGenerateRequest, FlashcardSetResponse, FlashcardResponse from core.database import get_db, SessionLocal from api.websocket_routes import manager from services.flashcard_service import flashcard_service from core import constants router = APIRouter(prefix="/api/flashcards", tags=["flashcards"]) logger = logging.getLogger(__name__) async def run_flashcard_generation(set_id: int, request: FlashcardGenerateRequest, user_id: int): """Background task for flashcard generation""" db = SessionLocal() connection_id = f"user_{user_id}" try: db_set = db.query(db_models.FlashcardSet).filter(db_models.FlashcardSet.id == set_id).first() if not db_set: return # Call AI service cards_data = await flashcard_service.generate_flashcards( file_key=request.file_key, text_input=request.text_input, difficulty=request.difficulty, quantity=request.quantity, topic=request.topic, language=request.language, progress_callback=lambda p, m: asyncio.create_task( manager.send_progress(connection_id, p, "processing", m) ) ) if not cards_data: raise Exception("AI returned empty flashcards data") # Save individual cards for item in cards_data: db_card = db_models.Flashcard( flashcard_set_id=db_set.id, question=item.get("question", ""), answer=item.get("answer", "") ) db.add(db_card) db_set.status = "completed" db.commit() # Notify via WebSocket await manager.send_result(connection_id, { "type": "flashcards", "id": db_set.id, "status": "completed", "title": db_set.title }) except Exception as e: logger.error(f"Background flashcard generation failed: {e}") db_set = db.query(db_models.FlashcardSet).filter(db_models.FlashcardSet.id == set_id).first() if db_set: db_set.status = "failed" db_set.error_message = str(e) db.commit() await manager.send_error(connection_id, f"Flashcard generation failed: {str(e)}") finally: db.close() @router.get("/config") async def get_flashcard_config(): """Returns available difficulties, quantities, and languages for flashcards.""" return { "difficulties": constants.DIFFICULTIES, "quantities": constants.FLASHCARD_QUANTITIES, "languages": constants.LANGUAGES } @router.post("/generate", response_model=FlashcardSetResponse) async def generate_flashcards( request: FlashcardGenerateRequest, background_tasks: BackgroundTasks, current_user: db_models.User = Depends(get_current_user), db: Session = Depends(get_db) ): """ Initiates flashcard generation in the background. """ source_id = None if request.file_key: source = db.query(db_models.Source).filter( db_models.Source.s3_key == request.file_key, db_models.Source.user_id == current_user.id ).first() if not source: raise HTTPException(status_code=403, detail="Not authorized to access this file") source_id = source.id # Create initial processing record file_base = request.file_key.split('/')[-1].rsplit('.', 1)[0] if request.file_key else None # Priority: 1. File-based name, 2. User Topic (if not default 'string'), 3. Default timestamp if file_base: title = f"Flashcard-{file_base}" elif request.topic and request.topic != "string": title = request.topic else: title = f"Flashcards {datetime.utcnow().strftime('%Y-%m-%d %H:%M')}" db_set = db_models.FlashcardSet( title=title, difficulty=request.difficulty, user_id=current_user.id, source_id=source_id, status="processing" ) db.add(db_set) db.commit() db.refresh(db_set) # Offload to background task background_tasks.add_task(run_flashcard_generation, db_set.id, request, current_user.id) return db_set @router.get("/sets", response_model=List[FlashcardSetResponse]) async def list_flashcard_sets( current_user: db_models.User = Depends(get_current_user), db: Session = Depends(get_db) ): """ Lists all flashcard sets for the current user. """ try: sets = db.query(db_models.FlashcardSet).filter( db_models.FlashcardSet.user_id == current_user.id ).order_by(db_models.FlashcardSet.created_at.desc()).all() return [FlashcardSetResponse.model_validate(s) for s in sets] except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.get("/set/{set_id}", response_model=FlashcardSetResponse) async def get_flashcard_set( set_id: int, current_user: db_models.User = Depends(get_current_user), db: Session = Depends(get_db) ): """ Retrieves a specific flashcard set. """ db_set = db.query(db_models.FlashcardSet).filter( db_models.FlashcardSet.id == set_id, db_models.FlashcardSet.user_id == current_user.id ).first() if not db_set: raise HTTPException(status_code=404, detail="Flashcard set not found") return FlashcardSetResponse.model_validate(db_set) @router.get("/explain") async def explain_flashcard( question: str, file_key: str = None, language: str = "English", current_user: db_models.User = Depends(get_current_user)): """ Provides a detailed explanation for a specific question. """ try: explanation = await flashcard_service.generate_explanation( question=question, file_key=file_key, language=language ) return {"explanation": explanation} except Exception as e: logger.error(f"Explanation failed: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.delete("/set/{set_id}") async def delete_flashcard_set( set_id: int, current_user: db_models.User = Depends(get_current_user), db: Session = Depends(get_db) ): """ Deletes a specific flashcard set and all its cards. """ db_set = db.query(db_models.FlashcardSet).filter( db_models.FlashcardSet.id == set_id, db_models.FlashcardSet.user_id == current_user.id ).first() if not db_set: raise HTTPException(status_code=404, detail="Flashcard set not found") db.delete(db_set) db.commit() return {"message": "Flashcard set and all associated cards deleted successfully"}