# Copyright 2020 Nokia Software.
#
#    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

from oslo_config import cfg
from oslo_log import log as logging
from stevedore import extension

from mistral_lib import actions as ml_actions
from mistral_lib.utils import inspect_utils as i_utils


CONF = cfg.CONF

LOG = logging.getLogger(__name__)


class GeneratedPythonActionDescriptor(ml_actions.PythonActionDescriptor):
    """Represents a legacy python action generated by a generator.

    It's needed temporarily until we fully refactor OpenStack actions in the
    'mistral-extra' project. The difference of this descriptor and the standard
    PythonActionDescriptor class is how they initialize a spec of parameters
    and description.
    """

    def __init__(self, name, action_cls, action_cls_attrs=None, namespace='',
                 project_id=None, scope=None, desc=None, params_spec=None):
        super(GeneratedPythonActionDescriptor, self).__init__(
            name,
            action_cls,
            action_cls_attrs,
            namespace,
            project_id,
            scope
        )

        if desc:
            self._desc = desc

        if params_spec:
            self._params_spec = params_spec

    def __repr__(self):
        return 'Generated Python action [name=%s, cls=%s, params_spec=%s]' % (
            self.name,
            self.action_class,
            self.params_spec
        )


class LegacyActionProvider(ml_actions.ActionProvider):
    """Represents the old way of configuring actions.

    There are two sources where this action provider loads actions
    from:
        * Action classes configured in the entry point "mistral.actions"
        * Action classes generated by generators configured in the
            entry point "mistral.generators" as a function returning a
            collection of them.
    """

    def __init__(self, name='legacy'):
        super().__init__(name)

        # TODO(rakhmerov): Come up with a convenient structure to keep action
        # classes indexed so that we could search and filter easily.
        self._action_descs = collections.OrderedDict()

        self._load_actions()

    def _load_actions(self):
        self._load_action_plugins()
        self._load_action_generators()

    def _load_action_plugins(self):
        if not CONF.legacy_action_provider.load_action_plugins:
            return

        LOG.info(
            "Loading actions plugged in with the entry point "
            "'mistral.actions'..."
        )

        ext_mgr = extension.ExtensionManager(
            namespace='mistral.actions',
            invoke_on_load=False
        )

        for action_name in ext_mgr.names():
            action_cls = ext_mgr[action_name].plugin

            if CONF.legacy_action_provider.only_builtin_actions:
                if not action_cls.__module__.startswith('mistral.'):
                    continue

            action_desc = ml_actions.PythonActionDescriptor(
                action_name,
                action_cls,
                namespace=''
            )

            self._action_descs[action_name] = action_desc

            LOG.debug('Registered action: %s', action_desc)

    def _load_action_generators(self):
        if not CONF.legacy_action_provider.load_action_generators:
            return

        LOG.info(
            "Loading actions from the action generators plugged in "
            "with the entry point 'mistral.generators'"
        )

        for gen in self._get_action_generators():
            self._register_generator_actions(gen)

    @staticmethod
    def _get_action_generators():
        res = []

        ext_mgr = extension.ExtensionManager(
            namespace='mistral.generators',
            invoke_on_load=True
        )

        # TODO(rakhmerov): this is all ugly. It turns out that the only
        # way to register actions via generators is to register a special
        # function in the entry point that returns a list of generators.
        # But we can't directly register a generator.
        for ext in ext_mgr:
            if ext.obj is not None:
                for gen in ext.obj:
                    res.append(gen)

        return res

    def _register_generator_actions(self, generator):
        # TODO(rakhmerov): Here we have an implicit dependency on
        # "mistral-extra" because ActionGenerator class is defined
        # in "mistral-extra". Of course, it works because of duck
        # typing but architecture wise it's just very bad. "mistral"
        # must not depend on "mistral-extra" because the latter is
        # just a project with mistral extensions. In fact, we can't
        # even extend ActionGenerator class within "mistral" for
        # testing purposes.
        # So it's all done this way for compatibility until all
        # OpenStack actions are redesigned with action providers.
        for action in generator.create_actions():
            action_desc = GeneratedPythonActionDescriptor(
                action['name'],
                generator.base_action_class,
                i_utils.get_public_fields(action['class']),
                desc=action['description'],
                params_spec=action['arg_list']
            )

            LOG.debug('Registered action: %s', action_desc)

            self._action_descs[action['name']] = action_desc

    def find(self, action_name, namespace=None):
        return self._action_descs.get(action_name)

    def find_all(self, namespace=None, limit=None, sort_fields=None,
                 sort_dirs=None, **filters):
        # TODO(rakhmerov): Apply sort_keys, sort_dirs, and filters.
        return self._action_descs.values()
