#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

import collections
import io
import random as random_module
import string


# NOTE(pas-ha) Heat officially supports only POSIX::Linux platform
# where os.urandom() and random.SystemRandom() are available
random = random_module.SystemRandom()


CHARACTER_CLASSES = (
    LETTERS_DIGITS, LETTERS, LOWERCASE, UPPERCASE,
    DIGITS, HEXDIGITS, OCTDIGITS,
) = (
    'lettersdigits', 'letters', 'lowercase', 'uppercase',
    'digits', 'hexdigits', 'octdigits',
)

_char_class_members = {
    LETTERS_DIGITS: string.ascii_letters + string.digits,
    LETTERS: string.ascii_letters,
    LOWERCASE: string.ascii_lowercase,
    UPPERCASE: string.ascii_uppercase,
    DIGITS: string.digits,
    HEXDIGITS: string.digits + 'ABCDEF',
    OCTDIGITS: string.octdigits,
}


CharClass = collections.namedtuple('CharClass',
                                   ('allowed_chars', 'min_count'))


def named_char_class(char_class, min_count=0):
    """Return a predefined character class.

    The result of this function can be passed to :func:`generate_password` as
    one of the character classes to use in generating a password.

    :param char_class: Any of the character classes named in
                       :const:`CHARACTER_CLASSES`
    :param min_count: The minimum number of members of this class to appear in
                      a generated password
    """
    assert char_class in CHARACTER_CLASSES
    return CharClass(frozenset(_char_class_members[char_class]), min_count)


def special_char_class(allowed_chars, min_count=0):
    """Return a character class containing custom characters.

    The result of this function can be passed to :func:`generate_password` as
    one of the character classes to use in generating a password.

    :param allowed_chars: Iterable of the characters in the character class
    :param min_count: The minimum number of members of this class to appear in
                      a generated password
    """
    return CharClass(frozenset(allowed_chars), min_count)


def generate_password(length, char_classes):
    """Generate a random password.

    The password will be of the specified length, and comprised of characters
    from the specified character classes, which can be generated using the
    :func:`named_char_class` and :func:`special_char_class` functions. Where
    a minimum count is specified in the character class, at least that number
    of characters in the resulting password are guaranteed to be from that
    character class.

    :param length: The length of the password to generate, in characters
    :param char_classes: Iterable over classes of characters from which to
                         generate a password
    """
    char_buffer = io.StringIO()
    all_allowed_chars = set()

    # Add the minimum number of chars from each char class
    for char_class in char_classes:
        all_allowed_chars |= char_class.allowed_chars
        allowed_chars = tuple(char_class.allowed_chars)
        for i in range(char_class.min_count):
            char_buffer.write(random.choice(allowed_chars))

    # Fill up rest with random chars from provided classes
    combined_chars = tuple(all_allowed_chars)
    for i in range(max(0, length - char_buffer.tell())):
        char_buffer.write(random.choice(combined_chars))

    # Shuffle string
    selected_chars = char_buffer.getvalue()
    char_buffer.close()
    return ''.join(random.sample(selected_chars, length))


def generate_openstack_password():
    """Generate a random password suitable for a Keystone User."""
    return generate_password(32, [named_char_class(LOWERCASE, 1),
                                  named_char_class(UPPERCASE, 1),
                                  named_char_class(DIGITS, 1),
                                  special_char_class('!@#%^&*', 1)])
