"""
MongoDB Support

"""
import datetime
from pymongo import errors
import logging
import pymongo

from newrelic_plugins_agent.plugins import base

LOGGER = logging.getLogger(__name__)


class MongoDB(base.Plugin):

    GUID = 'com.meetme.newrelic_mongodb_plugin_agent'

    def add_datapoints(self, name, stats):
        """Add all of the data points for a database

        :param str name: The name of the database for the stats
        :param dict stats: The stats data to add

        """
        base_key = 'Database/%s' % name
        self.add_gauge_value('%s/Extents' % base_key, 'extents',
                             stats.get('extents', 0))
        self.add_gauge_value('%s/Size' % base_key, 'bytes',
                             stats.get('dataSize', 0) / 1048576)
        self.add_gauge_value('%s/File Size' % base_key, 'bytes',
                             stats.get('fileSize', 0) / 1048576)
        self.add_gauge_value('%s/Objects' % base_key, 'objects',
                             stats.get('objects', 0))
        self.add_gauge_value('%s/Collections' % base_key, 'collections',
                             stats.get('collections', 0))
        self.add_gauge_value('%s/Index/Count' % base_key, 'indexes',
                             stats.get('indexes', 0))
        self.add_gauge_value('%s/Index/Size' % base_key, 'bytes',
                             stats.get('indexSize', 0))

    def add_server_repl_datapoints(self, stats):
        """Add all of the data points about replication status

        :param dict stats: The stats data to add

        """
        self.add_gauge_value('Repl/state', 'state', stats.get('myState', 0));

    def add_server_datapoints(self, stats):
        """Add all of the data points for a server

        :param dict stats: The stats data to add

        """
        asserts = stats.get('asserts', dict())
        self.add_derive_value('Asserts/Regular', 'asserts',
                              asserts.get('regular', 0))
        self.add_derive_value('Asserts/Warning', 'asserts',
                              asserts.get('warning', 0))
        self.add_derive_value('Asserts/Message', 'asserts',
                              asserts.get('msg', 0))
        self.add_derive_value('Asserts/User', 'asserts',
                              asserts.get('user', 0))
        self.add_derive_value('Asserts/Rollovers', 'asserts',
                              asserts.get('rollovers', 0))

        flush = stats.get('backgroundFlushing', dict())
        self.add_derive_timing_value('Background Flushes',
                                     'ms',
                                     flush.get('flushes', 0),
                                     flush.get('total_ms', 0),
                                     flush.get('last_ms', 0))
        self.add_gauge_value('Seconds since last flush',
                             'seconds',
                             (datetime.datetime.now() -
                              flush.get('last_finished',
                                        datetime.datetime.now())).seconds)

        conn = stats.get('connections', dict())
        self.add_gauge_value('Connections/Available', 'connections',
                             conn.get('available', 0))
        self.add_gauge_value('Connections/Current', 'connections',
                             conn.get('current', 0))

        cursors = stats.get('cursors', dict())
        self.add_gauge_value('Cursors/Open', 'cursors',
                             cursors.get('totalOpen', 0))
        self.add_derive_value('Cursors/Timed Out', 'cursors',
                              cursors.get('timedOut', 0))

        dur = stats.get('dur', dict())
        self.add_gauge_value('Durability/Commits in Write Lock', 'commits',
                             dur.get('commitsInWriteLock', 0))
        self.add_gauge_value('Durability/Early Commits', 'commits',
                             dur.get('earlyCommits', 0))
        self.add_gauge_value('Durability/Journal Commits', 'commits',
                             dur.get('commits', 0))
        self.add_gauge_value('Durability/Journal Bytes Written', 'bytes',
                             dur.get('journaledMB', 0) / 1048576)
        self.add_gauge_value('Durability/Data File Bytes Written', 'bytes',
                             dur.get('writeToDataFilesMB', 0) / 1048576)

        timems = dur.get('timeMs', dict())
        self.add_gauge_value('Durability/Timings/Duration Measured', 'ms',
                             timems.get('dt', 0))
        self.add_gauge_value('Durability/Timings/Log Buffer Preparation', 'ms',
                             timems.get('prepLogBuffer', 0))
        self.add_gauge_value('Durability/Timings/Write to Journal', 'ms',
                             timems.get('writeToJournal', 0))
        self.add_gauge_value('Durability/Timings/Write to Data Files', 'ms',
                             timems.get('writeToDataFiles', 0))
        self.add_gauge_value('Durability/Timings/Remaping Private View', 'ms',
                             timems.get('remapPrivateView', 0))

        locks = stats.get('globalLock', dict())
        self.add_derive_value('Global Locks/Held', 'ms',
                              locks.get('lockTime', 0) / 1000)
        self.add_derive_value('Global Locks/Ratio', 'ratio',
                              locks.get('ratio', 0))

        active = locks.get('activeClients', dict())
        self.add_derive_value('Global Locks/Active Clients/Total', 'clients',
                              active.get('total', 0))
        self.add_derive_value('Global Locks/Active Clients/Readers', 'clients',
                              active.get('readers', 0))
        self.add_derive_value('Global Locks/Active Clients/Writers', 'clients',
                              active.get('writers', 0))

        queue = locks.get('currentQueue', dict())
        self.add_derive_value('Global Locks/Queue/Total', 'locks',
                              queue.get('total', 0))
        self.add_derive_value('Global Locks/Queue/Readers', 'readers',
                              queue.get('readers', 0))
        self.add_derive_value('Global Locks/Queue/Writers', 'writers',
                              queue.get('writers', 0))

        index = stats.get('indexCounters', dict())
        btree_index = index.get('btree', dict())
        self.add_derive_value('Index/Accesses', 'accesses',
                              index.get('accesses', 0) +
                              btree_index.get('accesses', 0))
        self.add_derive_value('Index/Hits', 'hits',
                              index.get('hits', 0) +
                              btree_index.get('hits', 0))
        self.add_derive_value('Index/Misses', 'misses',
                              index.get('misses', 0) +
                              btree_index.get('misses', 0))
        self.add_derive_value('Index/Resets', 'resets',
                              index.get('resets', 0) +
                              btree_index.get('resets', 0))

        mem = stats.get('mem', dict())
        self.add_gauge_value('Memory/Mapped', 'bytes',
                             mem.get('mapped', 0) / 1048576)
        self.add_gauge_value('Memory/Mapped with Journal', 'bytes',
                             mem.get('mappedWithJournal', 0) / 1048576)
        self.add_gauge_value('Memory/Resident', 'bytes',
                             mem.get('resident', 0) / 1048576)
        self.add_gauge_value('Memory/Virtual', 'bytes',
                             mem.get('virtual', 0) / 1048576)

        net = stats.get('network', dict())
        self.add_derive_value('Network/Requests', 'requests',
                              net.get('numRequests', 0))
        self.add_derive_value('Network/Transfer/In', 'bytes',
                              net.get('bytesIn', 0))
        self.add_derive_value('Network/Transfer/Out', 'bytes',
                              net.get('bytesOut', 0))

        ops = stats.get('opcounters', dict())
        self.add_derive_value('Operations/Insert', 'ops', ops.get('insert', 0))
        self.add_derive_value('Operations/Query', 'ops', ops.get('query', 0))
        self.add_derive_value('Operations/Update', 'ops', ops.get('update', 0))
        self.add_derive_value('Operations/Delete', 'ops', ops.get('delete', 0))
        self.add_derive_value('Operations/Get More', 'ops',
                              ops.get('getmore', 0))
        self.add_derive_value('Operations/Command', 'ops',
                              ops.get('command', 0))

        extra = stats.get('extra_info', dict())
        self.add_gauge_value('System/Heap Usage', 'bytes',
                             extra.get('heap_usage_bytes', 0))
        self.add_derive_value('System/Page Faults', 'faults',
                              extra.get('page_faults', 0))

    def connect(self):
        kwargs = {'host': self.config.get('host', 'localhost'),
                  'port': self.config.get('port', 27017)}
        for key in ['ssl', 'ssl_keyfile', 'ssl_certfile',
                    'ssl_cert_reqs', 'ssl_ca_certs']:
            if key in self.config:
                kwargs[key] = self.config[key]
        try:
            return pymongo.MongoClient(**kwargs)
        except pymongo.errors.ConnectionFailure as error:
            LOGGER.error('Could not connect to MongoDB: %s', error)

    def get_and_add_db_stats(self):
        """Fetch the data from the MongoDB server and add the datapoints

        """
        databases = self.config.get('databases', list())
        if isinstance(databases, list):
            self.get_and_add_db_list(databases)
        else:
            self.get_and_add_db_dict(databases)

    def get_and_add_db_list(self, databases):
        """Handle the list of databases while supporting authentication for
        the admin if needed

        :param list databases: The database list

        """
        LOGGER.debug('Processing list of mongo databases')
        client = self.connect()
        if not client:
            return
        for database in databases:
            LOGGER.debug('Collecting stats for %s', database)
            db = client[database]
            try:
                self.add_datapoints(database, db.command('dbStats'))
            except errors.OperationFailure as error:
                LOGGER.critical('Could not fetch stats: %s', error)

    def get_and_add_db_dict(self, databases):
        """Handle the nested database structure with username and password.

        :param dict databases: The databases data structure

        """
        LOGGER.debug('Processing dict of mongo databases')
        client = self.connect()
        if not client:
            return
        db_names = databases.keys()
        for database in db_names:
            db = client[database]
            try:
                if 'username' in databases[database]:
                    db.authenticate(databases[database]['username'],
                                    databases[database].get('password'))
                self.add_datapoints(database, db.command('dbStats'))
                if 'username' in databases[database]:
                    db.logout()
            except errors.OperationFailure as error:
                LOGGER.critical('Could not fetch stats: %s', error)

    def get_and_add_server_stats(self):
        LOGGER.debug('Fetching server stats')
        client = self.connect()
        if not client:
            return
        if self.config.get('admin_username'):
            client.admin.authenticate(self.config['admin_username'],
                                   self.config.get('admin_password'))
        self.add_server_datapoints(client.db.command('serverStatus'))
        client.close()

    def get_and_add_replication_stats(self):
        LOGGER.debug('Fetching Replication stats')
        client = self.connect()
        if not client:
            return
        if self.config.get('admin_username'):
            client.admin.authenticate(self.config['admin_username'],
                                   self.config.get('admin_password'))
        self.add_server_repl_datapoints(client.db.command('replSetGetStatus'))
        client.close()

    def poll(self):
        self.initialize()
        self.get_and_add_server_stats()
        self.get_and_add_db_stats()
        self.get_and_add_replication_stats()
        self.finish()
