# All Rights Reserved.
#
#    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 copy
import operator
import re

from rally.common import logging
from rally.common.plugin import plugin
from rally import exceptions
from rally.task import types

from rally_openstack.common import osclients
from rally_openstack.common.services.image import image
from rally_openstack.common.services.storage import block


LOG = logging.getLogger(__name__)


configure = plugin.configure


class OpenStackResourceType(types.ResourceType):
    """A base class for OpenStack ResourceTypes plugins with help-methods"""

    def __init__(self, context=None, cache=None):
        super(OpenStackResourceType, self).__init__(context, cache)

        self._clients = None
        if self._context.get("admin"):
            self._clients = osclients.Clients(
                self._context["admin"]["credential"])
        elif self._context.get("users"):
            self._clients = osclients.Clients(
                self._context["users"][0]["credential"])

    def _find_resource(self, resource_spec, resources):
        """Return the resource whose name matches the pattern.

        .. note:: This method is a modified version of
            `rally.task.types.obj_from_name`. The difference is supporting the
            case of returning the latest version of resource in case of
            `accurate=False` option.

        :param resource_spec: resource specification to find.
            Expected keys:

            * name - The exact name of resource to search. If no exact match
              and value of *accurate* key is False (default behaviour), name
              will be interpreted as a regexp
            * regexp - a regexp of resource name to match. If several resources
              match and value of *accurate* key is False (default behaviour),
              the latest resource will be returned.
        :param resources: iterable containing all resources
        :raises InvalidScenarioArgument: if the pattern does
            not match anything.

        :returns: resource object mapped to `name` or `regex`
        """
        if "name" in resource_spec:
            # In a case of pattern string exactly matches resource name
            matching_exact = [resource for resource in resources
                              if resource.name == resource_spec["name"]]
            if len(matching_exact) == 1:
                return matching_exact[0]
            elif len(matching_exact) > 1:
                raise exceptions.InvalidScenarioArgument(
                    "%(typename)s with name '%(pattern)s' "
                    "is ambiguous, possible matches "
                    "by id: %(ids)s" % {
                        "typename": self.get_name().title(),
                        "pattern": resource_spec["name"],
                        "ids": ", ".join(map(operator.attrgetter("id"),
                                             matching_exact))})
            if resource_spec.get("accurate", False):
                raise exceptions.InvalidScenarioArgument(
                    "%(typename)s with name '%(name)s' not found" % {
                        "typename": self.get_name().title(),
                        "name": resource_spec["name"]})
            # Else look up as regex
            patternstr = resource_spec["name"]
        elif "regex" in resource_spec:
            patternstr = resource_spec["regex"]
        else:
            raise exceptions.InvalidScenarioArgument(
                "%(typename)s 'id', 'name', or 'regex' not found "
                "in '%(resource_spec)s' " % {
                    "typename": self.get_name().title(),
                    "resource_spec": resource_spec})

        pattern = re.compile(patternstr)
        matching = [resource for resource in resources
                    if re.search(pattern, resource.name or "")]
        if not matching:
            raise exceptions.InvalidScenarioArgument(
                "%(typename)s with pattern '%(pattern)s' not found" % {
                    "typename": self.get_name().title(),
                    "pattern": pattern.pattern})
        elif len(matching) > 1:
            if not resource_spec.get("accurate", False):
                return sorted(matching, key=lambda o: o.name or "")[-1]

            raise exceptions.InvalidScenarioArgument(
                "%(typename)s with name '%(pattern)s' is ambiguous, possible "
                "matches by id: %(ids)s" % {
                    "typename": self.get_name().title(),
                    "pattern": pattern.pattern,
                    "ids": ", ".join(map(operator.attrgetter("id"),
                                         matching))})
        return matching[0]


@plugin.configure(name="nova_flavor")
class Flavor(OpenStackResourceType):
    """Find Nova's flavor ID by name or regexp."""

    def pre_process(self, resource_spec, config):
        resource_id = resource_spec.get("id")
        if not resource_id:
            novaclient = self._clients.nova()
            resource_id = types._id_from_name(
                resource_config=resource_spec,
                resources=novaclient.flavors.list(),
                typename="flavor")
        return resource_id


@plugin.configure(name="glance_image")
class GlanceImage(OpenStackResourceType):
    """Find Glance's image ID by name or regexp."""

    def pre_process(self, resource_spec, config):
        resource_id = resource_spec.get("id")
        list_kwargs = resource_spec.get("list_kwargs", {})

        if not resource_id:
            cache_id = hash(frozenset(list_kwargs.items()))
            if cache_id not in self._cache:
                glance = image.Image(self._clients)
                self._cache[cache_id] = glance.list_images(**list_kwargs)
            images = self._cache[cache_id]
            resource = self._find_resource(resource_spec, images)
            return resource.id
        return resource_id


@plugin.configure(name="glance_image_args")
class GlanceImageArguments(OpenStackResourceType):
    """Process Glance image create options to look similar in case of V1/V2."""
    def pre_process(self, resource_spec, config):
        resource_spec = copy.deepcopy(resource_spec)
        if "is_public" in resource_spec:
            if "visibility" in resource_spec:
                resource_spec.pop("is_public")
            else:
                visibility = ("public" if resource_spec.pop("is_public")
                              else "private")
                resource_spec["visibility"] = visibility
        return resource_spec


@plugin.configure(name="ec2_image")
class EC2Image(OpenStackResourceType):
    """Find EC2 image ID."""

    def pre_process(self, resource_spec, config):
        if "name" not in resource_spec and "regex" not in resource_spec:
            # NOTE(wtakase): gets resource name from OpenStack id
            glanceclient = self._clients.glance()
            resource_name = types._name_from_id(
                resource_config=resource_spec,
                resources=list(glanceclient.images.list()),
                typename="image")
            resource_spec["name"] = resource_name

        # NOTE(wtakase): gets EC2 resource id from name or regex
        ec2client = self._clients.ec2()
        resource_ec2_id = types._id_from_name(
            resource_config=resource_spec,
            resources=list(ec2client.get_all_images()),
            typename="ec2_image")
        return resource_ec2_id


@plugin.configure(name="cinder_volume_type")
class VolumeType(OpenStackResourceType):
    """Find Cinder volume type ID by name or regexp."""

    def pre_process(self, resource_spec, config):
        resource_id = resource_spec.get("id")
        if not resource_id:
            cinder = block.BlockStorage(self._clients)
            resource_id = types._id_from_name(
                resource_config=resource_spec,
                resources=cinder.list_types(),
                typename="volume_type")
        return resource_id


@plugin.configure(name="neutron_network")
class NeutronNetwork(OpenStackResourceType):
    """Find Neutron network ID by it's name."""
    def pre_process(self, resource_spec, config):
        resource_id = resource_spec.get("id")
        if resource_id:
            return resource_id
        else:
            neutronclient = self._clients.neutron()
            for net in neutronclient.list_networks()["networks"]:
                if net["name"] == resource_spec.get("name"):
                    return net["id"]

        raise exceptions.InvalidScenarioArgument(
            "Neutron network with name '{name}' not found".format(
                name=resource_spec.get("name")))


@plugin.configure(name="watcher_strategy")
class WatcherStrategy(OpenStackResourceType):
    """Find Watcher strategy ID by it's name."""

    def pre_process(self, resource_spec, config):
        resource_id = resource_spec.get("id")
        if not resource_id:
            watcherclient = self._clients.watcher()
            resource_id = types._id_from_name(
                resource_config=resource_spec,
                resources=[watcherclient.strategy.get(
                    resource_spec.get("name"))],
                typename="strategy",
                id_attr="uuid")
        return resource_id


@plugin.configure(name="watcher_goal")
class WatcherGoal(OpenStackResourceType):
    """Find Watcher goal ID by it's name."""

    def pre_process(self, resource_spec, config):
        resource_id = resource_spec.get("id")
        if not resource_id:
            watcherclient = self._clients.watcher()
            resource_id = types._id_from_name(
                resource_config=resource_spec,
                resources=[watcherclient.goal.get(resource_spec.get("name"))],
                typename="goal",
                id_attr="uuid")
        return resource_id
