# Copyright 2019 Intel, Inc.
#
# 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 cyborg.common import exception
from cyborg.common.i18n import _
from cyborg.common import utils
from oslo_log import log as logging

LOG = logging.getLogger(__name__)


class NovaAPI(object):
    def __init__(self):
        self.nova_client = utils.get_sdk_adapter('compute')
        self.nova_client.default_microversion = '2.82'

    def _get_acc_changed_events(self, instance_uuid, arq_bind_statuses):
        return [{'name': 'accelerator-request-bound',
                 'server_uuid': instance_uuid,
                 'tag': arq_uuid,
                 'status': arq_bind_status,
                 } for (arq_uuid, arq_bind_status) in arq_bind_statuses]

    def _send_events(self, events):
        """Send events to Nova external events API.

        :param events: List of events to send to Nova.
        :raises: exception.InvalidAPIResponse, on unexpected error
        """
        url = "/os-server-external-events"
        body = {"events": events}
        response = self.nova_client.post(url, json=body)
        # NOTE(Sundar): Response status should always be 200/207. See
        # https://review.opendev.org/#/c/698037/
        if response.status_code == 200:
            LOG.info("Sucessfully sent events to Nova, events: %(events)s",
                     {"events": events})
        elif response.status_code == 207:
            # NOTE(Sundar): If Nova returns per-event code of 422, that
            # is due to a race condition where Nova has not associated
            # the instance with a host yet. See
            # https://bugs.launchpad.net/nova/+bug/1855752
            events = [ev for ev in response.json()['events']]
            event_codes = {ev['code'] for ev in events}
            if len(event_codes) == 1:  # all events have same event code
                if event_codes == {422}:
                    LOG.info('Ignoring Nova notification error that the '
                             'instance %s is not yet associated with a host.',
                             events[0]['server_uuid'])
                else:
                    msg = _('Unexpected event code %(code)s '
                            'for instance %(inst)s')
                    msg = msg % {'code': event_codes.pop(),
                                 'inst': events[0]["server_uuid"]}
                    raise exception.InvalidAPIResponse(
                        service='Nova', api=url[1:], msg=msg)
            else:
                msg = _('All event responses are expected to '
                        'have the same event code. Instance: %(inst)s')
                msg = msg % {'inst': events[0]['server_uuid']}
                raise exception.InvalidAPIResponse(
                    service='Nova', api=url[1:], msg=msg)
        else:
            # Unexpected return code from Nova
            msg = _('Failed to send events %(ev)s: HTTP %(code)s: %(txt)s')
            msg = msg % {'ev': events,
                         'code': response.status_code,
                         'txt': response.text}
            raise exception.InvalidAPIResponse(
                service='Nova', api=url[1:], msg=msg)

    def notify_binding(self, instance_uuid, arq_bind_statuses):
        """Notify Nova that ARQ bindings are resolved for a given instance.

        :param instance_uuid: UUID of the instance whose ARQs are resolved
        :param arq_bind_statuses: List of (arq_uuid, arq_bind_status) tuples
        :returns: None
        """
        events = self._get_acc_changed_events(instance_uuid, arq_bind_statuses)
        self._send_events(events)
