# Copyright 2019 Intel, Inc.
# 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.

from cyborg.common import exception
from cyborg.common import utils
from keystoneauth1 import exceptions as ks_exc
import os_resource_classes as orc
from oslo_log import log as logging
from oslo_middleware import request_id


LOG = logging.getLogger(__name__)
NESTED_PROVIDER_API_VERSION = '1.14'
POST_RPS_RETURNS_PAYLOAD_API_VERSION = '1.20'
PLACEMENT_CLIENT_SEMAPHORE = 'placement_client'


class PlacementClient(object):
    """Client class for reporting to placement."""

    def __init__(self):
        self._client = utils.get_sdk_adapter('placement')

    def get(self, url, version=None, global_request_id=None):
        res = self._client.get(url, microversion=version,
                               global_request_id=global_request_id)
        if res and res.status_code >= 500:
            raise exception.PlacementServerError(
                "Placement Server has some error at this time.")
        LOG.debug('Successfully get resources from placement: %s', url)
        return res

    def post(self, url, data, version=None, global_request_id=None):
        res = self._client.post(url, json=data, microversion=version,
                                global_request_id=global_request_id)
        if res.status_code >= 500:
            raise exception.PlacementServerError(
                "Placement Server has some error at this time.")
        LOG.debug('Successfully create resources from placement: %s', url)
        return res

    def put(self, url, data, version=None, global_request_id=None):
        kwargs = {}
        if data is not None:
            kwargs['json'] = data
        res = self._client.put(url, microversion=version,
                               global_request_id=global_request_id,
                               **kwargs)
        if res.status_code >= 500:
            raise exception.PlacementServerError(
                "Placement Server has some error at this time.")
        LOG.debug('Successfully update resources from placement: %s', url)
        return res

    def delete(self, url, version=None, global_request_id=None):
        res = self._client.delete(url, microversion=version,
                                  global_request_id=global_request_id)
        if res.status_code >= 500:
            raise exception.PlacementServerError(
                "Placement Server has some error at this time.")
        LOG.debug('Successfully delete resources from placement: %s', url)
        return res

    def _get_rp_traits(self, rp_uuid):
        resp = self.get("/resource_providers/%s/traits" % rp_uuid,
                        version='1.6')
        if resp.status_code != 200:
            raise Exception(
                "Failed to get traits for rp %s: HTTP %d: %s" %
                (rp_uuid, resp.status_code, resp.text))
        return resp.json()

    def _ensure_traits(self, trait_names):
        # TODO(Xinran): maintain a reference count of how many RPs use
        # this trait and do the deletion only when the last RP is deleted.
        for trait_name in trait_names:
            trait = self.get("/traits/%s" % trait_name, version='1.6')
            if trait:
                LOG.info("Trait %(trait)s already existed",
                         {"trait": trait_name})
                continue
            resp = self.put("/traits/%s" % trait_name, None, version='1.6')
            if resp.status_code == 201:
                LOG.info("Created trait %(trait)s", {"trait": trait_name})
            else:
                raise Exception(
                    "Failed to create trait %s: HTTP %d: %s" %
                    (trait_name, resp.status_code, resp.text))

    def _put_rp_traits(self, rp_uuid, traits_json):
        generation = self.get_resource_provider(
            resource_provider_uuid=rp_uuid)['generation']
        payload = {
            'resource_provider_generation': generation,
            'traits': traits_json["traits"],
        }
        resp = self.put(
            "/resource_providers/%s/traits" % rp_uuid, payload, version='1.6')
        if resp.status_code != 200:
            raise Exception(
                "Failed to set traits to %s for rp %s: HTTP %d: %s" %
                (traits_json, rp_uuid, resp.status_code, resp.text))

    def add_traits_to_rp(self, rp_uuid, trait_names):
        self._ensure_traits(trait_names)
        traits_json = self._get_rp_traits(rp_uuid)
        traits = list(set(traits_json['traits'] + trait_names))
        traits_json['traits'] = traits
        self._put_rp_traits(rp_uuid, traits_json)

    def delete_trait_by_name(self, context, rp_uuid, trait_name):
        traits_json = self._get_rp_traits(rp_uuid)
        traits = [
            trait for trait in traits_json['traits']
            if trait != trait_name
            ]
        traits_json['traits'] = traits
        self._put_rp_traits(rp_uuid, traits_json)
        self._delete_trait(context, trait_name)

    def delete_traits_with_prefixes(self, context, rp_uuid, trait_prefixes):
        traits_json = self._get_rp_traits(rp_uuid)
        traits = [
            trait for trait in traits_json['traits']
            if not any(trait.startswith(prefix)
                       for prefix in trait_prefixes)]
        delete_traits = set(traits_json['traits']) - set(traits)
        traits_json['traits'] = traits
        self._put_rp_traits(rp_uuid, traits_json)
        for trait in delete_traits:
            self._delete_trait(context, trait)

    def get_placement_request_id(self, response):
        if response is not None:
            return response.headers.get(request_id.HTTP_RESP_HEADER_REQUEST_ID)

    def update_inventory(
            self, resource_provider_uuid, inventories,
            resource_provider_generation=None, version=None):
        if resource_provider_generation is None:
            resource_provider_generation = self.get_resource_provider(
                resource_provider_uuid=resource_provider_uuid)['generation']
        url = '/resource_providers/%s/inventories' % resource_provider_uuid
        body = {
            'resource_provider_generation': resource_provider_generation,
            'inventories': inventories
        }
        try:
            return self.put(url, body, version=version).json()
        except ks_exc.NotFound:
            raise exception.PlacementResourceProviderNotFound(
                resource_provider=resource_provider_uuid)

    def get_resource_provider(self, resource_provider_uuid):
        """Get resource provider by UUID.

        :param resource_provider_uuid: UUID of the resource provider.
        :raises PlacementResourceProviderNotFound: For failure to find resource
        :returns: The Resource Provider matching the UUID.
        """
        url = '/resource_providers/%s' % resource_provider_uuid
        try:
            return self.get(url).json()
        except ks_exc.NotFound:
            raise exception.PlacementResourceProviderNotFound(
                resource_provider=resource_provider_uuid)

    def _create_resource_provider(self, context, uuid, name,
                                  parent_provider_uuid=None):
        """Calls the placement API to create a new resource provider record.

        :param context: The security context
        :param uuid: UUID of the new resource provider
        :param name: Name of the resource provider
        :param parent_provider_uuid: Optional UUID of the immediate parent
        :return: A dict of resource provider information object representing
                 the newly-created resource provider.
        :raise: ResourceProviderCreationFailed or
                ResourceProviderRetrievalFailed on error.
        """
        url = "/resource_providers"
        payload = {
            'uuid': uuid,
            'name': name,
        }
        if parent_provider_uuid is not None:
            payload['parent_provider_uuid'] = parent_provider_uuid

        # Bug #1746075: First try the microversion that returns the new
        # provider's payload.
        resp = self.post(url, payload,
                         version=POST_RPS_RETURNS_PAYLOAD_API_VERSION,
                         global_request_id=context.global_id)

        placement_req_id = self.get_placement_request_id(resp)

        if resp:
            msg = ("[%(placement_req_id)s] Created resource provider record "
                   "via placement API for resource provider with UUID "
                   "%(uuid)s and name %(name)s.")
            args = {
                'uuid': uuid,
                'name': name,
                'placement_req_id': placement_req_id,
            }
            LOG.info(msg, args)
            return resp.json()

    def ensure_resource_provider(self, context, uuid, name=None,
                                 parent_provider_uuid=None):
        resp = self.get("/resource_providers/%s" % uuid, version='1.6')
        if resp.status_code == 200:
            LOG.info("Resource Provider %(uuid)s already exists",
                     {"uuid": uuid})
        else:
            LOG.info("Creating resource provider %(provider)s",
                     {"provider": name or uuid})
            try:
                resp = self._create_resource_provider(context, uuid, name,
                                                      parent_provider_uuid)
            except Exception:
                raise exception.ResourceProviderCreationFailed(
                    name=name or uuid)
        return uuid

    def ensure_resource_classes(self, context, names):
        """Make sure resource classes exist."""
        version = '1.7'
        to_ensure = set(names)
        for name in to_ensure:
            # no payload on the put request
            # if rc exists in placement's db, skip it.
            if name in orc.STANDARDS:
                return
            resp = self.put(
                "/resource_classes/%s" % name, None, version=version,
                global_request_id=context.global_id)
            if not resp:
                msg = ("Failed to ensure resource class record with placement "
                       "API for resource class %(rc_name)s. Got "
                       "%(status_code)d: %(err_text)s.")
                args = {
                    'rc_name': name,
                    'status_code': resp.status_code,
                    'err_text': resp.text,
                }
                LOG.error(msg, args)
                raise exception.InvalidResourceClass(resource_class=name)
            elif resp.status_code == 204:
                LOG.info("Resource class  %(rc_name)s already exists",
                         {"rc_name": name})
            elif resp.status_code == 201:
                LOG.info("Successfully created resource class %(rc_name).", {
                         "rc_name", name})

    def get_providers_in_tree(self, context, uuid):
        """Queries the placement API for a list of the resource providers in
        the tree associated with the specified UUID.

        :param context: The security context
        :param uuid: UUID identifier for the resource provider to look up
        :return: A list of dicts of resource provider information, which may be
                 empty if no provider exists with the specified UUID.
        :raise: ResourceProviderRetrievalFailed on error.
        """
        resp = self.get("/resource_providers?in_tree=%s" % uuid,
                        version=NESTED_PROVIDER_API_VERSION,
                        global_request_id=context.global_id)

        if resp.status_code == 200:
            return resp.json()['resource_providers']

        # Some unexpected error
        placement_req_id = self.get_placement_request_id(resp)
        msg = ("[%(placement_req_id)s] Failed to retrieve resource provider "
               "tree from placement API for UUID %(uuid)s. Got "
               "%(status_code)d: %(err_text)s.")
        args = {
            'uuid': uuid,
            'status_code': resp.status_code,
            'err_text': resp.text,
            'placement_req_id': placement_req_id,
        }
        LOG.error(msg, args)
        raise exception.ResourceProviderRetrievalFailed(uuid=uuid)

    def delete_provider(self, rp_uuid, global_request_id=None):
        resp = self.delete('/resource_providers/%s' % rp_uuid,
                           global_request_id=global_request_id)
        # Check for 404 since we don't need to warn/raise if we tried to delete
        # something which doesn"t actually exist.
        if resp.ok:
            LOG.info("Deleted resource provider %s", rp_uuid)
            return

        msg = ("[%(placement_req_id)s] Failed to delete resource provider "
               "with UUID %(uuid)s from the placement API. Got "
               "%(status_code)d: %(err_text)s.")
        args = {
            'placement_req_id': self.get_placement_request_id(resp),
            'uuid': rp_uuid,
            'status_code': resp.status_code,
            'err_text': resp.text
        }
        LOG.error(msg, args)
        # On conflict, the caller may wish to delete allocations and
        # redrive.  (Note that this is not the same as a
        # PlacementAPIConflict case.)
        if resp.status_code == 409:
            raise exception.ResourceProviderInUse()
        raise exception.ResourceProviderDeletionFailed(uuid=rp_uuid)

    def delete_rc_by_name(self, context, name):
        """Delete resource class from placement by name."""
        resp = self.delete(
            "/resource_classes/%s" % name, global_request_id=context.global_id)
        if not resp:
            msg = ("Failed to delete resource class record with placement "
                   "API for resource class %(rc_name)s. Got "
                   "%(status_code)d: %(err_text)s.")
            args = {
                'rc_name': name,
                'status_code': resp.status_code,
                'err_text': resp.text,
            }
            LOG.error(msg, args)
        elif resp.status_code == 204:
            LOG.info("Successfully delete resource class %(rc_name).", {
                     "rc_name", name})

    def _delete_trait(self, context, name):
        """Delete trait from placement by name."""
        version = '1.6'
        resp = self.delete("/traits/%s" % name, version=version,
                           global_request_id=context.global_id)
        if not resp:
            msg = ("Failed to delete trait record with placement "
                   "API for trait %(trait_name)s. Got "
                   "%(status_code)d: %(err_text)s.")
            args = {
                'trait_name': name,
                'status_code': resp.status_code,
                'err_text': resp.text,
            }
            LOG.error(msg, args)
        elif resp.status_code == 204:
            LOG.info("Successfully delete trait %(trait_name).", {
                     "trait_name", name})

    def update_rp_inventory_reserved(self, rp_uuid, resource, total, reserved):
        update_inventory = {resource: {"total": total, "reserved": reserved}}
        self.update_inventory(rp_uuid, update_inventory, version='1.26')
