"""Participants management service for LiveKit rooms."""

# pylint: disable=too-many-arguments,no-name-in-module,too-many-positional-arguments
# ruff: noqa:PLR0913

import json
import uuid
from logging import getLogger
from typing import Dict, Optional

from asgiref.sync import async_to_sync
from livekit.api import (
    MuteRoomTrackRequest,
    RoomParticipantIdentity,
    TwirpError,
    UpdateParticipantRequest,
)
from livekit.protocol.models import ParticipantInfo

from core import utils

from .lobby import LobbyService
from .presence import PresenceCache

logger = getLogger(__name__)


class ParticipantsManagementException(Exception):
    """Exception raised when a participant management operations fail."""


class ParticipantNotFoundException(ParticipantsManagementException):
    """Raised when the target participant does not exist in the room."""


class ParticipantsManagement:
    """Service for managing participants."""

    @async_to_sync
    async def mute(self, room_name: str, identity: str, track_sid: str):
        """Mute a specific audio or video track for a participant in a room."""

        lkapi = utils.create_livekit_client()

        try:
            await lkapi.room.mute_published_track(
                MuteRoomTrackRequest(
                    room=room_name,
                    identity=identity,
                    track_sid=track_sid,
                    muted=True,
                )
            )

        except TwirpError as e:
            if e.code == "not_found":
                logger.warning(
                    "Participant %s not found in room %s, skipping muting",
                    identity,
                    room_name,
                )
                raise ParticipantNotFoundException("Participant does not exist") from e

            logger.exception(
                "Unexpected error muting participant %s for room %s",
                identity,
                room_name,
            )
            raise ParticipantsManagementException("Could not mute participant") from e

        finally:
            await lkapi.aclose()

    @async_to_sync
    async def remove(self, room_name: str, identity: str):
        """Remove a participant from a room and clear their lobby/presence cache."""

        PresenceCache().clear(room_name, identity)

        try:
            LobbyService().clear_participant_cache(
                room_id=uuid.UUID(room_name), participant_id=identity
            )
        except (ValueError, TypeError) as exc:
            logger.warning(
                "participants_management.remove: room_name '%s' is not a UUID; "
                "skipping lobby cache clear",
                room_name,
                exc_info=exc,
            )

        lkapi = utils.create_livekit_client()

        try:
            await lkapi.room.remove_participant(
                RoomParticipantIdentity(room=room_name, identity=identity)
            )
        except TwirpError as e:
            if e.code == "not_found":
                logger.warning(
                    "Participant %s not found in room %s, skipping removing",
                    identity,
                    room_name,
                )
                raise ParticipantNotFoundException("Participant does not exist") from e

            logger.exception(
                "Unexpected error removing participant %s for room %s",
                identity,
                room_name,
            )
            raise ParticipantsManagementException("Could not remove participant") from e

        finally:
            await lkapi.aclose()

    @async_to_sync
    async def update(  # noqa: PLR0917
        self,
        room_name: str,
        identity: str,
        metadata: Optional[Dict] = None,
        attributes: Optional[Dict] = None,
        permission: Optional[Dict] = None,
        name: Optional[str] = None,
    ):
        """Update participant properties such as metadata, attributes, permissions, or name."""

        lkapi = utils.create_livekit_client()

        try:
            await lkapi.room.update_participant(
                UpdateParticipantRequest(
                    room=room_name,
                    identity=identity,
                    metadata=json.dumps(metadata),
                    permission=permission,
                    attributes=attributes,
                    name=name,
                )
            )

        except TwirpError as e:
            if e.code == "not_found":
                logger.warning(
                    "Participant %s not found in room %s, skipping update",
                    identity,
                    room_name,
                )
                raise ParticipantNotFoundException("Participant does not exist") from e

            logger.exception(
                "Unexpected error updating participant %s for room %s",
                identity,
                room_name,
            )
            raise ParticipantsManagementException("Could not update participant") from e

        finally:
            await lkapi.aclose()

    def check_if_in_meeting_cached(self, room_name: str, identity: str) -> bool:
        """Cache-first variant of `check_if_in_meeting`.

        Cache hit  -> True without touching LiveKit.
        Cache miss -> ask LiveKit; memoize only positive answers.

        Raises the same exceptions as `check_if_in_meeting` so callers keep
        failing closed the same way.
        """
        if not room_name or not identity:
            return False

        presence_cache = PresenceCache()

        if presence_cache.is_marked_present(room_name, identity):
            return True

        present = self.check_if_in_meeting(room_name=room_name, identity=identity)

        if present:
            presence_cache.mark_present(room_name, identity)

        return present

    @async_to_sync
    async def check_if_in_meeting(self, room_name: str, identity: str) -> bool:
        """Check whether `identity` is currently a participant in `room_name`.

        Raises ParticipantsManagementException for unexpected LiveKit errors
        so callers can fail closed rather than silently allowing the action.
        """

        if not room_name or not identity:
            return False

        lkapi = utils.create_livekit_client()

        try:
            participant = await lkapi.room.get_participant(
                RoomParticipantIdentity(
                    room=room_name,
                    identity=identity,
                )
            )
        except TwirpError as e:
            if e.code == "not_found":
                raise ParticipantNotFoundException("Participant does not exist") from e

            logger.exception(
                "Unexpected error checking participant %s in room %s",
                identity,
                room_name,
            )
            raise ParticipantsManagementException(
                "Could not verify participant presence"
            ) from e

        finally:
            await lkapi.aclose()

        return (
            participant is not None
            and participant.state != ParticipantInfo.State.DISCONNECTED
        )
