import cherrypy
import math
import os
import time
from collections import OrderedDict
from mgr_module import MgrModule

# Defaults for the Prometheus HTTP server.  Can also set in config-key
# see https://github.com/prometheus/prometheus/wiki/Default-port-allocations
# for Prometheus exporter port registry

DEFAULT_ADDR = '::'
DEFAULT_PORT = 9283


# cherrypy likes to sys.exit on error.  don't let it take us down too!
def os_exit_noop():
    pass


os._exit = os_exit_noop


# to access things in class Module from subclass Root.  Because
# it's a dict, the writer doesn't need to declare 'global' for access

_global_instance = {'plugin': None}


def global_instance():
    assert _global_instance['plugin'] is not None
    return _global_instance['plugin']


# counter value types
PERFCOUNTER_TIME = 1
PERFCOUNTER_U64 = 2

# counter types
PERFCOUNTER_LONGRUNAVG = 4
PERFCOUNTER_COUNTER = 8
PERFCOUNTER_HISTOGRAM = 0x10
PERFCOUNTER_TYPE_MASK = ~2


def stattype_to_str(stattype):

    typeonly = stattype & PERFCOUNTER_TYPE_MASK
    if typeonly == 0:
        return 'gauge'
    if typeonly == PERFCOUNTER_LONGRUNAVG:
        # this lie matches the DaemonState decoding: only val, no counts
        return 'counter'
    if typeonly == PERFCOUNTER_COUNTER:
        return 'counter'
    if typeonly == PERFCOUNTER_HISTOGRAM:
        return 'histogram'

    return ''


class Metric(object):
    def __init__(self, mtype, name, desc, labels=None):
        self.mtype = mtype
        self.name = name
        self.desc = desc
        self.labelnames = labels    # tuple if present
        self.value = dict()         # indexed by label values

    def set(self, value, labelvalues=None):
        # labelvalues must be a tuple
        labelvalues = labelvalues or ('',)
        self.value[labelvalues] = value

    def str_expfmt(self):

        def promethize(path):
            ''' replace illegal metric name characters '''
            return path.replace('.', '_').replace('-', '_')

        def floatstr(value):
            ''' represent as Go-compatible float '''
            if value == float('inf'):
                return '+Inf'
            if value == float('-inf'):
                return '-Inf'
            if math.isnan(value):
                return 'NaN'
            return repr(float(value))

        name = promethize(self.name)
        expfmt = '''
# HELP {name} {desc}
# TYPE {name} {mtype}'''.format(
            name=name,
            desc=self.desc,
            mtype=self.mtype,
        )

        for labelvalues, value in self.value.items():
            if self.labelnames:
                labels = zip(self.labelnames, labelvalues)
                labels = ','.join('%s="%s"' % (k, v) for k, v in labels)
            else:
                labels = ''
            if labels:
                fmtstr = '\n{name}{{{labels}}} {value}'
            else:
                fmtstr = '\n{name} {value}'
            expfmt += fmtstr.format(
                name=name,
                labels=labels,
                value=floatstr(value),
            )
        return expfmt


class Module(MgrModule):

    def __init__(self, *args, **kwargs):
        super(Module, self).__init__(*args, **kwargs)
        self.notified = False
        self.serving = False
        self.metrics = dict()
        self.schema = OrderedDict()
        _global_instance['plugin'] = self

    def _get_ordered_schema(self, **kwargs):

        '''
        fetch an ordered-by-key performance counter schema
        ['perf_schema'][daemontype.id][countername] with keys
        'nick' (if present)
        'description'
        'type' (counter type....counter/gauge/avg/histogram/etc.)
        '''

        daemon_type = kwargs.get('daemon_type', '')
        daemon_id = kwargs.get('daemon_id', '')

        schema = self.get_perf_schema(daemon_type, daemon_id)
        if not schema:
            self.log.warning('_get_ordered_schema: no data')
            return

        new_schema = dict()
        for k1 in schema.keys():    # 'perf_schema', but assume only one
            for k2 in sorted(schema[k1].keys()):
                sorted_dict = OrderedDict(
                    sorted(schema[k1][k2].items(), key=lambda i: i[0])
                )
                new_schema[k2] = sorted_dict
        for k in sorted(new_schema.keys()):
            self.log.debug("updating schema for %s" % k)
            self.schema[k] = new_schema[k]

    def shutdown(self):
        self.serving = False
        pass

    # XXX duplicated from dashboard; factor out?
    def get_latest(self, daemon_type, daemon_name, stat):
        data = self.get_counter(daemon_type, daemon_name, stat)[stat]
        if data:
            return data[-1][1]
        else:
            return 0

    def get_stat(self, daemon, path):

        perfcounter = self.schema[daemon][path]
        stattype = stattype_to_str(perfcounter['type'])
        # XXX simplify first effort: no histograms
        # averages are already collapsed to one value for us
        if not stattype or stattype == 'histogram':
            self.log.debug('ignoring %s, type %s' % (path, stattype))
            return

        if path not in self.metrics:
            self.metrics[path] = Metric(
                stattype,
                path,
                perfcounter['description'],
                ('daemon',),
            )

        daemon_type, daemon_id = daemon.split('.')

        self.metrics[path].set(
            self.get_latest(daemon_type, daemon_id, path),
            (daemon,)
        )

    def collect(self):
        for daemon in self.schema.keys():
            for path in self.schema[daemon].keys():
                self.get_stat(daemon, path)
        return self.metrics

    def notify(self, ntype, nid):
        ''' Just try to sync and not run until we're notified once '''
        if not self.notified:
            self.serving = True
            self.notified = True
        if ntype == 'perf_schema_update':
            daemon_type, daemon_id = nid.split('.')
            self._get_ordered_schema(
                daemon_type=daemon_type,
                daemon_id=daemon_id
            )

    def serve(self):

        class Root(object):

            # collapse everything to '/'
            def _cp_dispatch(self, vpath):
                cherrypy.request.path = ''
                return self

            def format_metrics(self, metrics):
                formatted = ''
                for m in metrics.values():
                    formatted += m.str_expfmt()
                return formatted + '\n'

            @cherrypy.expose
            def index(self):
                metrics = global_instance().collect()
                cherrypy.response.headers['Content-Type'] = 'text/plain'
                if metrics:
                    return self.format_metrics(metrics)

        server_addr = self.get_localized_config('server_addr', DEFAULT_ADDR)
        server_port = self.get_localized_config('server_port', DEFAULT_PORT)
        self.log.info(
            "server_addr: %s server_port: %s" %
            (server_addr, server_port)
        )
        # wait for first notification (of any kind) to start up
        while not self.serving:
            time.sleep(1)

        cherrypy.config.update({
            'server.socket_host': server_addr,
            'server.socket_port': server_port,
            'engine.autoreload.on': False
        })
        cherrypy.tree.mount(Root(), "/")
        cherrypy.engine.start()
        cherrypy.engine.block()
