import logging from rest_framework import status from rest_framework.decorators import action from rest_framework.viewsets import ViewSet from drf_spectacular.utils import extend_schema, extend_schema_view from igny8_core.api.permissions import IsAuthenticatedAndActive, IsEditorOrAbove from igny8_core.api.response import success_response, error_response from igny8_core.api.throttles import DebugScopedRateThrottle from igny8_core.business.content.models import Content from igny8_core.business.optimization.services.optimizer_service import OptimizerService from igny8_core.business.billing.exceptions import InsufficientCreditsError from igny8_core.modules.optimizer.serializers import ( OptimizeContentSerializer, BatchOptimizeContentSerializer, AnalyzeContentSerializer, ) logger = logging.getLogger(__name__) @extend_schema_view( optimize=extend_schema(tags=['Optimizer']), batch_optimize=extend_schema(tags=['Optimizer']), analyze=extend_schema(tags=['Optimizer']), ) class OptimizerViewSet(ViewSet): """ API endpoints for content optimization operations. Unified API Standard v1.0 compliant """ permission_classes = [IsAuthenticatedAndActive, IsEditorOrAbove] throttle_scope = 'optimizer' throttle_classes = [DebugScopedRateThrottle] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.optimizer_service = OptimizerService() @action(detail=False, methods=['post']) def optimize(self, request): """ Optimize content (auto-detects entry point based on source). POST /api/v1/optimizer/optimize/ { "content_id": 123, "entry_point": "auto" // optional: auto, writer, wordpress, external, manual } """ serializer = OptimizeContentSerializer(data=request.data, context={'request': request}) if not serializer.is_valid(): return error_response(serializer.errors, status=status.HTTP_400_BAD_REQUEST, request=request) content_id = serializer.validated_data['content_id'] entry_point = serializer.validated_data.get('entry_point', 'auto') try: content = Content.objects.get(id=content_id) # Route to appropriate entry point if entry_point == 'auto': # Auto-detect based on source if content.source == 'igny8': content = self.optimizer_service.optimize_from_writer(content_id) elif content.source == 'wordpress': content = self.optimizer_service.optimize_from_wordpress_sync(content_id) elif content.source in ['shopify', 'custom']: content = self.optimizer_service.optimize_from_external_sync(content_id) else: content = self.optimizer_service.optimize_manual(content_id) elif entry_point == 'writer': content = self.optimizer_service.optimize_from_writer(content_id) elif entry_point == 'wordpress': content = self.optimizer_service.optimize_from_wordpress_sync(content_id) elif entry_point == 'external': content = self.optimizer_service.optimize_from_external_sync(content_id) else: # manual content = self.optimizer_service.optimize_manual(content_id) # Get latest optimization task task = content.optimization_tasks.order_by('-created_at').first() result = { 'content_id': content.id, 'optimizer_version': content.optimizer_version, 'scores_before': task.scores_before if task else {}, 'scores_after': content.optimization_scores, 'task_id': task.id if task else None, 'success': True, } return success_response(result, request=request) except ValueError as e: return error_response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST, request=request) except InsufficientCreditsError as e: return error_response({'error': str(e)}, status=status.HTTP_402_PAYMENT_REQUIRED, request=request) except Exception as e: logger.error(f"Error optimizing content {content_id}: {str(e)}", exc_info=True) return error_response({'error': 'Internal server error'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR, request=request) @action(detail=False, methods=['post']) def batch_optimize(self, request): """ Batch optimize multiple content items. POST /api/v1/optimizer/batch_optimize/ { "content_ids": [123, 456, 789], "entry_point": "auto" } """ serializer = BatchOptimizeContentSerializer(data=request.data, context={'request': request}) if not serializer.is_valid(): return error_response(serializer.errors, status=status.HTTP_400_BAD_REQUEST, request=request) content_ids = serializer.validated_data['content_ids'] entry_point = serializer.validated_data.get('entry_point', 'auto') results = [] errors = [] for content_id in content_ids: try: content = Content.objects.get(id=content_id) # Route to appropriate entry point if entry_point == 'auto': if content.source == 'igny8': content = self.optimizer_service.optimize_from_writer(content_id) elif content.source == 'wordpress': content = self.optimizer_service.optimize_from_wordpress_sync(content_id) elif content.source in ['shopify', 'custom']: content = self.optimizer_service.optimize_from_external_sync(content_id) else: content = self.optimizer_service.optimize_manual(content_id) elif entry_point == 'writer': content = self.optimizer_service.optimize_from_writer(content_id) elif entry_point == 'wordpress': content = self.optimizer_service.optimize_from_wordpress_sync(content_id) elif entry_point == 'external': content = self.optimizer_service.optimize_from_external_sync(content_id) else: content = self.optimizer_service.optimize_manual(content_id) task = content.optimization_tasks.order_by('-created_at').first() results.append({ 'content_id': content.id, 'optimizer_version': content.optimizer_version, 'scores_after': content.optimization_scores, 'success': True, }) except Exception as e: logger.error(f"Error optimizing content {content_id}: {str(e)}", exc_info=True) errors.append({ 'content_id': content_id, 'error': str(e), 'success': False, }) return success_response({ 'results': results, 'errors': errors, 'total': len(content_ids), 'succeeded': len(results), 'failed': len(errors), }, request=request) @action(detail=False, methods=['post']) def analyze(self, request): """ Analyze content without optimizing (preview scores). POST /api/v1/optimizer/analyze/ { "content_id": 123 } """ serializer = AnalyzeContentSerializer(data=request.data, context={'request': request}) if not serializer.is_valid(): return error_response(serializer.errors, status=status.HTTP_400_BAD_REQUEST, request=request) content_id = serializer.validated_data['content_id'] try: scores = self.optimizer_service.analyze_only(content_id) return success_response({ 'content_id': content_id, 'scores': scores, }, request=request) except ValueError as e: return error_response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST, request=request) except Exception as e: logger.error(f"Error analyzing content {content_id}: {str(e)}", exc_info=True) return error_response({'error': 'Internal server error'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR, request=request)