import math import re from django.contrib.auth import get_user_model from django.db import transaction from django.db.models import Q, Sum from django.shortcuts import get_object_or_404 from rest_framework import status from rest_framework.authentication import TokenAuthentication from rest_framework.authtoken.models import Token from rest_framework.permissions import BasePermission, IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView from apps.support.models import SupportMessage, SupportRoom from apps.support.serializers import ( AdminSupportMessageSerializer, AdminSupportRoomSerializer, SendSupportMessageSerializer, SupportMessageSerializer, SupportRoomSerializer, serialize_admin_chat_user, ) User = get_user_model() class IsSupportAdminPanelUser(BasePermission): """ Allows access to users who can log into the admin panel (super_admin, admin, professor, staff). """ def has_permission(self, request, view): user = request.user if not user or not user.is_authenticated: return False if getattr(user, 'is_superuser', False) or getattr(user, 'is_staff', False): return True if hasattr(user, 'can_access_admin_panel') and user.can_access_admin_panel(): return True return getattr(user, 'user_type', None) in ('super_admin', 'admin', 'professor', 'consultant') def cleanup_deleted_or_inactive_support_rooms() -> None: """ Deletes SupportRooms belonging to users who have been soft-deleted or deactivated. """ SupportRoom.objects.filter( Q(user__is_active=False) | Q(user__deleted_at__isnull=False) | Q(user__fullname__endswith=':deleted') ).delete() def active_support_rooms_queryset(): """ Returns SupportRooms belonging only to active, non-deleted users (including active guest users) that have at least one message. """ cleanup_deleted_or_inactive_support_rooms() return ( SupportRoom.objects.filter( user__is_active=True, user__deleted_at__isnull=True, messages__isnull=False, ) .exclude(user__fullname__endswith=':deleted') .distinct() ) def get_total_admin_unread_count() -> int: agg = ( SupportRoom.objects.filter( user__is_active=True, user__deleted_at__isnull=True, messages__isnull=False, ) .exclude(user__fullname__endswith=':deleted') .distinct() .aggregate(total=Sum('unread_by_admin')) ) return int(agg.get('total') or 0) def resolve_user_support_room(request, create_if_missing: bool = False): """ Resolves the SupportRoom for request.user (works for both logged-in and guest users). If the user is logged in, automatically checks for and merges any unmerged guest support room (from X-Guest-Token or Web IP+UA device_id). """ user = request.user if not getattr(user, 'is_guest', False) and getattr(user, 'email', None): from apps.account.guest_merge import resolve_guest_users_from_request, merge_guest_user_data guest_candidates = resolve_guest_users_from_request(request) for guest_user in guest_candidates: if guest_user.id != user.id: merge_guest_user_data(guest_user, user) room = SupportRoom.objects.filter(user=user).first() if room: return room if create_if_missing: room, _ = SupportRoom.objects.get_or_create(user=user) return room return None class SupportChatMessagesView(APIView): """ GET /api/support/messages/?after_id= Returns the user's support room and messages (all or after `after_id` for short polling). POST /api/support/messages/ Sends a new message from the user (or guest user) to support. """ authentication_classes = [TokenAuthentication] permission_classes = [IsAuthenticated] def get(self, request, *args, **kwargs): room = resolve_user_support_room(request, create_if_missing=False) if not room: return Response( { 'room': None, 'messages': [], }, status=status.HTTP_200_OK, ) after_id_param = request.query_params.get('after_id') messages_qs = room.messages.select_related('sender').order_by('id') if after_id_param is not None: try: after_id = int(after_id_param) if after_id > 0: messages_qs = messages_qs.filter(id__gt=after_id) except (TypeError, ValueError): pass messages_list = list(messages_qs) # Mark support replies as read when user fetches messages in open chat mark_read = request.query_params.get('mark_read', 'true').lower() != 'false' if mark_read and (room.unread_by_user > 0 or any( m.sender_type == SupportMessage.SenderType.SUPPORT and not m.is_read for m in messages_list )): SupportMessage.objects.filter( room=room, sender_type=SupportMessage.SenderType.SUPPORT, is_read=False, ).update(is_read=True) if room.unread_by_user != 0: room.unread_by_user = 0 room.save(update_fields=['unread_by_user', 'updated_at']) return Response( { 'room': SupportRoomSerializer(room).data, 'messages': SupportMessageSerializer(messages_list, many=True).data, }, status=status.HTTP_200_OK, ) @transaction.atomic def post(self, request, *args, **kwargs): serializer = SendSupportMessageSerializer(data=request.data) serializer.is_valid(raise_exception=True) room = resolve_user_support_room(request, create_if_missing=True) message = SupportMessage.objects.create( room=room, sender_type=SupportMessage.SenderType.USER, sender=request.user, content=serializer.validated_data['content'], ) room.refresh_from_db() return Response( { 'room': SupportRoomSerializer(room).data, 'message': SupportMessageSerializer(message).data, }, status=status.HTTP_201_CREATED, ) class SupportUnreadCountView(APIView): """ GET /api/support/unread/ Lightweight endpoint to check if the user has unread support specialist replies. """ authentication_classes = [TokenAuthentication] permission_classes = [IsAuthenticated] def get(self, request, *args, **kwargs): room = resolve_user_support_room(request, create_if_missing=False) return Response( { 'unread_count': room.unread_by_user if room else 0, 'status': room.status if room else None, }, status=status.HTTP_200_OK, ) class AdminSupportRoomListView(APIView): """ GET /api/support/admin/rooms/?page=1&per_page=20&search=... Lists all SupportRooms for the admin dashboard (`dovodi-panel`). """ authentication_classes = [TokenAuthentication] permission_classes = [IsAuthenticated, IsSupportAdminPanelUser] def get(self, request, *args, **kwargs): try: page = max(1, int(request.query_params.get('page', 1))) except (TypeError, ValueError): page = 1 try: per_page = max(1, min(100, int(request.query_params.get('per_page', 20)))) except (TypeError, ValueError): per_page = 20 search = (request.query_params.get('search') or '').strip() qs = active_support_rooms_queryset().select_related('user', 'assigned_to').order_by( '-last_message_at', '-updated_at', '-id', ) if search: q_filter = ( Q(user__fullname__icontains=search) | Q(user__email__icontains=search) | Q(user__phone_number__icontains=search) | Q(messages__content__icontains=search) ) digits_match = re.search(r'(\d+)', search) if digits_match: num_val = int(digits_match.group(1)) q_filter |= Q(id=num_val) | Q(user__id=num_val) elif search.lower() == 'guest': q_filter |= Q(user__email__isnull=True) qs = qs.filter(q_filter).distinct() total_rooms = qs.count() total_pages = max(1, math.ceil(total_rooms / per_page)) offset = (page - 1) * per_page rooms_page = list(qs[offset:offset + per_page]) serialized_rooms = AdminSupportRoomSerializer( rooms_page, many=True, context={'request': request}, ).data return Response( { 'status': 'success', 'current_user': serialize_admin_chat_user(request.user, request=request), 'room_messages': serialized_rooms, 'total_pages': total_pages, 'current_page': page, 'per_page': per_page, 'total_rooms': total_rooms, 'total_unread_count': get_total_admin_unread_count(), }, status=status.HTTP_200_OK, ) class AdminSupportRoomByUserView(APIView): """ POST /api/support/admin/rooms/by-user/ Body: { "user_id": } Gets or creates a SupportRoom for a specific user when an admin starts a new chat from the panel. """ authentication_classes = [TokenAuthentication] permission_classes = [IsAuthenticated, IsSupportAdminPanelUser] @transaction.atomic def post(self, request, *args, **kwargs): user_id = request.data.get('user_id') or request.data.get('recipient_id') target_user = get_object_or_404(User, pk=user_id) room, _ = SupportRoom.objects.get_or_create( user=target_user, defaults={'assigned_to': request.user}, ) # Mark any unread messages from this user as read if room.unread_by_admin > 0: SupportMessage.objects.filter( room=room, sender_type=SupportMessage.SenderType.USER, is_read=False, ).update(is_read=True) room.unread_by_admin = 0 room.save(update_fields=['unread_by_admin', 'updated_at']) messages_qs = room.messages.select_related('sender').order_by('id') messages_list = list(messages_qs) return Response( { 'status': 'success', 'room': AdminSupportRoomSerializer(room, context={'request': request}).data, 'messages': AdminSupportMessageSerializer( messages_list, many=True, context={'request': request}, ).data, 'current_user': serialize_admin_chat_user(request.user, request=request), 'total_pages': 1, 'current_page': 1, 'per_page': max(20, len(messages_list)), 'total_messages': len(messages_list), 'total_unread_count': get_total_admin_unread_count(), }, status=status.HTTP_200_OK, ) class AdminSupportRoomMessagesView(APIView): """ GET /api/support/admin/rooms//messages/?after_id= Returns messages for a SupportRoom and marks unread user messages as read. POST /api/support/admin/rooms//messages/ Sends a reply from the admin/specialist (`sender_type='support'`). """ authentication_classes = [TokenAuthentication] permission_classes = [IsAuthenticated, IsSupportAdminPanelUser] def get(self, request, room_id: int, *args, **kwargs): room = get_object_or_404( active_support_rooms_queryset().select_related('user', 'assigned_to'), pk=room_id, ) after_id_param = request.query_params.get('after_id') messages_qs = room.messages.select_related('sender').order_by('id') if after_id_param is not None: try: after_id = int(after_id_param) if after_id > 0: messages_qs = messages_qs.filter(id__gt=after_id) except (TypeError, ValueError): pass messages_list = list(messages_qs) # Mark user messages as read when admin views the room if room.unread_by_admin > 0 or any( m.sender_type == SupportMessage.SenderType.USER and not m.is_read for m in messages_list ): SupportMessage.objects.filter( room=room, sender_type=SupportMessage.SenderType.USER, is_read=False, ).update(is_read=True) if room.unread_by_admin != 0: room.unread_by_admin = 0 room.save(update_fields=['unread_by_admin', 'updated_at']) total_messages = room.messages.count() return Response( { 'status': 'success', 'room': AdminSupportRoomSerializer(room, context={'request': request}).data, 'messages': AdminSupportMessageSerializer( messages_list, many=True, context={'request': request}, ).data, 'current_user': serialize_admin_chat_user(request.user, request=request), 'total_pages': 1, 'current_page': 1, 'per_page': max(50, total_messages), 'total_messages': total_messages, 'total_unread_count': get_total_admin_unread_count(), }, status=status.HTTP_200_OK, ) @transaction.atomic def post(self, request, room_id: int, *args, **kwargs): room = get_object_or_404( active_support_rooms_queryset().select_related('user', 'assigned_to'), pk=room_id, ) serializer = SendSupportMessageSerializer(data=request.data) serializer.is_valid(raise_exception=True) if not room.assigned_to_id: room.assigned_to = request.user room.save(update_fields=['assigned_to', 'updated_at']) message = SupportMessage.objects.create( room=room, sender_type=SupportMessage.SenderType.SUPPORT, sender=request.user, content=serializer.validated_data['content'], ) room.refresh_from_db() return Response( { 'status': 'success', 'room': AdminSupportRoomSerializer(room, context={'request': request}).data, 'message': AdminSupportMessageSerializer( message, context={'request': request}, ).data, 'current_user': serialize_admin_chat_user(request.user, request=request), 'total_unread_count': get_total_admin_unread_count(), }, status=status.HTTP_201_CREATED, ) class AdminSupportMessageDetailView(APIView): """ PATCH /api/support/admin/messages// Updates the content of a support message. DELETE /api/support/admin/messages// Deletes a support message. """ authentication_classes = [TokenAuthentication] permission_classes = [IsAuthenticated, IsSupportAdminPanelUser] @transaction.atomic def patch(self, request, message_id: int, *args, **kwargs): message = get_object_or_404( SupportMessage.objects.select_related('room', 'sender'), pk=message_id, ) serializer = SendSupportMessageSerializer(data=request.data) serializer.is_valid(raise_exception=True) message.content = serializer.validated_data['content'] message.save(update_fields=['content']) return Response( { 'status': 'success', 'room_id': message.room_id, 'message': AdminSupportMessageSerializer( message, context={'request': request}, ).data, 'total_unread_count': get_total_admin_unread_count(), }, status=status.HTTP_200_OK, ) @transaction.atomic def delete(self, request, message_id: int, *args, **kwargs): message = get_object_or_404( SupportMessage.objects.select_related('room'), pk=message_id, ) room = message.room deleted_id = message.id message.delete() last_msg = room.messages.order_by('-id').first() room.last_message_at = last_msg.created_at if last_msg else None room.save(update_fields=['last_message_at', 'updated_at']) return Response( { 'status': 'success', 'room_id': room.id, 'deleted_message_id': deleted_id, 'room': AdminSupportRoomSerializer(room, context={'request': request}).data, 'total_unread_count': get_total_admin_unread_count(), }, status=status.HTTP_200_OK, )