from fastapi import APIRouter, Depends, HTTPException, Query from typing import List, Optional, Dict, Any import logging from datetime import datetime, timedelta from pydantic import BaseModel, Field from playhouse.shortcuts import model_to_dict from peewee import fn from ..dependencies import oauth2_scheme, valid_token, PRIVATE_IN_SCHEMA, handle_db_errors from ..db_engine import db, HelpCommand logger = logging.getLogger('database_api') router = APIRouter( prefix='/api/v3/help_commands', tags=['help_commands'] ) # Pydantic Models for API class HelpCommandModel(BaseModel): id: Optional[int] = None name: str = Field(..., min_length=2, max_length=32) title: str = Field(..., min_length=1, max_length=200) content: str = Field(..., min_length=1, max_length=4000) category: Optional[str] = Field(None, max_length=50) created_by_discord_id: str # Text to safely store Discord snowflake IDs created_at: Optional[str] = None updated_at: Optional[str] = None last_modified_by: Optional[str] = None # Text to safely store Discord snowflake IDs is_active: bool = True view_count: int = 0 display_order: int = 0 class HelpCommandListResponse(BaseModel): help_commands: List[HelpCommandModel] total_count: int page: int page_size: int total_pages: int has_more: bool class HelpCommandStatsResponse(BaseModel): total_commands: int active_commands: int total_views: int most_viewed_command: Optional[Dict[str, Any]] = None recent_commands_count: int = 0 # API Endpoints @router.get('') @handle_db_errors async def get_help_commands( name: Optional[str] = None, category: Optional[str] = None, is_active: Optional[bool] = True, sort: Optional[str] = 'name', page: int = Query(1, ge=1), page_size: int = Query(25, ge=1, le=100) ): """Get help commands with filtering and pagination""" try: # Build query query = HelpCommand.select() # Apply filters if name is not None: query = query.where(fn.Lower(HelpCommand.name).contains(name.lower())) if category is not None: query = query.where(fn.Lower(HelpCommand.category) == category.lower()) if is_active is not None: query = query.where(HelpCommand.is_active == is_active) # Get total count before pagination total_count = query.count() # Apply sorting sort_mapping = { 'name': HelpCommand.name, 'title': HelpCommand.title, 'category': HelpCommand.category, 'created_at': HelpCommand.created_at, 'updated_at': HelpCommand.updated_at, 'view_count': HelpCommand.view_count, 'display_order': HelpCommand.display_order } if sort.startswith('-'): sort_field = sort[1:] order_by = sort_mapping.get(sort_field, HelpCommand.name).desc() else: order_by = sort_mapping.get(sort, HelpCommand.name).asc() query = query.order_by(order_by) # Calculate pagination offset = (page - 1) * page_size total_pages = (total_count + page_size - 1) // page_size # Apply pagination query = query.limit(page_size).offset(offset) # Convert to dictionaries commands = [] for help_cmd in query: cmd_dict = model_to_dict(help_cmd) # Convert datetime objects to ISO strings if cmd_dict.get('created_at'): cmd_dict['created_at'] = cmd_dict['created_at'].isoformat() if cmd_dict.get('updated_at'): cmd_dict['updated_at'] = cmd_dict['updated_at'].isoformat() try: command_model = HelpCommandModel(**cmd_dict) commands.append(command_model) except Exception as e: logger.error(f"Error creating HelpCommandModel: {e}, data: {cmd_dict}") continue return HelpCommandListResponse( help_commands=commands, total_count=total_count, page=page, page_size=page_size, total_pages=total_pages, has_more=page < total_pages ) except Exception as e: logger.error(f"Error getting help commands: {e}") raise HTTPException(status_code=500, detail=str(e)) finally: db.close() @router.post('', include_in_schema=PRIVATE_IN_SCHEMA) @handle_db_errors async def create_help_command_endpoint( command: HelpCommandModel, token: str = Depends(oauth2_scheme) ): """Create a new help command""" if not valid_token(token): logger.warning(f'create_help_command - Bad Token: {token}') raise HTTPException(status_code=401, detail='Unauthorized') try: # Check if command name already exists existing = HelpCommand.get_by_name(command.name) if existing: raise HTTPException(status_code=409, detail=f"Help topic '{command.name}' already exists") # Create the command help_cmd = HelpCommand.create( name=command.name.lower(), title=command.title, content=command.content, category=command.category.lower() if command.category else None, created_by_discord_id=command.created_by_discord_id, created_at=datetime.now(), is_active=True, view_count=0, display_order=command.display_order ) # Return the created command result = model_to_dict(help_cmd) if result.get('created_at'): result['created_at'] = result['created_at'].isoformat() if result.get('updated_at'): result['updated_at'] = result['updated_at'].isoformat() return result except HTTPException: raise except Exception as e: logger.error(f"Error creating help command: {e}") raise HTTPException(status_code=500, detail=str(e)) finally: db.close() @router.put('/{command_id}', include_in_schema=PRIVATE_IN_SCHEMA) @handle_db_errors async def update_help_command_endpoint( command_id: int, command: HelpCommandModel, token: str = Depends(oauth2_scheme) ): """Update an existing help command""" if not valid_token(token): logger.warning(f'update_help_command - Bad Token: {token}') raise HTTPException(status_code=401, detail='Unauthorized') try: # Get the command help_cmd = HelpCommand.get_or_none(HelpCommand.id == command_id) if not help_cmd: raise HTTPException(status_code=404, detail=f"Help command {command_id} not found") # Update fields if command.title: help_cmd.title = command.title if command.content: help_cmd.content = command.content if command.category is not None: help_cmd.category = command.category.lower() if command.category else None if command.last_modified_by: help_cmd.last_modified_by = command.last_modified_by if command.display_order is not None: help_cmd.display_order = command.display_order help_cmd.updated_at = datetime.now() help_cmd.save() # Return updated command result = model_to_dict(help_cmd) if result.get('created_at'): result['created_at'] = result['created_at'].isoformat() if result.get('updated_at'): result['updated_at'] = result['updated_at'].isoformat() return result except HTTPException: raise except Exception as e: logger.error(f"Error updating help command {command_id}: {e}") raise HTTPException(status_code=500, detail=str(e)) finally: db.close() @router.patch('/{command_id}/restore', include_in_schema=PRIVATE_IN_SCHEMA) @handle_db_errors async def restore_help_command_endpoint( command_id: int, token: str = Depends(oauth2_scheme) ): """Restore a soft-deleted help command""" if not valid_token(token): logger.warning(f'restore_help_command - Bad Token: {token}') raise HTTPException(status_code=401, detail='Unauthorized') try: # Get the command help_cmd = HelpCommand.get_or_none(HelpCommand.id == command_id) if not help_cmd: raise HTTPException(status_code=404, detail=f"Help command {command_id} not found") # Restore the command help_cmd.restore() # Return restored command result = model_to_dict(help_cmd) if result.get('created_at'): result['created_at'] = result['created_at'].isoformat() if result.get('updated_at'): result['updated_at'] = result['updated_at'].isoformat() return result except HTTPException: raise except Exception as e: logger.error(f"Error restoring help command {command_id}: {e}") raise HTTPException(status_code=500, detail=str(e)) finally: db.close() @router.delete('/{command_id}', include_in_schema=PRIVATE_IN_SCHEMA) @handle_db_errors async def delete_help_command_endpoint( command_id: int, token: str = Depends(oauth2_scheme) ): """Soft delete a help command""" if not valid_token(token): logger.warning(f'delete_help_command - Bad Token: {token}') raise HTTPException(status_code=401, detail='Unauthorized') try: # Get the command help_cmd = HelpCommand.get_or_none(HelpCommand.id == command_id) if not help_cmd: raise HTTPException(status_code=404, detail=f"Help command {command_id} not found") # Soft delete the command help_cmd.soft_delete() return {"message": f"Help command {command_id} deleted successfully"} except HTTPException: raise except Exception as e: logger.error(f"Error deleting help command {command_id}: {e}") raise HTTPException(status_code=500, detail=str(e)) finally: db.close() @router.get('/stats') @handle_db_errors async def get_help_command_stats(): """Get comprehensive statistics about help commands""" try: # Get basic counts total_commands = HelpCommand.select().count() active_commands = HelpCommand.select().where(HelpCommand.is_active == True).count() # Get total views total_views = HelpCommand.select(fn.SUM(HelpCommand.view_count)).where( HelpCommand.is_active == True ).scalar() or 0 # Get most viewed command most_viewed = HelpCommand.get_most_viewed(limit=1).first() most_viewed_command = None if most_viewed: most_viewed_dict = model_to_dict(most_viewed) if most_viewed_dict.get('created_at'): most_viewed_dict['created_at'] = most_viewed_dict['created_at'].isoformat() if most_viewed_dict.get('updated_at'): most_viewed_dict['updated_at'] = most_viewed_dict['updated_at'].isoformat() most_viewed_command = most_viewed_dict # Get recent commands count (last 7 days) week_ago = datetime.now() - timedelta(days=7) recent_count = HelpCommand.select().where( (HelpCommand.created_at >= week_ago) & (HelpCommand.is_active == True) ).count() return HelpCommandStatsResponse( total_commands=total_commands, active_commands=active_commands, total_views=int(total_views), most_viewed_command=most_viewed_command, recent_commands_count=recent_count ) except Exception as e: logger.error(f"Error getting help command stats: {e}") raise HTTPException(status_code=500, detail=str(e)) finally: db.close() # Special endpoints for Discord bot integration @router.get('/by_name/{command_name}') @handle_db_errors async def get_help_command_by_name_endpoint( command_name: str, include_inactive: bool = Query(False) ): """Get a help command by name (for Discord bot)""" try: help_cmd = HelpCommand.get_by_name(command_name, include_inactive=include_inactive) if not help_cmd: raise HTTPException(status_code=404, detail=f"Help topic '{command_name}' not found") result = model_to_dict(help_cmd) if result.get('created_at'): result['created_at'] = result['created_at'].isoformat() if result.get('updated_at'): result['updated_at'] = result['updated_at'].isoformat() return result except HTTPException: raise except Exception as e: logger.error(f"Error getting help command by name '{command_name}': {e}") raise HTTPException(status_code=500, detail=str(e)) finally: db.close() @router.patch('/by_name/{command_name}/view', include_in_schema=PRIVATE_IN_SCHEMA) @handle_db_errors async def increment_view_count( command_name: str, token: str = Depends(oauth2_scheme) ): """Increment view count for a help command""" if not valid_token(token): logger.warning(f'increment_view_count - Bad Token: {token}') raise HTTPException(status_code=401, detail='Unauthorized') try: help_cmd = HelpCommand.get_by_name(command_name) if not help_cmd: raise HTTPException(status_code=404, detail=f"Help topic '{command_name}' not found") # Increment view count help_cmd.increment_view_count() # Return updated command result = model_to_dict(help_cmd) if result.get('created_at'): result['created_at'] = result['created_at'].isoformat() if result.get('updated_at'): result['updated_at'] = result['updated_at'].isoformat() return result except HTTPException: raise except Exception as e: logger.error(f"Error incrementing view count for '{command_name}': {e}") raise HTTPException(status_code=500, detail=str(e)) finally: db.close() @router.get('/autocomplete') @handle_db_errors async def get_help_names_for_autocomplete( q: str = Query(""), limit: int = Query(25, ge=1, le=100) ): """Get help command names for Discord autocomplete""" try: if q: results = HelpCommand.search_by_name(q, limit=limit) else: results = HelpCommand.get_all_active().limit(limit) # Return list of dictionaries with name, title, category return { 'results': [ { 'name': help_cmd.name, 'title': help_cmd.title, 'category': help_cmd.category } for help_cmd in results ] } except Exception as e: logger.error(f"Error getting help names for autocomplete: {e}") raise HTTPException(status_code=500, detail=str(e)) finally: db.close() @router.get('/{command_id}') @handle_db_errors async def get_help_command(command_id: int): """Get a single help command by ID""" try: help_cmd = HelpCommand.get_or_none(HelpCommand.id == command_id) if not help_cmd: raise HTTPException(status_code=404, detail=f"Help command {command_id} not found") result = model_to_dict(help_cmd) if result.get('created_at'): result['created_at'] = result['created_at'].isoformat() if result.get('updated_at'): result['updated_at'] = result['updated_at'].isoformat() return result except HTTPException: raise except Exception as e: logger.error(f"Error getting help command {command_id}: {e}") raise HTTPException(status_code=500, detail=str(e)) finally: db.close()