# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import base64
import hashlib
import logging

from django.conf import settings

try:
    from django.utils.encoding import smart_bytes
except ImportError:
    from django.utils.encoding import smart_str as smart_bytes

from django_browserid.base import get_audience, LocalVerifier, RemoteVerifier
from django_browserid.signals import user_created
from django_browserid.util import import_from_setting

try:
    from django.contrib.auth import get_user_model
except ImportError:
    from django.contrib.auth.models import User

    def get_user_model(*args, **kwargs):
        return User


logger = logging.getLogger(__name__)


def default_username_algo(email):
    # store the username as a base64 encoded sha1 of the email address
    # this protects against data leakage because usernames are often
    # treated as public identifiers (so we can't use the email address).
    return base64.urlsafe_b64encode(
        hashlib.sha1(smart_bytes(email)).digest()
    ).rstrip(b'=')


class BrowserIDBackend(object):
    supports_anonymous_user = False
    supports_inactive_user = True
    supports_object_permissions = False

    def __init__(self):
        # Store the current user model on creation to avoid issues if settings.AUTH_USER_MODEL
        # changes, which usually only happens during tests.
        self.User = get_user_model()

    def get_verifier(self):
        """
        Create a verifier for verifying assertions. Uses a
        :class:`django_browserid.base.RemoteVerifier` by default.
        """
        return RemoteVerifier()

    def filter_users_by_email(self, email):
        """Return all users matching the specified email."""
        return self.User.objects.filter(email=email)

    def create_user(self, email):
        """Return object for a newly created user account."""
        from django.db import IntegrityError  # Importing at the top causes issues on DB init.

        username_algo = getattr(settings, 'BROWSERID_USERNAME_ALGO', None)
        if username_algo is not None:
            username = username_algo(email)
        else:
            username = default_username_algo(email)

        try:
            return self.User.objects.create_user(username, email)
        except IntegrityError as err:
            # Race condition! Attempt to re-fetch from the database.
            logger.warning('IntegrityError during user creation: {0}'.format(err))

            try:
                return self.User.objects.get(email=email)
            except self.User.DoesNotExist:
                # Whatevs, let's re-raise the error.
                raise err

    def is_valid_email(self, email):
        """Return True if the email address is ok to log in."""
        # This method is basically for your overriding pleasures.
        return True

    def verify(self, assertion=None, audience=None, request=None, **kwargs):
        """
        Verify the given assertion and audience. See ``authenticate``
        for accepted arguments.
        """
        if audience is None and request:
            audience = get_audience(request)

        if audience is None or assertion is None:
            return None

        verifier = self.get_verifier()
        try:
            result = verifier.verify(assertion, audience, **kwargs)
        except Exception as e:
            result = None
            logger.warn('Error while verifying assertion %s with audience %s.', assertion,
                        audience)
            logger.warn(e)

        if not result:
            return None
        else:
            return result.email

    def authenticate(self, assertion=None, audience=None, request=None, **kwargs):
        """
        Authenticate a user by verifying a BrowserID assertion. Defers
        to the verifier returned by
        :func:`BrowserIDBackend.get_verifier` for verification.

        You may either pass the ``request`` parameter to determine the
        audience from the request, or pass the ``audience`` parameter
        explicitly.

        :param assertion:
            Assertion submitted by the user. This asserts that the user
            controls a specific email address.

        :param audience:
            The audience to use when verifying the assertion; this
            prevents another site using an assertion for their site to
            login to yours. This value takes precedence over the
            audience pulled from the request parameter, if given.

        :param request:
            The request that generated this authentication attempt. This
            is used to determine the audience to use during
            verification, using the
            :func:`django_browserid.base.get_audience` function. If the
            audience parameter is also passed, it will be used instead
            of the audience from the request.

        :param kwargs:
            All remaining keyword arguments are passed to the ``verify``
            function on the verifier.
        """
        email = self.verify(assertion, audience, request, **kwargs)
        if not email or not self.is_valid_email(email):
            return None

        # In the rare case that two user accounts have the same email address,
        # log and bail. Randomly selecting one seems really wrong.
        users = self.filter_users_by_email(email=email)
        if len(users) > 1:
            logger.warn('%s users with email address %s.', len(users), email)
            return None
        if len(users) == 1:
            return users[0]

        create_user = getattr(settings, 'BROWSERID_CREATE_USER', True)
        if not create_user:
            logger.debug('Login failed: No user with email %s found, and '
                         'BROWSERID_CREATE_USER is False', email)
            return None
        else:
            if create_user is True:
                create_function = self.create_user
            else:
                # Find the function to call.
                create_function = import_from_setting('BROWSERID_CREATE_USER')

            user = create_function(email)
            user_created.send(create_function, user=user)
            return user

    def get_user(self, user_id):
        try:
            user = self.User.objects.get(pk=user_id)
            return user
        except self.User.DoesNotExist:
            return None


class LocalBrowserIDBackend(BrowserIDBackend):
    """
    BrowserID authentication backend that uses local verification
    instead of remote verification.
    """
    def get_verifier(self):
        return LocalVerifier()


class AutoLoginBackend(BrowserIDBackend):
    """
    Auto-auths everyone as the user with an email matching the
    BROWSERID_AUTOLOGIN_EMAIL setting. If BROWSERID_AUTOLOGIN_ENABLED
    is False (the default value), auth-auth is disabled.

    .. warning:: Use of this backend creates an insecure site. Do not
                 use on a publicly-visible server!
    """
    def verify(self, *args, **kwargs):
        autologin_enabled = getattr(settings, 'BROWSERID_AUTOLOGIN_ENABLED', False)
        email = getattr(settings, 'BROWSERID_AUTOLOGIN_EMAIL', None)
        if autologin_enabled and email:
            return email
        else:
            return None
