#
# Copyright 2012, Red Hat, 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.

"""
Client side of the heat engine RPC API.
"""

from heat.common import messaging
from heat.rpc import api as rpc_api


class EngineClient(object):
    '''Client side of the heat engine rpc API.

    API version history::

        1.0 - Initial version.
        1.1 - Add support_status argument to list_resource_types()
        1.4 - Add support for service list
    '''

    BASE_RPC_API_VERSION = '1.0'

    def __init__(self):
        self._client = messaging.get_rpc_client(
            topic=rpc_api.ENGINE_TOPIC,
            version=self.BASE_RPC_API_VERSION)

    @staticmethod
    def make_msg(method, **kwargs):
        return method, kwargs

    def call(self, ctxt, msg, version=None):
        method, kwargs = msg
        if version is not None:
            client = self._client.prepare(version=version)
        else:
            client = self._client
        return client.call(ctxt, method, **kwargs)

    def cast(self, ctxt, msg, version=None):
        method, kwargs = msg
        if version is not None:
            client = self._client.prepare(version=version)
        else:
            client = self._client
        return client.cast(ctxt, method, **kwargs)

    def local_error_name(self, error):
        """
        Returns the name of the error with any _Remote postfix removed.

        :param error: Remote raised error to derive the name from.
        """
        error_name = error.__class__.__name__
        return error_name.split('_Remote')[0]

    def ignore_error_named(self, error, name):
        """
        Raises the error unless its local name matches the supplied name

        :param error: Remote raised error to derive the local name from.
        :param name: Name to compare local name to.
        """
        if self.local_error_name(error) != name:
            raise error

    def identify_stack(self, ctxt, stack_name):
        """
        The identify_stack method returns the full stack identifier for a
        single, live stack given the stack name.

        :param ctxt: RPC context.
        :param stack_name: Name of the stack you want to see,
                           or None to see all
        """
        return self.call(ctxt, self.make_msg('identify_stack',
                                             stack_name=stack_name))

    def list_stacks(self, ctxt, limit=None, marker=None, sort_keys=None,
                    sort_dir=None, filters=None, tenant_safe=True,
                    show_deleted=False, show_nested=False):
        """
        The list_stacks method returns attributes of all stacks.  It supports
        pagination (``limit`` and ``marker``), sorting (``sort_keys`` and
        ``sort_dir``) and filtering (``filters``) of the results.

        :param ctxt: RPC context.
        :param limit: the number of stacks to list (integer or string)
        :param marker: the ID of the last item in the previous page
        :param sort_keys: an array of fields used to sort the list
        :param sort_dir: the direction of the sort ('asc' or 'desc')
        :param filters: a dict with attribute:value to filter the list
        :param tenant_safe: if true, scope the request by the current tenant
        :param show_deleted: if true, show soft-deleted stacks
        :param show_nested: if true, show nested stacks
        :returns: a list of stacks
        """
        return self.call(ctxt,
                         self.make_msg('list_stacks', limit=limit,
                                       sort_keys=sort_keys, marker=marker,
                                       sort_dir=sort_dir, filters=filters,
                                       tenant_safe=tenant_safe,
                                       show_deleted=show_deleted,
                                       show_nested=show_nested))

    def count_stacks(self, ctxt, filters=None, tenant_safe=True,
                     show_deleted=False, show_nested=False):
        """
        Return the number of stacks that match the given filters
        :param ctxt: RPC context.
        :param filters: a dict of ATTR:VALUE to match against stacks
        :param tenant_safe: if true, scope the request by the current tenant
        :param show_deleted: if true, count will include the deleted stacks
        :param show_nested: if true, count will include nested stacks
        :returns: a integer representing the number of matched stacks
        """
        return self.call(ctxt, self.make_msg('count_stacks',
                                             filters=filters,
                                             tenant_safe=tenant_safe,
                                             show_deleted=show_deleted,
                                             show_nested=show_nested))

    def show_stack(self, ctxt, stack_identity):
        """
        Return detailed information about one or all stacks.
        :param ctxt: RPC context.
        :param stack_identity: Name of the stack you want to show, or None to
        show all
        """
        return self.call(ctxt, self.make_msg('show_stack',
                                             stack_identity=stack_identity))

    def preview_stack(self, ctxt, stack_name, template, params, files, args):
        """
        Simulates a new stack using the provided template.

        Note that at this stage the template has already been fetched from the
        heat-api process if using a template-url.

        :param ctxt: RPC context.
        :param stack_name: Name of the stack you want to create.
        :param template: Template of stack you want to create.
        :param params: Stack Input Params/Environment
        :param files: files referenced from the environment.
        :param args: Request parameters/args passed from API
        """
        return self.call(ctxt,
                         self.make_msg('preview_stack', stack_name=stack_name,
                                       template=template,
                                       params=params, files=files, args=args))

    def create_stack(self, ctxt, stack_name, template, params, files, args):
        """
        The create_stack method creates a new stack using the template
        provided.
        Note that at this stage the template has already been fetched from the
        heat-api process if using a template-url.

        :param ctxt: RPC context.
        :param stack_name: Name of the stack you want to create.
        :param template: Template of stack you want to create.
        :param params: Stack Input Params/Environment
        :param files: files referenced from the environment.
        :param args: Request parameters/args passed from API
        """
        return self._create_stack(ctxt, stack_name, template, params, files,
                                  args)

    def _create_stack(self, ctxt, stack_name, template, params, files, args,
                      owner_id=None, nested_depth=0, user_creds_id=None,
                      stack_user_project_id=None, parent_resource_name=None):
        """
        Internal create_stack interface for engine-to-engine communication via
        RPC.  Allows some additional options which should not be exposed to
        users via the API:
        :param owner_id: parent stack ID for nested stacks
        :param nested_depth: nested depth for nested stacks
        :param user_creds_id: user_creds record for nested stack
        :param stack_user_project_id: stack user project for nested stack
        :param parent_resource_name: the parent resource name
        """
        return self.call(
            ctxt, self.make_msg('create_stack', stack_name=stack_name,
                                template=template,
                                params=params, files=files, args=args,
                                owner_id=owner_id,
                                nested_depth=nested_depth,
                                user_creds_id=user_creds_id,
                                stack_user_project_id=stack_user_project_id,
                                parent_resource_name=parent_resource_name),
            version='1.7')

    def update_stack(self, ctxt, stack_identity, template, params,
                     files, args):
        """
        The update_stack method updates an existing stack based on the
        provided template and parameters.
        Note that at this stage the template has already been fetched from the
        heat-api process if using a template-url.

        :param ctxt: RPC context.
        :param stack_name: Name of the stack you want to create.
        :param template: Template of stack you want to create.
        :param params: Stack Input Params/Environment
        :param files: files referenced from the environment.
        :param args: Request parameters/args passed from API
        """
        return self.call(ctxt, self.make_msg('update_stack',
                                             stack_identity=stack_identity,
                                             template=template,
                                             params=params,
                                             files=files,
                                             args=args))

    def validate_template(self, ctxt, template, params=None):
        """
        The validate_template method uses the stack parser to check
        the validity of a template.

        :param ctxt: RPC context.
        :param template: Template of stack you want to create.
        :param params: Stack Input Params/Environment
        """
        return self.call(ctxt, self.make_msg('validate_template',
                                             template=template,
                                             params=params))

    def authenticated_to_backend(self, ctxt):
        """
        Verify that the credentials in the RPC context are valid for the
        current cloud backend.

        :param ctxt: RPC context.
        """
        return self.call(ctxt, self.make_msg('authenticated_to_backend'))

    def get_template(self, ctxt, stack_identity):
        """
        Get the template.

        :param ctxt: RPC context.
        :param stack_name: Name of the stack you want to see.
        """
        return self.call(ctxt, self.make_msg('get_template',
                                             stack_identity=stack_identity))

    def delete_stack(self, ctxt, stack_identity, cast=True):
        """
        The delete_stack method deletes a given stack.

        :param ctxt: RPC context.
        :param stack_identity: Name of the stack you want to delete.
        :param cast: cast the message or use call (default: True)
        """
        rpc_method = self.cast if cast else self.call
        return rpc_method(ctxt,
                          self.make_msg('delete_stack',
                                        stack_identity=stack_identity))

    def abandon_stack(self, ctxt, stack_identity):
        """
        The abandon_stack method deletes a given stack but
        resources would not be deleted.

        :param ctxt: RPC context.
        :param stack_identity: Name of the stack you want to abandon.
        """
        return self.call(ctxt,
                         self.make_msg('abandon_stack',
                                       stack_identity=stack_identity))

    def list_resource_types(self, ctxt, support_status=None):
        """
        Get a list of valid resource types.

        :param ctxt: RPC context.
        """
        return self.call(ctxt, self.make_msg('list_resource_types',
                                             support_status=support_status),
                         version='1.1')

    def resource_schema(self, ctxt, type_name):
        """
        Get the schema for a resource type.

        :param ctxt: RPC context.
        """
        return self.call(ctxt, self.make_msg('resource_schema',
                                             type_name=type_name))

    def generate_template(self, ctxt, type_name):
        """
        Generate a template based on the specified type.

        :param ctxt: RPC context.
        :param type_name: The resource type name to generate a template for.
        """
        return self.call(ctxt, self.make_msg('generate_template',
                                             type_name=type_name))

    def list_events(self, ctxt, stack_identity, filters=None, limit=None,
                    marker=None, sort_keys=None, sort_dir=None,):
        """
        The list_events method lists all events associated with a given stack.
        It supports pagination (``limit`` and ``marker``),
        sorting (``sort_keys`` and ``sort_dir``) and filtering(filters)
        of the results.

        :param ctxt: RPC context.
        :param stack_identity: Name of the stack you want to get events for
        :param filters: a dict with attribute:value to filter the list
        :param limit: the number of events to list (integer or string)
        :param marker: the ID of the last event in the previous page
        :param sort_keys: an array of fields used to sort the list
        :param sort_dir: the direction of the sort ('asc' or 'desc').
        """
        return self.call(ctxt, self.make_msg('list_events',
                                             stack_identity=stack_identity,
                                             filters=filters,
                                             limit=limit,
                                             marker=marker,
                                             sort_keys=sort_keys,
                                             sort_dir=sort_dir))

    def describe_stack_resource(self, ctxt, stack_identity, resource_name,
                                with_attr=None):
        """
        Get detailed resource information about a particular resource.
        :param ctxt: RPC context.
        :param stack_identity: Name of the stack.
        :param resource_name: the Resource.
        """
        return self.call(ctxt,
                         self.make_msg('describe_stack_resource',
                                       stack_identity=stack_identity,
                                       resource_name=resource_name,
                                       with_attr=with_attr),
                         version='1.2')

    def find_physical_resource(self, ctxt, physical_resource_id):
        """
        Return an identifier for the resource with the specified physical
        resource ID.
        :param ctxt RPC context.
        :param physcial_resource_id The physical resource ID to look up.
        """
        return self.call(ctxt,
                         self.make_msg(
                             'find_physical_resource',
                             physical_resource_id=physical_resource_id))

    def describe_stack_resources(self, ctxt, stack_identity, resource_name):
        """
        Get detailed resource information about one or more resources.
        :param ctxt: RPC context.
        :param stack_identity: Name of the stack.
        :param resource_name: the Resource.
        """
        return self.call(ctxt, self.make_msg('describe_stack_resources',
                                             stack_identity=stack_identity,
                                             resource_name=resource_name))

    def list_stack_resources(self, ctxt, stack_identity, nested_depth=0):
        """
        List the resources belonging to a stack.
        :param ctxt: RPC context.
        :param stack_identity: Name of the stack.
        :param nested_depth: Levels of nested stacks of which list resources.
        """
        return self.call(ctxt, self.make_msg('list_stack_resources',
                                             stack_identity=stack_identity,
                                             nested_depth=nested_depth))

    def stack_suspend(self, ctxt, stack_identity):
        return self.call(ctxt, self.make_msg('stack_suspend',
                                             stack_identity=stack_identity))

    def stack_resume(self, ctxt, stack_identity):
        return self.call(ctxt, self.make_msg('stack_resume',
                                             stack_identity=stack_identity))

    def stack_check(self, ctxt, stack_identity):
        return self.call(ctxt, self.make_msg('stack_check',
                                             stack_identity=stack_identity))

    def stack_cancel_update(self, ctxt, stack_identity):
        return self.call(ctxt, self.make_msg('stack_cancel_update',
                                             stack_identity=stack_identity))

    def metadata_update(self, ctxt, stack_identity, resource_name, metadata):
        """
        Update the metadata for the given resource.
        """
        return self.call(ctxt, self.make_msg('metadata_update',
                                             stack_identity=stack_identity,
                                             resource_name=resource_name,
                                             metadata=metadata))

    def resource_signal(self, ctxt, stack_identity, resource_name, details,
                        sync_call=False):
        """
        Generate an alarm on the resource.
        :param ctxt: RPC context.
        :param stack_identity: Name of the stack.
        :param resource_name: the Resource.
        :param details: the details of the signal.
        """
        return self.call(ctxt, self.make_msg('resource_signal',
                                             stack_identity=stack_identity,
                                             resource_name=resource_name,
                                             details=details,
                                             sync_call=sync_call),

                         version='1.3')

    def create_watch_data(self, ctxt, watch_name, stats_data):
        '''
        This could be used by CloudWatch and WaitConditions
        and treat HA service events like any other CloudWatch.
        :param ctxt: RPC context.
        :param watch_name: Name of the watch/alarm
        :param stats_data: The data to post.
        '''
        return self.call(ctxt, self.make_msg('create_watch_data',
                                             watch_name=watch_name,
                                             stats_data=stats_data))

    def show_watch(self, ctxt, watch_name):
        """
        The show_watch method returns the attributes of one watch
        or all watches if no watch_name is passed

        :param ctxt: RPC context.
        :param watch_name: Name of the watch/alarm you want to see,
                           or None to see all
        """
        return self.call(ctxt, self.make_msg('show_watch',
                                             watch_name=watch_name))

    def show_watch_metric(self, ctxt, metric_namespace=None, metric_name=None):
        """
        The show_watch_metric method returns the datapoints associated
        with a specified metric, or all metrics if no metric_name is passed

        :param ctxt: RPC context.
        :param metric_namespace: Name of the namespace you want to see,
                           or None to see all
        :param metric_name: Name of the metric you want to see,
                           or None to see all
        """
        return self.call(ctxt, self.make_msg('show_watch_metric',
                                             metric_namespace=metric_namespace,
                                             metric_name=metric_name))

    def set_watch_state(self, ctxt, watch_name, state):
        '''
        Temporarily set the state of a given watch
        :param ctxt: RPC context.
        :param watch_name: Name of the watch
        :param state: State (must be one defined in WatchRule class)
        '''
        return self.call(ctxt, self.make_msg('set_watch_state',
                                             watch_name=watch_name,
                                             state=state))

    def get_revision(self, ctxt):
        return self.call(ctxt, self.make_msg('get_revision'))

    def show_software_config(self, cnxt, config_id):
        return self.call(cnxt, self.make_msg('show_software_config',
                                             config_id=config_id))

    def create_software_config(self, cnxt, group, name, config,
                               inputs=None, outputs=None, options=None):
        inputs = inputs or []
        outputs = outputs or []
        options = options or {}
        return self.call(cnxt, self.make_msg('create_software_config',
                                             group=group,
                                             name=name,
                                             config=config,
                                             inputs=inputs,
                                             outputs=outputs,
                                             options=options))

    def delete_software_config(self, cnxt, config_id):
        return self.call(cnxt, self.make_msg('delete_software_config',
                                             config_id=config_id))

    def list_software_deployments(self, cnxt, server_id=None):
        return self.call(cnxt, self.make_msg('list_software_deployments',
                                             server_id=server_id))

    def metadata_software_deployments(self, cnxt, server_id):
        return self.call(cnxt, self.make_msg('metadata_software_deployments',
                                             server_id=server_id))

    def show_software_deployment(self, cnxt, deployment_id):
        return self.call(cnxt, self.make_msg('show_software_deployment',
                                             deployment_id=deployment_id))

    def create_software_deployment(self, cnxt, server_id, config_id=None,
                                   input_values=None, action='INIT',
                                   status='COMPLETE', status_reason='',
                                   stack_user_project_id=None):
        input_values = input_values or {}
        return self.call(cnxt, self.make_msg(
            'create_software_deployment',
            server_id=server_id,
            config_id=config_id,
            input_values=input_values,
            action=action,
            status=status,
            status_reason=status_reason,
            stack_user_project_id=stack_user_project_id))

    def update_software_deployment(self, cnxt, deployment_id,
                                   config_id=None, input_values=None,
                                   output_values=None, action=None,
                                   status=None, status_reason=None,
                                   updated_at=None):
        return self.call(
            cnxt, self.make_msg('update_software_deployment',
                                deployment_id=deployment_id,
                                config_id=config_id,
                                input_values=input_values,
                                output_values=output_values,
                                action=action,
                                status=status,
                                status_reason=status_reason,
                                updated_at=updated_at),
            version='1.5')

    def delete_software_deployment(self, cnxt, deployment_id):
        return self.call(cnxt, self.make_msg('delete_software_deployment',
                                             deployment_id=deployment_id))

    def signal_software_deployment(self, cnxt, deployment_id, details,
                                   updated_at=None):
        return self.call(
            cnxt, self.make_msg('signal_software_deployment',
                                deployment_id=deployment_id,
                                details=details,
                                updated_at=updated_at),
            version='1.6')

    def stack_snapshot(self, ctxt, stack_identity, name):
        return self.call(ctxt, self.make_msg('stack_snapshot',
                                             stack_identity=stack_identity,
                                             name=name))

    def show_snapshot(self, cnxt, stack_identity, snapshot_id):
        return self.call(cnxt, self.make_msg('show_snapshot',
                                             stack_identity=stack_identity,
                                             snapshot_id=snapshot_id))

    def delete_snapshot(self, cnxt, stack_identity, snapshot_id):
        return self.call(cnxt, self.make_msg('delete_snapshot',
                                             stack_identity=stack_identity,
                                             snapshot_id=snapshot_id))

    def stack_list_snapshots(self, cnxt, stack_identity):
        return self.call(cnxt, self.make_msg('stack_list_snapshots',
                                             stack_identity=stack_identity))

    def stack_restore(self, cnxt, stack_identity, snapshot_id):
        return self.call(cnxt, self.make_msg('stack_restore',
                                             stack_identity=stack_identity,
                                             snapshot_id=snapshot_id))

    def list_services(self, cnxt):
        return self.call(cnxt, self.make_msg('list_services'), version='1.4')
