"""
Chat views for conversations and messages.
"""
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.pagination import CursorPagination
from django.shortcuts import get_object_or_404
from django.contrib.auth import get_user_model
from django.utils import timezone
import secrets
import string

from chat.models import (
    Conversation, Message, MessageReaction, ConversationVisibility,
    ChatAppearance, ConversationMember, GroupJoinRequest, MessageRead
)

User = get_user_model()
from chat.serializers import (
    ConversationSerializer,
    ConversationCreateSerializer,
    MessageSerializer,
    MessageCreateSerializer,
    ReactionCreateSerializer,
    ChatAppearanceSerializer,
    GroupConversationCreateSerializer,
    GroupUpdateSerializer,
    ConversationMemberSerializer,
    GroupJoinRequestSerializer,
    MessageReadSerializer,
    GroupInviteLinkSerializer,
)
from chat.services import hide_conversation_for_user, mark_messages_as_read


def _is_admin(user, conversation):
    """Check if user is an admin of the conversation."""
    if not conversation.is_group:
        return False
    # Check if user is the creator OR in the admins list
    return conversation.admin_id == user.id or conversation.admins.filter(id=user.id).exists()


class MessageCursorPagination(CursorPagination):
    """Cursor pagination for messages, ordered by creation time."""
    page_size = 50
    ordering = '-created_at'
    cursor_query_param = 'cursor'


class ConversationViewSet(viewsets.ModelViewSet):
    """
    ViewSet for managing conversations.

    list: Get all visible conversations for the current user
    create: Start a new conversation with another user
    retrieve: Get a specific conversation
    destroy: Hide the conversation (soft delete for current user)
    """
    permission_classes = [IsAuthenticated]
    serializer_class = ConversationSerializer

    def get_queryset(self):
        """Get conversations visible to the current user."""
        user = self.request.user
        from django.db.models import Q
        queryset = Conversation.objects.filter(
            Q(participants=user) |
            Q(join_requests__user=user, join_requests__status='pending')
        )

        # In general list view, we usually only show visible ones
        # But we might want an action to see hidden ones.
        if self.action == 'list':
            show_hidden = self.request.query_params.get('hidden', 'false').lower() == 'true'
            if show_hidden:
                # ONLY show conversations that were explicitly "Hidden" (no history cleared)
                queryset = queryset.filter(
                    visibilities__user=user,
                    visibilities__is_visible=False,
                    visibilities__hidden_until__isnull=True
                )
            else:
                # Show visible conversations
                queryset = queryset.filter(
                    visibilities__user=user,
                    visibilities__is_visible=True
                )
        else:
            # For detail views, we might still want to see it even if hidden
            pass

        if self.action == 'list':
            is_requests = self.request.query_params.get('requests', 'false').lower() == 'true'
            if is_requests:
                # Show conversations received but not accepted OR pending group invites
                queryset = queryset.filter(
                    (Q(is_accepted=False, is_group=False) & ~Q(initiator=user)) |
                    Q(join_requests__user=user, join_requests__status='pending')
                )
            else:
                # Show accepted conversations OR conversations initiated by the user
                # Ensure we only show joined groups/convos here, not pending ones
                queryset = queryset.filter(participants=user)
                queryset = queryset.filter(
                    Q(is_accepted=True) | Q(initiator=user)
                )

        return queryset.distinct().order_by('-updated_at')

    @action(detail=True, methods=['post'])
    def accept(self, request, pk=None):
        """Accept a message request or group invitation."""
        conversation = self.get_object()

        if conversation.is_group:
             # Handle Group Invitation Acceptance
             try:
                 req = GroupJoinRequest.objects.get(
                     conversation=conversation,
                     user=request.user,
                     status='pending'
                 )
                 req.status = 'accepted'
                 req.save()

                 # Add to participants
                 conversation.participants.add(request.user)
                 ConversationMember.objects.create(conversation=conversation, user=request.user)

                 # Create visibility
                 ConversationVisibility.objects.get_or_create(
                     user=request.user,
                     conversation=conversation,
                     defaults={'is_visible': True}
                 )

                 # Broadcast member_joined
                 self._broadcast_group_event(conversation, 'group_user_added', {
                    'conversation_id': str(conversation.id),
                    'user': {
                        'id': request.user.id,
                        'username': request.user.username,
                        'avatar': request.user.avatar.url if request.user.avatar else None
                    }
                 })

                 return Response({'status': 'Group invitation accepted.'})
             except GroupJoinRequest.DoesNotExist:
                 # Double check if already joined?
                 if conversation.participants.filter(id=request.user.id).exists():
                     return Response({'status': 'Already joined.'})
                 return Response({'error': 'No pending invitation found.'}, status=404)

        conversation.is_accepted = True
        conversation.save()
        return Response({'status': 'Conversation accepted.'})

    def get_serializer_class(self):
        if self.action == 'create':
            return ConversationCreateSerializer
        if self.action == 'join_info':
            return GroupInviteLinkSerializer
        return ConversationSerializer

    def create(self, request, *args, **kwargs):
        """Start a new conversation."""
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        conversation = serializer.save()

        # Return full conversation data
        output_serializer = ConversationSerializer(
            conversation,
            context={'request': request}
        )

        # BROADCAST: Notify recipient via WebSocket if an initial message was sent
        initial_msg = conversation.messages.order_by('created_at').first()
        if initial_msg:
            from channels.layers import get_channel_layer
            from asgiref.sync import async_to_sync
            from django.contrib.auth import get_user_model

            channel_layer = get_channel_layer()
            if channel_layer:
                # We need to serialize the message like ChatConsumer does
                # Let's use absolute URLs for attachments
                def get_absolute_url(url):
                    if not url: return None
                    if url.startswith('http'): return url
                    return request.build_absolute_uri(url)

                message_data = {
                    "id": initial_msg.id,
                    "conversation_id": str(conversation.id),
                    "sender": {
                        "id": initial_msg.sender.id,
                        "username": initial_msg.sender.username,
                        "first_name": initial_msg.sender.first_name,
                        "last_name": initial_msg.sender.last_name,
                        "avatar": get_absolute_url(initial_msg.sender.avatar.url) if initial_msg.sender.avatar else None
                    },
                    "content": initial_msg.content,
                    "attachment": get_absolute_url(initial_msg.attachment.url) if initial_msg.attachment else None,
                    "attachment_type": initial_msg.attachment_type,
                    "is_read": initial_msg.is_read,
                    "reply_to": initial_msg.reply_to_id,
                    "created_at": initial_msg.created_at.isoformat()
                }

                participants = conversation.participants.all()
                for participant in participants:
                    if participant == request.user:
                        continue

                    # Send to participant's personal room
                    async_to_sync(channel_layer.group_send)(
                        f"user_{participant.id}",
                        {
                            "type": "chat_message",
                            "message": message_data
                        }
                    )

        return Response(output_serializer.data, status=status.HTTP_201_CREATED)

    def destroy(self, request, *args, **kwargs):
        """
        Delete conversation.
        If group and user is admin: Hard delete (cascade to all members/messages).
        Otherwise: Soft delete (hide for current user).
        """
        conversation = self.get_object()

        # Hard delete for group admin
        if conversation.is_group and conversation.admin == request.user:
            # Broadcast deletion to all members BEFORE deleting
            from channels.layers import get_channel_layer
            from asgiref.sync import async_to_sync

            channel_layer = get_channel_layer()
            if channel_layer:
                normalized_id = str(conversation.id).replace('-', '')
                # Notify all members
                for member in conversation.participants.all():
                    async_to_sync(channel_layer.group_send)(
                        f"user_updates_{member.id}",
                        {
                            "type": "group_deleted",
                            "conversation_id": normalized_id
                        }
                    )

            conversation.delete()
            return Response({'status': 'Group deleted permanently.'}, status=status.HTTP_200_OK)

        # Soft delete (hide) for regular cases
        # But if it's a pending group invite, REJECT it.
        if conversation.is_group:
            try:
                # Check for pending request
                pending_req = GroupJoinRequest.objects.get(
                    conversation=conversation,
                    user=request.user,
                    status='pending'
                )
                pending_req.status = 'rejected'
                pending_req.save()
                return Response({'status': 'Invitation rejected.'}, status=status.HTTP_200_OK)
            except GroupJoinRequest.DoesNotExist:
                # Not a pending request, proceed to hide
                pass

        hide_conversation_for_user(request.user, conversation)
        return Response({'status': 'Conversation hidden.'}, status=status.HTTP_200_OK)

    @action(detail=True, methods=['post'])
    def unhide(self, request, pk=None):
        """Make a hidden conversation visible again."""
        conversation = self.get_object()
        visibility, _ = ConversationVisibility.objects.get_or_create(
            user=request.user,
            conversation=conversation
        )
        visibility.is_visible = True
        visibility.hidden_until = None  # Restore history
        visibility.save()
        return Response({'status': 'Conversation visible.'})

    @action(detail=True, methods=['post'])
    def hide(self, request, pk=None):
        """Hide a conversation for the current user (archive)."""
        conversation = self.get_object()
        from chat.services import hide_conversation_for_user
        hide_conversation_for_user(request.user, conversation)
        return Response({'status': 'Conversation hidden.'})

    @action(detail=True, methods=['post'])
    def clear_chat(self, request, pk=None):
        """
        Clear all messages in a group chat (Admin only).
        """
        conversation = self.get_object()

        if not conversation.is_group or not _is_admin(request.user, conversation):
            return Response(
                {'error': 'Permission denied. Only group admins can clear chat.'},
                status=status.HTTP_403_FORBIDDEN
            )

        # Broadcast cleared event
        from channels.layers import get_channel_layer
        from asgiref.sync import async_to_sync

        channel_layer = get_channel_layer()
        if channel_layer:
            normalized_id = str(conversation.id).replace('-', '')
            for member in conversation.participants.all():
                async_to_sync(channel_layer.group_send)(
                    f"user_{member.id}",
                    {
                        "type": "chat_cleared",
                        "conversation_id": normalized_id
                    }
                )

        # Delete all messages
        conversation.messages.all().delete()

        return Response({'status': 'Chat history cleared.'})

    @action(detail=True, methods=['post'])
    def delete_conversation(self, request, pk=None):
        """Delete conversation for current user (clear history and hide)."""
        conversation = self.get_object()
        from chat.services import delete_conversation_for_user
        delete_conversation_for_user(request.user, conversation)
        return Response({'status': 'Conversation deleted.'})

    @action(detail=True, methods=['post'])
    def mute(self, request, pk=None):
        """Mute a conversation for the current user."""
        conversation = self.get_object()
        visibility, _ = ConversationVisibility.objects.get_or_create(
            user=request.user,
            conversation=conversation
        )
        visibility.is_muted = True
        visibility.save()
        return Response({'status': 'Conversation muted.'})

    @action(detail=True, methods=['post'])
    def unmute(self, request, pk=None):
        """Unmute a conversation for the current user."""
        conversation = self.get_object()
        visibility, _ = ConversationVisibility.objects.get_or_create(
            user=request.user,
            conversation=conversation
        )
        visibility.is_muted = False
        visibility.save()
        return Response({'status': 'Conversation unmuted.'})

    @action(detail=True, methods=['post'])
    def mark_read(self, request, pk=None):
        """Mark all messages in conversation as read."""
        conversation = self.get_object()
        mark_messages_as_read(request.user, conversation)

        # Broadcast via WebSocket to notify the other user
        from channels.layers import get_channel_layer
        from asgiref.sync import async_to_sync

        channel_layer = get_channel_layer()
        if channel_layer:
            normalized_id = str(conversation.id).replace('-', '')
            async_to_sync(channel_layer.group_send)(
                f"chat_{normalized_id}",
                {
                    "type": "messages_read",
                    "conversation_id": str(conversation.id),
                    "user_id": request.user.id
                }
            )

        return Response({'status': 'Messages marked as read.'})

    @action(detail=False, methods=['get'])
    def find_by_participant(self, request):
        """Find a conversation with a specific participant without creating it."""
        participant_id = request.query_params.get('participant_id')
        if not participant_id:
            return Response({'error': 'participant_id required.'}, status=400)

        try:
            participant = User.objects.get(id=participant_id)
        except User.DoesNotExist:
            return Response({'error': 'User not found.'}, status=404)

        from chat.services import get_or_create_conversation
        # Use service to find and UNHIDE if it exists
        conversation, _ = get_or_create_conversation(request.user, participant, create_if_not_exists=False)

        if conversation:
            serializer = ConversationSerializer(conversation, context={'request': request})
            return Response(serializer.data)
        return Response({'status': 'not_found'}, status=404)

    # ==================== Group Chat Actions ====================

    @action(detail=False, methods=['post'], url_path='group')
    def create_group(self, request):
        """Create a new group conversation."""
        serializer = GroupConversationCreateSerializer(data=request.data, context={'request': request})
        serializer.is_valid(raise_exception=True)
        conversation = serializer.save()

        output_serializer = ConversationSerializer(conversation, context={'request': request})

        # Broadcast group creation to all members
        self._broadcast_group_event(conversation, 'group_created', {
            'conversation': output_serializer.data
        })

        return Response(output_serializer.data, status=status.HTTP_201_CREATED)

    @action(detail=True, methods=['patch'], url_path='update-group')
    def update_group(self, request, pk=None):
        """Update group settings (name, image). Admin only."""
        conversation = self.get_object()

        if not conversation.is_group:
            return Response({'error': 'This is not a group conversation.'}, status=400)

        if not _is_admin(request.user, conversation):
            return Response({'error': 'Only group admins can update settings.'}, status=403)

        serializer = GroupUpdateSerializer(
            conversation, data=request.data, partial=True, context={'request': request}
        )
        serializer.is_valid(raise_exception=True)
        serializer.save()

        # Generate tokens if enabled but missing
        if conversation.invite_enabled and not conversation.invite_token:
            conversation.invite_token = secrets.token_urlsafe(32)
            conversation.invite_short_token = ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(12))
            conversation.save()

        output_serializer = ConversationSerializer(conversation, context={'request': request})

        # Broadcast group update
        self._broadcast_group_event(conversation, 'group_updated', {
            'conversation': output_serializer.data
        })

        return Response(output_serializer.data)

    @action(detail=True, methods=['get'], url_path='members')
    def get_members(self, request, pk=None):
        """Get list of group members."""
        conversation = self.get_object()

        if not conversation.is_group:
            return Response({'error': 'This is not a group conversation.'}, status=400)

        members = ConversationMember.objects.filter(conversation=conversation)
        serializer = ConversationMemberSerializer(members, many=True)
        return Response({
            'members': serializer.data,
            'admin_id': conversation.admin_id
        })

    @action(detail=True, methods=['post'], url_path='add-member')
    def add_member(self, request, pk=None):
        """Add a member to the group. Admin adds directly, others create a request."""
        conversation = self.get_object()

        if not conversation.is_group:
            return Response({'error': 'This is not a group conversation.'}, status=400)

        user_id = request.data.get('user_id')
        if not user_id:
            return Response({'error': 'user_id required.'}, status=400)

        try:
            user_to_add = User.objects.get(id=user_id)
        except User.DoesNotExist:
            return Response({'error': 'User not found.'}, status=404)

        # Check if already a member
        if conversation.participants.filter(id=user_id).exists():
            return Response({'error': 'User is already a member.'}, status=400)

        # Check group size limit
        # Limit removed as per user request
        # if conversation.participants.count() >= 10:
        #     return Response({'error': 'Group is at maximum capacity (10 members).'}, status=400)

        # Check if there's already a pending request for this user
        existing_request = GroupJoinRequest.objects.filter(
            conversation=conversation,
            user=user_to_add,
            status='pending'
        ).first()
        if existing_request:
            return Response({'error': 'A request for this user is already pending.'}, status=400)

        # If requester is admin, add directly
        if _is_admin(request.user, conversation):
            conversation.participants.add(user_to_add)

            ConversationMember.objects.create(
                conversation=conversation,
                user=user_to_add
            )

            ConversationVisibility.objects.get_or_create(
                 user=user_to_add,
                 conversation=conversation,
                 defaults={'is_visible': True}
            )

            # Broadcast update to new member
            from channels.layers import get_channel_layer
            from asgiref.sync import async_to_sync



            # Broadcast to the new member so they see the group immediately
            self._broadcast_to_user(user_to_add.id, 'group_user_added', {
                'conversation_id': str(conversation.id),
                'user': {
                    'id': user_to_add.id,
                    'username': user_to_add.username,
                    'first_name': user_to_add.first_name,
                    'last_name': user_to_add.last_name,
                    'avatar': user_to_add.avatar.url if user_to_add.avatar else None
                }
            })

            # Broadcast to GROUP that member joined (Crucial for real-time member count update)
            self._broadcast_group_event(conversation, 'group_user_added', {
                'conversation_id': str(conversation.id),
                'user': {
                    'id': user_to_add.id,
                    'username': user_to_add.username,
                    'first_name': user_to_add.first_name,
                    'last_name': user_to_add.last_name,
                    'avatar': user_to_add.avatar.url if user_to_add.avatar else None
                }
            })

            # Create system message for the member added event
            self._create_member_system_message(
                conversation=conversation,
                message_type='member_added',
                actor=request.user,
                target=user_to_add
            )

            return Response({'status': 'Member added.'}, status=200)

        # Non-admin: Create a join request for admin approval
        # Check if a pending request already exists
        existing_request = GroupJoinRequest.objects.filter(
            conversation=conversation,
            user=user_to_add,
            status='pending'
        ).first()

        if existing_request:
            return Response({'status': 'Request already pending.', 'request_sent': True}, status=200)

        # Also check for rejected requests and allow re-request
        GroupJoinRequest.objects.filter(
            conversation=conversation,
            user=user_to_add,
            status__in=['rejected', 'accepted']
        ).delete()

        join_request = GroupJoinRequest.objects.create(
            conversation=conversation,
            user=user_to_add,
            requested_by=request.user,
            status='pending'
        )

        # Broadcast to admin about the new request
        self._broadcast_to_user(conversation.admin.id, 'group_join_request', {
            'conversation_id': str(conversation.id),
            'request_id': join_request.id,
            'user': {
                'id': user_to_add.id,
                'username': user_to_add.username,
                'first_name': user_to_add.first_name,
                'last_name': user_to_add.last_name,
                'avatar': user_to_add.avatar.url if user_to_add.avatar else None
            },
            'requested_by': {
                'id': request.user.id,
                'username': request.user.username,
                'first_name': request.user.first_name,
                'last_name': request.user.last_name,
                'avatar': request.user.avatar.url if request.user.avatar else None
            }
        })

        return Response({'status': 'Join request created.'}, status=201)


    @action(detail=True, methods=['get'], url_path='join-requests')
    def join_requests(self, request, pk=None):
        """Get pending join requests for the group. Admin only."""
        conversation = self.get_object()
        if not conversation.is_group:
            return Response({'error': 'Not a group.'}, status=400)

        if not _is_admin(request.user, conversation):
            return Response({'error': 'Admin only.'}, status=403)

        requests = GroupJoinRequest.objects.filter(
            conversation=conversation,
            status='pending'
        )
        serializer = GroupJoinRequestSerializer(requests, many=True)
        return Response(serializer.data)

    @action(detail=True, methods=['post'], url_path='join-requests/(?P<request_id>[^/.]+)/respond')
    def respond_request(self, request, pk=None, request_id=None):
        """Respond to a join request (accept/reject). Admin only."""
        conversation = self.get_object()
        if not conversation.is_group or not _is_admin(request.user, conversation):
            return Response({'error': 'Permission denied.'}, status=403)

        try:
            join_req = GroupJoinRequest.objects.get(
                id=request_id,
                conversation=conversation,
                status='pending'
            )
        except GroupJoinRequest.DoesNotExist:
            return Response({'error': 'Request not found.'}, status=404)

        accept = request.data.get('accept', False)

        if accept:
            join_req.status = 'accepted'
            join_req.save()

            # Add user to participants
            conversation.participants.add(join_req.user)
            ConversationMember.objects.create(
                conversation=conversation,
                user=join_req.user
            )
             # Create visibility
            ConversationVisibility.objects.get_or_create(
                 user=join_req.user,
                 conversation=conversation,
                 defaults={'is_visible': True}
            )

            # Broadcast to NEW MEMBER that they were accepted
            # (They might need to refresh their list or get a push)
            self._broadcast_to_user(join_req.user.id, 'group_request_accepted', {
                'conversation_id': str(conversation.id),
                'group_name': conversation.name
            })

            # Broadcast to GROUP that member joined
            self._broadcast_group_event(conversation, 'group_user_added', {
                'conversation_id': str(conversation.id),
                'user': {
                    'id': join_req.user.id,
                    'username': join_req.user.username,
                    'avatar': join_req.user.avatar.url if join_req.user.avatar else None
                }
            })

            # Create system message for request accepted
            self._create_member_system_message(
                conversation=conversation,
                message_type='request_accepted',
                actor=request.user,
                target=join_req.user,
                requested_by=join_req.requested_by
            )

            return Response({'status': 'Request accepted.'})
        else:
            join_req.status = 'rejected'
            join_req.save()
            return Response({'status': 'Request rejected.'})

    @action(detail=True, methods=['post'], url_path='remove-member')
    def remove_member(self, request, pk=None):
        """Remove a member from the group. Admin only."""
        conversation = self.get_object()

        if not conversation.is_group:
            return Response({'error': 'This is not a group conversation.'}, status=400)

        if not _is_admin(request.user, conversation):
            return Response({'error': 'Only the group admin can remove members.'}, status=403)

        user_id = request.data.get('user_id')
        if not user_id:
            return Response({'error': 'user_id required.'}, status=400)

        if user_id == conversation.admin_id:
            return Response({'error': 'Cannot remove the group admin.'}, status=400)

        try:
            user_to_remove = User.objects.get(id=user_id)
        except User.DoesNotExist:
            return Response({'error': 'User not found.'}, status=404)

        if not conversation.participants.filter(id=user_id).exists():
            return Response({'error': 'User is not a member.'}, status=400)

        # Remove from participants and delete member record
        conversation.participants.remove(user_to_remove)
        ConversationMember.objects.filter(conversation=conversation, user=user_to_remove).delete()

        # Remove admin role if user was an admin
        if conversation.admins.filter(id=user_id).exists():
            conversation.admins.remove(user_to_remove)

        # Broadcast member removed
        self._broadcast_group_event(conversation, 'group_user_removed', {
            'conversation_id': str(conversation.id),
            'user_id': user_id,
            'removed_by': request.user.id
        })

        # Create system message for member kicked
        self._create_member_system_message(
            conversation=conversation,
            message_type='member_kicked',
            actor=request.user,
            target=user_to_remove
        )

        return Response({'status': 'Member removed.'})

    @action(detail=True, methods=['post'], url_path='leave')
    def leave_group(self, request, pk=None):
        """Leave the group conversation."""
        conversation = self.get_object()

        if not conversation.is_group:
            return Response({'error': 'This is not a group conversation.'}, status=400)

        if conversation.admin_id == request.user.id:
            return Response({
                'error': 'The group creator cannot leave. Transfer ownership first or delete the group.'
            }, status=400)

        # Remove user from group
        conversation.participants.remove(request.user)
        ConversationMember.objects.filter(conversation=conversation, user=request.user).delete()

        # Remove admin role if user was an admin
        if conversation.admins.filter(id=request.user.id).exists():
            conversation.admins.remove(request.user)

        # Broadcast user left
        self._broadcast_group_event(conversation, 'group_user_removed', {
            'conversation_id': str(conversation.id),
            'user_id': request.user.id,
            'left_voluntarily': True
        })

        # Create system message for member left
        print(f"[DEBUG leave_group] Creating system message for member_left. Actor: {request.user.username}")
        print(f"[DEBUG leave_group] Remaining participants: {list(conversation.participants.values_list('username', flat=True))}")
        self._create_member_system_message(
            conversation=conversation,
            message_type='member_left',
            actor=request.user
        )
        print(f"[DEBUG leave_group] System message created successfully")

        return Response({'status': 'Left group.'})

    @action(detail=True, methods=['get'], url_path='join-requests')
    def get_join_requests(self, request, pk=None):
        """Get pending join requests for the group. Admin only."""
        conversation = self.get_object()

        if not conversation.is_group:
            return Response({'error': 'This is not a group conversation.'}, status=400)

        if not _is_admin(request.user, conversation):
            return Response({'error': 'Only the group admin can view join requests.'}, status=403)

        requests = GroupJoinRequest.objects.filter(conversation=conversation, status='pending')
        serializer = GroupJoinRequestSerializer(requests, many=True)
        return Response(serializer.data)

    @action(detail=True, methods=['post'], url_path='accept-request')
    def accept_request(self, request, pk=None):
        """Accept a join request. Admin only."""
        conversation = self.get_object()

        if not conversation.is_group:
            return Response({'error': 'This is not a group conversation.'}, status=400)

        if not _is_admin(request.user, conversation):
            return Response({'error': 'Only the group admin can accept requests.'}, status=403)

        request_id = request.data.get('request_id')
        if not request_id:
            return Response({'error': 'request_id required.'}, status=400)

        try:
            join_request = GroupJoinRequest.objects.get(id=request_id, conversation=conversation, status='pending')
        except GroupJoinRequest.DoesNotExist:
            return Response({'error': 'Request not found.'}, status=404)

        # Check group size
        if conversation.participants.count() >= 10:
            return Response({'error': 'Group is at maximum capacity.'}, status=400)

        # Accept the request
        join_request.status = 'accepted'
        join_request.save()

        # Add user to group
        conversation.participants.add(join_request.user)
        ConversationMember.objects.create(conversation=conversation, user=join_request.user)

        # Create visibility record for new member
        ConversationVisibility.objects.get_or_create(
            user=join_request.user,
            conversation=conversation,
            defaults={'is_visible': True}
        )

        # Broadcast member added
        self._broadcast_group_event(conversation, 'group_user_added', {
            'conversation_id': str(conversation.id),
            'user': {
                'id': join_request.user.id,
                'username': join_request.user.username,
                'avatar': join_request.user.avatar.url if join_request.user.avatar else None
            }
        })

        # Create system message for request accepted
        print(f"[DEBUG accept_request] Creating request_accepted message. Actor: {request.user.username}, Target: {join_request.user.username}, RequestedBy: {join_request.requested_by.username}")
        self._create_member_system_message(
            conversation=conversation,
            message_type='request_accepted',
            actor=request.user,
            target=join_request.user,
            requested_by=join_request.requested_by
        )
        print(f"[DEBUG accept_request] Message created successfully")

        return Response({'status': 'Request accepted.'})

    @action(detail=True, methods=['post'], url_path='reject-request')
    def reject_request(self, request, pk=None):
        """Reject a join request. Admin only."""
        conversation = self.get_object()

        if not conversation.is_group:
            return Response({'error': 'This is not a group conversation.'}, status=400)

        if not _is_admin(request.user, conversation):
            return Response({'error': 'Only the group admin can reject requests.'}, status=403)

        request_id = request.data.get('request_id')
        if not request_id:
            return Response({'error': 'request_id required.'}, status=400)

        try:
            join_request = GroupJoinRequest.objects.get(id=request_id, conversation=conversation, status='pending')
        except GroupJoinRequest.DoesNotExist:
            return Response({'error': 'Request not found.'}, status=404)

        join_request.status = 'rejected'
        join_request.save()

        return Response({'status': 'Request rejected.'})

    @action(detail=True, methods=['post'], url_path='regenerate-invite-links')
    def regenerate_invite_links(self, request, pk=None):
        """Regenerate group invite links. Admin only."""
        conversation = self.get_object()
        if not conversation.is_group or not _is_admin(request.user, conversation):
            return Response({'error': 'Permission denied.'}, status=403)

        conversation.invite_token = secrets.token_urlsafe(32)
        conversation.invite_short_token = ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(12))
        conversation.save()

        serializer = self.get_serializer(conversation)
        return Response(serializer.data)

    @action(detail=True, methods=['post'], url_path='promote-admin')
    def promote_admin(self, request, pk=None):
        """Promote a member to admin. Admin only."""
        conversation = self.get_object()

        if not conversation.is_group:
            return Response({'error': 'This is not a group conversation.'}, status=400)

        if not _is_admin(request.user, conversation):
            return Response({'error': 'Only admins can promote members.'}, status=403)

        user_id = request.data.get('user_id')
        if not user_id:
            return Response({'error': 'user_id required.'}, status=400)

        try:
            user_to_promote = User.objects.get(id=user_id)
        except User.DoesNotExist:
            return Response({'error': 'User not found.'}, status=404)

        # Check if user is a member
        if not conversation.participants.filter(id=user_id).exists():
            return Response({'error': 'User is not a member of this group.'}, status=400)

        # Check if already an admin
        if conversation.admins.filter(id=user_id).exists() or conversation.admin_id == user_id:
            return Response({'error': 'User is already an admin.'}, status=400)

        # Add to admins
        conversation.admins.add(user_to_promote)

        # Broadcast update
        output_serializer = ConversationSerializer(conversation, context={'request': request})
        self._broadcast_group_event(conversation, 'group_updated', {
            'conversation': output_serializer.data
        })

        # Create system message
        self._create_member_system_message(
            conversation=conversation,
            message_type='member_promoted',
            actor=request.user,
            target=user_to_promote
        )

        return Response(output_serializer.data)

    @action(detail=True, methods=['post'], url_path='demote-admin')
    def demote_admin(self, request, pk=None):
        """Demote an admin to regular member. Admin only. Cannot demote the creator."""
        conversation = self.get_object()

        if not conversation.is_group:
            return Response({'error': 'This is not a group conversation.'}, status=400)

        if not _is_admin(request.user, conversation):
            return Response({'error': 'Only admins can demote other admins.'}, status=403)

        user_id = request.data.get('user_id')
        if not user_id:
            return Response({'error': 'user_id required.'}, status=400)

        try:
            user_to_demote = User.objects.get(id=user_id)
        except User.DoesNotExist:
            return Response({'error': 'User not found.'}, status=404)

        # Cannot demote the creator
        if conversation.admin_id == user_id:
            return Response({'error': 'Cannot demote the group creator.'}, status=400)

        # Check if user is an admin
        if not conversation.admins.filter(id=user_id).exists():
            return Response({'error': 'User is not an admin.'}, status=400)

        # Remove from admins
        conversation.admins.remove(user_to_demote)

        # Broadcast update
        output_serializer = ConversationSerializer(conversation, context={'request': request})
        self._broadcast_group_event(conversation, 'group_updated', {
            'conversation': output_serializer.data
        })

        # Create system message
        self._create_member_system_message(
            conversation=conversation,
            message_type='member_demoted',
            actor=request.user,
            target=user_to_demote
        )

        return Response(output_serializer.data)


    @action(detail=False, methods=['get'], url_path='join-info/(?P<token>[^/.]+)', permission_classes=[])
    def join_info(self, request, token=None):
        """Get group info via invite token. Publicly accessible."""
        from django.db.models import Q
        conversation = get_object_or_404(
            Conversation,
            Q(invite_token=token) | Q(invite_short_token=token),
            invite_enabled=True,
            is_group=True
        )

        # Check expiration
        if conversation.invite_expires_at and conversation.invite_expires_at < timezone.now():
            return Response({'error': 'Invite link has expired.'}, status=410)

        serializer = GroupInviteLinkSerializer(conversation, context={'request': request})

        # Check if user is already a member
        is_member = False
        if request.user.is_authenticated:
            is_member = conversation.participants.filter(id=request.user.id).exists()

        return Response({
            'group': serializer.data,
            'is_member': is_member
        })

    @action(detail=False, methods=['post'], url_path='join-by-link/(?P<token>[^/.]+)')
    def join_by_link(self, request, token=None):
        """Join a group via invite token."""
        from django.db.models import Q
        conversation = get_object_or_404(
            Conversation,
            Q(invite_token=token) | Q(invite_short_token=token),
            invite_enabled=True,
            is_group=True
        )

        # Check expiration
        if conversation.invite_expires_at and conversation.invite_expires_at < timezone.now():
            return Response({'error': 'Invite link has expired.'}, status=410)

        # Check if already a member
        if request.user.is_authenticated and conversation.participants.filter(id=request.user.id).exists():
            return Response({'error': 'You are already a member of this group.'}, status=400)

        # Check group size limit
        # if conversation.participants.count() >= 10:
        #     return Response({'error': 'Group is full.'}, status=400)

        if conversation.invite_require_approval:
            # Create a join request
            join_request, created = GroupJoinRequest.objects.get_or_create(
                conversation=conversation,
                user=request.user,
                defaults={'requested_by': request.user, 'status': 'pending'}
            )

            if not created and join_request.status != 'pending':
                if join_request.status == 'rejected':
                    join_request.status = 'pending'
                    join_request.save()

            # Broadcast to admin
            self._broadcast_to_user(conversation.admin.id, 'group_join_request', {
                'conversation_id': str(conversation.id),
                'request_id': join_request.id,
                'user': {
                    'id': request.user.id,
                    'username': request.user.username,
                    'avatar': request.user.avatar.url if request.user.avatar else None
                },
                'via_link': True
            })

            return Response({'status': 'approval_required', 'message': 'Join request sent to admin.'})
        else:
            # Join directly
            conversation.participants.add(request.user)
            ConversationMember.objects.create(conversation=conversation, user=request.user)
            ConversationVisibility.objects.get_or_create(
                user=request.user,
                conversation=conversation,
                defaults={'is_visible': True}
            )

            # Broadcast to group
            self._broadcast_group_event(conversation, 'group_user_added', {
                'conversation_id': str(conversation.id),
                'user': {
                    'id': request.user.id,
                    'username': request.user.username,
                    'avatar': request.user.avatar.url if request.user.avatar else None
                }
            })

            # Create system message
            self._create_member_system_message(
                conversation=conversation,
                message_type='member_added',
                actor=request.user
            )

            return Response({'status': 'joined', 'conversation_id': conversation.id})

    def _broadcast_group_event(self, conversation, event_type, data):
        """Broadcast an event to all group members."""
        import json
        from django.core.serializers.json import DjangoJSONEncoder
        from channels.layers import get_channel_layer
        from asgiref.sync import async_to_sync

        channel_layer = get_channel_layer()
        if channel_layer:
            # Ensure data is JSON serializable (handle datetimes like created_at)
            safe_data = json.loads(json.dumps(data, cls=DjangoJSONEncoder))
            normalized_id = str(conversation.id).replace('-', '')

            # 1. Broadcast to chat room
            async_to_sync(channel_layer.group_send)(
                f"chat_{normalized_id}",
                {
                    "type": event_type,
                    **safe_data
                }
            )

            # 2. Broadcast to each participant's personal room (for list updates)
            for participant in conversation.participants.all():
                async_to_sync(channel_layer.group_send)(
                    f"user_{participant.id}",
                    {
                        "type": event_type,
                        **safe_data
                    }
                )

    def _broadcast_to_user(self, user_id, event_type, data):
        """Broadcast an event to a specific user."""
        from channels.layers import get_channel_layer
        from asgiref.sync import async_to_sync

        channel_layer = get_channel_layer()
        if channel_layer:
            async_to_sync(channel_layer.group_send)(
                f"user_{user_id}",
                {
                    "type": event_type,
                    **data
                }
            )

    def _create_member_system_message(self, conversation, message_type, actor, target=None, requested_by=None):
        """
        Create a system message for member events and broadcast it.

        Args:
            conversation: The group conversation
            message_type: 'member_added', 'member_left', 'member_kicked', or 'request_accepted'
            actor: The user performing the action (admin for add/kick, user for leave)
            target: The user being added/kicked (None for leave)
            requested_by: The user who requested the addition (for request_accepted)
        """
        import json

        # Build content with actor and target usernames
        if message_type == 'member_left':
            content = json.dumps({
                'actor': actor.username,
                'actor_name': actor.first_name or actor.username
            })
        elif message_type == 'request_accepted':
            content = json.dumps({
                'actor': actor.username,
                'actor_name': actor.first_name or actor.username,
                'target': target.username if target else None,
                'target_name': (target.first_name or target.username) if target else None,
                'requested_by': requested_by.username if requested_by else None,
                'requested_by_name': (requested_by.first_name or requested_by.username) if requested_by else None
            })
        else:
            content = json.dumps({
                'actor': actor.username,
                'actor_name': actor.first_name or actor.username,
                'target': target.username if target else None,
                'target_name': (target.first_name or target.username) if target else None
            })

        # Create the system message
        system_message = Message.objects.create(
            conversation=conversation,
            sender=None,  # System message has no sender
            message_type=message_type,
            content=content
        )

        # Broadcast via WebSocket
        from channels.layers import get_channel_layer
        from asgiref.sync import async_to_sync

        channel_layer = get_channel_layer()
        if channel_layer:
            normalized_id = str(conversation.id).replace('-', '')
            message_data = {
                "id": system_message.id,
                "conversation_id": str(conversation.id),
                "message_type": message_type,
                "content": content,
                "sender": None,
                "created_at": system_message.created_at.isoformat()
            }

            # 1. Broadcast to chat room
            async_to_sync(channel_layer.group_send)(
                f"chat_{normalized_id}",
                {
                    "type": "chat_message",
                    "message": message_data
                }
            )

            # 2. Also broadcast to each participant's personal room
            # (similar to how regular messages are broadcast)
            for participant in conversation.participants.all():
                print(f"[DEBUG] Broadcasting system message to user_{participant.id} ({participant.username})")
                async_to_sync(channel_layer.group_send)(
                    f"user_{participant.id}",
                    {
                        "type": "chat_message",
                        "message": message_data
                    }
                )

        return system_message

    @action(detail=True, methods=['post'], url_path='share-post')
    def share_post(self, request, pk=None):
        """Share a post to this conversation."""
        conversation = self.get_object()

        post_id = request.data.get('post_id')
        if not post_id:
            return Response({'error': 'post_id required.'}, status=status.HTTP_400_BAD_REQUEST)

        from posts.models import Post
        try:
            post = Post.objects.get(id=post_id, is_active=True)
        except Post.DoesNotExist:
            return Response({'error': 'Post not found or inactive.'}, status=status.HTTP_404_NOT_FOUND)

        # Vérifier si le partage est autorisé pour ce post
        if not post.allow_sharing:
            return Response({'error': 'Sharing is disabled for this post'}, status=status.HTTP_403_FORBIDDEN)

        message_text = request.data.get('message', '').strip()

        # Create the message with shared post
        message = Message.objects.create(
            conversation=conversation,
            sender=request.user,
            sender_name_snapshot=f"{request.user.first_name} {request.user.last_name}".strip() or request.user.username,
            sender_avatar_snapshot=request.user.avatar.url if request.user.avatar else None,
            message_type='post',
            shared_post=post,
            content=message_text
        )

        # Increment shared_count on the post
        post.shared_count += 1
        post.save(update_fields=['shared_count'])

        # Ensure everyone sees the conversation
        from chat.services import ensure_visibility_on_new_message
        ensure_visibility_on_new_message(conversation, request.user)

        # Update conversation timestamp
        conversation.save()

        # Broadcast via WebSocket
        from chat.serializers import MessageSerializer
        serialized_message = MessageSerializer(message, context={'request': request}).data

        from channels.layers import get_channel_layer
        from asgiref.sync import async_to_sync

        channel_layer = get_channel_layer()
        if channel_layer:
            normalized_id = str(conversation.id).replace('-', '')

            # Broadcast to chat room
            async_to_sync(channel_layer.group_send)(
                f"chat_{normalized_id}",
                {
                    "type": "chat_message",
                    "message_id": message.id,
                    "message": serialized_message
                }
            )

            # Broadcast to each participant's personal room
            for participant in conversation.participants.all():
                async_to_sync(channel_layer.group_send)(
                    f"user_{participant.id}",
                    {
                        "type": "chat_message",
                        "message_id": message.id,
                        "message": serialized_message
                    }
                )

        return Response(serialized_message, status=status.HTTP_201_CREATED)

    @action(detail=True, methods=['post'], url_path='share-category')
    def share_category(self, request, pk=None):
        """Share a category to this conversation."""
        conversation = self.get_object()

        category_id = request.data.get('category_id')
        if not category_id:
            return Response({'error': 'category_id required.'}, status=status.HTTP_400_BAD_REQUEST)

        from categories.models import Category
        try:
            category = Category.objects.get(id=category_id)
        except Category.DoesNotExist:
            return Response({'error': 'Category not found.'}, status=status.HTTP_404_NOT_FOUND)

        message_text = request.data.get('message', '').strip()

        # Create the message with shared category
        message = Message.objects.create(
            conversation=conversation,
            sender=request.user,
            sender_name_snapshot=f"{request.user.first_name} {request.user.last_name}".strip() or request.user.username,
            sender_avatar_snapshot=request.user.avatar.url if request.user.avatar else None,
            message_type='category',
            shared_category=category,
            content=message_text
        )

        # Increment shared_count on the category
        category.shared_count += 1
        category.save(update_fields=['shared_count'])

        # Create Notification for category owner if it's someone else
        # (Optional, but good practice if we want to track it)

        # Ensure everyone sees the conversation
        from chat.services import ensure_visibility_on_new_message
        ensure_visibility_on_new_message(conversation, request.user)

        # Update conversation timestamp
        conversation.save()

        # Broadcast via WebSocket
        from chat.serializers import MessageSerializer
        serialized_message = MessageSerializer(message, context={'request': request}).data

        from channels.layers import get_channel_layer
        from asgiref.sync import async_to_sync

        channel_layer = get_channel_layer()
        if channel_layer:
            normalized_id = str(conversation.id).replace('-', '')

            # Broadcast to chat room
            async_to_sync(channel_layer.group_send)(
                f"chat_{normalized_id}",
                {
                    "type": "chat_message",
                    "message_id": message.id,
                    "message": serialized_message
                }
            )

            # Broadcast to each participant's personal room
            for participant in conversation.participants.all():
                async_to_sync(channel_layer.group_send)(
                    f"user_{participant.id}",
                    {
                        "type": "chat_message",
                        "message_id": message.id,
                        "message": serialized_message
                    }
                )

        return Response(serialized_message, status=status.HTTP_201_CREATED)

    @action(detail=True, methods=['post'], url_path='share-profile')
    def share_profile(self, request, pk=None):
        """Share a user profile to this conversation."""
        conversation = self.get_object()

        user_id = request.data.get('user_id')
        if not user_id:
            return Response({'error': 'user_id required.'}, status=status.HTTP_400_BAD_REQUEST)

        from django.contrib.auth import get_user_model
        User = get_user_model()
        try:
            profile = User.objects.get(id=user_id)
        except User.DoesNotExist:
            return Response({'error': 'User not found.'}, status=status.HTTP_404_NOT_FOUND)

        message_text = request.data.get('message', '').strip()

        # Create the message with shared profile
        message = Message.objects.create(
            conversation=conversation,
            sender=request.user,
            sender_name_snapshot=f"{request.user.first_name} {request.user.last_name}".strip() or request.user.username,
            sender_avatar_snapshot=request.user.avatar.url if request.user.avatar else None,
            message_type='profile',
            shared_profile=profile,
            content=message_text
        )

        # Ensure everyone sees the conversation
        from chat.services import ensure_visibility_on_new_message
        ensure_visibility_on_new_message(conversation, request.user)

        # Update conversation timestamp
        conversation.save()

        # Broadcast via WebSocket
        from chat.serializers import MessageSerializer
        serialized_message = MessageSerializer(message, context={'request': request}).data

        from channels.layers import get_channel_layer
        from asgiref.sync import async_to_sync

        channel_layer = get_channel_layer()
        if channel_layer:
            normalized_id = str(conversation.id).replace('-', '')

            # Broadcast to chat room
            async_to_sync(channel_layer.group_send)(
                f"chat_{normalized_id}",
                {
                    "type": "chat_message",
                    "message_id": message.id,
                    "message": serialized_message
                }
            )

            # Broadcast to each participant's personal room
            for participant in conversation.participants.all():
                async_to_sync(channel_layer.group_send)(
                    f"user_{participant.id}",
                    {
                        "type": "chat_message",
                        "message_id": message.id,
                        "message": serialized_message
                    }
                )

        return Response(serialized_message, status=status.HTTP_201_CREATED)

class MessageViewSet(viewsets.ModelViewSet):
    """
    ViewSet for managing messages within a conversation.

    list: Get paginated message history
    create: Send a new message
    destroy: Soft delete a message
    """
    permission_classes = [IsAuthenticated]
    serializer_class = MessageSerializer
    pagination_class = MessageCursorPagination

    def get_queryset(self):
        """Get messages for the specified conversation."""
        conversation_id = self.kwargs.get('conversation_pk')
        user = self.request.user

        # Verify user is a participant
        conversation = get_object_or_404(
            Conversation.objects.filter(participants=user),
            pk=conversation_id
        )

        # Get visibility settings
        visibility = ConversationVisibility.objects.filter(
            user=user,
            conversation=conversation
        ).first()

        queryset = Message.objects.filter(
            conversation=conversation,
            is_deleted=False
        ).exclude(deleted_by_users=user)

        # Filter messages hidden before hidden_until
        if visibility and visibility.hidden_until:
            queryset = queryset.filter(created_at__gt=visibility.hidden_until)

        return queryset.select_related('sender').prefetch_related(
            'read_by',
            'read_by__user',
            'reactions',
            'reactions__user'
        ).order_by('-created_at')

    def get_serializer_class(self):
        if self.action == 'create':
            return MessageCreateSerializer
        return MessageSerializer

    def create(self, request, *args, **kwargs):
        """Send a new message."""
        conversation_id = self.kwargs.get('conversation_pk')
        user = request.user

        # Verify user is a participant
        conversation = get_object_or_404(
            Conversation.objects.filter(participants=user),
            pk=conversation_id
        )

        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)

        # Create message
        message = Message.objects.create(
            conversation=conversation,
            sender=user,
            sender_name_snapshot=f"{user.first_name} {user.last_name}".strip() or user.username,
            sender_avatar_snapshot=user.avatar.url if user.avatar else None,
            **serializer.validated_data
        )

        # Ensure everyone sees the conversation
        from chat.services import ensure_visibility_on_new_message
        ensure_visibility_on_new_message(conversation, user)

        # Update conversation timestamp
        conversation.save()

        # Broadcast via WebSocket
        self._broadcast_message(message, event_type="chat_message")

        # Return full message data
        output_serializer = MessageSerializer(message, context={'request': request})
        return Response(output_serializer.data, status=status.HTTP_201_CREATED)

    def partial_update(self, request, *args, **kwargs):
        """Edit a message."""
        instance = self.get_object()

        # Only sender can edit their message
        if instance.sender != request.user:
            return Response(
                {'error': 'You can only edit your own messages.'},
                status=status.HTTP_403_FORBIDDEN
            )

        # Cannot edit deleted messages
        if instance.is_deleted:
            return Response(
                {'error': 'Cannot edit a deleted message.'},
                status=status.HTTP_400_BAD_REQUEST
            )

        serializer = self.get_serializer(instance, data=request.data, partial=True)
        serializer.is_valid(raise_exception=True)

        original_content = instance.content
        serializer.save()

        # Check if content actually changed
        if 'content' in serializer.validated_data and serializer.validated_data['content'] != original_content:
            instance.is_edited = True
            instance.save()

            # Broadcast the update
            self._broadcast_message(instance, event_type="message_updated")

        return Response(MessageSerializer(instance).data)

    def _broadcast_message(self, message, event_type="chat_message"):
        """Broadcast message to WebSocket clients."""
        import json
        from django.core.serializers.json import DjangoJSONEncoder
        from channels.layers import get_channel_layer
        from asgiref.sync import async_to_sync
        from .serializers import MessageSerializer, MessageReactionSerializer

        channel_layer = get_channel_layer()
        if channel_layer is None:
            return

        # Explicitly construct the payload to ensure exact compatibility with frontend expectations
        # Get absolute URLs where possible
        request = getattr(self, 'request', None)

        def get_absolute_url(url):
            if not url: return None
            if url.startswith('http'): return url
            if request: return request.build_absolute_uri(url)
            return url

        serializer = MessageSerializer(message)

        # We construct a raw dictionary that might contain UUIDs/DateTimes
        message_data_raw = {
            "id": message.id,
            "conversation_id": message.conversation_id,
            "sender": {
                "id": message.sender_id,
                "username": message.sender.username if message.sender else "Deleted Account",
                "avatar": get_absolute_url(message.sender.avatar.url) if message.sender and message.sender.avatar else (message.sender_avatar_snapshot or "/default_user_avatar.png"),
                "display_name": message.sender_name_snapshot or (f"{message.sender.first_name} {message.sender.last_name}".strip() if message.sender else "Deleted Account")
            },
            "content": message.content,
            "attachment": get_absolute_url(message.attachment.url) if message.attachment else None,
            "attachment_type": message.attachment_type,
            "is_read": message.is_read,
            "is_edited": message.is_edited,
            "is_deleted": message.is_deleted,
            "reply_to": message.reply_to_id,
            "reply_to_preview": serializer.get_reply_to_preview(message),
            "reactions_count": serializer.get_reactions_count(message),
            "reactions": MessageReactionSerializer(message.reactions.all(), many=True).data,
            "created_at": message.created_at
        }

        # The MAGIC TRICK: use DjangoJSONEncoder to stringify everything nested (UUIDs, dates, etc)
        # and then parse it back to a clean dictionary for Channels.
        message_data = json.loads(json.dumps(message_data_raw, cls=DjangoJSONEncoder))

        normalized_conv_id = str(message.conversation_id).replace('-', '')

        # 1. Broadcast to the conversation room
        async_to_sync(channel_layer.group_send)(
            f"chat_{normalized_conv_id}",
            {
                "type": "chat_message",
                "event_type": event_type,
                "message": message_data
            }
        )

        # This ensures they get the message even if they haven't joined the chat room yet (e.g. new convo)
        # or if their connection hasn't subscribed to all rooms yet.
        for participant in message.conversation.participants.all():
            if participant == message.sender:
                continue

            async_to_sync(channel_layer.group_send)(
                f"user_{participant.id}",
                {
                    "type": "chat_message",
                    "event_type": event_type,
                    "message": message_data
                }
            )

    def destroy(self, request, *args, **kwargs):
        """Soft delete a message."""
        message = self.get_object()

        # Only sender can delete their message
        if message.sender != request.user:
            return Response(
                {'error': 'You can only delete your own messages.'},
                status=status.HTTP_403_FORBIDDEN
            )

        message.is_deleted = True
        message.save()

        # Broadcast the deletion
        self._broadcast_message(message, event_type="message_deleted")

        return Response(status=status.HTTP_204_NO_CONTENT)

    @action(detail=True, methods=['post'])
    def delete_for_me(self, request, conversation_pk=None, pk=None):
        """Hide a message only for the current user."""
        message = self.get_object()
        message.deleted_by_users.add(request.user)
        return Response({'status': 'Message deleted for you.'}, status=status.HTTP_200_OK)

    @action(detail=True, methods=['post'])
    def add_reaction(self, request, conversation_pk=None, pk=None):
        """Add a reaction to a message."""
        message = self.get_object()

        serializer = ReactionCreateSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        emoji = serializer.validated_data['emoji']

        # Create or update reaction
        reaction, created = MessageReaction.objects.get_or_create(
            message=message,
            user=request.user,
            emoji=emoji
        )

        if created:
            self._broadcast_message(message, event_type="message_updated")
            return Response({'status': 'Reaction added.'}, status=status.HTTP_201_CREATED)
        return Response({'status': 'Reaction already present.'})

    @action(detail=True, methods=['delete'])
    def remove_reaction(self, request, conversation_pk=None, pk=None):
        """Remove a reaction from a message."""
        message = self.get_object()
        emoji = request.query_params.get('emoji', '')

        deleted, _ = MessageReaction.objects.filter(
            message=message,
            user=request.user,
            emoji=emoji
        ).delete()

        if deleted:
            self._broadcast_message(message, event_type="message_updated")
            return Response({'status': 'Reaction removed.'})
        return Response(
            {'error': 'Reaction not found.'},
            status=status.HTTP_404_NOT_FOUND
        )


class ChatAppearanceViewSet(viewsets.ViewSet):
    """
    ViewSet for managing chat appearance per conversation.
    Allows users to customize backgrounds, colors, and display modes.

    GET /conversations/{id}/appearance/ - Get appearance settings
    PUT /conversations/{id}/appearance/ - Update appearance settings
    DELETE /conversations/{id}/appearance/ - Reset to default
    POST /conversations/{id}/appearance/upload_background/ - Upload custom image
    """
    permission_classes = [IsAuthenticated]

    def _get_conversation(self, pk):
        """Helper to get conversation and ensure user is a participant."""
        from django.shortcuts import get_object_or_404
        conversation = get_object_or_404(
            Conversation.objects.filter(participants=self.request.user),
            pk=pk
        )
        return conversation

    def list(self, request, conversation_pk=None):
        """Get current appearance for this conversation."""
        conversation = self._get_conversation(conversation_pk)

        # Shared appearance: No user required, OneToOne
        appearance, created = ChatAppearance.objects.get_or_create(
            conversation=conversation
        )

        serializer = ChatAppearanceSerializer(appearance, context={'request': request})
        return Response(serializer.data)

    def create(self, request, conversation_pk=None):
        """Create or update appearance settings (POST method)."""
        conversation = self._get_conversation(conversation_pk)

        appearance, created = ChatAppearance.objects.get_or_create(
            conversation=conversation
        )

        serializer = ChatAppearanceSerializer(appearance, data=request.data, partial=True, context={'request': request})
        serializer.is_valid(raise_exception=True)
        appearance = serializer.save()

        # Broadcast update to channel
        from asgiref.sync import async_to_sync
        from channels.layers import get_channel_layer
        channel_layer = get_channel_layer()
        async_to_sync(channel_layer.group_send)(
            f"chat_{conversation.id}",
            {
                "type": "appearance_update",
                "conversation_id": str(conversation.id),
                "appearance": serializer.data
            }
        )

        return Response(serializer.data)

    def update(self, request, conversation_pk=None, pk=None):
        """Update appearance settings via PUT."""
        return self._do_update(request, conversation_pk)

    def partial_update(self, request, conversation_pk=None, pk=None):
        """Update appearance settings via PATCH."""
        return self._do_update(request, conversation_pk)

    def _do_update(self, request, conversation_pk):
        """Shared update logic for PUT/PATCH."""
        conversation = self._get_conversation(conversation_pk)

        appearance, created = ChatAppearance.objects.get_or_create(
            conversation=conversation
        )

        serializer = ChatAppearanceSerializer(appearance, data=request.data, partial=True, context={'request': request})
        serializer.is_valid(raise_exception=True)
        serializer.save()

        # Broadcast update
        from asgiref.sync import async_to_sync
        from channels.layers import get_channel_layer
        channel_layer = get_channel_layer()
        if channel_layer:
            async_to_sync(channel_layer.group_send)(
                f"chat_{conversation.id}",
                {
                    "type": "appearance_update",
                    "conversation_id": str(conversation.id),
                    "appearance": serializer.data
                }
            )

        return Response(serializer.data)

    @action(detail=False, methods=['put', 'patch'])
    def update_settings(self, request, conversation_pk=None):
        """Update appearance settings via PUT/PATCH (legacy action)."""
        return self._do_update(request, conversation_pk)

    @action(detail=False, methods=['delete'])
    def reset(self, request, conversation_pk=None):
        """Reset to default appearance."""
        conversation = self._get_conversation(conversation_pk)

        deleted_count, _ = ChatAppearance.objects.filter(
            conversation=conversation
        ).delete()

        if deleted_count:
            # Broadcast reset
            from asgiref.sync import async_to_sync
            from channels.layers import get_channel_layer
            channel_layer = get_channel_layer()
            async_to_sync(channel_layer.group_send)(
                f"chat_{conversation.id}",
                {
                    "type": "appearance_update",
                    "conversation_id": str(conversation.id),
                    "appearance": None
                }
            )
            return Response({'status': 'Reset to default.'})
        return Response({'status': 'Already using default.'})

    @action(detail=False, methods=['post'])
    def upload_background(self, request, conversation_pk=None):
        """Upload a custom background image."""
        conversation = self._get_conversation(conversation_pk)

        if 'bg_image' not in request.FILES:
            return Response(
                {'error': 'No image file provided.'},
                status=status.HTTP_400_BAD_REQUEST
            )

        appearance, created = ChatAppearance.objects.get_or_create(
            conversation=conversation
        )

        # Update with new image
        appearance.bg_image = request.FILES['bg_image']
        appearance.bg_type = 'image'
        appearance.save()

        serializer = ChatAppearanceSerializer(appearance, context={'request': request})

        # Broadcast update
        from asgiref.sync import async_to_sync
        from channels.layers import get_channel_layer
        channel_layer = get_channel_layer()
        async_to_sync(channel_layer.group_send)(
            f"chat_{conversation.id}",
            {
                "type": "appearance_update",
                "conversation_id": str(conversation.id),
                "appearance": serializer.data
            }
        )

        return Response(serializer.data)
