#
# Copyright 2013 Rackspace Hosting.
#
# 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 fnmatch
import os

import jsonpath_rw
from oslo_config import cfg
from oslo_utils import timeutils
import six
import yaml

from ceilometer.event.storage import models
from ceilometer.i18n import _
from ceilometer.openstack.common import log

OPTS = [
    cfg.StrOpt('definitions_cfg_file',
               default="event_definitions.yaml",
               help="Configuration file for event definitions."
               ),
    cfg.BoolOpt('drop_unmatched_notifications',
                default=False,
                help='Drop notifications if no event definition matches. '
                '(Otherwise, we convert them with just the default traits)'),

]

cfg.CONF.register_opts(OPTS, group='event')

LOG = log.getLogger(__name__)


class EventDefinitionException(Exception):
    def __init__(self, message, definition_cfg):
        super(EventDefinitionException, self).__init__(message)
        self.definition_cfg = definition_cfg

    def __str__(self):
        return '%s %s: %s' % (self.__class__.__name__,
                              self.definition_cfg, self.message)


class TraitDefinition(object):

    def __init__(self, name, trait_cfg, plugin_manager):
        self.cfg = trait_cfg
        self.name = name

        type_name = trait_cfg.get('type', 'text')

        if 'plugin' in trait_cfg:
            plugin_cfg = trait_cfg['plugin']
            if isinstance(plugin_cfg, six.string_types):
                plugin_name = plugin_cfg
                plugin_params = {}
            else:
                try:
                    plugin_name = plugin_cfg['name']
                except KeyError:
                    raise EventDefinitionException(
                        _('Plugin specified, but no plugin name supplied for '
                          'trait %s') % name, self.cfg)
                plugin_params = plugin_cfg.get('parameters')
                if plugin_params is None:
                    plugin_params = {}
            try:
                plugin_ext = plugin_manager[plugin_name]
            except KeyError:
                raise EventDefinitionException(
                    _('No plugin named %(plugin)s available for '
                      'trait %(trait)s') % dict(plugin=plugin_name,
                                                trait=name), self.cfg)
            plugin_class = plugin_ext.plugin
            self.plugin = plugin_class(**plugin_params)
        else:
            self.plugin = None

        if 'fields' not in trait_cfg:
            raise EventDefinitionException(
                _("Required field in trait definition not specified: "
                  "'%s'") % 'fields',
                self.cfg)

        fields = trait_cfg['fields']
        if not isinstance(fields, six.string_types):
            # NOTE(mdragon): if not a string, we assume a list.
            if len(fields) == 1:
                fields = fields[0]
            else:
                fields = '|'.join('(%s)' % path for path in fields)
        try:
            self.fields = jsonpath_rw.parse(fields)
        except Exception as e:
            raise EventDefinitionException(
                _("Parse error in JSONPath specification "
                  "'%(jsonpath)s' for %(trait)s: %(err)s")
                % dict(jsonpath=fields, trait=name, err=e), self.cfg)
        self.trait_type = models.Trait.get_type_by_name(type_name)
        if self.trait_type is None:
            raise EventDefinitionException(
                _("Invalid trait type '%(type)s' for trait %(trait)s")
                % dict(type=type_name, trait=name), self.cfg)

    def _get_path(self, match):
        if match.context is not None:
            for path_element in self._get_path(match.context):
                yield path_element
            yield str(match.path)

    def to_trait(self, notification_body):
        values = [match for match in self.fields.find(notification_body)
                  if match.value is not None]

        if self.plugin is not None:
            value_map = [('.'.join(self._get_path(match)), match.value) for
                         match in values]
            value = self.plugin.trait_value(value_map)
        else:
            value = values[0].value if values else None

        if value is None:
            return None

        # NOTE(mdragon): some openstack projects (mostly Nova) emit ''
        # for null fields for things like dates.
        if self.trait_type != models.Trait.TEXT_TYPE and value == '':
            return None

        value = models.Trait.convert_value(self.trait_type, value)
        return models.Trait(self.name, self.trait_type, value)


class EventDefinition(object):

    DEFAULT_TRAITS = dict(
        service=dict(type='text', fields='publisher_id'),
        request_id=dict(type='text', fields='_context_request_id'),
        tenant_id=dict(type='text', fields=['payload.tenant_id',
                                            '_context_tenant']),
    )

    def __init__(self, definition_cfg, trait_plugin_mgr):
        self._included_types = []
        self._excluded_types = []
        self.traits = dict()
        self.cfg = definition_cfg

        try:
            event_type = definition_cfg['event_type']
            traits = definition_cfg['traits']
        except KeyError as err:
            raise EventDefinitionException(
                _("Required field %s not specified") % err.args[0], self.cfg)

        if isinstance(event_type, six.string_types):
            event_type = [event_type]

        for t in event_type:
            if t.startswith('!'):
                self._excluded_types.append(t[1:])
            else:
                self._included_types.append(t)

        if self._excluded_types and not self._included_types:
            self._included_types.append('*')

        for trait_name in self.DEFAULT_TRAITS:
            self.traits[trait_name] = TraitDefinition(
                trait_name,
                self.DEFAULT_TRAITS[trait_name],
                trait_plugin_mgr)
        for trait_name in traits:
            self.traits[trait_name] = TraitDefinition(
                trait_name,
                traits[trait_name],
                trait_plugin_mgr)