import json
import logging
from uuid import UUID
from decimal import Decimal
from datetime import datetime
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async
from django.utils import timezone
from .models import Conversation, ConversationParticipant, Message, UserPresence, MessageRead
from .serializers import MessageSerializer, ConversationSerializer
from Auth.models import Profile

logger = logging.getLogger(__name__)

def serialize_for_websocket(data):
    """
    Convertit récursivement les objets non-sérialisables en JSON en types sérialisables
    """
    if isinstance(data, dict):
        return {key: serialize_for_websocket(value) for key, value in data.items()}
    elif isinstance(data, list):
        return [serialize_for_websocket(item) for item in data]
    elif isinstance(data, UUID):
        return str(data)
    elif isinstance(data, Decimal):
        return float(data)
    elif isinstance(data, datetime):
        return data.isoformat()
    else:
        return data

class ChatConsumer(AsyncWebsocketConsumer):
    """Consumer WebSocket pour la messagerie instantanée"""

    async def connect(self):
        self.user_id = self.scope['url_route']['kwargs']['user_id']
        self.user_group_name = f'user_{self.user_id}'

        # Vérifier si l'utilisateur est authentifié
        user = self.scope.get('user')
        if not user or user.is_anonymous:
            logger.warning(f"Tentative de connexion WebSocket non authentifiée pour user_id: {self.user_id}")
            await self.close()
            return

        # Vérifier que l'utilisateur connecté correspond à l'user_id de l'URL
        if str(user.id) != str(self.user_id):
            logger.warning(f"Tentative d'accès WebSocket avec user_id incorrect: {self.user_id} vs {user.id}")
            await self.close()
            return

        self.user = user

        # Rejoindre le groupe utilisateur
        await self.channel_layer.group_add(
            self.user_group_name,
            self.channel_name
        )

        # Rejoindre les groupes des conversations de l'utilisateur
        conversations = await self.get_user_conversations(self.user_id)
        for conversation in conversations:
            await self.channel_layer.group_add(
                f'conversation_{conversation.id}',
                self.channel_name
            )

        # Mettre à jour le statut de présence
        presence = await self.update_user_presence(self.user_id, 'online')

        # Notifier les autres utilisateurs du changement de statut
        if presence:
            await self.broadcast_presence_update(self.user_id, 'online')

        await self.accept()

        # Envoyer les statuts de présence existants des autres utilisateurs connectés
        await self.send_existing_user_presences()

        logger.info(f"User {self.user_id} connected to chat")

    async def send_existing_user_presences(self):
        """Envoyer les statuts de présence existants des autres utilisateurs connectés"""
        try:
            # Obtenir toutes les conversations de cet utilisateur
            conversations = await self.get_user_conversations(self.user_id)

            # Créer un set pour éviter les doublons d'utilisateurs
            other_users = set()

            # Pour chaque conversation, récupérer les participants
            for conversation in conversations:
                participants = await self.get_conversation_participants(conversation.id)
                for participant in participants:
                    if participant.id != self.user_id:
                        other_users.add(participant.id)

            # Envoyer le statut de présence pour chaque utilisateur
            for user_id in other_users:
                presence = await self.get_user_presence(user_id)
                if presence:
                    await self.send(text_data=json.dumps({
                        'type': 'presence_update',
                        'user_id': str(user_id),
                        'status': presence.status
                    }))

        except Exception as e:
            logger.error(f"Error sending existing user presences: {str(e)}")

    async def disconnect(self, close_code):
        # Quitter tous les groupes
        await self.channel_layer.group_discard(
            self.user_group_name,
            self.channel_name
        )

        if hasattr(self, 'user'):
            conversations = await self.get_user_conversations(self.user_id)
            for conversation in conversations:
                await self.channel_layer.group_discard(
                    f'conversation_{conversation.id}',
                    self.channel_name
                )

            # Mettre à jour le statut de présence
            presence = await self.update_user_presence(self.user_id, 'offline')

            # Notifier les autres utilisateurs du changement de statut
            if presence:
                await self.broadcast_presence_update(self.user_id, 'offline')

        logger.info(f"User {self.user_id} disconnected from chat")

    async def receive(self, text_data):
        """Recevoir et traiter les messages du client"""
        try:
            text_data_json = json.loads(text_data)
            message_type = text_data_json.get('type')

            if message_type == 'send_message':
                await self.handle_send_message(text_data_json)
            elif message_type == 'typing_start':
                await self.handle_typing_start(text_data_json)
            elif message_type == 'typing_stop':
                await self.handle_typing_stop(text_data_json)
            elif message_type == 'mark_as_read':
                await self.handle_mark_as_read(text_data_json)
            elif message_type == 'join_conversation':
                await self.handle_join_conversation(text_data_json)
            elif message_type == 'leave_conversation':
                await self.handle_leave_conversation(text_data_json)
            elif message_type == 'presence_update':
                await self.handle_presence_update(text_data_json)
            else:
                logger.warning(f"Unknown message type: {message_type}")
                await self.send(text_data=json.dumps({
                    'type': 'error',
                    'message': f'Unknown message type: {message_type}'
                }))

        except json.JSONDecodeError:
            await self.send(text_data=json.dumps({
                'type': 'error',
                'message': 'Invalid JSON format'
            }))
        except Exception as e:
            logger.error(f"Error in receive: {str(e)}")
            await self.send(text_data=json.dumps({
                'type': 'error',
                'message': 'Internal server error'
            }))

    async def handle_send_message(self, data):
        """Gérer l'envoi d'un nouveau message"""
        conversation_id = data.get('conversation_id')
        content = data.get('content', '').strip()
        message_type = data.get('message_type', 'text')
        attachment = data.get('attachment')

        # Vérifier qu'on a soit du contenu, soit un fichier
        if not conversation_id or (not content and not attachment):
            await self.send(text_data=json.dumps({
                'type': 'error',
                'message': 'Conversation ID and content or attachment are required'
            }))
            return

        # Vérifier que l'utilisateur peut envoyer dans cette conversation
        can_send = await self.can_user_send_message(self.user_id, conversation_id)
        if not can_send:
            await self.send(text_data=json.dumps({
                'type': 'error',
                'message': 'You cannot send messages to this conversation'
            }))
            return

        # Créer le message avec ou sans fichier
        message = await self.create_message(conversation_id, self.user_id, content, message_type, attachment)
        if not message:
            await self.send(text_data=json.dumps({
                'type': 'error',
                'message': 'Failed to create message'
            }))
            return

        # Sérialiser le message
        message_data = await self.serialize_message(message)

        # Auto-marquer comme lu pour les utilisateurs connectés (présents dans la conversation)
        await self.auto_mark_read_for_online_users(message, conversation_id)

        # Envoyer le message à tous les participants de la conversation
        await self.channel_layer.group_send(
            f'conversation_{conversation_id}',
            {
                'type': 'chat_message',
                'message': message_data
            }
        )

    async def handle_typing_start(self, data):
        """Gérer le début de frappe"""
        conversation_id = data.get('conversation_id')
        if conversation_id:
            await self.update_typing_status(self.user_id, conversation_id, True)

            # Récupérer le nom de l'utilisateur
            user_name = await self.get_user_display_name(self.user_id)

            await self.channel_layer.group_send(
                f'conversation_{conversation_id}',
                {
                    'type': 'typing_status',
                    'user_id': str(self.user_id),
                    'user_name': user_name,
                    'is_typing': True,
                    'conversation_id': str(conversation_id)
                }
            )

    async def handle_typing_stop(self, data):
        """Gérer l'arrêt de frappe"""
        conversation_id = data.get('conversation_id')
        if conversation_id:
            await self.update_typing_status(self.user_id, conversation_id, False)

            # Récupérer le nom de l'utilisateur
            user_name = await self.get_user_display_name(self.user_id)

            await self.channel_layer.group_send(
                f'conversation_{conversation_id}',
                {
                    'type': 'typing_status',
                    'user_id': str(self.user_id),
                    'user_name': user_name,
                    'is_typing': False,
                    'conversation_id': str(conversation_id)
                }
            )

    async def handle_mark_as_read(self, data):
        """Marquer les messages comme lus"""
        conversation_id = data.get('conversation_id')
        if conversation_id:
            await self.mark_conversation_as_read(self.user_id, conversation_id)
            await self.channel_layer.group_send(
                f'conversation_{conversation_id}',
                {
                    'type': 'messages_read',
                    'user_id': str(self.user_id),
                    'conversation_id': str(conversation_id)
                }
            )

    async def handle_join_conversation(self, data):
        """Rejoindre une conversation"""
        conversation_id = data.get('conversation_id')
        if conversation_id:
            await self.channel_layer.group_add(
                f'conversation_{conversation_id}',
                self.channel_name
            )

    async def handle_leave_conversation(self, data):
        """Quitter une conversation"""
        conversation_id = data.get('conversation_id')
        if conversation_id:
            await self.channel_layer.group_discard(
                f'conversation_{conversation_id}',
                self.channel_name
            )

    async def handle_presence_update(self, data):
        """Gérer les mises à jour de présence"""
        status = data.get('status', 'online')
        if status in ['online', 'offline', 'away']:
            await self.update_user_presence(self.user_id, status)
            await self.broadcast_presence_update(self.user_id, status)

    # Méthodes pour recevoir les messages des groupes
    async def chat_message(self, event):
        """Envoyer un message de chat au WebSocket"""
        message_data = serialize_for_websocket(event['message'])
        await self.send(text_data=json.dumps({
            'type': 'new_message',
            'message': message_data
        }))

    async def typing_status(self, event):
        """Envoyer le statut de frappe"""
        # Ne pas envoyer à l'expéditeur
        if str(event['user_id']) != str(self.user_id):
            await self.send(text_data=json.dumps({
                'type': 'typing_status',
                'user_id': str(event['user_id']),
                'user_name': event.get('user_name', ''),
                'is_typing': event['is_typing'],
                'conversation_id': str(event['conversation_id'])
            }))

    async def messages_read(self, event):
        """Notifier que des messages ont été lus"""
        if str(event['user_id']) != str(self.user_id):
            await self.send(text_data=json.dumps({
                'type': 'messages_read',
                'user_id': str(event['user_id']),
                'conversation_id': str(event['conversation_id'])
            }))

    async def user_presence_update(self, event):
        """Notifier d'un changement de présence"""
        await self.send(text_data=json.dumps({
            'type': 'presence_update',
            'user_id': str(event['user_id']),
            'status': event['status']
        }))

    async def new_conversation_notification(self, event):
        """Notifier d'une nouvelle conversation"""
        conversation = serialize_for_websocket(event['conversation'])
        conversation_id = conversation['id']

        # Rejoindre automatiquement le groupe de la nouvelle conversation
        await self.channel_layer.group_add(
            f'conversation_{conversation_id}',
            self.channel_name
        )

        # Envoyer la notification au client
        await self.send(text_data=json.dumps({
            'type': 'new_conversation',
            'conversation': conversation
        }))

        # Diffuser les présences des participants de cette nouvelle conversation
        try:
            participants = await self.get_conversation_participants(conversation_id)
            for participant in participants:
                # Ne pas diffuser sa propre présence à soi-même
                if participant.id != self.user_id:
                    presence = await self.get_user_presence(participant.id)
                    if presence:
                        await self.send(text_data=json.dumps({
                            'type': 'presence_update',
                            'user_id': str(participant.id),
                            'status': presence.status
                        }))
        except Exception as e:
            logger.error(f"Error sending presence updates for new conversation: {str(e)}")

    async def broadcast_presence_update(self, user_id, status):
        """Diffuser un changement de statut de présence à tous les utilisateurs connectés"""
        try:
            # Obtenir tous les utilisateurs qui ont des conversations avec cet utilisateur
            user_conversations = await self.get_user_conversations(user_id)

            # Créer un set pour éviter les doublons d'utilisateurs
            notified_users = set()

            # Pour chaque conversation, notifier tous les participants
            for conversation in user_conversations:
                participants = await self.get_conversation_participants(conversation.id)
                for participant in participants:
                    if participant.id != user_id and participant.id not in notified_users:
                        notified_users.add(participant.id)
                        # Envoyer la notification au groupe utilisateur
                        await self.channel_layer.group_send(
                            f'user_{participant.id}',
                            {
                                'type': 'user_presence_update',
                                'user_id': user_id,
                                'status': status
                            }
                        )

            logger.info(f"Broadcasted presence update for user {user_id} ({status}) to {len(notified_users)} users")

        except Exception as e:
            logger.error(f"Error broadcasting presence update: {str(e)}")

    # Méthodes d'accès aux données (database_sync_to_async)
    @database_sync_to_async
    def get_user(self, user_id):
        try:
            return Profile.objects.get(id=user_id)
        except Profile.DoesNotExist:
            return None

    @database_sync_to_async
    def get_user_display_name(self, user_id):
        """Obtenir le nom d'affichage d'un utilisateur"""
        try:
            user = Profile.objects.get(id=user_id)
            if user.first_name and user.last_name:
                return f"{user.first_name} {user.last_name}"
            elif user.first_name:
                return user.first_name
            elif user.last_name:
                return user.last_name
            else:
                return user.email or f"Utilisateur {user_id}"
        except Profile.DoesNotExist:
            return f"Utilisateur {user_id}"

    @database_sync_to_async
    def get_user_conversations(self, user_id):
        return list(Conversation.objects.filter(
            participants__participant_id=user_id,
            participants__is_active=True,
            is_active=True
        ).distinct())

    @database_sync_to_async
    def get_conversation_participants(self, conversation_id):
        """Obtenir tous les participants d'une conversation"""
        return list(Profile.objects.filter(
            conversation_participants__conversation_id=conversation_id,
            conversation_participants__is_active=True
        ))

    @database_sync_to_async
    def get_conversations_data(self, user_id):
        try:
            user = Profile.objects.get(id=user_id)
            conversations = Conversation.objects.filter(
                participants__participant=user,
                participants__is_active=True,
                is_active=True
            ).distinct()

            serializer = ConversationSerializer(conversations, many=True, context={'user': user})
            return serializer.data
        except Exception as e:
            logger.error(f"Error getting conversations data: {str(e)}")
            return []

    @database_sync_to_async
    def can_user_send_message(self, user_id, conversation_id):
        return ConversationParticipant.objects.filter(
            conversation_id=conversation_id,
            participant_id=user_id,
            is_active=True
        ).exists()

    @database_sync_to_async
    def create_message(self, conversation_id, sender_id, content, message_type, attachment=None):
        try:
            conversation = Conversation.objects.get(id=conversation_id)
            sender = Profile.objects.get(id=sender_id)

            message_data = {
                'conversation': conversation,
                'sender': sender,
                'content': content,
                'message_type': message_type
            }

            # Ajouter les informations du fichier si présent
            if attachment:
                message_data.update({
                    'file_url': attachment.get('fileUrl'),
                    'file_name': attachment.get('fileName'),
                    'file_size': attachment.get('fileSize'),
                    'file_type': attachment.get('fileType'),
                })
                # Si c'est un fichier, s'assurer que le type de message est correct
                if attachment.get('fileType', '').startswith('image/'):
                    message_data['message_type'] = 'image'
                else:
                    message_data['message_type'] = 'file'

            message = Message.objects.create(**message_data)

            # Mettre à jour l'activité de la conversation
            conversation.last_activity = message.created_at
            conversation.save(update_fields=['last_activity'])

            return message
        except Exception as e:
            logger.error(f"Error creating message: {str(e)}")
            return None

    @database_sync_to_async
    def serialize_message(self, message):
        serializer = MessageSerializer(message)
        return serialize_for_websocket(serializer.data)

    @database_sync_to_async
    def get_user_presence(self, user_id):
        """Récupérer la présence d'un utilisateur"""
        try:
            return UserPresence.objects.get(user_id=user_id)
        except UserPresence.DoesNotExist:
            return None

    @database_sync_to_async
    def update_user_presence(self, user_id, status):
        try:
            user = Profile.objects.get(id=user_id)
            presence, created = UserPresence.objects.get_or_create(user=user)
            old_status = presence.status
            presence.status = status
            presence.save()

            # Si le statut a changé, notifier les autres utilisateurs
            if old_status != status or created:
                logger.info(f"User {user_id} presence changed from {old_status} to {status}")

            return presence
        except Exception as e:
            logger.error(f"Error updating user presence: {str(e)}")
            return None

    @database_sync_to_async
    def update_typing_status(self, user_id, conversation_id, is_typing):
        try:
            user = Profile.objects.get(id=user_id)
            presence, created = UserPresence.objects.get_or_create(user=user)
            if is_typing:
                conversation = Conversation.objects.get(id=conversation_id)
                presence.is_typing_in = conversation
            else:
                presence.is_typing_in = None
            presence.save()
        except Exception as e:
            logger.error(f"Error updating typing status: {str(e)}")

    @database_sync_to_async
    def mark_conversation_as_read(self, user_id, conversation_id):
        """Marquer tous les messages non lus d'une conversation comme lus"""
        try:
            # Mettre à jour le last_read_at du participant
            participant = ConversationParticipant.objects.get(
                conversation_id=conversation_id,
                participant_id=user_id
            )
            current_time = timezone.now()
            participant.last_read_at = current_time
            participant.save(update_fields=['last_read_at'])

            # Créer des enregistrements MessageRead pour tous les messages non lus
            # que l'utilisateur n'a pas encore explicitement lus
            unread_messages = Message.objects.filter(
                conversation_id=conversation_id,
                created_at__lte=current_time,
                is_deleted=False
            ).exclude(
                sender_id=user_id  # Exclure ses propres messages
            ).exclude(
                read_by__participant_id=user_id  # Exclure les messages déjà marqués comme lus
            )

            # Créer les enregistrements MessageRead en batch
            message_reads = [
                MessageRead(message=message, participant_id=user_id, read_at=current_time)
                for message in unread_messages
            ]

            if message_reads:
                MessageRead.objects.bulk_create(message_reads, ignore_conflicts=True)
                logger.info(f"Marked {len(message_reads)} messages as read for user {user_id} in conversation {conversation_id}")

        except Exception as e:
            logger.error(f"Error marking conversation as read: {str(e)}")

    @database_sync_to_async
    def auto_mark_read_for_online_users(self, message, conversation_id):
        """Auto-marquer comme lu pour les utilisateurs en ligne dans la conversation"""
        try:
            # Obtenir tous les participants de la conversation (synchrone)
            participants = ConversationParticipant.objects.filter(
                conversation_id=conversation_id,
                is_active=True
            ).exclude(participant_id=message.sender.id)

            # Obtenir l'heure de création du message
            message_time = message.created_at

            # Préparer les enregistrements MessageRead à créer
            message_reads = []

            for participant_obj in participants:
                participant = participant_obj.participant

                # Vérifier si l'utilisateur est en ligne (synchrone)
                try:
                    presence = UserPresence.objects.filter(user=participant).first()
                    if presence and presence.status == 'online':
                        # Vérifier qu'il n'existe pas déjà un enregistrement MessageRead
                        if not MessageRead.objects.filter(message=message, participant=participant).exists():
                            message_reads.append(MessageRead(
                                message=message,
                                participant=participant,
                                read_at=message_time
                            ))
                except:
                    # En cas d'erreur de présence, ne pas marquer comme lu
                    continue

            # Créer les enregistrements MessageRead en batch
            if message_reads:
                MessageRead.objects.bulk_create(message_reads, ignore_conflicts=True)
                logger.info(f"Auto-marked {len(message_reads)} messages as read for online users in conversation {conversation_id}")

        except Exception as e:
            logger.error(f"Error in auto_mark_read_for_online_users: {str(e)}")
