# Copyright 2017 Huawei Technologies Co.,LTD.
# 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.

"""Cyborg base exception handling.

SHOULD include dedicated exception logging.

"""

from http import HTTPStatus

from oslo_log import log

from cyborg.common.i18n import _
from cyborg.conf import CONF


LOG = log.getLogger(__name__)


class CyborgException(Exception):
    """Base Cyborg Exception

    To correctly use this class, inherit from it and define
    a '_msg_fmt' property. That message will get printf'd
    with the keyword arguments provided to the constructor.

    If you need to access the message from an exception you should use
    str(exc).
    """

    _msg_fmt = _("An unknown exception occurred.")
    code = HTTPStatus.INTERNAL_SERVER_ERROR
    headers = {}
    safe = False

    def __init__(self, message=None, **kwargs):
        self.kwargs = kwargs

        if 'code' not in self.kwargs:
            try:
                self.kwargs['code'] = self.code
            except AttributeError:
                pass

        if not message:
            try:
                message = self._msg_fmt % kwargs
            except Exception:
                # kwargs doesn't match a variable in self._msg_fmt
                # log the issue and the kwargs
                LOG.exception('Exception in string format operation')
                for name, value in kwargs.items():
                    LOG.error(
                        "%(name)s: %(value)s", {"name": name, "value": value}
                    )

                if CONF.fatal_exception_format_errors:
                    raise
                else:
                    # at least get the core self._msg_fmt out if something
                    # happened
                    message = self._msg_fmt

        super().__init__(message)

    def __str__(self):
        """Encode to utf-8 then wsme api can consume it as well."""
        return self.args[0]


class Forbidden(CyborgException):
    _msg_fmt = _("Forbidden")
    code = HTTPStatus.FORBIDDEN


class ARQBadState(CyborgException):
    _msg_fmt = _(
        'Bad state: %(state)s for ARQ: %(uuid)s. '
        'Expected state(s): %(expected)s'
    )


class AttachHandleAlreadyExists(CyborgException):
    _msg_fmt = _("AttachHandle with uuid %(uuid)s already exists.")


class AttachHandleUUIDNeeded(CyborgException):
    _msg_fmt = _("Need to provide AttachHandle uuid.")


class ControlpathIDAlreadyExists(CyborgException):
    _msg_fmt = _("ControlpathID with uuid %(uuid)s already exists.")


class ConfigInvalid(CyborgException):
    _msg_fmt = _("Invalid configuration file. %(error_msg)s")


class DeviceAlreadyExists(CyborgException):
    _msg_fmt = _("Device with uuid %(uuid)s already exists.")


class DeviceProfileAlreadyExists(CyborgException):
    _msg_fmt = _("DeviceProfile with uuid %(uuid)s already exists.")


class DeviceProfileNameNeeded(CyborgException):
    _msg_fmt = _("DeviceProfile name needed.")


class DeviceProfileGroupsExpected(CyborgException):
    _msg_fmt = _("DeviceProfile needs groups field.")


class DeployableAlreadyExists(CyborgException):
    _msg_fmt = _("Deployable with uuid %(uuid)s already exists.")


class ExtArqAlreadyExists(CyborgException):
    _msg_fmt = _("ExtArq with uuid %(uuid)s already exists.")


class ExpectedOneObject(CyborgException):
    _msg_fmt = _("Expected one object of type %(obj)s but got %(count)s.")


class InUse(CyborgException):
    _msg_fmt = _("%(object) with %(ident)s is in use.")


class Invalid(CyborgException):
    _msg_fmt = _("Invalid parameters.")
    code = HTTPStatus.BAD_REQUEST


class InvalidIdentity(Invalid):
    _msg_fmt = _("Expected a uuid/id but received %(identity)s.")


class InvalidUUID(Invalid):
    _msg_fmt = _("Expected a uuid but received %(uuid)s.")


class InvalidJsonType(Invalid):
    _msg_fmt = _("%(value)s is not JSON serializable.")


class InvalidAPIVersionString(Invalid):
    _msg_fmt = _(
        "API Version String %(version)s is of invalid format. Must "
        "be of format MajorNum.MinorNum."
    )


# TODO(All): Consider whether Placement/Image exceptions can be included here.
class InvalidAPIResponse(Invalid):
    _msg_fmt = _(
        'Bad API response from %(service)s for %(api)s API. Details: %(msg)s'
    )


# Cannot be templated as the error syntax varies.
# msg needs to be constructed when raised.
class InvalidParameterValue(Invalid):
    _msg_fmt = _("%(err)s")


class NeedAtleastOne(Invalid):
    _msg_fmt = _("Need at least one %(obj)s.")


class PatchError(Invalid):
    _msg_fmt = _("Couldn't apply patch. Reason: %(reason)s")


class NotAuthorized(CyborgException):
    _msg_fmt = _("Not authorized.")
    code = HTTPStatus.UNAUTHORIZED


class HTTPForbidden(Forbidden):
    _msg_fmt = _("Access was denied to the following resource: %(resource)s")


class NotFound(CyborgException):
    _msg_fmt = _("Resource could not be found.")
    code = HTTPStatus.NOT_FOUND


class ServiceUnavailable(CyborgException):
    _msg_fmt = _("Service is unavailable at this time.")
    code = HTTPStatus.SERVICE_UNAVAILABLE


class ServiceNotFound(NotFound):
    _msg_fmt = _("Service %(service_id)s could not be found.")


class ConfGroupForServiceTypeNotFound(ServiceNotFound):
    _msg_fmt = _(
        "No conf group name could be found for service type %(stype)s."
    )


class InvalidDeployType(CyborgException):
    _msg_fmt = _("Deployable have an invalid type")


class Conflict(CyborgException):
    _msg_fmt = _('Conflict.')
    code = HTTPStatus.CONFLICT


class DuplicateDeviceName(Conflict):
    _msg_fmt = _("A device with name %(name)s already exists.")


class DuplicateDeviceProfileName(Conflict):
    _msg_fmt = _("A device_profile with name %(name)s already exists.")


class DuplicateDeployableName(Conflict):
    _msg_fmt = _("A deployable with name %(name)s already exists.")


class PlacementEndpointNotFound(NotFound):
    _msg_fmt = _("Placement API endpoint not found")


class PlacementResourceProviderNotFound(NotFound):
    _msg_fmt = _(
        "Placement resource provider not found: %(resource_provider)s."
    )


class PlacementInventoryNotFound(NotFound):
    _msg_fmt = _(
        "Placement inventory not found for resource provider "
        "%(resource_provider)s, resource class %(resource_class)s."
    )


class PlacementInventoryUpdateConflict(Conflict):
    _msg_fmt = _(
        "Placement inventory update conflict for resource provider "
        "%(resource_provider)s, resource class %(resource_class)s."
    )


class ObjectActionError(CyborgException):
    _msg_fmt = _('Object action %(action)s failed because: %(reason)s')


class AttributeInvalid(CyborgException):
    _msg_fmt = _("Attribute is invalid")


class AttributeAlreadyExists(CyborgException):
    _msg_fmt = _("Attribute with uuid %(uuid)s already exists.")


# An exception with this name is used on both sides of the placement/
# cyborg interaction.
class ResourceClassNotFound(NotFound):
    _msg_fmt = _("No such resource class %(name_or_uuid)s.")


class ResourceProviderInUse(CyborgException):
    _msg_fmt = _("Resource provider has allocations.")


class ResourceProviderRetrievalFailed(CyborgException):
    _msg_fmt = _("Failed to get resource provider with UUID %(uuid)s")


class ResourceProviderAggregateRetrievalFailed(CyborgException):
    _msg_fmt = _(
        "Failed to get aggregates for resource provider with UUID %(uuid)s"
    )


class ResourceProviderTraitRetrievalFailed(CyborgException):
    _msg_fmt = _(
        "Failed to get traits for resource provider with UUID %(uuid)s"
    )


class ResourceProviderCreationFailed(CyborgException):
    _msg_fmt = _("Failed to create resource provider %(name)s")


class ResourceProviderDeletionFailed(CyborgException):
    _msg_fmt = _("Failed to delete resource provider %(uuid)s")


class ResourceProviderUpdateFailed(CyborgException):
    _msg_fmt = _(
        "Failed to update resource provider via URL %(url)s: %(error)s"
    )


class ResourceProviderSyncFailed(CyborgException):
    _msg_fmt = _(
        "Failed to synchronize the placement service with resource "
        "provider information supplied by the compute host."
    )


class PlacementAPIConnectFailure(CyborgException):
    _msg_fmt = _("Unable to communicate with the Placement API.")


class PlacementServerError(CyborgException):
    _msg_fmt = _("Placement Server has some error at this time.")


class PlacementAPIConflict(CyborgException):
    """Any 409 error from placement APIs should use (a subclass of) this
    exception.
    """

    _msg_fmt = _(
        "A conflict was encountered attempting to invoke the "
        "placement API at URL %(url)s: %(error)s"
    )


class ResourceProviderUpdateConflict(PlacementAPIConflict):
    """A 409 caused by generation mismatch from attempting to update an
    existing provider record or its associated data (aggregates, traits, etc.).
    """

    _msg_fmt = _(
        "A conflict was encountered attempting to update resource "
        "provider %(uuid)s (generation %(generation)d): %(error)s"
    )


class TraitCreationFailed(CyborgException):
    _msg_fmt = _("Failed to create trait %(name)s: %(error)s")


class TraitRetrievalFailed(CyborgException):
    _msg_fmt = _("Failed to retrieve traits from the placement API: %(error)s")


class InvalidResourceClass(Invalid):
    _msg_fmt = _("Resource class '%(resource_class)s' invalid.")


class InvalidResourceAmount(Invalid):
    _msg_fmt = _("Resource amounts must be integers. Received '%(amount)s'.")


class InvalidInventory(Invalid):
    _msg_fmt = _(
        "Inventory for '%(resource_class)s' on "
        "resource provider '%(resource_provider)s' invalid."
    )


# An exception with this name is used on both sides of the placement/
# cyborg interaction.
class InventoryInUse(InvalidInventory):
    # NOTE(mriedem): This message cannot change without impacting the
    # cyborg.services.client.report._RE_INV_IN_USE regex.
    _msg_fmt = _(
        "Inventory for '%(resource_classes)s' on "
        "resource provider '%(resource_provider)s' in use."
    )


class QuotaNotFound(NotFound):
    _msg_fmt = _("Quota could not be found")


class QuotaUsageNotFound(QuotaNotFound):
    _msg_fmt = _("Quota usage for project %(project_id)s could not be found.")


class QuotaResourceUnknown(QuotaNotFound):
    _msg_fmt = _("Unknown quota resources %(unknown)s.")


class InvalidReservationExpiration(Invalid):
    _msg_fmt = _("Invalid reservation expiration %(expire)s.")


class GlanceConnectionFailed(CyborgException):
    _msg_fmt = _("Connection to glance host %(server)s failed: %(reason)s")


class ImageUnacceptable(Invalid):
    _msg_fmt = _("Image %(image_id)s is unacceptable: %(reason)s")


class ImageNotAuthorized(CyborgException):
    _msg_fmt = _("Not authorized for image %(image_id)s.")


class ImageBadRequest(Invalid):
    _msg_fmt = _(
        "Request of image %(image_id)s got BadRequest response: %(response)s"
    )


class InvalidDriver(Invalid):
    _msg_fmt = _("Found an invalid driver: %(name)s")


class InvalidVGPUType(Invalid):
    _msg_fmt = _("Invalid requested vGPU type: %(name)s")


class InvalidvGPUConfig(Invalid):
    _msg_fmt = _("Invalid vGPU config: %(reason)s")


class PciDeviceWrongAddressFormat(Invalid):
    _msg_fmt = _("The PCI address %(address)s has an incorrect format.")


class InvalidType(Invalid):
    _msg_fmt = _("Invalid type for %(obj)s: %(type)s.Expected: %(expected)s")


class ResourceNotFound(NotFound):
    _msg_fmt = _("%(resource)s not found %(msg)s")


class NotAcceptable(CyborgException):
    _msg_fmt = _("Request not acceptable.")
    code = HTTPStatus.NOT_ACCEPTABLE


class FPGAProgramError(CyborgException):
    _msg_fmt = _("FPGA programming failed with return %(ret)s.")


class PciDeviceNotFoundById(NotFound):
    _msg_fmt = _("PCI device %(id)s not found")


class PciConfigInvalidWhitelist(Invalid):
    _msg_fmt = _("Invalid PCI devices whitelist config: %(reason)s.")


class PciDeviceInvalidDeviceName(CyborgException):
    _msg_fmt = _(
        "Invalid PCI whitelist: The PCI whitelist can specify "
        "devname or address, but not both."
    )
