# Copyright (C) 2024 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later

from channels.generic.websocket import AsyncJsonWebsocketConsumer
from django.contrib.auth.models import User
from django.db.models import Count, F, Q
from registration.models import Registration

from .models import Channel, Message


class ChatConsumer(AsyncJsonWebsocketConsumer):
    """
    Ce consommateur gère les connexions WebSocket pour le chat.
    """
    async def connect(self) -> None:
        """
        Cette fonction est appelée lorsqu'un nouveau websocket tente de se connecter au serveur.
        On n'accept que si c'est un⋅e utilisateur⋅rice connecté⋅e.
        """
        if '_fake_user_id' in self.scope['session']:
            # Dans le cas d'une impersonification, on charge l'utilisateur⋅rice concerné
            self.scope['user'] = await User.objects.aget(pk=self.scope['session']['_fake_user_id'])

        # Récupération de l'utilisateur⋅rice courant⋅e
        user = self.scope['user']
        if user.is_anonymous:
            # L'utilisateur⋅rice n'est pas connecté⋅e
            await self.close()
            return

        reg = await Registration.objects.aget(user_id=user.id)
        self.registration = reg

        # Acceptation de la connexion
        await self.accept()

        # Récupération des canaux accessibles en lecture et/ou en écriture
        self.read_channels = await Channel.get_accessible_channels(user, 'read')
        self.write_channels = await Channel.get_accessible_channels(user, 'write')

        # Abonnement aux canaux de diffusion Websocket pour les différents canaux de chat
        async for channel in self.read_channels.all():
            await self.channel_layer.group_add(f"chat-{channel.id}", self.channel_name)
        # Abonnement à un canal de diffusion Websocket personnel, utile pour s'adresser à une unique personne
        await self.channel_layer.group_add(f"user-{user.id}", self.channel_name)

    async def disconnect(self, close_code: int) -> None:
        """
        Cette fonction est appelée lorsqu'un websocket se déconnecte du serveur.
        :param close_code: Le code d'erreur.
        """
        if self.scope['user'].is_anonymous:
            # L'utilisateur⋅rice n'était pas connecté⋅e, on ne fait rien
            return

        async for channel in self.read_channels.all():
            # Désabonnement des canaux de diffusion Websocket liés aux canaux de chat
            await self.channel_layer.group_discard(f"chat-{channel.id}", self.channel_name)
        # Désabonnement du canal de diffusion Websocket personnel
        await self.channel_layer.group_discard(f"user-{self.scope['user'].id}", self.channel_name)

    async def receive_json(self, content: dict, **kwargs) -> None:
        """
        Appelée lorsque le client nous envoie des données, décodées depuis du JSON.
        :param content: Les données envoyées par le client, décodées depuis du JSON. Doit contenir un champ 'type'.
        """
        match content['type']:
            case 'fetch_channels':
                # Demande de récupération des canaux disponibles
                await self.fetch_channels()
            case 'send_message':
                # Envoi d'un message dans un canal
                await self.receive_message(**content)
            case 'edit_message':
                # Modification d'un message
                await self.edit_message(**content)
            case 'delete_message':
                # Suppression d'un message
                await self.delete_message(**content)
            case 'fetch_messages':
                # Récupération des messages d'un canal (ou d'une partie)
                await self.fetch_messages(**content)
            case 'mark_read':
                # Marquage de messages comme lus
                await self.mark_read(**content)
            case 'start_private_chat':
                # Démarrage d'une conversation privée avec un⋅e autre utilisateur⋅rice
                await self.start_private_chat(**content)
            case unknown:
                # Type inconnu, on soulève une erreur
                raise ValueError(f"Unknown message type: {unknown}")

    async def fetch_channels(self) -> None:
        """
        L'utilisateur⋅rice demande à récupérer la liste des canaux disponibles.
        On lui renvoie alors la liste des canaux qui lui sont accessibles en lecture,
        en fournissant nom, catégorie, permission de lecture et nombre de messages non lus.
        """
        user = self.scope['user']

        # Récupération des canaux accessibles en lecture, avec le nombre de messages non lus
        channels = self.read_channels.prefetch_related('invited') \
            .annotate(total_messages=Count('messages', distinct=True)) \
            .annotate(read_messages=Count('messages', filter=Q(messages__users_read=user), distinct=True)) \
            .annotate(unread_messages=F('total_messages') - F('read_messages')).all()

        # Envoi de la liste des canaux
        message = {
            'type': 'fetch_channels',
            'channels': [
                {
                    'id': channel.id,
                    'name': channel.get_visible_name(user),
                    'category': channel.category,
                    'read_access': True,
                    'write_access': await self.write_channels.acontains(channel),
                    'unread_messages': channel.unread_messages,
                }
                async for channel in channels
            ]
        }
        await self.send_json(message)

    async def receive_message(self, channel_id: int, content: str, **kwargs) -> None:
        """
        L'utilisateur⋅ice a envoyé un message dans un canal.
        On vérifie d'abord la permission d'écriture, puis on crée le message et on l'envoie à tou⋅tes les
        utilisateur⋅ices abonné⋅es au canal.

        :param channel_id: Identifiant du canal où envoyer le message.
        :param content: Contenu du message.
        """
        user = self.scope['user']

        # Récupération du canal
        channel = await Channel.objects.prefetch_related('tournament__pools__juries', 'pool', 'team', 'invited') \
            .aget(id=channel_id)
        if not await self.write_channels.acontains(channel):
            # L'utilisateur⋅ice n'a pas la permission d'écrire dans ce canal, on abandonne
            return

        # Création du message
        message = await Message.objects.acreate(
            author=user,
            channel=channel,
            content=content,
        )

        # Envoi du message à toutes les personnes connectées sur le canal
        await self.channel_layer.group_send(f'chat-{channel.id}', {
            'type': 'chat.send_message',
            'id': message.id,
            'channel_id': channel.id,
            'timestamp': message.created_at.isoformat(),
            'author_id': message.author_id,
            'author': await message.aget_author_name(),
            'content': message.content,
        })

    async def edit_message(self, message_id: int, content: str, **kwargs) -> None:
        """
        L'utilisateur⋅ice a modifié un message.
        On vérifie d'abord que l'utilisateur⋅ice a le droit de modifier le message, puis on modifie le message
        et on envoie la modification à tou⋅tes les utilisateur⋅ices abonné⋅es au canal.

        :param message_id: Identifiant du message à modifier.
        :param content: Nouveau contenu du message.
        """
        user = self.scope['user']

        # Récupération du message
        message = await Message.objects.aget(id=message_id)
        if user.id != message.author_id and not user.is_superuser:
            # Seul⋅e l'auteur⋅ice du message ou un⋅e admin peut modifier un message
            return

        # Modification du contenu du message
        message.content = content
        await message.asave()

        # Envoi de la modification à tou⋅tes les personnes connectées sur le canal
        await self.channel_layer.group_send(f'chat-{message.channel_id}', {
            'type': 'chat.edit_message',
            'id': message_id,
            'channel_id': message.channel_id,
            'content': content,
        })

    async def delete_message(self, message_id: int, **kwargs) -> None:
        """
        L'utilisateur⋅ice a supprimé un message.
        On vérifie d'abord que l'utilisateur⋅ice a le droit de supprimer le message, puis on supprime le message
        et on envoie la suppression à tou⋅tes les utilisateur⋅ices abonné⋅es au canal.

        :param message_id: Identifiant du message à supprimer.
        """
        user = self.scope['user']

        # Récupération du message
        message = await Message.objects.aget(id=message_id)
        if user.id != message.author_id and not user.is_superuser:
            return

        # Suppression effective du message
        await message.adelete()

        # Envoi de la suppression à tou⋅tes les personnes connectées sur le canal
        await self.channel_layer.group_send(f'chat-{message.channel_id}', {
            'type': 'chat.delete_message',
            'id': message_id,
            'channel_id': message.channel_id,
        })

    async def fetch_messages(self, channel_id: int, offset: int = 0, limit: int = 50, **_kwargs) -> None:
        """
        L'utilisateur⋅ice demande à récupérer les messages d'un canal.
        On vérifie la permission de lecture, puis on renvoie les messages demandés.

        :param channel_id: Identifiant du canal où récupérer les messages.
        :param offset: Décalage pour la pagination, à partir du dernier message.
                       Par défaut : 0, on commence au dernier message.
        :param limit: Nombre de messages à récupérer. Par défaut, on récupère 50 messages.
        """
        # Récupération du canal
        channel = await Channel.objects.aget(id=channel_id)
        if not await self.read_channels.acontains(channel):
            # L'utilisateur⋅rice n'a pas la permission de lire ce canal, on abandonne
            return

        limit = min(limit, 200)  # On limite le nombre de messages à 200 maximum

        # Récupération des messages, avec un indicateur de lecture pour l'utilisateur⋅ice courant⋅e
        messages = Message.objects \
            .filter(channel=channel) \
            .annotate(read=Count('users_read', filter=Q(users_read=self.scope['user']))) \
            .order_by('-created_at')[offset:offset + limit].all()

        # Envoi de la liste des messages, en les renvoyant dans l'ordre chronologique
        await self.send_json({
            'type': 'fetch_messages',
            'channel_id': channel_id,
            'messages': list(reversed([
                {
                    'id': message.id,
                    'timestamp': message.created_at.isoformat(),
                    'author_id': message.author_id,
                    'author': await message.aget_author_name(),
                    'content': message.content,
                    'read': message.read > 0,
                }
                async for message in messages
            ]))
        })

    async def mark_read(self, message_ids: list[int], **_kwargs) -> None:
        """
        L'utilisateur⋅ice marque des messages comme lus, après les avoir affichés à l'écran.

        :param message_ids: Liste des identifiants des messages qu'il faut marquer comme lus.
        """
        # Récupération des messages à marquer comme lus
        messages = Message.objects.filter(id__in=message_ids)
        async for message in messages.all():
            # Ajout de l'utilisateur⋅ice courant⋅e à la liste des personnes ayant lu le message
            await message.users_read.aadd(self.scope['user'])

        # Actualisation du nombre de messages non lus par canal
        unread_messages_by_channel = Message.objects.exclude(users_read=self.scope['user']).values('channel_id') \
            .annotate(unread_messages=Count('channel_id'))

        # Envoi des identifiants des messages non lus et du nombre de messages non lus par canal, actualisés
        await self.send_json({
            'type': 'mark_read',
            'messages': [{'id': message.id, 'channel_id': message.channel_id} async for message in messages.all()],
            'unread_messages': {group['channel_id']: group['unread_messages']
                                async for group in unread_messages_by_channel.all()},
        })

    async def start_private_chat(self, user_id: int, **kwargs) -> None:
        """
        L'utilisateur⋅ice souhaite démarrer une conversation privée avec un⋅e autre utilisateur⋅ice.
        Pour cela, on récupère le salon privé s'il existe, sinon on en crée un.
        Dans le cas d'une création, les deux personnes sont transférées immédiatement dans ce nouveau canal.

        :param user_id: L'utilisateur⋅rice avec qui démarrer la conversation privée.
        """
        user = self.scope['user']
        # Récupération de l'autre utilisateur⋅ice avec qui démarrer la conversation
        other_user = await User.objects.aget(id=user_id)

        # Vérification de l'existence d'un salon privé entre les deux personnes
        channel_qs = Channel.objects.filter(private=True).filter(invited=user).filter(invited=other_user)
        if not await channel_qs.aexists():
            # Le salon privé n'existe pas, on le crée alors
            channel = await Channel.objects.acreate(
                name=f"{user.first_name} {user.last_name}, {other_user.first_name} {other_user.last_name}",
                category=Channel.ChannelCategory.PRIVATE,
                private=True,
            )
            await channel.invited.aset([user, other_user])

            # On s'ajoute au salon privé
            await self.channel_layer.group_add(f"chat-{channel.id}", self.channel_name)

            if user != other_user:
                # On transfère l'autre utilisateur⋅ice dans le salon privé
                await self.channel_layer.group_send(f"user-{other_user.id}", {
                    'type': 'chat.start_private_chat',
                    'channel': {
                        'id': channel.id,
                        'name': f"{user.first_name} {user.last_name}",
                        'category': channel.category,
                        'read_access': True,
                        'write_access': True,
                    }
                })
        else:
            # Récupération dudit salon privé
            channel = await channel_qs.afirst()

        # Invitation de l'autre utilisateur⋅rice à rejoindre le salon privé
        await self.channel_layer.group_send(f"user-{user.id}", {
            'type': 'chat.start_private_chat',
            'channel': {
                'id': channel.id,
                'name': f"{other_user.first_name} {other_user.last_name}",
                'category': channel.category,
                'read_access': True,
                'write_access': True,
            }
        })

    async def chat_send_message(self, message) -> None:
        """
        Envoi d'un message à tou⋅tes les personnes connectées sur un canal.
        :param message: Dictionnaire contenant les informations du message à envoyer,
                contenant l'identifiant du message "id", l'identifiant du canal "channel_id",
                l'heure de création "timestamp", l'identifiant de l'auteur "author_id",
                le nom de l'auteur "author" et le contenu du message "content".
        """
        await self.send_json({'type': 'send_message', 'id': message['id'], 'channel_id': message['channel_id'],
                              'timestamp': message['timestamp'], 'author': message['author'],
                              'content': message['content']})

    async def chat_edit_message(self, message) -> None:
        """
        Envoi d'une modification de message à tou⋅tes les personnes connectées sur un canal.
        :param message: Dictionnaire contenant les informations du message à modifier,
                contenant l'identifiant du message "id", l'identifiant du canal "channel_id"
                et le nouveau contenu "content".
        """
        await self.send_json({'type': 'edit_message', 'id': message['id'], 'channel_id': message['channel_id'],
                              'content': message['content']})

    async def chat_delete_message(self, message) -> None:
        """
        Envoi d'une suppression de message à tou⋅tes les personnes connectées sur un canal.
        :param message: Dictionnaire contenant les informations du message à supprimer,
                contenant l'identifiant du message "id" et l'identifiant du canal "channel_id".
        """
        await self.send_json({'type': 'delete_message', 'id': message['id'], 'channel_id': message['channel_id']})

    async def chat_start_private_chat(self, message) -> None:
        """
        Envoi d'un message pour démarrer une conversation privée à une personne connectée.
        :param message: Dictionnaire contenant les informations du nouveau canal privé.
        """
        await self.channel_layer.group_add(f"chat-{message['channel']['id']}", self.channel_name)
        await self.send_json({'type': 'start_private_chat', 'channel': message['channel']})
