# Copyright (c) 2015 Mirantis, 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 neutron_lib import constants
from oslo_config import cfg
from oslo_log import log
from oslo_serialization import jsonutils
from pecan import hooks

from neutron.api.rpc.agentnotifiers import dhcp_rpc_agent_api
from neutron.common import rpc as n_rpc
from neutron import manager
from neutron.pecan_wsgi import constants as pecan_constants
from neutron.pecan_wsgi.hooks import utils

LOG = log.getLogger(__name__)


class NotifierHook(hooks.PecanHook):
    priority = 135

    @property
    def _notifier(self):
        if not hasattr(self, '_notifier_inst'):
            self._notifier_inst = n_rpc.get_notifier('network')
        return self._notifier_inst

    def _nova_notify(self, action, resource, *args):
        action_resource = '%s_%s' % (action, resource)
        if not hasattr(self, '_nova_notifier'):
            # this is scoped to avoid a dependency on nova client when nova
            # notifications aren't enabled
            from neutron.notifiers import nova
            self._nova_notifier = nova.Notifier.get_instance()
        self._nova_notifier.send_network_change(action_resource, *args)

    def _notify_dhcp_agent(self, context, resource_name, action, resources):
        # NOTE(kevinbenton): we should remove this whole method in Ocata and
        # make plugins emit the core resource events
        plugin = manager.NeutronManager.get_plugin_for_resource(resource_name)
        notifier_method = '%s.%s.end' % (resource_name, action)
        # use plugin's dhcp notifier, if this is already instantiated
        agent_notifiers = getattr(plugin, 'agent_notifiers', {})
        dhcp_agent_notifier = (
            agent_notifiers.get(constants.AGENT_TYPE_DHCP) or
            dhcp_rpc_agent_api.DhcpAgentNotifyAPI()
        )
        native_map = getattr(dhcp_agent_notifier, 'uses_native_notifications',
                             {})
        if native_map.get(resource_name, {}).get(action):
            return
        # The DHCP Agent does not accept bulk notifications
        for resource in resources:
            item = {resource_name: resource}
            LOG.debug("Sending DHCP agent notification for: %s", item)
            dhcp_agent_notifier.notify(context, item, notifier_method)

    def before(self, state):
        if state.request.method not in ('POST', 'PUT', 'DELETE'):
            return
        resource = state.request.context.get('resource')
        if not resource:
            return
        if utils.is_member_action(utils.get_controller(state)):
            return
        action = pecan_constants.ACTION_MAP.get(state.request.method)
        event = '%s.%s.start' % (resource, action)
        if action in ('create', 'update'):
            # notifier just gets plain old body without any treatment other
            # than the population of the object ID being operated on
            payload = state.request.json.copy()
            if action == 'update':
                payload['id'] = state.request.context.get('resource_id')
        elif action == 'delete':
            resource_id = state.request.context.get('resource_id')
            payload = {resource + '_id': resource_id}
        self._notifier.info(state.request.context.get('neutron_context'),
                            event, payload)

    def after(self, state):
        resource_name = state.request.context.get('resource')
        collection_name = state.request.context.get('collection')
        neutron_context = state.request.context.get('neutron_context')
        if not resource_name:
            LOG.debug("Skipping NotifierHook processing as there was no "
                      "resource associated with the request")
            return
        action = pecan_constants.ACTION_MAP.get(state.request.method)
        if not action or action == 'get':
            LOG.debug("No notification will be sent for action: %s", action)
            return
        if utils.is_member_action(utils.get_controller(state)):
            return
        if state.response.status_int > 300:
            LOG.debug("No notification will be sent due to unsuccessful "
                      "status code: %s", state.response.status_int)
            return

        if action == 'delete':
            # The object has been deleted, so we must notify the agent with the
            # data of the original object
            data = {collection_name:
                    state.request.context.get('original_resources', [])}
        else:
            try:
                data = jsonutils.loads(state.response.body)
            except ValueError:
                if not state.response.body:
                    data = {}
        resources = []
        if data:
            if resource_name in data:
                resources = [data[resource_name]]
            elif collection_name in data:
                # This was a bulk request
                resources = data[collection_name]
        # Send a notification only if a resource can be identified in the
        # response. This means that for operations such as add_router_interface
        # no notification will be sent
        if cfg.CONF.dhcp_agent_notification and data:
            self._notify_dhcp_agent(
                neutron_context, resource_name,
                action, resources)
        if cfg.CONF.notify_nova_on_port_data_changes:
            orig = {}
            if action == 'update':
                orig = state.request.context.get('original_resources')[0]
            elif action == 'delete':
                # NOTE(kevinbenton): the nova notifier is a bit strange because
                # it expects the original to be in the last argument on a
                # delete rather than in the 'original_obj' position
                resources = (
                    state.request.context.get('original_resources') or [])
            for resource in resources:
                self._nova_notify(action, resource_name, orig,
                                  {resource_name: resource})

        event = '%s.%s.end' % (resource_name, action)
        if action == 'delete':
            resource_id = state.request.context.get('resource_id')
            payload = {resource_name + '_id': resource_id}
        elif action in ('create', 'update'):
            if not resources:
                # create/update did not complete so no notification
                return
            if len(resources) > 1:
                payload = {collection_name: resources}
            else:
                payload = {resource_name: resources[0]}
        else:
            return
        self._notifier.info(neutron_context, event, payload)
