"""Serializers for the ``rest_email_auth`` app.

The serializers handle the conversion of data between the JSON or form
data the API receives and native Python datatypes.
"""

import logging

from django.contrib.auth import get_user_model, password_validation
from django.utils.translation import ugettext_lazy as _

from rest_framework import serializers

from rest_email_auth import models, signals, app_settings

logger = logging.getLogger(__name__)


class EmailSerializer(serializers.ModelSerializer):
    """
    Serializer for email addresses.
    """

    class Meta(object):
        extra_kwargs = {
            "email": {
                # We remove the autogenerated 'unique' validator to
                # avoid leaking email addresses.
                "validators": []
            }
        }
        fields = ("id", "created_at", "email", "is_primary", "is_verified")
        model = models.EmailAddress
        read_only_fields = ("is_verified",)

    def create(self, validated_data):
        """
        Create a new email and send a confirmation to it.

        Returns:
            The newly creating ``EmailAddress`` instance.
        """
        email_query = models.EmailAddress.objects.filter(
            email=self.validated_data["email"]
        )

        if email_query.exists():
            email = email_query.get()

            email.send_duplicate_notification()
        else:
            email = super(EmailSerializer, self).create(validated_data)
            email.send_confirmation()

            user = validated_data.get("user")
            query = models.EmailAddress.objects.filter(
                is_primary=True, user=user
            )

            if not query.exists():
                email.set_primary()

        return email

    def update(self, instance, validated_data):
        """
        Update the instance the serializer is bound to.

        Args:
            instance:
                The instance the serializer is bound to.
            validated_data:
                The data to update the serializer with.

        Returns:
            The updated instance.
        """
        is_primary = validated_data.pop("is_primary", False)

        instance = super(EmailSerializer, self).update(
            instance, validated_data
        )

        if is_primary:
            instance.set_primary()

        return instance

    def validate_email(self, email):
        """
        Validate the provided email address.

        The email address is first modified to match the RFC spec.
        Namely, the domain portion of the email is lowercased.

        Returns:
            The validated email address.

        Raises:
            serializers.ValidationError:
                If the serializer is bound and the provided email
                doesn't match the existing address.
        """
        user, domain = email.rsplit("@", 1)
        email = "@".join([user, domain.lower()])

        if self.instance and email and self.instance.email != email:
            raise serializers.ValidationError(
                _(
                    "Existing emails may not be edited. Create a new one "
                    "instead."
                )
            )

        return email

    def validate_is_primary(self, is_primary):
        """
        Validate the provided 'is_primary' parameter.

        Returns:
            The validated 'is_primary' value.

        Raises:
            serializers.ValidationError:
                If the user attempted to mark an unverified email as
                their primary email address.
        """
        # TODO: Setting 'is_primary' to 'False' should probably not be
        #       allowed.
        if is_primary and not (self.instance and self.instance.is_verified):
            raise serializers.ValidationError(
                _(
                    "Unverified email addresses may not be used as the "
                    "primary address."
                )
            )

        return is_primary


class EmailVerificationSerializer(serializers.Serializer):
    """
    Serializer for verifying an email address.
    """

    email = serializers.EmailField(read_only=True)
    key = serializers.CharField(write_only=True)
    password = serializers.CharField(
        style={"input_type": "password"}, write_only=True
    )

    def __init__(self, *args, **kwargs):
        """
        Conditionally remove the password field based on if a password
        is required to verify an email address.
        """
        super().__init__(*args, **kwargs)

        self._confirmation = None

        if not app_settings.EMAIL_VERIFICATION_PASSWORD_REQUIRED:
            self.fields.pop("password")

    def save(self):
        """
        Confirm the email address matching the confirmation key.
        """
        self._confirmation.confirm()
        self._confirmation.delete()

    def validate(self, data):
        """
        Validate the provided data.

        Returns:
            dict:
                The validated data.

        Raises:
            serializers.ValidationError:
                If the provided password is invalid.
        """
        user = self._confirmation.email.user

        if (
            app_settings.EMAIL_VERIFICATION_PASSWORD_REQUIRED
            and not user.check_password(data["password"])
        ):
            raise serializers.ValidationError(
                _("The provided password is invalid.")
            )

        # Add email to returned data
        data["email"] = self._confirmation.email.email

        return data

    def validate_key(self, key):
        """
        Validate the provided confirmation key.

        Returns:
            str:
                The validated confirmation key.

        Raises:
            serializers.ValidationError:
                If there is no email confirmation with the given key or
                the confirmation has expired.
        """
        try:
            confirmation = models.EmailConfirmation.objects.select_related(
                "email__user"
            ).get(key=key)
        except models.EmailConfirmation.DoesNotExist:
            raise serializers.ValidationError(
                _("The provided verification key is invalid.")
            )

        if confirmation.is_expired:
            raise serializers.ValidationError(
                _("That verification code has expired.")
            )

        # Cache confirmation instance
        self._confirmation = confirmation

        return key


class PasswordResetRequestSerializer(serializers.Serializer):
    """
    Serializer for requesting a password reset.
    """

    email = serializers.EmailField(
        help_text=_("The email address to send the password reset to.")
    )

    def save(self):
        """
        Send out a password reset if the provided data is valid.

        If the provided email address exists and is verified, a reset
        email is sent to the address.

        Returns:
            The password reset token if it was returned and ``None``
            otherwise.
        """
        try:
            email = models.EmailAddress.objects.get(
                email=self.validated_data["email"], is_verified=True
            )
        except models.EmailAddress.DoesNotExist:
            return None

        token = models.PasswordResetToken.objects.create(email=email)
        token.send()

        return token


class PasswordResetSerializer(serializers.Serializer):
    """
    Serializer for reseting a user's password.
    """

    key = serializers.UUIDField(
        help_text=_(
            "The key received by the user in the password reset " "email."
        ),
        write_only=True,
    )
    password = serializers.CharField(
        help_text=_("The user's new password."),
        style={"input_type": "password"},
        write_only=True,
    )

    def save(self):
        """
        Reset the user's password if the provided information is valid.
        """
        token = models.PasswordResetToken.objects.get(
            key=self.validated_data["key"]
        )

        token.email.user.set_password(self.validated_data["password"])
        token.email.user.save()

        logger.info("Reset password for %s", token.email.user)

        token.delete()

    def validate_key(self, key):
        """
        Validate the provided reset key.

        Returns:
            The validated key.

        Raises:
            serializers.ValidationError:
                If the provided key does not exist.
        """
        if not models.PasswordResetToken.valid_tokens.filter(key=key).exists():
            raise serializers.ValidationError(
                _("The provided reset token does not exist, or is expired.")
            )

        return key

    def validate_password(self, password):
        """
        Validate the provided password by running it through Django's
        password validation system.

        Returns:
            The validated password.

        Raises:
            ValidationError:
                If the provided password does not pass the configured
                password validators.
        """
        password_validation.validate_password(password)

        return password


class RegistrationSerializer(serializers.ModelSerializer):
    """
    Serializer for registering new users.
    """

    email = serializers.EmailField()

    class Meta(object):
        extra_kwargs = {
            "password": {
                "style": {"input_type": "password"},
                "write_only": True,
            }
        }
        fields = (get_user_model().USERNAME_FIELD, "email", "password")
        model = get_user_model()

    def create(self, validated_data):
        """
        Create a new user from the data passed to the serializer.

        If the provided email has not been verified yet, the user is
        created and a verification email is sent to the address.
        Otherwise we send a notification to the email address that
        someone attempted to register with an email that's already been
        verified.

        Args:
            validated_data (dict):
                The data passed to the serializer after it has been
                validated.

        Returns:
            A new user created from the provided data.
        """
        email = validated_data.pop("email")
        password = validated_data.pop("password")

        # We don't save the user instance yet in case the provided email
        # address already exists.
        user = get_user_model()(**validated_data)
        user.set_password(password)

        # We set an ephemeral email property so that it is included in
        # the data returned by the serializer.
        user.email = email

        email_query = models.EmailAddress.objects.filter(email=email)

        if email_query.exists():
            existing_email = email_query.get()
            existing_email.send_duplicate_notification()
        else:
            user.save()

            email_instance = models.EmailAddress.objects.create(
                email=email, is_primary=True, user=user
            )
            email_instance.send_confirmation()

            signals.user_registered.send(sender=self.__class__, user=user)

        return user

    def validate_email(self, email):
        """
        Validate the provided email address.

        Args:
            email:
                The email address to validate.

        Returns:
            The provided email address, transformed to match the RFC
            spec. Namely, the domain portion of the email must be
            lowercase.
        """
        user, domain = email.rsplit("@", 1)

        return "@".join([user, domain.lower()])

    def validate_password(self, password):
        """
        Validate the provided password.

        Args:
            password (str):
                The password provided by the user.

        Returns:
            str:
                The validated password.

        Raises:
            ValidationError:
                If the provided password doesn't pass Django's provided
                password validation.
        """
        password_validation.validate_password(password)

        return password


class ResendVerificationSerializer(serializers.Serializer):
    """
    Serializer for resending a verification email.
    """

    email = serializers.EmailField()

    def save(self):
        """
        Resend a verification email to the provided address.

        If the provided email is already verified no action is taken.
        """
        try:
            email = models.EmailAddress.objects.get(
                email=self.validated_data["email"], is_verified=False
            )

            logger.debug(
                "Resending verification email to %s",
                self.validated_data["email"],
            )

            email.send_confirmation()
        except models.EmailAddress.DoesNotExist:
            logger.debug(
                "Not resending verification email to %s because the address "
                "doesn't exist in the database.",
                self.validated_data["email"],
            )
