#!/usr/bin/env python

"""This tool examines your Bcfg2 specifications for errors."""

import sys
import time
import inspect
import logging
import Bcfg2.Logger
import Bcfg2.Options
import Bcfg2.Server.Core
import Bcfg2.Server.Lint

LOGGER = logging.getLogger('bcfg2-lint')


def run_serverless_plugins(plugins, setup=None, errorhandler=None, files=None):
    """ Run serverless plugins """
    LOGGER.debug("Running serverless plugins")
    for plugin_name, plugin in list(plugins.items()):
        run_plugin(plugin, plugin_name, errorhandler=errorhandler,
                   setup=setup, files=files)


def run_server_plugins(plugins, setup=None, errorhandler=None, files=None):
    """ run plugins that require a running server to run """
    core = load_server(setup)
    try:
        LOGGER.debug("Running server plugins")
        for plugin_name, plugin in list(plugins.items()):
            run_plugin(plugin, plugin_name, args=[core],
                       errorhandler=errorhandler, setup=setup, files=files)
    finally:
        core.shutdown()


def run_plugin(plugin, plugin_name, setup=None, errorhandler=None,
               args=None, files=None):
    """ run a single plugin, server-ful or serverless. """
    LOGGER.debug("  Running %s" % plugin_name)
    if args is None:
        args = []

    if errorhandler is None:
        errorhandler = get_errorhandler(setup)

    if setup is not None and setup.cfp.has_section(plugin_name):
        arg = setup
        for key, val in setup.cfp.items(plugin_name):
            arg[key] = val
        args.append(arg)
    else:
        args.append(setup)

    # python 2.5 doesn't support mixing *magic and keyword arguments
    start = time.time()
    rv = plugin(*args, **dict(files=files, errorhandler=errorhandler)).Run()
    LOGGER.debug("  Ran %s in %0.2f seconds" % (plugin_name,
                                                time.time() - start))
    return rv


def get_errorhandler(setup):
    """ get a Bcfg2.Server.Lint.ErrorHandler object """
    if setup.cfp.has_section("errors"):
        errors = dict(setup.cfp.items("errors"))
    else:
        errors = None
    return Bcfg2.Server.Lint.ErrorHandler(errors=errors)


def load_server(setup):
    """ load server """
    core = Bcfg2.Server.Core.BaseCore(setup)
    core.load_plugins()
    core.block_for_fam_events(handle_events=True)
    return core


def load_plugin(module, obj_name=None):
    """ load a single plugin """
    parts = module.split(".")
    if obj_name is None:
        obj_name = parts[-1]

    mod = __import__(module)
    for part in parts[1:]:
        mod = getattr(mod, part)
    return getattr(mod, obj_name)


def load_plugins(setup):
    """ get list of plugins to run """
    if setup['args']:
        plugin_list = setup['args']
    elif "bcfg2-repo-validate" in sys.argv[0]:
        plugin_list = 'RequiredAttrs,Validate'.split(',')
    elif setup['lint_plugins']:
        plugin_list = setup['lint_plugins']
    else:
        plugin_list = Bcfg2.Server.Lint.plugins

    allplugins = dict()
    for plugin in plugin_list:
        try:
            allplugins[plugin] = load_plugin("Bcfg2.Server.Lint." + plugin)
        except ImportError:
            try:
                allplugins[plugin] = \
                    load_plugin("Bcfg2.Server.Plugins." + plugin,
                                obj_name=plugin + "Lint")
            except (ImportError, AttributeError):
                err = sys.exc_info()[1]
                LOGGER.error("Failed to load plugin %s: %s" %
                             (plugin + "Lint", err))
        except AttributeError:
            err = sys.exc_info()[1]
            LOGGER.error("Failed to load plugin %s: %s" % (plugin, err))

    for plugin in setup['plugins']:
        if plugin in allplugins:
            # already loaded
            continue

        try:
            allplugins[plugin] = \
                load_plugin("Bcfg2.Server.Plugins." + plugin,
                            obj_name=plugin + "Lint")
        except AttributeError:
            pass
        except ImportError:
            err = sys.exc_info()[1]
            LOGGER.error("Failed to load plugin %s: %s" % (plugin + "Lint",
                                                           err))

    serverplugins = dict()
    serverlessplugins = dict()
    for plugin_name, plugin in allplugins.items():
        if [c for c in inspect.getmro(plugin)
            if c == Bcfg2.Server.Lint.ServerPlugin]:
            serverplugins[plugin_name] = plugin
        else:
            serverlessplugins[plugin_name] = plugin
    return (serverlessplugins, serverplugins)


def main():
    optinfo = dict(lint_config=Bcfg2.Options.LINT_CONFIG,
                   showerrors=Bcfg2.Options.LINT_SHOW_ERRORS,
                   stdin=Bcfg2.Options.LINT_FILES_ON_STDIN,
                   schema=Bcfg2.Options.SCHEMA_PATH,
                   lint_plugins=Bcfg2.Options.LINT_PLUGINS)
    optinfo.update(Bcfg2.Options.CLI_COMMON_OPTIONS)
    optinfo.update(Bcfg2.Options.SERVER_COMMON_OPTIONS)
    setup = Bcfg2.Options.OptionParser(optinfo)
    setup.parse(sys.argv[1:])

    log_args = dict(to_syslog=setup['syslog'], to_console=logging.WARNING)
    if setup['verbose']:
        log_args['to_console'] = logging.DEBUG
    Bcfg2.Logger.setup_logging('bcfg2-info', **log_args)

    setup.cfp.read(setup['lint_config'])
    setup.reparse()

    if setup['stdin']:
        files = [s.strip() for s in sys.stdin.readlines()]
    else:
        files = None

    (serverlessplugins, serverplugins) = load_plugins(setup)

    errorhandler = get_errorhandler(setup)

    if setup['showerrors']:
        for plugin in serverplugins.values() + serverlessplugins.values():
            errorhandler.RegisterErrors(getattr(plugin, 'Errors')())

        print("%-35s %-35s" % ("Error name", "Handler"))
        for err, handler in errorhandler.errortypes.items():
            print("%-35s %-35s" % (err, handler.__name__))
        raise SystemExit(0)

    run_serverless_plugins(serverlessplugins, errorhandler=errorhandler,
                           setup=setup, files=files)

    if serverplugins:
        if errorhandler.errors:
            # it would be swell if we could try to start the server
            # even if there were errors with the serverless plugins,
            # but since XML parsing errors occur in the FAM thread
            # (not in the core server thread), there's no way we can
            # start the server and try to catch exceptions --
            # bcfg2-lint isn't in the same stack as the exceptions.
            # so we're forced to assume that a serverless plugin error
            # will prevent the server from starting
            print("Serverless plugins encountered errors, skipping server "
                  "plugins")
        else:
            run_server_plugins(serverplugins, errorhandler=errorhandler,
                               setup=setup, files=files)

    if errorhandler.errors or errorhandler.warnings or setup['verbose']:
        print("%d errors" % errorhandler.errors)
        print("%d warnings" % errorhandler.warnings)

    if errorhandler.errors:
        raise SystemExit(2)
    elif errorhandler.warnings:
        raise SystemExit(3)

if __name__ == '__main__':
    sys.exit(main())
