# Copyright 2014 Mirantis, Inc.
#
# 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 collections
from time import monotonic as now

from oslo_log import log
from oslo_utils import timeutils

import ceilometer
from ceilometer.compute.pollsters import util
from ceilometer.compute.virt import inspector as virt_inspector
from ceilometer.polling import plugin_base
from ceilometer import sample

LOG = log.getLogger(__name__)


class NoVolumeException(Exception):
    pass


class GenericComputePollster(plugin_base.PollsterBase):
    """This class aims to cache instance statistics data

    First polled pollsters that inherit of this will retrieve and cache
    stats of an instance, then other pollsters will just build the samples
    without queyring the backend anymore.
    """

    sample_name = None
    sample_unit = ''
    sample_type = sample.TYPE_GAUGE
    sample_stats_key = None
    inspector_method = None

    def setup_environment(self):
        super().setup_environment()
        self.inspector = GenericComputePollster._get_inspector(self.conf)

    @staticmethod
    def aggregate_method(stats):
        # Don't aggregate anything by default
        return stats

    @staticmethod
    def _get_inspector(conf):
        # FIXME(sileht): This doesn't looks threadsafe...
        try:
            inspector = GenericComputePollster._inspector
        except AttributeError:
            inspector = virt_inspector.get_hypervisor_inspector(conf)
            GenericComputePollster._inspector = inspector
        return inspector

    @property
    def default_discovery(self):
        return 'local_instances'

    def _record_poll_time(self):
        """Method records current time as the poll time.

        :return: time in seconds since the last poll time was recorded
        """
        current_time = timeutils.utcnow()
        duration = None
        if hasattr(self, '_last_poll_time'):
            duration = timeutils.delta_seconds(self._last_poll_time,
                                               current_time)
        self._last_poll_time = current_time
        return duration

    @staticmethod
    def get_additional_metadata(instance, stats):
        pass

    @staticmethod
    def get_resource_id(instance, stats):
        return instance.id

    def _inspect_cached(self, cache, instance, duration):
        cache.setdefault(self.inspector_method, {})
        if instance.id not in cache[self.inspector_method]:
            result = getattr(self.inspector, self.inspector_method)(
                instance, duration)
            polled_time = now()
            # Ensure we don't cache an iterator
            if isinstance(result, collections.abc.Iterable):
                result = list(result)
            else:
                result = [result]
            cache[self.inspector_method][instance.id] = (polled_time, result)
        return cache[self.inspector_method][instance.id]

    def _stats_to_sample(self, instance, stats, polled_time):
        volume = getattr(stats, self.sample_stats_key)
        LOG.debug(
            "%(instance_id)s/%(name)s volume: %(volume)s",
            {'name': self.sample_name,
             'instance_id': instance.id,
             'volume': (volume if volume is not None else 'Unavailable')})

        if volume is None:
            raise NoVolumeException()

        return util.make_sample_from_instance(
            self.conf,
            instance,
            name=self.sample_name,
            unit=self.sample_unit,
            type=self.sample_type,
            resource_id=self.get_resource_id(instance, stats),
            volume=volume,
            additional_metadata=self.get_additional_metadata(
                instance, stats),
            monotonic_time=polled_time,
        )

    def get_samples(self, manager, cache, resources):
        self._inspection_duration = self._record_poll_time()
        for instance in resources:
            try:
                polled_time, result = self._inspect_cached(
                    cache, instance, self._inspection_duration)
                if not result:
                    continue
                for stats in self.aggregate_method(result):
                    yield self._stats_to_sample(instance, stats, polled_time)
            except NoVolumeException:
                # FIXME(sileht): This should be a removed... but I will
                # not change the test logic for now
                LOG.warning("%(name)s statistic in not available for "
                            "instance %(instance_id)s",
                            {'name': self.sample_name,
                             'instance_id': instance.id})
            except virt_inspector.InstanceNotFoundException as err:
                # Instance was deleted while getting samples. Ignore it.
                LOG.debug('Exception while getting samples %s', err)
            except virt_inspector.InstanceShutOffException as e:
                LOG.debug('Instance %(instance_id)s was shut off while '
                          'getting sample of %(name)s: %(exc)s',
                          {'instance_id': instance.id,
                           'name': self.sample_name, 'exc': e})
            except virt_inspector.NoDataException as e:
                LOG.warning('Cannot inspect data of %(pollster)s for '
                            '%(instance_id)s, non-fatal reason: %(exc)s',
                            {'pollster': self.__class__.__name__,
                             'instance_id': instance.id, 'exc': e})
            except ceilometer.NotImplementedError:
                # Selected inspector does not implement this pollster.
                LOG.debug('%(inspector)s does not provide data for '
                          '%(pollster)s',
                          {'inspector': self.inspector.__class__.__name__,
                           'pollster': self.__class__.__name__})
                raise plugin_base.PollsterPermanentError(resources)
            except Exception as err:
                LOG.error(
                    'Could not get %(name)s events for %(id)s: %(e)s', {
                        'name': self.sample_name, 'id': instance.id, 'e': err},
                    exc_info=True)


class InstanceMetadataPollster(plugin_base.PollsterBase):
    """A base class for implementing a pollster using instance metadata.

    This metadata is originally supplied by Nova, but if
    instance_discovery_method is set to libvirt_metadata,
    metadata is fetched from the local libvirt socket,
    just like with the standard compute pollsters.
    """

    sample_name = None
    sample_unit = ''
    sample_type = sample.TYPE_GAUGE

    @property
    def default_discovery(self):
        return 'local_instances'

    def get_resource_id(self, instance):
        return instance.id

    def get_volume(self, instance):
        raise ceilometer.NotImplementedError

    def get_additional_metadata(self, instance):
        return {}

    def get_samples(self, manager, cache, resources):
        for instance in resources:
            yield util.make_sample_from_instance(
                self.conf,
                instance,
                name=self.sample_name,
                unit=self.sample_unit,
                type=self.sample_type,
                resource_id=self.get_resource_id(instance),
                volume=self.get_volume(instance),
                additional_metadata=self.get_additional_metadata(instance),
                monotonic_time=now(),
            )
