# Copyright 2016 NTT DATA
# 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.

"""Handles all processes relating to notifications.

The :py:class:`MasakariManager` class is a
:py:class:`masakari.manager.Manager` that handles RPC calls relating to
notifications. It is responsible for processing notifications and executing
workflows.

"""

from oslo_log import log as logging
import oslo_messaging as messaging
from oslo_service import periodic_task
from oslo_utils import timeutils

import masakari.conf
from masakari.engine import driver
from masakari.engine import instance_events as virt_events
from masakari import exception
from masakari import manager
from masakari import objects
from masakari.objects import fields
from masakari import utils

CONF = masakari.conf.CONF

LOG = logging.getLogger(__name__)


class MasakariManager(manager.Manager):
    """Manages the running notifications"""

    target = messaging.Target(version='1.0')

    def __init__(self, masakari_driver=None, *args, **kwargs):
        """Load configuration options"""
        LOG.debug("Initializing Masakari Manager.")
        super(MasakariManager, self).__init__(service_name="engine",
                                             *args, **kwargs)

        self.driver = driver.load_masakari_driver(masakari_driver)

    def _handle_notification_type_process(self, context, notification):
        notification_status = fields.NotificationStatus.FINISHED
        notification_event = notification.payload.get('event')
        process_name = notification.payload.get('process_name')

        if notification_event.upper() == 'STARTED':
            LOG.info("Notification type '%(type)s' received for host "
                     "'%(host_uuid)s': '%(process_name)s' has been "
                     "%(event)s.",
                     {'type': notification.type,
                      'host_uuid': notification.source_host_uuid,
                      'process_name': process_name,
                      'event': notification_event})
        elif notification_event.upper() == 'STOPPED':
            host_obj = objects.Host.get_by_uuid(
                context, notification.source_host_uuid)
            host_name = host_obj.name

            # Mark host on_maintenance mode as True
            update_data = {
                'on_maintenance': True,
            }
            host_obj.update(update_data)
            host_obj.save()

            try:
                self.driver.execute_process_failure(
                    context, process_name, host_name,
                    notification.notification_uuid)
            except exception.SkipProcessRecoveryException:
                notification_status = fields.NotificationStatus.FINISHED
            except (exception.MasakariException,
                    exception.ProcessRecoveryFailureException):
                notification_status = fields.NotificationStatus.ERROR
        else:
            LOG.warning("Invalid event: %(event)s received for "
                        "notification type: %(notification_type)s",
                        {'event': notification_event,
                         'notification_type': notification.type})
            notification_status = fields.NotificationStatus.IGNORED

        return notification_status

    def _handle_notification_type_instance(self, context, notification):
        if not virt_events.is_valid_event(notification.payload):
            LOG.info("Notification '%(uuid)s' received with payload "
                     "%(payload)s is ignored.",
                     {"uuid": notification.notification_uuid,
                      "payload": notification.payload})
            return fields.NotificationStatus.IGNORED

        notification_status = fields.NotificationStatus.FINISHED
        try:
            self.driver.execute_instance_failure(
                context, notification.payload.get('instance_uuid'),
                notification.notification_uuid)
        except exception.IgnoreInstanceRecoveryException:
            notification_status = fields.NotificationStatus.IGNORED
        except exception.SkipInstanceRecoveryException:
            notification_status = fields.NotificationStatus.FINISHED
        except (exception.MasakariException,
                exception.InstanceRecoveryFailureException):
            notification_status = fields.NotificationStatus.ERROR

        return notification_status

    def _handle_notification_type_host(self, context, notification):
        notification_status = fields.NotificationStatus.FINISHED
        notification_event = notification.payload.get('event')

        if notification_event.upper() == 'STARTED':
            LOG.info("Notification type '%(type)s' received for host "
                     "'%(host_uuid)s' has been %(event)s.",
                     {'type': notification.type,
                      'host_uuid': notification.source_host_uuid,
                      'event': notification_event})
        elif notification_event.upper() == 'STOPPED':
            host_obj = objects.Host.get_by_uuid(
                context, notification.source_host_uuid)
            host_name = host_obj.name
            recovery_method = host_obj.failover_segment.recovery_method

            # Mark host on_maintenance mode as True
            update_data = {
                'on_maintenance': True,
            }

            # Set reserved flag to False if this host is reserved
            if host_obj.reserved:
                update_data['reserved'] = False

            host_obj.update(update_data)
            host_obj.save()

            reserved_host_list = None
            if not recovery_method == (
                    fields.FailoverSegmentRecoveryMethod.AUTO):
                reserved_host_list = objects.HostList.get_all(
                    context, filters={
                        'failover_segment_id': host_obj.failover_segment_id,
                        'reserved': True,
                        'on_maintenance': False
                        })

            try:
                self.driver.execute_host_failure(
                    context, host_name, recovery_method,
                    notification.notification_uuid,
                    reserved_host_list=reserved_host_list)
            except exception.SkipHostRecoveryException:
                notification_status = fields.NotificationStatus.FINISHED
            except (exception.HostRecoveryFailureException,
                    exception.ReservedHostsUnavailable,
                    exception.MasakariException):
                notification_status = fields.NotificationStatus.ERROR
        else:
            LOG.warning("Invalid event: %(event)s received for "
                        "notification type: %(type)s",
                        {'event': notification_event,
                         'type': notification.type})
            notification_status = fields.NotificationStatus.IGNORED

        return notification_status

    def _process_notification(self, context, notification):
        @utils.synchronized(notification.source_host_uuid, blocking=True)
        def do_process_notification(notification):
            LOG.info('Processing notification %(notification_uuid)s of '
                     'type: %(type)s',
                     {'notification_uuid': notification.notification_uuid,
                      'type': notification.type})

            update_data = {
                'status': fields.NotificationStatus.RUNNING,
            }
            notification.update(update_data)
            notification.save()

            if notification.type == fields.NotificationType.PROCESS:
                notification_status = self._handle_notification_type_process(
                    context, notification)
            elif notification.type == fields.NotificationType.VM:
                notification_status = self._handle_notification_type_instance(
                    context, notification)
            elif notification.type == fields.NotificationType.COMPUTE_HOST:
                notification_status = self._handle_notification_type_host(
                    context, notification)

            LOG.info("Notification %(notification_uuid)s exits with "
                     "status: %(status)s.",
                     {'notification_uuid': notification.notification_uuid,
                      'status': notification_status})

            update_data = {
                'status': notification_status
            }
            notification.update(update_data)
            notification.save()

        do_process_notification(notification)

    def process_notification(self, context, notification=None):
        """Processes the notification"""
        self._process_notification(context, notification)

    @periodic_task.periodic_task(
        spacing=CONF.process_unfinished_notifications_interval)
    def _process_unfinished_notifications(self, context):
        filters = {
            'status': [fields.NotificationStatus.ERROR,
                       fields.NotificationStatus.NEW]
        }
        notifications_list = objects.NotificationList.get_all(context,
                                                              filters=filters)

        for notification in notifications_list:
            if (notification.status == fields.NotificationStatus.ERROR or
                    (notification.status == fields.NotificationStatus.NEW and
                timeutils.is_older_than(
                    notification.generated_time,
                    CONF.retry_notification_new_status_interval))):
                self._process_notification(context, notification)

            # get updated notification from db after workflow execution
            notification_db = objects.Notification.get_by_uuid(
                context, notification.notification_uuid)

            if notification_db.status == fields.NotificationStatus.ERROR:
                # update notification status as failed
                notification_status = fields.NotificationStatus.FAILED
                update_data = {
                    'status': notification_status
                }

                notification_db.update(update_data)
                notification_db.save()
                LOG.error(
                    "Periodic task 'process_unfinished_notifications': "
                    "Notification %(notification_uuid)s exits with "
                    "status: %(status)s.",
                    {'notification_uuid': notification.notification_uuid,
                     'status': notification_status})
