# 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.

"""Mapping database models to REST Api support."""

from collections.abc import Callable
from typing import Any, Never

from django.urls import reverse

try:
    import pydantic.v1 as pydantic
except ImportError:
    import pydantic as pydantic  # type: ignore

from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework.request import Request

from debusine.artifacts.models import ArtifactCategory
from debusine.client.models import LookupChildType
from debusine.db.models import (
    Artifact,
    ArtifactRelation,
    Collection,
    CollectionItem,
    File,
    TaskDatabase,
    WorkRequest,
    WorkflowTemplate,
    Workspace,
    default_workspace,
)
from debusine.server.collections.lookup import LookupResult
from debusine.server.workflows import Workflow
from debusine.server.workflows.models import WorkRequestManualUnblockAction
from debusine.tasks.models import EventReactions, WorkerType


class WorkRequestSerializer(serializers.ModelSerializer[WorkRequest]):
    """Serializer of a WorkRequest."""

    artifacts = serializers.SerializerMethodField("artifacts_for_work_request")
    workspace = serializers.SlugRelatedField(
        queryset=Workspace.objects.all(), slug_field="name"
    )
    workflow_data = serializers.JSONField(
        source="workflow_data_json", required=False
    )
    event_reactions = serializers.JSONField(
        source='event_reactions_json', required=False
    )

    def __init__(self, *args, **kwargs):
        """
        Initialize object.

        :param kwargs['only_fields']: if not None: any field in .data that does
          not will make validate() raise a ValidationError.
        """
        self._only_fields = kwargs.pop('only_fields', None)
        super().__init__(*args, **kwargs)

    def artifacts_for_work_request(
        self, work_request: WorkRequest
    ) -> list[int]:
        """Return list of artifact ids created by this work request."""
        return list(
            work_request.artifact_set.all()
            .order_by("id")
            .values_list("id", flat=True)
        )

    def validate(self, data):
        """
        Add extra validation in the WorkRequestSerializer. Used by .is_valid().

        -if only_fields was passed in the __init__: raise ValidationError
          if the initial data had an unexpected field.
        """
        if self._only_fields is not None:
            fields_in_data = set(self.initial_data.keys())
            wanted_fields = set(self._only_fields)

            if unwanted_fields := (fields_in_data - wanted_fields):
                unwanted_fields = ', '.join(sorted(unwanted_fields))
                raise serializers.ValidationError(
                    f'Invalid fields: {unwanted_fields}'
                )

        return data

    def validate_event_reactions(self, value: Any) -> Any:
        """Validate event reactions."""
        try:
            EventReactions.parse_obj(value)
        except pydantic.ValidationError as e:
            raise serializers.ValidationError(f"Invalid event_reactions: {e}")
        return value

    class Meta:
        model = WorkRequest
        fields = [
            'id',
            'task_name',
            'created_at',
            'created_by',
            'started_at',
            'completed_at',
            'duration',
            'worker',
            'task_type',
            'task_data',
            'dynamic_task_data',
            'priority_base',
            'priority_adjustment',
            'workflow_data',
            'event_reactions',
            'status',
            'result',
            'artifacts',
            'workspace',
        ]


class WorkRequestUpdateSerializer(WorkRequestSerializer):
    """Serializer for updating work requests."""

    class Meta:
        model = WorkRequest
        fields = ['priority_adjustment']

    def __init__(self, *args, **kwargs):
        """Ensure that only expected fields are passed."""
        kwargs["only_fields"] = self.Meta.fields
        super().__init__(*args, **kwargs)


class WorkRequestUnblockSerializer(serializers.Serializer[Never]):
    """Serializer for the data used to edit a manual unblock."""

    notes = serializers.CharField(max_length=65536, required=False)
    action = serializers.ChoiceField(
        choices=WorkRequestManualUnblockAction, required=False
    )

    def validate(self, data):
        """Ensure at least one of notes and action is present."""
        if not data.get("notes") and data.get("action") is None:
            raise serializers.ValidationError(
                "At least one of notes and action must be set"
            )

        return data


class WorkRequestExternalDebsignSerializer(WorkRequestSerializer):
    """
    Serializer for an `ExternalDebsign` work request.

    This is similar to normal work request serialization, except that we
    make sure to return up-to-date dynamic task data so that it can be used
    to get the unsigned artifact ID.
    """

    def to_representation(self, instance: WorkRequest) -> dict[str, Any]:
        """Augment the representation with current dynamic task data."""
        data = super().to_representation(instance)

        try:
            task = instance.get_task()
            dynamic_task_data = task.compute_dynamic_data(
                TaskDatabase(instance)
            )
            data["dynamic_task_data"] = (
                None if dynamic_task_data is None else dynamic_task_data.dict()
            )
        except Exception as exc:
            raise ValidationError(detail=f"Cannot process task data: {exc}")

        return data


class WorkRequestExternalDebsignRequestSerializer(
    serializers.Serializer[Never]
):
    """Serializer for the data used to complete an `ExternalDebsign` task."""

    signed_artifact = serializers.PrimaryKeyRelatedField(
        queryset=Artifact.objects
    )

    def validate_signed_artifact(self, value: Any) -> Any:
        """Check that the signed artifact has the correct category."""
        if value.category != ArtifactCategory.UPLOAD:
            raise ValidationError(
                detail=(
                    f"Expected signed artifact of category "
                    f"{ArtifactCategory.UPLOAD}; got {value.category}"
                )
            )
        return value


class WorkerRegisterSerializer(serializers.Serializer[Never]):
    """Serializer for the data when a worker is registering."""

    token = serializers.CharField(max_length=64)
    fqdn = serializers.CharField(max_length=400)
    worker_type = serializers.ChoiceField(
        choices=WorkerType, default=WorkerType.EXTERNAL
    )


class WorkRequestCompletedSerializer(serializers.ModelSerializer[WorkRequest]):
    """Serializer for the data when a work request is completed."""

    result = serializers.ChoiceField(
        choices=[
            choice
            for choice in WorkRequest.Results
            if choice != WorkRequest.Results.NONE
        ]
    )

    class Meta:
        model = WorkRequest
        fields = ['result']


class OnWorkRequestCompleted(serializers.Serializer[Never]):
    """Serializer used by the OnWorkRequestCompletedConsumer."""

    type = serializers.CharField()
    text = serializers.CharField()
    work_request_id = serializers.IntegerField()
    completed_at = serializers.DateTimeField()
    result = serializers.ChoiceField(choices=WorkRequest.Results)


class FileSerializer(serializers.Serializer[File]):
    """Serializer for the File class."""

    size = serializers.IntegerField(min_value=0)
    checksums = serializers.DictField(
        child=serializers.CharField(max_length=255)
    )
    type = serializers.ChoiceField(choices=("file",))


class FileSerializerResponse(FileSerializer):
    """Serializer for the File responses. Include URL."""

    url = serializers.URLField()


class ArtifactSerializer(serializers.Serializer[Artifact]):
    """Serializer to deserialize the client request to create an artifact."""

    category = serializers.CharField(max_length=255)
    workspace = serializers.CharField(max_length=255)
    files = serializers.DictField(child=FileSerializer())
    # Deliberately incompatible with Serializer.data.
    data = serializers.DictField()  # type: ignore[assignment]
    work_request = serializers.IntegerField(allow_null=True, required=False)
    expiration_delay = serializers.DurationField(
        allow_null=True, required=False
    )


class ArtifactSerializerResponse(serializers.ModelSerializer[Artifact]):
    """Serializer for returning the information of an Artifact."""

    # Artifact's workspace
    workspace = serializers.SerializerMethodField("_workspace_name")

    # Files with their hash and size
    files = serializers.SerializerMethodField("_files")

    # List of file paths pending to be uploaded
    files_to_upload = serializers.SerializerMethodField("_files_to_upload")

    # URL to download the artifact
    download_tar_gz_url = serializers.SerializerMethodField(
        "_download_tar_gz_url"
    )

    _base_download_url: str

    class Meta:
        model = Artifact
        fields = [
            "id",
            "workspace",
            "category",
            "data",
            "created_at",
            "expire_at",
            "download_tar_gz_url",
            "files_to_upload",
            "files",
        ]

    def _workspace_name(self, artifact: Artifact) -> str:
        """
        Return the workspace name.

        By default the ModelSerializer return workspace id..
        """
        return artifact.workspace.name

    def _files_to_upload(self, artifact: Artifact) -> list[str]:
        """Return file paths from artifact that have not been uploaded yet."""
        files_to_upload = []

        for file_in_artifact in artifact.fileinartifact_set.all().order_by(
            "path"
        ):
            if not artifact.workspace.is_file_in_workspace(
                file_in_artifact.file
            ):
                files_to_upload.append(file_in_artifact.path)

        return files_to_upload

    def _files(self, artifact: Artifact) -> dict[str, dict[str, Any]]:
        """
        Return files in the artifact with their hash and size.

        The files might not be available yet if they have not been uploaded.
        """
        files: dict[str, dict[str, Any]] = {}

        for file_in_artifact in artifact.fileinartifact_set.all().order_by(
            "path"
        ):
            file = file_in_artifact.file
            data = {
                "size": file.size,
                "type": "file",
                "checksums": {
                    file.current_hash_algorithm: file.hash_digest.hex()
                },
                "url": self._base_download_url + file_in_artifact.path,
            }
            file_serialized = FileSerializerResponse(data=data)
            file_serialized.is_valid(raise_exception=True)

            files[file_in_artifact.path] = file_serialized.data

        return files

    def _download_tar_gz_url(self, obj) -> str:  # noqa: U100
        """Based on self._base_download_url return URL to download .tar.gz."""
        return self._base_download_url + "?archive=tar.gz"

    @staticmethod
    def _build_absolute_download_url(artifact_id: int, request: Request) -> str:
        """Return URL to download the artifact (without the archive format)."""
        return request.build_absolute_uri(
            reverse(
                "artifacts:download",
                kwargs={"artifact_id": artifact_id},
            )
        )

    @classmethod
    def from_artifact(
        cls, artifact: Artifact, request: Request
    ) -> "ArtifactSerializerResponse":
        """Given an artifact and request return ArtifactSerializerResponse."""
        serialized = ArtifactSerializerResponse(artifact)
        serialized._base_download_url = cls._build_absolute_download_url(
            artifact.id, request
        )
        return serialized


class ArtifactRelationResponseSerializer(
    serializers.ModelSerializer[ArtifactRelation]
):
    """Serializer for relations between artifacts."""

    media_type = "application/json"

    class Meta:
        model = ArtifactRelation
        fields = ["id", "artifact", "target", "type"]
        # Don't let REST framework validate uniqueness constraints (which it
        # does automatically as of 3.15.0).  We want GetOrCreateAPIView to
        # return 200 if the relation already exists, which it will do if
        # REST framework hasn't returned 400 first due to a validation
        # failure.
        validators: list[Callable[[ArtifactRelation], None]] = []


class LookupExpectTypeChoiceField(serializers.ChoiceField):
    """
    A field for `Lookup*.expect_type`.

    This handles values of `LookupChildType`, but also accepts values of
    `CollectionItem.Types` for compatibility with old clients.
    """

    def __init__(self, **kwargs: Any) -> None:
        """Initialize the field."""
        return super().__init__(choices=LookupChildType)

    def to_internal_value(self, data: Any) -> str:
        """Accept values of `CollectionItemTypes` for compatibility."""
        if data in CollectionItem.Types:
            data = {
                CollectionItem.Types.BARE: LookupChildType.BARE,
                CollectionItem.Types.ARTIFACT: LookupChildType.ARTIFACT,
                CollectionItem.Types.COLLECTION: LookupChildType.COLLECTION,
            }[data]
        return super().to_internal_value(data)


class LookupSingleSerializer(serializers.Serializer[Never]):
    """Serializer for the data used to look up a single collection item."""

    lookup = serializers.JSONField()
    work_request = serializers.PrimaryKeyRelatedField(
        queryset=WorkRequest.objects
    )
    default_category = serializers.CharField(
        max_length=255, required=False, allow_null=True
    )
    expect_type = LookupExpectTypeChoiceField()


class LookupMultipleSerializer(serializers.Serializer[Never]):
    """Serializer for the data used to look up multiple collection items."""

    lookup = serializers.JSONField()
    work_request = serializers.PrimaryKeyRelatedField(
        queryset=WorkRequest.objects
    )
    default_category = serializers.CharField(
        max_length=255, required=False, allow_null=True
    )
    expect_type = LookupExpectTypeChoiceField()


class LookupResponseSerializer(serializers.Serializer[LookupResult]):
    """Serializer for the response to looking up collection items."""

    media_type = "application/json"

    result_type = serializers.ChoiceField(choices=CollectionItem.Types)
    collection_item: "serializers.PrimaryKeyRelatedField[CollectionItem]" = (
        serializers.PrimaryKeyRelatedField(read_only=True, allow_null=True)
    )
    artifact: "serializers.PrimaryKeyRelatedField[Artifact]" = (
        serializers.PrimaryKeyRelatedField(read_only=True, allow_null=True)
    )
    collection: "serializers.PrimaryKeyRelatedField[Collection]" = (
        serializers.PrimaryKeyRelatedField(read_only=True, allow_null=True)
    )


class WorkflowTemplateSerializer(serializers.ModelSerializer[WorkflowTemplate]):
    """Serializer for workflow templates."""

    media_type = "application/json"

    workspace = serializers.SlugRelatedField(
        queryset=Workspace.objects.all(),
        slug_field="name",
        default=default_workspace,
    )

    class Meta:
        model = WorkflowTemplate
        fields = [
            "id",
            "name",
            "workspace",
            "task_name",
            "task_data",
            "priority",
        ]

    def validate_task_name(self, value):
        """
        Ensure that task_name is valid.

        We don't currently validate task_data, since a template's task data
        is allowed to be incomplete.
        """
        try:
            Workflow.from_name(value)
        except ValueError as exc:
            raise serializers.ValidationError({"task_name": str(exc)})

        return value


class CreateWorkflowRequestSerializer(serializers.ModelSerializer[WorkRequest]):
    """Serializer for requests to create workflows."""

    media_type = "application/json"

    template_name = serializers.CharField(max_length=255)
    workspace = serializers.SlugRelatedField(
        queryset=Workspace.objects.all(),
        slug_field="name",
        default=default_workspace,
    )

    class Meta:
        model = WorkRequest
        fields = [
            "template_name",
            "workspace",
            "task_data",
        ]
