# -*- coding: utf-8 -*-

""" License

    Copyright (C) 2018 YunoHost

    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 sys
import os
import json
import string
import subprocess

SMALL_PWD_LIST = [
    "yunohost",
    "olinuxino",
    "olinux",
    "raspberry",
    "admin",
    "root",
    "test",
    "rpi",
]

MOST_USED_PASSWORDS = "/usr/share/yunohost/100000-most-used-passwords.txt"

# Length, digits, lowers, uppers, others
STRENGTH_LEVELS = [
    (8, 0, 0, 0, 0),
    (8, 1, 1, 1, 0),
    (8, 1, 1, 1, 1),
    (12, 1, 1, 1, 1),
]


def assert_password_is_compatible(password):
    """
    UNIX seems to not like password longer than 127 chars ...
    e.g. SSH login gets broken (or even 'su admin' when entering the password)
    """

    if len(password) >= 127:

        # Note that those imports are made here and can't be put
        # on top (at least not the moulinette ones)
        # because the moulinette needs to be correctly initialized
        # as well as modules available in python's path.
        from yunohost.utils.error import YunohostValidationError

        raise YunohostValidationError("admin_password_too_long")


def assert_password_is_strong_enough(profile, password):

    PasswordValidator(profile).validate(password)


class PasswordValidator:
    def __init__(self, profile):
        """
        Initialize a password validator.

        The profile shall be either "user" or "admin"
        and will correspond to a validation strength
        defined via the setting "security.password.<profile>.strength"
        """

        self.profile = profile
        try:
            # We do this "manually" instead of using settings_get()
            # from settings.py because this file is also meant to be
            # use as a script by ssowat.
            # (or at least that's my understanding -- Alex)
            settings = json.load(open("/etc/yunohost/settings.json", "r"))
            setting_key = "security.password." + profile + ".strength"
            self.validation_strength = int(settings[setting_key]["value"])
        except Exception:
            # Fallback to default value if we can't fetch settings for some reason
            self.validation_strength = 1

    def validate(self, password):
        """
        Check the validation_summary and trigger an exception
        if the password does not pass tests.

        This method is meant to be used from inside YunoHost's code
        (compared to validation_summary which is meant to be called
        by ssowat)
        """
        if self.validation_strength == -1:
            return

        # Note that those imports are made here and can't be put
        # on top (at least not the moulinette ones)
        # because the moulinette needs to be correctly initialized
        # as well as modules available in python's path.
        from yunohost.utils.error import YunohostValidationError

        status, msg = self.validation_summary(password)
        if status == "error":
            raise YunohostValidationError(msg)

    def validation_summary(self, password):
        """
        Check if a password is listed in the list of most used password
        and if the overall strength is good enough compared to the
        validation_strength defined in the constructor.

        Produces a summary-tuple comprised of a level (succes or error)
        and a message key describing the issues found.
        """
        if self.validation_strength < 0:
            return ("success", "")

        listed = password in SMALL_PWD_LIST or self.is_in_most_used_list(password)
        strength_level = self.strength_level(password)
        if listed:
            # i18n: password_listed
            return ("error", "password_listed")
        if strength_level < self.validation_strength:
            # i18n: password_too_simple_1
            # i18n: password_too_simple_2
            # i18n: password_too_simple_3
            # i18n: password_too_simple_4
            return ("error", "password_too_simple_%s" % self.validation_strength)

        return ("success", "")

    def strength(self, password):
        """
        Returns the strength of a password, defined as a tuple
        containing the length of the password, the number of digits,
        lowercase letters, uppercase letters, and other characters.

        For instance, "PikachuDu67" is (11, 2, 7, 2, 0)
        """

        length = len(password)
        digits = 0
        uppers = 0
        lowers = 0
        others = 0

        for character in password:
            if character in string.digits:
                digits = digits + 1
            elif character in string.ascii_uppercase:
                uppers = uppers + 1
            elif character in string.ascii_lowercase:
                lowers = lowers + 1
            else:
                others = others + 1

        return (length, digits, lowers, uppers, others)

    def strength_level(self, password):
        """
        Computes the strength of a password and compares
        it to the STRENGTH_LEVELS.

        Returns an int corresponding to the highest STRENGTH_LEVEL
        satisfied by the password.
        """

        strength = self.strength(password)

        strength_level = 0
        # Iterate over each level and its criterias
        for level, level_criterias in enumerate(STRENGTH_LEVELS):
            # Iterate simulatenously over the level criterias (e.g. [8, 1, 1, 1, 0])
            # and the strength of the password (e.g. [11, 2, 7, 2, 0])
            # and compare the values 1-by-1.
            # If one False is found, the password does not satisfy the level
            if False in [s >= c for s, c in zip(strength, level_criterias)]:
                break
            # Otherwise, the strength of the password is at least of the current level.
            strength_level = level + 1

        return strength_level

    def is_in_most_used_list(self, password):

        # Decompress file if compressed
        if os.path.exists("%s.gz" % MOST_USED_PASSWORDS):
            os.system("gzip -fd %s.gz" % MOST_USED_PASSWORDS)

        # Grep the password in the file
        # We use '-f -' to feed the pattern (= the password) through
        # stdin to avoid it being shown in ps -ef --forest...
        command = "grep -q -F -f - %s" % MOST_USED_PASSWORDS
        p = subprocess.Popen(command.split(), stdin=subprocess.PIPE)
        p.communicate(input=password.encode("utf-8"))
        return not bool(p.returncode)


# This file is also meant to be used as an executable by
# SSOwat to validate password from the portal when an user
# change its password.
if __name__ == "__main__":
    if len(sys.argv) < 2:
        import getpass

        pwd = getpass.getpass("")
        # print("usage: password.py PASSWORD")
    else:
        pwd = sys.argv[1]
    status, msg = PasswordValidator("user").validation_summary(pwd)
    print(msg)
    sys.exit(0)