#!/usr/bin/env python

import os
import sys
import getpass
import json
import gpgme
import assword
import subprocess
import pkg_resources

############################################################

PROG = os.path.basename(sys.argv[0])

def version():
    print PROG, pkg_resources.get_distribution('assword').version

def usage():
    print "Usage:", PROG, "<command> [<args>...]"
    print """
Commands:

  add [<context>]    Add a new entry.  If context is '-' read from stdin.
                     If not specified, user will be prompted for context.
                     See ASSWORD_PASSWORD for information on passwords.

  dump [<string>]    Dump search results as json.  If string not specified all
                     entries are returned.  Passwords will not be displayed
                     unless ASSWORD_DUMP_PASSWORDS is set.

  gui [<string>]     GUI interface, good for X11 window manager integration.
                     Upon invocation the user will be prompted to decrypt the
                     database, after which a graphical search prompt will be
                     presented.  If an additional string is provided, it will
                     be added as the initial search string.  All matching results
                     for the query will be presented to the user.  When a result
                     is selected, the password will be retrieved according to the
                     method specified by ASSWORD_XPASTE.  If no match is found,
                     the user has the opportunity to generate and store a new
                     password, which is then delivered via ASSWORD_XPASTE.

  remove <context>   Delete an entry from the database.

  version            Report the version of this program.

  help               This help.

Environment:

  ASSWORD_DB        Path to assword database file.  Default: ~/.assword/db

  ASSWORD_KEYFILE   File containing OpenPGP key ID of database encryption
                    recipient.  Default: ~/.assword/keyid

  ASSWORD_KEYID     OpenPGP key ID of database encryption recipient.  This
                    overrides ASSWORD_KEYFILE if set.

  ASSWORD_PASSWORD  For new entries, entropy of auto-generated password
                    in bytes (actual generated password will be longer
                    due to base64 encoding). If set to 'prompt' user
                    will be prompted for for password.  Default: %d

  ASSWORD_DUMP_PASSWORDS Include passwords in dump when set.

  ASSWORD_XPASTE    Method for password retrieval.  Options are: 'xdo', which
                    attempts to type the password into the window that had
                    focus on launch, or 'xclip' which inserts the password in
                    the X clipboard.  Default: xdo
"""%(assword.DEFAULT_NEW_PASSWORD_OCTETS)

############################################################

ASSWORD_DIR = os.path.join(os.path.expanduser('~'),'.assword')

DBPATH = os.getenv('ASSWORD_DB', os.path.join(ASSWORD_DIR, 'db'))

############################################################

def xclip(text):
    p = subprocess.Popen(' '.join(["xclip", "-i"]),
                         shell=True,
                         stdin=subprocess.PIPE)
    p.communicate(text)

############################################################
# Return codes:
# 1 command/load line error
# 10 db error
# 20 gpg/key error
############################################################

def open_db(keyid=None):
    try:
        db = assword.Database(DBPATH, keyid)
    except assword.DatabaseError as e:
        print >>sys.stderr, 'Assword database error: %s' % e.msg
        sys.exit(10)
    return db

def get_keyid():
    keyid = os.getenv('ASSWORD_KEYID')
    keyfile = os.getenv('ASSWORD_KEYFILE', os.path.join(ASSWORD_DIR, 'keyid'))

    if not keyid and os.path.exists(keyfile):
        with open(keyfile, 'r') as f:
            keyid = f.read().strip()

    save = False
    if not keyid:
        print >>sys.stderr, "OpenPGP key ID of encryption target not specified."
        print >>sys.stderr, "Please provide key ID in ASSWORD_KEYID environment variable,"
        print >>sys.stderr, "or specify key ID now to save in ~/.assword/keyid file."
        keyid = raw_input('OpenPGP key ID: ')
        if keyid == '':
            keyid = None
        else:
            save = True

    if not keyid:
        sys.exit(20)

    try:
        gpg = gpgme.Context()
        gpg.get_key(keyid)
    except gpgme.GpgmeError:
        print >>sys.stderr, "Invalid key ID:", keyid
        sys.exit(20)

    if save:
        if not os.path.isdir(os.path.dirname(keyfile)):
            os.mkdir(os.path.dirname(keyfile))
        with open(keyfile, 'w') as f:
            f.write(keyid)

    return keyid

def password_prompt():
    try:
        password0 = getpass.getpass('password: ')
        password1 = getpass.getpass('reenter password: ')
        if password0 != password1:
            print >>sys.stderr, "Error: Passwords do not match.  Aborting."
            sys.exit(1)
        return password0
    except KeyboardInterrupt:
        sys.exit(-1)

# Add a password to the database.
# First argument is potentially a context.
def add(args):
    keyid = get_keyid()
    try:
        # get context as argument
        context = args[0]
        # or from stdin
        if context == '-':
            context = sys.stdin.read()
    # prompt for context if not specified
    except IndexError:
        context = raw_input('context: ')
    except KeyboardInterrupt:
        sys.exit(-1)

    db = open_db(keyid)

    # get password from prompt if requested
    if os.getenv('ASSWORD_PASSWORD') == 'prompt':
        password = password_prompt()
    # otherwise auto generate
    else:
        print >>sys.stderr, "Auto-generating password..."
        password = None

    db.add(context.strip(), password)
    # NOTE: This is what actually saves the new database!
    db.save()
    print >>sys.stderr, "New entry writen."

def dump(args):
    query = ' '.join(args)
    if not os.path.exists(DBPATH):
        print >>sys.stderr, """Assword database does not exist.
To add an entry to the database use 'assword add'.
See 'assword help' for more information."""
        sys.exit(10)
    db = open_db()
    results = db.search(query)
    output = {}
    for context in results:
        output[context] = {}
        output[context]['date'] = results[context]['date']
        if os.getenv('ASSWORD_DUMP_PASSWORDS'):
            output[context]['password'] = results[context]['password']
    print json.dumps(output, sort_keys=True, indent=2)

# The X GUI
def gui(args, method='xdo'):
    query = ' '.join(args)
    if method == 'xdo':
        try:
            import xdo
        except:
            print >>sys.stderr, "The xdo module is not found, so the 'xdo' paste method is not available."
            print >>sys.stderr, "Please install python-xdo."
            sys.exit(1)
        # initialize xdo
        x = xdo.xdo()
        # get the id of the currently focused window
        win = x.get_focused_window()
    elif method == 'xclip':
        pass
    else:
        print >>sys.stderr, "Unknown X paste method:", method
        sys.exit(1)
    # do it
    keyid = get_keyid()
    db = open_db(keyid)
    result = assword.Gui(db, query=query).returnValue()
    # type the password in the saved window
    if result:
        if method == 'xdo':
            x.focus_window(win)
            x.wait_for_window_focus(win)
            x.type(result['password'])
        elif method == 'xclip':
            xclip(result['password'])

def remove(args):
    keyid = get_keyid()
    try:
        context = args[0]
    except IndexError:
        print >>sys.stderr, "Must specify index to remove."
        sys.exit(1)
    db = open_db(keyid)
    if context not in db.entries:
        print >>sys.stderr, "No entry with context '%s'." % (context)
        sys.exit(1)
    try:
        print >>sys.stderr, "Really remove entry '%s'?" % (context)
        response = raw_input("Type 'yes' to remove: ")
    except KeyboardInterrupt:
        sys.exit(-1)
    if response != 'yes':
        sys.exit(-1)
    db.remove(context)
    db.save()
    print >>sys.stderr, "Entry removed."

############################################################
# Basically main()

if len(sys.argv) < 2:
    print >>sys.stderr, "Command not specified."
    usage()
    sys.exit(1)

cmd = sys.argv[1]

if cmd == 'add':
    add(sys.argv[2:])
elif cmd == 'dump':
    dump(sys.argv[2:])
elif cmd == 'gui':
    method = os.getenv('ASSWORD_XPASTE', 'xdo')
    gui(sys.argv[2:], method=method)
elif cmd == 'remove':
    remove(sys.argv[2:])
elif cmd == 'version' or cmd == '--version':
    version()
elif cmd == 'help' or cmd == '--help':
    usage()
else:
    print >>sys.stderr, "Unknown command:", cmd
    print >>sys.stderr
    usage()
    sys.exit(1)
