#!/usr/bin/env python
"""Query reporting system for client status."""

import os
import sys
import datetime
from optparse import OptionParser, OptionGroup, make_option
from Bcfg2.Compat import ConfigParser

try:
    import Bcfg2.settings
except ConfigParser.NoSectionError:
    print("Your bcfg2.conf is currently missing the [database] section which "
          "is necessary for the reporting interface. Please see bcfg2.conf(5) "
          "for more details.")
    sys.exit(1)

project_directory = os.path.dirname(Bcfg2.settings.__file__)
project_name = os.path.basename(project_directory)
sys.path.append(os.path.join(project_directory, '..'))
project_module = __import__(project_name, '', '', [''])
sys.path.pop()
# Set DJANGO_SETTINGS_MODULE appropriately.
os.environ['DJANGO_SETTINGS_MODULE'] = '%s.settings' % project_name

from Bcfg2.Reporting.models import (Client, BaseEntry)
from django import db

def hosts_by_entry_type(clients, etype, entryspec):
    result = []
    for entry in entryspec:
        for client in clients:
            items = getattr(client.current_interaction, etype)()
            for item in items:
                if (item.entry_type == entry[0] and
                    item.name == entry[1]):
                    result.append(client)
    return result

def print_fields(fields, client, fmt, extra=None):
    """
    Prints the fields specified in fields of client, max_name
    specifies the column width of the name column.
    """
    fdata = []
    if extra is None:
        extra = dict()
    for field in fields:
        if field == 'time':
            fdata.append(str(client.current_interaction.timestamp))
        elif field == 'state':
            if client.current_interaction.isclean():
                fdata.append("clean")
            else:
                fdata.append("dirty")
        elif field == 'total':
            fdata.append(client.current_interaction.total_count)
        elif field == 'good':
            fdata.append(client.current_interaction.good_count)
        elif field == 'modified':
            fdata.append(client.current_interaction.modified_count)
        elif field == 'extra':
            fdata.append(client.current_interaction.extra_count)
        elif field == 'bad':
            fdata.append(client.current_interaction.bad_count)
        else:
            try:
                fdata.append(getattr(client, field))
            except:
                fdata.append(extra.get(field, "N/A"))

    print(fmt % tuple(fdata))

def print_entries(interaction, etype):
    items = getattr(interaction, etype)()
    for item in items:
        print("%-70s %s" % (item.entry_type + ":" + item.name, etype))

def main():
    parser = OptionParser(usage="%prog [options] <mode> [arg]")

    # single host modes
    multimodes = []
    singlemodes = []
    multimodes.append(make_option("-b", "--bad", action="store_true",
                                  default=False,
                                  help="Show bad entries from HOST"))
    multimodes.append(make_option("-e", "--extra", action="store_true",
                                  default=False,
                                  help="Show extra entries from HOST"))
    multimodes.append(make_option("-m", "--modified", action="store_true",
                                  default=False,
                                  help="Show modified entries from HOST"))
    multimodes.append(make_option("-s", "--show", action="store_true",
                                  default=False,
                                  help="Equivalent to --bad --extra --modified"))
    singlemodes.append(make_option("-t", "--total", action="store_true",
                                   default=False,
                                   help="Show total number of managed and good "
                                   "entries from HOST"))
    singlemodes.append(make_option("-x", "--expire", action="store_true",
                                   default=False,
                                   help="Toggle expired/unexpired state of "
                                   "HOST"))
    hostmodes = \
        OptionGroup(parser, "Single-Host Modes",
                    "The following mode flags require a single HOST argument")
    hostmodes.add_options(multimodes)
    hostmodes.add_options(singlemodes)
    parser.add_option_group(hostmodes)

    # all host modes
    allhostmodes = OptionGroup(parser, "Host Selection Modes",
                               "The following mode flags require no arguments")
    allhostmodes.add_option("-a", "--all", action="store_true", default=False,
                            help="Show all hosts, including expired hosts")
    allhostmodes.add_option("-c", "--clean", action="store_true", default=False,
                            help="Show only clean hosts")
    allhostmodes.add_option("-d", "--dirty", action="store_true", default=False,
                            help="Show only dirty hosts")
    allhostmodes.add_option("--stale", action="store_true", default=False,
                            help="Show hosts that haven't run in the last 24 "
                            "hours")
    parser.add_option_group(allhostmodes)

    # entry modes
    entrymodes = \
        OptionGroup(parser, "Entry Modes",
                    "The following mode flags require either any number of "
                    "TYPE:NAME arguments describing entries, or the --file "
                    "option")
    entrymodes.add_option("--badentry", action="store_true", default=False,
                          help="Show hosts that have bad entries that match "
                          "the argument")
    entrymodes.add_option("--modifiedentry", action="store_true", default=False,
                          help="Show hosts that have modified entries that "
                          "match the argument")
    entrymodes.add_option("--extraentry", action="store_true", default=False,
                          help="Show hosts that have extra entries that match "
                          "the argument")
    entrymodes.add_option("--entrystatus", action="store_true", default=False,
                          help="Show the status of the named entry on all "
                          "hosts. Only supports a single entry.")
    parser.add_option_group(entrymodes)

    # entry options
    entryopts = OptionGroup(parser, "Entry Options",
                            "Options that can be used with entry modes")
    entryopts.add_option("--fields", metavar="FIELD,FIELD,...",
                         help="Only display the listed fields",
                         default='name,time,state')
    entryopts.add_option("--file", metavar="FILE",
                         help="Read TYPE:NAME pairs from the specified file "
                         "instead of the command line")
    parser.add_option_group(entryopts)

    options, args = parser.parse_args()

    # make sure we've specified exactly one mode
    mode_family = None
    mode = None
    for opt in allhostmodes.option_list + entrymodes.option_list + \
            singlemodes:
        if getattr(options, opt.dest):
            if mode is not None:
                parser.error("Only one mode can be specified; found %s and %s" %
                             (mode.get_opt_string(), opt.get_opt_string()))
            mode = opt
            mode_family = parser.get_option_group(opt.get_opt_string())

    # you can specify more than one of --bad, --extra, --modified, --show, so
    # consider single-host options separately
    if not mode_family:
        for opt in multimodes:
            if getattr(options, opt.dest):
                mode_family = parser.get_option_group(opt.get_opt_string())
                break

    if not mode_family:
        parser.error("You must specify a mode")

    if mode_family == hostmodes:
        try:
            cname = args.pop()
            client = Client.objects.select_related().get(name=cname)
        except IndexError:
            parser.error("%s require a single HOST argument" % hostmodes.title)
        except Client.DoesNotExist:
            print("No such host: %s" % cname)
            return 2

        if options.expire:
            if client.expiration == None:
                client.expiration = datetime.datetime.now()
                print("Host expired.")
            else:
                client.expiration = None
                print("Host un-expired.")
            client.save()
        elif options.total:
            managed = client.current_interaction.total_count
            good = client.current_interaction.good_count
            print("Total managed entries: %d (good: %d)" % (managed, good))
        elif mode_family == hostmodes:
            if options.bad or options.show:
                print_entries(client.current_interaction, "bad")

            if options.modified or options.show:
                print_entries(client.current_interaction, "modified")

            if options.extra or options.show:
                print_entries(client.current_interaction, "extra")
    else:
        clients = Client.objects.exclude(current_interaction__isnull=True)
        result = list()
        edata = dict()
        fields = options.fields.split(',')

        if mode_family == allhostmodes:
            if args:
                print("%s do not take any arguments, ignoring" %
                      allhostmodes.title)

            for client in clients:
                interaction = client.current_interaction
                if (options.all or
                    (options.stale and interaction.isstale()) or
                    (options.clean and interaction.isclean()) or
                    (options.dirty and not interaction.isclean())):
                    result.append(client)
        else:
            # entry query modes
            if options.file:
                try:
                    entries = [l.strip().split(":")
                               for l in open(options.file)]
                except IOError:
                    err = sys.exc_info()[1]
                    print("Cannot read entries from %s: %s" % (options.file,
                                                               err))
                    return 2
            elif args:
                entries = [a.split(":") for a in args]
            else:
                parser.error("%s require either a list of entries on the "
                             "command line or the --file options" %
                             mode_family.title)

            if options.badentry:
                result = hosts_by_entry_type(clients, "bad", entries)
            elif options.modifiedentry:
                result = hosts_by_entry_type(clients, "modified", entries)
            elif options.extraentry:
                result = hosts_by_entry_type(clients, "extra", entries)
            elif options.entrystatus:
                if 'state' in fields:
                    fields.remove('state')
                fields.append("entry state")

                try:
                    entry_cls = BaseEntry.entry_from_type(entries[0][0])
                except ValueError:
                    print("Unhandled/unkown type %s" % entries[0][0])
                    return 2

                # todo batch fetch this.  sqlite could break
                for client in clients:
                    ents = entry_cls.objects.filter(name=entries[0][1],
                            interaction=client.current_interaction)
                    if len(ents) == 0:
                        continue
                    edata[client] = {"entry state": ents[0].get_state_display(),
                                     "reason": ents[0]}
                    result.append(client)


        if 'name' not in fields:
            fields.insert(0, "name")
        if not result:
            print("No match found")
            return
        max_name = max(len(c.name) for c in result)
        ffmt = []
        for field in fields:
            if field == "name":
                ffmt.append("%%-%ds" % max_name)
            elif field == "time":
                ffmt.append("%-19s")
            else:
                ffmt.append("%%-%ds" % len(field))
        fmt = "  ".join(ffmt)
        print(fmt % tuple(f.title() for f in fields))
        for client in result:
            if not client.expiration:
                print_fields(fields, client, fmt,
                             extra=edata.get(client, None))
    db.close_connection()


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