#
# Copyright 2015 NEC Corporation.
#
# 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 copy
import fnmatch
import operator

from oslo_config import cfg
from oslo_log import log
from oslo_utils import timeutils

from aodh import evaluator
from aodh.i18n import _, _LE

LOG = log.getLogger(__name__)

COMPARATORS = {
    'gt': operator.gt,
    'lt': operator.lt,
    'ge': operator.ge,
    'le': operator.le,
    'eq': operator.eq,
    'ne': operator.ne,
}

OPTS = [
    cfg.IntOpt('event_alarm_cache_ttl',
               default=60,
               help='TTL of event alarm caches, in seconds. '
                    'Set to 0 to disable caching.'),
]


class EventAlarmEvaluator(evaluator.Evaluator):

    def __init__(self, conf, notifier):
        super(EventAlarmEvaluator, self).__init__(conf, notifier)
        self.caches = {}

    def evaluate_events(self, events):
        """Evaluate the events by referring related alarms."""

        if not isinstance(events, list):
            events = [events]

        LOG.debug('Starting event alarm evaluation: #events = %d',
                  len(events))
        for event in events:
            LOG.debug('Evaluating event: event = %s', event)

            if not self._validate(event):
                LOG.debug('Aborting evaluation of the event.')
                continue

            project = self._get_project(event)
            alarms = self._get_project_alarms(project)
            LOG.debug('Found %(num)d alarms related to the event '
                      '(message_id=%(id)s)',
                      {'num': len(alarms), 'id': event['message_id']})

            for alarm in alarms:
                try:
                    self._evaluate_alarm(alarm, event)
                except Exception:
                    LOG.exception(_LE('Failed to evaluate alarm (id=%(a)s) '
                                      'triggered by event = %(e)s.'),
                                  {'a': alarm.alarm_id, 'e': event})

        LOG.debug('Finished event alarm evaluation.')

    @staticmethod
    def _validate(event):
        """Validate received event has mandatory parameters."""

        if not event:
            LOG.error(_LE('Received invalid event (empty or None)'))
            return False

        if not event.get('event_type'):
            LOG.error(_LE('Failed to extract event_type from event = %s'),
                      event)
            return False

        if not event.get('message_id'):
            LOG.error(_LE('Failed to extract message_id from event = %s'),
                      event)
            return False

        return True

    @staticmethod
    def _get_project(event):
        """Extract project ID from the event."""
        for trait in event.get('traits') or []:
            if trait[0] in (u'tenant_id', u'project_id'):
                return trait[2]
        return ''

    def _get_project_alarms(self, project):
        if self.conf.event_alarm_cache_ttl and project in self.caches:
            if timeutils.is_older_than(self.caches[project]['updated'],
                                       self.conf.event_alarm_cache_ttl):
                del self.caches[project]
            else:
                return self.caches[project]['alarms']

        alarms = self._storage_conn.get_alarms(enabled=True,
                                               alarm_type='event',
                                               project=project)

        if self.conf.event_alarm_cache_ttl:
            self.caches[project] = {
                'alarms': alarms,
                'updated': timeutils.utcnow()
            }

        return alarms

    @staticmethod
    def _sanitize(event):
        """Change traits format to dict."""
        e = copy.deepcopy(event)
        e['traits'] = {t[0]: t[2] for t in event.get('traits', [])}
        return e

    def _evaluate_alarm(self, alarm, event):
        """Evaluate the alarm by referring the received event.

        This function compares each condition of the alarm on the assumption
        that all conditions are combined by AND operator.
        When the received event met conditions defined in alarm 'event_type'
        and 'query', the alarm will be fired and updated to state='alarm'
        (alarmed).
        Note: by this evaluator, the alarm won't be changed to state='ok'
        nor state='insufficient data'.
        """

        LOG.debug('Evaluating alarm (id=%(a)s) triggered by event '
                  '(message_id=%(e)s).',
                  {'a': alarm.alarm_id, 'e': event['message_id']})

        if not alarm.repeat_actions and alarm.state == evaluator.ALARM:
            LOG.debug('Skip evaluation of the alarm id=%s which have already '
                      'fired.', alarm.alarm_id)
            return

        event_pattern = alarm.rule['event_type']
        if not fnmatch.fnmatch(event['event_type'], event_pattern):
            LOG.debug('Aborting evaluation of the alarm (id=%s) due to '
                      'uninterested event_type.', alarm.alarm_id)
            return

        value = self._sanitize(event)

        def _compare(condition):
            op = COMPARATORS[condition.get('op', 'eq')]
            v = value
            for f in condition['field'].split('.'):
                if hasattr(v, 'get'):
                    v = v.get(f)
                else:
                    break
            LOG.debug('Comparing value=%(v)s against condition=%(c)s .',
                      {'v': v, 'c': condition})
            return op(v, condition['value'])

        for condition in alarm.rule['query']:
            if not _compare(condition):
                LOG.debug('Aborting evaluation of the alarm due to '
                          'unmet condition=%s .', condition)
                return

        self._fire_alarm(alarm, event)

    def _fire_alarm(self, alarm, event):
        """Update alarm state and fire alarm via alarm notifier."""

        state = evaluator.ALARM
        reason = (_('Event (message_id=%(message)s) hit the query of alarm '
                    '(id=%(alarm)s)') %
                  {'message': event['message_id'], 'alarm': alarm.alarm_id})
        reason_data = {'type': 'event', 'event': event}
        self._refresh(alarm, state, reason, reason_data)

    def _refresh(self, alarm, state, reason, reason_data):
        super(EventAlarmEvaluator, self)._refresh(alarm, state,
                                                  reason, reason_data)

        project = alarm.project_id
        if self.conf.event_alarm_cache_ttl and project in self.caches:
            for index, a in enumerate(self.caches[project]['alarms']):
                if a.alarm_id == alarm.alarm_id:
                    alarm.state = state
                    self.caches[project]['alarms'][index] = alarm
                    break

    # NOTE(r-mibu): This method won't be used, but we have to define here in
    # order to overwrite the abstract method in the super class.
    # TODO(r-mibu): Change the base (common) class design for evaluators.
    def evaluate(self, alarm):
        pass
