#
# input_checking.py : input & password/passphrase input checking
#
# Copyright (C) 2013, 2017  Red Hat, Inc.
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions of
# the GNU General Public License v.2, or (at your option) any later version.
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY expressed or implied, including the implied warranties of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
# Public License for more details.  You should have received a copy of the
# GNU General Public License along with this program; if not, write to the
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
# source code or documentation are not subject to the GNU General Public
# License and may only be used or replicated with the express permission of
# Red Hat, Inc.
#

import pwquality

from pyanaconda.core.signal import Signal
from pyanaconda.core.i18n import _
from pyanaconda.core import constants, regexes
from pyanaconda.core import users
from pyanaconda.anaconda_loggers import get_module_logger
from pyanaconda.modules.common.constants.objects import USER_INTERFACE
from pyanaconda.modules.common.constants.services import BOSS
from pyanaconda.modules.common.structures.policy import PasswordPolicy

log = get_module_logger(__name__)


def get_policy(policy_name) -> PasswordPolicy:
    """Get the password policy data.

    :param policy_name: a name of the policy
    :return: a password policy data
    """
    proxy = BOSS.get_proxy(USER_INTERFACE)
    policies = PasswordPolicy.from_structure_dict(proxy.PasswordPolicies)

    if policy_name in policies:
        return policies[policy_name]

    return PasswordPolicy.from_defaults(policy_name)


class PwqualitySettingsCache(object):
    """Cache for libpwquality settings used for password validation.

    Libpwquality settings instantiation is probably not exactly cheap
    and we might need the settings for checking every password (even when
    it is being typed by the user) so it makes sense to cache the objects
    for reuse. As there might be multiple active policies for different
    passwords we need to be able to cache multiple policies based on
    minimum password length, as we don't input anything else to libpwquality
    than minimum password length and the password itself.
    """
    def __init__(self):
        self._pwq_settings = {}

    def get_settings_by_minlen(self, minlen):
        settings = self._pwq_settings.get(minlen)
        if settings is None:
            settings = pwquality.PWQSettings()
            settings.read_config()
            settings.minlen = minlen
            self._pwq_settings[minlen] = settings
        return settings


pwquality_settings_cache = PwqualitySettingsCache()


class PasswordCheckRequest(object):
    """A wrapper for a password check request.

    This in general means the password to be checked as well as its validation criteria
    such as minimum length, if it can be empty, etc.
    """

    def __init__(self):
        self._password = ""
        self._password_confirmation = ""
        self._policy = None
        self._pwquality_settings = None
        self._username = "root"
        self._fullname = ""
        self._secret_type = constants.SecretType.PASSWORD

    @property
    def password(self):
        """Password string to be checked.

        :returns: password string for the check
        :rtype: str
        """
        return self._password

    @password.setter
    def password(self, new_password):
        self._password = new_password

    @property
    def password_confirmation(self):
        """Content of the password confirmation field.

        :returns: password confirmation string
        :rtype: str
        """
        return self._password_confirmation

    @password_confirmation.setter
    def password_confirmation(self, new_password_confirmation):
        self._password_confirmation = new_password_confirmation

    @property
    def policy(self):
        """Password quality policy.

        :returns: password quality policy
        """
        return self._policy

    @policy.setter
    def policy(self, new_policy):
        self._policy = new_policy

    @property
    def pwquality_settings(self):
        """Settings for libpwquality (if any).

        :returns: libpwquality settings
        :rtype: pwquality settings object or None
        """
        if not self._pwquality_settings:
            self._pwquality_settings = pwquality_settings_cache.get_settings_by_minlen(
                self.policy.min_length
            )
        return self._pwquality_settings

    @property
    def username(self):
        """The username for which the password is being set.

        If no username is provided, "root" will be used.
        Use username=None to disable the username check.

        :returns: username corresponding to the password
        :rtype: str or None
        """
        return self._username

    @username.setter
    def username(self, new_username):
        self._username = new_username

    @property
    def fullname(self):
        """The full name of the user for which the password is being set.

        If no full name is provided, "root" will be used.

        :returns: full user name corresponding to the password
        :rtype: str or None
        """
        return self._fullname

    @fullname.setter
    def fullname(self, new_fullname):
        self._fullname = new_fullname

    @property
    def secret_type(self):
        """Type of secret that is being checked.

        At the moment is either a password or a passphrase and this property decides how it will be
        called in error and status messages.

        :returns: secret type
        :rtype: Enum
        """
        return self._secret_type

    @secret_type.setter
    def secret_type(self, new_type):
        """Set type of secret being checked.

        :param Enum new_type: new secret type
        """
        # Prevent uknown/unsuported secret type from being set, so that
        # this does no blow up later on once we try to access the status/error message
        # corresponding to the secret type.
        if new_type not in constants.SecretType:
            raise RuntimeError("Unknown secret type: {}".format(new_type))
        self._secret_type = new_type


class CheckResult(object):
    """Result of an input check."""

    def __init__(self):
        self._success = False
        self._error_message = ""
        self.error_message_changed = Signal()

    @property
    def success(self):
        return self._success

    @success.setter
    def success(self, value):
        self._success = value

    @property
    def error_message(self):
        """Optional error message describing why the input is not valid.

        :returns: why the input is bad (provided it is bad) or None
        :rtype: str or None
        """
        return self._error_message

    @error_message.setter
    def error_message(self, new_error_message):
        self._error_message = new_error_message
        self.error_message_changed.emit(new_error_message)


class PasswordValidityCheckResult(CheckResult):
    """A wrapper for results for a password check."""

    def __init__(self):
        super().__init__()
        self._password_score = 0
        self.password_score_changed = Signal()
        self._status_text = ""
        self.status_text_changed = Signal()
        self._password_quality = 0
        self.password_quality_changed = Signal()
        self._length_ok = False
        self.length_ok_changed = Signal()

    @property
    def password_score(self):
        """A high-level integer score indicating password quality.

        Goes from 0 (invalid password) to 4 (valid & very strong password).
        Mainly used to drive the password quality indicator in the GUI.
        """
        return self._password_score

    @password_score.setter
    def password_score(self, new_score):
        self._password_score = new_score
        self.password_score_changed.emit(new_score)

    @property
    def status_text(self):
        """A short overall status message describing the password.

        Generally something like "Good.", "Too short.", "Empty.", etc.

        :rtype: short status message
        :rtype: str
        """
        return self._status_text

    @status_text.setter
    def status_text(self, new_status_text):
        self._status_text = new_status_text
        self.status_text_changed.emit(new_status_text)

    @property
    def password_quality(self):
        """More fine grained integer indicator describing password strength.

        This basically exports the quality score assigned by libpwquality to the password,
        which goes from 0 (unacceptable password) to 100 (strong password).

        Note of caution though about using the password quality value - it is intended
        mainly for on-line password strength hints, not for long-term stability,
        even just because password dictionary updates and other peculiarities of password
        strength judging.

        :returns: password quality value as reported by libpwquality
        :rtype: int
        """
        return self._password_quality

    @password_quality.setter
    def password_quality(self, value):
        self._password_quality = value
        self.password_quality_changed.emit(value)

    @property
    def length_ok(self):
        """Reports if the password is long enough.

        :returns: if the password is long enough
        :rtype: bool
        """
        return self._length_ok

    @length_ok.setter
    def length_ok(self, value):
        self._length_ok = value
        self.length_ok_changed.emit(value)


class InputCheck(object):
    """Input checking base class."""

    def __init__(self):
        self._result = CheckResult()
        self._skip = False

    @property
    def result(self):
        return self._result

    @property
    def skip(self):
        """A flag hinting if this check should be skipped."""
        return self._skip

    @skip.setter
    def skip(self, value):
        # Checks flagged as skipped are
        # considered successful.
        # Otherwise old state will linger even if the
        # check is skipped during checking runs.
        if value:
            self.result.error_message = ""
            self.result.success = True
        self._skip = value

    def run(self, check_request):
        """Run the check.

        :param check_request: arbitrary input data to be processed

        Subclasses need to always implement this.
        """
        raise NotImplementedError


class PasswordValidityCheck(InputCheck):
    """Check the validity and quality of a password."""

    def __init__(self):
        super().__init__()
        self._result = PasswordValidityCheckResult()

    def run(self, check_request):
        """Check the validity and quality of a password.

           This is how password quality checking works:
           - starts with a password and an optional parameters
           - will report if this password can be used at all (score >0)
           - will report how strong the password approximately is on a scale of 1-100
           - if the password is unusable it will be reported why

           This function uses libpwquality to check the password strength.
           Pwquality will raise a PWQError on a weak password but this function does
           not pass that forward.

           If the password fails the PWQSettings conditions, the score will be set to 0
           and the resulting error message will contain the reason why the password is bad.

           :param check_request: a password check request wrapper
           :type check_request: a PasswordCheckRequest instance
           :returns: a password check result wrapper
           :rtype: a PasswordCheckResult instance
        """

        length_ok = False
        error_message = ""
        pw_quality = 0
        try:
            # lets run the password through libpwquality
            pw_quality = check_request.pwquality_settings.check(check_request.password, None, check_request.username)
        except pwquality.PWQError as e:
            # Leave valid alone here: the password is weak but can still
            # be accepted.
            # PWQError values are built as a tuple of (int, str)
            error_message = e.args[1]

        if check_request.policy.allow_empty and not check_request.password:
            # if we are OK with empty passwords, then empty passwords are also fine length wise
            length_ok = True
        else:
            length_ok = len(check_request.password) >= check_request.policy.min_length

        if not check_request.password:
            if check_request.policy.allow_empty:
                pw_score = 1
            else:
                pw_score = 0
            status_text = _(constants.SecretStatus.EMPTY.value)
        elif not length_ok:
            pw_score = 0
            status_text = _(constants.SecretStatus.TOO_SHORT.value)
            # If the password is too short replace the libpwquality error
            # message with a generic "password is too short" message.
            # This is because the error messages returned by libpwquality
            # for short passwords don't make much sense.
            error_message = _(constants.SECRET_TOO_SHORT[check_request.secret_type])
        elif error_message:
            pw_score = 1
            status_text = _(constants.SecretStatus.WEAK.value)
        elif pw_quality < 30:
            pw_score = 2
            status_text = _(constants.SecretStatus.FAIR.value)
        elif pw_quality < 70:
            pw_score = 3
            status_text = _(constants.SecretStatus.GOOD.value)
        else:
            pw_score = 4
            status_text = _(constants.SecretStatus.STRONG.value)

        # the policy influences the overall success of the check
        # - score 0 & strict == True -> success = False
        # - score 0 & strict == False -> success = True
        success = not error_message

        # set the result now so that the *_changed signals fire only once the check is done
        self.result.success = success  # pylint: disable=attribute-defined-outside-init
        self.result.password_score = pw_score  # pylint: disable=attribute-defined-outside-init
        self.result.status_text = status_text  # pylint: disable=attribute-defined-outside-init
        self.result.password_quality = pw_quality  # pylint: disable=attribute-defined-outside-init
        self.result.error_message = error_message  # pylint: disable=attribute-defined-outside-init
        self.result.length_ok = length_ok  # pylint: disable=attribute-defined-outside-init


class PasswordConfirmationCheck(InputCheck):
    """Check if the password & password confirmation match."""

    def __init__(self):
        super().__init__()
        self._success_if_confirmation_empty = False

    @property
    def success_if_confirmation_empty(self):
        """Enables success-if-confirmation-empty mode.

        This property can be used to tell the check to report success if the confirmation filed is empty,
        which is a paradigm used by Anaconda uses for two things:
        - to make it possible for users to exit without setting a valid password
        - to make it possible to exit the spoke if only the password is set
          but confirmation is empty
        """
        return self._success_if_confirmation_empty

    @success_if_confirmation_empty.setter
    def success_if_confirmation_empty(self, value):
        self._success_if_confirmation_empty = value

    def run(self, check_request):
        """If the user has entered confirmation data, check whether it matches the password."""
        if self.success_if_confirmation_empty and not check_request.password_confirmation:
            self.result.error_message = ""
            self.result.success = True
        elif check_request.password != check_request.password_confirmation:
            self.result.error_message = _(constants.SECRET_CONFIRM_ERROR_GUI[check_request.secret_type])
            self.result.success = False
        else:
            self.result.error_message = ""
            self.result.success = True


class PasswordASCIICheck(InputCheck):
    """Check if the password contains non-ASCII characters.

    Non-ASCII characters might be hard to type in the console and in the LUKS volume unlocking
    screen, so check if the password contains them so we can warn the user.
    """

    def run(self, check_request):
        """Fail if the password contains non-ASCII characters."""
        has_non_ASCII = any(char not in constants.PW_ASCII_CHARS for char in check_request.password)
        if check_request.password and has_non_ASCII:
            self.result.error_message = _(constants.SECRET_ASCII[check_request.secret_type])
            self.result.success = False
        else:
            self.result.error_message = ""
            self.result.success = True


class PasswordEmptyCheck(InputCheck):
    """Check if the password is set."""

    def run(self, check_request):
        """Check whether a password has been specified at all."""
        if check_request.password:
            # password set is always success
            self.result.error_message = ""
            self.result.success = True
        else:
            # otherwise empty password is an error
            self.result.error_message = _(constants.SECRET_EMPTY_ERROR[check_request.secret_type])
            self.result.success = False


class UsernameCheck(InputCheck):
    """Check if the username is valid."""

    def __init__(self):
        super().__init__()
        self._success_if_username_empty = False

    @property
    def success_if_username_empty(self):
        """Should empty username be considered a success ?"""
        return self._success_if_username_empty

    @success_if_username_empty.setter
    def success_if_username_empty(self, value):
        self._success_if_username_empty = value

    def run(self, check_request):
        """Check if the username is valid."""
        # in some cases an empty username is also considered valid,
        # so that the user can exit the User spoke without filling it in
        if self.success_if_username_empty and not check_request.username:
            self.result.error_message = ""
            self.result.success = True
        else:
            success, error_message = users.check_username(check_request.username)
            self.result.error_message = error_message
            self.result.success = success


class FullnameCheck(InputCheck):
    """Check if the full user name is valid.

    Most importantly the full user name cannot contain colons.
    """

    def run(self, check_request):
        """Check if the full user name is valid."""
        if regexes.GECOS_VALID.match(check_request.fullname):
            self.result.error_message = ""
            self.result.success = True
        else:
            self.result.error_message = _("Full name cannot contain colon characters")
            self.result.success = False


class InputField(object):
    """An input field containing data to be checked.

    The input field can have an initial value that can be
    monitored for change via signals.
    """

    def __init__(self, initial_content):
        self._initial_content = initial_content
        self._content = initial_content
        self.changed = Signal()
        self._initial_change_signal_fired = False
        self.changed_from_initial_state = Signal()

    @property
    def content(self):
        return self._content

    @content.setter
    def content(self, new_content):
        old_content = self._content
        self._content = new_content
        # check if the input changed from the initial state
        if old_content != new_content:
            self.changed.emit()
            # also fire the changed-from-initial-state signal if required
            if not self._initial_change_signal_fired and new_content != self._initial_content:
                self.changed_from_initial_state.emit()
                self._initial_change_signal_fired = True


class PasswordChecker(object):
    """Run multiple password and input checks in a given order and report the results.

    All added checks (in insertion order) will be run and results returned as error message
    and success value (True/False). If any check fails success will be False and the
    error message of the first check to fail will be returned.

    It's also possible to mark individual checks to be skipped by setting their skip property to True.
    Such check will be skipped during the checking run.
    """

    def __init__(self, initial_password_content, initial_password_confirmation_content,
                 policy_name):
        self._password = InputField(initial_password_content)
        self._password_confirmation = InputField(initial_password_confirmation_content)
        self._checks = []
        self._success = False
        self._error_message = ""
        self._failed_checks = []
        self._policy_name = policy_name
        self._username = None
        self._fullname = ""
        self._secret_type = constants.SecretType.PASSWORD
        # connect to the password field signals
        self.password.changed.connect(self.run_checks)
        self.password_confirmation.changed.connect(self.run_checks)

        # signals
        self.checks_done = Signal()

    @property
    def password(self):
        """Main password field."""
        return self._password

    @property
    def password_confirmation(self):
        """Password confirmation field."""
        return self._password_confirmation

    @property
    def checks(self):
        return self._checks

    @property
    def success(self):
        return self._success

    @property
    def error_message(self):
        return self._error_message

    @property
    def failed_checks(self):
        """List of checks failed during the last checking run.

        If no checks have failed the list will be empty.

        :returns: list of failed checks (if any)
        :rtype: list
        """
        return self._failed_checks

    @property
    def policy(self):
        return get_policy(self._policy_name)

    @property
    def username(self):
        return self._username

    @username.setter
    def username(self, new_username):
        self._username = new_username

    @property
    def fullname(self):
        """The full name of the user for which the password is being set.

        If no full name is provided, "root" will be used.

        :returns: full user name corresponding to the password
        :rtype: str or None
        """
        return self._fullname

    @fullname.setter
    def fullname(self, new_fullname):
        self._fullname = new_fullname

    @property
    def secret_type(self):
        """Type of secret that is being checked.

        At the moment is either a password or a passphrase and this property decides how it will be
        called in error and status messages.

        :returns: secret type
        :rtype: Enum
        """
        return self._secret_type

    @secret_type.setter
    def secret_type(self, new_type):
        """Set type of secret being checked.

        :param Enum new_type: new secret type
        """
        # Prevent uknown/unsuported secret type from being set, so that
        # this does no blow up later on once we try to access the status/error message
        # corresponding to the secret type.
        if new_type not in constants.SecretType:
            raise RuntimeError("Unknown secret type: {}".format(new_type))
        self._secret_type = new_type

    def add_check(self, check_instance):
        """Add check instance to list of checks."""
        self._checks.append(check_instance)

    def run_checks(self):
        # first we need to prepare a check request instance
        check_request = PasswordCheckRequest()
        check_request.password = self.password.content
        check_request.password_confirmation = self.password_confirmation.content
        check_request.policy = self.policy
        check_request.username = self.username
        check_request.fullname = self.fullname
        check_request.secret_type = self.secret_type

        # reset the list of failed checks
        self._failed_checks = []

        error_message = ""
        for check in self.checks:
            if not check.skip:
                check.run(check_request)
                if not check.result.success:
                    if not self.failed_checks:
                        # a check failed:
                        # - remember that & it's error message
                        # - run other checks as well and ignore their error messages (if any)
                        # - fail the overall check run (success = False)
                        error_message = check.result.error_message
                    self._failed_checks.append(check)
        if self.failed_checks:
            self._error_message = error_message
            self._success = False
        else:
            self._success = True
            self._error_message = ""
        # trigger the success changed signal
        self.checks_done.emit(self._error_message)
