import unicodedata
from datetime import timedelta

from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.password_validation import validate_password, get_password_validators
from django.core.exceptions import ValidationError
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from rest_framework import exceptions
from rest_framework.generics import GenericAPIView
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet

from django_rest_passwordreset.models import ResetPasswordToken, clear_expired, get_password_reset_token_expiry_time, \
    get_password_reset_lookup_field
from django_rest_passwordreset.serializers import EmailSerializer, PasswordTokenSerializer, ResetTokenSerializer
from django_rest_passwordreset.signals import reset_password_token_created, pre_password_reset, post_password_reset
from django_rest_passwordreset.throttling import ResetPasswordRequestTokenThrottle

User = get_user_model()

__all__ = [
    'ResetPasswordValidateToken',
    'ResetPasswordConfirm',
    'ResetPasswordRequestToken',
    'reset_password_validate_token',
    'reset_password_confirm',
    'reset_password_request_token',
    'ResetPasswordValidateTokenViewSet',
    'ResetPasswordConfirmViewSet',
    'ResetPasswordRequestTokenViewSet'
]

HTTP_USER_AGENT_HEADER = getattr(settings, 'DJANGO_REST_PASSWORDRESET_HTTP_USER_AGENT_HEADER', 'HTTP_USER_AGENT')
HTTP_IP_ADDRESS_HEADER = getattr(settings, 'DJANGO_REST_PASSWORDRESET_IP_ADDRESS_HEADER', 'REMOTE_ADDR')


def _unicode_ci_compare(s1, s2):
    """
    Perform case-insensitive comparison of two identifiers, using the
    recommended algorithm from Unicode Technical Report 36, section
    2.11.2(B)(2).
    """
    normalized1 = unicodedata.normalize('NFKC', s1)
    normalized2 = unicodedata.normalize('NFKC', s2)

    return normalized1.casefold() == normalized2.casefold()


def clear_expired_tokens():
    """
    Delete all existing expired tokens
    """
    password_reset_token_validation_time = get_password_reset_token_expiry_time()

    # datetime.now minus expiry hours
    now_minus_expiry_time = timezone.now() - timedelta(hours=password_reset_token_validation_time)

    # delete all tokens where created_at < now - 24 hours
    clear_expired(now_minus_expiry_time)


def generate_token_for_email(email, user_agent='', ip_address=''):
    # find a user by email address (case-insensitive search)
    users = User.objects.filter(**{'{}__iexact'.format(get_password_reset_lookup_field()): email})

    active_user_found = False

    # iterate over all users and check if there is any user that is active
    # also check whether the password can be changed (is usable), as there could be users that are not allowed
    # to change their password (e.g., LDAP user)
    for user in users:
        if user.eligible_for_reset():
            active_user_found = True
            break

    # No active user found, raise a ValidationError
    # but not if DJANGO_REST_PASSWORDRESET_NO_INFORMATION_LEAKAGE == True, in that case we return None
    if not active_user_found:
        if not getattr(settings, 'DJANGO_REST_PASSWORDRESET_NO_INFORMATION_LEAKAGE', False):
            raise exceptions.ValidationError({
                'email': [_(
                    "We couldn't find an account associated with that email. Please try a different e-mail address.")],
            })
        return None

    # last but not least: iterate over all users that are active and can change their password
    # and create a Reset Password Token and send a signal with the created token
    for user in users:
        if user.eligible_for_reset() and _unicode_ci_compare(email, getattr(user, get_password_reset_lookup_field())):
            password_reset_tokens = user.password_reset_tokens.all()

            # check if the user already has a token
            if password_reset_tokens.count():
                # yes, already has a token, re-use this token
                return password_reset_tokens.first()

            # no token exists, generate a new token
            return ResetPasswordToken.objects.create(
                user=user,
                user_agent=user_agent,
                ip_address=ip_address.split(",")[0],
            )


class ResetPasswordValidateToken(GenericAPIView):
    """
    An Api View which provides a method to verify that a token is valid
    """
    throttle_classes = ()
    permission_classes = ()
    serializer_class = ResetTokenSerializer
    authentication_classes = ()

    def post(self, request, *args, **kwargs):
        serializer = self.serializer_class(data=request.data)
        serializer.is_valid(raise_exception=True)

        return_data = {'status': 'OK'}

        if getattr(settings, 'DJANGO_REST_PASSWORDRESET_USER_DETAILS_ON_VALIDATION', False):
            token = ResetPasswordToken.objects.get(key=serializer.validated_data['token'])

            return_data['username'] = token.user.username
            return_data['email'] = token.user.email

        return Response(return_data)


class ResetPasswordConfirm(GenericAPIView):
    """
    An Api View which provides a method to reset a password based on a unique token
    """
    throttle_classes = ()
    permission_classes = ()
    serializer_class = PasswordTokenSerializer
    authentication_classes = ()

    def post(self, request, *args, **kwargs):
        serializer = self.serializer_class(data=request.data)
        serializer.is_valid(raise_exception=True)
        password = serializer.validated_data['password']
        token = serializer.validated_data['token']

        # find token
        reset_password_token = ResetPasswordToken.objects.filter(key=token).first()

        # change users password (if we got to this code it means that the user is_active)
        if reset_password_token.user.eligible_for_reset():
            pre_password_reset.send(
                sender=self.__class__,
                user=reset_password_token.user,
                reset_password_token=reset_password_token,
            )
            try:
                # validate the password against existing validators
                validate_password(
                    password,
                    user=reset_password_token.user,
                    password_validators=get_password_validators(settings.AUTH_PASSWORD_VALIDATORS)
                )
            except ValidationError as e:
                # raise a validation error for the serializer
                raise exceptions.ValidationError({
                    'password': e.messages
                })

            reset_password_token.user.set_password(password)
            reset_password_token.user.save()
            post_password_reset.send(
                sender=self.__class__,
                user=reset_password_token.user,
                reset_password_token=reset_password_token,
            )

        # Delete all password reset tokens for this user
        ResetPasswordToken.objects.filter(user=reset_password_token.user).delete()

        return Response({'status': 'OK'})


class ResetPasswordRequestToken(GenericAPIView):
    """
    An Api View which provides a method to request a password reset token based on an e-mail address

    Sends a signal reset_password_token_created when a reset token was created
    """
    throttle_classes = (ResetPasswordRequestTokenThrottle,)
    permission_classes = ()
    serializer_class = EmailSerializer
    authentication_classes = ()

    def post(self, request, *args, **kwargs):
        serializer = self.serializer_class(data=request.data)
        serializer.is_valid(raise_exception=True)

        clear_expired_tokens()
        token = generate_token_for_email(
            email=serializer.validated_data['email'],
            user_agent=request.META.get(HTTP_USER_AGENT_HEADER, ''),
            ip_address=request.META.get(HTTP_IP_ADDRESS_HEADER, ''),
        )

        if token:
            # send a signal that the password token was created
            # let whoever receives this signal handle sending the email for the password reset
            reset_password_token_created.send(
                sender=self.__class__,
                instance=self, reset_password_token=token
            )

        return Response({'status': 'OK'})


class ResetPasswordValidateTokenViewSet(ResetPasswordValidateToken, GenericViewSet):
    """
    An Api ViewSet which provides a method to verify that a token is valid
    """

    def create(self, request, *args, **kwargs):
        return super(ResetPasswordValidateTokenViewSet, self).post(request, *args, **kwargs)


class ResetPasswordConfirmViewSet(ResetPasswordConfirm, GenericViewSet):
    """
    An Api ViewSet which provides a method to reset a password based on a unique token
    """

    def create(self, request, *args, **kwargs):
        return super(ResetPasswordConfirmViewSet, self).post(request, *args, **kwargs)


class ResetPasswordRequestTokenViewSet(ResetPasswordRequestToken, GenericViewSet):
    """
    An Api ViewSet which provides a method to request a password reset token based on an e-mail address

    Sends a signal reset_password_token_created when a reset token was created
    """

    def create(self, request, *args, **kwargs):
        return super(ResetPasswordRequestTokenViewSet, self).post(request, *args, **kwargs)


reset_password_validate_token = ResetPasswordValidateToken.as_view()
reset_password_confirm = ResetPasswordConfirm.as_view()
reset_password_request_token = ResetPasswordRequestToken.as_view()
