"""Room role management service.

Single entry point for changing a user's role on a room, used by:
- the in-meeting endpoint (promote/demote a connected participant)
- (more to come soon)

`ResourceAccess` is the source of truth. The LiveKit `room_role`
participant attribute is only a projection of it, synced best-effort.
"""

from logging import getLogger
from uuid import UUID

from core import models
from core.services.participants_management import (
    ParticipantNotFoundException,
    ParticipantsManagement,
    ParticipantsManagementException,
)

logger = getLogger(__name__)


class RoomRoleError(Exception):
    """Base exception for room role management errors."""

    status_code = 400


class SelfActionError(RoomRoleError):
    """Raised when a user tries to change their own role."""

    status_code = 403


class OwnerRoleError(RoomRoleError):
    """Raised when trying to demote an owner or grant ownership."""

    status_code = 403


class ParticipantNotInMeetingError(RoomRoleError):
    """Raised when the target participant is not connected to the meeting."""

    status_code = 404


class UserNotFoundError(RoomRoleError):
    """Raised when the target participant has no user account in database."""

    status_code = 404


ASSIGNABLE_ROLES = (models.RoleChoices.MEMBER, models.RoleChoices.ADMIN)


class RoomRoleService:
    """Manage promotion and demotion of room co-hosts."""

    def set_role(
        self, room: models.Room, user: models.User, role: str, actor: models.User
    ):
        """Persist `role` for `user` on `room`, idempotently and atomically.

        Returns the up-to-date `ResourceAccess`. Never grants or removes
        ownership: granting OWNER is refused, and an existing OWNER access
        is never modified.
        """

        if role not in ASSIGNABLE_ROLES:
            raise OwnerRoleError("Ownership cannot be granted through this action.")

        if actor is not None and user == actor:
            raise SelfActionError("You cannot change your own role.")

        access, created = models.ResourceAccess.objects.get_or_create(
            resource=room,
            user=user,
            defaults={"role": role},
        )

        if created:
            return access

        if access.role == models.RoleChoices.OWNER:
            raise OwnerRoleError("Room owners cannot be demoted.")

        if access.role != role:
            access.role = role
            access.save(update_fields=["role", "updated_at"])

        return access

    def set_participant_role(
        self,
        room: models.Room,
        participant_identity: UUID,
        role: str,
        actor: models.User,
    ):
        """Change the role of a participant currently connected to the meeting.

        - The participant must be connected (checked against LiveKit).
        - The participant must map to a user account.
        - The role is persisted in DB then mirrored to LiveKit.

        Returns a dict: {"role", "livekit_synced"}.
        """

        room_name = str(room.pk)
        participants_management = ParticipantsManagement()

        try:
            is_in_meeting = participants_management.check_if_in_meeting(
                room_name=room_name, identity=str(participant_identity)
            )
        except ParticipantNotFoundException as e:
            raise ParticipantNotInMeetingError(
                "Participant is not connected to this meeting."
            ) from e

        if not is_in_meeting:
            raise ParticipantNotInMeetingError(
                "Participant is not connected to this meeting."
            )

        user = models.User.objects.filter(sub=participant_identity).first()

        if user is None:
            raise UserNotFoundError(
                "This participant has no user account and cannot be assigned a role."
            )

        # Source of truth first: even if the LiveKit sync below fails,
        # the role is real and any fresh token will carry it.
        self.set_role(room=room, user=user, role=role, actor=actor)

        livekit_synced = self._sync_livekit_role(
            room_name=room_name,
            participant_identity=str(participant_identity),
            role=str(role),
        )

        return {
            "role": role,
            "livekit_synced": livekit_synced,
        }

    @staticmethod
    def _sync_livekit_role(room_name: str, participant_identity: str, role: str):
        """Mirror the role to the participant's LiveKit attributes.

        Best-effort: returns False on failure instead of raising, so callers
        can report a partial success. Re-running the action re-syncs.
        """
        try:
            ParticipantsManagement().update(
                room_name=room_name,
                identity=participant_identity,
                attributes={"room_role": role},
            )
        except ParticipantNotFoundException:
            # The participant left between the presence check and the update:
            # harmless, the DB state (if any) remains authoritative.
            logger.info(
                "Participant %s left room %s before role sync",
                participant_identity,
                room_name,
            )
            return False
        except ParticipantsManagementException:
            logger.exception(
                "Could not sync role to LiveKit for participant %s in room %s",
                participant_identity,
                room_name,
            )
            return False
        return True
