#!/usr/bin/python3 -s
#
#    Copyright (c) 2009-2019 Gary Roderick, Tom Keffer <tkeffer@gmail.com> and
#                            Matthew Wall
#
#    See the file LICENSE.txt for your rights.
#
""" Generate weewx debug info """

from __future__ import absolute_import
from __future__ import print_function

import copy
import optparse
import os
import platform
import sys

import six
from configobj import ConfigObj

# weewx imports
import weecfg
import weedb
import weeutil.logger
import weewx.manager
import weewx.units
from weecfg.extension import ExtensionEngine
from weeutil.weeutil import TimeSpan, timestamp_to_string
from weewx.manager import DaySummaryManager

VERSION = weewx.__version__
WEE_DEBUG_VERSION = '0.8'

# keys/setting names to obfuscate in weewx.conf, key value will be obfuscated
# if the key starts any element in the list. Can add additional string elements
# to list if required
OBFUSCATE_MAP = {
    "obfuscate": [
        "apiKey", "api_key", "app_key", "archive_security_key", "id", "key",
        "oauth_token", "password", "raw_security_key", "token", "user",
        "server_url", "station"],
    "do_not_obfuscate": [
        "station_type"]
    }

# weewx archive field to use as the basis of counting number of archive records
# Can be any field but dateTime is preferred as it should be in every weewx
# archive
COUNT_FIELD = 'dateTime'

# Redirect the import of setup (needed to get extension info)
sys.modules['setup'] = weecfg.extension

usage = """wee_debug --help
       wee_debug --info
            [CONFIG_FILE|--config=CONFIG_FILE]
            [--output|--output DEBUG_PATH]
            [--verbosity=0|1|2]
       wee_debug --version

Description:

Generate a standard suite of system/weewx information to aid in remote
debugging. The wee_debug output consists of two parts, the first part containing
a snapshot of relevant system/weewx information and the second part a parsed and
obfuscated copy of weewx.conf. This output can be redirected to file and posted
when seeking assistance via forums or email.

Actions:

--info           Generate a debug report."""

epilog = """wee_debug will attempt to obfuscate obvious personal/private
information in weewx.conf such as user names, passwords and API keys; however,
the user should thoroughly check the generated output for personal/private
information before posting the information publicly."""

def main():

    # Set defaults for logging:
    weeutil.logger.setup('wee_debug', {})

    # Create a command line parser:
    parser = optparse.OptionParser(usage = usage,
                                   epilog = epilog)

    # Add the various options:
    parser.add_option("--config", dest="config_path", type=str,
                      metavar="CONFIG_FILE",
                      help="Use configuration file CONFIG_FILE.")
    parser.add_option("--info", dest="info", action='store_true',
                      help="Generate weewx debug output.")
    parser.add_option('--output', action='callback',
                      callback=optional_arg('/var/tmp/weewx.debug'),
                      dest='debug_file', metavar="DEBUG_PATH",
                      help="Write wee_debug output to DEBUG_PATH. DEBUG_PATH "
                      "includes path and file name. Default is "
                      "/var/tmp/weewx.debug.")
    parser.add_option('--verbosity', type=int, default=1,
                      metavar="N", help="How much detail to display, "
                      "0-2, default=1.")
    parser.add_option('--version', dest='version', action='store_true',
                      help='Display wee_debug version number.')

    # Now we are ready to parse the command line:
    (options, args) = parser.parse_args()  # @UnusedVariable

    # check weewx version number for compatibility
    if int(VERSION[0]) < 3:
        # incompatible version of weewx
        print("Incompatible version of weewx detected (%s). "
              "Weewx v3.0.0 or greater required." % VERSION)
        print("Nothing done, exiting.")
        exit(1)

    # get config_dict to use
    config_path, config_dict = weecfg.read_config(options.config_path, args)

    # display wee_debug version info
    if options.version:
        print("wee_debug version: %s" % WEE_DEBUG_VERSION)
        exit(1)

    # display debug info
    if options.info:
        # first a message re verbosity
        if options.verbosity == 0:
            print("Using verbosity=0, displaying minimal info")
        elif options.verbosity == 1:
            print("Using verbosity=1, displaying most info")
        else:
            print("Using verbosity=2, displaying all info")
        print()
        # then a message re output destination
        if options.debug_file is not None:
            print("wee_debug output will be written to %s" % options.debug_file)
        else:
            print("wee_debug output will be sent to stdout(console)")
        print()

        # get some key weewx parameters
        db_binding_wx = get_binding(config_dict)
        database_wx = config_dict['DataBindings'][db_binding_wx]['database']

        # generate our debug info sending it to file or console
        if options.debug_file is not None:
            # save stdout for when we clean up
            old_stdout = sys.stdout
            # open our debug file for writing
            _debug_file = open(options.debug_file, 'w')
            # redirect stdout to our file
            sys.stdout = _debug_file
        print("Using configuration file %s" % config_path)
        print("Using database binding '%s', which is bound to database '%s'" % (db_binding_wx,
                                                                                database_wx))
        print()
        # generate our debug info
        generateDebugInfo(config_dict,
                          config_path,
                          db_binding_wx,
                          options.verbosity)
        print()
        print("Parsed and obfuscated weewx.conf")
        # generate our obfuscated weewx.conf
        generateDebugConf(config_dict)
        if options.debug_file is not None:
            # close our file
            _debug_file.close()
            # revert stdout
            sys.stdout = old_stdout
            print("wee_debug output successfully written to %s" % options.debug_file)
        else:
            print()
            print("wee_debug report successfully generated")
        exit(1)

    # if we have a compatible weewx version but did not use --version or --info
    # then show the wee_debug help
    parser.print_help()

def optional_arg(arg_default):
    """ Callback function to implement optparse command line parameters that
        support default values and optional parameters.

        http://stackoverflow.com/questions/1229146/parsing-empty-options-in-python
    """

    def func(option, opt_str, value, parser):  # @UnusedVariable
        if parser.rargs and not parser.rargs[0].startswith('-'):
            val = parser.rargs[0]
            parser.rargs.pop(0)
        else:
            val = arg_default
        setattr(parser.values, option.dest, val)

    return func

def generateDebugInfo(config_dict, config_path, db_binding_wx, verbosity):
    """ Generate system/weewx debug info """

    # system/OS info
    generateSysInfo(verbosity)

    # weewx version info
    print("General Weewx info")
    print("  Weewx version %s detected." % VERSION)
    print()

    # station info
    print("Station info")
    stationType = config_dict['Station']['station_type']
    print("  Station type: %s" % stationType)
    print("  Driver:       %s" % config_dict[stationType]['driver'])
    print()

    # driver info
    if verbosity > 0:
        print("Driver info")
        driver_dict = {stationType: config_dict[stationType]}
        _config = ConfigObj(driver_dict)
        if six.PY2:
            _config.write(sys.stdout)
        else:
            # The ConfigObj.write() method always writes in binary,
            # which is not accepted by Python 3 for output to stdout
            # So write to sys.stdout.buffer
            _config.write(sys.stdout.buffer)
        print()

    # installed extensions info
    print("Currently installed extensions")
    ext = ExtensionEngine(config_path=config_path,
                          config_dict=config_dict)
    ext.enumerate_extensions()
    print()

    # weewx archive info
    try:
        manager_info_dict = getManagerInfo(config_dict, db_binding_wx)
    except weedb.CannotConnect as e:
        print("Unable to connect to database:", e)
        print()
    except weedb.OperationalError as e:
        print("Error hitting database. It may not be properly initialized:")
        print(e)
        print()
    else:
        units_nickname = weewx.units.unit_nicknames.get(manager_info_dict['units'], "Unknown unit constant")
        print("Archive info")
        print("  Database name:        %s" % manager_info_dict['db_name'])
        print("  Table name:           %s" % manager_info_dict['table_name'])
        print("  Version               %s" % manager_info_dict['version'])
        print("  Unit system:          %s (%s)" % (manager_info_dict['units'],
                                                  units_nickname))
        print("  First good timestamp: %s" % timestamp_to_string(manager_info_dict['first_ts']))
        print("  Last good timestamp:  %s" % timestamp_to_string(manager_info_dict['last_ts']))
        if manager_info_dict['ts_count']:
            print("  Number of records:    %s" % manager_info_dict['ts_count'].value)
        else:
            print("  Number of records:    %s (no archive records found)" % \
                manager_info_dict['ts_count'])
        # if we have a database and a table but no start or stop ts and no records
        # inform the user that the database/table exists but appears empty
        if (manager_info_dict['db_name'] and manager_info_dict['table_name']) and \
        not (manager_info_dict['ts_count'] or manager_info_dict['units'] or \
        manager_info_dict['first_ts'] or manager_info_dict['last_ts']):
            print("                        It is likely that the database (%s) archive table (%s)" % \
                (manager_info_dict['db_name'], manager_info_dict['table_name']))
            print("                        exists but contains no data.")
        print("  weewx (weewx.conf) is set to use an archive interval of %s seconds." % \
            config_dict['StdArchive']['archive_interval'])
        print("  The station hardware was not interrogated in determining archive interval.")
        print()

    # weewx database info
    if verbosity > 0:
        print("Databases configured in weewx.conf")
        for db_keys in config_dict['Databases']:
            database_dict = weewx.manager.get_database_dict_from_config(config_dict,
                                                                        db_keys)
            _ = sorted(database_dict.keys())
            print("  Database name:        %s" % database_dict['database_name'])
            print("  Database driver:      %s" % database_dict['driver'])
            if 'host' in database_dict:
                print("  Database host:        %s" % database_dict['host'])
            print()

    # sqlkeys/obskeys info
    if verbosity > 1:
        print("Supported SQL keys")
        formatListCols(manager_info_dict['sqlkeys'], 3)
        print()
        print("Supported observation keys")
        formatListCols(manager_info_dict['obskeys'], 3)
        print()

def generateDebugConf(config_dict):
    """ Generate a parsed and obfuscated weewx.conf and write to sys.stdout """

    # obfuscate config_dict
    _obs_config_dict = obfuscateKey(config_dict,
                                    OBFUSCATE_MAP['obfuscate'],
                                    OBFUSCATE_MAP['do_not_obfuscate'])
    # put obfuscated config_dict into weewx.conf form
    if six.PY2:
        _obs_config_dict.write(sys.stdout)
    else:
        # The ConfigObj.write() method always writes in binary,
        # which is not accepted by Python 3 for output to stdout.
        # So write to sys.stdout.buffer
        _obs_config_dict.write(sys.stdout.buffer)


def generateSysInfo(verbosity):
    # % string operator deprecated Python 3.1 and up.
    print("System info")

    if verbosity > 0:
        print("  Platform:       " + platform.platform())
        print("  Python Version: " + platform.python_version())
        
        # environment 
        if verbosity > 1:
            print("\nEnvironment")
            for n in os.environ:
                print("  %s=%s" % (n, os.environ[n]))
        
        # load info
        try:
            loadavg =  '%.2f %.2f %.2f' % os.getloadavg()
            (load1, load5, load15) = loadavg.split(" ")
        
            print("\nLoad Information\n  1 minute load average:  " + load1)
            print("  5 minute load average:  " + load5)
            print("  15 minute load average: " + load15)
        except OSError:
            print("Sorry, the load average not available on this platform")

    print()

def obfuscateKey(src_dict, obfuscate_list, retain_list):
    """ Obfuscate any dictionary items whose key is contained in passed list.
        Uses recursion to obfuscate any nested keys.
    """

    # make a copy of our source dict as we may need to alter some entries
    _dict = copy.deepcopy(src_dict)
    # step through each key and value pair
    for k, v in src_dict.items():
        # if its a dict then recurse
        if isinstance(v, dict):
            _dict[k] = obfuscateKey(v, obfuscate_list, retain_list)
        # must be a value so obfuscate if in our list or leave it if its not
        else:
            if (any(k.startswith(key) for key in obfuscate_list)
                and not k in retain_list):
                _dict[k] = "XXX obfuscated by wee_debug XXX"
            else:
                _dict[k] = v
    return _dict

def get_binding(config_dict):
    """ Get db_binding for the weewx database """

    # Extract our binding from the StdArchive section of the config file. If
    # it's missing, return None.
    if 'StdArchive' in config_dict:
        db_binding_wx = config_dict['StdArchive'].get('data_binding', 'wx_binding')
    else:
        db_binding_wx = None

    return db_binding_wx

def getManagerInfo(config_dict, db_binding_wx):
    """ Get info from the manager of a weewx archive for inclusion in debug
        report
    """

    with weewx.manager.open_manager_with_config(config_dict, db_binding_wx) as dbmanager_wx:
        info = {
        'db_name'    : dbmanager_wx.database_name,
        'table_name' : dbmanager_wx.table_name,
        'version'    : getattr(dbmanager_wx, 'version', 'unknown'),
        'units'      : dbmanager_wx.std_unit_system,
        'first_ts'   : dbmanager_wx.first_timestamp,
        'last_ts'    : dbmanager_wx.last_timestamp,
        'sqlkeys'    : dbmanager_wx.sqlkeys,
        'obskeys'    : dbmanager_wx.obskeys
        }
        # do we have any records in our archive?
        if info['first_ts'] and info['last_ts']:
            # We have some records so proceed to count them.
            # Since we are (more than likely) using archive field 'dateTime' for
            # our record count we need to call the getAggregate() method from our
            # parent class. Note that if we change to some other field the 'count'
            # might take a while longer depending on the archive size.
            info['ts_count'] = super(DaySummaryManager, dbmanager_wx).getAggregate(TimeSpan(info['first_ts'], info['last_ts']),
                                                              COUNT_FIELD,
                                                              'count')
        else:
            info['ts_count'] = None
    return info

def _readproc_dict(filename):
    """ Read proc file that has 'name:value' format for each line """

    info = {}
    with open(filename) as fp:
        for line in fp:
            if line.find(':') >= 0:
                (n,v) = line.split(':',1)
                info[n.strip()] = v.strip()
    return info

def _readproc_line(filename):
    """ Read single line proc file, return the string """

    with open(filename) as fp:
        info = fp.read()
    return info

def formatListCols(the_list, cols):
    """ Format a list of strings into a given number of columns respecting the
        width of the largest list entry
    """

    max_width = max([len(x) for x in the_list])
    justifyList = [x.ljust(max_width) for x in the_list]
    lines = (' '.join(justifyList[i:i + cols])
             for i in range(0, len(justifyList), cols))
    print("\n".join(lines))

if __name__=="__main__" :
    main()
