from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.mixins import (
    LoginRequiredMixin as AuthenticatedUserRequiredMixin,
)
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
from django.db.models import Q
from django.db.models.functions import Lower
from django.urls import reverse_lazy
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _

from hosting.models import Profile
from hosting.utils import value_without_invalid_marker

from .auth import auth_log
from .utils import is_password_compromised

User = get_user_model()


class LoginRequiredMixin(AuthenticatedUserRequiredMixin):
    """
    An own view mixin enabling the usage of a custom URL parameter name
    for the redirection after successful authentication. Needed due to
    arbitrary limitations on the parameter name customization by Django.
    """
    redirect_field_name = settings.REDIRECT_FIELD_NAME


class UserModifyMixin(object):
    def get_success_url(self, *args, **kwargs):
        try:
            return self.object.profile.get_edit_url()
        except Profile.DoesNotExist:
            return reverse_lazy('profile_create')


def flatpages_as_templates(cls):
    """
    View decorator:
    Facilitates rendering flat pages as Django templates, including usage of
    tags and the view's context. Performs some magic to capture the specific
    view's custom context and provides a helper function `render_flat_page`.
    """
    context_func_name = 'get_context_data'
    context_func = getattr(cls, context_func_name, None)
    if context_func:
        def _get_context_data_superfunc(self, **kwargs):
            context = context_func(self, **kwargs)
            self._flat_page_context = context
            return context
        setattr(cls, context_func_name, _get_context_data_superfunc)

    def render_flat_page(self, page):
        if not page:
            return ''
        from django.template import engines
        template = engines.all()[0].from_string(page['content'])
        return template.render(
            getattr(self, '_flat_page_context', render_flat_page._view_context),
            self.request)
    cls.render_flat_page = render_flat_page
    cls.render_flat_page._view_context = {}

    return cls


class UsernameFormMixin(object):
    """
    A form mixin that performs a case-insensitive uniqueness validation of the
    provided username value on form submit.
    """
    username_error_messages = {
        # We do not want to disclose the exact usernames in the system through
        # the error messages, and thus facilitate user enumeration attacks...
        'unique': _("A user with a similar username already exists."),
        # Clearly spell out to the potential new users what a valid username is.
        'invalid': mark_safe(_(
            "Enter a username conforming to these rules: "
            " This value may contain only letters, numbers, and the symbols"
            " <kbd>@</kbd> <kbd>.</kbd> <kbd>+</kbd> <kbd>-</kbd> <kbd>_</kbd>."
            " Spaces are not allowed."
        )),
        # Indicate what are the limitations in terms of number of characters.
        'max_length': _(
            "Ensure that this value has at most %(limit_value)d characters "
            "(it has now %(show_value)d)."
        ),
    }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # Stores the value before the change.
        self.previous_uname = self.instance.username

    def clean_username(self):
        """
        Ensure that the username provided is unique (in a case-insensitive manner).
        This check replaces the Django's built-in uniqueness verification.
        """
        username = self.cleaned_data['username']
        if username == self.previous_uname:
            return username
        threshold = 1 if username.lower() != self.previous_uname.lower() else 2
        if User.objects.filter(username__iexact=username).count() >= threshold:
            raise ValidationError(self._meta.error_messages['username']['unique'])
        return username


class SystemEmailFormMixin(object):
    """
    A form mixin that performs a case-insensitive uniqueness validation of the
    provided email address value on form submit. Both valid and invalid existing
    emails in the database are taken into account.
    """
    email_error_messages = {
        'max_length': _(
            "Ensure that this value has at most %(limit_value)d characters "
            "(it has now %(show_value)d)."
        ),
    }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # Stores the value before the change.
        self.previous_email = value_without_invalid_marker(self.instance.email)

    def clean_email(self):
        """
        Ensure that the email address provided is unique (in a case-insensitive manner).
        """
        email_value = self.cleaned_data['email']
        if not email_value:
            raise ValidationError(_("Enter a valid email address."))
        invalid_email = '{}{}'.format(settings.INVALID_PREFIX, email_value)
        emails_lookup = Q(email_lc=email_value.lower()) | Q(email_lc=invalid_email.lower())
        if email_value and email_value.lower() != self.previous_email.lower() \
                and User.objects.annotate(email_lc=Lower('email')).filter(emails_lookup).exists():
            raise ValidationError(_("User address already in use."))
        return email_value


class PasswordFormMixin(object):
    """
    A form mixin adding a functionality of verifying (anonymously) whether
    the submitted password value has not been compromised, via the HIBP's
    Pwned Passwords service.
    """

    def analyze_password(self, password_field_value):
        insecure, howmuch = is_password_compromised(password_field_value)

        if insecure and howmuch > 99:
            self.add_error(NON_FIELD_ERRORS, ValidationError(_(
                "The password selected by you is too insecure. "
                "Such combination of characters is very well-known to cyber-criminals."),
                code='compromised_password'))
            self.add_error(self.analyze_password_field, _("Choose a less easily guessable password."))
        elif insecure and howmuch > 1:
            self.add_error(NON_FIELD_ERRORS, ValidationError(_(
                "The password selected by you is not very secure. "
                "Such combination of characters is known to cyber-criminals."),
                code='compromised_password'))
            self.add_error(self.analyze_password_field, _("Choose a less easily guessable password."))

        if insecure:
            auth_log.warning(
                "Password with HIBP count {:d} selected in {}.".format(howmuch, self.__class__.__name__),
                extra={'request': self.view_request} if hasattr(self, 'view_request') else None,
            )

    def clean(self):
        cleaned_data = super().clean()
        if self.analyze_password_field in cleaned_data:
            self.analyze_password(cleaned_data[self.analyze_password_field])
        return cleaned_data