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

from oslo_config import cfg
from oslo_log import log
from oslo_messaging import exceptions as oslo_msg_exc
from oslo_utils import excutils
from oslo_versionedobjects import exception as oslo_vo_exc

from ironic.common import exception
from ironic.common.i18n import _
from ironic.objects import allocation as allocation_objects
from ironic.objects import chassis as chassis_objects
from ironic.objects import deploy_template as deploy_template_objects
from ironic.objects import fields
from ironic.objects import inspection_rule as inspection_rule_objects
from ironic.objects import node as node_objects
from ironic.objects import notification
from ironic.objects import port as port_objects
from ironic.objects import portgroup as portgroup_objects
from ironic.objects import runbook as runbook_objects
from ironic.objects import volume_connector as volume_connector_objects
from ironic.objects import volume_target as volume_target_objects

LOG = log.getLogger(__name__)
CONF = cfg.CONF


CRUD_NOTIFY_OBJ = {
    'allocation': (allocation_objects.AllocationCRUDNotification,
                   allocation_objects.AllocationCRUDPayload),
    'chassis': (chassis_objects.ChassisCRUDNotification,
                chassis_objects.ChassisCRUDPayload),
    'deploytemplate': (deploy_template_objects.DeployTemplateCRUDNotification,
                       deploy_template_objects.DeployTemplateCRUDPayload),
    'node': (node_objects.NodeCRUDNotification,
             node_objects.NodeCRUDPayload),
    'port': (port_objects.PortCRUDNotification,
             port_objects.PortCRUDPayload),
    'portgroup': (portgroup_objects.PortgroupCRUDNotification,
                  portgroup_objects.PortgroupCRUDPayload),
    'runbook': (runbook_objects.RunbookCRUDNotification,
                runbook_objects.RunbookCRUDPayload),
    'volumeconnector':
        (volume_connector_objects.VolumeConnectorCRUDNotification,
         volume_connector_objects.VolumeConnectorCRUDPayload),
    'volumetarget':
        (volume_target_objects.VolumeTargetCRUDNotification,
         volume_target_objects.VolumeTargetCRUDPayload),
    'inspectionrule':
        (inspection_rule_objects.InspectionRuleCRUDNotification,
         inspection_rule_objects.InspectionRuleCRUDPayload),
}


def _emit_api_notification(context, obj, action, level, status, **kwargs):
    """Helper for emitting API notifications.

    :param context: request context.
    :param obj: resource rpc object.
    :param action: Action string to go in the EventType.
    :param level: Notification level. One of
                  `ironic.objects.fields.NotificationLevel.ALL`
    :param status: Status to go in the EventType. One of
                   `ironic.objects.fields.NotificationStatus.ALL`
    :param kwargs: kwargs to use when creating the notification payload.
    """
    resource = obj.__class__.__name__.lower()
    extra_args = kwargs
    try:
        try:
            if action == 'maintenance_set':
                notification_method = node_objects.NodeMaintenanceNotification
                payload_method = node_objects.NodePayload
            elif resource not in CRUD_NOTIFY_OBJ:
                notification_name = payload_name = _("is not defined")
                raise KeyError(_("Unsupported resource: %s") % resource)
            else:
                notification_method, payload_method = CRUD_NOTIFY_OBJ[resource]

            notification_name = notification_method.__name__
            payload_name = payload_method.__name__
        finally:
            # Prepare our exception message just in case
            exception_values = {"resource": resource,
                                "uuid": obj.uuid,
                                "action": action,
                                "status": status,
                                "level": level,
                                "notification_method": notification_name,
                                "payload_method": payload_name}
            exception_message = (_("Failed to send baremetal.%(resource)s."
                                   "%(action)s.%(status)s notification for "
                                   "%(resource)s %(uuid)s with level "
                                   "%(level)s, notification method "
                                   "%(notification_method)s, payload method "
                                   "%(payload_method)s, error %(error)s"))

        payload = payload_method(obj, **extra_args)
        if resource == 'node':
            notification.mask_secrets(payload)
        notification_method(
            publisher=notification.NotificationPublisher(
                service='ironic-api', host=CONF.host),
            event_type=notification.EventType(
                object=resource, action=action, status=status),
            level=level,
            payload=payload).emit(context)
    except (exception.NotificationSchemaObjectError,
            exception.NotificationSchemaKeyError,
            exception.NotificationPayloadError,
            oslo_msg_exc.MessageDeliveryFailure,
            oslo_vo_exc.VersionedObjectsException) as e:
        exception_values['error'] = e
        LOG.warning(exception_message, exception_values)
    except Exception as e:
        exception_values['error'] = e
        LOG.exception(exception_message, exception_values)


def emit_start_notification(context, obj, action, **kwargs):
    """Helper for emitting API 'start' notifications.

    :param context: request context.
    :param obj: resource rpc object.
    :param action: Action string to go in the EventType.
    :param kwargs: kwargs to use when creating the notification payload.
    """
    _emit_api_notification(context, obj, action,
                           fields.NotificationLevel.INFO,
                           fields.NotificationStatus.START,
                           **kwargs)


@contextlib.contextmanager
def handle_error_notification(context, obj, action, **kwargs):
    """Context manager to handle any error notifications.

    :param context: request context.
    :param obj: resource rpc object.
    :param action: Action string to go in the EventType.
    :param kwargs: kwargs to use when creating the notification payload.
    """
    try:
        yield
    except Exception:
        with excutils.save_and_reraise_exception():
            _emit_api_notification(context, obj, action,
                                   fields.NotificationLevel.ERROR,
                                   fields.NotificationStatus.ERROR,
                                   **kwargs)


def emit_end_notification(context, obj, action, **kwargs):
    """Helper for emitting API 'end' notifications.

    :param context: request context.
    :param obj: resource rpc object.
    :param action: Action string to go in the EventType.
    :param kwargs: kwargs to use when creating the notification payload.
    """
    _emit_api_notification(context, obj, action,
                           fields.NotificationLevel.INFO,
                           fields.NotificationStatus.END,
                           **kwargs)
