# Copyright (c) 2016 b<>com
#
#
# 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 abc

from oslo_log import log
from taskflow import task as flow_task

from watcher._i18n import _
from watcher.applier.actions import factory
from watcher.common import clients
from watcher.common import exception
from watcher.common.loader import loadable
from watcher import notifications
from watcher import objects
from watcher.objects import fields


LOG = log.getLogger(__name__)


class BaseWorkFlowEngine(loadable.Loadable, metaclass=abc.ABCMeta):

    def __init__(self, config, context=None, applier_manager=None):
        """Constructor

        :param config: A mapping containing the configuration of this
                       workflow engine
        :type config: dict
        :param osc: an OpenStackClients object, defaults to None
        :type osc: :py:class:`~.OpenStackClients` instance, optional
        """
        super().__init__(config)
        self._context = context
        self._applier_manager = applier_manager
        self._action_factory = factory.ActionFactory()
        self._osc = None
        self._is_notified = False
        self.execution_rule = None

    @classmethod
    def get_config_opts(cls):
        """Defines the configuration options to be associated to this loadable

        :return: A list of configuration options relative to this Loadable
        :rtype: list of :class:`oslo_config.cfg.Opt` instances
        """
        return []

    @property
    def context(self):
        return self._context

    @property
    def osc(self):
        if not self._osc:
            self._osc = clients.OpenStackClients()
        return self._osc

    @property
    def applier_manager(self):
        return self._applier_manager

    @property
    def action_factory(self):
        return self._action_factory

    def notify(self, action, state, status_message=None):
        db_action = objects.Action.get_by_uuid(self.context, action.uuid,
                                               eager=True)
        db_action.state = state
        if status_message:
            db_action.status_message = status_message
        db_action.save()
        return db_action

    def notify_cancel_start(self, action_plan_uuid):
        action_plan = objects.ActionPlan.get_by_uuid(self.context,
                                                     action_plan_uuid,
                                                     eager=True)
        if not self._is_notified:
            self._is_notified = True
            notifications.action_plan.send_cancel_notification(
                self._context, action_plan,
                action=fields.NotificationAction.CANCEL,
                phase=fields.NotificationPhase.START)

    @abc.abstractmethod
    def execute(self, actions):
        raise NotImplementedError()


class BaseTaskFlowActionContainer(flow_task.Task):

    def __init__(self, name, db_action, engine, **kwargs):
        super().__init__(name=name)
        self._db_action = db_action
        self._engine = engine
        self.loaded_action = None

    @property
    def engine(self):
        return self._engine

    @property
    def action(self):
        if self.loaded_action is None:
            action = self.engine.action_factory.make_action(
                self._db_action,
                osc=self._engine.osc)
            self.loaded_action = action
        return self.loaded_action

    @abc.abstractmethod
    def do_pre_execute(self):
        raise NotImplementedError()

    @abc.abstractmethod
    def do_execute(self, *args, **kwargs):
        raise NotImplementedError()

    @abc.abstractmethod
    def do_post_execute(self):
        raise NotImplementedError()

    @abc.abstractmethod
    def do_revert(self):
        raise NotImplementedError()

    @abc.abstractmethod
    def do_abort(self, *args, **kwargs):
        raise NotImplementedError()

    def _fail_action(self, phase, reason=None):
        # Fail action and send notification to the user.
        # If a reason is given, it will be used to set the status_message.
        LOG.error('The workflow engine has failed '
                  'to execute the action: %s', self._db_action.uuid)
        kwargs = {}
        if reason:
            kwargs["status_message"] = (_(
                "Action failed in %s: %s") % (phase, reason))
        db_action = self.engine.notify(self._db_action,
                                       objects.action.State.FAILED,
                                       **kwargs)
        notifications.action.send_execution_notification(
            self.engine.context, db_action,
            fields.NotificationAction.EXECUTION,
            fields.NotificationPhase.ERROR,
            priority=fields.NotificationPriority.ERROR)

    # NOTE(alexchadin): taskflow does 3 method calls (pre_execute, execute,
    # post_execute) independently. We want to support notifications in base
    # class, so child's methods should be named with `do_` prefix and wrapped.
    def pre_execute(self):
        try:
            # NOTE(adisky): check the state of action plan before starting
            # next action, if action plan is cancelled raise the exceptions
            # so that taskflow does not schedule further actions.
            action_plan = objects.ActionPlan.get_by_id(
                self.engine.context, self._db_action.action_plan_id)
            if action_plan.state in objects.action_plan.State.CANCEL_STATES:
                raise exception.ActionPlanCancelled(uuid=action_plan.uuid)
            if self._db_action.state == objects.action.State.SKIPPED:
                LOG.debug("Action %s is skipped manually",
                          self._db_action.uuid)
                return
            db_action = self.do_pre_execute()
            notifications.action.send_execution_notification(
                self.engine.context, db_action,
                fields.NotificationAction.EXECUTION,
                fields.NotificationPhase.START)
        except exception.ActionPlanCancelled as e:
            LOG.exception(e)
            self.engine.notify_cancel_start(action_plan.uuid)
            raise
        except exception.ActionSkipped as e:
            LOG.info("Action %s was skipped automatically: %s",
                     self._db_action.uuid, str(e))
            status_message = (_(
                "Action was skipped automatically: %s") % str(e))
            db_action = self.engine.notify(self._db_action,
                                           objects.action.State.SKIPPED,
                                           status_message=status_message)
            notifications.action.send_update(
                self.engine.context, db_action,
                old_state=objects.action.State.PENDING)
        except exception.WatcherException as e:
            LOG.exception(e)
            self._fail_action("pre_condition", reason=str(e))
        except Exception as e:
            LOG.exception(e)
            self._fail_action("pre_condition", reason=type(e).__name__)

    def execute(self, *args, **kwargs):
        action_object = objects.Action.get_by_uuid(
            self.engine.context, self._db_action.uuid, eager=True)
        if action_object.state in [objects.action.State.SKIPPED,
                                   objects.action.State.FAILED]:
            return True

        try:
            db_action = self.do_execute(*args, **kwargs)
            notifications.action.send_execution_notification(
                self.engine.context, db_action,
                fields.NotificationAction.EXECUTION,
                fields.NotificationPhase.END)
            action_object = objects.Action.get_by_uuid(
                self.engine.context, self._db_action.uuid, eager=True)
            if action_object.state == objects.action.State.SUCCEEDED:
                return True
            else:
                return False
        except exception.WatcherException as e:
            LOG.exception(e)
            self._fail_action("execute", reason=str(e))
            return False
        except Exception as e:
            LOG.exception(e)
            self._fail_action("execute", reason=type(e).__name__)
            return False

    def post_execute(self):
        action_object = objects.Action.get_by_uuid(
            self.engine.context, self._db_action.uuid, eager=True)
        if action_object.state == objects.action.State.SKIPPED:
            return
        try:
            self.do_post_execute()
        except exception.WatcherException as e:
            LOG.exception(e)
            # We only add status_message in failed post_condition if the
            # action has no status_message yet. Do not override if one
            # has been added in the execute method.
            if action_object.status_message is None:
                reason = str(e)
            else:
                reason = None
            self._fail_action("post_condition", reason=reason)
        except Exception as e:
            LOG.exception(e)
            # We only add status_message in failed post_condition if the
            # action has no status_message yet. Do not override if one
            # has been added in the execute method.
            if action_object.status_message is None:
                reason = type(e).__name__
            else:
                reason = None
            self._fail_action("post_condition", reason=reason)

    def revert(self, *args, **kwargs):
        action_plan = objects.ActionPlan.get_by_id(
            self.engine.context, self._db_action.action_plan_id, eager=True)
        action_object = objects.Action.get_by_uuid(
            self.engine.context, self._db_action.uuid, eager=True)

        # NOTE: check if revert cause by cancel action plan or
        # some other exception occurred during action plan execution
        # if due to some other exception keep the flow intact.
        # NOTE(dviroel): If the action was skipped, we should not
        # revert it.
        if (action_plan.state not in
                objects.action_plan.State.CANCEL_STATES and
                action_object.state != objects.action.State.SKIPPED):
            self.do_revert()
            return

        try:
            if action_object.state == objects.action.State.ONGOING:
                action_object.state = objects.action.State.CANCELLING
                action_object.save()
                notifications.action.send_cancel_notification(
                    self.engine.context, action_object,
                    fields.NotificationAction.CANCEL,
                    fields.NotificationPhase.START)
                action_object = self.abort()

                notifications.action.send_cancel_notification(
                    self.engine.context, action_object,
                    fields.NotificationAction.CANCEL,
                    fields.NotificationPhase.END)

            if action_object.state == objects.action.State.PENDING:
                notifications.action.send_cancel_notification(
                    self.engine.context, action_object,
                    fields.NotificationAction.CANCEL,
                    fields.NotificationPhase.START)
                action_object.state = objects.action.State.CANCELLED
                action_object.save()
                notifications.action.send_cancel_notification(
                    self.engine.context, action_object,
                    fields.NotificationAction.CANCEL,
                    fields.NotificationPhase.END)

        except Exception as e:
            LOG.exception(e)
            action_object.state = objects.action.State.FAILED
            action_object.save()
            notifications.action.send_cancel_notification(
                self.engine.context, action_object,
                fields.NotificationAction.CANCEL,
                fields.NotificationPhase.ERROR,
                priority=fields.NotificationPriority.ERROR)

    def abort(self, *args, **kwargs):
        # NOTE(dviroel): only ONGOING actions are called
        # to abort the operation.
        return self.do_abort(*args, **kwargs)
