"""Let people sign in with either their username or their email address.

Owners were migrated from WordPress with usernames they may never have chosen
and won't remember; the email address is the thing they know. Matching is
case-insensitive on both.
"""
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from django.db.models import Q

from .throttle import client_ip, locked_out

User = get_user_model()


class UsernameOrEmailBackend(ModelBackend):
    def authenticate(self, request, username=None, password=None, **kwargs):
        identifier = username or kwargs.get(User.USERNAME_FIELD) or kwargs.get("email")
        if identifier is None or password is None:
            return None

        # Refuse before checking the password, so a locked-out attacker learns
        # nothing from a correct guess either.
        if locked_out(identifier, client_ip(request)):
            return None

        matches = list(
            User.objects.filter(
                Q(username__iexact=identifier) | Q(email__iexact=identifier)
            )[:2]
        )

        if not matches:
            # Run the hasher anyway so a missing account takes the same time as
            # a wrong password — otherwise response timing reveals which
            # addresses have accounts.
            User().set_password(password)
            return None

        if len(matches) > 1:
            # Two accounts share this address; refuse rather than guess which.
            # Exact username match still wins, since that is unambiguous.
            exact = [u for u in matches if u.username.lower() == identifier.lower()]
            if len(exact) != 1:
                return None
            matches = exact

        user = matches[0]
        if user.check_password(password) and self.user_can_authenticate(user):
            return user
        return None
