# (C) Copyright NuoDB, Inc. 2017-2020
#
# This source code is licensed under the MIT license found in the LICENSE
# file in the root directory of this source tree.

import logging
import os
import re
import socket
import threading
import time
import datetime

from copy import deepcopy
from pynuoca.nuoca_plugin import NuocaMPInputPlugin
from pynuoca.nuoca_util import nuoca_log

try:
    from pynuoadmin import nuodb_mgmt
except ImportError:
    import nuodb_mgmt

# NuoAdminNuoMonitor plugin
#
# NOTE: This is an Alpha version of a NuoCA plugin that can collect NuoDB
# Engine metrics (NuoMon) from the NuoDB new Admin layer (which is a Beta
# feature for NuoDB release 3.2). This NuoCA plugin has minimal testing and
# is expected to change (or be incorporated into the NuoCA NuoMon plugin)
#
# Example NuoAdminNuoMonitor plugin configuration:
#
# - NuoAdminNuoMonitor:
#    description : Collection from internal nuoAdminNuoMonitor tool
#    database_regex_pattern: dbt2
#    api_server: localhost:8888
#    client_key: /opt/nuodb/tls-config/keys/nuocmd.pem
#    server_cert: None
#    server_id: nuoadmin0

class BaseCollector(object):

    def message_received(self, root):
        pass

    def invalid_message(self, message):
        pass

    def closed(self):
        pass


class BaseMetricsCollector(BaseCollector):
    def __init__(self):
        super(BaseMetricsCollector, self).__init__()
        self.__first = True
        self.__process = None

    @property
    def process(self):
        return self.__process

    @process.setter
    def process(self, p):
        self.__process = p

    def __get_item(self, attrs):
        units = ["COUNT", "MILLISECONDS", "STATE",
                 "NUMBER", "PERCENT", "IDENTIFIER",
                 "DELTA"]
        return {"unit": units[int(attrs['units']) - 1],
                "description": attrs['header']}

    # @trace
    def message_received(self, root):
        def parseStr(x):
            try:
                return int(x)
            except:
                return x

        items = {}
        if root.tag == "Items":
            for child in root:
                items[child.attrib['name']] = self.__get_item(child.attrib)
            items['Database'] = dict(unit="IDENTIFIER", description="Database Name")
            items['Region'] = dict(unit="IDENTIFIER", description="Region Name")
            self.onStart(items)
            return None
        elif root.tag == 'Status':
            new_values = dict([(k, parseStr(v)) for k, v in root.attrib.iteritems()])
            if self.__first:
                new_values['Database'] = self.process.db_name
                new_values['Region'] = self.process.region_name
                self.__first = False
            new_values['TimeStamp'] = int(time.time() * 1000.0)
            self.onChange(new_values)
            return None

    def closed(self):
        self.onEnd()
        pass


class MetricsCollector(BaseMetricsCollector):
    """ Base class for metrics collection.
    Remembers previous values"""

    def __init__(self):
        super(MetricsCollector, self).__init__()
        self.__metrics = {}
        self.__values = {}
        pass

    @property
    def metrics(self):
        return self.__metrics

    @property
    def values(self):
        return self.__values

    def init(self, args):
        return self

    def onStart(self, metrics):
        """ remembers metrics  """
        self.__metrics = metrics

    def onChange(self, metrics):
        """ remembers previous values """
        self.__values.update(metrics)

    def onEnd(self):
        """ zero all values """
        zeroMetrics = {}
        for k, v in self.__values.iteritems():
            if v != 0 and type(v) is int:
                zeroMetrics[k] = 0
        zeroMetrics['TimeStamp'] = int(time.time() * 1000.0)
        self.onChange(zeroMetrics)
        pass


class NuoAdminNuoMonMessageConsumer(object):
    """ NuoAdminNuoMonMessageConsumer"""

    _conn = None
    _nuo_monitor_obj = None

    def __init__(self, conn, nuo_monitor_obj):
        self._conn = conn
        self._nuo_monitor_obj = nuo_monitor_obj
        self._process_metrics_dict = {}

    def get_stats(self, db_name=None, start_id=None, server_id=None):
        while self._nuo_monitor_obj._enabled:
            try:
                for process_msg in self._get_messages(None, db_name,
                                                      start_id, server_id):
                    if process_msg and 'msg' in process_msg:
                        self._nuo_monitor_obj.nuoAdminNuoMonitor_collect_queue.append(
                            deepcopy(process_msg['msg']))
            except Exception as e:
                nuoca_log(logging.ERROR, "Exception in NuoAdminNuoMonMessage"
                                         "ConsumerNuoAdminNuoMon.get_stats: %s"
                          % str(e))
            time.sleep(10)

    def _get_messages(self, log_options, db_name, start_id, server_id,
                      include_process=False):

        # message generator is for a single process if `start_id` is specified;
        # otherwise, it is aggregated
        if start_id is not None:
            msg_stream = self._conn.monitor_process(start_id, log_options)
        else:
            msg_stream = self._conn.monitor_database(db_name, log_options,
                                                     keep_open=True)
        # filter messages by name based on whether we are streaming stats or
        # log messages
        message_name = 'Status' if log_options is None else 'LogMessage'
        for process, xml_message in msg_stream:
            if server_id:
                if process.server_id != server_id:
                    continue
            if process.start_id not in self._process_metrics_dict:
                mc = MetricsCollector()
                self._process_metrics_dict[process.start_id] = mc
                mc.process = process
            else:
                mc = self._process_metrics_dict[process.start_id]
            mc.message_received(xml_message)
            items = mc.values
            json_msg = items
            if len(json_msg) != 0:
                # add timestamp to message attributes; TODO: in the future, we
                # may want to include timestamp on the sending side
                if 'Time' not in json_msg:
                    json_msg['Time'] = self._get_timestamp()
                # combine process and message; if `include_process`, return all
                # process attributes; otherwise just return start ID

                if include_process:
                    process._dict['msg'] = json_msg
                    yield process
                else:
                    yield dict(startId=process.start_id, msg=json_msg)

    @staticmethod
    def _get_timestamp(time_sec=None):
        if time_sec is None:
            time_sec = time.time()
        dt = datetime.datetime.fromtimestamp(time_sec)
        return dt.strftime('%Y-%m-%dT%H:%M:%S.%f')


class NuoAdminNuoMonitorPlugin(NuocaMPInputPlugin):
    DEFAULT_API_SERVER = 'localhost:8888'

    def __init__(self, parent_pipe):
        super(NuoAdminNuoMonitorPlugin, self).__init__(parent_pipe, 'NuoAdminNuoMon')
        self._config = None
        self._nuocaCollectionName = None
        self._api_server = NuoAdminNuoMonitorPlugin.DEFAULT_API_SERVER
        self._server_id = None
        self._client_key = None
        self._server_cert = None
        self._enabled = False
        self._numon_handler_ready = False
        self._domain = None
        self._domain_metrics = None
        self._domain_metrics_host = None
        self._database_regex_pattern = '.*'
        self._host_uuid_shortname = False
        self._thread = None
        self._nuoAdminNuoMonitor_collect_queue = []
        self._collection_interval = 30

    def _get_admin_conn(self):
        client_key = self._client_key
        if client_key is not None and ',' in client_key:
            client_key = client_key.split(',')
            if len(client_key) != 2:
                raise ValueError(
                    'Expected at most two tokens for --client-key')
        server_cert = self._server_cert
        api_server = self._api_server
        if (not api_server.startswith('http://') and
                not api_server.startswith('https://')):  # noqa
            if client_key is None and server_cert is None:
                api_server = 'http://' + api_server
            else:
                api_server = 'https://' + api_server
        verify = server_cert
        if not verify:
            verify = False

        return nuodb_mgmt.AdminConnection(api_server, client_key, verify)

    def wait_for_terminate(self):
        if self._domain:
            self._domain.waitForTerminate()

    @property
    def nuoAdminNuoMonitor_collect_queue(self):
        return self._nuoAdminNuoMonitor_collect_queue

    def _NuoAdminNuoMon_handler_thread(self):
        self._numon_handler_ready = True
        self._domain_metrics.get_stats(server_id=self._server_id)

    def startup_NuoAdminNuoMon(self):
        try:
            self._numon_handler_ready = False
            self._conn = self._get_admin_conn()
            self._enabled = True
            self._domain_metrics = NuoAdminNuoMonMessageConsumer(self._conn, self)
            self._thread = threading.Thread(target=self._NuoAdminNuoMon_handler_thread)
            self._thread.daemon = True
            self._thread.start()
            try_count = 0
            while not self._numon_handler_ready and try_count < 5:
                try_count += 1
                time.sleep(1)
            return self._numon_handler_ready
        except Exception as e:
            nuoca_log(logging.ERROR, "NuoAdminNuoMon Startup error: %s" % str(e))
            return False

    def startup(self, config=None):
        try:
            self._config = config

            # Validate the configuration.
            required_config_items = ['api_server']
            if not self.has_required_config_items(config, required_config_items):
                return False

            # Don't reveal the domain password in the NuoCA log file.
            display_config = {}
            display_config.update(config)
            display_config['domain_password'] = ''
            nuoca_log(logging.INFO, "NuoAdminNuoMon: plugin config: %s" %
                      str(display_config))

            self._api_server = os.path.expandvars(config['api_server'])
            if 'server_id' in config:
                self._server_id = os.path.expandvars(config['server_id'])
            if 'client_key' in config:
                self._client_key = os.path.expandvars(config['client_key'])
            if 'server_cert' in config:
                self._server_cert = os.path.expandvars(config['server_cert'])
            if 'nuocaCollectionName' in config:
                self._nuocaCollectionName = config['nuocaCollectionName']
            if 'domain_metrics_host' in config:
                self._domain_metrics_host = os.path.expandvars(config['domain_metrics_host'])
                if self._domain_metrics_host == 'localhost':
                    self._domain_metrics_host = socket.gethostname()
            if 'database_regex_pattern' in config:
                self._database_regex_pattern = config['database_regex_pattern']
            if 'host_uuid_shortname' in config:
                self._host_uuid_shortname = config['host_uuid_shortname']
            self.startup_NuoAdminNuoMon()
            return self._numon_handler_ready
        except Exception as e:
            nuoca_log(logging.ERROR, "NuoAdminNuoMon Plugin Startup error: %s" % str(e))
            return False

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

    def collect(self, collection_interval):
        uuid_hostname_regex = \
            '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}-'
        rval = None
        self._collection_interval = collection_interval
        try:
            nuoca_log(logging.DEBUG, "NuoAdminNuoMon: collect()")
            base_values = super(NuoAdminNuoMonitorPlugin, self).collect(collection_interval)
            collection_count = len(self._nuoAdminNuoMonitor_collect_queue)
            nuoca_log(logging.DEBUG, "NuoAdminNuoMon: collection_count %s" %
                      str(collection_count))
            if not collection_count:
                return rval

            rval = []
            for i in range(collection_count):
                collected_dict = self._nuoAdminNuoMonitor_collect_queue.pop(0)
                if self._domain_metrics_host and 'Hostname' in collected_dict:
                    if collected_dict['Hostname'] != self._domain_metrics_host:
                        continue
                if self._nuocaCollectionName:
                    collected_dict['nuocaCollectionName'] = self._nuocaCollectionName
                if 'Database' in collected_dict:
                    m = re.search(self._database_regex_pattern, collected_dict['Database'])
                    if m:
                        if self._host_uuid_shortname:
                            m2 = re.search(uuid_hostname_regex, collected_dict['Hostname'])
                            if m2:
                                shortid = collected_dict['Hostname'][37:]
                                if 'NodeType' in collected_dict:
                                    if collected_dict['NodeType'] == 'Transaction':
                                        shortid += "(TE)"
                                    elif collected_dict['NodeType'] == 'Storage':
                                        shortid += "(SM)"
                                shortid_with_pid = shortid + str(collected_dict['ProcessId'])
                                collected_dict['HostShortID'] = shortid
                                collected_dict['HostShortIDwithPID'] = shortid_with_pid
                        rval.append(collected_dict)
                else:
                    rval.append(collected_dict)

        except Exception as e:
            nuoca_log(logging.ERROR, str(e))
        return rval
