# Copyright 2016-2021 Canonical Limited.
#
# 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 glob
import grp
import os
import pwd
import six
import yaml

from charmhelpers.core.hookenv import (
    log,
    DEBUG,
    INFO,
    WARNING,
    ERROR,
)


# Global settings cache. Since each hook fire entails a fresh module import it
# is safe to hold this in memory and not risk missing config changes (since
# they will result in a new hook fire and thus re-import).
__SETTINGS__ = {}


def _get_defaults(modules):
    """Load the default config for the provided modules.

    :param modules: stack modules config defaults to lookup.
    :returns: modules default config dictionary.
    """
    default = os.path.join(os.path.dirname(__file__),
                           'defaults/%s.yaml' % (modules))
    return yaml.safe_load(open(default))


def _get_schema(modules):
    """Load the config schema for the provided modules.

    NOTE: this schema is intended to have 1-1 relationship with they keys in
    the default config and is used a means to verify valid overrides provided
    by the user.

    :param modules: stack modules config schema to lookup.
    :returns: modules default schema dictionary.
    """
    schema = os.path.join(os.path.dirname(__file__),
                          'defaults/%s.yaml.schema' % (modules))
    return yaml.safe_load(open(schema))


def _get_user_provided_overrides(modules):
    """Load user-provided config overrides.

    :param modules: stack modules to lookup in user overrides yaml file.
    :returns: overrides dictionary.
    """
    overrides = os.path.join(os.environ['JUJU_CHARM_DIR'],
                             'hardening.yaml')
    if os.path.exists(overrides):
        log("Found user-provided config overrides file '%s'" %
            (overrides), level=DEBUG)
        settings = yaml.safe_load(open(overrides))
        if settings and settings.get(modules):
            log("Applying '%s' overrides" % (modules), level=DEBUG)
            return settings.get(modules)

        log("No overrides found for '%s'" % (modules), level=DEBUG)
    else:
        log("No hardening config overrides file '%s' found in charm "
            "root dir" % (overrides), level=DEBUG)

    return {}


def _apply_overrides(settings, overrides, schema):
    """Get overrides config overlaid onto modules defaults.

    :param modules: require stack modules config.
    :returns: dictionary of modules config with user overrides applied.
    """
    if overrides:
        for k, v in six.iteritems(overrides):
            if k in schema:
                if schema[k] is None:
                    settings[k] = v
                elif type(schema[k]) is dict:
                    settings[k] = _apply_overrides(settings[k], overrides[k],
                                                   schema[k])
                else:
                    raise Exception("Unexpected type found in schema '%s'" %
                                    type(schema[k]), level=ERROR)
            else:
                log("Unknown override key '%s' - ignoring" % (k), level=INFO)

    return settings


def get_settings(modules):
    global __SETTINGS__
    if modules in __SETTINGS__:
        return __SETTINGS__[modules]

    schema = _get_schema(modules)
    settings = _get_defaults(modules)
    overrides = _get_user_provided_overrides(modules)
    __SETTINGS__[modules] = _apply_overrides(settings, overrides, schema)
    return __SETTINGS__[modules]


def ensure_permissions(path, user, group, permissions, maxdepth=-1):
    """Ensure permissions for path.

    If path is a file, apply to file and return. If path is a directory,
    apply recursively (if required) to directory contents and return.

    :param user: user name
    :param group: group name
    :param permissions: octal permissions
    :param maxdepth: maximum recursion depth. A negative maxdepth allows
                     infinite recursion and maxdepth=0 means no recursion.
    :returns: None
    """
    if not os.path.exists(path):
        log("File '%s' does not exist - cannot set permissions" % (path),
            level=WARNING)
        return

    _user = pwd.getpwnam(user)
    os.chown(path, _user.pw_uid, grp.getgrnam(group).gr_gid)
    os.chmod(path, permissions)

    if maxdepth == 0:
        log("Max recursion depth reached - skipping further recursion",
            level=DEBUG)
        return
    elif maxdepth > 0:
        maxdepth -= 1

    if os.path.isdir(path):
        contents = glob.glob("%s/*" % (path))
        for c in contents:
            ensure_permissions(c, user=user, group=group,
                               permissions=permissions, maxdepth=maxdepth)