# 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.

import os_resource_classes as orc

from keystoneauth1 import exceptions as ks_exc
from oslo_log import log as logging
from oslo_middleware import request_id

from cyborg.common import exception
from cyborg.common import utils


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:
    """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(f"/resource_providers/{rp_uuid}/traits", version='1.6')
        if resp.status_code != 200:
            raise Exception(
                f"Failed to get traits for rp {rp_uuid}:"
                f" HTTP {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(f"/traits/{trait_name}", version='1.6')
            if trait:
                LOG.info(
                    "Trait %(trait)s already existed", {"trait": trait_name}
                )
                continue
            resp = self.put(f"/traits/{trait_name}", None, version='1.6')
            if resp.status_code == 201:
                LOG.info("Created trait %(trait)s", {"trait": trait_name})
            else:
                raise Exception(
                    f"Failed to create trait {trait_name}:"
                    f" HTTP {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(
            f"/resource_providers/{rp_uuid}/traits", payload, version='1.6'
        )
        if resp.status_code != 200:
            raise Exception(
                f"Failed to set traits to {traits_json} for rp {rp_uuid}:"
                f" HTTP {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 = f'/resource_providers/{resource_provider_uuid}/inventories'
        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 = f'/resource_providers/{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(f"/resource_providers/{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(
                f"/resource_classes/{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(
            f"/resource_providers?in_tree={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(
            f'/resource_providers/{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(
            f"/resource_classes/{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(
            f"/traits/{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')
