from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, BackgroundTasks from fastapi.responses import StreamingResponse from typing import Optional, List from io import BytesIO import logging from datetime import datetime from app.schemas.file import ( FileSchema, CreateFileRequest, CreateFileResponse, GetFileResponse, DeleteFileResponse, GetFileQueryParams ) from app.schemas.box import ( CreateBoxResponse, CreateBoxRequest, BoxSchema, UpdateBoxRequest, CreateOpenBoxRequest, CreateOpenBoxResponse, OpenBoxSchema ) from app.services.file import FileService from app.services.box import BoxService from app.services.task import TaskService from app.services.pricing import PricingService from app.dependencies import ( get_file_service, get_box_service, get_task_service, get_create_file_metadata, get_box_data, get_box_update_data, get_open_box_data, get_pricing_service ) logger = logging.getLogger(__name__) router = APIRouter(prefix="/api", tags=["cards"]) MAX_FILE_SIZE = 100 * 1024 * 1024 # 100 MB async def validate_file_upload(file: UploadFile) -> bytes: """Validate uploaded file and return its contents.""" if not file.filename: raise HTTPException(status_code=400, detail="No filename provided") content = await file.read() if len(content) > MAX_FILE_SIZE: raise HTTPException(status_code=413, detail="File too large") return content @router.post("/files", response_model=CreateFileResponse, status_code=201) async def create_file( background_tasks: BackgroundTasks, file: UploadFile = File(...), metadata: CreateFileRequest = Depends(get_create_file_metadata), file_service: FileService = Depends(get_file_service), task_service: TaskService = Depends(get_task_service) ) -> CreateFileResponse: """Create a new file entry with the uploaded file.""" try: content = await validate_file_upload(file) logger.debug(f"File received: {file.filename}") logger.debug(f"Metadata: {metadata}") metadata.filename = metadata.filename or file.filename if not file_service.validate_file(content, metadata): raise HTTPException(status_code=400, detail="Invalid file content") created_file = file_service.create_file(content, metadata) if metadata.source == 'manabox': background_tasks.add_task(task_service.process_manabox_file, created_file) return CreateFileResponse( status_code=201, success=True, files=[FileSchema.from_orm(created_file)] ) except HTTPException as http_ex: raise http_ex except Exception as e: logger.error(f"File upload failed: {str(e)}") raise HTTPException( status_code=500, detail="Internal server error occurred during file upload" ) finally: await file.close() @router.get("/files/{file_id:path}", response_model=GetFileResponse) @router.get("/files", response_model=GetFileResponse) async def get_file( file_id: Optional[str] = None, query: GetFileQueryParams = Depends(), file_service: FileService = Depends(get_file_service) ) -> GetFileResponse: """ Get file(s) by optional ID and/or status. If file_id is provided, returns that specific file. If status is provided, returns all files with that status. If neither is provided, returns all files. """ try: if file_id: file = file_service.get_file(file_id) files = [file] else: files = file_service.get_files(status=query.status) return GetFileResponse( status_code=200, success=True, files=[FileSchema.from_orm(f) for f in files] ) except Exception as e: logger.error(f"Get file(s) failed: {str(e)}") raise HTTPException(status_code=400, detail=str(e)) @router.delete("/files/{file_id}", response_model=DeleteFileResponse) async def delete_file( file_id: str, file_service: FileService = Depends(get_file_service) ) -> DeleteFileResponse: """Delete a file by ID.""" try: file = file_service.delete_file(file_id) return DeleteFileResponse( status_code=200, success=True, files=[FileSchema.from_orm(file)] ) except Exception as e: logger.error(f"Delete file failed: {str(e)}") raise HTTPException(status_code=400, detail=str(e)) @router.post("/boxes", response_model=CreateBoxResponse, status_code=201) async def create_box( box_data: CreateBoxRequest = Depends(get_box_data), box_service: BoxService = Depends(get_box_service) ) -> CreateBoxResponse: """Create a new box.""" try: result, success = box_service.create_box(box_data) if not success: raise HTTPException(status_code=400, detail="Box creation failed, box already exists") return CreateBoxResponse( status_code=201, success=True, box=[BoxSchema.from_orm(result)] ) except Exception as e: logger.error(f"Create box failed: {str(e)}") raise HTTPException(status_code=400, detail=str(e)) @router.put("/boxes/{box_id}", response_model=CreateBoxResponse) async def update_box( box_id: str, box_data: UpdateBoxRequest = Depends(get_box_update_data), box_service: BoxService = Depends(get_box_service) ) -> CreateBoxResponse: """Update an existing box.""" try: result = box_service.update_box(box_id, box_data) return CreateBoxResponse( status_code=200, success=True, box=[BoxSchema.from_orm(result)] ) except Exception as e: logger.error(f"Update box failed: {str(e)}") raise HTTPException(status_code=400, detail=str(e)) @router.delete("/boxes/{box_id}", response_model=CreateBoxResponse) async def delete_box( box_id: str, box_service: BoxService = Depends(get_box_service) ) -> CreateBoxResponse: """Delete a box by ID.""" try: result = box_service.delete_box(box_id) return CreateBoxResponse( status_code=200, success=True, box=[BoxSchema.from_orm(result)] ) except Exception as e: logger.error(f"Delete box failed: {str(e)}") raise HTTPException(status_code=400, detail=str(e)) @router.post("/boxes/{box_id}/open", response_model=CreateOpenBoxResponse, status_code=201) async def open_box( box_id: str, box_data: CreateOpenBoxRequest = Depends(get_open_box_data), box_service: BoxService = Depends(get_box_service) ) -> CreateOpenBoxResponse: """Open a box by ID.""" try: result = box_service.open_box(box_id, box_data) return CreateOpenBoxResponse( status_code=201, success=True, open_box=[OpenBoxSchema.from_orm(result)] ) except Exception as e: logger.error(f"Open box failed: {str(e)}") raise HTTPException(status_code=400, detail=str(e)) @router.delete("/boxes/{box_id}/open", response_model=CreateOpenBoxResponse, status_code=200) async def delete_open_box( box_id: str, box_service: BoxService = Depends(get_box_service) ) -> CreateOpenBoxResponse: """Delete an open box by ID.""" try: result = box_service.delete_open_box(box_id) return CreateOpenBoxResponse( status_code=201, success=True, open_box=[OpenBoxSchema.from_orm(result)] ) except Exception as e: logger.error(f"Delete open box failed: {str(e)}") raise HTTPException(status_code=400, detail=str(e) ) @router.post("/tcgplayer/inventory/add", response_class=StreamingResponse) async def create_inventory_add_file( request: dict, # Just use a dict instead pricing_service: PricingService = Depends(get_pricing_service), ): """Create a new inventory add file for download.""" try: # Get IDs directly from the dict open_box_ids = request.get('open_box_ids', []) content = pricing_service.generate_tcgplayer_inventory_update_file_with_pricing(open_box_ids) stream = BytesIO(content) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") return StreamingResponse( iter([stream.getvalue()]), media_type="text/csv", headers={ 'Content-Disposition': f'attachment; filename="inventory_add_{timestamp}.csv"' } ) except Exception as e: logger.error(f"Create inventory add file failed: {str(e)}") raise HTTPException(status_code=400, detail=str(e)) @router.get("/tcgplayer/inventory/update", response_class=StreamingResponse) async def create_inventory_update_file( pricing_service: PricingService = Depends(get_pricing_service), ): """Create a new inventory update file for download.""" try: content = pricing_service.generate_tcgplayer_inventory_update_file_with_pricing() stream = BytesIO(content) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") return StreamingResponse( iter([stream.getvalue()]), media_type="text/csv", headers={ 'Content-Disposition': f'attachment; filename="inventory_update_{timestamp}.csv"' } ) except Exception as e: logger.error(f"Create inventory update file failed: {str(e)}") raise HTTPException(status_code=400, detail=str(e))