# 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 typing import Optional

from django.urls import reverse

from rest_framework import serializers
from rest_framework.request import Request

from debusine.db.models import (
    Artifact,
    ArtifactRelation,
    File,
    WorkRequest,
    Workspace,
)


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"
    )

    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

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


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

    token = serializers.CharField(max_length=64)
    fqdn = serializers.CharField(max_length=400)


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

    result = serializers.ChoiceField(choices=WorkRequest.Results)

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


class OnWorkRequestCompleted(serializers.Serializer):
    """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())
    data = serializers.DictField()
    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: Optional[str] = None

    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, FileSerializerResponse]:
        """
        Return files in the artifact with their hash and size.

        The files might not be available yet if they have not been uploaded.
        """
        files = {}

        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:detail",
                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"]
