# -*- encoding: utf-8 -*-
#
# Copyright © 2017-2018 Red Hat, Inc.
# Copyright © 2014-2015 eNovance
#
# 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
import functools
import itertools
import operator

import daiquiri
import numpy
import six

from gnocchi.carbonara import TIMESERIES_ARRAY_DTYPE
from gnocchi import exceptions
from gnocchi import utils

LOG = daiquiri.getLogger(__name__)


Measure = collections.namedtuple("Measure", ['timestamp', 'value'])


ITEMGETTER_1 = operator.itemgetter(1)


class ReportGenerationError(Exception):
    pass


class SackDetectionError(Exception):
    pass


@functools.total_ordering
class Sack(object):
    """A sack is a recipient that contains measures for a group of metrics.

    It is identified by a positive integer called `number`.
    """

    # Use slots to make them as small as possible since we can create a ton of
    # those.
    __slots__ = [
        "number",
        "total",
        "name",
    ]

    def __init__(self, number, total, name):
        """Create a new sack.

        :param number: The sack number, identifying it.
        :param total: The total number of sacks.
        :param name: The sack name.
        """
        self.number = number
        self.total = total
        self.name = name

    def __str__(self):
        return self.name

    def __repr__(self):
        return "<%s(%d/%d) %s>" % (
            self.__class__.__name__, self.number, self.total, str(self),
        )

    def _compare(self, op, other):
        if isinstance(other, Sack):
            if self.total != other.total:
                raise TypeError(
                    "Cannot compare %s with different total number" %
                    self.__class__.__name__)
            return op(self.number, other.number)
        raise TypeError("Cannot compare %r with %r" % (self, other))

    def __lt__(self, other):
        return self._compare(operator.lt, other)

    def __eq__(self, other):
        return self._compare(operator.eq, other)

    def __ne__(self, other):
        # neither total_ordering nor py2 sets ne as the opposite of eq
        return self._compare(operator.ne, other)

    def __hash__(self):
        return hash(self.name)


class IncomingDriver(object):
    MEASURE_PREFIX = "measure"
    SACK_NAME_FORMAT = "incoming{total}-{number}"
    CFG_PREFIX = 'gnocchi-config'
    CFG_SACKS = 'sacks'
    # NOTE(sileht): By default we use threads, but some driver can disable
    # threads by setting this to utils.sequencial_map
    MAP_METHOD = staticmethod(utils.parallel_map)

    @property
    def NUM_SACKS(self):
        if not hasattr(self, '_num_sacks'):
            try:
                self._num_sacks = int(self._get_storage_sacks())
            except Exception as e:
                raise SackDetectionError(e)
        return self._num_sacks

    def __init__(self, conf, greedy=True):
        self._sacks = None

    def upgrade(self, num_sacks):
        try:
            self.NUM_SACKS
        except SackDetectionError:
            self.set_storage_settings(num_sacks)

    @staticmethod
    def set_storage_settings(num_sacks):
        raise exceptions.NotImplementedError

    @staticmethod
    def remove_sack_group(num_sacks):
        raise exceptions.NotImplementedError

    @staticmethod
    def get_storage_sacks():
        """Return the number of sacks in storage. None if not set."""
        raise exceptions.NotImplementedError

    def _make_measures_array(self):
        return numpy.array([], dtype=TIMESERIES_ARRAY_DTYPE)

    @staticmethod
    def _array_concatenate(arrays):
        if arrays:
            return numpy.concatenate(arrays)
        return arrays

    def _unserialize_measures(self, measure_id, data):
        try:
            return numpy.frombuffer(data, dtype=TIMESERIES_ARRAY_DTYPE)
        except ValueError:
            LOG.error(
                "Unable to decode measure %s, possible data corruption",
                measure_id)
            raise

    def _encode_measures(self, measures):
        return numpy.fromiter(measures,
                              dtype=TIMESERIES_ARRAY_DTYPE).tobytes()

    def group_metrics_by_sack(self, metrics):
        """Iterate on a list of metrics, grouping them by sack.

        :param metrics: A list of metric uuid.
        :return: An iterator yield (group, metrics).
        """
        metrics_and_sacks = sorted(
            ((m, self.sack_for_metric(m)) for m in metrics),
            key=ITEMGETTER_1)
        for sack, metrics in itertools.groupby(metrics_and_sacks,
                                               key=ITEMGETTER_1):
            yield sack, [m[0] for m in metrics]

    def add_measures(self, metric_id, measures):
        """Add a measure to a metric.

        :param metric_id: The metric measured.
        :param measures: The actual measures.
        """
        self.add_measures_batch({metric_id: measures})

    def add_measures_batch(self, metrics_and_measures):
        """Add a batch of measures for some metrics.

        :param metrics_and_measures: A dict where keys are metric objects
                                     and values are a list of
                                     :py:class:`gnocchi.incoming.Measure`.
        """
        self.MAP_METHOD(self._store_new_measures,
                        ((metric_id, self._encode_measures(measures))
                         for metric_id, measures
                         in six.iteritems(metrics_and_measures)))

    @staticmethod
    def _store_new_measures(metric_id, data):
        raise exceptions.NotImplementedError

    def measures_report(self, details=True):
        """Return a report of pending to process measures.

        Only useful for drivers that process measurements in background

        :return: {'summary': {'metrics': count, 'measures': count},
                  'details': {metric_id: pending_measures_count}}
        """
        metrics, measures, full_details = self._build_report(details)
        report = {'summary': {'metrics': metrics, 'measures': measures}}
        if full_details is not None:
            report['details'] = full_details
        return report

    @staticmethod
    def _build_report(details):
        raise exceptions.NotImplementedError

    @staticmethod
    def delete_unprocessed_measures_for_metric(metric_id):
        raise exceptions.NotImplementedError

    @staticmethod
    def process_measure_for_metrics(metric_id):
        raise exceptions.NotImplementedError

    @staticmethod
    def process_measures_for_sack(sack):
        raise exceptions.NotImplementedError

    @staticmethod
    def has_unprocessed(metric_id):
        raise exceptions.NotImplementedError

    def _get_sack_name(self, number):
        return self.SACK_NAME_FORMAT.format(
            total=self.NUM_SACKS, number=number)

    def _make_sack(self, i):
        return Sack(i, self.NUM_SACKS, self._get_sack_name(i))

    def sack_for_metric(self, metric_id):
        return self._make_sack(metric_id.int % self.NUM_SACKS)

    def iter_sacks(self):
        return (self._make_sack(i) for i in six.moves.range(self.NUM_SACKS))

    @staticmethod
    def iter_on_sacks_to_process():
        """Return an iterable of sack that got new measures to process."""
        raise exceptions.NotImplementedError

    @staticmethod
    def finish_sack_processing(sack):
        """Mark sack processing has finished."""
        pass


@utils.retry_on_exception_and_log("Unable to initialize incoming driver")
def get_driver(conf):
    """Return configured incoming driver only

    :param conf: incoming configuration only (not global)
    """
    return utils.get_driver_class('gnocchi.incoming', conf.incoming)(
        conf.incoming, conf.metricd.greedy)