#!/opt/dosis2/bin/python3-venv
__author__ = 'taylor'

import logging.config
import os
import platform
import subprocess
import sys

import bs4
import distro
import psycopg2

logging.config.dictConfig({
    'disable_existing_loggers': False,
    'version': 1,
    'formatters': {
        'verbose': {
            'format': '%(levelname)s {ident}.%(name)s [%(filename)s:%(lineno)s] %(message)s'.format(
                ident='dosis-system-query'
            )
        },
        'simple': {
            'format': '%(levelname)s [%(filename)s:%(lineno)s] %(message)s'
        }
    },
    'handlers': {
        'info': {
            'level': 'INFO',
            'class': 'logging.handlers.SysLogHandler',
            'address': '/dev/log',
            'formatter': 'verbose'
        },
        'debug': {
            'level': 'DEBUG',
            'class': 'logging.StreamHandler',
            'stream': sys.stdout,
            'formatter': 'simple'
        }
    },
    'loggers': {
        'logger': {
            'handlers': ['info', 'debug'],
            'level': 'DEBUG'
        }
    }
})


logger = logging.getLogger('logger')


def coalesce(*values):
    """
    Coalesce a list of values.

    :param values: values to coalesce.
    :return: The first value that was not None or None if all values are None.
    """
    for v in values:
        if v is not None:
            return v
    else:
        return None


class System:
    def __init__(self):
        self._system_type = None
        self._system_version = None
        self._system_package_versions = {}

    def set_system_type(self, system_type):
        self._system_type = system_type

    def get_system_type(self):
        if self._system_type is None:
            raise ValueError('system type not set')
        else:
            return self._system_type

    def set_system_version(self, system_version):
        self._system_version = system_version

    def get_system_version(self):
        if self._system_version is None:
            raise ValueError('system version not set')
        else:
            return self._system_version

    def set_system_package_version(self, package_name, package_version):
        self._system_package_versions[package_name] = package_version

    def get_system_package_version(self, package_name):
        return self._system_package_versions[package_name]


# The actual system query class
class DosisSystemQuery:
    def __init__(self, baseconfig, customconfig):
        # get the database credentials from our files in /opt/dosis2/etc
        dbcreds = self.__read_dbcreads(baseconfig, customconfig)

        # connect to the database
        connection = self.__init_db_connection(dbcreds)
        self.__populate_database(self.__system_info(
            dbcreds,
            {
                'dosis_os': ['dosis_os', 'dosis_config', 'dosis-config'],
                'dosis-base': ['dosis-base'],
                'mcontrol': ['mcontrol'],
                'l60': ['L60', 'l60'],
                'ctraq': ['dosis_ctraq']
            }
        ), connection, dbcreds)
        connection.close()

    @staticmethod
    def __read_dbcreads(base, custom):
        """
        Read database credentials from config files.

        :param base: base config file to read database credentials from.
        :param custom: custom config file to read database credentials from.
        :return: dictionary of database credentials.
        """
        if os.path.isfile(base):
            with open(base, 'r') as f:
                basesoup = bs4.BeautifulSoup(f.read(), 'lxml')
        else:
            basesoup = bs4.BeautifulSoup('<config></config>', 'lxml')

        if os.path.isfile(custom):
            with open(custom, 'r') as f:
                customsoup = bs4.BeautifulSoup(f.read(), 'lxml')
        else:
            customsoup = bs4.BeautifulSoup('<config></config>', 'lxml')

        keypairs = {
            'HOST': 'DBServer',
            'DATABASE': 'DBName',
            'USER': 'DBUsername',
            'PASS': 'DBPassword',
            'PORT': 'DBPort',
            'DAFE_TAG': 'DafeTag',
        }

        dbcreds = {}
        for key, propname in list(keypairs.items()):
            value = coalesce(
                customsoup.select_one('config > section[name="general"] > property[name="%s"] > value' % (propname,)),
                basesoup.select_one('config > section[name="general"] > property[name="%s"] > value' % (propname,))
            )

            if value is None:
                raise KeyError('cannot find value for property %s -> %s' % (propname, key))
            else:
                dbcreds[key] = value.text

        return dbcreds

    @staticmethod
    def __init_db_connection(creds):
        """
        Initialize a connection to the database.

        :param creds: credentials used to connect to the database.
        :return: Database connection.
        """
        try:
            connection = psycopg2.connect(
                host=creds['HOST'],
                database=creds['DATABASE'],
                user=creds['USER'],
                port=creds['PORT'],
                password=creds['PASS']
            )
            return connection
        except psycopg2.Error as error:
            logger.exception(error)
            raise

    @staticmethod
    def __system_info(creds, keynames):
        """
        Obtain information about the system, this includes the current package
        versions, system type, and os version.

        :param creds: Credentials read from  __read_dbcreds
        :param keynames: Dictionary mapping keyname in database to list of possible rpm/package name on system.
        :return: System() object with populated information.
        """
        system = System()

        try:
            system.set_system_type(creds['DAFE_TAG'])
        except Exception as e:
            logger.exception(e)
            raise

        system.set_system_version(platform.release())

        for keyname, package_names in list(keynames.items()):
            linux_distribution = str(distro.linux_distribution()[0])
            found_package_name = None

            if linux_distribution == 'Ubuntu':
                for package_name in package_names:
                    try:
                        version = [l.strip() for l in subprocess.check_output([
                            'dpkg-query', '--showformat=${Version}\n', '--show', package_name
                        ]).decode('utf-8').split('\n') if l][-1]
                    except subprocess.CalledProcessError:
                        system.set_system_package_version(keyname, 'Not Installed')
                    except Exception as e:
                        logger.exception(e)
                        system.set_system_package_version(keyname, 'Unknown')
                    else:
                        system.set_system_package_version(keyname, version)
                        found_package_name = package_name
                        break
            elif linux_distribution == 'CentOS':
                for package_name in package_names:
                    try:
                        version = [l.strip() for l in subprocess.check_output([
                            'rpm', '-q', package_name, '--queryformat=%{VERSION}-%{RELEASE}\n'
                        ]).split('\n') if l][-1]
                    except subprocess.CalledProcessError:
                        system.set_system_package_version(keyname, 'Not Installed')
                    except Exception as e:
                        logger.exception(e)
                        system.set_system_package_version(keyname, 'Unknown')
                    else:
                        system.set_system_package_version(keyname, version)
                        found_package_name = package_name
                        break
            else:
                raise ValueError('Unknown Linux distribution: %s' % (linux_distribution,))

            logger.info('Found package[%s]: %s' % (found_package_name, system.get_system_package_version(keyname)))

        return system

    # populate the variables in the database, this will be done from
    # a function assumed to be contained within the database
    @staticmethod
    def __populate_database(system, connection, dbcreds):
        """
        Populate the variables in the database, this will be done from
        a function assumed to be contained within the database.

        :param system: system variables to put in the database.
        :param connection: database connection.
        :param dbcreds: database credentials.
        """
        try:
            cursor = connection.cursor()
            query = 'SELECT equipment.update_os_versions(%s);' % (', '.join([
                '%(dafe_tag)s',
                '%(os_ver)s',
                '%(pkg_os_ver)s',
                '%(pkg_dosis_base_ver)s',
                '%(pkg_mcontrol_ver)s',
                '%(pkg_l60_ver)s',
                '%(pkg_ctraq_ver)s'
            ]))
            cursor.execute(query, {
                'dafe_tag': system.get_system_type(),
                'os_ver': system.get_system_version(),
                'pkg_os_ver': system.get_system_package_version('dosis_os'),
                'pkg_dosis_base_ver': system.get_system_package_version('dosis-base'),
                'pkg_mcontrol_ver': system.get_system_package_version('mcontrol'),
                'pkg_l60_ver': system.get_system_package_version('l60'),
                'pkg_ctraq_ver': system.get_system_package_version('ctraq')
            })
            if dbcreds.get('HOST') == '127.0.0.1':
                cursor.execute(query, {
                    'dafe_tag': 'SYSTEM',
                    'os_ver': system.get_system_version(),
                    'pkg_os_ver': system.get_system_package_version('dosis_os'),
                    'pkg_dosis_base_ver': system.get_system_package_version('dosis-base'),
                    'pkg_mcontrol_ver': system.get_system_package_version('mcontrol'),
                    'pkg_l60_ver': system.get_system_package_version('l60'),
                    'pkg_ctraq_ver': system.get_system_package_version('ctraq')
                })
        except Exception as e:
            logger.exception(e)
        else:
            connection.commit()


if __name__ == '__main__':
    DosisSystemQuery('/opt/dosis2/etc/dosisConfig.xml', '/opt/dosis2/etc/custom/dosisConfig.xml')
