"""
Base DRF serializers for Console API so that
customizations can consistently and quickly be
deployed throughout the entire application.
"""

from copy import copy
from typing import Any, cast, TypeAlias, TYPE_CHECKING
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError as DjangoValidationError
from django.db.models import QuerySet
from drf_spectacular.utils import extend_schema_field, inline_serializer
from drf_spectacular.types import OpenApiTypes
from rest_framework import exceptions
from rest_framework.serializers import (
    CharField,
    HyperlinkedIdentityField,
    HyperlinkedModelSerializer,
    ListSerializer,
    ModelSerializer,
    Serializer,
    SlugRelatedField,
)
from .validators import (
    PreCreateValidation,
)
from console_base.validators import (
    validate_no_spaces,
)

if TYPE_CHECKING:
    from console_base.models import BaseUUIDPKModel

    CrudOpBase: TypeAlias = ModelSerializer
else:
    CrudOpBase = object


# ---------------------------------------------------------------------------
class LCSerializer(Serializer):
    pass


# ---------------------------------------------------------------------------
class PasswordSerializer(LCSerializer):
    password = CharField(validators=[validate_no_spaces])


# ---------------------------------------------------------------------------
class CrudOp(CrudOpBase):
    """
    Mixin to track whether record is being Created or Updated
    """

    def is_creating(self) -> bool:
        return not self.instance or not self.instance.modified

    def is_updating(self) -> bool:
        return self.instance is not None

    def is_valid(self, raise_exception: bool = False) -> bool:
        """
        Rest Framework doesn't call <model>.full_clean during Serializer validation,
        so validators assigned on the model fields aren't called.

        We want to do this so that model validators don't need to be duplicated, and
        so that all validation can be confirmed easily for doing updates via List Views.

        Override to call `full_clean` on model to catch any model field validators.
        """
        is_valid = super().is_valid(raise_exception=raise_exception)

        if not is_valid:
            return False

        return self.call_clean_fields(raise_exception)

    def get_record_instance(self) -> 'BaseUUIDPKModel':
        if self.is_updating():
            # set validated data on a copy of the serializer instance
            # since we're skipping distinctions between related fields
            record = copy(self.instance)
            for attr, value in self.validated_data.items():
                setattr(record, attr, value)
            return cast('BaseUUIDPKModel', record)

        return self.Meta.model(**self.validated_data)

    def call_clean_fields(self, raise_exception: bool) -> bool:
        """
        Call field validators defined on the model, so they
        don't need to be duplicated in the serializers.
        """
        record = self.get_record_instance()
        errors = {}

        try:
            record.clean_fields()
        except DjangoValidationError as e:
            errors = self.scrub_clean_fields_errors(e.message_dict)

        # Form.clean() is run even if other validation fails, so do the
        # same with Model.clean() for consistency.
        try:
            record.clean()
        except DjangoValidationError as e:
            errors = e.update_error_dict(errors)

        if errors:
            self._errors.update(errors)
            if raise_exception:
                raise exceptions.ValidationError(self._errors) from None

        return not bool(self._errors)

    def scrub_clean_fields_errors(self, full_clean_errors: dict) -> dict:
        """
        clean_fields adds messages for all fields that don't have values defined and where
        there's no "blank=True", but we're only interested in assigned field validators

        Some field errors should not be handled by calling full_clean from serializers,
        such as field values that might be auto-calculated on model.save.

        Override on subclasses to extend logic.
        """
        errors = {}
        serializer_fields = set(self.Meta.fields)
        serializer_fields.add(NON_FIELD_ERRORS)
        blank_msg = 'this field cannot be blank.'

        for field, message in full_clean_errors.items():
            if field in serializer_fields and message[0].lower() != blank_msg:
                errors[field] = message

        return errors

    def partial_mode_skip_validation(
        self,
        data: dict,
        required_fields: tuple[str, ...],
    ) -> bool:
        """
        When serializer is doing a partial update only, then perform
        validation only if every required fields is present in the data.
        """
        if self.is_creating() or not self.partial:
            return False

        for field in required_fields:
            if field not in data:
                return True

        return False


# ---------------------------------------------------------------------------
class LCHyperlinkedModelSerializer(PreCreateValidation, CrudOp, HyperlinkedModelSerializer):
    pass


# ---------------------------------------------------------------------------
class LCModelSerializer(PreCreateValidation, CrudOp, ModelSerializer):
    pass


# ---------------------------------------------------------------------------
class LCListSerializer(ListSerializer):
    pass


# ---------------------------------------------------------------------------
class LCHyperlinkedIdentityField(HyperlinkedIdentityField):
    def __init__(self, view_name: str | None = None, **kwargs: Any) -> None:
        if 'lookup_field' not in kwargs:
            kwargs['lookup_field'] = 'cid'
        super().__init__(view_name=view_name, **kwargs)


# ---------------------------------------------------------------------------
@extend_schema_field(field=OpenApiTypes.UUID)
class CanonicalIDField(SlugRelatedField):
    def __init__(self, **kwargs: Any):
        help_text = kwargs.get('help_text') or ''
        kwargs['help_text'] = f'{help_text} Canonical ID (cid) of related record'
        super().__init__(slug_field='cid', **kwargs)

    def get_queryset(self) -> QuerySet:
        """
        12-15% faster to only return `cid` field!
        """
        return super().get_queryset().only('cid')

    def to_representation(self, obj: 'BaseUUIDPKModel') -> str:
        return str(obj.cid)

    def display_value(self, instance: 'BaseUUIDPKModel') -> str:
        """
        Only print `cid` field to avoid extra database hits
        that the full instance representation might require.
        """
        return str(instance.cid)


class PrimaryKeyField(SlugRelatedField):
    def __init__(self, slug_field='pk', **kwargs):
        super().__init__(slug_field, **kwargs)

    def display_value(self, instance: Any) -> int:
        """
        Only print `pk` field to avoid extra database hits
        that the full instance representation might require.
        """
        return instance.pk


StatusSerializer = inline_serializer('status', {'status': CharField()})
CidSerializer = inline_serializer('cid', {'cid': CanonicalIDField()})
CidStatusSerializer = inline_serializer(
    'cid_status',
    {
        'cid': CanonicalIDField(),
        'status': CharField(),
    },
)

__all__ = (
    'CanonicalIDField',
    'CrudOp',
    'LCHyperlinkedIdentityField',
    'LCHyperlinkedModelSerializer',
    'LCListSerializer',
    'LCModelSerializer',
    'PasswordSerializer',
    'LCSerializer',
    'StatusSerializer',
    'CidSerializer',
    'CidStatusSerializer',
    'PrimaryKeyField',
)
