# authentic2 - versatile identity manager
# Copyright (C) 2010-2019 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import abc
import random
import re
import string

from django.core.exceptions import ValidationError
from django.utils.functional import lazy
from django.utils.module_loading import import_string
from django.utils.translation import ugettext as _

from . import app_settings


def generate_password():
    """Generate a password that validates current password policy.

    Beware that A2_PASSWORD_POLICY_REGEX cannot be validated.
    """
    digits = string.digits
    lower = string.ascii_lowercase
    upper = string.ascii_uppercase
    punc = string.punctuation

    min_len = max(app_settings.A2_PASSWORD_POLICY_MIN_LENGTH, 8)
    min_class_count = max(app_settings.A2_PASSWORD_POLICY_MIN_CLASSES, 3)
    new_password = []

    generator = random.SystemRandom()
    while len(new_password) < min_len:
        for cls in (digits, lower, upper, punc)[:min_class_count]:
            new_password.append(generator.choice(cls))
    generator.shuffle(new_password)
    return ''.join(new_password)


class PasswordChecker(metaclass=abc.ABCMeta):
    class Check:
        def __init__(self, label, result):
            self.label = label
            self.result = result

    def __init__(self, *args, **kwargs):
        pass

    @abc.abstractmethod
    def __call__(self, password, **kwargs):
        """Return an iterable of Check objects giving the list of checks and
        their result."""
        return []


class DefaultPasswordChecker(PasswordChecker):
    @property
    def min_length(self):
        return app_settings.A2_PASSWORD_POLICY_MIN_LENGTH

    @property
    def at_least_one_lowercase(self):
        return app_settings.A2_PASSWORD_POLICY_MIN_CLASSES > 0

    @property
    def at_least_one_digit(self):
        return app_settings.A2_PASSWORD_POLICY_MIN_CLASSES > 1

    @property
    def at_least_one_uppercase(self):
        return app_settings.A2_PASSWORD_POLICY_MIN_CLASSES > 2

    @property
    def regexp(self):
        return app_settings.A2_PASSWORD_POLICY_REGEX

    @property
    def regexp_label(self):
        return app_settings.A2_PASSWORD_POLICY_REGEX_ERROR_MSG

    def __call__(self, password, **kwargs):
        if self.min_length:
            yield self.Check(
                result=len(password) >= self.min_length, label=_('%s characters') % self.min_length
            )

        if self.at_least_one_lowercase:
            yield self.Check(result=any(c.islower() for c in password), label=_('1 lowercase letter'))

        if self.at_least_one_digit:
            yield self.Check(result=any(c.isdigit() for c in password), label=_('1 digit'))

        if self.at_least_one_uppercase:
            yield self.Check(result=any(c.isupper() for c in password), label=_('1 uppercase letter'))

        if self.regexp and self.regexp_label:
            yield self.Check(result=bool(re.match(self.regexp, password)), label=self.regexp_label)


def get_password_checker(*args, **kwargs):
    return import_string(app_settings.A2_PASSWORD_POLICY_CLASS)(*args, **kwargs)


def validate_password(password):
    error = password_help_text(password, only_errors=True)
    if error:
        raise ValidationError(_('This password is not accepted.'))


def password_help_text(password='', only_errors=False):
    password_checker = get_password_checker()
    criteria = [check.label for check in password_checker(password) if not (only_errors and check.result)]
    if criteria:
        html_criteria = ['<span class="a2-password-policy-rule">%s</span>' % criter for criter in criteria]
        return _(
            'In order to create a secure password, please use at least: '
            '<span class="a2-password-policy-container">%s</span>'
        ) % (''.join(html_criteria))
    else:
        return ''


password_help_text = lazy(password_help_text, str)
