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

import six

from heat.common import exception
from heat.common.i18n import _
from heat.engine import constraints
from heat.engine import properties
from heat.engine import resource
from heat.engine import support
from heat.engine import watchrule


COMMON_PROPERTIES = (
    ALARM_ACTIONS, OK_ACTIONS, REPEAT_ACTIONS, INSUFFICIENT_DATA_ACTIONS,
    DESCRIPTION, ENABLED,
) = (
    'alarm_actions', 'ok_actions', 'repeat_actions',
    'insufficient_data_actions', 'description', 'enabled',
)


common_properties_schema = {
    DESCRIPTION: properties.Schema(
        properties.Schema.STRING,
        _('Description for the alarm.'),
        update_allowed=True
    ),
    ENABLED: properties.Schema(
        properties.Schema.BOOLEAN,
        _('True if alarm evaluation/actioning is enabled.'),
        default='true',
        update_allowed=True
    ),
    ALARM_ACTIONS: properties.Schema(
        properties.Schema.LIST,
        _('A list of URLs (webhooks) to invoke when state transitions to '
          'alarm.'),
        update_allowed=True
    ),
    OK_ACTIONS: properties.Schema(
        properties.Schema.LIST,
        _('A list of URLs (webhooks) to invoke when state transitions to '
          'ok.'),
        update_allowed=True
    ),
    INSUFFICIENT_DATA_ACTIONS: properties.Schema(
        properties.Schema.LIST,
        _('A list of URLs (webhooks) to invoke when state transitions to '
          'insufficient-data.'),
        update_allowed=True
    ),
    REPEAT_ACTIONS: properties.Schema(
        properties.Schema.BOOLEAN,
        _('False to trigger actions when the threshold is reached AND '
          'the alarm\'s state has changed. By default, actions are called '
          'each time the threshold is reached.'),
        default='true',
        update_allowed=True
    )
}


NOVA_METERS = ['instance', 'memory', 'memory.usage',
               'cpu', 'cpu_util', 'vcpus',
               'disk.read.requests', 'disk.read.requests.rate',
               'disk.write.requests', 'disk.write.requests.rate',
               'disk.read.bytes', 'disk.read.bytes.rate',
               'disk.write.bytes', 'disk.write.bytes.rate',
               'disk.device.read.requests', 'disk.device.read.requests.rate',
               'disk.device.write.requests', 'disk.device.write.requests.rate',
               'disk.device.read.bytes', 'disk.device.read.bytes.rate',
               'disk.device.write.bytes', 'disk.device.write.bytes.rate',
               'disk.root.size', 'disk.ephemeral.size',
               'network.incoming.bytes', 'network.incoming.bytes.rate',
               'network.outgoing.bytes', 'network.outgoing.bytes.rate',
               'network.incoming.packets', 'network.incoming.packets.rate',
               'network.outgoing.packets', 'network.outgoing.packets.rate']


def actions_to_urls(stack, properties):
    kwargs = {}
    for k, v in iter(properties.items()):
        if k in [ALARM_ACTIONS, OK_ACTIONS,
                 INSUFFICIENT_DATA_ACTIONS] and v is not None:
            kwargs[k] = []
            for act in v:
                # if the action is a resource name
                # we ask the destination resource for an alarm url.
                # the template writer should really do this in the
                # template if possible with:
                # {Fn::GetAtt: ['MyAction', 'AlarmUrl']}
                if act in stack:
                    url = stack[act].FnGetAtt('AlarmUrl')
                    kwargs[k].append(url)
                else:
                    if act:
                        kwargs[k].append(act)
        else:
            kwargs[k] = v
    return kwargs


class CeilometerAlarm(resource.Resource):

    PROPERTIES = (
        COMPARISON_OPERATOR, EVALUATION_PERIODS, METER_NAME, PERIOD,
        STATISTIC, THRESHOLD, MATCHING_METADATA, QUERY,
    ) = (
        'comparison_operator', 'evaluation_periods', 'meter_name', 'period',
        'statistic', 'threshold', 'matching_metadata', 'query',
    )

    QUERY_FACTOR_FIELDS = (
        QF_FIELD, QF_OP, QF_VALUE,
    ) = (
        'field', 'op', 'value',
    )

    QF_OP_VALS = constraints.AllowedValues(['le', 'ge', 'eq',
                                            'lt', 'gt', 'ne'])

    properties_schema = {
        COMPARISON_OPERATOR: properties.Schema(
            properties.Schema.STRING,
            _('Operator used to compare specified statistic with threshold.'),
            constraints=[
                constraints.AllowedValues(['ge', 'gt', 'eq', 'ne', 'lt',
                                           'le']),
            ],
            update_allowed=True
        ),
        EVALUATION_PERIODS: properties.Schema(
            properties.Schema.INTEGER,
            _('Number of periods to evaluate over.'),
            update_allowed=True
        ),
        METER_NAME: properties.Schema(
            properties.Schema.STRING,
            _('Meter name watched by the alarm.'),
            required=True
        ),
        PERIOD: properties.Schema(
            properties.Schema.INTEGER,
            _('Period (seconds) to evaluate over.'),
            update_allowed=True
        ),
        STATISTIC: properties.Schema(
            properties.Schema.STRING,
            _('Meter statistic to evaluate.'),
            constraints=[
                constraints.AllowedValues(['count', 'avg', 'sum', 'min',
                                           'max']),
            ],
            update_allowed=True
        ),
        THRESHOLD: properties.Schema(
            properties.Schema.NUMBER,
            _('Threshold to evaluate against.'),
            required=True,
            update_allowed=True
        ),
        MATCHING_METADATA: properties.Schema(
            properties.Schema.MAP,
            _('Meter should match this resource metadata (key=value) '
              'additionally to the meter_name.'),
            default={},
            update_allowed=True
        ),
        QUERY: properties.Schema(
            properties.Schema.LIST,
            _('A list of query factors, each comparing '
              'a Sample attribute with a value. '
              'Implicitly combined with matching_metadata, if any.'),
            update_allowed=True,
            support_status=support.SupportStatus(version='2015.1'),
            schema=properties.Schema(
                properties.Schema.MAP,
                schema={
                    QF_FIELD: properties.Schema(
                        properties.Schema.STRING,
                        _('Name of attribute to compare. '
                          'Names of the form metadata.user_metadata.X '
                          'or metadata.metering.X are equivalent to what '
                          'you can address through matching_metadata; '
                          'the former for Nova meters, '
                          'the latter for all others. '
                          'To see the attributes of your Samples, '
                          'use `ceilometer --debug sample-list`. ')
                    ),
                    QF_OP: properties.Schema(
                        properties.Schema.STRING,
                        _('Comparison operator'),
                        constraints=[QF_OP_VALS]
                    ),
                    QF_VALUE: properties.Schema(
                        properties.Schema.STRING,
                        _('String value with which to compare')
                    )
                }
            )
        )
    }
    properties_schema.update(common_properties_schema)

    default_client_name = 'ceilometer'

    def cfn_to_ceilometer(self, stack, properties):
        """Apply all relevant compatibility xforms."""

        kwargs = actions_to_urls(stack, properties)
        kwargs['type'] = 'threshold'
        if kwargs.get(self.METER_NAME) in NOVA_METERS:
            prefix = 'user_metadata.'
        else:
            prefix = 'metering.'

        rule = {}
        for field in ['period', 'evaluation_periods', 'threshold',
                      'statistic', 'comparison_operator', 'meter_name']:
            if field in kwargs:
                rule[field] = kwargs[field]
                del kwargs[field]
        mmd = properties.get(self.MATCHING_METADATA) or {}
        query = properties.get(self.QUERY) or []

        # make sure the matching_metadata appears in the query like this:
        # {field: metadata.$prefix.x, ...}
        for m_k, m_v in six.iteritems(mmd):
            if m_k.startswith('metadata.%s' % prefix):
                key = m_k
            elif m_k.startswith(prefix):
                key = 'metadata.%s' % m_k
            else:
                key = 'metadata.%s%s' % (prefix, m_k)
            # NOTE(prazumovsky): type of query value must be a string, but
            # matching_metadata value type can not be a string, so we
            # must convert value to a string type.
            query.append(dict(field=key, op='eq', value=six.text_type(m_v)))
        if self.MATCHING_METADATA in kwargs:
            del kwargs[self.MATCHING_METADATA]
        if self.QUERY in kwargs:
            del kwargs[self.QUERY]
        if query:
            rule['query'] = query
        kwargs['threshold_rule'] = rule
        return kwargs

    def handle_create(self):
        props = self.cfn_to_ceilometer(self.stack,
                                       self.properties)
        props['name'] = self.physical_resource_name()
        alarm = self.ceilometer().alarms.create(**props)
        self.resource_id_set(alarm.alarm_id)

        # the watchrule below is for backwards compatibility.
        # 1) so we don't create watch tasks unneccessarly
        # 2) to support CW stats post, we will redirect the request
        #    to ceilometer.
        wr = watchrule.WatchRule(context=self.context,
                                 watch_name=self.physical_resource_name(),
                                 rule=self.parsed_template('Properties'),
                                 stack_id=self.stack.id)
        wr.state = wr.CEILOMETER_CONTROLLED
        wr.store()

    def handle_update(self, json_snippet, tmpl_diff, prop_diff):
        if prop_diff:
            kwargs = {'alarm_id': self.resource_id}
            kwargs.update(self.properties)
            kwargs.update(prop_diff)
            alarms_client = self.ceilometer().alarms
            alarms_client.update(**self.cfn_to_ceilometer(self.stack, kwargs))

    def handle_suspend(self):
        if self.resource_id is not None:
            self.ceilometer().alarms.update(alarm_id=self.resource_id,
                                            enabled=False)

    def handle_resume(self):
        if self.resource_id is not None:
            self.ceilometer().alarms.update(alarm_id=self.resource_id,
                                            enabled=True)

    def handle_delete(self):
        try:
            wr = watchrule.WatchRule.load(
                self.context, watch_name=self.physical_resource_name())
            wr.destroy()
        except exception.WatchRuleNotFound:
            pass

        if self.resource_id is not None:
            try:
                self.ceilometer().alarms.delete(self.resource_id)
            except Exception as ex:
                self.client_plugin().ignore_not_found(ex)


class CombinationAlarm(resource.Resource):

    support_status = support.SupportStatus(version='2014.1')

    PROPERTIES = (
        ALARM_IDS, OPERATOR,
    ) = (
        'alarm_ids', 'operator',
    )

    properties_schema = {
        ALARM_IDS: properties.Schema(
            properties.Schema.LIST,
            _('List of alarm identifiers to combine.'),
            required=True,
            constraints=[constraints.Length(min=1)],
            update_allowed=True),
        OPERATOR: properties.Schema(
            properties.Schema.STRING,
            _('Operator used to combine the alarms.'),
            constraints=[constraints.AllowedValues(['and', 'or'])],
            update_allowed=True)
    }
    properties_schema.update(common_properties_schema)

    default_client_name = 'ceilometer'

    def handle_create(self):
        properties = actions_to_urls(self.stack,
                                     self.properties)
        properties['name'] = self.physical_resource_name()
        properties['type'] = 'combination'

        alarm = self.ceilometer().alarms.create(
            **self._reformat_properties(properties))
        self.resource_id_set(alarm.alarm_id)

    def _reformat_properties(self, properties):
        combination_rule = {}
        for name in [self.ALARM_IDS, self.OPERATOR, REPEAT_ACTIONS]:
            value = properties.pop(name, None)
            if value:
                combination_rule[name] = value
        if combination_rule:
            properties['combination_rule'] = combination_rule
        return properties

    def handle_update(self, json_snippet, tmpl_diff, prop_diff):
        if prop_diff:
            kwargs = {'alarm_id': self.resource_id}
            kwargs.update(prop_diff)
            alarms_client = self.ceilometer().alarms
            alarms_client.update(**self._reformat_properties(
                actions_to_urls(self.stack, kwargs)))

    def handle_suspend(self):
        self.ceilometer().alarms.update(
            alarm_id=self.resource_id, enabled=False)

    def handle_resume(self):
        self.ceilometer().alarms.update(
            alarm_id=self.resource_id, enabled=True)

    def handle_delete(self):
        try:
            self.ceilometer().alarms.delete(self.resource_id)
        except Exception as ex:
            self.client_plugin().ignore_not_found(ex)


def resource_mapping():
    return {
        'OS::Ceilometer::Alarm': CeilometerAlarm,
        'OS::Ceilometer::CombinationAlarm': CombinationAlarm,
    }
