import contextlib
import socket
import time

SETTINGS_KEY = 'sprockets.mixins.metrics.statsd'
"""``self.settings`` key that configures this mix-in."""


class StatsdMixin(object):
    """
    Mix this class in to record metrics to a Statsd server.

    **Configuration**

    :namespace:
        Path to prefix metrics with.  If undefined, this defaults to
        ``applications`` + ``self.__class__.__module__``

    :host:
        Host name of the StatsD server to send metrics to.  If undefined,
        this defaults to ``127.0.0.1``.

    :port:
        Port number that the StatsD server is listening on.  If undefined,
        this defaults to ``8125``.

    """
    def initialize(self):
        super(StatsdMixin, self).initialize()
        settings = self.settings.setdefault(SETTINGS_KEY, {})
        if 'socket' not in settings:
            sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0)
            settings['socket'] = sock
        if 'namespace' not in settings:
            settings['namespace'] = 'applications.{}'.format(
                self.__class__.__module__)
        settings.setdefault('host', '127.0.0.1')
        settings.setdefault('port', '8125')
        self.__status_code = None

    def set_metric_tag(self, tag, value):
        """Ignored for statsd since it does not support tagging."""

    def set_status(self, status_code, reason=None):
        # Extended to track status code to avoid referencing the
        # _status internal variable
        self.__status_code = status_code
        super(StatsdMixin, self).set_status(status_code, reason=reason)

    def record_timing(self, duration, *path):
        """
        Record a timing.

        :param float duration: timing to record in seconds
        :param path: elements of the metric path to record

        This method records a timing to the application's namespace
        followed by a calculated path.  Each element of `path` is
        converted to a string and normalized before joining the
        elements by periods.  The normalization process is little
        more than replacing periods with dashes.

        """
        self._send(self._build_path(path), duration * 1000.0, 'ms')

    def increase_counter(self, *path, **kwargs):
        """
        Increase a counter.

        :param path: elements of the metric path to incr
        :keyword int amount: amount to increase the counter by.  If
            omitted, the counter is increased by one.

        This method increases a counter within the application's
        namespace.  Each element of `path` is converted to a string
        and normalized before joining the elements by periods.  The
        normalization process is little more than replacing periods
        with dashes.

        """
        self._send(self._build_path(path), kwargs.get('amount', '1'), 'c')

    @contextlib.contextmanager
    def execution_timer(self, *path):
        """
        Record the time it takes to perform an arbitrary code block.

        :param path: elements of the metric path to record

        This method returns a context manager that records the amount
        of time spent inside of the context and submits a timing metric
        to the specified `path` using (:meth:`record_timing`).

        """
        start = time.time()
        try:
            yield
        finally:
            self.record_timing(max(start, time.time()) - start, *path)

    def on_finish(self):
        """
        Records the time taken to process the request.

        This method records the amount of time taken to process the request
        (as reported by
        :meth:`~tornado.httputil.HTTPServerRequest.request_time`) under the
        path defined by the class's module, it's name, the request method,
        and the status code.  The :meth:`.record_timing` method is used
        to send the metric, so the configured namespace is used as well.

        """
        super(StatsdMixin, self).on_finish()
        self.record_timing(self.request.request_time(),
                           self.__class__.__name__, self.request.method,
                           self.__status_code)

    def _build_path(self, path):
        """Return a normalized path."""
        return '{}.{}'.format(self.settings[SETTINGS_KEY]['namespace'],
                              '.'.join(str(p).replace('.', '-') for p in path))

    def _send(self, path, value, stat_type):
        """Send a metric to Statsd."""
        settings = self.settings[SETTINGS_KEY]
        msg = '{0}:{1}|{2}'.format(path, value, stat_type)
        settings['socket'].sendto(msg.encode('ascii'),
                                  (settings['host'], int(settings['port'])))