from typing import Any, Dict, List, Optional from fastapi import APIRouter, Depends, HTTPException, status from src.server.models.config import AppConfig, ConfigUpdate, ValidationResult from src.server.services.config_service import ( ConfigBackupError, ConfigServiceError, ConfigValidationError, get_config_service, ) from src.server.utils.dependencies import require_auth router = APIRouter(prefix="/api/config", tags=["config"]) @router.get("", response_model=AppConfig) def get_config(auth: Optional[dict] = Depends(require_auth)) -> AppConfig: """Return current application configuration.""" try: config_service = get_config_service() return config_service.load_config() except ConfigServiceError as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to load config: {e}" ) from e @router.put("", response_model=AppConfig) def update_config( update: ConfigUpdate, auth: dict = Depends(require_auth) ) -> AppConfig: """Apply an update to the configuration and persist it. Creates automatic backup before applying changes. """ try: config_service = get_config_service() return config_service.update_config(update) except ConfigValidationError as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Invalid configuration: {e}" ) from e except ConfigServiceError as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to update config: {e}" ) from e @router.post("/validate", response_model=ValidationResult) def validate_config( cfg: AppConfig, auth: dict = Depends(require_auth) # noqa: ARG001 ) -> ValidationResult: """Validate a provided AppConfig without applying it. Returns ValidationResult with any validation errors. """ try: config_service = get_config_service() return config_service.validate_config(cfg) except Exception as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e) ) from e @router.get("/backups", response_model=List[Dict[str, object]]) def list_backups( auth: dict = Depends(require_auth) ) -> List[Dict[str, object]]: """List all available configuration backups. Returns list of backup metadata including name, size, and created time. """ try: config_service = get_config_service() return config_service.list_backups() except ConfigServiceError as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to list backups: {e}" ) from e @router.post("/backups", response_model=Dict[str, str]) def create_backup( name: Optional[str] = None, auth: dict = Depends(require_auth) ) -> Dict[str, str]: """Create a backup of the current configuration. Args: name: Optional custom backup name (timestamp used if not provided) Returns: Dictionary with backup name and message """ try: config_service = get_config_service() backup_path = config_service.create_backup(name) return { "name": backup_path.name, "message": "Backup created successfully" } except ConfigBackupError as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Failed to create backup: {e}" ) from e @router.post("/backups/{backup_name}/restore", response_model=AppConfig) def restore_backup( backup_name: str, auth: dict = Depends(require_auth) ) -> AppConfig: """Restore configuration from a backup. Creates backup of current config before restoring. Args: backup_name: Name of backup file to restore Returns: Restored configuration """ try: config_service = get_config_service() return config_service.restore_backup(backup_name) except ConfigBackupError as e: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Failed to restore backup: {e}" ) from e @router.delete("/backups/{backup_name}") def delete_backup( backup_name: str, auth: dict = Depends(require_auth) ) -> Dict[str, str]: """Delete a configuration backup. Args: backup_name: Name of backup file to delete Returns: Success message """ try: config_service = get_config_service() config_service.delete_backup(backup_name) return {"message": f"Backup '{backup_name}' deleted successfully"} except ConfigBackupError as e: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Failed to delete backup: {e}" ) from e @router.get("/section/advanced", response_model=Dict[str, object]) def get_advanced_config( auth: Optional[dict] = Depends(require_auth) ) -> Dict[str, object]: """Get advanced configuration section. Returns: Dictionary with advanced configuration settings """ try: config_service = get_config_service() app_config = config_service.load_config() return app_config.other.get("advanced", {}) except ConfigServiceError as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to load advanced config: {e}" ) from e @router.post("/section/advanced", response_model=Dict[str, str]) def update_advanced_config( config: Dict[str, object], auth: dict = Depends(require_auth) ) -> Dict[str, str]: """Update advanced configuration section. Args: config: Advanced configuration settings auth: Authentication token (required) Returns: Success message """ try: config_service = get_config_service() app_config = config_service.load_config() # Update advanced section in other if "advanced" not in app_config.other: app_config.other["advanced"] = {} app_config.other["advanced"].update(config) config_service.save_config(app_config) return {"message": "Advanced configuration updated successfully"} except ConfigServiceError as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to update advanced config: {e}" ) from e @router.post("/directory", response_model=Dict[str, Any]) async def update_directory( directory_config: Dict[str, str], auth: dict = Depends(require_auth) ) -> Dict[str, Any]: """Update anime directory configuration. Args: directory_config: Dictionary with 'directory' key auth: Authentication token (required) Returns: Success message """ try: directory = directory_config.get("directory") if not directory: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Directory path is required" ) config_service = get_config_service() app_config = config_service.load_config() # Store directory in other section app_config.other["anime_directory"] = directory config_service.save_config(app_config) # Sync series from data files to database sync_count = 0 try: import structlog from src.server.services.anime_service import sync_series_from_data_files logger = structlog.get_logger(__name__) sync_count = await sync_series_from_data_files(directory, logger) logger.info( "Directory updated: synced series from data files", directory=directory, count=sync_count ) except Exception as e: # Log but don't fail the directory update if sync fails import structlog structlog.get_logger(__name__).warning( "Failed to sync series after directory update", error=str(e) ) response: Dict[str, Any] = { "message": "Anime directory updated successfully", "synced_series": sync_count } return response except ConfigServiceError as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to update directory: {e}" ) from e @router.post("/export") async def export_config( export_options: Dict[str, bool], auth: dict = Depends(require_auth) ): """Export configuration to JSON file. Args: export_options: Options for export (include_sensitive, etc.) auth: Authentication token (required) Returns: JSON file download response """ try: import json from fastapi.responses import Response config_service = get_config_service() app_config = config_service.load_config() # Convert to dict config_dict = app_config.model_dump() # Optionally remove sensitive data if not export_options.get("include_sensitive", False): # Remove sensitive fields if present config_dict.pop("password_salt", None) config_dict.pop("password_hash", None) # Create filename with timestamp from datetime import datetime timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") filename = f"aniworld_config_{timestamp}.json" # Return as downloadable JSON content = json.dumps(config_dict, indent=2) return Response( content=content, media_type="application/json", headers={ "Content-Disposition": f'attachment; filename="{filename}"' } ) except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to export config: {e}" ) from e @router.post("/reset", response_model=Dict[str, str]) def reset_config( reset_options: Dict[str, bool], auth: dict = Depends(require_auth) ) -> Dict[str, str]: """Reset configuration to defaults. Args: reset_options: Options for reset (preserve_security, etc.) auth: Authentication token (required) Returns: Success message """ try: config_service = get_config_service() # Create backup before resetting config_service.create_backup("pre_reset") # Load default config default_config = AppConfig() # If preserve_security is True, keep authentication settings if reset_options.get("preserve_security", True): current_config = config_service.load_config() # Preserve security-related fields from other if "password_salt" in current_config.other: default_config.other["password_salt"] = ( current_config.other["password_salt"] ) if "password_hash" in current_config.other: default_config.other["password_hash"] = ( current_config.other["password_hash"] ) # Save default config config_service.save_config(default_config) return { "message": "Configuration reset to defaults successfully" } except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to reset config: {e}" ) from e