#
#    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 random
import string

import six

from heat.common import exception
from heat.common.i18n import _
from heat.engine import attributes
from heat.engine import constraints
from heat.engine import properties
from heat.engine import resource
from heat.engine import support
from heat.engine import translation


class RandomString(resource.Resource):
    """A resource which generates a random string.

    This is useful for configuring passwords and secrets on services. Random
    string can be generated from specified character sequences, which means
    that all characters will be randomly chosen from specified sequences, or
    with some classes, e.g. letterdigits, which means that all character will
    be randomly chosen from union of ascii letters and digits. Output string
    will be randomly generated string with specified length (or with length of
    32, if length property doesn't specified).
    """

    support_status = support.SupportStatus(version='2014.1')

    PROPERTIES = (
        LENGTH, SEQUENCE, CHARACTER_CLASSES, CHARACTER_SEQUENCES,
        SALT,
    ) = (
        'length', 'sequence', 'character_classes', 'character_sequences',
        'salt',
    )

    _CHARACTER_CLASSES_KEYS = (
        CHARACTER_CLASSES_CLASS, CHARACTER_CLASSES_MIN,
    ) = (
        'class', 'min',
    )

    _CHARACTER_SEQUENCES = (
        CHARACTER_SEQUENCES_SEQUENCE, CHARACTER_SEQUENCES_MIN,
    ) = (
        'sequence', 'min',
    )

    ATTRIBUTES = (
        VALUE,
    ) = (
        'value',
    )

    properties_schema = {
        LENGTH: properties.Schema(
            properties.Schema.INTEGER,
            _('Length of the string to generate.'),
            default=32,
            constraints=[
                constraints.Range(1, 512),
            ]
        ),
        SEQUENCE: properties.Schema(
            properties.Schema.STRING,
            _('Sequence of characters to build the random string from.'),
            constraints=[
                constraints.AllowedValues(['lettersdigits', 'letters',
                                           'lowercase', 'uppercase',
                                           'digits', 'hexdigits',
                                           'octdigits']),
            ],
            support_status=support.SupportStatus(
                status=support.HIDDEN,
                version='5.0.0',
                previous_status=support.SupportStatus(
                    status=support.DEPRECATED,
                    message=_('Use property %s.') % CHARACTER_CLASSES,
                    version='2014.2'
                )
            )
        ),
        CHARACTER_CLASSES: properties.Schema(
            properties.Schema.LIST,
            _('A list of character class and their constraints to generate '
              'the random string from.'),
            schema=properties.Schema(
                properties.Schema.MAP,
                schema={
                    CHARACTER_CLASSES_CLASS: properties.Schema(
                        properties.Schema.STRING,
                        (_('A character class and its corresponding %(min)s '
                           'constraint to generate the random string from.')
                         % {'min': CHARACTER_CLASSES_MIN}),
                        constraints=[
                            constraints.AllowedValues(
                                ['lettersdigits', 'letters', 'lowercase',
                                 'uppercase', 'digits', 'hexdigits',
                                 'octdigits']),
                        ],
                        default='lettersdigits'),
                    CHARACTER_CLASSES_MIN: properties.Schema(
                        properties.Schema.INTEGER,
                        _('The minimum number of characters from this '
                          'character class that will be in the generated '
                          'string.'),
                        default=1,
                        constraints=[
                            constraints.Range(1, 512),
                        ]
                    )
                }
            ),
            # add defaults for backward compatibility
            default=[{CHARACTER_CLASSES_CLASS: 'lettersdigits',
                      CHARACTER_CLASSES_MIN: 1}]

        ),
        CHARACTER_SEQUENCES: properties.Schema(
            properties.Schema.LIST,
            _('A list of character sequences and their constraints to '
              'generate the random string from.'),
            schema=properties.Schema(
                properties.Schema.MAP,
                schema={
                    CHARACTER_SEQUENCES_SEQUENCE: properties.Schema(
                        properties.Schema.STRING,
                        _('A character sequence and its corresponding %(min)s '
                          'constraint to generate the random string '
                          'from.') % {'min': CHARACTER_SEQUENCES_MIN},
                        required=True),
                    CHARACTER_SEQUENCES_MIN: properties.Schema(
                        properties.Schema.INTEGER,
                        _('The minimum number of characters from this '
                          'sequence that will be in the generated '
                          'string.'),
                        default=1,
                        constraints=[
                            constraints.Range(1, 512),
                        ]
                    )
                }
            )
        ),
        SALT: properties.Schema(
            properties.Schema.STRING,
            _('Value which can be set or changed on stack update to trigger '
              'the resource for replacement with a new random string. The '
              'salt value itself is ignored by the random generator.')
        ),
    }

    attributes_schema = {
        VALUE: attributes.Schema(
            _('The random string generated by this resource. This value is '
              'also available by referencing the resource.'),
            cache_mode=attributes.Schema.CACHE_NONE,
            type=attributes.Schema.STRING
        ),
    }

    _sequences = {
        'lettersdigits': 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
    }

    def translation_rules(self, props):
        if props.get(self.SEQUENCE):
            return [
                translation.TranslationRule(
                    props,
                    translation.TranslationRule.ADD,
                    [self.CHARACTER_CLASSES],
                    [{self.CHARACTER_CLASSES_CLASS: props.get(
                        self.SEQUENCE),
                        self.CHARACTER_CLASSES_MIN: 1}]),
                translation.TranslationRule(
                    props,
                    translation.TranslationRule.DELETE,
                    [self.SEQUENCE]
                )
            ]

    def _generate_random_string(self, char_sequences, char_classes, length):
        random_string = ""

        # Add the minimum number of chars from each char sequence & char class
        if char_sequences:
            for char_seq in char_sequences:
                seq = char_seq[self.CHARACTER_SEQUENCES_SEQUENCE]
                seq_min = char_seq[self.CHARACTER_SEQUENCES_MIN]
                for i in six.moves.xrange(seq_min):
                    random_string += random.choice(seq)

        if char_classes:
            for char_class in char_classes:
                cclass_class = char_class[self.CHARACTER_CLASSES_CLASS]
                cclass_seq = self._sequences[cclass_class]
                cclass_min = char_class[self.CHARACTER_CLASSES_MIN]
                for i in six.moves.xrange(cclass_min):
                    random_string += random.choice(cclass_seq)

        def random_class_char():
            cclass_dict = random.choice(char_classes)
            cclass_class = cclass_dict[self.CHARACTER_CLASSES_CLASS]
            cclass_seq = self._sequences[cclass_class]
            return random.choice(cclass_seq)

        def random_seq_char():
            seq_dict = random.choice(char_sequences)
            seq = seq_dict[self.CHARACTER_SEQUENCES_SEQUENCE]
            return random.choice(seq)

        # Fill up rest with random chars from provided sequences & classes
        if char_sequences and char_classes:
            weighted_choices = ([True] * len(char_classes) +
                                [False] * len(char_sequences))
            while len(random_string) < length:
                if random.choice(weighted_choices):
                    random_string += random_class_char()
                else:
                    random_string += random_seq_char()

        elif char_sequences:
            while len(random_string) < length:
                random_string += random_seq_char()

        else:
            while len(random_string) < length:
                random_string += random_class_char()

        # Randomize string
        random_string = ''.join(random.sample(random_string,
                                              len(random_string)))
        return random_string

    def validate(self):
        super(RandomString, self).validate()
        char_sequences = self.properties[self.CHARACTER_SEQUENCES]
        char_classes = self.properties[self.CHARACTER_CLASSES]

        def char_min(char_dicts, min_prop):
            if char_dicts:
                return sum(char_dict[min_prop] for char_dict in char_dicts)
            return 0

        length = self.properties[self.LENGTH]
        min_length = (char_min(char_sequences, self.CHARACTER_SEQUENCES_MIN) +
                      char_min(char_classes, self.CHARACTER_CLASSES_MIN))
        if min_length > length:
            msg = _("Length property cannot be smaller than combined "
                    "character class and character sequence minimums")
            raise exception.StackValidationFailed(message=msg)

    def handle_create(self):
        char_sequences = self.properties[self.CHARACTER_SEQUENCES]
        char_classes = self.properties[self.CHARACTER_CLASSES]
        length = self.properties[self.LENGTH]

        random_string = self._generate_random_string(char_sequences,
                                                     char_classes,
                                                     length)
        self.data_set('value', random_string, redact=True)
        self.resource_id_set(self.physical_resource_name())

    def _resolve_attribute(self, name):
        if name == self.VALUE:
            return self.data().get(self.VALUE)

    def get_reference_id(self):
        if self.resource_id is not None:
            return self.data().get('value')
        else:
            return six.text_type(self.name)


def resource_mapping():
    return {
        'OS::Heat::RandomString': RandomString,
    }
