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


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.

        Returns:
            The validated email address.

        Raises:
            serializers.ValidationError:
                If the serializer is bound and the provided email
                doesn't match the existing address.
        """
        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 save(self):
        """
        Confirm the email address matching the confirmation key.
        """
        self._confirmation.confirm()

    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 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,
                user=user)
            email_instance.send_confirmation()

        return user

    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'])
