"""Plex Source Serializer"""

from django.shortcuts import get_object_or_404
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
from rest_framework.fields import CharField
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ValidationError
from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger

from authentik.api.validation import validate
from authentik.core.api.sources import SourceSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer
from authentik.flows.challenge import RedirectChallenge
from authentik.flows.views.executor import to_stage_response
from authentik.rbac.decorators import permission_required
from authentik.sources.plex.models import PlexSource, UserPlexSourceConnection
from authentik.sources.plex.plex import PlexAuth, PlexSourceFlowManager

LOGGER = get_logger()


class PlexSourceSerializer(SourceSerializer):
    """Plex Source Serializer"""

    class Meta:
        model = PlexSource
        fields = SourceSerializer.Meta.fields + [
            "group_matching_mode",
            "client_id",
            "allowed_servers",
            "allow_friends",
            "plex_token",
        ]


class PlexTokenRedeemSerializer(PassiveSerializer):
    """Serializer to redeem a plex token"""

    plex_token = CharField()


class PlexSourceViewSet(UsedByMixin, ModelViewSet):
    """Plex source Viewset"""

    queryset = PlexSource.objects.all()
    serializer_class = PlexSourceSerializer
    lookup_field = "slug"
    filterset_fields = [
        "pbm_uuid",
        "name",
        "slug",
        "enabled",
        "authentication_flow",
        "enrollment_flow",
        "policy_engine_mode",
        "user_matching_mode",
        "group_matching_mode",
        "client_id",
        "allow_friends",
    ]
    search_fields = ["name", "slug"]
    ordering = ["name"]

    @permission_required(None)
    @extend_schema(
        request=PlexTokenRedeemSerializer(),
        responses={
            200: RedirectChallenge(),
            400: OpenApiResponse(description="Token not found"),
            403: OpenApiResponse(description="Access denied"),
        },
        parameters=[
            OpenApiParameter(
                name="slug",
                location=OpenApiParameter.QUERY,
                type=OpenApiTypes.STR,
            )
        ],
    )
    @action(
        methods=["POST"],
        detail=False,
        pagination_class=None,
        filter_backends=[],
        permission_classes=[AllowAny],
    )
    @validate(PlexTokenRedeemSerializer)
    def redeem_token(self, request: Request, body: PlexTokenRedeemSerializer) -> Response:
        """Redeem a plex token, check it's access to resources against what's allowed
        for the source, and redirect to an authentication/enrollment flow."""
        source: PlexSource = get_object_or_404(
            PlexSource, slug=request.query_params.get("slug", "")
        )
        plex_token = body.validated_data.get("plex_token", None)
        if not plex_token:
            raise ValidationError("No plex token given")
        auth_api = PlexAuth(source, plex_token)
        user_info, identifier = auth_api.get_user_info()
        # Check friendship first, then check server overlay
        friends_allowed = False
        if source.allow_friends:
            owner_api = PlexAuth(source, source.plex_token)
            friends_allowed = owner_api.check_friends_overlap(identifier)
        servers_allowed = auth_api.check_server_overlap()
        if any([friends_allowed, servers_allowed]):
            sfm = PlexSourceFlowManager(
                source=source,
                request=request,
                identifier=str(identifier),
                user_info={
                    "info": user_info,
                    "auth_api": auth_api,
                },
                policy_context={},
            )
            return to_stage_response(request, sfm.get_flow(plex_token=plex_token))
        LOGGER.warning(
            "Denying plex auth because no server overlay and no friends and not owner",
            user=user_info["username"],
        )
        raise PermissionDenied("Access denied.")

    @extend_schema(
        request=PlexTokenRedeemSerializer(),
        responses={
            204: OpenApiResponse(),
            400: OpenApiResponse(description="Token not found"),
            403: OpenApiResponse(description="Access denied"),
        },
        parameters=[
            OpenApiParameter(
                name="slug",
                location=OpenApiParameter.QUERY,
                type=OpenApiTypes.STR,
            )
        ],
    )
    @action(
        methods=["POST"],
        detail=False,
        pagination_class=None,
        filter_backends=[],
        permission_classes=[IsAuthenticated],
    )
    @validate(PlexTokenRedeemSerializer)
    def redeem_token_authenticated(
        self, request: Request, body: PlexTokenRedeemSerializer
    ) -> Response:
        """Redeem a plex token for an authenticated user, creating a connection"""
        source: PlexSource = get_object_or_404(
            PlexSource, slug=request.query_params.get("slug", "")
        )
        plex_token = body.validated_data.get("plex_token", None)
        if not plex_token:
            raise ValidationError("No plex token given")
        auth_api = PlexAuth(source, plex_token)
        user_info, identifier = auth_api.get_user_info()
        # Check friendship first, then check server overlay
        friends_allowed = False
        if source.allow_friends:
            owner_api = PlexAuth(source, source.plex_token)
            friends_allowed = owner_api.check_friends_overlap(identifier)
        servers_allowed = auth_api.check_server_overlap()
        if any([friends_allowed, servers_allowed]):
            UserPlexSourceConnection.objects.create(
                plex_token=plex_token,
                user=request.user,
                identifier=identifier,
                source=source,
            )
            return Response(status=204)
        LOGGER.warning(
            "Denying plex connection because no server overlay and no friends and not owner",
            user=user_info["username"],
            friends_allowed=friends_allowed,
            servers_allowed=servers_allowed,
        )
        raise PermissionDenied("Access denied.")
