# Copyright (c) 2013-2014 Rackspace, 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.

"""
Server-side (i.e. worker side) classes and logic.
"""
import datetime
import functools

try:
    import newrelic.agent
    from newrelic.api import application
    newrelic_loaded = True
except ImportError:
    newrelic_loaded = False

from oslo_service import service
from oslo_utils import timeutils

from barbican.common import utils
from barbican.model import models
from barbican.model import repositories
from barbican import queue
from barbican.tasks import common
from barbican.tasks import resources

if newrelic_loaded:
    newrelic.agent.initialize('/etc/newrelic/newrelic.ini')

LOG = utils.getLogger(__name__)


# Maps the common/shared RetryTasks (returned from lower-level business logic
# and plugin processing) to top-level RPC tasks in the Tasks class below.
MAP_RETRY_TASKS = {}


def find_function_name(func, if_no_name=None):
    """Returns pretty-formatted function name."""
    return getattr(func, '__name__', if_no_name)


def retryable_order(fn):
    """Provides retry/scheduling support to Order-related tasks."""

    @functools.wraps(fn)
    def wrapper(method_self, *args, **kwargs):
        result = fn(method_self, *args, **kwargs)
        retry_rpc_method = schedule_order_retry_tasks(
            fn, result, *args, **kwargs)
        if retry_rpc_method:
            LOG.info("Scheduled RPC method for retry: '%s'", retry_rpc_method)
        else:
            LOG.info("Task '%s' did not have to be retried",
                     find_function_name(fn, if_no_name='???'))

    return wrapper


def transactional(fn):
    """Provides request-scoped database transaction support to tasks."""

    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        fn_name = find_function_name(fn, if_no_name='???')

        if not queue.is_server_side():
            # Non-server mode directly invokes tasks.
            fn(*args, **kwargs)
            LOG.info("Completed worker task: '%s'", fn_name)
        else:
            # Manage session/transaction.
            try:
                fn(*args, **kwargs)
                repositories.commit()
                LOG.info("Completed worker task (post-commit): '%s'", fn_name)
            except Exception:
                """NOTE: Wrapped functions must process with care!

                Exceptions that reach here will revert the entire transaction,
                including any updates made to entities such as setting error
                codes and error messages.
                """
                LOG.exception("Problem seen processing worker task: '%s'",
                              fn_name
                              )
                repositories.rollback()
            finally:
                repositories.clear()

    return wrapper


def monitored(fn):  # pragma: no cover
    """Provides monitoring capabilities for task methods."""
    # TODO(jvrbanac): Figure out how we should test third-party monitoring

    # Support NewRelic Monitoring
    if newrelic_loaded:
        # Create a NewRelic app instance
        app = application.application_instance()

        def newrelic_wrapper(*args, **kwargs):
            # Resolve real name since decorators are wrapper the method
            if len(args) > 0 and hasattr(args[0], fn.__name__):
                cls = type(args[0])
                task_name = '{0}:{1}.{2}'.format(
                    cls.__module__,
                    cls.__name__,
                    fn.__name__
                )
            else:
                task_name = newrelic.agent.callable_name(fn)

            # Execute task under a monitored context
            with newrelic.agent.BackgroundTask(app, task_name):
                fn(*args, **kwargs)

        return newrelic_wrapper

    return fn


def schedule_order_retry_tasks(
        invoked_task, retry_result, context, *args, **kwargs):
    """Schedules an Order-related task for retry.

    :param invoked_task: The RPC method that was just invoked.
    :param retry_result: A :class:`FollowOnProcessingStatusDTO` if follow-on
                         processing (such as retrying this or another task) is
                         required, otherwise None indicates no such follow-on
                         processing is required.
    :param context: Queue context, not used.
    :param order_id: ID of the Order entity the task to retry is for.
    :param args: List of arguments passed in to the just-invoked task.
    :param kwargs: Dict of arguments passed in to the just-invoked task.
    :return: Returns the RPC task method scheduled for a retry, None if no RPC
             task was scheduled.
    """

    retry_rpc_method = None
    order_id = kwargs.get('order_id')

    if not retry_result or not order_id:
        pass

    elif common.RetryTasks.INVOKE_SAME_TASK == retry_result.retry_task:
        if invoked_task:
            retry_rpc_method = find_function_name(invoked_task)

    else:
        retry_rpc_method = MAP_RETRY_TASKS.get(retry_result.retry_task)

    if retry_rpc_method:
        LOG.debug(
            'Scheduling RPC method for retry: {0}'.format(retry_rpc_method))

        date_to_retry_at = timeutils.utcnow() + datetime.timedelta(
            milliseconds=retry_result.retry_msec)

        retry_model = models.OrderRetryTask()
        retry_model.order_id = order_id
        retry_model.retry_task = retry_rpc_method
        retry_model.retry_at = date_to_retry_at
        retry_model.retry_args = args
        retry_model.retry_kwargs = kwargs
        retry_model.retry_count = 0

        retry_repo = repositories.get_order_retry_tasks_repository()
        retry_repo.create_from(retry_model)

    return retry_rpc_method


class Tasks(object):
    """Tasks that can be invoked asynchronously in Barbican.

    Only place task methods and implementations on this class, as they can be
    called directly from the client side for non-asynchronous standalone
    single-node operation.

    If a new method is added that can be retried, please also add its method
    name to MAP_RETRY_TASKS above.

    The TaskServer class below extends this class to implement a worker-side
    server utilizing Oslo messaging's RPC server. This RPC server can invoke
    methods on itself, which include the methods in this class.
    """

    @monitored
    @transactional
    @retryable_order
    def process_type_order(self, context, order_id, project_id, request_id):
        """Process TypeOrder."""
        message = "Processing type order: order ID is '%(order)s' and " \
                  "request ID is '%(request)s'"
        LOG.info(message, {'order': order_id, 'request': request_id})
        return resources.BeginTypeOrder().process_and_suppress_exceptions(
            order_id, project_id)


class TaskServer(Tasks, service.Service):
    """Server to process asynchronous tasking from Barbican API nodes.

    This server is an Oslo service that exposes task methods that can
    be invoked from the Barbican API nodes. It delegates to an Oslo
    RPC messaging server to invoke methods asynchronously on this class.
    Since this class also extends the Tasks class above, its task-based
    methods are hence available to the RPC messaging server.
    """
    def __init__(self):
        super(TaskServer, self).__init__()

        # Setting up db engine to avoid lazy initialization
        repositories.setup_database_engine_and_factory()

        # This property must be defined for the 'endpoints' specified below,
        #   as the oslo_messaging RPC server will ask for it.
        self.target = queue.get_target()

        # Create an oslo RPC server, that calls back on to this class
        #   instance to invoke tasks, such as 'process_order()' on the
        #   extended Tasks class above.
        self._server = queue.get_server(target=self.target,
                                        endpoints=[self])

    def start(self):
        LOG.info("Starting the TaskServer")
        self._server.start()
        super(TaskServer, self).start()

    def stop(self):
        LOG.info("Halting the TaskServer")
        super(TaskServer, self).stop()
        self._server.stop()
