import copy
from gettext import gettext as _
import json

import createrepo_c
from django.db import IntegrityError, transaction
from rest_framework import serializers

from pulpcore.plugin.serializers import (
    ModelSerializer,
    NoArtifactContentUploadSerializer,
)

from pulp_rpm.app.advisory import hash_update_record
from pulp_rpm.app.fields import (
    UpdateCollectionPackagesField,
    UpdateReferenceField,
)

from pulp_rpm.app.constants import (
    CR_UPDATE_REFERENCE_ATTRS,
    PULP_UPDATE_COLLECTION_ATTRS,
    PULP_UPDATE_RECORD_ATTRS,
    PULP_UPDATE_REFERENCE_ATTRS,
)

from pulp_rpm.app.models import (
    UpdateCollection,
    UpdateCollectionPackage,
    UpdateRecord,
    UpdateReference,
)


class UpdateCollectionSerializer(ModelSerializer):
    """
    A Serializer for UpdateCollection.
    """

    name = serializers.CharField(help_text=_("Collection name."), allow_blank=True, allow_null=True)

    shortname = serializers.CharField(
        help_text=_("Collection short name."), allow_blank=True, allow_null=True
    )

    module = serializers.JSONField(help_text=_("Collection modular NSVCA."), allow_null=True)

    packages = UpdateCollectionPackagesField(
        source="*", read_only=True, help_text=_("List of packages")
    )

    class Meta:
        fields = ("name", "shortname", "module", "packages")
        model = UpdateCollection


class UpdateRecordSerializer(NoArtifactContentUploadSerializer):
    """
    A Serializer for UpdateRecord.
    """

    id = serializers.CharField(
        help_text=_("Update id (short update name, e.g. RHEA-2013:1777)"),
        read_only=True,
    )
    updated_date = serializers.CharField(
        help_text=_("Date when the update was updated (e.g. '2013-12-02 00:00:00')"),
        read_only=True,
    )

    description = serializers.CharField(
        help_text=_("Update description"), allow_blank=True, read_only=True
    )
    issued_date = serializers.CharField(
        help_text=_("Date when the update was issued (e.g. '2013-12-02 00:00:00')"),
        read_only=True,
    )
    fromstr = serializers.CharField(
        help_text=_("Source of the update (e.g. security@redhat.com)"),
        allow_blank=True,
        read_only=True,
    )
    status = serializers.CharField(
        help_text=_("Update status ('final', ...)"), allow_blank=True, read_only=True
    )
    title = serializers.CharField(help_text=_("Update name"), allow_blank=True, read_only=True)
    summary = serializers.CharField(help_text=_("Short summary"), allow_blank=True, read_only=True)
    version = serializers.CharField(
        help_text=_("Update version (probably always an integer number)"),
        allow_blank=True,
        read_only=True,
    )

    type = serializers.CharField(
        help_text=_("Update type ('enhancement', 'bugfix', ...)"),
        allow_blank=True,
        read_only=True,
    )
    severity = serializers.CharField(help_text=_("Severity"), allow_blank=True, read_only=True)
    solution = serializers.CharField(help_text=_("Solution"), allow_blank=True, read_only=True)
    release = serializers.CharField(help_text=_("Update release"), allow_blank=True, read_only=True)
    rights = serializers.CharField(help_text=_("Copyrights"), allow_blank=True, read_only=True)
    pushcount = serializers.CharField(help_text=_("Push count"), allow_blank=True, read_only=True)
    reboot_suggested = serializers.BooleanField(help_text=_("Reboot suggested"), read_only=True)
    pkglist = UpdateCollectionSerializer(
        source="collections", read_only=True, many=True, help_text=_("List of packages")
    )
    references = UpdateReferenceField(
        source="pk", read_only=True, help_text=_("List of references")
    )

    def create(self, validated_data):
        """
        Create UpdateRecord and its subclasses from JSON file.

        Returns:
            UpdateRecord instance

        """
        references = validated_data.pop("references", [])
        pkglist = validated_data.pop("pkglist", [])

        # detach any specified repository; attach once the advisory exists
        repository = validated_data.pop("repository", None)

        update_collection_packages_to_save = list()
        update_references_to_save = list()
        with transaction.atomic():
            try:
                # This persists an advisory with an empty digest
                update_record = super().create(validated_data)
            except IntegrityError:
                # At this point, the advisory has an empty digest. If we hit this,
                # it means a previous advisory-create failed to clean up after itself.
                raise serializers.ValidationError("Advisory already exists in Pulp.")

            for collection in pkglist:
                new_coll = copy.deepcopy(collection)
                packages = new_coll.pop("packages", [])
                new_coll[PULP_UPDATE_COLLECTION_ATTRS.SHORTNAME] = new_coll.pop("short", "")
                coll = UpdateCollection(**new_coll)
                coll.update_record = update_record
                coll.save()
                for package in packages:
                    pkg = UpdateCollectionPackage(**package)
                    try:
                        pkg.sum_type = createrepo_c.checksum_type(pkg.sum_type)
                    except TypeError:
                        raise TypeError(f'"{pkg.sum_type}" is not supported.')
                    pkg.update_collection = coll
                    update_collection_packages_to_save.append(pkg)
            for reference in references:
                new_ref = dict()
                new_ref[PULP_UPDATE_REFERENCE_ATTRS.HREF] = reference.get(
                    CR_UPDATE_REFERENCE_ATTRS.HREF, ""
                )
                new_ref[PULP_UPDATE_REFERENCE_ATTRS.ID] = reference.get(
                    CR_UPDATE_REFERENCE_ATTRS.ID, ""
                )
                new_ref[PULP_UPDATE_REFERENCE_ATTRS.TITLE] = reference.get(
                    CR_UPDATE_REFERENCE_ATTRS.TITLE, ""
                )
                new_ref[PULP_UPDATE_REFERENCE_ATTRS.TYPE] = reference.get(
                    CR_UPDATE_REFERENCE_ATTRS.TYPE, ""
                )
                ref = UpdateReference(**new_ref)
                ref.update_record = update_record
                update_references_to_save.append(ref)

            if update_collection_packages_to_save:
                UpdateCollectionPackage.objects.bulk_create(update_collection_packages_to_save)
            if update_references_to_save:
                UpdateReference.objects.bulk_create(update_references_to_save)

            cr_update_record = update_record.to_createrepo_c()
            update_record.digest = hash_update_record(cr_update_record)

            # The advisory now has a digest - *if* this works
            update_record.save()

        # create new repo version with uploaded advisory
        if repository:
            repository.cast()
            content_to_add = self.Meta.model.objects.filter(pk=update_record.pk)
            with repository.new_version() as new_version:
                new_version.add_content(content_to_add)

        return update_record

    def deferred_validate(self, data):
        """
        Read a file for a JSON data and validate a UpdateRecord data.

        Also change few fields to match Pulp internals if exists as this is usually handle by
        createrepo_c which is not used here.
        """
        data = super().deferred_validate(data)

        update_record_data = dict()
        try:
            update_record_data.update(json.loads(data["file"].read()))
        except UnicodeDecodeError:
            raise serializers.ValidationError("JSON file is expected")
        update_record_data.update(data)

        update_record_data[PULP_UPDATE_RECORD_ATTRS.FROMSTR] = update_record_data.pop(
            "from", update_record_data.get(PULP_UPDATE_RECORD_ATTRS.FROMSTR, "")
        )
        update_record_data[PULP_UPDATE_RECORD_ATTRS.ISSUED_DATE] = update_record_data.pop(
            "issued", update_record_data.get(PULP_UPDATE_RECORD_ATTRS.ISSUED_DATE, "")
        )
        update_record_data[PULP_UPDATE_RECORD_ATTRS.UPDATED_DATE] = update_record_data.pop(
            "updated", update_record_data.get(PULP_UPDATE_RECORD_ATTRS.UPDATED_DATE, "")
        )

        if (
            not update_record_data.get(PULP_UPDATE_RECORD_ATTRS.ID)
            or not update_record_data.get(PULP_UPDATE_RECORD_ATTRS.UPDATED_DATE)
            or not update_record_data.get(PULP_UPDATE_RECORD_ATTRS.ISSUED_DATE)
        ):
            raise serializers.ValidationError(
                "All '{}', '{}' and '{}' must be specified.".format(
                    PULP_UPDATE_RECORD_ATTRS.ID,
                    PULP_UPDATE_RECORD_ATTRS.UPDATED_DATE,
                    PULP_UPDATE_RECORD_ATTRS.ISSUED_DATE,
                )
            )

        return update_record_data

    def retrieve(self, validated_data):
        # This feature cannot be implemented until we can reliably calculate the digest of the
        # advisory before saving anything to the database. By current standards, we can't.
        return None

    class Meta:
        fields = NoArtifactContentUploadSerializer.Meta.fields + (
            "id",
            "updated_date",
            "description",
            "issued_date",
            "fromstr",
            "status",
            "title",
            "summary",
            "version",
            "type",
            "severity",
            "solution",
            "release",
            "rights",
            "pushcount",
            "pkglist",
            "references",
            "reboot_suggested",
        )
        model = UpdateRecord


class MinimalUpdateRecordSerializer(UpdateRecordSerializer):
    """
    A minimal serializer for RPM update records.
    """

    class Meta:
        fields = NoArtifactContentUploadSerializer.Meta.fields + (
            "id",
            "title",
            "severity",
            "type",
        )
        model = UpdateRecord
