#!/usr/bin/env python
"""
Command-line tool to query a database filled by nl_loader
"""
__rcsid__ = "$Id: nl_query 802 2008-06-06 18:15:21Z dang $"
__author__ = "Dan Gunter (dkgunter (at) lbl.gov)"

from copy import copy
import calendar
import logging
from magicdate import MagicDateOption
from optparse import Option, OptionParser, OptionValueError
from optparse import IndentedHelpFormatter, make_option
import re
import sys
#
from netlogger import util
from netlogger.analysis import loader
from netlogger.analysis.query import grep, describe

log = None

def check_namevalue(option, opt, value):
    try:
        return value.split('=')        
    except ValueError:
        raise OptionValueError(
            "option %s: invalid name=value value: %r" % (opt, value))

def check_ofile(option, opt, value):
    try:
        if isinstance(value,str):
            f = file(value, 'w')
        else:
            f = value
        print "returning",f
        return f
    except IOError,E:
        raise OptionValueError(
            "option %s: cannot open %s for writing: %s" % (opt, value, E))

def namevalue_to_dict(nv, on_dup = None, allow_dup=True):
    d = { }
    for n,v in nv:
        if d.has_key(n):
            if not allow_dup:
                raise KeyError(n)
            if on_dup:
                on_dup(n, d[n], v)
            d[n] = v
        else:
            d[n] = v
    return d

class MyOption(MagicDateOption):
    TYPES = MagicDateOption.TYPES + ("namevalue","ofile")
    TYPE_CHECKER = copy(MagicDateOption.TYPE_CHECKER)
    TYPE_CHECKER["namevalue"] = check_namevalue
    TYPE_CHECKER["ofile"] = check_ofile

class MyHelpFormatter(IndentedHelpFormatter):
    """Exclude parts between {{ ... }} from formatting
    """
    def format_description(self, desc):
        if '{{' not in desc:
            return IndentedHelpFormatter(self, desc)
        i = desc.find('{{')
        j = desc.find('}}') + 2
        s = IndentedHelpFormatter.format_description(self, desc[:i]) + \
            desc[i+2:j-2] + \
            IndentedHelpFormatter.format_description(self, desc[j:])
        return s

def add_db_args(parser):
    c = parser.add_option_group("Database connection")
    c.add_option('-P', '--param', dest='db_param', default=[ ],
                 action='append', type='namevalue',
                 help="additional arguments in the form name=value")
    c.add_option('-D', '--database', type='choice',
                 action='store', dest='db_mod', choices=loader.AVAIL_DB,
                 default = loader.AVAIL_DB[0],
                 help="database engine. choices: %s "
                 "(default=%%default)" % str(loader.AVAIL_DB))
    c.add_option('-U', '--uri',
                 action='store', dest='db_uri', metavar='URI',
                 help="database connection URI")    

def add_output_args(parser):
    c = parser.add_option_group("Output")
    c.add_option('-o', '--output', default=sys.stdout,
                 action="store", dest="ofile", type="ofile",
                 help="output file for results (default=%default)")

class SubcommandOptionParser(OptionParser):
    def __init__(self, *args, **kw):
        OptionParser.__init__(self, *args, **kw)
        add_db_args(self)
        add_output_args(self)

def _msplit1(s, e):
    """Split s once on the first of the characters in the
    string e that is encountered
    """
    for i,c in enumerate(s):
        if  e.find(c) >= 0:
            return s[:i], s[i:]
    raise ValueError("cannot split '%s' on '%s'" % (s, e))

def matchSubcommand(cmd):
    """Return all SUBCOMMANDS with a prefix of 'cmd'.
    """
    matched = [ ]
    for subcommand in SUBCOMMANDS:
        if subcommand.startswith(cmd):
            matched.append(subcommand)
    return matched

def getSubcommandFunction(cmd):
    return eval("run_%s" % cmd)

def warn(s):
    sys.stderr.write("warning: %s\n" % s)

def setupDatabase(options):
    """Connect to the database.
    On success, returns (database-module, database-connection)
    Raises ValueError (i.e. bad params) on failure
    """
    def dupWarn(name, old, new):
        warn("duplicate database keyword '%s'," % name +
             "replacing old value %s with %s" % (old, new))
    conn_kw = namevalue_to_dict(options.db_param, on_dup=dupWarn)
    return loader.connectToDatabase(module=options.db_mod, 
                                    uri=options.db_uri, **conn_kw)

def run_describe(argv):
    usage = "%prog describe [options]"
    parser = SubcommandOptionParser(usage=usage, option_class=MyOption)
    options, args = parser.parse_args(argv)
    try:
        module, conn = setupDatabase(options)
    except ValueError, E:
        parser.error("connecting to database: %s" % E)
    try:
        log.debug(dict(event="describe.start"))
        describe.doQuery(module, conn, ofile=options.ofile)
    except module.Error, E:
        log.error("query failed: %s", E)
    log.debug(dict(event="describe.end"))
    
def run_grep(argv):
    usage = "%prog grep [options]"
    parser = SubcommandOptionParser(usage=usage, option_class=MyOption)
    parser.add_option('-a', '--attr', action="append", dest="attr",
                      default=[ ], type="string",
                      help="log event attribute to return, "
                      "may be repeated as many times as necessary. "
                      "If the first one starts with '!', then "
                      "the complement of the set is returned "
                      "(default=<ALL>)")
    parser.add_option('-d','--depth', action="store", type="int",
                      metavar='NUM',
                      dest="depth",
                      help="how many times to go back and query "
                      "for events related to those found so far. "
                           "'0' means none, > 0 means that many times, "
                      "and < 0 means forever. (default=%default)",
                      default=0)
    parser.add_option('-n', '--dn', action="store", dest="dn",
                      metavar='DN',
                      help="Distinguished name value, or pattern "
                      "using SQL '%' syntax (default=<any>)",
                      default="")
    parser.add_option('-e', '--event', action="store", dest="event",
                      metavar='EXPR',
                      help="wildcard expression, e.g., "
                      "\"org.globus.%%\" or just "
                      "\"org.globus.GridFTP.transfer.start\" "
                      "for a single event (default=%default)",
                      default="*")
    parser.add_option('-f', '--filter', action="append", dest="restrict",
                      default=[ ],
                      help="simple boolean "
                      "expression with a single attribute, "
                      "e.g. status < 0 or host = 'foo.org'. ")
    parser.add_option('-l', '--limit', action="store", type='int',
                      dest="maxresult", metavar="ROWS",
                      help="Limit size of result to ROWS rows. "
                      "Each row corresponds roughly to one attribute "
                      "in a log event. A value of 0 means no limit "
                      "(default=%default)",
                      default=100)
    parser.add_option('-s', '--start', action="store", dest="start",
                      metavar='DATE', type="magicdate",
                      help="start of search time. accepted formats: "+
                      util.MAGICDATE_EXAMPLES +
                      " (default=%default)", default="1 week ago")
    parser.add_option('-t', '--stop', action="store", dest="stop",
                      metavar='DATE', type="magicdate",
                      help="end of search time. accepted formats: "+
                      util.MAGICDATE_EXAMPLES +
                      " (default=%default)", default="now")
    options, args = parser.parse_args(argv)
    try:
        module, conn = setupDatabase(options)
    except ValueError, E:
        parser.error("connecting to database: %s" % E)
    query_param = { }
    if options.start is None:
        parser.error("cannot parse start time: "
                     "try 'now' or '<n> week(s) ago'")
    if options.stop is None:
        parser.error("cannot parse stop time "
                     "try 'now' or '<n> week(s) ago'")
    #print '@@',options.start, options.stop
    query_param['time'] = map(lambda t: '%lf' % util.parseDatetime(t),
                              (options.start, options.stop))
    if options.attr:
        is_exclude = False
        if options.attr[0][0] == '!':
            is_exclude = True
            options.attr[0] = options.attr[0][1:]
        query_param['attr'] = is_exclude, dict.fromkeys(options.attr)
    if options.restrict:
        ids_list, expr_list = [ ], [ ]
        for expr in options.restrict:
            name, rest = _msplit1(expr,"<>!=~ ") 
            rest = rest.replace('%','%%')
            if name in ('guid', 'id') or name.endswith('.id'):
                ids_list.append((name, rest))
            else:
                expr_list.append((name, rest))            
        if ids_list:
            query_param['ids'] = ids_list
        if expr_list:
            query_param['restrict'] = expr_list
    if options.maxresult:
        query_param['maxresult'] = int(options.maxresult)
    if options.event:
        query_param['event'] = options.event
    try:
        log.debug(dict(event="grep.start"))
        grep.doQuery(module, conn, ofile=options.ofile, **query_param)    
    except module.Error, E:
        log.error("query with parameters %s failed: %s", query_param, E)
    log.debug(dict(event="grep.end"))
        
def run_jobs(argv):
    log.error("sorry, 'jobs' is not implemented yet")

def run_gridftp(argv):
    log.error("sorry, 'gridftp' is not implemented yet")

SUBCOMMANDS = ['grep', 'jobs', 'gridftp', 'describe']
SUBCOMMANDS.sort()

def main():
    global log    
    usage = "%prog <subcommand> [options] [args]"
    description = """Type '%%prog help <subcommand>' for help 
on a specific subcommand. Most subcommands take additional options 
that further define the query. In addition, there are some 
global options describing how to connect to the database 
(which database module to use, the connection URI, etc.).{{
Available subcommands:
%s

}}%%prog is part of the NetLogger toolkit.
For additional information, see http://dsd.lbl.gov/NetLoggerWiki
""" % '\n'.join(['   %s' % x for x in SUBCOMMANDS])
    parser = util.ScriptOptionParser(usage=usage, description=description,
                                     version="1.0",
                                     option_class=MyOption,
                                     formatter=MyHelpFormatter())
    parser.disable_interspersed_args()
    options, args = parser.parse_args()
    if len(args) < 1:
        parser.error("<subcommand> is required.\n" 
                     "any distinct prefix (e.g. 'g' for grep) is OK.\n"
                     "use -h/--help to see all subcommands")
    log = util.getProgramLogger(options=options)
    if args[0] == 'help':
        if len(args) < 2:
            parser.error("'help' requires a subcommand, "
                         "use -h/--help to see all subcommands")
        if len(args) > 2:
            warn("additional arguments after subcommand ignored")
        matched = matchSubcommand(args[1])
        if not matched:
            parser.error("unknown subcommand '%s', " % args[1] + 
                         "use -h/--help to see all subcommands")
        fn = getSubcommandFunction(matched[0])
        fn(['-h'])
        return

    matched = matchSubcommand(args[0])
    if not matched:
        parser.error("unknown subcommand '%s', " % args[0] +
                     "use -h/--help to see all subcommands")
    elif len(matched) > 1:
        possible = ' '.join(matched)
        parser.error("ambiguous subcommand, matches: %s" % possible)
    subcommand = matched[0]
    log.info("start")
    fn = getSubcommandFunction(subcommand)
    fn(args[1:])
    log.info("end")

if __name__ == '__main__':
    main()
