from django.core.exceptions import ObjectDoesNotExist
from django.db.backends.postgresql.psycopg_any import NumericRange
from django.utils.translation import gettext as _
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from netaddr import IPNetwork
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework.relations import PrimaryKeyRelatedField, RelatedField

from utilities.data import get_inclusive_integer_range_bounds

__all__ = (
    'AttributesField',
    'ChoiceField',
    'ContentTypeField',
    'IPNetworkSerializer',
    'IntegerRangeSerializer',
    'RelatedObjectCountField',
    'SerializedPKRelatedField',
)


class ChoiceField(serializers.Field):
    """
    Represent a ChoiceField as {'value': <DB value>, 'label': <string>}. Accepts a single value on write.

    :param choices: An iterable of choices in the form (value, key).
    :param allow_blank: Allow blank values in addition to the listed choices.
    """
    def __init__(self, choices, allow_blank=False, **kwargs):
        self.choiceset = choices
        self.allow_blank = allow_blank
        self._choices = dict()

        # Unpack grouped choices
        for k, v in choices:
            if type(v) in [list, tuple]:
                for k2, v2 in v:
                    self._choices[k2] = v2
            else:
                self._choices[k] = v

        super().__init__(**kwargs)

    def validate_empty_values(self, data):
        # Convert null to an empty string unless allow_null == True
        if data is None:
            if self.allow_null:
                return True, None
            data = ''
        return super().validate_empty_values(data)

    def to_representation(self, obj):
        if obj != '':
            # Use an empty string in place of the choice label if it cannot be resolved (i.e. because a previously
            # configured choice has been removed from FIELD_CHOICES).
            return {
                'value': obj,
                'label': self._choices.get(obj, ''),
            }
        return None

    def to_internal_value(self, data):
        if data == '':
            if self.allow_blank:
                return data
            raise ValidationError(_("This field may not be blank."))

        # Provide an explicit error message if the request is trying to write a dict or list
        if isinstance(data, (dict, list)):
            raise ValidationError(
                _('Value must be passed directly (e.g. "foo": 123); do not use a dictionary or list.')
            )

        # Check for string representations of boolean/integer values
        if hasattr(data, 'lower'):
            if data.lower() == 'true':
                data = True
            elif data.lower() == 'false':
                data = False
            else:
                try:
                    data = int(data)
                except ValueError:
                    pass

        try:
            if data in self._choices:
                return data
        except TypeError:  # Input is an unhashable type
            pass

        raise ValidationError(_("{value} is not a valid choice.").format(value=data))

    @property
    def choices(self):
        return self._choices


@extend_schema_field(OpenApiTypes.STR)
class ContentTypeField(RelatedField):
    """
    Represent a ContentType as '<app_label>.<model>'
    """
    default_error_messages = {
        "does_not_exist": _("Invalid content type: {content_type}"),
        "invalid": _("Invalid value. Specify a content type as '<app_label>.<model_name>'."),
    }

    def to_internal_value(self, data):
        try:
            app_label, model = data.split('.')
            # Scoped to the field's declared queryset, not the raw ContentType table (#22748).
            return self.get_queryset().get(app_label=app_label, model=model)
        except ObjectDoesNotExist:
            self.fail('does_not_exist', content_type=data)
        except (AttributeError, TypeError, ValueError):
            self.fail('invalid')

    def to_representation(self, obj):
        return f"{obj.app_label}.{obj.model}"


class IPNetworkSerializer(serializers.Serializer):
    """
    Representation of an IP network value (e.g. 192.0.2.0/24).
    """
    def to_representation(self, instance):
        return str(instance)

    def to_internal_value(self, value):
        return IPNetwork(value)


class SerializedPKRelatedField(PrimaryKeyRelatedField):
    """
    Extends PrimaryKeyRelatedField to return a serialized object on read. This is useful for representing related
    objects in a ManyToManyField while still allowing a set of primary keys to be written.
    """
    def __init__(self, serializer, nested=False, **kwargs):
        self.serializer = serializer
        self.nested = nested
        self.pk_field = kwargs.pop('pk_field', None)

        super().__init__(**kwargs)

    def to_representation(self, value):
        return self.serializer(value, nested=self.nested, context={'request': self.context['request']}).data


@extend_schema_field(OpenApiTypes.INT64)
class RelatedObjectCountField(serializers.ReadOnlyField):
    """
    Represents a read-only integer count of related objects (e.g. the number of racks assigned to a site). This field
    is detected by get_annotations_for_serializer() when determining the annotations to be added to a queryset
    depending on the serializer fields selected for inclusion in the response.
    """
    def __init__(self, relation, **kwargs):
        self.relation = relation

        super().__init__(**kwargs)


class IntegerRangeSerializer(serializers.Serializer):
    """
    Represents a range of integers.
    """
    def to_internal_value(self, data):
        if not isinstance(data, (list, tuple)) or len(data) != 2:
            raise ValidationError(_("Ranges must be specified in the form (lower, upper)."))
        if type(data[0]) is not int or type(data[1]) is not int:
            raise ValidationError(_("Range boundaries must be defined as integers."))

        return NumericRange(data[0], data[1] + 1, bounds='[)')

    def to_representation(self, instance):
        return get_inclusive_integer_range_bounds(instance)


class AttributesField(serializers.JSONField):
    """
    Custom attributes stored as JSON data.
    """
    def to_internal_value(self, data):
        data = super().to_internal_value(data)

        # If updating an object, start with the initial attribute data. This enables the client to modify
        # individual attributes without having to rewrite the entire field.
        if data and self.parent.instance:
            initial_data = getattr(self.parent.instance, self.source, None) or {}
            return {**initial_data, **data}

        return data
