# Copyright 2021-2022 The Debusine Developers
# See the AUTHORS file at the top-level directory of this distribution
#
# This file is part of Debusine. It is subject to the license terms
# in the LICENSE file found in the top-level directory of this
# distribution. No part of Debusine, including this file, may be copied,
# modified, propagated, or distributed except according to the terms
# contained in the LICENSE file.

"""Views for the server application."""

import logging
from functools import lru_cache

from rest_framework import status, views
from rest_framework.permissions import BasePermission
from rest_framework.response import Response

from debusine.db.models import Token, WorkRequest, Worker
from debusine.server.scheduler import schedule
from debusine.server.serializers import (
    WorkRequestCompletedSerializer,
    WorkRequestSerializer,
    WorkerRegisterSerializer,
)
from debusine.tasks import Task

logger = logging.getLogger(__name__)


class ProblemResponse(Response):
    """
    Holds a title and other optional fields to return problems to the client.

    Follows RFC7807 (https://www.rfc-editor.org/rfc/rfc7807#section-6.1)
    """

    def __init__(
        self,
        title,
        detail=None,
        validation_errors=None,
        status_code=status.HTTP_400_BAD_REQUEST,
    ):
        """
        Initialize object.

        :param title: included in the response data.
        :param detail: if not None, included in the response data.
        :param validation_errors: if not None, included in the response data.
        :param status_code: HTTP status code for the response.
        """
        data = {"title": title}

        if detail is not None:
            data["detail"] = detail

        if validation_errors is not None:
            data["validation_errors"] = validation_errors

        super().__init__(
            data, status_code, content_type="application/problem+json"
        )


class IsTokenAuthenticated(BasePermission):
    """Allows access only to requests with a valid Token."""

    @lru_cache(1)
    def token(self, request):
        """Return the token or None (if no token header or not found in DB)."""
        token_key = request.headers.get('token')

        if token_key is None:
            return None

        token = Token.objects.get_token_or_none(token_key=token_key)

        return token

    def has_permission(self, request, view):  # noqa: U100
        """Return True if the request is authenticated with a Token."""
        token = self.token(request)

        return token is not None and token.enabled


class IsWorkerAuthenticated(IsTokenAuthenticated):
    """Allows access only to requests with a valid Token and with a Worker."""

    def has_permission(self, request, view):
        """
        Return True if the request is an authenticated worker.

        The Token must exist in the database and have a Worker.
        """
        if super().has_permission(request, view) is False:
            # No token authenticated: no Worker Authenticated
            return False

        if not hasattr(self.token(request), 'worker'):
            # The Token doesn't have a worker associated
            return False

        return True


class RegisterView(views.APIView):
    """View used by workers to register to debusine."""

    def post(self, request):
        """Worker registers (sends token and fqdn)."""
        worker_register = WorkerRegisterSerializer(data=request.data)

        if not worker_register.is_valid():
            return ProblemResponse(
                "Cannot deserialize worker",
                validation_errors=worker_register.errors,
            )

        token_key = worker_register.validated_data['token']
        fqdn = worker_register.validated_data['fqdn']

        token, _ = Token.objects.get_or_create(key=token_key)

        Worker.objects.create_with_fqdn(fqdn, token)

        logger.info('Client registered. Token key: %s', token_key)

        return Response(status=status.HTTP_201_CREATED)


class GetNextWorkRequestView(views.APIView):
    """View used by workers to request a task."""

    permission_classes = [IsWorkerAuthenticated]

    def get(self, request):
        """Return the task to build."""
        token_key = request.headers['token']
        worker = Worker.objects.get_worker_by_token_key_or_none(token_key)

        work_request = WorkRequest.objects.running(worker=worker).first()

        if work_request is None:
            work_request = WorkRequest.objects.pending(worker=worker).first()

        if work_request:
            content = WorkRequestSerializer(work_request).data
            work_request.mark_running()
            status_code = status.HTTP_200_OK
        else:
            # There is no work request available for the worker
            content = None
            status_code = status.HTTP_204_NO_CONTENT

        return Response(content, status=status_code)


class WorkRequestView(views.APIView):
    """View used by the debusine client to get information of a WorkRequest."""

    permission_classes = [IsTokenAuthenticated]

    def get(self, request, work_request_id: int):  # noqa: U100
        """Return status information for WorkRequest or not found."""
        try:
            work_request = WorkRequest.objects.get(pk=work_request_id)
        except WorkRequest.DoesNotExist:
            return Response(
                {'detail': 'Work request id not found'},
                status=status.HTTP_404_NOT_FOUND,
            )

        return Response(
            WorkRequestSerializer(work_request).data, status=status.HTTP_200_OK
        )

    def post(self, request):
        """Create a new work request."""
        token_key = request.headers["token"]

        work_request_deserialized = WorkRequestSerializer(
            data=request.data, only_fields=['task_name', 'task_data']
        )

        if not work_request_deserialized.is_valid():
            errors = work_request_deserialized.errors

            logger.debug(
                "Error creating work request (token key: %s). "
                "Could not be deserialized: %s",
                token_key,
                errors,
            )

            return ProblemResponse(
                "Cannot deserialize work request", validation_errors=errors
            )

        task_name = request.data['task_name']

        if not Task.is_valid_task_name(task_name):
            valid_task_names = ', '.join(Task.task_names())
            logger.debug(
                "Error creating work request (token key: %s). "
                "Non-registered task name: %s",
                token_key,
                task_name,
            )
            return ProblemResponse(
                "Cannot create work request: not registered task name",
                detail=f'Task name: "{task_name}". '
                f"Registered task names: {valid_task_names}",
            )

        task_data = request.data['task_data']
        w = WorkRequest.objects.create(task_name=task_name, task_data=task_data)

        schedule()

        w.refresh_from_db()

        return Response(
            WorkRequestSerializer(w).data, status=status.HTTP_200_OK
        )


class UpdateWorkRequestAsCompletedView(views.APIView):
    """View used by the workers to mark a task as completed."""

    permission_classes = [IsWorkerAuthenticated]

    def put(self, request, work_request_id: int):
        """Mark a work request as completed."""
        token_key = request.headers['token']
        worker = Worker.objects.get_worker_by_token_key_or_none(token_key)

        try:
            work_request = WorkRequest.objects.get(pk=work_request_id)
        except WorkRequest.DoesNotExist:
            return ProblemResponse(
                "Work request not found",
                status_code=status.HTTP_404_NOT_FOUND,
            )

        if work_request.worker == worker:
            work_request_completed_serializer = WorkRequestCompletedSerializer(
                data=request.data
            )

            if work_request_completed_serializer.is_valid():
                work_request.mark_completed(
                    work_request_completed_serializer.validated_data['result']
                )
                content = None
                status_code = status.HTTP_200_OK
            else:
                return ProblemResponse(
                    "Cannot change work request as completed",
                    validation_errors=work_request_completed_serializer.errors,
                )
        else:
            return ProblemResponse(
                "Invalid worker to update the work request",
                status_code=status.HTTP_401_UNAUTHORIZED,
            )

        return Response(content, status=status_code)


class UpdateWorkerDynamicMetadataView(views.APIView):
    """View used by the workers to post dynamic metadata."""

    permission_classes = [IsWorkerAuthenticated]

    def put(self, request):
        """Update Worker dynamic metadata."""
        token_key = request.headers['token']
        worker = Worker.objects.get_worker_by_token_key_or_none(token_key)

        worker.set_dynamic_metadata(request.data)

        return Response(status=status.HTTP_204_NO_CONTENT)
