# Copyright 2020 The StackStorm Authors.
# Copyright 2019 Extreme Networks, Inc.
#
# 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.

from __future__ import absolute_import

import os
import re
import collections

import six

from st2common.util import schema as util_schema
from st2common.constants.pack import MANIFEST_FILE_NAME
from st2common.constants.pack import PACK_REF_WHITELIST_REGEX
from st2common.content.loader import MetaLoader
from st2common.persistence.pack import Pack
from st2common.exceptions.apivalidation import ValueValidationException
from st2common.util import jinja as jinja_utils

__all__ = [
    'get_pack_ref_from_metadata',
    'get_pack_metadata',
    'get_pack_warnings',

    'get_pack_common_libs_path_for_pack_ref',
    'get_pack_common_libs_path_for_pack_db',

    'validate_config_against_schema',

    'normalize_pack_version'
]

# Common format for python 2.7 warning
if six.PY2:
    PACK_PYTHON2_WARNING = "DEPRECATION WARNING: Pack %s only supports Python 2.x. " \
                           "Python 2 support will be dropped in future releases. " \
                           "Please consider updating your packs to work with Python 3.x"
else:
    PACK_PYTHON2_WARNING = "DEPRECATION WARNING: Pack %s only supports Python 2.x. " \
                           "Python 2 support has been removed since st2 v3.4.0. " \
                           "Please update your packs to work with Python 3.x"


def get_pack_ref_from_metadata(metadata, pack_directory_name=None):
    """
    Utility function which retrieves pack "ref" attribute from the pack metadata file.

    If this attribute is not provided, an attempt is made to infer "ref" from the "name" attribute.

    :rtype: ``str``
    """
    pack_ref = None

    # The rules for the pack ref are as follows:
    # 1. If ref attribute is available, we used that
    # 2. If pack_directory_name is available we use that (this only applies to packs
    # which are in sub-directories)
    # 2. If attribute is not available, but pack name is and pack name meets the valid name
    # criteria, we use that
    if metadata.get('ref', None):
        pack_ref = metadata['ref']
    elif pack_directory_name and re.match(PACK_REF_WHITELIST_REGEX, pack_directory_name):
        pack_ref = pack_directory_name
    else:
        if re.match(PACK_REF_WHITELIST_REGEX, metadata['name']):
            pack_ref = metadata['name']
        else:
            msg = ('Pack name "%s" contains invalid characters and "ref" attribute is not '
                   'available. You either need to add "ref" attribute which contains only word '
                   'characters to the pack metadata file or update name attribute to contain only'
                   'word characters.')
            raise ValueError(msg % (metadata['name']))

    return pack_ref


def get_pack_metadata(pack_dir):
    """
    Return parsed metadata for a particular pack directory.

    :rtype: ``dict``
    """
    manifest_path = os.path.join(pack_dir, MANIFEST_FILE_NAME)

    if not os.path.isfile(manifest_path):
        raise ValueError('Pack "%s" is missing %s file' % (pack_dir, MANIFEST_FILE_NAME))

    meta_loader = MetaLoader()
    content = meta_loader.load(manifest_path)
    if not content:
        raise ValueError('Pack "%s" metadata file is empty' % (pack_dir))

    return content


def get_pack_warnings(pack_metadata):
    """
    Return warning string if pack metadata indicates only python 2 is supported

    :rtype: ``str``
    """
    warning = None
    versions = pack_metadata.get('python_versions', None)
    pack_name = pack_metadata.get('name', None)
    if versions and set(versions) == set(['2']):
        warning = PACK_PYTHON2_WARNING % pack_name
    return warning


def validate_config_against_schema(config_schema, config_object, config_path,
                                  pack_name=None):
    """
    Validate provided config dictionary against the provided config schema
    dictionary.
    """
    # NOTE: Lazy improt to avoid performance overhead of importing this module when it's not used
    import jsonschema

    pack_name = pack_name or 'unknown'

    schema = util_schema.get_schema_for_resource_parameters(parameters_schema=config_schema,
                                                            allow_additional_properties=True)
    instance = config_object

    try:
        cleaned = util_schema.validate(instance=instance, schema=schema,
                                       cls=util_schema.CustomValidator, use_default=True,
                                       allow_default_none=True)
        for key in cleaned:
            if (jinja_utils.is_jinja_expression(value=cleaned.get(key)) and
                    "decrypt_kv" in cleaned.get(key) and config_schema.get(key).get('secret')):
                raise ValueValidationException('Values specified as "secret: True" in config '
                                               'schema are automatically decrypted by default. Use '
                                               'of "decrypt_kv" jinja filter is not allowed for '
                                               'such values. Please check the specified values in '
                                               'the config or the default values in the schema.')
    except jsonschema.ValidationError as e:
        attribute = getattr(e, 'path', [])

        if isinstance(attribute, (tuple, list, collections.Iterable)):
            attribute = [str(item) for item in attribute]
            attribute = '.'.join(attribute)
        else:
            attribute = str(attribute)

        msg = ('Failed validating attribute "%s" in config for pack "%s" (%s): %s' %
               (attribute, pack_name, config_path, six.text_type(e)))
        raise jsonschema.ValidationError(msg)

    return cleaned


def get_pack_common_libs_path_for_pack_ref(pack_ref):
    pack_db = Pack.get_by_ref(pack_ref)
    pack_common_libs_path = get_pack_common_libs_path_for_pack_db(pack_db=pack_db)
    return pack_common_libs_path


def get_pack_common_libs_path_for_pack_db(pack_db):
    """
    Return the pack's common lib path. This is the path where common code for sensors
    and actions are placed.

    For example, if the pack is at /opt/stackstorm/packs/my_pack, you can place
    common library code for actions and sensors in /opt/stackstorm/packs/my_pack/lib/.
    This common library code is only available for python sensors and actions. The lib
    structure also needs to follow a python convention with a __init__.py file.

    :param pack_db: Pack DB model
    :type pack_db: :class:`PackDB`

    :rtype: ``str``
    """
    pack_dir = getattr(pack_db, 'path', None)

    if not pack_dir:
        return None

    libs_path = os.path.join(pack_dir, 'lib')

    return libs_path


def normalize_pack_version(version):
    """
    Normalize old, pre StackStorm v2.1 non valid semver version string (e.g. 0.2) to a valid
    semver version string (0.2.0).

    :rtype: ``str``
    """
    version = str(version)

    version_seperator_count = version.count('.')
    if version_seperator_count == 1:
        version = version + '.0'

    return version

[((95, 20, 95, 62), 'os.path.join', 'os.path.join', ({(95, 33, 95, 41): 'pack_dir', (95, 43, 95, 61): 'MANIFEST_FILE_NAME'}, {}), '(pack_dir, MANIFEST_FILE_NAME)', False, 'import os\n'), ((100, 18, 100, 30), 'st2common.content.loader.MetaLoader', 'MetaLoader', ({}, {}), '()', False, 'from st2common.content.loader import MetaLoader\n'), ((133, 13, 134, 93), 'st2common.util.schema.get_schema_for_resource_parameters', 'util_schema.get_schema_for_resource_parameters', (), '', True, 'from st2common.util import schema as util_schema\n'), ((166, 14, 166, 39), 'st2common.persistence.pack.Pack.get_by_ref', 'Pack.get_by_ref', ({(166, 30, 166, 38): 'pack_ref'}, {}), '(pack_ref)', False, 'from st2common.persistence.pack import Pack\n'), ((191, 16, 191, 45), 'os.path.join', 'os.path.join', ({(191, 29, 191, 37): 'pack_dir', (191, 39, 191, 44): '"""lib"""'}, {}), "(pack_dir, 'lib')", False, 'import os\n'), ((97, 11, 97, 40), 'os.path.isfile', 'os.path.isfile', ({(97, 26, 97, 39): 'manifest_path'}, {}), '(manifest_path)', False, 'import os\n'), ((138, 18, 140, 63), 'st2common.util.schema.validate', 'util_schema.validate', (), '', True, 'from st2common.util import schema as util_schema\n'), ((74, 33, 74, 88), 're.match', 're.match', ({(74, 42, 74, 66): 'PACK_REF_WHITELIST_REGEX', (74, 68, 74, 87): 'pack_directory_name'}, {}), '(PACK_REF_WHITELIST_REGEX, pack_directory_name)', False, 'import re\n'), ((77, 11, 77, 63), 're.match', 're.match', ({(77, 20, 77, 44): 'PACK_REF_WHITELIST_REGEX', (77, 46, 77, 62): "metadata['name']"}, {}), "(PACK_REF_WHITELIST_REGEX, metadata['name'])", False, 'import re\n'), ((160, 14, 160, 45), 'jsonschema.ValidationError', 'jsonschema.ValidationError', ({(160, 41, 160, 44): 'msg'}, {}), '(msg)', False, 'import jsonschema\n'), ((144, 22, 148, 97), 'st2common.exceptions.apivalidation.ValueValidationException', 'ValueValidationException', ({(144, 47, 148, 96): '"""Values specified as "secret: True" in config schema are automatically decrypted by default. Use of "decrypt_kv" jinja filter is not allowed for such values. Please check the specified values in the config or the default values in the schema."""'}, {}), '(\n    \'Values specified as "secret: True" in config schema are automatically decrypted by default. Use of "decrypt_kv" jinja filter is not allowed for such values. Please check the specified values in the config or the default values in the schema.\'\n    )', False, 'from st2common.exceptions.apivalidation import ValueValidationException\n'), ((159, 51, 159, 67), 'six.text_type', 'six.text_type', ({(159, 65, 159, 66): 'e'}, {}), '(e)', False, 'import six\n')]