#    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.
"""Handlers for placement API.

Individual handlers are associated with URL paths in the
ROUTE_DECLARATIONS dictionary. At the top level each key is a Routes
compliant path. The value of that key is a dictionary mapping
individual HTTP request methods to a Python function representing a
simple WSGI application for satisfying that request.

The ``make_map`` method processes ROUTE_DECLARATIONS to create a
Routes.Mapper, including automatic handlers to respond with a
405 when a request is made against a valid URL with an invalid
method.
"""

import routes
import webob

from oslo_log import log as logging

from placement import exception
from placement.handlers import aggregate
from placement.handlers import allocation
from placement.handlers import allocation_candidate
from placement.handlers import inventory
from placement.handlers import reshaper
from placement.handlers import resource_class
from placement.handlers import resource_provider
from placement.handlers import root
from placement.handlers import trait
from placement.handlers import usage
from placement import util

LOG = logging.getLogger(__name__)

# URLs and Handlers
# NOTE(cdent): When adding URLs here, do not use regex patterns in
# the path parameters (e.g. {uuid:[0-9a-zA-Z-]+}) as that will lead
# to 404s that are controlled outside of the individual resources
# and thus do not include specific information on the why of the 404.
ROUTE_DECLARATIONS = {
    '/': {
        'GET': root.home,
    },
    # NOTE(cdent): This allows '/placement/' and '/placement' to
    # both work as the root of the service, which we probably want
    # for those situations where the service is mounted under a
    # prefix (as it is in devstack). While weird, an empty string is
    # a legit key in a dictionary and matches as desired in Routes.
    '': {
        'GET': root.home,
    },
    '/resource_classes': {
        'GET': resource_class.list_resource_classes,
        'POST': resource_class.create_resource_class
    },
    '/resource_classes/{name}': {
        'GET': resource_class.get_resource_class,
        'PUT': resource_class.update_resource_class,
        'DELETE': resource_class.delete_resource_class,
    },
    '/resource_providers': {
        'GET': resource_provider.list_resource_providers,
        'POST': resource_provider.create_resource_provider
    },
    '/resource_providers/{uuid}': {
        'GET': resource_provider.get_resource_provider,
        'DELETE': resource_provider.delete_resource_provider,
        'PUT': resource_provider.update_resource_provider
    },
    '/resource_providers/{uuid}/inventories': {
        'GET': inventory.get_inventories,
        'POST': inventory.create_inventory,
        'PUT': inventory.set_inventories,
        'DELETE': inventory.delete_inventories
    },
    '/resource_providers/{uuid}/inventories/{resource_class}': {
        'GET': inventory.get_inventory,
        'PUT': inventory.update_inventory,
        'DELETE': inventory.delete_inventory
    },
    '/resource_providers/{uuid}/usages': {
        'GET': usage.list_usages
    },
    '/resource_providers/{uuid}/aggregates': {
        'GET': aggregate.get_aggregates,
        'PUT': aggregate.set_aggregates
    },
    '/resource_providers/{uuid}/allocations': {
        'GET': allocation.list_for_resource_provider,
    },
    '/allocations': {
        'POST': allocation.set_allocations,
    },
    '/allocations/{consumer_uuid}': {
        'GET': allocation.list_for_consumer,
        'PUT': allocation.set_allocations_for_consumer,
        'DELETE': allocation.delete_allocations,
    },
    '/allocation_candidates': {
        'GET': allocation_candidate.list_allocation_candidates,
    },
    '/traits': {
        'GET': trait.list_traits,
    },
    '/traits/{name}': {
        'GET': trait.get_trait,
        'PUT': trait.put_trait,
        'DELETE': trait.delete_trait,
    },
    '/resource_providers/{uuid}/traits': {
        'GET': trait.list_traits_for_resource_provider,
        'PUT': trait.update_traits_for_resource_provider,
        'DELETE': trait.delete_traits_for_resource_provider
    },
    '/usages': {
        'GET': usage.get_total_usages,
    },
    '/reshaper': {
        'POST': reshaper.reshape,
    },
}


def dispatch(environ, start_response, mapper):
    """Find a matching route for the current request.

    If no match is found, raise a 404 response.
    If there is a matching route, but no matching handler
    for the given method, raise a 405.
    """
    result = mapper.match(environ=environ)
    if result is None:
        raise webob.exc.HTTPNotFound(
            json_formatter=util.json_error_formatter)
    # We can't reach this code without action being present.
    handler = result.pop('action')
    environ['wsgiorg.routing_args'] = ((), result)
    return handler(environ, start_response)


def handle_405(environ, start_response):
    """Return a 405 response when method is not allowed.

    If _methods are in routing_args, send an allow header listing
    the methods that are possible on the provided URL.
    """
    _methods = util.wsgi_path_item(environ, '_methods')
    headers = {}
    if _methods:
        # Ensure allow header is a python 2 or 3 native string (thus
        # not unicode in python 2 but stay a string in python 3)
        # In the process done by Routes to save the allowed methods
        # to its routing table they become unicode in py2.
        headers['allow'] = str(_methods)
    # Use Exception class as WSGI Application. We don't want to raise here.
    response = webob.exc.HTTPMethodNotAllowed(
        'The method specified is not allowed for this resource.',
        headers=headers, json_formatter=util.json_error_formatter)
    return response(environ, start_response)


def make_map(declarations):
    """Process route declarations to create a Route Mapper."""
    mapper = routes.Mapper()
    for route, targets in declarations.items():
        allowed_methods = []
        for method in targets:
            mapper.connect(route, action=targets[method],
                           conditions=dict(method=[method]))
            allowed_methods.append(method)
        allowed_methods = ', '.join(allowed_methods)
        mapper.connect(route, action=handle_405, _methods=allowed_methods)
    return mapper


class PlacementHandler(object):
    """Serve Placement API.

    Dispatch to handlers defined in ROUTE_DECLARATIONS.
    """

    def __init__(self, **local_config):
        self._map = make_map(ROUTE_DECLARATIONS)
        self.config = local_config['config']

    def __call__(self, environ, start_response):
        # set a reference to the oslo.config ConfigOpts on the RequestContext
        context = environ['placement.context']
        context.config = self.config
        # Check that an incoming request with a content-length header
        # that is an integer > 0 and not empty, also has a content-type
        # header that is not empty. If not raise a 400.
        clen = environ.get('CONTENT_LENGTH')
        try:
            if clen and (int(clen) > 0) and not environ.get('CONTENT_TYPE'):
                raise webob.exc.HTTPBadRequest(
                    'content-type header required when content-length > 0',
                    json_formatter=util.json_error_formatter)
        except ValueError:
            raise webob.exc.HTTPBadRequest(
                'content-length header must be an integer',
                json_formatter=util.json_error_formatter)
        try:
            return dispatch(environ, start_response, self._map)
        # Trap the NotFound exceptions raised by the objects used
        # with the API and transform them into webob.exc.HTTPNotFound.
        except exception.NotFound as exc:
            raise webob.exc.HTTPNotFound(
                exc, json_formatter=util.json_error_formatter)
        except exception.PolicyNotAuthorized as exc:
            raise webob.exc.HTTPForbidden(
                exc.format_message(),
                json_formatter=util.json_error_formatter)
        # Remaining uncaught exceptions will rise first to the Microversion
        # middleware, where any WebOb generated exceptions will be caught and
        # transformed into legit HTTP error responses (with microversion
        # headers added), and then to the FaultWrapper middleware which will
        # catch anything else and transform them into 500 responses.
        # NOTE(cdent): There should be very few uncaught exceptions which are
        # not WebOb exceptions at this stage as the handlers are contained by
        # the wsgify decorator which will transform those exceptions to
        # responses itself.
