#
#    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 heat.common import exception
from heat.common.i18n import _
from heat.engine import attributes
from heat.engine import constraints
from heat.engine import properties
from heat.engine.resources import signal_responder
from heat.engine import support
from heat.scaling import scalingutil as sc_util

LOG = logging.getLogger(__name__)


class AutoScalingPolicy(signal_responder.SignalResponder):
    """A resource to manage scaling of `OS::Heat::AutoScalingGroup`.

    **Note** while it may incidentally support
    `AWS::AutoScaling::AutoScalingGroup` for now, please don't use it for that
    purpose and use `AWS::AutoScaling::ScalingPolicy` instead.

    Resource to manage scaling for `OS::Heat::AutoScalingGroup`, i.e. define
    which metric should be scaled and scaling adjustment, set cooldown etc.
    """
    PROPERTIES = (
        AUTO_SCALING_GROUP_NAME, SCALING_ADJUSTMENT, ADJUSTMENT_TYPE,
        COOLDOWN, MIN_ADJUSTMENT_STEP
    ) = (
        'auto_scaling_group_id', 'scaling_adjustment', 'adjustment_type',
        'cooldown', 'min_adjustment_step',
    )

    ATTRIBUTES = (
        ALARM_URL, SIGNAL_URL
    ) = (
        'alarm_url', 'signal_url'
    )

    properties_schema = {
        # TODO(Qiming): property name should be AUTO_SCALING_GROUP_ID
        AUTO_SCALING_GROUP_NAME: properties.Schema(
            properties.Schema.STRING,
            _('AutoScaling group ID to apply policy to.'),
            required=True
        ),
        SCALING_ADJUSTMENT: properties.Schema(
            properties.Schema.NUMBER,
            _('Size of adjustment.'),
            required=True,
            update_allowed=True
        ),
        ADJUSTMENT_TYPE: properties.Schema(
            properties.Schema.STRING,
            _('Type of adjustment (absolute or percentage).'),
            required=True,
            constraints=[
                constraints.AllowedValues(
                    [sc_util.CHANGE_IN_CAPACITY,
                     sc_util.EXACT_CAPACITY,
                     sc_util.PERCENT_CHANGE_IN_CAPACITY]),
            ],
            update_allowed=True
        ),
        COOLDOWN: properties.Schema(
            properties.Schema.NUMBER,
            _('Cooldown period, in seconds.'),
            update_allowed=True
        ),
        MIN_ADJUSTMENT_STEP: properties.Schema(
            properties.Schema.INTEGER,
            _('Minimum number of resources that are added or removed '
              'when the AutoScaling group scales up or down. This can '
              'be used only when specifying percent_change_in_capacity '
              'for the adjustment_type property.'),
            constraints=[
                constraints.Range(
                    min=0,
                ),
            ],
            update_allowed=True
        ),

    }

    attributes_schema = {
        ALARM_URL: attributes.Schema(
            _("A signed url to handle the alarm."),
            type=attributes.Schema.STRING,
            cache_mode=attributes.Schema.CACHE_NONE
        ),
        SIGNAL_URL: attributes.Schema(
            _("A url to handle the alarm using native API."),
            support_status=support.SupportStatus(version='5.0.0'),
            type=attributes.Schema.STRING
        ),
    }

    def validate(self):
        """Add validation for min_adjustment_step."""
        super(AutoScalingPolicy, self).validate()
        self._validate_min_adjustment_step()

    def _validate_min_adjustment_step(self):
        adjustment_type = self.properties.get(self.ADJUSTMENT_TYPE)
        adjustment_step = self.properties.get(self.MIN_ADJUSTMENT_STEP)
        if (adjustment_type != sc_util.PERCENT_CHANGE_IN_CAPACITY
                and adjustment_step is not None):
            raise exception.ResourcePropertyValueDependency(
                prop1=self.MIN_ADJUSTMENT_STEP,
                prop2=self.ADJUSTMENT_TYPE,
                value=sc_util.PERCENT_CHANGE_IN_CAPACITY)

    def handle_create(self):
        super(AutoScalingPolicy, self).handle_create()
        self.resource_id_set(self._get_user_id())

    def handle_update(self, json_snippet, tmpl_diff, prop_diff):
        """Updates self.properties, if Properties has changed.

        If Properties has changed, update self.properties, so we get the new
        values during any subsequent adjustment.
        """
        if prop_diff:
            self.properties = json_snippet.properties(self.properties_schema,
                                                      self.context)

    def handle_signal(self, details=None):
        # Template author can use scaling policy with any of the actions
        # of an alarm (i.e alarm_actions, insufficient_data_actions) and
        # it would be actioned irrespective of the alarm state. It's
        # fair to assume that the alarm state would be the appropriate one.
        # The responsibility of using a scaling policy with desired actions
        # lies with the template author, though this is normally expected to
        # be used with 'alarm_actions'.
        #
        # We also assume that the alarm state is 'alarm' when 'details' is None
        # or no 'current'/'state' key in 'details'. Watchrule has upper case
        # states, so we lower() them. This is only used for logging the alarm
        # state.

        if details is None:
            alarm_state = 'alarm'
        else:
            alarm_state = details.get('current',
                                      details.get('state', 'alarm')).lower()

        LOG.info('Alarm %(name)s, new state %(state)s',
                 {'name': self.name, 'state': alarm_state})

        asgn_id = self.properties[self.AUTO_SCALING_GROUP_NAME]
        group = self.stack.resource_by_refid(asgn_id)

        if group is None:
            raise exception.NotFound(_('Alarm %(alarm)s could not find '
                                       'scaling group named "%(group)s"'
                                       ) % {'alarm': self.name,
                                            'group': asgn_id})

        LOG.info('%(name)s alarm, adjusting group %(group)s with id '
                 '%(asgn_id)s by %(filter)s',
                 {'name': self.name, 'group': group.name,
                  'asgn_id': asgn_id,
                  'filter': self.properties[self.SCALING_ADJUSTMENT]})

        with group.frozen_properties():
            group.adjust(
                self.properties[self.SCALING_ADJUSTMENT],
                self.properties[self.ADJUSTMENT_TYPE],
                self.properties[self.MIN_ADJUSTMENT_STEP],
                self.properties[self.COOLDOWN])

    def _resolve_attribute(self, name):
        if self.resource_id is None:
            return
        if name == self.ALARM_URL:
            return str(self._get_ec2_signed_url(never_expire=True))
        elif name == self.SIGNAL_URL:
            return str(self._get_heat_signal_url())


def resource_mapping():
    return {
        'OS::Heat::ScalingPolicy': AutoScalingPolicy,
    }
