#
#    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.

"""Resources for Rackspace Auto Scale."""

import copy
import six

from heat.common import exception
from heat.common.i18n import _
from heat.common import template_format
from heat.engine import attributes
from heat.engine import constraints
from heat.engine import properties
from heat.engine import resource
from heat.engine import support
from heat.engine import template as templatem

try:
    from pyrax.exceptions import Forbidden
    from pyrax.exceptions import NotFound
    PYRAX_INSTALLED = True
except ImportError:
    class Forbidden(Exception):
        """Dummy pyrax exception - only used for testing."""

    class NotFound(Exception):
        """Dummy pyrax exception - only used for testing."""

    PYRAX_INSTALLED = False


class Group(resource.Resource):
    """Represents a scaling group."""

    # pyrax differs drastically from the actual Auto Scale API. We'll prefer
    # the true API here, but since pyrax doesn't support the full flexibility
    # of the API, we'll have to restrict what users can provide.

    support_status = support.SupportStatus(
        status=support.UNSUPPORTED,
        message=_('This resource is not supported, use at your own risk.'))

    # properties are identical to the API POST /groups.
    PROPERTIES = (
        GROUP_CONFIGURATION, LAUNCH_CONFIGURATION,
    ) = (
        'groupConfiguration', 'launchConfiguration',
    )

    _GROUP_CONFIGURATION_KEYS = (
        GROUP_CONFIGURATION_MAX_ENTITIES, GROUP_CONFIGURATION_COOLDOWN,
        GROUP_CONFIGURATION_NAME, GROUP_CONFIGURATION_MIN_ENTITIES,
        GROUP_CONFIGURATION_METADATA,
    ) = (
        'maxEntities', 'cooldown',
        'name', 'minEntities',
        'metadata',
    )

    _LAUNCH_CONFIG_KEYS = (
        LAUNCH_CONFIG_ARGS, LAUNCH_CONFIG_TYPE,
    ) = (
        'args', 'type',
    )

    _LAUNCH_CONFIG_ARGS_KEYS = (
        LAUNCH_CONFIG_ARGS_LOAD_BALANCERS,
        LAUNCH_CONFIG_ARGS_SERVER,
        LAUNCH_CONFIG_ARGS_STACK,
    ) = (
        'loadBalancers',
        'server',
        'stack',
    )

    _LAUNCH_CONFIG_ARGS_LOAD_BALANCER_KEYS = (
        LAUNCH_CONFIG_ARGS_LOAD_BALANCER_ID,
        LAUNCH_CONFIG_ARGS_LOAD_BALANCER_PORT,
    ) = (
        'loadBalancerId',
        'port',
    )

    _LAUNCH_CONFIG_ARGS_SERVER_KEYS = (
        LAUNCH_CONFIG_ARGS_SERVER_NAME, LAUNCH_CONFIG_ARGS_SERVER_FLAVOR_REF,
        LAUNCH_CONFIG_ARGS_SERVER_IMAGE_REF,
        LAUNCH_CONFIG_ARGS_SERVER_METADATA,
        LAUNCH_CONFIG_ARGS_SERVER_PERSONALITY,
        LAUNCH_CONFIG_ARGS_SERVER_NETWORKS,
        LAUNCH_CONFIG_ARGS_SERVER_DISK_CONFIG,
        LAUNCH_CONFIG_ARGS_SERVER_KEY_NAME,
        LAUNCH_CONFIG_ARGS_SERVER_USER_DATA,
        LAUNCH_CONFIG_ARGS_SERVER_CDRIVE
    ) = (
        'name', 'flavorRef',
        'imageRef',
        'metadata',
        'personality',
        'networks',
        'diskConfig',  # technically maps to OS-DCF:diskConfig
        'key_name',
        'user_data',
        'config_drive'
    )

    _LAUNCH_CONFIG_ARGS_SERVER_NETWORK_KEYS = (
        LAUNCH_CONFIG_ARGS_SERVER_NETWORK_UUID,
    ) = (
        'uuid',
    )

    _LAUNCH_CONFIG_ARGS_STACK_KEYS = (
        LAUNCH_CONFIG_ARGS_STACK_TEMPLATE,
        LAUNCH_CONFIG_ARGS_STACK_TEMPLATE_URL,
        LAUNCH_CONFIG_ARGS_STACK_DISABLE_ROLLBACK,
        LAUNCH_CONFIG_ARGS_STACK_ENVIRONMENT,
        LAUNCH_CONFIG_ARGS_STACK_FILES,
        LAUNCH_CONFIG_ARGS_STACK_PARAMETERS,
        LAUNCH_CONFIG_ARGS_STACK_TIMEOUT_MINS
    ) = (
        'template',
        'template_url',
        'disable_rollback',
        'environment',
        'files',
        'parameters',
        'timeout_mins'
    )

    _launch_configuration_args_schema = {
        LAUNCH_CONFIG_ARGS_LOAD_BALANCERS: properties.Schema(
            properties.Schema.LIST,
            _('List of load balancers to hook the '
              'server up to. If not specified, no '
              'load balancing will be configured.'),
            default=[],
            schema=properties.Schema(
                properties.Schema.MAP,
                schema={
                    LAUNCH_CONFIG_ARGS_LOAD_BALANCER_ID: properties.Schema(
                        properties.Schema.STRING,
                        _('ID of the load balancer.'),
                        required=True
                    ),
                    LAUNCH_CONFIG_ARGS_LOAD_BALANCER_PORT: properties.Schema(
                        properties.Schema.INTEGER,
                        _('Server port to connect the load balancer to.')
                    ),
                },
            )
        ),
        LAUNCH_CONFIG_ARGS_SERVER: properties.Schema(
            properties.Schema.MAP,
            _('Server creation arguments, as accepted by the Cloud Servers '
              'server creation API.'),
            required=False,
            schema={
                LAUNCH_CONFIG_ARGS_SERVER_NAME: properties.Schema(
                    properties.Schema.STRING,
                    _('Server name.'),
                    required=True
                ),
                LAUNCH_CONFIG_ARGS_SERVER_FLAVOR_REF: properties.Schema(
                    properties.Schema.STRING,
                    _('The ID or name of the flavor to boot onto.'),
                    constraints=[
                        constraints.CustomConstraint('nova.flavor')
                    ],
                    required=True
                ),
                LAUNCH_CONFIG_ARGS_SERVER_IMAGE_REF: properties.Schema(
                    properties.Schema.STRING,
                    _('The ID or name of the image to boot with.'),
                    constraints=[
                        constraints.CustomConstraint('glance.image')
                    ],
                    required=True
                ),
                LAUNCH_CONFIG_ARGS_SERVER_METADATA: properties.Schema(
                    properties.Schema.MAP,
                    _('Metadata key and value pairs.')
                ),
                LAUNCH_CONFIG_ARGS_SERVER_PERSONALITY: properties.Schema(
                    properties.Schema.MAP,
                    _('File path and contents.')
                ),
                LAUNCH_CONFIG_ARGS_SERVER_CDRIVE: properties.Schema(
                    properties.Schema.BOOLEAN,
                    _('Enable config drive on the instance.')
                ),
                LAUNCH_CONFIG_ARGS_SERVER_USER_DATA: properties.Schema(
                    properties.Schema.STRING,
                    _('User data for bootstrapping the instance.')
                ),
                LAUNCH_CONFIG_ARGS_SERVER_NETWORKS: properties.Schema(
                    properties.Schema.LIST,
                    _('Networks to attach to. If unspecified, the instance '
                      'will be attached to the public Internet and private '
                      'ServiceNet networks.'),
                    schema=properties.Schema(
                        properties.Schema.MAP,
                        schema={
                            LAUNCH_CONFIG_ARGS_SERVER_NETWORK_UUID:
                            properties.Schema(
                                properties.Schema.STRING,
                                _('UUID of network to attach to.'),
                                required=True)
                        }
                    )
                ),
                LAUNCH_CONFIG_ARGS_SERVER_DISK_CONFIG: properties.Schema(
                    properties.Schema.STRING,
                    _('Configuration specifying the partition layout. AUTO to '
                      'create a partition utilizing the entire disk, and '
                      'MANUAL to create a partition matching the source '
                      'image.'),
                    constraints=[
                        constraints.AllowedValues(['AUTO', 'MANUAL']),
                    ]
                ),
                LAUNCH_CONFIG_ARGS_SERVER_KEY_NAME: properties.Schema(
                    properties.Schema.STRING,
                    _('Name of a previously created SSH keypair to allow '
                      'key-based authentication to the server.')
                ),
            },
        ),
        LAUNCH_CONFIG_ARGS_STACK: properties.Schema(
            properties.Schema.MAP,
            _('The attributes that Auto Scale uses to create a new stack. The '
              'attributes that you specify for the stack entity apply to all '
              'new stacks in the scaling group. Note the stack arguments are '
              'directly passed to Heat when creating a stack.'),
            schema={
                LAUNCH_CONFIG_ARGS_STACK_TEMPLATE: properties.Schema(
                    properties.Schema.STRING,
                    _('The template that describes the stack. Either the '
                      'template or template_url property must be specified.'),
                ),
                LAUNCH_CONFIG_ARGS_STACK_TEMPLATE_URL: properties.Schema(
                    properties.Schema.STRING,
                    _('A URI to a template. Either the template or '
                      'template_url property must be specified.')
                ),
                LAUNCH_CONFIG_ARGS_STACK_DISABLE_ROLLBACK: properties.Schema(
                    properties.Schema.BOOLEAN,
                    _('Keep the resources that have been created if the stack '
                      'fails to create. Defaults to True.'),
                    default=True
                ),
                LAUNCH_CONFIG_ARGS_STACK_ENVIRONMENT: properties.Schema(
                    properties.Schema.MAP,
                    _('The environment for the stack.'),
                ),
                LAUNCH_CONFIG_ARGS_STACK_FILES: properties.Schema(
                    properties.Schema.MAP,
                    _('The contents of files that the template references.')
                ),
                LAUNCH_CONFIG_ARGS_STACK_PARAMETERS: properties.Schema(
                    properties.Schema.MAP,
                    _('Key/value pairs of the parameters and their values to '
                      'pass to the parameters in the template.')
                ),
                LAUNCH_CONFIG_ARGS_STACK_TIMEOUT_MINS: properties.Schema(
                    properties.Schema.INTEGER,
                    _('The stack creation timeout in minutes.')
                )
            }
        )
    }

    properties_schema = {
        GROUP_CONFIGURATION: properties.Schema(
            properties.Schema.MAP,
            _('Group configuration.'),
            schema={
                GROUP_CONFIGURATION_MAX_ENTITIES: properties.Schema(
                    properties.Schema.INTEGER,
                    _('Maximum number of entities in this scaling group.'),
                    required=True
                ),
                GROUP_CONFIGURATION_COOLDOWN: properties.Schema(
                    properties.Schema.NUMBER,
                    _('Number of seconds after capacity changes during '
                      'which further capacity changes are disabled.'),
                    required=True
                ),
                GROUP_CONFIGURATION_NAME: properties.Schema(
                    properties.Schema.STRING,
                    _('Name of the scaling group.'),
                    required=True
                ),
                GROUP_CONFIGURATION_MIN_ENTITIES: properties.Schema(
                    properties.Schema.INTEGER,
                    _('Minimum number of entities in this scaling group.'),
                    required=True
                ),
                GROUP_CONFIGURATION_METADATA: properties.Schema(
                    properties.Schema.MAP,
                    _('Arbitrary key/value metadata to associate with '
                      'this group.')
                ),
            },
            required=True,
            update_allowed=True
        ),
        LAUNCH_CONFIGURATION: properties.Schema(
            properties.Schema.MAP,
            _('Launch configuration.'),
            schema={
                LAUNCH_CONFIG_ARGS: properties.Schema(
                    properties.Schema.MAP,
                    _('Type-specific launch arguments.'),
                    schema=_launch_configuration_args_schema,
                    required=True
                ),
                LAUNCH_CONFIG_TYPE: properties.Schema(
                    properties.Schema.STRING,
                    _('Launch configuration method. Only launch_server and '
                      'launch_stack are currently supported.'),
                    required=True,
                    constraints=[
                        constraints.AllowedValues(['launch_server',
                                                   'launch_stack']),
                    ]
                ),
            },
            required=True,
            update_allowed=True
        ),
        # We don't allow scaling policies to be specified here, despite the
        # fact that the API supports it. Users should use the ScalingPolicy
        # resource.
    }

    def _get_group_config_args(self, groupconf):
        """Get the groupConfiguration-related pyrax arguments."""
        return dict(
            name=groupconf[self.GROUP_CONFIGURATION_NAME],
            cooldown=groupconf[self.GROUP_CONFIGURATION_COOLDOWN],
            min_entities=groupconf[self.GROUP_CONFIGURATION_MIN_ENTITIES],
            max_entities=groupconf[self.GROUP_CONFIGURATION_MAX_ENTITIES],
            metadata=groupconf.get(self.GROUP_CONFIGURATION_METADATA, None))

    def _get_launch_config_server_args(self, launchconf):
        lcargs = launchconf[self.LAUNCH_CONFIG_ARGS]
        server_args = lcargs[self.LAUNCH_CONFIG_ARGS_SERVER]
        lb_args = lcargs.get(self.LAUNCH_CONFIG_ARGS_LOAD_BALANCERS)
        lbs = copy.deepcopy(lb_args)
        for lb in lbs:
            # if the port is not specified, the lbid must be that of a
            # RackConnectV3 lb pool.
            if not lb[self.LAUNCH_CONFIG_ARGS_LOAD_BALANCER_PORT]:
                del lb[self.LAUNCH_CONFIG_ARGS_LOAD_BALANCER_PORT]
                continue
            lbid = int(lb[self.LAUNCH_CONFIG_ARGS_LOAD_BALANCER_ID])
            lb[self.LAUNCH_CONFIG_ARGS_LOAD_BALANCER_ID] = lbid
        personality = server_args.get(
            self.LAUNCH_CONFIG_ARGS_SERVER_PERSONALITY)
        if personality:
            personality = [{'path': k, 'contents': v} for k, v in
                           personality.items()]
        user_data = server_args.get(self.LAUNCH_CONFIG_ARGS_SERVER_USER_DATA)
        cdrive = (server_args.get(self.LAUNCH_CONFIG_ARGS_SERVER_CDRIVE) or
                  bool(user_data is not None and len(user_data.strip())))
        image_id = self.client_plugin('glance').find_image_by_name_or_id(
            server_args[self.LAUNCH_CONFIG_ARGS_SERVER_IMAGE_REF])
        flavor_id = self.client_plugin('nova').find_flavor_by_name_or_id(
            server_args[self.LAUNCH_CONFIG_ARGS_SERVER_FLAVOR_REF])

        return dict(
            launch_config_type=launchconf[self.LAUNCH_CONFIG_TYPE],
            server_name=server_args[self.GROUP_CONFIGURATION_NAME],
            image=image_id,
            flavor=flavor_id,
            disk_config=server_args.get(
                self.LAUNCH_CONFIG_ARGS_SERVER_DISK_CONFIG),
            metadata=server_args.get(self.GROUP_CONFIGURATION_METADATA),
            config_drive=cdrive,
            user_data=user_data,
            personality=personality,
            networks=server_args.get(self.LAUNCH_CONFIG_ARGS_SERVER_NETWORKS),
            load_balancers=lbs,
            key_name=server_args.get(self.LAUNCH_CONFIG_ARGS_SERVER_KEY_NAME),
        )

    def _get_launch_config_stack_args(self, launchconf):
        lcargs = launchconf[self.LAUNCH_CONFIG_ARGS]
        stack_args = lcargs[self.LAUNCH_CONFIG_ARGS_STACK]
        return dict(
            launch_config_type=launchconf[self.LAUNCH_CONFIG_TYPE],
            template=stack_args[self.LAUNCH_CONFIG_ARGS_STACK_TEMPLATE],
            template_url=stack_args[
                self.LAUNCH_CONFIG_ARGS_STACK_TEMPLATE_URL],
            disable_rollback=stack_args[
                self.LAUNCH_CONFIG_ARGS_STACK_DISABLE_ROLLBACK],
            environment=stack_args[self.LAUNCH_CONFIG_ARGS_STACK_ENVIRONMENT],
            files=stack_args[self.LAUNCH_CONFIG_ARGS_STACK_FILES],
            parameters=stack_args[self.LAUNCH_CONFIG_ARGS_STACK_PARAMETERS],
            timeout_mins=stack_args[self.LAUNCH_CONFIG_ARGS_STACK_TIMEOUT_MINS]
        )

    def _get_launch_config_args(self, launchconf):
        """Get the launchConfiguration-related pyrax arguments."""
        if launchconf[self.LAUNCH_CONFIG_ARGS].get(
                self.LAUNCH_CONFIG_ARGS_SERVER):
            return self._get_launch_config_server_args(launchconf)
        else:
            return self._get_launch_config_stack_args(launchconf)

    def _get_create_args(self):
        """Get pyrax-style arguments for creating a scaling group."""
        args = self._get_group_config_args(
            self.properties[self.GROUP_CONFIGURATION])
        args['group_metadata'] = args.pop('metadata')
        args.update(self._get_launch_config_args(
            self.properties[self.LAUNCH_CONFIGURATION]))
        return args

    def handle_create(self):
        """Create the autoscaling group and set resource_id.

        The resource_id is set to the resulting group's ID.
        """
        asclient = self.auto_scale()
        group = asclient.create(**self._get_create_args())
        self.resource_id_set(str(group.id))

    def handle_check(self):
        self.auto_scale().get(self.resource_id)

    def handle_update(self, json_snippet, tmpl_diff, prop_diff):
        """Update the group configuration and the launch configuration."""
        asclient = self.auto_scale()
        if self.GROUP_CONFIGURATION in prop_diff:
            args = self._get_group_config_args(
                prop_diff[self.GROUP_CONFIGURATION])
            asclient.replace(self.resource_id, **args)
        if self.LAUNCH_CONFIGURATION in prop_diff:
            args = self._get_launch_config_args(
                prop_diff[self.LAUNCH_CONFIGURATION])
            asclient.replace_launch_config(self.resource_id, **args)

    def handle_delete(self):
        """Delete the scaling group.

        Since Auto Scale doesn't allow deleting a group until all its servers
        are gone, we must set the minEntities and maxEntities of the group to 0
        and then keep trying the delete until Auto Scale has deleted all the
        servers and the delete will succeed.
        """
        if self.resource_id is None:
            return
        asclient = self.auto_scale()
        args = self._get_group_config_args(
            self.properties[self.GROUP_CONFIGURATION])
        args['min_entities'] = 0
        args['max_entities'] = 0
        try:
            asclient.replace(self.resource_id, **args)
        except NotFound:
            pass

    def check_delete_complete(self, result):
        """Try the delete operation until it succeeds."""
        if self.resource_id is None:
            return True
        try:
            self.auto_scale().delete(self.resource_id)
        except Forbidden:
            return False
        except NotFound:
            return True
        else:
            return True

    def _check_rackconnect_v3_pool_exists(self, pool_id):
        pools = self.client("rackconnect").list_load_balancer_pools()
        if pool_id in (p.id for p in pools):
            return True
        return False

    def validate(self):
        super(Group, self).validate()
        launchconf = self.properties[self.LAUNCH_CONFIGURATION]
        lcargs = launchconf[self.LAUNCH_CONFIG_ARGS]

        server_args = lcargs.get(self.LAUNCH_CONFIG_ARGS_SERVER)
        st_args = lcargs.get(self.LAUNCH_CONFIG_ARGS_STACK)

        # launch_server and launch_stack are required and mutually exclusive.
        if ((not server_args and not st_args) or
                (server_args and st_args)):
            msg = (_('Must provide one of %(server)s or %(stack)s in %(conf)s')
                   % {'server': self.LAUNCH_CONFIG_ARGS_SERVER,
                      'stack': self.LAUNCH_CONFIG_ARGS_STACK,
                      'conf': self.LAUNCH_CONFIGURATION})
            raise exception.StackValidationFailed(msg)

        lb_args = lcargs.get(self.LAUNCH_CONFIG_ARGS_LOAD_BALANCERS)
        lbs = copy.deepcopy(lb_args)
        for lb in lbs:
            lb_port = lb.get(self.LAUNCH_CONFIG_ARGS_LOAD_BALANCER_PORT)
            lb_id = lb[self.LAUNCH_CONFIG_ARGS_LOAD_BALANCER_ID]
            if not lb_port:
                # check if lb id is a valid RCV3 pool id
                if not self._check_rackconnect_v3_pool_exists(lb_id):
                    msg = _('Could not find RackConnectV3 pool '
                            'with id %s') % (lb_id)
                    raise exception.StackValidationFailed(msg)

        if st_args:
            st_tmpl = st_args.get(self.LAUNCH_CONFIG_ARGS_STACK_TEMPLATE)
            st_tmpl_url = st_args.get(
                self.LAUNCH_CONFIG_ARGS_STACK_TEMPLATE_URL)
            st_env = st_args.get(self.LAUNCH_CONFIG_ARGS_STACK_ENVIRONMENT)
            # template and template_url are required and mutually exclusive.
            if ((not st_tmpl and not st_tmpl_url) or
                    (st_tmpl and st_tmpl_url)):
                msg = _('Must provide one of template or template_url.')
                raise exception.StackValidationFailed(msg)

            if st_tmpl:
                st_files = st_args.get(self.LAUNCH_CONFIG_ARGS_STACK_FILES)
                try:
                    tmpl = template_format.simple_parse(st_tmpl)
                    templatem.Template(tmpl, files=st_files, env=st_env)
                except Exception as exc:
                    msg = (_('Encountered error while loading template: %s') %
                           six.text_type(exc))
                    raise exception.StackValidationFailed(msg)

    def auto_scale(self):
        return self.client('auto_scale')


class ScalingPolicy(resource.Resource):
    """Represents a Rackspace Auto Scale scaling policy."""

    support_status = support.SupportStatus(
        status=support.UNSUPPORTED,
        message=_('This resource is not supported, use at your own risk.'))

    PROPERTIES = (
        GROUP, NAME, CHANGE, CHANGE_PERCENT, DESIRED_CAPACITY,
        COOLDOWN, TYPE, ARGS,
    ) = (
        'group', 'name', 'change', 'changePercent', 'desiredCapacity',
        'cooldown', 'type', 'args',
    )

    properties_schema = {
        # group isn't in the post body, but it's in the URL to post to.
        GROUP: properties.Schema(
            properties.Schema.STRING,
            _('Scaling group ID that this policy belongs to.'),
            required=True
        ),
        NAME: properties.Schema(
            properties.Schema.STRING,
            _('Name of this scaling policy.'),
            required=True,
            update_allowed=True
        ),
        CHANGE: properties.Schema(
            properties.Schema.INTEGER,
            _('Amount to add to or remove from current number of instances. '
              'Incompatible with changePercent and desiredCapacity.'),
            update_allowed=True
        ),
        CHANGE_PERCENT: properties.Schema(
            properties.Schema.NUMBER,
            _('Percentage-based change to add or remove from current number '
              'of instances. Incompatible with change and desiredCapacity.'),
            update_allowed=True
        ),
        DESIRED_CAPACITY: properties.Schema(
            properties.Schema.INTEGER,
            _('Absolute number to set the number of instances to. '
              'Incompatible with change and changePercent.'),
            update_allowed=True
        ),
        COOLDOWN: properties.Schema(
            properties.Schema.NUMBER,
            _('Number of seconds after a policy execution during which '
              'further executions are disabled.'),
            update_allowed=True
        ),
        TYPE: properties.Schema(
            properties.Schema.STRING,
            _('Type of this scaling policy. Specifies how the policy is '
              'executed.'),
            required=True,
            constraints=[
                constraints.AllowedValues(['webhook', 'schedule',
                                           'cloud_monitoring']),
            ],
            update_allowed=True
        ),
        ARGS: properties.Schema(
            properties.Schema.MAP,
            _('Type-specific arguments for the policy.'),
            update_allowed=True
        ),
    }

    def _get_args(self, properties):
        """Get pyrax-style create arguments for scaling policies."""
        args = dict(
            scaling_group=properties[self.GROUP],
            name=properties[self.NAME],
            policy_type=properties[self.TYPE],
            cooldown=properties[self.COOLDOWN],
        )
        if properties.get(self.CHANGE) is not None:
            args['change'] = properties[self.CHANGE]
        elif properties.get(self.CHANGE_PERCENT) is not None:
            args['change'] = properties[self.CHANGE_PERCENT]
            args['is_percent'] = True
        elif properties.get(self.DESIRED_CAPACITY) is not None:
            args['desired_capacity'] = properties[self.DESIRED_CAPACITY]
        if properties.get(self.ARGS) is not None:
            args['args'] = properties[self.ARGS]
        return args

    def handle_create(self):
        """Create the scaling policy and initialize the resource ID.

        The resource ID is initialized to {group_id}:{policy_id}.
        """
        asclient = self.auto_scale()
        args = self._get_args(self.properties)
        policy = asclient.add_policy(**args)
        resource_id = '%s:%s' % (self.properties[self.GROUP], policy.id)
        self.resource_id_set(resource_id)

    def _get_policy_id(self):
        return self.resource_id.split(':', 1)[1]

    def handle_update(self, json_snippet, tmpl_diff, prop_diff):
        asclient = self.auto_scale()
        props = json_snippet.properties(self.properties_schema,
                                        self.context)
        args = self._get_args(props)
        args['policy'] = self._get_policy_id()
        asclient.replace_policy(**args)

    def handle_delete(self):
        """Delete the policy if it exists."""
        asclient = self.auto_scale()
        if self.resource_id is None:
            return
        policy_id = self._get_policy_id()
        try:
            asclient.delete_policy(self.properties[self.GROUP], policy_id)
        except NotFound:
            pass

    def auto_scale(self):
        return self.client('auto_scale')


class WebHook(resource.Resource):
    """Represents a Rackspace AutoScale webhook.

    Exposes the URLs of the webhook as attributes.
    """

    support_status = support.SupportStatus(
        status=support.UNSUPPORTED,
        message=_('This resource is not supported, use at your own risk.'))

    PROPERTIES = (
        POLICY, NAME, METADATA,
    ) = (
        'policy', 'name', 'metadata',
    )

    ATTRIBUTES = (
        EXECUTE_URL, CAPABILITY_URL,
    ) = (
        'executeUrl', 'capabilityUrl',
    )

    properties_schema = {
        POLICY: properties.Schema(
            properties.Schema.STRING,
            _('The policy that this webhook should apply to, in '
              '{group_id}:{policy_id} format. Generally a Ref to a Policy '
              'resource.'),
            required=True
        ),
        NAME: properties.Schema(
            properties.Schema.STRING,
            _('The name of this webhook.'),
            required=True,
            update_allowed=True
        ),
        METADATA: properties.Schema(
            properties.Schema.MAP,
            _('Arbitrary key/value metadata for this webhook.'),
            update_allowed=True
        ),
    }

    attributes_schema = {
        EXECUTE_URL: attributes.Schema(
            _("The url for executing the webhook (requires auth)."),
            cache_mode=attributes.Schema.CACHE_NONE
        ),
        CAPABILITY_URL: attributes.Schema(
            _("The url for executing the webhook (doesn't require auth)."),
            cache_mode=attributes.Schema.CACHE_NONE
        ),
    }

    def _get_args(self, props):
        group_id, policy_id = props[self.POLICY].split(':', 1)
        return dict(
            name=props[self.NAME],
            scaling_group=group_id,
            policy=policy_id,
            metadata=props.get(self.METADATA))

    def handle_create(self):
        asclient = self.auto_scale()
        args = self._get_args(self.properties)
        webhook = asclient.add_webhook(**args)
        self.resource_id_set(webhook.id)

        for link in webhook.links:
            rel_to_key = {'self': 'executeUrl',
                          'capability': 'capabilityUrl'}
            key = rel_to_key.get(link['rel'])
            if key is not None:
                url = link['href'].encode('utf-8')
                self.data_set(key, url)

    def handle_update(self, json_snippet, tmpl_diff, prop_diff):
        asclient = self.auto_scale()
        args = self._get_args(json_snippet.properties(self.properties_schema,
                                                      self.context))
        args['webhook'] = self.resource_id
        asclient.replace_webhook(**args)

    def _resolve_attribute(self, key):
        v = self.data().get(key)
        if v is not None:
            return v.decode('utf-8')
        else:
            return None

    def handle_delete(self):
        if self.resource_id is None:
            return
        asclient = self.auto_scale()
        group_id, policy_id = self.properties[self.POLICY].split(':', 1)
        try:
            asclient.delete_webhook(group_id, policy_id, self.resource_id)
        except NotFound:
            pass

    def auto_scale(self):
        return self.client('auto_scale')


def resource_mapping():
    return {
        'Rackspace::AutoScale::Group': Group,
        'Rackspace::AutoScale::ScalingPolicy': ScalingPolicy,
        'Rackspace::AutoScale::WebHook': WebHook
    }


def available_resource_mapping():
    if PYRAX_INSTALLED:
        return resource_mapping()
    return {}
