#!/usr/bin/env python

import os, sys, time

from glob import glob
from optparse import OptionParser
from exceptions import ImportError, ValueError

from signal import SIGHUP, SIG_IGN, SIGKILL, SIGTERM, signal

try:
    from cloud_wiki import normalize, Database, DatabaseError, Wiki
except ImportError, exc:
    sys.path += '..'

    from cloud_wiki import normalize, Database, DatabaseError, Wiki

def get_pid( db ):
    try:
        return int( db.getConfig( 'pid' ) )or None
    except:
        return None

def stop_server( db, signal=SIGTERM ):
    print "Stopping server.."
    pid = get_pid( db )

    if pid is not None:
        os.kill( pid, signal )
    else:
        print "There does not appear to be a server associated with this"
        print "database."

def kill_server( db, signal=SIGKILL ):
    pid = get_pid( db )

    if pid is not None:
        print "Killing server.."
        try: os.kill( pid, signal )
        except: pass
    else:
        print "There does not appear to be a server associated with this"
        print "database."

    db.delConfig( "pid" )

def start_server( wiki ):
    pid = get_pid( wiki.getDatabase() )

    if pid:
        print "There is already a cloud wiki server running on this database."
        print "Try cloud-wiki stop, first, to terminate the old server.  If"
        print "the server will not respond to the stop command, use cloud-wiki"
        print "kill."
    else:
        print "Starting server.."
        run_server( wiki )

    return 5

def restart_server( wiki ):
    stop_server( wiki.getDatabase() )
    time.sleep(2)
    start_server( wiki )

def get_list( db ):
    return db.getNodeKeys( )
    
def get_node( db, key ):
    key = normalize( key )
    node = db.fetchNode( key )
    content = node.getContent() 
    if content is not None:
        return node.getContent()
    else:
        print "  Node not found."
        return None
        
def put_node( db, key, content ):
    key = normalize( key )
    node = db.fetchNode( key )
    node.setContent( content ) 
    
def rm_node( db, key ):
    node = db.fetchNode( key )
    
    if node.getContent() is not None:
        db.removeNode( key )
    else:
        print "  Node not found."

def halt_server( db, server ):
    db.delConfig( "pid" )
    sys.exit(0)

def run_server( wiki ):
    pid = os.fork()
    if pid:
        wiki.getDatabase().setConfig( "pid", pid )
        os._exit( 0 )
    else:
        os.setsid()

        signal( SIGHUP, SIG_IGN )
        signal( SIGTERM, lambda sig, frame: halt_server( db, wiki.getServer() ) )
        wiki.runForever()

def get_config( db, key ):
    return db.getConfig( key )
    
def set_config( db, key, value ):
    db.setConfig( key, value )

def rm_config( db, key ):
    db.delConfig( key )

def ls_config( db ):
    for key in db.getConfigKeys():
        yield key
    
def set_passwd( db, key, value ):
    db.setPassword( key, value )

def rm_passwd( db, key ):
    db.delPassword( key )

def ls_users( db ):
    for username in db.getUsernames():
        yield username

def migrate( wiki ):
    db = wiki.getDatabase()
    
    dbMajor, dbMinor = db.getVersion()
    svMajor, svMinor = wiki.getVersion()
    
    #TODO: Handle major database changes.
    if dbMajor != svMajor: #TODO: This will be dbMajor > svMajor, soon.
        print "There has been a major change in database structure that this"
        print "server cannot migrate automatically. Please refer to the"
        print "documentation for information on how to migrate your wiki"
        print "automatically."
    
        return
            
    if dbMinor != svMinor:
        # Minor version changes indicate a possible change in the markup
        # language. We will regenerate all content in response.
        
        print "Clearing all cached content html.."
       
        db.transact( "DELETE FROM CloudHtml;" )

        print "Changing database version."
        
        db.setVersion( svMajor, svMinor )
        
        print "Migration complete."
    else:
        print "No migration is required at this time."

def check_version( wiki ):
    return wiki.getVersion() == wiki.getDatabase().getVersion()
            
topical_help = {
    'summary':(
        'Usage: cloud-wiki <option>* <command> <argument>*',
        '',
        'To view a list of global cloud-wiki options, see:',
        '   cloud-wiki help options.',
        'To view a list of cloud-wiki commands, see:',
        '   cloud-wiki help commands.',
        'For more information, visit http://kabuki.merseine.nu:1080/cloud for',
        'up to date information.'
    ),
    'commands':(
        'Commands understood by cloud-wiki:',
        '',
        'backup     Backs up all nodes in the database to the specified path.',
        'ls         Lists the titles of all the nodes in the database.',
        'get        Retrieves listed nodes from the database.',
        'restore    Restores a list of previously backed up nodes.',
        'rm         Removes a node from the wiki database.',
        'start      Starts the wiki server in the background.',
        'stop       Stops the wiki server.',
        'restart    Restarts the wiki server in the background.',
        'kill       Forces a nonresponsive wiki server to terminate.',
        'config     Manipulates wiki configuration settings.',
        'passwd     Manipulates passwords in the wiki user table.',
        'help       Provides documentation for the cloud-wiki utility.',
        '',
        'For more information about a given command, see:',
        '    cloud-wiki help <command>',
        'For information about other cloud-wiki topics, see:',
        '    cloud-wiki help'
    ),
    'options':(
        "Global options understood by cloud-wiki:",
        "",
        "-v, --verbose           Causes all logging output to be diverted to",
        "                        the console.",
        "-d, --database <path>   Specifies a path to the wiki database.",
        "-i, --initialize        If database file does not exist, create one.",
        "-l, --logfile <path>    Specified the path to the logfile.",
        "-p, --port <port>       Overrides the wiki's default port.  Usually",
        "                        used with the start and restart commands.",
        "",
        'For information about other cloud-wiki topics, see:',
        '    cloud-wiki help'
    ),
    'backup':(
        'Usage: cloud-wiki <option>* backup <path>',
        '',
        'Backs up all nodes in the database in the specified directory.  Node',
        'files will be given a name equivalent to their title.  This function',
        'is intended for periodic full backups of the wiki as insurance',
        'catastrophic failure, or hostile editor problems.'
    ),

    'ls':(
        'Usage: cloud-wiki <option>* ls',
        '',
        'Lists the titles of nodes in the wiki database, one per line.'
    ),

    'get':(
        'Usage: cloud-wiki <option>* get <path>+',
        '',
        'Gets each node specified by the last path component of path from ',
        'the database, and stores its contents at the specified path.',
    ),

    'restore':(
        'Usage: cloud-wiki <option>* restore <path>+',
        '',
        'Loads each node specified by the last path component of path from ',
        'from the filesystem and updates the database with it.  Useful for ',
        'restoring backed up nodes.'
    ),

    'rm':(
        'Usage: cloud-wiki <option>* rm <title>+',
        '',
        'Deletes each specified node from the database, and all change ',
        'information corresponding with the node.'
    ),

    'start':(
        'Usage: cloud-wiki <option>* start',
        '',
        'Starts the wiki server, associating the process with the wiki ',
        'database, then returns to the command prompt.'
    ),

    'stop':(
        'Usage: cloud-wiki <option>* stop',
        '',
        'Stops the wiki server associated with the database, or clears a ',
        'stale PID from the database.'
    ),

    'restart':(
        'Usage: cloud-wiki <option>* restart',
        '',
        'Equivalent to invoking cloud-wiki stop, then cloud-wiki start'
    ),

    'kill':(
        'Usage: cloud-wiki <option>* kill',
        '',
        'Similar to stop, but instead of politely instructing the process to',
        'terminate, sends a SIGKILL which will halt the process immediately.',
        '',
        'Only use this as a last resort -- cloud-wiki stop should be',
        'sufficient for most situations.'
    ),

    'config':(
        'Usage: cloud-wiki <option>* config <key>(:(<value>?)?)*',
        '',
        'Manipulates configuration settings stored in the database.  Cloud ',
        'wiki uses a key:value system to handle site configuration, and ',
        'stores the information in a table in the database.  Documentation ',
        'about configuration settings should be available at: ',
        '    http://kabuki.merseine.nu:1080/cloud',
        '',
        'Examples:',
        '',
        'cloud-wiki config',
        '    Returns a list of all non-default configuration settings in',
        '    the database.',
        'cloud-wiki config site-title',
        '    Returns the current configuration setting of "site-title"',
        'cloud-wiki config site-title:\'Cloud Wiki\'',
        '    Configures the title of the wiki site to "Cloud Wiki"'
    ),

    'passwd':(
        'Usage: cloud-wiki <option>* passwd <user-name>(:(<password>?)?)*',
        '',
        'Manipulates password entries in the database.  This is only valid',
        'on servers that use authentication modules that use the wiki\'s',
        'user database, like "md5" and "cloud" authenicators."',
        '',
        'For entering MD5 passwords, ensure that the supply password is',
        'actually an MD5 hash, not the plaintext password.',
        '',
        'Examples:',
        '',
        'cloud-wiki passwd',
        '    Returns a list of all non-default configuration settings in',
        '    the database.',
        'cloud-wiki passwd \'Joe User\'',
        '    Exits nonzero if Joe User is not in the database."',
        'cloud-wiki passwd \'Joe User\':obvious-password',
        '    Assigns "obvious-password" as Joe User\'s password.'
    ),
    
    'migrate':(
        'Usage: cloud-wiki <option>* migrate',
        '',
        'Updates a Cloud Wiki database to match the current version of the',
        'server.  Normally invoked after an upgrade to regenerate node html.',
    )
}

topic_aliases = {
    'command':'commands',
    'option':'options'
}

def display_help( topic=None ):
    if topic is None: topic = 'summary'
   
    try:
        print '\n'.join( topical_help[ topic_aliases.get( topic, topic ) ] )
    except:
        print 'Information on that topic is not available.  Try cloud-wiki help'

if __name__ == '__main__':
    parser = OptionParser()
    parser.add_option( 
        "-d", "--database", dest="database",
        help="wiki database file path",
        default="wiki.db",
        metavar="FILE"
    )
    parser.add_option(
        "-i", "--initialize", dest="initialize",
        action='store_true',
        help="if database file does not exist, create one",
        default=False
    )
    parser.add_option(
        "-l", "--logfile", dest="logfile",
        help="use the specified logfile, instead of the default",
        metavar="FILE",
        default=None
    )
    parser.add_option(
        "-p", "--port", dest="port",
        type='int',
        help="use the specified port, instead of the default",
        metavar="PORT",
        default=None
    )
    parser.add_option(
        "-v", "--verbose", dest="verbose",
        action='store_true',
        help="output all SQL transactions to stdout.",
        default=False
    )
    options, args = parser.parse_args()
    
    if len(args) == 0:
        print "Try cloud-wiki help to view a list of commands."
        sys.exit( 3 )
    
    cmd = args[0].lower()
    
    if options.verbose:
        options.logfile = sys.stdout

    try:
        wiki = Wiki( 
            options.database, 
            options.logfile, 
            options.port, 
            options.initialize 
        ) 
    except DatabaseError:
        print "Could not access your wiki database. Aborting."
        sys.exit(4)

    if cmd == 'migrate':
        migrate( wiki )        
        sys.exit(0)
    elif not check_version( wiki ):
        print "Your database's version is out of sync with this server's version. You must use 'cloud-wiki migrate' to migrate the database."
        sys.exit(5)
        
    db = wiki.getDatabase()

    if cmd == 'backup':
        dest = args[1]
        
        for key in get_list( db ):
            print "Getting %s.." % (key,)

            data = get_node( db, key )
            if data is not None: open( dest + "/" + key, "w" ).write( data )
        print "Done."
    elif cmd ==  'ls':
        for key in get_list( db ):
            print key
    elif cmd == 'get':
        for fn in args[1:]:
            key = normalize( os.path.basename( fn ) )
            
            print "Getting %s to %s.." % ( key, fn )
            data = get_node( db, key )
            if data is not None:
                open( fn, "w" ).write( data )

    elif cmd ==  'restore':
        for pattern in args[1:]:
            for fn in glob( pattern ):
                key = normalize( os.path.basename( fn ) )

                print "Putting %s in %s.." % ( fn, key )
                put_node( db, key, ''.join( open( fn, "r" ).readlines() ) )

        print "Done."
    elif cmd ==  'rm':
        for key in args[1:]:
            print "Removing %s.." % (key,)
            rm_node( db, key )
        print "Done."
    elif cmd == 'start':
        start_server( wiki )
    elif cmd == 'stop':
        stop_server( db )
    elif cmd == 'restart':
        restart_server( wiki )
    elif cmd == 'kill':
        kill_server( db )
    elif cmd ==  'config':
        ops = args[1:]
        if ops:
            for op in args[1:]:
                op = op.strip()
                try:
                    key, value = op.split( ':', 1 )
                except ValueError:
                    value = get_config(db, op)
                    if value is None:
                        print '%s is not set.' % (op,)
                    else:
                        print '%s:%s' % (op, value)
                    continue
                
                if value:
                    set_config( db, key, value )
                    print "%s:%s" % (key, value)
                else:
                    rm_config( db, key )
                    print "Removed %s." % (key,)
        else:
            for key in ls_config( db ):
                print "%s:%s" % (key, get_config( db, key ))
    elif cmd ==  'passwd':
        ops = args[1:]
        if ops:
            for op in args[1:]:
                op = op.strip()
                key, value = op.split( ':', 1 )
                
                if value:
                    set_passwd( db, key, value )
                    print key
                else:
                    rm_passwd( db, key )
                    print "Removed %s." % (key,)
        else:
            for username in ls_usernames( db ):
                print username
    elif cmd == 'help':
        if len(args) > 1:
            display_help( args[1].lower() )
        else:
            display_help( )
    else:
        print "Unrecognized command or option \"%s\"" % (cmd,)
        sys.exit( 3 )

    sys.exit( 0 )
