""" Card import router endpoints. Provides endpoints for importing card collections from files, viewing import status, and confirming imports. """ import json from typing import List, Optional, Dict, Any from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from sqlalchemy.orm import selectinload from datetime import datetime from app.core.database import get_db from app.core.security import get_current_user from app.models.card_import_batch import CardImportBatch from app.models.user_card_import_record import UserCardImportRecord from app.models.user_card_collection import UserCardCollection from app.models.models import MtgonlineCard from app.models.user_deck import UserDeck, UserDeckCard from app.services.file_parser import FileParser from app.services.import_batch_processor import ImportBatchProcessor from app.services.fuzzy_card_matcher import FuzzyCardMatcher from app.schemas.card_import_schemas import ( CardImportRequest, CardImportResponse, CardImportStatusResponse, CardMatchResult, CardImportSummary, MessageResponse, ) from app.schemas.user_deck_schemas import DeckCardResponse, DeckCardListResponse router = APIRouter() @router.post("/import", response_model=CardImportResponse, status_code=status.HTTP_201_CREATED) async def upload_card_import( file: UploadFile = File(...), db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """ Upload a card import file (XLSX, CSV, JSON, ODS). Parses the file, performs fuzzy matching, and creates an import batch. """ user_id = int(current_user["user_id"]) # Validate file type if not file.filename: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="File must have a filename" ) file_type = file.filename.split(".")[-1].lower() supported_types = ["xlsx", "csv", "json", "ods"] if file_type not in supported_types: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Unsupported file type: {file_type}. Supported types: {supported_types}" ) # Read file content content = await file.read() file_size = len(content) # Parse file from pathlib import Path import tempfile with tempfile.NamedTemporaryFile(suffix=f".{file_type}", delete=False) as tmp_file: tmp_file.write(content) tmp_file_path = Path(tmp_file.name) try: card_names = await FileParser.parse_file(tmp_file_path) except Exception as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Failed to parse file: {str(e)}" ) if not card_names: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="File contains no card names" ) # Create import batch batch = await ImportBatchProcessor.create_batch( db=db, user_id=user_id, filename=file.filename, file_type=file_type, file_size=file_size, card_names=card_names, ) # Process batch result = await ImportBatchProcessor.process_batch( db=db, batch=batch, card_names=card_names, ) return CardImportResponse( message=f"Import batch created successfully", batch_id=batch.id, card_count=len(card_names), status=result["status"], imported_at=datetime.utcnow(), ) @router.get("/import/{import_id}/status", response_model=CardImportStatusResponse) async def get_import_status( import_id: int, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """Get the status of an import batch.""" user_id = int(current_user["user_id"]) batch = await ImportBatchProcessor.get_batch_status(db, import_id) if not batch: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Import batch {import_id} not found" ) if batch.user_id != user_id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) return CardImportStatusResponse( has_import=True, batch_id=batch.id, status=batch.status, card_count=batch.total_cards, matched_count=batch.matched_cards, unmatched_count=batch.unmatched_cards, last_imported=batch.updated_at, error_message=batch.error_message, ) @router.get("/import/{import_id}/results", response_model=CardImportSummary) async def get_import_results( import_id: int, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """Get the match results for an import batch.""" user_id = int(current_user["user_id"]) batch = await ImportBatchProcessor.get_batch_status(db, import_id) if not batch: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Import batch {import_id} not found" ) if batch.user_id != user_id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) if batch.status != "completed": raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Import batch {import_id} has not completed yet (status: {batch.status})" ) match_results = batch.match_results or [] matched_cards = [] unmatched_cards = [] for original_name, card_id, matched_name, confidence, match_type in match_results: if matched_name: matched_cards.append(CardMatchResult( card_id=card_id, card_name=original_name, matched_name=matched_name, match_type=match_type, confidence=confidence, )) else: unmatched_cards.append(original_name) return CardImportSummary( total_cards=len(match_results), matched_cards=matched_cards, unmatched_cards=unmatched_cards, import_id=batch.id, ) @router.post("/import/{import_id}/confirm", response_model=MessageResponse) async def confirm_import( import_id: int, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """Confirm an import batch and save to user's card collection.""" user_id = int(current_user["user_id"]) batch = await ImportBatchProcessor.get_batch_status(db, import_id) if not batch: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Import batch {import_id} not found" ) if batch.user_id != user_id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) if batch.status != "completed": raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Import batch {import_id} has not completed yet (status: {batch.status})" ) # Confirm batch try: await ImportBatchProcessor.confirm_batch(db, import_id, user_id) except ValueError as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e) ) # Save matched cards to user's collection match_results = batch.match_results or [] saved_count = 0 for original_name, card_id, matched_name, confidence, match_type in match_results: if card_id and match_type in ["exact", "high_confidence"]: # Check if already in collection stmt = select(UserCardCollection).where( UserCardCollection.user_id == user_id, UserCardCollection.card_id == card_id, ) result = await db.execute(stmt) existing = result.scalar_one_or_none() if existing: existing.quantity += 1 else: new_collection = UserCardCollection( user_id=user_id, card_id=card_id, quantity=1, ) db.add(new_collection) saved_count += 1 await db.flush() return MessageResponse( message=f"Import confirmed. {saved_count} cards added to your collection." ) @router.get("/user/cards", response_model=List[Dict[str, Any]]) async def get_user_cards( db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """List user's imported cards.""" user_id = int(current_user["user_id"]) stmt = select(UserCardCollection).where(UserCardCollection.user_id == user_id).order_by(UserCardCollection.created_at.desc()) result = await db.execute(stmt) collections = result.scalars().all() # Fetch card details card_ids = [c.card_id for c in collections] card_details = {} if card_ids: card_stmt = select(MtgonlineCard).where(MtgonlineCard.id.in_(card_ids)) card_result = await db.execute(card_stmt) for card in card_result.scalars().all(): card_details[card.id] = card result_list = [] for collection in collections: card = card_details.get(collection.card_id) result_list.append({ "collection_id": collection.id, "card_id": collection.card_id, "card_name": card.name if card else f"Card#{collection.card_id}", "card_type_line": card.type_line if card else "", "quantity": collection.quantity, "condition": collection.condition, "language": collection.language, "is_foil": collection.is_foil, "is_alt_art": collection.is_alt_art, "acquired_date": collection.acquired_date, }) return result_list @router.delete("/user/cards/{card_import_id}", response_model=MessageResponse) async def delete_user_card( card_import_id: int, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """Remove a card from user's collection.""" user_id = int(current_user["user_id"]) stmt = select(UserCardCollection).where( UserCardCollection.id == card_import_id, UserCardCollection.user_id == user_id, ) result = await db.execute(stmt) collection = result.scalar_one_or_none() if not collection: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Card not found in your collection" ) await db.delete(collection) await db.flush() return MessageResponse(message="Card removed from your collection") @router.get("/user/decks", response_model=List[Dict[str, Any]]) async def get_user_decks( db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """List user's decks.""" user_id = int(current_user["user_id"]) stmt = select(UserDeck).where(UserDeck.user_id == user_id).order_by(UserDeck.updated_at.desc()) result = await db.execute(stmt) decks = result.scalars().all() result_list = [] for deck in decks: result_list.append({ "deck_id": deck.id, "name": deck.name, "status": deck.status, "format": deck.format, "folder_id": deck.folder_id, "notes": deck.notes, "is_precedent": deck.is_precedent, "created_at": deck.created_at, "updated_at": deck.updated_at, }) return result_list