from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from django.db import connection from .typesense_client import TypesenseService import logging import time logger = logging.getLogger(__name__) class ProductSearchView(APIView): def get(self, request): try: q = request.query_params.get('q', '*').strip() category = request.query_params.get('category', '').strip() min_price = request.query_params.get('min_price') max_price = request.query_params.get('max_price') is_gi_tagged = request.query_params.get('is_gi_tagged') supplier_id = request.query_params.get('supplier_id') sort_by_param = request.query_params.get('sort_by', '').strip() try: page = max(1, int(request.query_params.get('page', 1))) per_page = min(250, max(1, int(request.query_params.get('per_page', 20)))) except (ValueError, TypeError): return Response({'error': 'Invalid page or per_page parameter'}, status=status.HTTP_400_BAD_REQUEST) filter_clauses = ['is_active:=true'] if category: escaped_category = category.replace('"', '\\"') filter_clauses.append(f'category:="{escaped_category}"') if supplier_id: try: s_id = int(supplier_id) filter_clauses.append(f'supplier_id:={s_id}') except (ValueError, TypeError): return Response({'error': 'supplier_id must be an integer'}, status=status.HTTP_400_BAD_REQUEST) if is_gi_tagged is not None: val = is_gi_tagged.strip().lower() if val in ('true', '1'): filter_clauses.append('is_gi_tagged:=true') elif val in ('false', '0'): filter_clauses.append('is_gi_tagged:=false') if min_price is not None or max_price is not None: try: p_min = float(min_price) if min_price else 0.0 p_max = float(max_price) if max_price else 9999999.0 if p_min > p_max: p_min, p_max = p_max, p_min filter_clauses.append(f'price:[{p_min}..{p_max}]') except (ValueError, TypeError): return Response({'error': 'Price filters must be valid numbers'}, status=status.HTTP_400_BAD_REQUEST) filter_by_str = ' && '.join(filter_clauses) if sort_by_param == 'price_asc': sort_by_str = 'price:asc' elif sort_by_param == 'price_desc': sort_by_str = 'price:desc' elif sort_by_param == 'relevance': sort_by_str = '_text_match:desc,created_at:desc' elif sort_by_param == 'newest': sort_by_str = 'created_at:desc' else: sort_by_str = '_text_match:desc,created_at:desc' if (q and q != '*') else 'created_at:desc' res = TypesenseService.search_products( query=q, filter_by=filter_by_str, sort_by=sort_by_str, page=page, per_page=per_page ) hits = res.get('hits', []) found = res.get('found', 0) total_pages = (found + per_page - 1) // per_page if per_page > 0 else 1 results = [hit.get('document', {}) for hit in hits] facet_counts = res.get('facet_counts', []) return Response({ 'count': found, 'page': page, 'per_page': per_page, 'total_pages': total_pages, 'facets': facet_counts, 'results': results }, status=status.HTTP_200_OK) except Exception as e: logger.error(f"Typesense Search Error: {e}", exc_info=True) return Response({'error': str(e), 'results': [], 'count': 0}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class IndexProductView(APIView): def post(self, request): try: data = request.data if not data: return Response({'error': 'Payload cannot be empty'}, status=status.HTTP_400_BAD_REQUEST) if isinstance(data, list): valid_items = [] for item in data: if isinstance(item, dict) and ('id' in item or 'sku' in item): valid_items.append(item) if not valid_items: return Response({'error': 'Each product in list must have an id or sku'}, status=status.HTTP_400_BAD_REQUEST) res = TypesenseService.index_products_batch(valid_items) return Response({'status': 'indexed', 'count': len(valid_items), 'result': res}, status=status.HTTP_200_OK) elif isinstance(data, dict): if 'id' not in data and 'sku' not in data: return Response({'error': 'Product id or sku is required'}, status=status.HTTP_400_BAD_REQUEST) res = TypesenseService.index_product(data) return Response({'status': 'indexed', 'result': res}, status=status.HTTP_200_OK) else: return Response({'error': 'Invalid JSON format. Expected JSON object or array.'}, status=status.HTTP_400_BAD_REQUEST) except Exception as e: logger.error(f"Product Index Error: {e}", exc_info=True) return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class DeleteProductView(APIView): def delete(self, request, product_id): try: if not product_id or not str(product_id).strip(): return Response({'error': 'Product identifier is required'}, status=status.HTTP_400_BAD_REQUEST) res = TypesenseService.delete_product(product_id) return Response({'status': 'deleted', 'result': res}, status=status.HTTP_200_OK) except Exception as e: logger.error(f"Product Delete Error: {e}", exc_info=True) return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class ReindexCatalogView(APIView): def post(self, request): try: TypesenseService.ensure_schema(force=True) indexed_count = 0 batch_size = 250 batch = [] with connection.cursor() as cursor: cursor.execute(""" SELECT id, sku, title, description, category, supplier_id, price, compare_at_price, stock, image, is_gi_tagged, is_active, status FROM api_product """) columns = [col[0] for col in cursor.description] rows = cursor.fetchall() now_ts = int(time.time()) for idx, row in enumerate(rows): item = dict(zip(columns, row)) # Retain descending ordering with timestamp offset item['created_at'] = now_ts + idx batch.append(item) if len(batch) >= batch_size: TypesenseService.index_products_batch(batch) indexed_count += len(batch) batch = [] if batch: TypesenseService.index_products_batch(batch) indexed_count += len(batch) return Response({'status': 'completed', 'total_indexed': indexed_count}, status=status.HTTP_200_OK) except Exception as e: logger.error(f"Full Reindex Error: {e}", exc_info=True) return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)