# Copyright 2015 - Mirantis, Inc.
# Copyright 2015 - StackStorm, Inc.
# Copyright 2016 - Nokia Networks.
# Copyright 2016 - Brocade Communications Systems, 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 oslo_log import log as logging
from osprofiler import profiler
import traceback as tb

from mistral.db.v2 import api as db_api
from mistral.db.v2.sqlalchemy import models
from mistral.engine import tasks
from mistral.engine import workflow_handler as wf_handler
from mistral import exceptions as exc
from mistral.services import scheduler
from mistral.workbook import parser as spec_parser
from mistral.workflow import base as wf_base
from mistral.workflow import commands as wf_cmds
from mistral.workflow import states


"""Responsible for running tasks and handling results."""

LOG = logging.getLogger(__name__)

_REFRESH_TASK_STATE_PATH = (
    'mistral.engine.task_handler._refresh_task_state'
)

_SCHEDULED_ON_ACTION_COMPLETE_PATH = (
    'mistral.engine.task_handler._scheduled_on_action_complete'
)


@profiler.trace('task-handler-run-task')
def run_task(wf_cmd):
    """Runs workflow task.

    :param wf_cmd: Workflow command.
    """

    task = _build_task_from_command(wf_cmd)

    try:
        task.run()
    except exc.MistralException as e:
        wf_ex = wf_cmd.wf_ex
        task_spec = wf_cmd.task_spec

        msg = (
            "Failed to run task [wf=%s, task=%s]: %s\n%s" %
            (wf_ex, task_spec.get_name(), e, tb.format_exc())
        )

        LOG.error(msg)

        task.set_state(states.ERROR, msg)

        wf_handler.fail_workflow(wf_ex, msg)

        return

    if not task.task_ex:
        # It is possible that task execution was not created
        # (and therefore not associated with Task instance).
        # For example, in case of 'join' that has already been
        # created by a different transaction. In this case
        # we should skip post completion scheduled checks.
        return

    if task.is_waiting():
        _schedule_refresh_task_state(task.task_ex)

    if task.is_completed():
        wf_handler.schedule_on_task_complete(task.task_ex)


@profiler.trace('task-handler-on-action-complete')
def _on_action_complete(action_ex):
    """Handles action completion event.

    :param action_ex: Action execution.
    """

    task_ex = action_ex.task_execution

    if not task_ex:
        return

    task_spec = spec_parser.get_task_spec(task_ex.spec)

    wf_ex = task_ex.workflow_execution

    task = _create_task(
        wf_ex,
        spec_parser.get_workflow_spec_by_execution_id(wf_ex.id),
        task_spec,
        task_ex.in_context,
        task_ex
    )

    try:
        task.on_action_complete(action_ex)
    except exc.MistralException as e:
        wf_ex = task_ex.workflow_execution

        msg = ("Failed to handle action completion [wf=%s, task=%s,"
               " action=%s]: %s\n%s" %
               (wf_ex.name, task_ex.name, action_ex.name, e, tb.format_exc()))

        LOG.error(msg)

        task.set_state(states.ERROR, msg)

        wf_handler.fail_workflow(wf_ex, msg)

        return

    if task.is_completed():
        wf_handler.schedule_on_task_complete(task_ex)


def fail_task(task_ex, msg):
    wf_spec = spec_parser.get_workflow_spec_by_execution_id(
        task_ex.workflow_execution_id
    )

    task = _build_task_from_execution(wf_spec, task_ex)

    task.set_state(states.ERROR, msg)

    wf_handler.fail_workflow(task_ex.workflow_execution, msg)


def continue_task(task_ex):
    wf_spec = spec_parser.get_workflow_spec_by_execution_id(
        task_ex.workflow_execution_id
    )

    task = _build_task_from_execution(wf_spec, task_ex)

    try:
        task.set_state(states.RUNNING, None)

        task.run()
    except exc.MistralException as e:
        wf_ex = task_ex.workflow_execution

        msg = (
            "Failed to run task [wf=%s, task=%s]: %s\n%s" %
            (wf_ex, task_ex.name, e, tb.format_exc())
        )

        LOG.error(msg)

        task.set_state(states.ERROR, msg)

        wf_handler.fail_workflow(wf_ex, msg)

        return

    if task.is_completed():
        wf_handler.schedule_on_task_complete(task_ex)


def complete_task(task_ex, state, state_info):
    wf_spec = spec_parser.get_workflow_spec_by_execution_id(
        task_ex.workflow_execution_id
    )

    task = _build_task_from_execution(wf_spec, task_ex)

    try:
        task.complete(state, state_info)
    except exc.MistralException as e:
        wf_ex = task_ex.workflow_execution

        msg = (
            "Failed to complete task [wf=%s, task=%s]: %s\n%s" %
            (wf_ex, task_ex.name, e, tb.format_exc())
        )

        LOG.error(msg)

        task.set_state(states.ERROR, msg)

        wf_handler.fail_workflow(wf_ex, msg)

        return

    if task.is_completed():
        wf_handler.schedule_on_task_complete(task_ex)


def _build_task_from_execution(wf_spec, task_ex, task_spec=None):
    return _create_task(
        task_ex.workflow_execution,
        wf_spec,
        task_spec or spec_parser.get_task_spec(task_ex.spec),
        task_ex.in_context,
        task_ex
    )


@profiler.trace('task-handler-build-task-from-command')
def _build_task_from_command(cmd):
    if isinstance(cmd, wf_cmds.RunExistingTask):
        task = _create_task(
            cmd.wf_ex,
            cmd.wf_spec,
            spec_parser.get_task_spec(cmd.task_ex.spec),
            cmd.ctx,
            task_ex=cmd.task_ex,
            unique_key=cmd.task_ex.unique_key
        )

        if cmd.reset:
            task.reset()

        return task

    if isinstance(cmd, wf_cmds.RunTask):
        task = _create_task(
            cmd.wf_ex,
            cmd.wf_spec,
            cmd.task_spec,
            cmd.ctx,
            unique_key=cmd.unique_key
        )

        if cmd.is_waiting():
            task.defer()

        return task

    raise exc.MistralError('Unsupported workflow command: %s' % cmd)


def _create_task(wf_ex, wf_spec, task_spec, ctx, task_ex=None,
                 unique_key=None):
    if task_spec.get_with_items():
        return tasks.WithItemsTask(
            wf_ex,
            wf_spec,
            task_spec,
            ctx,
            task_ex,
            unique_key
        )

    return tasks.RegularTask(
        wf_ex,
        wf_spec,
        task_spec,
        ctx,
        task_ex,
        unique_key
    )


def _refresh_task_state(task_ex_id):
    with db_api.transaction():
        task_ex = db_api.get_task_execution(task_ex_id)

        wf_spec = spec_parser.get_workflow_spec_by_execution_id(
            task_ex.workflow_execution_id
        )

        wf_ctrl = wf_base.get_controller(
            task_ex.workflow_execution,
            wf_spec
        )

        state, state_info = wf_ctrl.get_logical_task_state(task_ex)

        if state == states.RUNNING:
            continue_task(task_ex)
        elif state == states.ERROR:
            fail_task(task_ex, state_info)
        elif state == states.WAITING:
            # TODO(rakhmerov): Algorithm for increasing rescheduling delay.
            _schedule_refresh_task_state(task_ex, 1)
        else:
            # Must never get here.
            raise RuntimeError(
                'Unexpected logical task state [task_ex=%s, state=%s]' %
                (task_ex, state)
            )


def _schedule_refresh_task_state(task_ex, delay=0):
    """Schedules task preconditions check.

    This method provides transactional decoupling of task preconditions
    check from events that can potentially satisfy those preconditions.

    It's needed in non-locking model in order to avoid 'phantom read'
    phenomena when reading state of multiple tasks to see if a task that
    depends on them can start. Just starting a separate transaction
    without using scheduler is not safe due to concurrency window that
    we'll have in this case (time between transactions) whereas scheduler
    is a special component that is designed to be resistant to failures.

    :param task_ex: Task execution.
    :param delay: Delay.
    :return:
    """
    key = 'th_c_t_s_a-%s' % task_ex.id

    scheduler.schedule_call(
        None,
        _REFRESH_TASK_STATE_PATH,
        delay,
        unique_key=key,
        task_ex_id=task_ex.id
    )


def _scheduled_on_action_complete(action_ex_id, wf_action):
    with db_api.transaction():
        if wf_action:
            action_ex = db_api.get_workflow_execution(action_ex_id)
        else:
            action_ex = db_api.get_action_execution(action_ex_id)

        _on_action_complete(action_ex)


def schedule_on_action_complete(action_ex, delay=0):
    """Schedules task completion check.

    This method provides transactional decoupling of action completion from
    task completion check. It's needed in non-locking model in order to
    avoid 'phantom read' phenomena when reading state of multiple actions
    to see if a task is completed. Just starting a separate transaction
    without using scheduler is not safe due to concurrency window that we'll
    have in this case (time between transactions) whereas scheduler is a
    special component that is designed to be resistant to failures.

    :param action_ex: Action execution.
    :param delay: Minimum amount of time before task completion check
        should be made.
    """

    # Optimization to avoid opening a new transaction if it's not needed.
    if not action_ex.task_execution.spec.get('with-items'):
        _on_action_complete(action_ex)

        return

    key = 'th_on_a_c-%s' % action_ex.task_execution_id

    scheduler.schedule_call(
        None,
        _SCHEDULED_ON_ACTION_COMPLETE_PATH,
        delay,
        unique_key=key,
        action_ex_id=action_ex.id,
        wf_action=isinstance(action_ex, models.WorkflowExecution)
    )
